Merge branch 'mealie-next' into mealie-next

This commit is contained in:
Kuchenpirat 2025-07-31 16:35:56 +02:00 committed by GitHub
commit 2b74119300
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
180 changed files with 14471 additions and 15145 deletions

View file

@ -17,7 +17,7 @@ jobs:
name: Build Package name: Build Package
uses: ./.github/workflows/build-package.yml uses: ./.github/workflows/build-package.yml
with: with:
tag: release tag: ${{ github.event.release.tag_name }}
publish: publish:
permissions: permissions:

View file

@ -12,7 +12,7 @@ repos:
exclude: ^tests/data/ exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.12.2 rev: v0.12.5
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format

View file

@ -179,9 +179,15 @@ def inject_nuxt_values():
all_langs = [] all_langs = []
for match in locales_dir.glob("*.json"): for match in locales_dir.glob("*.json"):
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}" }},' match_data = LOCALE_DATA.get(match.stem)
match_dir = match_data.dir if match_data else "ltr"
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}", dir: "{match_dir}" }},'
all_langs.append(lang_string) all_langs.append(lang_string)
all_langs.sort()
all_date_locales.sort()
log.debug(f"injecting locales into nuxt config -> {nuxt_config}") log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs) inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales) inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)

View file

@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do: We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case! 1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.8.0` 2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.0.2`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access. 3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container 4. Restart the container

View file

@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml ```yaml
services: services:
mealie: mealie:
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3) image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
container_name: mealie container_name: mealie
restart: always restart: always
ports: ports:

View file

@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml ```yaml
services: services:
mealie: mealie:
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3) image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
container_name: mealie container_name: mealie
restart: always restart: always
ports: ports:

View file

@ -44,33 +44,17 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { ReadCookBook } from "~/lib/api/types/cookbook";
import { Organizer } from "~/lib/api/types/non-generated"; import { Organizer } from "~/lib/api/types/non-generated";
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue"; import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
import type { FieldDefinition } from "~/composables/use-query-filter-builder"; import type { FieldDefinition } from "~/composables/use-query-filter-builder";
import type { ReadCookBook } from "~/lib/api/types/cookbook";
export default defineNuxtComponent({ const modelValue = defineModel<ReadCookBook>({ required: true });
components: { QueryFilterBuilder },
props: {
modelValue: {
type: Object as () => ReadCookBook,
required: true,
},
actions: {
type: Object as () => any,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const i18n = useI18n(); const i18n = useI18n();
const cookbook = toRef(modelValue);
const cookbook = toRef(() => props.modelValue);
function handleInput(value: string | undefined) { function handleInput(value: string | undefined) {
cookbook.value.queryFilterString = value || ""; cookbook.value.queryFilterString = value || "";
emit("update:modelValue", cookbook.value);
} }
const fieldDefs: FieldDefinition[] = [ const fieldDefs: FieldDefinition[] = [
@ -110,12 +94,4 @@ export default defineNuxtComponent({
type: "date", type: "date",
}, },
]; ];
return {
cookbook,
handleInput,
fieldDefs,
};
},
});
</script> </script>

View file

@ -17,7 +17,6 @@
<v-card-text> <v-card-text>
<CookbookEditor <CookbookEditor
v-model="editTarget" v-model="editTarget"
:actions="actions"
/> />
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
@ -65,7 +64,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useLazyRecipes } from "~/composables/recipes"; import { useLazyRecipes } from "~/composables/recipes";
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue"; import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
import { useCookbookStore } from "~/composables/store/use-cookbook-store"; import { useCookbookStore } from "~/composables/store/use-cookbook-store";
@ -74,9 +73,6 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
import type { RecipeCookBook } from "~/lib/api/types/cookbook"; import type { RecipeCookBook } from "~/lib/api/types/cookbook";
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue"; import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
export default defineNuxtComponent({
components: { RecipeCardSection, CookbookEditor },
setup() {
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
@ -89,7 +85,6 @@ export default defineNuxtComponent({
const { actions } = useCookbookStore(); const { actions } = useCookbookStore();
const router = useRouter(); const router = useRouter();
const tab = ref(null);
const book = getOne(slug); const book = getOne(slug);
const isOwnHousehold = computed(() => { const isOwnHousehold = computed(() => {
@ -132,23 +127,4 @@ export default defineNuxtComponent({
useSeoMeta({ useSeoMeta({
title: book?.value?.name || "Cookbook", title: book?.value?.name || "Cookbook",
}); });
return {
book,
slug,
tab,
appendRecipes,
assignSorted,
recipes,
removeRecipe,
replaceRecipes,
canEdit,
dialogStates,
editTarget,
handleEditCookbook,
editCookbook,
actions,
};
},
});
</script> </script>

View file

@ -20,18 +20,14 @@
</v-data-table> </v-data-table>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { parseISO, formatDistanceToNow } from "date-fns"; import { parseISO, formatDistanceToNow } from "date-fns";
import type { GroupDataExport } from "~/lib/api/types/group"; import type { GroupDataExport } from "~/lib/api/types/group";
export default defineNuxtComponent({ defineProps<{
props: { exports: GroupDataExport[];
exports: { }>();
type: Array as () => GroupDataExport[],
required: true,
},
},
setup() {
const i18n = useI18n(); const i18n = useI18n();
const headers = [ const headers = [
@ -53,12 +49,4 @@ export default defineNuxtComponent({
function downloadData(_: any) { function downloadData(_: any) {
console.log("Downloading data..."); console.log("Downloading data...");
} }
return {
downloadData,
headers,
getTimeToExpire,
};
},
});
</script> </script>

View file

@ -9,30 +9,10 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
export default defineNuxtComponent({ import type { ReadGroupPreferences } from "~/lib/api/types/user";
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const preferences = computed({
get() {
return props.modelValue;
},
set(val) {
context.emit("update:modelValue", val);
},
});
return { const preferences = defineModel<ReadGroupPreferences>({ required: true });
preferences,
};
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View file

@ -1,91 +0,0 @@
<template>
<v-select
v-model="selected"
:items="households"
:label="label"
:hint="description"
:persistent-hint="!!description"
item-title="name"
:multiple="multiselect"
:prepend-inner-icon="$globals.icons.household"
return-object
>
<template #chip="data">
<v-chip
:key="data.index"
class="ma-1"
:input-value="data.item"
size="small"
closable
label
color="accent"
dark
@click:close="removeByIndex(data.index)"
>
{{ data.item.raw.name || data.item }}
</v-chip>
</template>
</v-select>
</template>
<script lang="ts">
import { useHouseholdStore } from "~/composables/store/use-household-store";
interface HouseholdLike {
id: string;
name: string;
}
export default defineNuxtComponent({
props: {
modelValue: {
type: Array as () => HouseholdLike[],
required: true,
},
multiselect: {
type: Boolean,
default: false,
},
description: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
setup(props, context) {
const selected = computed({
get: () => props.modelValue,
set: (val) => {
context.emit("update:modelValue", val);
},
});
onMounted(() => {
if (selected.value === undefined) {
selected.value = [];
}
});
const i18n = useI18n();
const label = computed(
() => props.multiselect ? i18n.t("household.households") : i18n.t("household.household"),
);
const { store: households } = useHouseholdStore();
function removeByIndex(index: number) {
if (selected.value === undefined) {
return;
}
const newSelected = selected.value.filter((_, i) => i !== index);
selected.value = [...newSelected];
}
return {
selected,
label,
households,
removeByIndex,
};
},
});
</script>

View file

@ -18,7 +18,7 @@
:open-on-hover="mdAndUp" :open-on-hover="mdAndUp"
content-class="d-print-none" content-class="d-print-none"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-btn <v-btn
:class="{ 'rounded-circle': fab }" :class="{ 'rounded-circle': fab }"
:size="fab ? 'small' : undefined" :size="fab ? 'small' : undefined"
@ -26,7 +26,7 @@
:icon="!fab" :icon="!fab"
variant="text" variant="text"
dark dark
v-bind="props" v-bind="activatorProps"
@click.prevent @click.prevent
> >
<v-icon>{{ icon }}</v-icon> <v-icon>{{ icon }}</v-icon>
@ -50,7 +50,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue"; import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
import type { ShoppingListSummary } from "~/lib/api/types/household"; import type { ShoppingListSummary } from "~/lib/api/types/household";
@ -64,33 +64,25 @@ export interface ContextMenuItem {
isPublic: boolean; isPublic: boolean;
} }
export default defineNuxtComponent({ interface Props {
components: { recipes?: Recipe[];
RecipeDialogAddToShoppingList, menuTop?: boolean;
}, fab?: boolean;
props: { color?: string;
recipes: { menuIcon?: string | null;
type: Array as () => Recipe[], }
default: () => [], const props = withDefaults(defineProps<Props>(), {
}, recipes: () => [],
menuTop: { menuTop: true,
type: Boolean, fab: false,
default: true, color: "primary",
}, menuIcon: null,
fab: { });
type: Boolean,
default: false, const emit = defineEmits<{
}, [key: string]: [];
color: { }>();
type: String,
default: "primary",
},
menuIcon: {
type: String,
default: null,
},
},
setup(props, context) {
const { mdAndUp } = useDisplay(); const { mdAndUp } = useDisplay();
const i18n = useI18n(); const i18n = useI18n();
@ -111,6 +103,8 @@ export default defineNuxtComponent({
], ],
}); });
const { shoppingListDialog, menuItems } = toRefs(state);
const icon = props.menuIcon || $globals.icons.dotsVertical; const icon = props.menuIcon || $globals.icons.dotsVertical;
const shoppingLists = ref<ShoppingListSummary[]>(); const shoppingLists = ref<ShoppingListSummary[]>();
@ -147,18 +141,7 @@ export default defineNuxtComponent({
return; return;
} }
context.emit(eventKey); emit(eventKey);
state.loading = false; state.loading = false;
} }
return {
...toRefs(state),
contextMenuEventHandler,
icon,
recipesWithScales,
shoppingLists,
mdAndUp,
};
},
});
</script> </script>

View file

@ -5,12 +5,12 @@
style="gap: 10px" style="gap: 10px"
> >
<v-select <v-select
v-model="inputDay" v-model="day"
:items="MEAL_DAY_OPTIONS" :items="MEAL_DAY_OPTIONS"
:label="$t('meal-plan.rule-day')" :label="$t('meal-plan.rule-day')"
/> />
<v-select <v-select
v-model="inputEntryType" v-model="entryType"
:items="MEAL_TYPE_OPTIONS" :items="MEAL_TYPE_OPTIONS"
:label="$t('meal-plan.meal-type')" :label="$t('meal-plan.meal-type')"
/> />
@ -19,53 +19,38 @@
<div class="mb-5"> <div class="mb-5">
<QueryFilterBuilder <QueryFilterBuilder
:field-defs="fieldDefs" :field-defs="fieldDefs"
:initial-query-filter="queryFilter" :initial-query-filter="props.queryFilter"
@input="handleQueryFilterInput" @input="handleQueryFilterInput"
/> />
</div> </div>
<!-- TODO: proper pluralization of inputDay --> <!-- TODO: proper pluralization of inputDay -->
{{ $t('meal-plan.this-rule-will-apply', { {{ $t('meal-plan.this-rule-will-apply', {
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]), dayCriteria: day === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [day]),
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType]), mealTypeCriteria: entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [entryType]),
}) }} }) }}
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue"; import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
import type { FieldDefinition } from "~/composables/use-query-filter-builder"; import type { FieldDefinition } from "~/composables/use-query-filter-builder";
import { Organizer } from "~/lib/api/types/non-generated"; import { Organizer } from "~/lib/api/types/non-generated";
import type { QueryFilterJSON } from "~/lib/api/types/response"; import type { QueryFilterJSON } from "~/lib/api/types/response";
export default defineNuxtComponent({ interface Props {
components: { queryFilter?: QueryFilterJSON | null;
QueryFilterBuilder, showHelp?: boolean;
}, }
props: { const props = withDefaults(defineProps<Props>(), {
day: { queryFilter: null,
type: String, showHelp: false,
default: "unset", });
},
entryType: { const day = defineModel<string>("day", { default: "unset" });
type: String, const entryType = defineModel<string>("entryType", { default: "unset" });
default: "unset", const queryFilterString = defineModel<string>("queryFilterString", { default: "" });
},
queryFilterString: {
type: String,
default: "",
},
queryFilter: {
type: Object as () => QueryFilterJSON,
default: null,
},
showHelp: {
type: Boolean,
default: false,
},
},
emits: ["update:day", "update:entry-type", "update:query-filter-string"],
setup(props, context) {
const i18n = useI18n(); const i18n = useI18n();
const MEAL_TYPE_OPTIONS = [ const MEAL_TYPE_OPTIONS = [
@ -87,36 +72,10 @@ export default defineNuxtComponent({
{ title: i18n.t("meal-plan.day-any"), value: "unset" }, { title: i18n.t("meal-plan.day-any"), value: "unset" },
]; ];
const inputDay = computed({
get: () => {
return props.day;
},
set: (val) => {
context.emit("update:day", val);
},
});
const inputEntryType = computed({
get: () => {
return props.entryType;
},
set: (val) => {
context.emit("update:entry-type", val);
},
});
const inputQueryFilterString = computed({
get: () => {
return props.queryFilterString;
},
set: (val) => {
context.emit("update:query-filter-string", val);
},
});
function handleQueryFilterInput(value: string | undefined) { function handleQueryFilterInput(value: string | undefined) {
inputQueryFilterString.value = value || ""; console.warn("handleQueryFilterInput called with value:", value);
}; queryFilterString.value = value || "";
}
const fieldDefs: FieldDefinition[] = [ const fieldDefs: FieldDefinition[] = [
{ {
@ -160,16 +119,4 @@ export default defineNuxtComponent({
type: "date", type: "date",
}, },
]; ];
return {
MEAL_TYPE_OPTIONS,
MEAL_DAY_OPTIONS,
inputDay,
inputEntryType,
inputQueryFilterString,
handleQueryFilterInput,
fieldDefs,
};
},
});
</script> </script>

View file

@ -16,11 +16,11 @@
:label="$t('settings.webhooks.webhook-url')" :label="$t('settings.webhooks.webhook-url')"
variant="underlined" variant="underlined"
/> />
<v-time-picker <v-text-field
v-model="scheduledTime" v-model="scheduledTime"
class="elevation-2" type="time"
ampm-in-title clearable
format="ampm" variant="underlined"
/> />
</v-card-text> </v-card-text>
<v-card-actions class="py-0 justify-end"> <v-card-actions class="py-0 justify-end">
@ -50,19 +50,20 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { ReadWebhook } from "~/lib/api/types/household"; import type { ReadWebhook } from "~/lib/api/types/household";
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks"; import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
export default defineNuxtComponent({ const props = defineProps<{
props: { webhook: ReadWebhook;
webhook: { }>();
type: Object as () => ReadWebhook,
required: true, const emit = defineEmits<{
}, delete: [id: string];
}, save: [webhook: ReadWebhook];
emits: ["delete", "save", "test"], test: [id: string];
setup(props, { emit }) { }>();
const i18n = useI18n(); const i18n = useI18n();
const itemUTC = ref<string>(props.webhook.scheduledTime); const itemUTC = ref<string>(props.webhook.scheduledTime);
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime)); const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
@ -88,14 +89,4 @@ export default defineNuxtComponent({
useSeoMeta({ useSeoMeta({
title: i18n.t("settings.webhooks.webhooks"), title: i18n.t("settings.webhooks.webhooks"),
}); });
return {
webhookCopy,
scheduledTime,
handleSave,
itemUTC,
itemLocal,
};
},
});
</script> </script>

View file

@ -41,18 +41,10 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { ReadHouseholdPreferences } from "~/lib/api/types/household"; import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
export default defineNuxtComponent({ const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const i18n = useI18n(); const i18n = useI18n();
type Preference = { type Preference = {
@ -124,23 +116,6 @@ export default defineNuxtComponent({
value: 6, value: 6,
}, },
]; ];
const preferences = computed({
get() {
return props.modelValue;
},
set(val) {
context.emit("update:modelValue", val);
},
});
return {
allDays,
preferences,
recipePreferences,
};
},
});
</script> </script>
<style lang="css"> <style lang="css">

View file

@ -147,7 +147,7 @@
:model-value="field.value" :model-value="field.value"
type="number" type="number"
variant="underlined" variant="underlined"
@:model-value="setFieldValue(field, index, $event)" @update:model-value="setFieldValue(field, index, $event)"
/> />
<v-checkbox <v-checkbox
v-else-if="field.type === 'boolean'" v-else-if="field.type === 'boolean'"
@ -163,14 +163,14 @@
max-width="290px" max-width="290px"
min-width="auto" min-width="auto"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-text-field <v-text-field
v-model="field.value" v-model="field.value"
persistent-hint persistent-hint
:prepend-icon="$globals.icons.calendar" :prepend-icon="$globals.icons.calendar"
variant="underlined" variant="underlined"
color="primary" color="primary"
v-bind="props" v-bind="activatorProps"
readonly readonly
/> />
</template> </template>
@ -184,53 +184,53 @@
</v-menu> </v-menu>
<RecipeOrganizerSelector <RecipeOrganizerSelector
v-else-if="field.type === Organizer.Category" v-else-if="field.type === Organizer.Category"
:model-value="field.organizers" v-model="field.organizers"
:selector-type="Organizer.Category" :selector-type="Organizer.Category"
:show-add="false" :show-add="false"
:show-label="false" :show-label="false"
:show-icon="false" :show-icon="false"
variant="underlined" variant="underlined"
@update:model-value="setOrganizerValues(field, index, $event)" @update:model-value="setFieldOrganizers(field, index, $event)"
/> />
<RecipeOrganizerSelector <RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tag" v-else-if="field.type === Organizer.Tag"
:model-value="field.organizers" v-model="field.organizers"
:selector-type="Organizer.Tag" :selector-type="Organizer.Tag"
:show-add="false" :show-add="false"
:show-label="false" :show-label="false"
:show-icon="false" :show-icon="false"
variant="underlined" variant="underlined"
@update:model-value="setOrganizerValues(field, index, $event)" @update:model-value="setFieldOrganizers(field, index, $event)"
/> />
<RecipeOrganizerSelector <RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tool" v-else-if="field.type === Organizer.Tool"
:model-value="field.organizers" v-model="field.organizers"
:selector-type="Organizer.Tool" :selector-type="Organizer.Tool"
:show-add="false" :show-add="false"
:show-label="false" :show-label="false"
:show-icon="false" :show-icon="false"
variant="underlined" variant="underlined"
@update:model-value="setOrganizerValues(field, index, $event)" @update:model-value="setFieldOrganizers(field, index, $event)"
/> />
<RecipeOrganizerSelector <RecipeOrganizerSelector
v-else-if="field.type === Organizer.Food" v-else-if="field.type === Organizer.Food"
:model-value="field.organizers" v-model="field.organizers"
:selector-type="Organizer.Food" :selector-type="Organizer.Food"
:show-add="false" :show-add="false"
:show-label="false" :show-label="false"
:show-icon="false" :show-icon="false"
variant="underlined" variant="underlined"
@update:model-value="setOrganizerValues(field, index, $event)" @update:model-value="setFieldOrganizers(field, index, $event)"
/> />
<RecipeOrganizerSelector <RecipeOrganizerSelector
v-else-if="field.type === Organizer.Household" v-else-if="field.type === Organizer.Household"
:model-value="field.organizers" v-model="field.organizers"
:selector-type="Organizer.Household" :selector-type="Organizer.Household"
:show-add="false" :show-add="false"
:show-label="false" :show-label="false"
:show-icon="false" :show-icon="false"
variant="underlined" variant="underlined"
@update:model-value="setOrganizerValues(field, index, $event)" @update:model-value="setFieldOrganizers(field, index, $event)"
/> />
</v-col> </v-col>
<!-- right parenthesis --> <!-- right parenthesis -->
@ -297,7 +297,7 @@
</v-card> </v-card>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { VueDraggable } from "vue-draggable-plus"; import { VueDraggable } from "vue-draggable-plus";
import { useDebounceFn } from "@vueuse/core"; import { useDebounceFn } from "@vueuse/core";
import { useHouseholdSelf } from "~/composables/use-households"; import { useHouseholdSelf } from "~/composables/use-households";
@ -307,12 +307,7 @@ import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalK
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store"; import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder"; import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
export default defineNuxtComponent({ const props = defineProps({
components: {
VueDraggable,
RecipeOrganizerSelector,
},
props: {
fieldDefs: { fieldDefs: {
type: Array as () => FieldDefinition[], type: Array as () => FieldDefinition[],
required: true, required: true,
@ -321,9 +316,13 @@ export default defineNuxtComponent({
type: Object as () => QueryFilterJSON | null, type: Object as () => QueryFilterJSON | null,
default: null, default: null,
}, },
}, });
emits: ["input", "inputJSON"],
setup(props, context) { const emit = defineEmits<{
(event: "input", value: string | undefined): void;
(event: "inputJSON", value: QueryFilterJSON | undefined): void;
}>();
const { household } = useHouseholdSelf(); const { household } = useHouseholdSelf();
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder(); const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
@ -337,6 +336,7 @@ export default defineNuxtComponent({
datePickers: [] as boolean[], datePickers: [] as boolean[],
drag: false, drag: false,
}); });
const { showAdvanced, datePickers, drag } = toRefs(state);
const storeMap = { const storeMap = {
[Organizer.Category]: useCategoryStore(), [Organizer.Category]: useCategoryStore(),
@ -369,7 +369,7 @@ export default defineNuxtComponent({
id: useUid(), id: useUid(),
}); });
state.datePickers.push(false); state.datePickers.push(false);
}; }
function setField(index: number, fieldLabel: string) { function setField(index: number, fieldLabel: string) {
state.datePickers[index] = false; state.datePickers[index] = false;
@ -419,15 +419,16 @@ export default defineNuxtComponent({
fields.value[index].values = values; fields.value[index].values = values;
} }
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) { function setFieldOrganizers(field: FieldWithId, index: number, organizers: OrganizerBase[]) {
setFieldValues(field, index, values.map(value => value.id.toString())); fields.value[index].organizers = organizers;
fields.value[index].organizers = values; // Sync the values array with the organizers array
fields.value[index].values = organizers.map(org => org.id?.toString() || "").filter(id => id);
} }
function removeField(index: number) { function removeField(index: number) {
fields.value.splice(index, 1); fields.value.splice(index, 1);
state.datePickers.splice(index, 1); state.datePickers.splice(index, 1);
}; }
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => { const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
/* newFields.forEach((field, index) => { /* newFields.forEach((field, index) => {
@ -441,19 +442,17 @@ export default defineNuxtComponent({
} }
state.qfValid = !!qf; state.qfValid = !!qf;
context.emit("input", qf || undefined); emit("input", qf || undefined);
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined); emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
}, 500); }, 500);
watch(fields, fieldsUpdater, { deep: true }); watch(fields, fieldsUpdater, { deep: true });
async function hydrateOrganizers(field: FieldWithId, index: number) { async function hydrateOrganizers(field: FieldWithId, _index: number) {
if (!field.values?.length || !isOrganizerType(field.type)) { if (!field.values?.length || !isOrganizerType(field.type)) {
return; return;
} }
field.organizers = [];
const { store, actions } = storeMap[field.type]; const { store, actions } = storeMap[field.type];
if (!store.value.length) { if (!store.value.length) {
await actions.refresh(); await actions.refresh();
@ -467,8 +466,9 @@ export default defineNuxtComponent({
} }
return organizer; return organizer;
}); });
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[]; field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
setOrganizerValues(field, index, field.organizers); return field;
} }
function initFieldsError(error = "") { function initFieldsError(error = "") {
@ -482,14 +482,15 @@ export default defineNuxtComponent({
} }
} }
function initializeFields() { async function initializeFields() {
if (!props.initialQueryFilter?.parts?.length) { if (!props.initialQueryFilter?.parts?.length) {
return initFieldsError(); return initFieldsError();
}; }
const initFields: FieldWithId[] = []; const initFields: FieldWithId[] = [];
let error = false; let error = false;
props.initialQueryFilter.parts.forEach((part: QueryFilterJSONPart, index: number) => {
for (const [index, part] of props.initialQueryFilter.parts.entries()) {
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.name === part.attributeName); const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.name === part.attributeName);
if (!fieldDef) { if (!fieldDef) {
error = true; error = true;
@ -522,7 +523,7 @@ export default defineNuxtComponent({
} }
if (isOrganizerType(field.type)) { if (isOrganizerType(field.type)) {
hydrateOrganizers(field, index); await hydrateOrganizers(field, index);
} }
} }
else if (field.type === "boolean") { else if (field.type === "boolean") {
@ -553,7 +554,7 @@ export default defineNuxtComponent({
} }
initFields.push(field); initFields.push(field);
}); }
if (initFields.length && !error) { if (initFields.length && !error) {
fields.value = initFields; fields.value = initFields;
@ -561,14 +562,16 @@ export default defineNuxtComponent({
else { else {
initFieldsError(); initFieldsError();
} }
}; }
onMounted(async () => {
try { try {
initializeFields(); await initializeFields();
} }
catch (error) { catch (error) {
initFieldsError(`Error initializing fields: ${(error || "").toString()}`); initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
} }
});
function buildQueryFilterJSON(): QueryFilterJSON { function buildQueryFilterJSON(): QueryFilterJSON {
const parts = fields.value.map((field) => { const parts = fields.value.map((field) => {
@ -643,30 +646,6 @@ export default defineNuxtComponent({
}, },
}; };
}); });
return {
Organizer,
...toRefs(state),
logOps,
relOps,
config,
firstDayOfWeek,
onDragEnd,
// Fields
fields,
addField,
setField,
setLeftParenthesisValue,
setRightParenthesisValue,
setLogicalOperatorValue,
setRelationalOperatorValue,
setFieldValue,
setFieldValues,
setOrganizerValues,
removeField,
};
},
});
</script> </script>
<style scoped> <style scoped>

View file

@ -17,8 +17,8 @@
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always /> <RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" /> <RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" />
<div v-if="loggedIn"> <div v-if="loggedIn">
<v-tooltip v-if="canEdit" bottom color="info"> <v-tooltip v-if="canEdit" location="bottom" color="info">
<template #activator="{ props }"> <template #activator="{ props: tooltipProps }">
<v-btn <v-btn
icon icon
variant="flat" variant="flat"
@ -26,7 +26,7 @@
size="small" size="small"
color="info" color="info"
class="ml-1" class="ml-1"
v-bind="props" v-bind="tooltipProps"
@click="$emit('edit', true)" @click="$emit('edit', true)"
> >
<v-icon size="x-large"> <v-icon size="x-large">
@ -86,7 +86,7 @@
</v-toolbar> </v-toolbar>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeContextMenu from "./RecipeContextMenu.vue"; import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue"; import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
@ -97,48 +97,29 @@ const DELETE_EVENT = "delete";
const CLOSE_EVENT = "close"; const CLOSE_EVENT = "close";
const JSON_EVENT = "json"; const JSON_EVENT = "json";
export default defineNuxtComponent({ interface Props {
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimelineBadge }, recipe: Recipe;
props: { slug: string;
recipe: { recipeScale?: number;
required: true, open: boolean;
type: Object as () => Recipe, name: string;
}, loggedIn?: boolean;
slug: { recipeId: string;
required: true, canEdit?: boolean;
type: String, }
}, withDefaults(defineProps<Props>(), {
recipeScale: { recipeScale: 1,
type: Number, loggedIn: false,
default: 1, canEdit: false,
}, });
open: {
required: true, const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
type: Boolean,
},
name: {
required: true,
type: String,
},
loggedIn: {
type: Boolean,
default: false,
},
recipeId: {
required: true,
type: String,
},
canEdit: {
type: Boolean,
default: false,
},
},
emits: ["print", "input", "delete", "close", "edit"],
setup(_, context) {
const deleteDialog = ref(false); const deleteDialog = ref(false);
const i18n = useI18n(); const i18n = useI18n();
const { $globals } = useNuxtApp(); const { $globals } = useNuxtApp();
const editorButtons = [ const editorButtons = [
{ {
text: i18n.t("general.delete"), text: i18n.t("general.delete"),
@ -169,31 +150,22 @@ export default defineNuxtComponent({
function emitHandler(event: string) { function emitHandler(event: string) {
switch (event) { switch (event) {
case CLOSE_EVENT: case CLOSE_EVENT:
context.emit(CLOSE_EVENT); emit("close");
context.emit("input", false); emit("input", false);
break; break;
case DELETE_EVENT: case DELETE_EVENT:
deleteDialog.value = true; deleteDialog.value = true;
break; break;
default: default:
context.emit(event); emit(event as any);
break; break;
} }
} }
function emitDelete() { function emitDelete() {
context.emit(DELETE_EVENT); emit("delete");
context.emit("input", false); emit("input", false);
} }
return {
deleteDialog,
editorButtons,
emitHandler,
emitDelete,
};
},
});
</script> </script>
<style scoped> <style scoped>

View file

@ -15,7 +15,7 @@
> >
<template #prepend> <template #prepend>
<div class="ma-auto"> <div class="ma-auto">
<v-tooltip bottom> <v-tooltip location="bottom">
<template #activator="{ props: tooltipProps }"> <template #activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps"> <v-icon v-bind="tooltipProps">
{{ getIconDefinition(item.icon).icon }} {{ getIconDefinition(item.icon).icon }}

View file

@ -1,13 +1,12 @@
<template> <template>
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition --> <!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
<v-lazy>
<div> <div>
<v-hover <v-hover
v-slot="{ isHovering, props }" v-slot="{ isHovering, props: hoverProps }"
:open-delay="50" :open-delay="50"
> >
<v-card <v-card
v-bind="props" v-bind="hoverProps"
:class="{ 'on-hover': isHovering }" :class="{ 'on-hover': isHovering }"
:style="{ cursor }" :style="{ cursor }"
:elevation="isHovering ? 12 : 2" :elevation="isHovering ? 12 : 2"
@ -99,10 +98,9 @@
</v-card> </v-card>
</v-hover> </v-hover>
</div> </div>
</v-lazy>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeChips from "./RecipeChips.vue"; import RecipeChips from "./RecipeChips.vue";
import RecipeContextMenu from "./RecipeContextMenu.vue"; import RecipeContextMenu from "./RecipeContextMenu.vue";
@ -110,50 +108,31 @@ import RecipeCardImage from "./RecipeCardImage.vue";
import RecipeRating from "./RecipeRating.vue"; import RecipeRating from "./RecipeRating.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
export default defineNuxtComponent({ interface Props {
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage }, name: string;
props: { slug: string;
name: { description?: string | null;
type: String, rating?: number;
required: true, ratingColor?: string;
}, image?: string;
slug: { tags?: Array<any>;
type: String, recipeId: string;
required: true, imageHeight?: number;
}, }
description: { const props = withDefaults(defineProps<Props>(), {
type: String, description: null,
default: null, rating: 0,
}, ratingColor: "secondary",
rating: { image: "abc123",
type: Number, tags: () => [],
required: false, imageHeight: 200,
default: 0, });
},
ratingColor: { defineEmits<{
type: String, click: [];
default: "secondary", delete: [slug: string];
}, }>();
image: {
type: String,
required: false,
default: "abc123",
},
tags: {
type: Array,
default: () => [],
},
recipeId: {
required: true,
type: String,
},
imageHeight: {
type: Number,
default: 200,
},
},
emits: ["click", "delete"],
setup(props) {
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
@ -164,15 +143,6 @@ export default defineNuxtComponent({
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : ""; return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
}); });
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto"); const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
return {
isOwnGroup,
recipeRoute,
showRecipeContent,
cursor,
};
},
});
</script> </script>
<style> <style>
@ -197,6 +167,7 @@ export default defineNuxtComponent({
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 8; -webkit-line-clamp: 8;
line-clamp: 8;
overflow: hidden; overflow: hidden;
} }
</style> </style>

View file

@ -28,47 +28,32 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useStaticRoutes, useUserApi } from "~/composables/api"; import { useStaticRoutes } from "~/composables/api";
export default defineNuxtComponent({ interface Props {
props: { tiny?: boolean | null;
tiny: { small?: boolean | null;
type: Boolean, large?: boolean | null;
default: null, iconSize?: number | string;
}, slug?: string | null;
small: { recipeId: string;
type: Boolean, imageVersion?: string | null;
default: null, height?: number | string;
}, }
large: { const props = withDefaults(defineProps<Props>(), {
type: Boolean, tiny: null,
default: null, small: null,
}, large: null,
iconSize: { iconSize: 100,
type: [Number, String], slug: null,
default: 100, imageVersion: null,
}, height: "100%",
slug: { });
type: String,
default: null, defineEmits<{
}, click: [];
recipeId: { }>();
type: String,
required: true,
},
imageVersion: {
type: String,
default: null,
},
height: {
type: [Number, String],
default: "100%",
},
},
emits: ["click"],
setup(props) {
const api = useUserApi();
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes(); const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
@ -97,15 +82,6 @@ export default defineNuxtComponent({
return recipeImage(recipeId, props.imageVersion); return recipeImage(recipeId, props.imageVersion);
} }
} }
return {
api,
fallBackImage,
imageSize,
getImage,
};
},
});
</script> </script>
<style scoped> <style scoped>

View file

@ -3,7 +3,10 @@
<v-expand-transition> <v-expand-transition>
<v-card <v-card
:ripple="false" :ripple="false"
:class="isFlat ? 'mx-auto flat' : 'mx-auto'" :class="[
isFlat ? 'mx-auto flat' : 'mx-auto',
{ 'disable-highlight': disableHighlight },
]"
:style="{ cursor }" :style="{ cursor }"
hover hover
height="100%" height="100%"
@ -123,7 +126,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeContextMenu from "./RecipeContextMenu.vue"; import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeCardImage from "./RecipeCardImage.vue"; import RecipeCardImage from "./RecipeCardImage.vue";
@ -131,59 +134,34 @@ import RecipeRating from "./RecipeRating.vue";
import RecipeChips from "./RecipeChips.vue"; import RecipeChips from "./RecipeChips.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
export default defineNuxtComponent({ interface Props {
components: { name: string;
RecipeFavoriteBadge, slug: string;
RecipeContextMenu, description: string;
RecipeRating, rating?: number;
RecipeCardImage, image?: string;
RecipeChips, tags?: Array<any>;
}, recipeId: string;
props: { vertical?: boolean;
name: { isFlat?: boolean;
type: String, height?: number;
required: true, disableHighlight?: boolean;
}, }
slug: { const props = withDefaults(defineProps<Props>(), {
type: String, rating: 0,
required: true, image: "abc123",
}, tags: () => [],
description: { vertical: false,
type: String, isFlat: false,
required: true, height: 150,
}, disableHighlight: false,
rating: { });
type: Number,
default: 0, defineEmits<{
}, selected: [];
image: { delete: [slug: string];
type: String, }>();
required: false,
default: "abc123",
},
tags: {
type: Array,
default: () => [],
},
recipeId: {
type: String,
required: true,
},
vertical: {
type: Boolean,
default: false,
},
isFlat: {
type: Boolean,
default: false,
},
height: {
type: [Number],
default: 150,
},
},
emits: ["selected", "delete"],
setup(props) {
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
@ -194,15 +172,6 @@ export default defineNuxtComponent({
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : ""; return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
}); });
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto"); const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
return {
isOwnGroup,
recipeRoute,
showRecipeContent,
cursor,
};
},
});
</script> </script>
<style scoped> <style scoped>
@ -241,4 +210,8 @@ export default defineNuxtComponent({
box-shadow: none !important; box-shadow: none !important;
background-color: transparent !important; background-color: transparent !important;
} }
.disable-highlight :deep(.v-card__overlay) {
opacity: 0 !important;
}
</style> </style>

View file

@ -36,11 +36,11 @@
offset-y offset-y
start start
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-btn <v-btn
variant="text" variant="text"
:icon="$vuetify.display.xs" :icon="$vuetify.display.xs"
v-bind="props" v-bind="activatorProps"
:loading="sortLoading" :loading="sortLoading"
> >
<v-icon :start="!$vuetify.display.xs"> <v-icon :start="!$vuetify.display.xs">
@ -162,7 +162,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useThrottleFn } from "@vueuse/core"; import { useThrottleFn } from "@vueuse/core";
import RecipeCard from "./RecipeCard.vue"; import RecipeCard from "./RecipeCard.vue";
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
@ -175,42 +175,30 @@ import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
const REPLACE_RECIPES_EVENT = "replaceRecipes"; const REPLACE_RECIPES_EVENT = "replaceRecipes";
const APPEND_RECIPES_EVENT = "appendRecipes"; const APPEND_RECIPES_EVENT = "appendRecipes";
export default defineNuxtComponent({ interface Props {
components: { disableToolbar?: boolean;
RecipeCard, disableSort?: boolean;
RecipeCardMobile, icon?: string | null;
}, title?: string | null;
props: { singleColumn?: boolean;
disableToolbar: { recipes?: Recipe[];
type: Boolean, query?: RecipeSearchQuery | null;
default: false, }
}, const props = withDefaults(defineProps<Props>(), {
disableSort: { disableToolbar: false,
type: Boolean, disableSort: false,
default: false, icon: null,
}, title: null,
icon: { singleColumn: false,
type: String, recipes: () => [],
default: null, query: null,
}, });
title: {
type: String, const emit = defineEmits<{
default: null, replaceRecipes: [recipes: Recipe[]];
}, appendRecipes: [recipes: Recipe[]];
singleColumn: { }>();
type: Boolean,
default: false,
},
recipes: {
type: Array as () => Recipe[],
default: () => [],
},
query: {
type: Object as () => RecipeSearchQuery,
default: null,
},
},
setup(props, context) {
const { $vuetify } = useNuxtApp(); const { $vuetify } = useNuxtApp();
const preferences = useUserSortPreferences(); const preferences = useUserSortPreferences();
@ -234,9 +222,7 @@ export default defineNuxtComponent({
return props.icon || $globals.icons.tags; return props.icon || $globals.icons.tags;
}); });
const state = reactive({ const sortLoading = ref(false);
sortLoading: false,
});
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || ""); const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
@ -251,7 +237,7 @@ export default defineNuxtComponent({
const router = useRouter(); const router = useRouter();
const queryFilter = computed(() => { const queryFilter = computed(() => {
return props.query.queryFilter || null; return props.query?.queryFilter || null;
// TODO: allow user to filter out null values when ordering by a value that may be null (such as lastMade) // TODO: allow user to filter out null values when ordering by a value that may be null (such as lastMade)
@ -290,7 +276,7 @@ export default defineNuxtComponent({
let lastQuery: string | undefined = JSON.stringify(props.query); let lastQuery: string | undefined = JSON.stringify(props.query);
watch( watch(
() => props.query, () => props.query,
async (newValue: RecipeSearchQuery | undefined) => { async (newValue: RecipeSearchQuery | undefined | null) => {
const newValueString = JSON.stringify(newValue); const newValueString = JSON.stringify(newValue);
if (lastQuery !== newValueString) { if (lastQuery !== newValueString) {
lastQuery = newValueString; lastQuery = newValueString;
@ -315,7 +301,7 @@ export default defineNuxtComponent({
// since we doubled the first call, we also need to advance the page // since we doubled the first call, we also need to advance the page
page.value = page.value + 1; page.value = page.value + 1;
context.emit(REPLACE_RECIPES_EVENT, newRecipes); emit(REPLACE_RECIPES_EVENT, newRecipes);
} }
const infiniteScroll = useThrottleFn(async () => { const infiniteScroll = useThrottleFn(async () => {
@ -331,14 +317,14 @@ export default defineNuxtComponent({
hasMore.value = false; hasMore.value = false;
} }
if (newRecipes.length) { if (newRecipes.length) {
context.emit(APPEND_RECIPES_EVENT, newRecipes); emit(APPEND_RECIPES_EVENT, newRecipes);
} }
loading.value = false; loading.value = false;
}, 500); }, 500);
async function sortRecipes(sortType: string) { async function sortRecipes(sortType: string) {
if (state.sortLoading || loading.value) { if (sortLoading.value || loading.value) {
return; return;
} }
@ -403,14 +389,14 @@ export default defineNuxtComponent({
page.value = 1; page.value = 1;
hasMore.value = true; hasMore.value = true;
state.sortLoading = true; sortLoading.value = true;
loading.value = true; loading.value = true;
// fetch new recipes // fetch new recipes
const newRecipes = await fetchRecipes(); const newRecipes = await fetchRecipes();
context.emit(REPLACE_RECIPES_EVENT, newRecipes); emit(REPLACE_RECIPES_EVENT, newRecipes);
state.sortLoading = false; sortLoading.value = false;
loading.value = false; loading.value = false;
} }
@ -426,22 +412,6 @@ export default defineNuxtComponent({
function toggleMobileCards() { function toggleMobileCards() {
preferences.value.useMobileCards = !preferences.value.useMobileCards; preferences.value.useMobileCards = !preferences.value.useMobileCards;
} }
return {
...toRefs(state),
displayTitleIcon,
EVENTS,
infiniteScroll,
ready,
loading,
navigateRandom,
preferences,
sortRecipes,
toggleMobileCards,
useMobileCards,
};
},
});
</script> </script>
<style> <style>

View file

@ -23,52 +23,31 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe"; import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
export type UrlPrefixParam = "tags" | "categories" | "tools"; export type UrlPrefixParam = "tags" | "categories" | "tools";
export default defineNuxtComponent({ interface Props {
props: { truncate?: boolean;
truncate: { items?: RecipeCategory[] | RecipeTag[] | RecipeTool[];
type: Boolean, title?: boolean;
default: false, urlPrefix?: UrlPrefixParam;
}, limit?: number;
items: { small?: boolean;
type: Array as () => RecipeCategory[] | RecipeTag[] | RecipeTool[], maxWidth?: string | null;
default: () => [], }
}, const props = withDefaults(defineProps<Props>(), {
title: { truncate: false,
type: Boolean, items: () => [],
default: false, title: false,
}, urlPrefix: "categories",
urlPrefix: { limit: 999,
type: String as () => UrlPrefixParam, small: false,
default: "categories", maxWidth: null,
},
limit: {
type: Number,
default: 999,
},
small: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: null,
},
},
emits: ["item-selected"],
setup(props) {
const $auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
const baseRecipeRoute = computed<string>(() => {
return `/g/${groupSlug.value}`;
}); });
defineEmits(["item-selected"]);
function truncateText(text: string, length = 20, clamp = "...") { function truncateText(text: string, length = 20, clamp = "...") {
if (!props.truncate) return text; if (!props.truncate) return text;
const node = document.createElement("div"); const node = document.createElement("div");
@ -76,13 +55,6 @@ export default defineNuxtComponent({
const content = node.textContent || ""; const content = node.textContent || "";
return content.length > length ? content.slice(0, length) + clamp : content; return content.length > length ? content.slice(0, length) + clamp : content;
} }
return {
baseRecipeRoute,
truncateText,
};
},
});
</script> </script>
<style></style> <style></style>

View file

@ -12,7 +12,12 @@
@confirm="deleteRecipe()" @confirm="deleteRecipe()"
> >
<v-card-text> <v-card-text>
<template v-if="isAdminAndNotOwner">
{{ $t("recipe.admin-delete-confirmation") }}
</template>
<template v-else>
{{ $t("recipe.delete-confirmation") }} {{ $t("recipe.delete-confirmation") }}
</template>
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
<BaseDialog <BaseDialog
@ -50,12 +55,12 @@
max-width="290px" max-width="290px"
min-width="auto" min-width="auto"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-text-field <v-text-field
v-model="newMealdateString" v-model="newMealdateString"
:label="$t('general.date')" :label="$t('general.date')"
:prepend-icon="$globals.icons.calendar" :prepend-icon="$globals.icons.calendar"
v-bind="props" v-bind="activatorProps"
readonly readonly
/> />
</template> </template>
@ -95,7 +100,7 @@
:open-on-hover="$vuetify.display.mdAndUp" :open-on-hover="$vuetify.display.mdAndUp"
content-class="d-print-none" content-class="d-print-none"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-btn <v-btn
icon icon
:variant="fab ? 'flat' : undefined" :variant="fab ? 'flat' : undefined"
@ -103,7 +108,7 @@
:size="fab ? 'small' : undefined" :size="fab ? 'small' : undefined"
:color="fab ? 'info' : 'secondary'" :color="fab ? 'info' : 'secondary'"
:fab="fab" :fab="fab"
v-bind="props" v-bind="activatorProps"
@click.prevent @click.prevent
> >
<v-icon <v-icon
@ -125,32 +130,27 @@
</v-list-item> </v-list-item>
<div v-if="useItems.recipeActions && recipeActions && recipeActions.length"> <div v-if="useItems.recipeActions && recipeActions && recipeActions.length">
<v-divider /> <v-divider />
<v-list-group @click.stop>
<template #activator="{ props }">
<v-list-item-title v-bind="props">
{{ $t("recipe.recipe-actions") }}
</v-list-item-title>
</template>
<v-list density="compact" class="ma-0 pa-0">
<v-list-item <v-list-item
v-for="(action, index) in recipeActions" v-for="(action, index) in recipeActions"
:key="index" :key="index"
class="pl-6"
@click="executeRecipeAction(action)" @click="executeRecipeAction(action)"
> >
<template #prepend>
<v-icon color="undefined">
{{ $globals.icons.linkVariantPlus }}
</v-icon>
</template>
<v-list-item-title> <v-list-item-title>
{{ action.title }} {{ action.title }}
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list>
</v-list-group>
</div> </div>
</v-list> </v-list>
</v-menu> </v-menu>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue"; import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue"; import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
import RecipeDialogShare from "./RecipeDialogShare.vue"; import RecipeDialogShare from "./RecipeDialogShare.vue";
@ -186,16 +186,22 @@ export interface ContextMenuItem {
isPublic: boolean; isPublic: boolean;
} }
export default defineNuxtComponent({ interface Props {
components: { useItems?: ContextMenuIncludes;
RecipeDialogAddToShoppingList, appendItems?: ContextMenuItem[];
RecipeDialogPrintPreferences, leadingItems?: ContextMenuItem[];
RecipeDialogShare, menuTop?: boolean;
}, fab?: boolean;
props: { color?: string;
useItems: { slug: string;
type: Object as () => ContextMenuIncludes, menuIcon?: string | null;
default: () => ({ name: string;
recipe?: Recipe;
recipeId: string;
recipeScale?: number;
}
const props = withDefaults(defineProps<Props>(), {
useItems: () => ({
delete: true, delete: true,
edit: true, edit: true,
download: true, download: true,
@ -207,75 +213,42 @@ export default defineNuxtComponent({
share: true, share: true,
recipeActions: true, recipeActions: true,
}), }),
}, appendItems: () => [],
// Append items are added at the end of the useItems list leadingItems: () => [],
appendItems: { menuTop: true,
type: Array as () => ContextMenuItem[], fab: false,
default: () => [], color: "primary",
}, menuIcon: null,
// Append items are added at the beginning of the useItems list recipe: undefined,
leadingItems: { recipeScale: 1,
type: Array as () => ContextMenuItem[],
default: () => [],
},
menuTop: {
type: Boolean,
default: true,
},
fab: {
type: Boolean,
default: false,
},
color: {
type: String,
default: "primary",
},
slug: {
type: String,
required: true,
},
menuIcon: {
type: String,
default: null,
},
name: {
required: true,
type: String,
},
recipe: {
type: Object as () => Recipe,
default: undefined,
},
recipeId: {
required: true,
type: String,
},
recipeScale: {
type: Number,
default: 1,
},
},
emits: ["delete"],
setup(props, context) {
const api = useUserApi();
const state = reactive({
printPreferencesDialog: false,
shareDialog: false,
recipeDeleteDialog: false,
mealplannerDialog: false,
shoppingListDialog: false,
recipeDuplicateDialog: false,
recipeName: props.name,
loading: false,
menuItems: [] as ContextMenuItem[],
newMealdate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000),
newMealType: "dinner" as PlanEntryType,
pickerMenu: false,
}); });
const emit = defineEmits<{
[key: string]: any;
delete: [slug: string];
}>();
const api = useUserApi();
const printPreferencesDialog = ref(false);
const shareDialog = ref(false);
const recipeDeleteDialog = ref(false);
const mealplannerDialog = ref(false);
const shoppingListDialog = ref(false);
const recipeDuplicateDialog = ref(false);
const recipeName = ref(props.name);
const loading = ref(false);
const menuItems = ref<ContextMenuItem[]>([]);
const newMealdate = ref(new Date());
const newMealType = ref<PlanEntryType>("dinner");
const pickerMenu = ref(false);
const newMealdateString = computed(() => { const newMealdateString = computed(() => {
return state.newMealdate.toISOString().substring(0, 10); // Format the date to YYYY-MM-DD in the same timezone as newMealdate
const year = newMealdate.value.getFullYear();
const month = String(newMealdate.value.getMonth() + 1).padStart(2, "0");
const day = String(newMealdate.value.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}); });
const i18n = useI18n(); const i18n = useI18n();
@ -360,18 +333,8 @@ export default defineNuxtComponent({
}, },
}; };
// Get Default Menu Items Specified in Props
for (const [key, value] of Object.entries(props.useItems)) {
if (value) {
const item = defaultItems[key];
if (item && (item.isPublic || isOwnGroup.value)) {
state.menuItems.push(item);
}
}
}
// Add leading and Appending Items // Add leading and Appending Items
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems]; menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
const icon = props.menuIcon || $globals.icons.dotsVertical; const icon = props.menuIcon || $globals.icons.dotsVertical;
@ -383,6 +346,30 @@ export default defineNuxtComponent({
const recipeRefWithScale = computed(() => const recipeRefWithScale = computed(() =>
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined, recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
); );
const isAdminAndNotOwner = computed(() => {
return (
$auth.user.value?.admin
&& $auth.user.value?.id !== recipeRef.value?.userId
);
});
const canDelete = computed(() => {
const user = $auth.user.value;
const recipe = recipeRef.value;
return user && recipe && (user.admin || user.id === recipe.userId);
});
// Get Default Menu Items Specified in Props
for (const [key, value] of Object.entries(props.useItems)) {
if (!value) continue;
// Skip delete if not allowed
if (key === "delete" && !canDelete.value) continue;
const item = defaultItems[key];
if (item && (item.isPublic || isOwnGroup.value)) {
menuItems.value.push(item);
}
}
async function getShoppingLists() { async function getShoppingLists() {
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
@ -420,7 +407,7 @@ export default defineNuxtComponent({
if (data?.slug) { if (data?.slug) {
router.push(`/g/${groupSlug.value}`); router.push(`/g/${groupSlug.value}`);
} }
context.emit("delete", props.slug); emit("delete", props.slug);
} }
const download = useDownloader(); const download = useDownloader();
@ -436,7 +423,7 @@ export default defineNuxtComponent({
async function addRecipeToPlan() { async function addRecipeToPlan() {
const { response } = await api.mealplans.createOne({ const { response } = await api.mealplans.createOne({
date: newMealdateString.value, date: newMealdateString.value,
entryType: state.newMealType, entryType: newMealType.value,
title: "", title: "",
text: "", text: "",
recipeId: props.recipeId, recipeId: props.recipeId,
@ -451,7 +438,7 @@ export default defineNuxtComponent({
} }
async function duplicateRecipe() { async function duplicateRecipe() {
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName); const { data } = await api.recipes.duplicateOne(props.slug, recipeName.value);
if (data && data.slug) { if (data && data.slug) {
router.push(`/g/${groupSlug.value}/r/${data.slug}`); router.push(`/g/${groupSlug.value}/r/${data.slug}`);
} }
@ -461,21 +448,21 @@ export default defineNuxtComponent({
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const eventHandlers: { [key: string]: () => void | Promise<any> } = { const eventHandlers: { [key: string]: () => void | Promise<any> } = {
delete: () => { delete: () => {
state.recipeDeleteDialog = true; recipeDeleteDialog.value = true;
}, },
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"), edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
download: handleDownloadEvent, download: handleDownloadEvent,
duplicate: () => { duplicate: () => {
state.recipeDuplicateDialog = true; recipeDuplicateDialog.value = true;
}, },
mealplanner: () => { mealplanner: () => {
state.mealplannerDialog = true; mealplannerDialog.value = true;
}, },
printPreferences: async () => { printPreferences: async () => {
if (!recipeRef.value) { if (!recipeRef.value) {
await refreshRecipe(); await refreshRecipe();
} }
state.printPreferencesDialog = true; printPreferencesDialog.value = true;
}, },
shoppingList: () => { shoppingList: () => {
const promises: Promise<void>[] = [getShoppingLists()]; const promises: Promise<void>[] = [getShoppingLists()];
@ -484,11 +471,11 @@ export default defineNuxtComponent({
} }
Promise.allSettled(promises).then(() => { Promise.allSettled(promises).then(() => {
state.shoppingListDialog = true; shoppingListDialog.value = true;
}); });
}, },
share: () => { share: () => {
state.shareDialog = true; shareDialog.value = true;
}, },
}; };
@ -497,32 +484,14 @@ export default defineNuxtComponent({
if (handler && typeof handler === "function") { if (handler && typeof handler === "function") {
handler(); handler();
state.loading = false; loading.value = false;
return; return;
} }
context.emit(eventKey); emit(eventKey);
state.loading = false; loading.value = false;
} }
const planTypeOptions = usePlanTypeOptions(); const planTypeOptions = usePlanTypeOptions();
const recipeActions = groupRecipeActionsStore.recipeActions;
return {
...toRefs(state),
newMealdateString,
recipeRef,
recipeRefWithScale,
executeRecipeAction,
recipeActions: groupRecipeActionsStore.recipeActions,
shoppingLists,
duplicateRecipe,
contextMenuEventHandler,
deleteRecipe,
addRecipeToPlan,
icon,
planTypeOptions,
firstDayOfWeek,
};
},
});
</script> </script>

View file

@ -33,7 +33,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { whenever } from "@vueuse/core"; import { whenever } from "@vueuse/core";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe"; import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
@ -42,28 +42,19 @@ export interface GenericAlias {
name: string; name: string;
} }
export default defineNuxtComponent({ interface Props {
props: { data: IngredientFood | IngredientUnit;
modelValue: { }
type: Boolean,
default: false, const props = defineProps<Props>();
},
data: { const emit = defineEmits<{
type: Object as () => IngredientFood | IngredientUnit, submit: [aliases: GenericAlias[]];
required: true, cancel: [];
}, }>();
},
emits: ["submit", "update:modelValue", "cancel"],
setup(props, context) {
// V-Model Support // V-Model Support
const dialog = computed({ const dialog = defineModel<boolean>({ default: false });
get: () => {
return props.modelValue;
},
set: (val) => {
context.emit("update:modelValue", val);
},
});
function createAlias() { function createAlias() {
aliases.value.push({ aliases.value.push({
@ -85,7 +76,7 @@ export default defineNuxtComponent({
initAliases(); initAliases();
whenever( whenever(
() => props.modelValue, () => dialog.value,
() => { () => {
initAliases(); initAliases();
}, },
@ -111,17 +102,6 @@ export default defineNuxtComponent({
}); });
aliases.value = keepAliases; aliases.value = keepAliases;
context.emit("submit", keepAliases); emit("submit", keepAliases);
} }
return {
aliases,
createAlias,
dialog,
deleteAlias,
saveAliases,
validators,
};
},
});
</script> </script>

View file

@ -3,12 +3,13 @@
v-model="selected" v-model="selected"
item-key="id" item-key="id"
show-select show-select
:sort-by="[{ key: 'dateAdded', order: 'desc' }]" :sort-by="sortBy"
:headers="headers" :headers="headers"
:items="recipes" :items="recipes"
:items-per-page="15" :items-per-page="15"
class="elevation-0" class="elevation-0"
:loading="loading" :loading="loading"
return-object
> >
<template #[`item.name`]="{ item }"> <template #[`item.name`]="{ item }">
<a <a
@ -61,7 +62,7 @@
</v-data-table> </v-data-table>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import UserAvatar from "../User/UserAvatar.vue"; import UserAvatar from "../User/UserAvatar.vue";
import RecipeChip from "./RecipeChips.vue"; import RecipeChip from "./RecipeChips.vue";
import type { Recipe, RecipeCategory, RecipeTool } from "~/lib/api/types/recipe"; import type { Recipe, RecipeCategory, RecipeTool } from "~/lib/api/types/recipe";
@ -69,8 +70,6 @@ import { useUserApi } from "~/composables/api";
import type { UserSummary } from "~/lib/api/types/user"; import type { UserSummary } from "~/lib/api/types/user";
import type { RecipeTag } from "~/lib/api/types/household"; import type { RecipeTag } from "~/lib/api/types/household";
const INPUT_EVENT = "update:modelValue";
interface ShowHeaders { interface ShowHeaders {
id: boolean; id: boolean;
owner: boolean; owner: boolean;
@ -83,53 +82,43 @@ interface ShowHeaders {
dateAdded: boolean; dateAdded: boolean;
} }
export default defineNuxtComponent({ interface Props {
components: { RecipeChip, UserAvatar }, loading?: boolean;
props: { recipes?: Recipe[];
modelValue: { showHeaders?: ShowHeaders;
type: Array as PropType<Recipe[]>, }
required: false, const props = withDefaults(defineProps<Props>(), {
default: () => [], loading: false,
}, recipes: () => [],
loading: { showHeaders: () => ({
type: Boolean,
required: false,
default: false,
},
recipes: {
type: Array as () => Recipe[],
default: () => [],
},
showHeaders: {
type: Object as () => ShowHeaders,
required: false,
default: () => {
return {
id: true, id: true,
owner: false, owner: false,
tags: true, tags: true,
categories: true, categories: true,
tools: true,
recipeServings: true, recipeServings: true,
recipeYieldQuantity: true, recipeYieldQuantity: true,
recipeYield: true, recipeYield: true,
dateAdded: true, dateAdded: true,
}; }),
}, });
},
}, defineEmits<{
emits: ["click"], click: [];
setup(props, context) { }>();
const selected = defineModel<Recipe[]>({ default: () => [] });
const i18n = useI18n(); const i18n = useI18n();
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const groupSlug = $auth.user.value?.groupSlug; const groupSlug = $auth.user.value?.groupSlug;
const router = useRouter(); const router = useRouter();
const selected = computed({
get: () => props.modelValue, // Initialize sort state with default sorting by dateAdded descending
set: value => context.emit(INPUT_EVENT, value), const sortBy = ref([{ key: "dateAdded", order: "desc" as const }]);
});
const headers = computed(() => { const headers = computed(() => {
const hdrs: Array<{ title: string; value: string; align?: string; sortable?: boolean }> = []; const hdrs: Array<{ title: string; value: string; align?: "center" | "start" | "end"; sortable?: boolean }> = [];
if (props.showHeaders.id) { if (props.showHeaders.id) {
hdrs.push({ title: i18n.t("general.id"), value: "id" }); hdrs.push({ title: i18n.t("general.id"), value: "id" });
@ -203,16 +192,4 @@ export default defineNuxtComponent({
return i18n.t("general.none"); return i18n.t("general.none");
} }
return {
selected,
groupSlug,
headers,
formatDate,
members,
getMember,
filterItems,
};
},
});
</script> </script>

View file

@ -51,7 +51,7 @@
<BaseDialog <BaseDialog
v-if="shoppingListIngredientDialog" v-if="shoppingListIngredientDialog"
v-model="dialog" v-model="dialog"
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')" :title="selectedShoppingList?.name || $t('recipe.add-to-list')"
:icon="$globals.icons.cartCheck" :icon="$globals.icons.cartCheck"
width="70%" width="70%"
:submit-text="$t('recipe.add-to-list')" :submit-text="$t('recipe.add-to-list')"
@ -137,7 +137,7 @@
color="secondary" color="secondary"
density="compact" density="compact"
/> />
<div :key="ingredientData.ingredient.quantity"> <div :key="`${ingredientData.ingredient.quantity || 'no-qty'}-${i}`">
<RecipeIngredientListItem <RecipeIngredientListItem
:ingredient="ingredientData.ingredient" :ingredient="ingredientData.ingredient"
:disable-amount="ingredientData.disableAmount" :disable-amount="ingredientData.disableAmount"
@ -172,7 +172,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { toRefs } from "@vueuse/core"; import { toRefs } from "@vueuse/core";
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue"; import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
@ -203,49 +203,31 @@ export interface ShoppingListRecipeIngredientSection {
ingredientSections: ShoppingListIngredientSection[]; ingredientSections: ShoppingListIngredientSection[];
} }
export default defineNuxtComponent({ interface Props {
components: { recipes?: RecipeWithScale[];
RecipeIngredientListItem, shoppingLists?: ShoppingListSummary[];
}, }
props: { const props = withDefaults(defineProps<Props>(), {
modelValue: { recipes: undefined,
type: Boolean, shoppingLists: () => [],
default: false, });
},
recipes: { const dialog = defineModel<boolean>({ default: false });
type: Array as () => RecipeWithScale[],
default: undefined,
},
shoppingLists: {
type: Array as () => ShoppingListSummary[],
default: () => [],
},
},
emits: ["update:modelValue"],
setup(props, context) {
const i18n = useI18n(); const i18n = useI18n();
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const api = useUserApi(); const api = useUserApi();
const preferences = useShoppingListPreferences(); const preferences = useShoppingListPreferences();
const ready = ref(false); const ready = ref(false);
// v-model support
const dialog = computed({
get: () => {
return props.modelValue;
},
set: (val) => {
context.emit("update:modelValue", val);
initState();
},
});
const state = reactive({ const state = reactive({
shoppingListDialog: true, shoppingListDialog: true,
shoppingListIngredientDialog: false, shoppingListIngredientDialog: false,
shoppingListShowAllToggled: false, shoppingListShowAllToggled: false,
}); });
const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllToggled: _shoppingListShowAllToggled } = toRefs(state);
const userHousehold = computed(() => { const userHousehold = computed(() => {
return $auth.user.value?.householdSlug || ""; return $auth.user.value?.householdSlug || "";
}); });
@ -269,6 +251,12 @@ export default defineNuxtComponent({
}, },
); );
watch(dialog, (val) => {
if (!val) {
initState();
}
});
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) { async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>(); const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
for (const recipe of recipes) { for (const recipe of recipes) {
@ -277,7 +265,10 @@ export default defineNuxtComponent({
} }
if (recipeSectionMap.has(recipe.slug)) { if (recipeSectionMap.has(recipe.slug)) {
recipeSectionMap.get(recipe.slug).recipeScale += recipe.scale; const existingSection = recipeSectionMap.get(recipe.slug);
if (existingSection) {
existingSection.recipeScale += recipe.scale;
}
continue; continue;
} }
@ -421,22 +412,6 @@ export default defineNuxtComponent({
state.shoppingListIngredientDialog = false; state.shoppingListIngredientDialog = false;
dialog.value = false; dialog.value = false;
} }
return {
dialog,
preferences,
ready,
shoppingListChoices,
...toRefs(state),
addRecipesToList,
bulkCheckIngredients,
openShoppingListIngredientDialog,
setShowAllToggled,
recipeIngredientSections,
selectedShoppingList,
};
},
});
</script> </script>
<style scoped lang="css"> <style scoped lang="css">

View file

@ -4,9 +4,9 @@
v-model="dialog" v-model="dialog"
width="800" width="800"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<BaseButton <BaseButton
v-bind="props" v-bind="activatorProps"
@click="inputText = inputTextProp" @click="inputText = inputTextProp"
> >
{{ $t("new-recipe.bulk-add") }} {{ $t("new-recipe.bulk-add") }}
@ -89,28 +89,27 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
export default defineNuxtComponent({ interface Props {
props: { inputTextProp?: string;
inputTextProp: { }
type: String, const props = withDefaults(defineProps<Props>(), {
required: false, inputTextProp: "",
default: "",
},
},
emits: ["bulk-data"],
setup(props, context) {
const state = reactive({
dialog: false,
inputText: props.inputTextProp,
}); });
const emit = defineEmits<{
"bulk-data": [data: string[]];
}>();
const dialog = ref(false);
const inputText = ref(props.inputTextProp);
function splitText() { function splitText() {
return state.inputText.split("\n").filter(line => !(line === "\n" || !line)); return inputText.value.split("\n").filter(line => !(line === "\n" || !line));
} }
function removeFirstCharacter() { function removeFirstCharacter() {
state.inputText = splitText() inputText.value = splitText()
.map(line => line.substring(1)) .map(line => line.substring(1))
.join("\n"); .join("\n");
} }
@ -119,11 +118,11 @@ export default defineNuxtComponent({
function splitByNumberedLine() { function splitByNumberedLine() {
// Split inputText by numberedLineRegex // Split inputText by numberedLineRegex
const matches = state.inputText.match(numberedLineRegex); const matches = inputText.value.match(numberedLineRegex);
matches?.forEach((match, idx) => { matches?.forEach((match, idx) => {
const replaceText = idx === 0 ? "" : "\n"; const replaceText = idx === 0 ? "" : "\n";
state.inputText = state.inputText.replace(match, replaceText); inputText.value = inputText.value.replace(match, replaceText);
}); });
} }
@ -134,12 +133,12 @@ export default defineNuxtComponent({
splitLines[index] = element.trim(); splitLines[index] = element.trim();
}); });
state.inputText = splitLines.join("\n"); inputText.value = splitLines.join("\n");
} }
function save() { function save() {
context.emit("bulk-data", splitText()); emit("bulk-data", splitText());
state.dialog = false; dialog.value = false;
} }
const i18n = useI18n(); const i18n = useI18n();
@ -161,16 +160,4 @@ export default defineNuxtComponent({
action: splitByNumberedLine, action: splitByNumberedLine,
}, },
]; ];
return {
utilities,
splitText,
trimAllLines,
removeFirstCharacter,
splitByNumberedLine,
save,
...toRefs(state),
};
},
});
</script> </script>

View file

@ -44,6 +44,7 @@
<v-switch <v-switch
v-model="preferences.showDescription" v-model="preferences.showDescription"
hide-details hide-details
color="primary"
:label="$t('recipe.description')" :label="$t('recipe.description')"
/> />
</v-row> </v-row>
@ -51,6 +52,7 @@
<v-switch <v-switch
v-model="preferences.showNotes" v-model="preferences.showNotes"
hide-details hide-details
color="primary"
:label="$t('recipe.notes')" :label="$t('recipe.notes')"
/> />
</v-row> </v-row>
@ -63,6 +65,7 @@
<v-switch <v-switch
v-model="preferences.showNutrition" v-model="preferences.showNutrition"
hide-details hide-details
color="primary"
:label="$t('recipe.nutrition')" :label="$t('recipe.nutrition')"
/> />
</v-row> </v-row>
@ -83,45 +86,19 @@
</BaseDialog> </BaseDialog>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences"; import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue"; import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
import type { NoUndefinedField } from "~/lib/api/types/non-generated"; import type { NoUndefinedField } from "~/lib/api/types/non-generated";
export default defineNuxtComponent({ interface Props {
components: { recipe?: NoUndefinedField<Recipe>;
RecipePrintView, }
}, withDefaults(defineProps<Props>(), {
props: { recipe: undefined,
modelValue: { });
type: Boolean,
default: false, const dialog = defineModel<boolean>({ default: false });
},
recipe: {
type: Object as () => NoUndefinedField<Recipe>,
default: undefined,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const preferences = useUserPrintPreferences(); const preferences = useUserPrintPreferences();
// V-Model Support
const dialog = computed({
get: () => {
return props.modelValue;
},
set: (val) => {
context.emit("update:modelValue", val);
},
});
return {
dialog,
ImagePosition,
preferences,
};
},
});
</script> </script>

View file

@ -52,10 +52,6 @@
<div class="mr-auto"> <div class="mr-auto">
{{ $t("search.results") }} {{ $t("search.results") }}
</div> </div>
<!-- <router-link
:to="advancedSearchUrl"
class="text-primary"
> {{ $t("search.advanced-search") }} </router-link> -->
</v-card-actions> </v-card-actions>
<RecipeCardMobile <RecipeCardMobile
@ -76,7 +72,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import type { RecipeSummary } from "~/lib/api/types/recipe"; import type { RecipeSummary } from "~/lib/api/types/recipe";
@ -85,17 +81,15 @@ import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
import { usePublicExploreApi } from "~/composables/api/api-client"; import { usePublicExploreApi } from "~/composables/api/api-client";
const SELECTED_EVENT = "selected"; const SELECTED_EVENT = "selected";
export default defineNuxtComponent({
components: {
RecipeCardMobile,
},
setup(_, context) { // Define emits
const emit = defineEmits<{
selected: [recipe: RecipeSummary];
}>();
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const state = reactive({ const loading = ref(false);
loading: false, const selectedIndex = ref(-1);
selectedIndex: -1,
});
// =========================================================================== // ===========================================================================
// Dialog State Management // Dialog State Management
@ -105,7 +99,7 @@ export default defineNuxtComponent({
watch(dialog, (val) => { watch(dialog, (val) => {
if (!val) { if (!val) {
search.query.value = ""; search.query.value = "";
state.selectedIndex = -1; selectedIndex.value = -1;
search.data.value = []; search.data.value = [];
} }
}); });
@ -116,17 +110,17 @@ export default defineNuxtComponent({
function selectRecipe() { function selectRecipe() {
const recipeCards = document.getElementsByClassName("arrow-nav"); const recipeCards = document.getElementsByClassName("arrow-nav");
if (recipeCards) { if (recipeCards) {
if (state.selectedIndex < 0) { if (selectedIndex.value < 0) {
state.selectedIndex = -1; selectedIndex.value = -1;
document.getElementById("arrow-search")?.focus(); document.getElementById("arrow-search")?.focus();
return; return;
} }
if (state.selectedIndex >= recipeCards.length) { if (selectedIndex.value >= recipeCards.length) {
state.selectedIndex = recipeCards.length - 1; selectedIndex.value = recipeCards.length - 1;
} }
(recipeCards[state.selectedIndex] as HTMLElement).focus(); (recipeCards[selectedIndex.value] as HTMLElement).focus();
} }
} }
@ -137,11 +131,11 @@ export default defineNuxtComponent({
} }
else if (e.key === "ArrowUp") { else if (e.key === "ArrowUp") {
e.preventDefault(); e.preventDefault();
state.selectedIndex--; selectedIndex.value--;
} }
else if (e.key === "ArrowDown") { else if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
state.selectedIndex++; selectedIndex.value++;
} }
else { else {
return; return;
@ -158,9 +152,8 @@ export default defineNuxtComponent({
} }
}); });
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const route = useRoute(); const route = useRoute();
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`); const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
watch(route, close); watch(route, close);
function open() { function open() {
@ -177,22 +170,15 @@ export default defineNuxtComponent({
const search = useRecipeSearch(api); const search = useRecipeSearch(api);
// Select Handler // Select Handler
function handleSelect(recipe: RecipeSummary) { function handleSelect(recipe: RecipeSummary) {
close(); close();
context.emit(SELECTED_EVENT, recipe); emit(SELECTED_EVENT, recipe);
} }
return { // Expose functions to parent components
...toRefs(state), defineExpose({
advancedSearchUrl,
dialog,
open, open,
close, close,
handleSelect,
search,
};
},
}); });
</script> </script>

View file

@ -14,14 +14,14 @@
max-width="290px" max-width="290px"
min-width="auto" min-width="auto"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-text-field <v-text-field
v-model="expirationDateString" v-model="expirationDateString"
:label="$t('recipe-share.expiration-date')" :label="$t('recipe-share.expiration-date')"
:hint="$t('recipe-share.default-30-days')" :hint="$t('recipe-share.default-30-days')"
persistent-hint persistent-hint
:prepend-icon="$globals.icons.calendar" :prepend-icon="$globals.icons.calendar"
v-bind="props" v-bind="activatorProps"
readonly readonly
/> />
</template> </template>
@ -92,56 +92,35 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useClipboard, useShare, whenever } from "@vueuse/core"; import { useClipboard, useShare, whenever } from "@vueuse/core";
import type { RecipeShareToken } from "~/lib/api/types/recipe"; import type { RecipeShareToken } from "~/lib/api/types/recipe";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useHouseholdSelf } from "~/composables/use-households"; import { useHouseholdSelf } from "~/composables/use-households";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
export default defineNuxtComponent({ interface Props {
props: { recipeId: string;
modelValue: { name: string;
type: Boolean, }
default: false, const props = defineProps<Props>();
},
recipeId: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, context) {
// V-Model Support
const dialog = computed({
get: () => {
return props.modelValue;
},
set: (val) => {
context.emit("update:modelValue", val);
},
});
const state = reactive({ const dialog = defineModel<boolean>({ default: false });
datePickerMenu: false,
expirationDate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000), const datePickerMenu = ref(false);
tokens: [] as RecipeShareToken[], const expirationDate = ref(new Date(Date.now() - new Date().getTimezoneOffset() * 60000));
}); const tokens = ref<RecipeShareToken[]>([]);
const expirationDateString = computed(() => { const expirationDateString = computed(() => {
return state.expirationDate.toISOString().substring(0, 10); return expirationDate.value.toISOString().substring(0, 10);
}); });
whenever( whenever(
() => props.modelValue, () => dialog.value,
() => { () => {
// Set expiration date to today + 30 Days // Set expiration date to today + 30 Days
const today = new Date(); const today = new Date();
state.expirationDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); expirationDate.value = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
refreshTokens(); refreshTokens();
}, },
); );
@ -165,17 +144,17 @@ export default defineNuxtComponent({
// Convert expiration date to timestamp // Convert expiration date to timestamp
const { data } = await userApi.recipes.share.createOne({ const { data } = await userApi.recipes.share.createOne({
recipeId: props.recipeId, recipeId: props.recipeId,
expiresAt: state.expirationDate.toISOString(), expiresAt: expirationDate.value.toISOString(),
}); });
if (data) { if (data) {
state.tokens.push(data); tokens.value.push(data);
} }
} }
async function deleteToken(id: string) { async function deleteToken(id: string) {
await userApi.recipes.share.deleteOne(id); await userApi.recipes.share.deleteOne(id);
state.tokens = state.tokens.filter(token => token.id !== id); tokens.value = tokens.value.filter(token => token.id !== id);
} }
async function refreshTokens() { async function refreshTokens() {
@ -183,7 +162,7 @@ export default defineNuxtComponent({
if (data) { if (data) {
// @ts-expect-error - TODO: This routes doesn't have pagination, but the type are mismatched. // @ts-expect-error - TODO: This routes doesn't have pagination, but the type are mismatched.
state.tokens = data ?? []; tokens.value = data ?? [];
} }
} }
@ -225,17 +204,4 @@ export default defineNuxtComponent({
await copyTokenLink(token); await copyTokenLink(token);
} }
} }
return {
...toRefs(state),
expirationDateString,
dialog,
createNewToken,
deleteToken,
firstDayOfWeek,
shareRecipe,
copyTokenLink,
};
},
});
</script> </script>

View file

@ -4,7 +4,7 @@
nudge-right="50" nudge-right="50"
:color="buttonStyle ? 'info' : 'secondary'" :color="buttonStyle ? 'info' : 'secondary'"
> >
<template #activator="{ props }"> <template #activator="{ props: tooltipProps }">
<v-btn <v-btn
v-if="isFavorite || showAlways" v-if="isFavorite || showAlways"
icon icon
@ -13,7 +13,7 @@
size="small" size="small"
:color="buttonStyle ? 'info' : 'secondary'" :color="buttonStyle ? 'info' : 'secondary'"
:fab="buttonStyle" :fab="buttonStyle"
v-bind="{ ...props, ...$attrs }" v-bind="{ ...tooltipProps, ...$attrs }"
@click.prevent="toggleFavorite" @click.prevent="toggleFavorite"
> >
<v-icon <v-icon
@ -28,26 +28,21 @@
</v-tooltip> </v-tooltip>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useUserSelfRatings } from "~/composables/use-users"; import { useUserSelfRatings } from "~/composables/use-users";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
export default defineNuxtComponent({ interface Props {
props: { recipeId?: string;
recipeId: { showAlways?: boolean;
type: String, buttonStyle?: boolean;
default: "", }
}, const props = withDefaults(defineProps<Props>(), {
showAlways: { recipeId: "",
type: Boolean, showAlways: false,
default: false, buttonStyle: false,
}, });
buttonStyle: {
type: Boolean,
default: false,
},
},
setup(props) {
const api = useUserApi(); const api = useUserApi();
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { userRatings, refreshUserRatings } = useUserSelfRatings(); const { userRatings, refreshUserRatings } = useUserSelfRatings();
@ -67,8 +62,4 @@ export default defineNuxtComponent({
} }
await refreshUserRatings(); await refreshUserRatings();
} }
return { isFavorite, toggleFavorite };
},
});
</script> </script>

View file

@ -7,11 +7,11 @@
nudge-top="6" nudge-top="6"
:close-on-content-click="false" :close-on-content-click="false"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-btn <v-btn
color="accent" color="accent"
dark dark
v-bind="props" v-bind="activatorProps"
> >
<v-icon start> <v-icon start>
{{ $globals.icons.fileImage }} {{ $globals.icons.fileImage }}
@ -61,52 +61,42 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
const REFRESH_EVENT = "refresh"; const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload"; const UPLOAD_EVENT = "upload";
export default defineNuxtComponent({ const props = defineProps<{ slug: string }>();
props: {
slug: { const emit = defineEmits<{
type: String, refresh: [];
required: true, upload: [fileObject: File];
}, }>();
},
setup(props, context) { const url = ref("");
const state = reactive({ const loading = ref(false);
url: "", const menu = ref(false);
loading: false,
menu: false,
});
function uploadImage(fileObject: File) { function uploadImage(fileObject: File) {
context.emit(UPLOAD_EVENT, fileObject); emit(UPLOAD_EVENT, fileObject);
state.menu = false; menu.value = false;
} }
const api = useUserApi(); const api = useUserApi();
async function getImageFromURL() { async function getImageFromURL() {
state.loading = true; loading.value = true;
if (await api.recipes.updateImagebyURL(props.slug, state.url)) { if (await api.recipes.updateImagebyURL(props.slug, url.value)) {
context.emit(REFRESH_EVENT); emit(REFRESH_EVENT);
} }
state.loading = false; loading.value = false;
state.menu = false; menu.value = false;
} }
const i18n = useI18n(); const i18n = useI18n();
const messages = props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")]; const messages = computed(() =>
props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")],
return { );
...toRefs(state),
uploadImage,
getImageFromURL,
messages,
};
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View file

@ -63,6 +63,22 @@
clearable clearable
@keyup.enter="handleUnitEnter" @keyup.enter="handleUnitEnter"
> >
<template #prepend>
<v-tooltip v-if="unitError" location="bottom">
<template #activator="{ props: unitTooltipProps }">
<v-icon
v-bind="unitTooltipProps"
class="ml-2 mr-n3 opacity-100"
color="primary"
>
{{ $globals.icons.alert }}
</v-icon>
</template>
<span v-if="unitErrorTooltip">
{{ unitErrorTooltip }}
</span>
</v-tooltip>
</template>
<template #no-data> <template #no-data>
<div class="caption text-center pb-2"> <div class="caption text-center pb-2">
{{ $t("recipe.press-enter-to-create") }} {{ $t("recipe.press-enter-to-create") }}
@ -104,6 +120,22 @@
clearable clearable
@keyup.enter="handleFoodEnter" @keyup.enter="handleFoodEnter"
> >
<template #prepend>
<v-tooltip v-if="foodError" location="bottom">
<template #activator="{ props: foodTooltipProps }">
<v-icon
v-bind="foodTooltipProps"
class="ml-2 mr-n3 opacity-100"
color="primary"
>
{{ $globals.icons.alert }}
</v-icon>
</template>
<span v-if="foodErrorTooltip">
{{ foodErrorTooltip }}
</span>
</v-tooltip>
</template>
<template #no-data> <template #no-data>
<div class="caption text-center pb-2"> <div class="caption text-center pb-2">
{{ $t("recipe.press-enter-to-create") }} {{ $t("recipe.press-enter-to-create") }}
@ -153,7 +185,6 @@
@toggle-original="toggleOriginalText" @toggle-original="toggleOriginalText"
@insert-above="$emit('insert-above')" @insert-above="$emit('insert-above')"
@insert-below="$emit('insert-below')" @insert-below="$emit('insert-below')"
@insert-ingredient="$emit('insert-ingredient')"
@delete="$emit('delete')" @delete="$emit('delete')"
/> />
</div> </div>
@ -184,22 +215,33 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
// defineModel replaces modelValue prop // defineModel replaces modelValue prop
const model = defineModel<RecipeIngredient>({ required: true }); const model = defineModel<RecipeIngredient>({ required: true });
const props = defineProps({ defineProps({
disableAmount: { disableAmount: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
allowInsertIngredient: { unitError: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
unitErrorTooltip: {
type: String,
default: "",
},
foodError: {
type: Boolean,
default: false,
},
foodErrorTooltip: {
type: String,
default: "",
},
}); });
defineEmits([ defineEmits([
"clickIngredientField", "clickIngredientField",
"insert-above", "insert-above",
"insert-below", "insert-below",
"insert-ingredient",
"delete", "delete",
]); ]);
@ -228,13 +270,6 @@ const contextMenuOptions = computed(() => {
}, },
]; ];
if (props.allowInsertIngredient) {
options.push({
text: i18n.t("recipe.insert-ingredient"),
event: "insert-ingredient",
});
}
if (model.value.originalText) { if (model.value.originalText) {
options.push({ options.push({
text: i18n.t("recipe.see-original-text"), text: i18n.t("recipe.see-original-text"),

View file

@ -3,21 +3,13 @@
<div v-html="safeMarkup" /> <div v-html="safeMarkup" />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients"; import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients";
export default defineNuxtComponent({ interface Props {
props: { markup: string;
markup: { }
type: String, const props = defineProps<Props>();
required: true,
},
},
setup(props) {
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup)); const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
return {
safeMarkup,
};
},
});
</script> </script>

View file

@ -28,34 +28,22 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { RecipeIngredient } from "~/lib/api/types/household"; import type { RecipeIngredient } from "~/lib/api/types/household";
import { useParsedIngredientText } from "~/composables/recipes"; import { useParsedIngredientText } from "~/composables/recipes";
export default defineNuxtComponent({ interface Props {
props: { ingredient: RecipeIngredient;
ingredient: { disableAmount?: boolean;
type: Object as () => RecipeIngredient, scale?: number;
required: true, }
}, const props = withDefaults(defineProps<Props>(), {
disableAmount: { disableAmount: false,
type: Boolean, scale: 1,
default: false,
},
scale: {
type: Number,
default: 1,
},
},
setup(props) {
const parsedIng = computed(() => {
return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale);
}); });
return { const parsedIng = computed(() => {
parsedIng, return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale);
};
},
}); });
</script> </script>

View file

@ -53,40 +53,30 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue"; import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import { parseIngredientText } from "~/composables/recipes"; import { parseIngredientText } from "~/composables/recipes";
import type { RecipeIngredient } from "~/lib/api/types/recipe"; import type { RecipeIngredient } from "~/lib/api/types/recipe";
export default defineNuxtComponent({ interface Props {
components: { RecipeIngredientListItem }, value?: RecipeIngredient[];
props: { disableAmount?: boolean;
value: { scale?: number;
type: Array as () => RecipeIngredient[], isCookMode?: boolean;
default: () => [], }
}, const props = withDefaults(defineProps<Props>(), {
disableAmount: { value: () => [],
type: Boolean, disableAmount: false,
default: false, scale: 1,
}, isCookMode: false,
scale: { });
type: Number,
default: 1, function validateTitle(title?: string | null) {
},
isCookMode: {
type: Boolean,
default: false,
},
},
setup(props) {
function validateTitle(title?: string) {
return !(title === undefined || title === "" || title === null); return !(title === undefined || title === "" || title === null);
} }
const state = reactive({ const checked = ref(props.value.map(() => false));
checked: props.value.map(() => false), const showTitleEditor = computed(() => props.value.map(x => validateTitle(x.title)));
showTitleEditor: computed(() => props.value.map(x => validateTitle(x.title))),
});
const ingredientCopyText = computed(() => { const ingredientCopyText = computed(() => {
const components: string[] = []; const components: string[] = [];
@ -108,16 +98,8 @@ export default defineNuxtComponent({
function toggleChecked(index: number) { function toggleChecked(index: number) {
// TODO Find a better way to do this - $set is not available, and // TODO Find a better way to do this - $set is not available, and
// direct array modifications are not propagated for some reason // direct array modifications are not propagated for some reason
state.checked.splice(index, 1, !state.checked[index]); checked.value.splice(index, 1, !checked.value[index]);
} }
return {
...toRefs(state),
ingredientCopyText,
toggleChecked,
};
},
});
</script> </script>
<style> <style>

View file

@ -3,6 +3,7 @@
<div> <div>
<BaseDialog <BaseDialog
v-model="madeThisDialog" v-model="madeThisDialog"
:loading="madeThisFormLoading"
:icon="$globals.icons.chefHat" :icon="$globals.icons.chefHat"
:title="$t('recipe.made-this')" :title="$t('recipe.made-this')"
:submit-text="$t('recipe.add-to-timeline')" :submit-text="$t('recipe.add-to-timeline')"
@ -29,11 +30,11 @@
offset-y offset-y
max-width="290px" max-width="290px"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-text-field <v-text-field
v-model="newTimelineEventTimestampString" v-model="newTimelineEventTimestampString"
:prepend-icon="$globals.icons.calendar" :prepend-icon="$globals.icons.calendar"
v-bind="props" v-bind="activatorProps"
readonly readonly
/> />
</template> </template>
@ -85,13 +86,13 @@
<div> <div>
<div v-if="lastMadeReady" class="d-flex justify-center flex-wrap"> <div v-if="lastMadeReady" class="d-flex justify-center flex-wrap">
<v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger"> <v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger">
<v-tooltip bottom> <v-tooltip location="bottom">
<template #activator="{ props }"> <template #activator="{ props: tooltipProps }">
<v-btn <v-btn
rounded rounded
variant="outlined" variant="outlined"
size="x-large" size="x-large"
v-bind="props" v-bind="tooltipProps"
style="border-color: rgb(var(--v-theme-primary));" style="border-color: rgb(var(--v-theme-primary));"
@click="madeThisDialog = true" @click="madeThisDialog = true"
> >
@ -116,22 +117,19 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { whenever } from "@vueuse/core"; import { whenever } from "@vueuse/core";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { useHouseholdSelf } from "~/composables/use-households"; import { useHouseholdSelf } from "~/composables/use-households";
import type { Recipe, RecipeTimelineEventIn } from "~/lib/api/types/recipe"; import type { Recipe, RecipeTimelineEventIn, RecipeTimelineEventOut } from "~/lib/api/types/recipe";
import type { VForm } from "~/types/auto-forms"; import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({ const props = defineProps<{ recipe: Recipe }>();
props: { const emit = defineEmits<{
recipe: { eventCreated: [event: RecipeTimelineEventOut];
type: Object as () => Recipe, }>();
required: true,
},
},
emits: ["eventCreated"],
setup(props, context) {
const madeThisDialog = ref(false); const madeThisDialog = ref(false);
const userApi = useUserApi(); const userApi = useUserApi();
const { household } = useHouseholdSelf(); const { household } = useHouseholdSelf();
@ -196,12 +194,26 @@ export default defineNuxtComponent({
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject); newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
} }
const state = reactive({ datePickerMenu: false }); const datePickerMenu = ref(false);
const madeThisFormLoading = ref(false);
function resetMadeThisForm() {
madeThisFormLoading.value = false;
newTimelineEvent.value.eventMessage = "";
newTimelineEvent.value.timestamp = undefined;
clearImage();
madeThisDialog.value = false;
domMadeThisForm.value?.reset();
}
async function createTimelineEvent() { async function createTimelineEvent() {
if (!(newTimelineEventTimestampString.value && props.recipe?.id && props.recipe?.slug)) { if (!(newTimelineEventTimestampString.value && props.recipe?.id && props.recipe?.slug)) {
return; return;
} }
madeThisFormLoading.value = true;
newTimelineEvent.value.recipeId = props.recipe.id; newTimelineEvent.value.recipeId = props.recipe.id;
// Note: $auth.user is now a ref // Note: $auth.user is now a ref
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName }); newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName });
@ -210,17 +222,37 @@ export default defineNuxtComponent({
// we choose the end of day so it always comes after "new recipe" events // we choose the end of day so it always comes after "new recipe" events
newTimelineEvent.value.timestamp = new Date(newTimelineEventTimestampString.value + "T23:59:59").toISOString(); newTimelineEvent.value.timestamp = new Date(newTimelineEventTimestampString.value + "T23:59:59").toISOString();
let newEvent: RecipeTimelineEventOut | null = null;
try {
const eventResponse = await userApi.recipes.createTimelineEvent(newTimelineEvent.value); const eventResponse = await userApi.recipes.createTimelineEvent(newTimelineEvent.value);
const newEvent = eventResponse.data; newEvent = eventResponse.data;
if (!newEvent) {
throw new Error("No event created");
}
}
catch (error) {
console.error("Failed to create timeline event:", error);
alert.error(i18n.t("recipe.failed-to-add-to-timeline"));
resetMadeThisForm();
return;
}
// we also update the recipe's last made value // we also update the recipe's last made value
if (!lastMade.value || newTimelineEvent.value.timestamp > lastMade.value) { if (!lastMade.value || newTimelineEvent.value.timestamp > lastMade.value) {
try {
lastMade.value = newTimelineEvent.value.timestamp; lastMade.value = newTimelineEvent.value.timestamp;
await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp); await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp);
} }
catch (error) {
console.error("Failed to update last made date:", error);
alert.error(i18n.t("recipe.failed-to-update-recipe"));
}
}
// update the image, if provided // update the image, if provided
if (newTimelineEventImage.value && newEvent) { let imageError = false;
if (newTimelineEventImage.value) {
try {
const imageResponse = await userApi.recipes.updateTimelineEventImage( const imageResponse = await userApi.recipes.updateTimelineEventImage(
newEvent.id, newEvent.id,
newTimelineEventImage.value, newTimelineEventImage.value,
@ -230,34 +262,20 @@ export default defineNuxtComponent({
newEvent.image = imageResponse.data.image; newEvent.image = imageResponse.data.image;
} }
} }
catch (error) {
// reset form imageError = true;
newTimelineEvent.value.eventMessage = ""; console.error("Failed to upload image for timeline event:", error);
newTimelineEvent.value.timestamp = undefined; }
clearImage();
madeThisDialog.value = false;
domMadeThisForm.value?.reset();
context.emit("eventCreated", newEvent);
} }
return { if (imageError) {
...toRefs(state), alert.error(i18n.t("recipe.added-to-timeline-but-failed-to-add-image"));
domMadeThisForm, }
madeThisDialog, else {
firstDayOfWeek, alert.success(i18n.t("recipe.added-to-timeline"));
newTimelineEvent, }
newTimelineEventImage,
newTimelineEventImagePreviewUrl, resetMadeThisForm();
newTimelineEventTimestamp, emit("eventCreated", newEvent);
newTimelineEventTimestampString, }
lastMade,
lastMadeReady,
createTimelineEvent,
clearImage,
uploadImage,
updateUploadedImage,
};
},
});
</script> </script>

View file

@ -51,40 +51,28 @@
</v-list> </v-list>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { useFraction } from "~/composables/recipes/use-fraction"; import { useFraction } from "~/composables/recipes/use-fraction";
import type { ShoppingListItemOut } from "~/lib/api/types/household"; import type { ShoppingListItemOut } from "~/lib/api/types/household";
import type { RecipeSummary } from "~/lib/api/types/recipe"; import type { RecipeSummary } from "~/lib/api/types/recipe";
export default defineNuxtComponent({ interface Props {
props: { recipes: RecipeSummary[];
recipes: { listItem?: ShoppingListItemOut;
type: Array as () => RecipeSummary[], small?: boolean;
required: true, tile?: boolean;
}, showDescription?: boolean;
listItem: { disabled?: boolean;
type: Object as () => ShoppingListItemOut | undefined, }
default: undefined, const props = withDefaults(defineProps<Props>(), {
}, listItem: undefined,
small: { small: false,
type: Boolean, tile: false,
default: false, showDescription: false,
}, disabled: false,
tile: { });
type: Boolean,
default: false,
},
showDescription: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props) {
const $auth = useMealieAuth(); const $auth = useMealieAuth();
const { frac } = useFraction(); const { frac } = useFraction();
const route = useRoute(); const route = useRoute();
@ -180,12 +168,4 @@ export default defineNuxtComponent({
return listItemDescriptions; return listItemDescriptions;
}); });
return {
attrs,
groupSlug,
listItemDescriptions,
};
},
});
</script> </script>

View file

@ -45,29 +45,25 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useNutritionLabels } from "~/composables/recipes"; import { useNutritionLabels } from "~/composables/recipes";
import type { Nutrition } from "~/lib/api/types/recipe"; import type { Nutrition } from "~/lib/api/types/recipe";
import type { NutritionLabelType } from "~/composables/recipes/use-recipe-nutrition"; import type { NutritionLabelType } from "~/composables/recipes/use-recipe-nutrition";
export default defineNuxtComponent({ interface Props {
props: { edit?: boolean;
modelValue: { }
type: Object as () => Nutrition, const props = withDefaults(defineProps<Props>(), {
required: true, edit: true,
}, });
edit: {
type: Boolean, const modelValue = defineModel<Nutrition>({ required: true });
default: true,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const { labels } = useNutritionLabels(); const { labels } = useNutritionLabels();
const valueNotNull = computed(() => { const valueNotNull = computed(() => {
let key: keyof Nutrition; let key: keyof Nutrition;
for (key in props.modelValue) { for (key in modelValue.value) {
if (props.modelValue[key] !== null) { if (modelValue.value[key] !== null) {
return true; return true;
} }
} }
@ -77,31 +73,21 @@ export default defineNuxtComponent({
const showViewer = computed(() => !props.edit && valueNotNull.value); const showViewer = computed(() => !props.edit && valueNotNull.value);
function updateValue(key: number | string, event: Event) { function updateValue(key: number | string, event: Event) {
context.emit("update:modelValue", { ...props.modelValue, [key]: event }); modelValue.value = { ...modelValue.value, [key]: event };
} }
// Build a new list that only contains nutritional information that has a value // Build a new list that only contains nutritional information that has a value
const renderedList = computed(() => { const renderedList = computed(() => {
return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => { return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => {
if (props.modelValue[key]?.trim()) { if (modelValue.value[key]?.trim()) {
item[key] = { item[key] = {
...label, ...label,
value: props.modelValue[key], value: modelValue.value[key],
}; };
} }
return item; return item;
}, {}); }, {});
}); });
return {
labels,
valueNotNull,
showViewer,
updateValue,
renderedList,
};
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View file

@ -60,54 +60,39 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store"; import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated"; import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated";
const CREATED_ITEM_EVENT = "created-item"; const CREATED_ITEM_EVENT = "created-item";
export default defineNuxtComponent({ interface Props {
props: { color?: string | null;
modelValue: { tagDialog?: boolean;
type: Boolean, itemType?: RecipeOrganizer;
default: false, }
}, const props = withDefaults(defineProps<Props>(), {
color: { color: null,
type: String, tagDialog: true,
default: null, itemType: "category" as RecipeOrganizer,
}, });
tagDialog: {
type: Boolean, const emit = defineEmits<{
default: true, "created-item": [item: any];
}, }>();
itemType: {
type: String as () => RecipeOrganizer, const dialog = defineModel<boolean>({ default: false });
default: "category",
},
},
emits: ["update:modelValue"],
setup(props, context) {
const i18n = useI18n(); const i18n = useI18n();
const state = reactive({ const name = ref("");
name: "", const onHand = ref(false);
onHand: false,
});
const dialog = computed({
get() {
return props.modelValue;
},
set(value) {
context.emit("update:modelValue", value);
},
});
watch( watch(
() => props.modelValue, dialog,
(val: boolean) => { (val: boolean) => {
if (!val) state.name = ""; if (!val) name.value = "";
}, },
); );
@ -154,25 +139,14 @@ export default defineNuxtComponent({
async function select() { async function select() {
if (store) { if (store) {
// @ts-expect-error the same state is used for different organizer types, which have different requirements // @ts-expect-error the same state is used for different organizer types, which have different requirements
await store.actions.createOne({ ...state }); await store.actions.createOne({ name: name.value, onHand: onHand.value });
} }
const newItem = store.store.value.find(item => item.name === state.name); const newItem = store.store.value.find(item => item.name === name.value);
context.emit(CREATED_ITEM_EVENT, newItem); emit(CREATED_ITEM_EVENT, newItem);
dialog.value = false; dialog.value = false;
} }
return {
Organizer,
...toRefs(state),
dialog,
properties,
rules,
select,
};
},
});
</script> </script>
<style></style> <style></style>

View file

@ -122,9 +122,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { useContextPresets } from "~/composables/use-context-presents"; import { useContextPresets } from "~/composables/use-context-presents";
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue"; import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated"; import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
@ -138,26 +137,17 @@ interface GenericItem {
onHand: boolean; onHand: boolean;
} }
export default defineNuxtComponent({ const props = defineProps<{
components: { items: GenericItem[];
RecipeOrganizerDialog, icon: string;
}, itemType: RecipeOrganizer;
props: { }>();
items: {
type: Array as () => GenericItem[], const emit = defineEmits<{
required: true, update: [item: GenericItem];
}, delete: [id: string];
icon: { }>();
type: String,
required: true,
},
itemType: {
type: String as () => RecipeOrganizer,
required: true,
},
},
emits: ["update", "delete"],
setup(props, { emit }) {
const state = reactive({ const state = reactive({
// Search Options // Search Options
options: { options: {
@ -271,23 +261,4 @@ export default defineNuxtComponent({
function isTitle(str: number | string) { function isTitle(str: number | string) {
return typeof str === "string" && str.length === 1; return typeof str === "string" && str.length === 1;
} }
return {
groupSlug,
isTitle,
dialogs,
confirmDelete,
openUpdateDialog,
updateOne,
updateTarget,
deleteOne,
deleteTarget,
Organizer,
presets,
itemsSorted,
searchString,
translationKey,
};
},
});
</script> </script>

View file

@ -3,7 +3,7 @@
v-model="selected" v-model="selected"
v-bind="inputAttrs" v-bind="inputAttrs"
v-model:search="searchInput" v-model:search="searchInput"
:items="storeItem" :items="items"
:label="label" :label="label"
chips chips
closable-chips closable-chips
@ -46,67 +46,40 @@
</v-autocomplete> </v-autocomplete>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe"; import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
import type { RecipeTool } from "~/lib/api/types/admin"; import type { RecipeTool } from "~/lib/api/types/admin";
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated"; import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
import type { HouseholdSummary } from "~/lib/api/types/household"; import type { HouseholdSummary } from "~/lib/api/types/household";
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store"; import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
export default defineNuxtComponent({ interface Props {
props: { selectorType: RecipeOrganizer;
modelValue: { inputAttrs?: Record<string, any>;
type: Array as () => ( returnObject?: boolean;
showAdd?: boolean;
showLabel?: boolean;
showIcon?: boolean;
variant?: "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled";
}
const props = withDefaults(defineProps<Props>(), {
inputAttrs: () => ({}),
returnObject: true,
showAdd: true,
showLabel: true,
showIcon: true,
variant: "outlined",
});
const selected = defineModel<(
| HouseholdSummary | HouseholdSummary
| RecipeTag | RecipeTag
| RecipeCategory | RecipeCategory
| RecipeTool | RecipeTool
| IngredientFood | IngredientFood
| string | string
)[] | undefined, )[] | undefined>({ required: true });
required: true,
},
/**
* The type of organizer to use.
*/
selectorType: {
type: String as () => RecipeOrganizer,
required: true,
},
inputAttrs: {
type: Object as () => Record<string, any>,
default: () => ({}),
},
returnObject: {
type: Boolean,
default: true,
},
showAdd: {
type: Boolean,
default: true,
},
showLabel: {
type: Boolean,
default: true,
},
showIcon: {
type: Boolean,
default: true,
},
variant: {
type: String as () => "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled",
default: "outlined",
},
},
emits: ["update:modelValue"],
setup(props, context) {
const selected = computed({
get: () => props.modelValue,
set: (val) => {
context.emit("update:modelValue", val);
},
});
onMounted(() => { onMounted(() => {
if (selected.value === undefined) { if (selected.value === undefined) {
@ -205,21 +178,6 @@ export default defineNuxtComponent({
function resetSearchInput() { function resetSearchInput() {
searchInput.value = ""; searchInput.value = "";
} }
return {
Organizer,
appendCreated,
dialog,
storeItem: items,
label,
icon,
selected,
removeByIndex,
searchInput,
resetSearchInput,
};
},
});
</script> </script>
<style scoped> <style scoped>

View file

@ -37,7 +37,7 @@
<RecipePageIngredientEditor v-if="isEditForm" v-model="recipe" /> <RecipePageIngredientEditor v-if="isEditForm" v-model="recipe" />
</div> </div>
<div> <div>
<RecipePageScale v-model:scale="scale" :recipe="recipe" /> <RecipePageScale v-model="scale" :recipe="recipe" />
</div> </div>
<!-- <!--
@ -81,7 +81,7 @@
</v-card> </v-card>
<WakelockSwitch /> <WakelockSwitch />
<RecipePageComments <RecipePageComments
v-if="!recipe.settings.disableComments && !isEditForm && !isCookMode" v-if="!recipe.settings?.disableComments && !isEditForm && !isCookMode"
v-model="recipe" v-model="recipe"
class="px-1 my-4 d-print-none" class="px-1 my-4 d-print-none"
/> />
@ -96,7 +96,7 @@
<v-row style="height: 100%" no-gutters class="overflow-hidden"> <v-row style="height: 100%" no-gutters class="overflow-hidden">
<v-col cols="12" sm="5" class="overflow-y-auto pl-4 pr-3 py-2" style="height: 100%"> <v-col cols="12" sm="5" class="overflow-y-auto pl-4 pr-3 py-2" style="height: 100%">
<div class="d-flex align-center"> <div class="d-flex align-center">
<RecipePageScale v-model:scale="scale" :recipe="recipe" /> <RecipePageScale v-model="scale" :recipe="recipe" />
</div> </div>
<RecipePageIngredientToolsView <RecipePageIngredientToolsView
v-if="!isEditForm" v-if="!isEditForm"
@ -124,7 +124,7 @@
</v-sheet> </v-sheet>
<v-sheet v-show="isCookMode && hasLinkedIngredients"> <v-sheet v-show="isCookMode && hasLinkedIngredients">
<div class="mt-2 px-2 px-md-4"> <div class="mt-2 px-2 px-md-4">
<RecipePageScale v-model:scale="scale" :recipe="recipe" /> <RecipePageScale v-model="scale" :recipe="recipe" />
</div> </div>
<RecipePageInstructions <RecipePageInstructions
v-model="recipe.recipeInstructions" v-model="recipe.recipeInstructions"
@ -278,7 +278,7 @@ async function deleteRecipe() {
* View Preferences * View Preferences
*/ */
const landscape = computed(() => { const landscape = computed(() => {
const preferLandscape = recipe.value.settings.landscapeView; const preferLandscape = recipe.value.settings?.landscapeView;
const smallScreen = !$vuetify.display.smAndUp.value; const smallScreen = !$vuetify.display.smAndUp.value;
if (preferLandscape) { if (preferLandscape) {

View file

@ -26,7 +26,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useRecipePermissions } from "~/composables/recipes"; import { useRecipePermissions } from "~/composables/recipes";
import RecipePageInfoCard from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue"; import RecipePageInfoCard from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue";
@ -35,32 +35,22 @@ import { useStaticRoutes, useUserApi } from "~/composables/api";
import type { HouseholdSummary } from "~/lib/api/types/household"; import type { HouseholdSummary } from "~/lib/api/types/household";
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
import type { NoUndefinedField } from "~/lib/api/types/non-generated"; import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import { usePageState, usePageUser, PageMode, EditorMode } from "~/composables/recipe-page/shared-state"; import { usePageState, usePageUser, PageMode } from "~/composables/recipe-page/shared-state";
interface Props {
recipe: NoUndefinedField<Recipe>;
recipeScale?: number;
landscape?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
recipeScale: 1,
landscape: false,
});
defineEmits(["save", "delete"]);
export default defineNuxtComponent({
components: {
RecipePageInfoCard,
RecipeActionMenu,
},
props: {
recipe: {
type: Object as () => NoUndefinedField<Recipe>,
required: true,
},
recipeScale: {
type: Number,
default: 1,
},
landscape: {
type: Boolean,
default: false,
},
},
emits: ["save", "delete"],
setup(props) {
const { $vuetify } = useNuxtApp();
const { recipeImage } = useStaticRoutes(); const { recipeImage } = useStaticRoutes();
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug); const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
const { user } = usePageUser(); const { user } = usePageUser();
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
@ -78,9 +68,6 @@ export default defineNuxtComponent({
} }
const hideImage = ref(false); const hideImage = ref(false);
const imageHeight = computed(() => {
return $vuetify.display.xs.value ? "200" : "400";
});
const recipeImageUrl = computed(() => { const recipeImageUrl = computed(() => {
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value); return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
@ -92,25 +79,4 @@ export default defineNuxtComponent({
hideImage.value = false; hideImage.value = false;
}, },
); );
return {
isOwnGroup,
setMode,
toggleEditMode,
recipeImage,
canEditRecipe,
imageKey,
user,
PageMode,
pageMode,
EditorMode,
editMode,
printRecipe,
imageHeight,
hideImage,
isEditMode,
recipeImageUrl,
};
},
});
</script> </script>

View file

@ -35,7 +35,7 @@
> >
<RecipeYield <RecipeYield
:yield-quantity="recipe.recipeYieldQuantity" :yield-quantity="recipe.recipeYieldQuantity"
:yield="recipe.recipeYield" :yield-text="recipe.recipeYield"
:scale="recipeScale" :scale="recipeScale"
class="mb-4" class="mb-4"
/> />
@ -76,7 +76,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue"; import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue"; import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
@ -86,34 +86,15 @@ import RecipePageInfoCardImage from "~/components/Domain/Recipe/RecipePage/Recip
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
import type { NoUndefinedField } from "~/lib/api/types/non-generated"; import type { NoUndefinedField } from "~/lib/api/types/non-generated";
export default defineNuxtComponent({ interface Props {
components: { recipe: NoUndefinedField<Recipe>;
RecipeRating, recipeScale?: number;
RecipeLastMade, landscape: boolean;
RecipeTimeCard, }
RecipeYield,
RecipePageInfoCardImage,
},
props: {
recipe: {
type: Object as () => NoUndefinedField<Recipe>,
required: true,
},
recipeScale: {
type: Number,
default: 1,
},
landscape: {
type: Boolean,
required: true,
},
},
setup() {
const { isOwnGroup } = useLoggedInState();
return { withDefaults(defineProps<Props>(), {
isOwnGroup, recipeScale: 1,
};
},
}); });
const { isOwnGroup } = useLoggedInState();
</script> </script>

View file

@ -12,25 +12,21 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useStaticRoutes, useUserApi } from "~/composables/api"; import { useStaticRoutes, useUserApi } from "~/composables/api";
import type { HouseholdSummary } from "~/lib/api/types/household"; import type { HouseholdSummary } from "~/lib/api/types/household";
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state"; import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
import type { NoUndefinedField } from "~/lib/api/types/non-generated"; import type { NoUndefinedField } from "~/lib/api/types/non-generated";
export default defineNuxtComponent({ interface Props {
props: { recipe: NoUndefinedField<Recipe>;
recipe: { maxWidth?: string;
type: Object as () => NoUndefinedField<Recipe>, }
required: true, const props = withDefaults(defineProps<Props>(), {
}, maxWidth: undefined,
maxWidth: { });
type: String,
default: undefined,
},
},
setup(props) {
const { $vuetify } = useNuxtApp(); const { $vuetify } = useNuxtApp();
const { recipeImage } = useStaticRoutes(); const { recipeImage } = useStaticRoutes();
const { imageKey } = usePageState(props.recipe.slug); const { imageKey } = usePageState(props.recipe.slug);
@ -59,13 +55,4 @@ export default defineNuxtComponent({
hideImage.value = false; hideImage.value = false;
}, },
); );
return {
recipeImageUrl,
imageKey,
hideImage,
imageHeight,
};
},
});
</script> </script>

View file

@ -42,7 +42,7 @@
/> />
<div class="d-flex flex-wrap justify-center justify-sm-end mt-3"> <div class="d-flex flex-wrap justify-center justify-sm-end mt-3">
<v-tooltip <v-tooltip
top location="top"
color="accent" color="accent"
> >
<template #activator="{ props }"> <template #activator="{ props }">

View file

@ -36,7 +36,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state"; import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { useToolStore } from "~/composables/store"; import { useToolStore } from "~/composables/store";
@ -48,25 +48,15 @@ interface RecipeToolWithOnHand extends RecipeTool {
onHand: boolean; onHand: boolean;
} }
export default defineNuxtComponent({ interface Props {
components: { recipe: NoUndefinedField<Recipe>;
RecipeIngredients, scale: number;
}, isCookMode?: boolean;
props: { }
recipe: { const props = withDefaults(defineProps<Props>(), {
type: Object as () => NoUndefinedField<Recipe>, isCookMode: false,
required: true, });
},
scale: {
type: Number,
required: true,
},
isCookMode: {
type: Boolean,
default: false,
},
},
setup(props) {
const { isOwnGroup } = useLoggedInState(); const { isOwnGroup } = useLoggedInState();
const toolStore = isOwnGroup.value ? useToolStore() : null; const toolStore = isOwnGroup.value ? useToolStore() : null;
@ -106,13 +96,4 @@ export default defineNuxtComponent({
console.log("no user, skipping server update"); console.log("no user, skipping server update");
} }
} }
return {
toolStore,
recipeTools,
isEditMode,
updateTool,
};
},
});
</script> </script>

View file

@ -29,33 +29,31 @@
{{ activeText }} {{ activeText }}
</p> </p>
<v-divider class="mb-4" /> <v-divider class="mb-4" />
<v-checkbox <v-checkbox-btn
v-for="ing in unusedIngredients" v-for="ing in unusedIngredients"
:key="ing.referenceId" :key="ing.referenceId"
v-model="activeRefs" v-model="activeRefs"
:value="ing.referenceId" :value="ing.referenceId"
class="mb-n2 mt-n2"
> >
<template #label> <template #label>
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" /> <RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" />
</template> </template>
</v-checkbox> </v-checkbox-btn>
<template v-if="usedIngredients.length > 0"> <template v-if="usedIngredients.length > 0">
<h4 class="py-3 ml-1"> <h4 class="py-3 ml-1">
{{ $t("recipe.linked-to-other-step") }} {{ $t("recipe.linked-to-other-step") }}
</h4> </h4>
<v-checkbox <v-checkbox-btn
v-for="ing in usedIngredients" v-for="ing in usedIngredients"
:key="ing.referenceId" :key="ing.referenceId"
v-model="activeRefs" v-model="activeRefs"
:value="ing.referenceId" :value="ing.referenceId"
class="mb-n2 mt-n2"
> >
<template #label> <template #label>
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" /> <RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" />
</template> </template>
</v-checkbox> </v-checkbox-btn>
</template> </template>
</v-card-text> </v-card-text>

View file

@ -2,55 +2,26 @@
<div class="d-flex justify-space-between align-center pt-2 pb-3"> <div class="d-flex justify-space-between align-center pt-2 pb-3">
<RecipeScaleEditButton <RecipeScaleEditButton
v-if="!isEditMode" v-if="!isEditMode"
v-model.number="scaleValue" v-model.number="scale"
:recipe-servings="recipeServings" :recipe-servings="recipeServings"
:edit-scale="!recipe.settings.disableAmount && !isEditMode" :edit-scale="!recipe.settings.disableAmount && !isEditMode"
/> />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeScaleEditButton from "~/components/Domain/Recipe/RecipeScaleEditButton.vue"; import RecipeScaleEditButton from "~/components/Domain/Recipe/RecipeScaleEditButton.vue";
import type { NoUndefinedField } from "~/lib/api/types/non-generated"; import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
import { usePageState } from "~/composables/recipe-page/shared-state"; import { usePageState } from "~/composables/recipe-page/shared-state";
export default defineNuxtComponent({ const props = defineProps<{ recipe: NoUndefinedField<Recipe> }>();
components: {
RecipeScaleEditButton, const scale = defineModel<number>({ default: 1 });
},
props: {
recipe: {
type: Object as () => NoUndefinedField<Recipe>,
required: true,
},
scale: {
type: Number,
default: 1,
},
},
emits: ["update:scale"],
setup(props, { emit }) {
const { isEditMode } = usePageState(props.recipe.slug); const { isEditMode } = usePageState(props.recipe.slug);
const recipeServings = computed<number>(() => { const recipeServings = computed<number>(() => {
return props.recipe.recipeServings || props.recipe.recipeYieldQuantity || 1; return props.recipe.recipeServings || props.recipe.recipeYieldQuantity || 1;
}); });
const scaleValue = computed<number>({
get() {
return props.scale;
},
set(val) {
emit("update:scale", val);
},
});
return {
recipeServings,
scaleValue,
isEditMode,
};
},
});
</script> </script>

View file

@ -8,24 +8,17 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue"; import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
export default defineNuxtComponent({ interface Props {
components: { recipe: Recipe;
RecipePrintView, scale?: number;
}, }
props: {
recipe: { withDefaults(defineProps<Props>(), {
type: Object as () => Recipe, scale: 1,
required: true,
},
scale: {
type: Number,
default: 1,
},
},
}); });
</script> </script>

View file

@ -166,7 +166,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue"; import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import { useStaticRoutes } from "~/composables/api"; import { useStaticRoutes } from "~/composables/api";
@ -188,25 +188,16 @@ type InstructionSection = {
instructions: RecipeStep[]; instructions: RecipeStep[];
}; };
export default defineNuxtComponent({ interface Props {
components: { recipe: NoUndefinedField<Recipe>;
RecipeTimeCard, scale?: number;
}, dense?: boolean;
props: { }
recipe: { const props = withDefaults(defineProps<Props>(), {
type: Object as () => NoUndefinedField<Recipe>, scale: 1,
required: true, dense: false,
}, });
scale: {
type: Number,
default: 1,
},
dense: {
type: Boolean,
default: false,
},
},
setup(props) {
const i18n = useI18n(); const i18n = useI18n();
const preferences = useUserPrintPreferences(); const preferences = useUserPrintPreferences();
const { recipeImage } = useStaticRoutes(); const { recipeImage } = useStaticRoutes();
@ -219,10 +210,9 @@ export default defineNuxtComponent({
ALLOWED_TAGS: ["strong", "sup"], ALLOWED_TAGS: ["strong", "sup"],
}); });
} }
const servingsDisplay = computed(() => { const servingsDisplay = computed(() => {
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale); const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale);
return scaledAmountDisplay return scaledAmountDisplay || props.recipe.recipeYield
? i18n.t("recipe.yields-amount-with-text", { ? i18n.t("recipe.yields-amount-with-text", {
amount: scaledAmountDisplay, amount: scaledAmountDisplay,
text: props.recipe.recipeYield, text: props.recipe.recipeYield,
@ -333,22 +323,6 @@ export default defineNuxtComponent({
function parseText(ingredient: RecipeIngredient) { function parseText(ingredient: RecipeIngredient) {
return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false, props.scale); return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false, props.scale);
} }
return {
labels,
hasNotes,
imageKey,
ImagePosition,
parseText,
parseIngredientText,
preferences,
recipeImageUrl,
recipeYield,
ingredientSections,
instructionSections,
};
},
});
</script> </script>
<style scoped> <style scoped>

View file

@ -10,11 +10,11 @@
nudge-top="6" nudge-top="6"
:close-on-content-click="false" :close-on-content-click="false"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-tooltip <v-tooltip
v-if="canEditScale" v-if="canEditScale"
size="small" size="small"
top location="top"
color="secondary-darken-1" color="secondary-darken-1"
> >
<template #activator="{ props: tooltipProps }"> <template #activator="{ props: tooltipProps }">
@ -23,7 +23,7 @@
dark dark
color="secondary-darken-1" color="secondary-darken-1"
size="small" size="small"
v-bind="{ ...props, ...tooltipProps }" v-bind="{ ...activatorProps, ...tooltipProps }"
:style="{ cursor: canEditScale ? '' : 'default' }" :style="{ cursor: canEditScale ? '' : 'default' }"
> >
<v-icon <v-icon
@ -45,7 +45,7 @@
dark dark
color="secondary-darken-1" color="secondary-darken-1"
size="small" size="small"
v-bind="props" v-bind="activatorProps"
:style="{ cursor: canEditScale ? '' : 'default' }" :style="{ cursor: canEditScale ? '' : 'default' }"
> >
<v-icon <v-icon
@ -66,21 +66,22 @@
<v-card-text class="mt-n5"> <v-card-text class="mt-n5">
<div class="mt-4 d-flex align-center"> <div class="mt-4 d-flex align-center">
<v-text-field <v-text-field
:model-value="yieldQuantityEditorValue" :model-value="yieldQuantity"
type="number" type="number"
:min="0" :min="0"
variant="underlined" variant="underlined"
hide-spin-buttons hide-spin-buttons
@update:model-value="recalculateScale(yieldQuantityEditorValue)" @update:model-value="recalculateScale(parseFloat($event) || 0)"
/> />
<v-tooltip <v-tooltip
end location="end"
color="secondary-darken-1" color="secondary-darken-1"
> >
<template #activator="{ props }"> <template #activator="{ props: resetTooltipProps }">
<v-btn <v-btn
v-bind="props" v-bind="resetTooltipProps"
icon icon
flat
class="mx-1" class="mx-1"
size="small" size="small"
@click="scale = 1" @click="scale = 1"
@ -121,38 +122,24 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount"; import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
export default defineNuxtComponent({ interface Props {
props: { recipeServings?: number;
modelValue: { editScale?: boolean;
type: Number, }
required: true, const props = withDefaults(defineProps<Props>(), {
}, recipeServings: 0,
recipeServings: { editScale: false,
type: Number, });
default: 0,
}, const scale = defineModel<number>({ required: true });
editScale: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const i18n = useI18n(); const i18n = useI18n();
const menu = ref<boolean>(false); const menu = ref<boolean>(false);
const canEditScale = computed(() => props.editScale && props.recipeServings > 0); const canEditScale = computed(() => props.editScale && props.recipeServings > 0);
const scale = computed({
get: () => props.modelValue,
set: (value) => {
const newScaleNumber = parseFloat(`${value}`);
emit("update:modelValue", isNaN(newScaleNumber) ? 0 : newScaleNumber);
},
});
function recalculateScale(newYield: number) { function recalculateScale(newYield: number) {
if (isNaN(newYield) || newYield <= 0) { if (isNaN(newYield) || newYield <= 0) {
return; return;
@ -178,33 +165,7 @@ export default defineNuxtComponent({
: ""; : "";
}); });
// only update yield quantity when the menu opens, so we don't override the user's input
const yieldQuantityEditorValue = ref(recipeYieldAmount.value.scaledAmount);
watch(
() => menu.value,
() => {
if (!menu.value) {
return;
}
yieldQuantityEditorValue.value = recipeYieldAmount.value.scaledAmount;
},
);
const disableDecrement = computed(() => { const disableDecrement = computed(() => {
return recipeYieldAmount.value.scaledAmount <= 1; return yieldQuantity.value <= 1;
});
return {
menu,
canEditScale,
scale,
recalculateScale,
yieldDisplay,
yieldQuantity,
yieldQuantityEditorValue,
disableDecrement,
};
},
}); });
</script> </script>

View file

@ -1,54 +0,0 @@
<template>
<div class="d-flex justify-center align-center">
<v-btn-toggle v-model="selected" tile group color="primary accent-3" mandatory="force" @change="emitMulti">
<v-btn size="small" :value="false">
{{ $t("search.include") }}
</v-btn>
<v-btn size="small" :value="true">
{{ $t("search.exclude") }}
</v-btn>
</v-btn-toggle>
<v-btn-toggle v-model="match" tile group color="primary accent-3" mandatory="force" @change="emitMulti">
<v-btn size="small" :value="false" class="text-uppercase">
{{ $t("search.and") }}
</v-btn>
<v-btn size="small" :value="true" class="text-uppercase">
{{ $t("search.or") }}
</v-btn>
</v-btn-toggle>
</div>
</template>
<script lang="ts">
type SelectionValue = "include" | "exclude" | "any";
export default defineNuxtComponent({
props: {
modelValue: {
type: String as () => SelectionValue,
default: "include",
},
},
emits: ["update:modelValue", "update"],
data() {
return {
selected: false,
match: false,
};
},
methods: {
emitChange() {
this.$emit("update:modelValue", this.selected);
},
emitMulti() {
const updateData = {
exclude: this.selected,
matchAny: this.match,
};
this.$emit("update", updateData);
},
},
});
</script>
<style lang="scss" scoped></style>

View file

@ -14,9 +14,7 @@
<div v-for="(organizer, idx) in missingOrganizers" :key="idx"> <div v-for="(organizer, idx) in missingOrganizers" :key="idx">
<v-col v-if="organizer.show" cols="12"> <v-col v-if="organizer.show" cols="12">
<div class="d-flex flex-row flex-wrap align-center pt-2"> <div class="d-flex flex-row flex-wrap align-center pt-2">
<v-icon class="ma-0 pa-0"> <v-icon class="ma-0 pa-0" />
{{ organizer.icon }}
</v-icon>
<v-card-text class="mr-0 my-0 pl-1 py-0" style="width: min-content"> <v-card-text class="mr-0 my-0 pl-1 py-0" style="width: min-content">
{{ $t("recipe-finder.missing") }}: {{ $t("recipe-finder.missing") }}:
</v-card-text> </v-card-text>
@ -41,7 +39,7 @@
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
import type { IngredientFood, RecipeSummary, RecipeTool } from "~/lib/api/types/recipe"; import type { IngredientFood, RecipeSummary, RecipeTool } from "~/lib/api/types/recipe";
@ -51,27 +49,25 @@ interface Organizer {
selected: boolean; selected: boolean;
} }
export default defineNuxtComponent({ interface Props {
components: { RecipeCardMobile }, recipe: RecipeSummary;
props: { missingFoods?: IngredientFood[] | null;
recipe: { missingTools?: RecipeTool[] | null;
type: Object as () => RecipeSummary, disableCheckbox?: boolean;
required: true, }
}, const props = withDefaults(defineProps<Props>(), {
missingFoods: { missingFoods: null,
type: Array as () => IngredientFood[] | null, missingTools: null,
default: null, disableCheckbox: false,
}, });
missingTools: {
type: Array as () => RecipeTool[] | null, const emit = defineEmits<{
default: null, "add-food": [food: IngredientFood];
}, "remove-food": [food: IngredientFood];
disableCheckbox: { "add-tool": [tool: RecipeTool];
type: Boolean, "remove-tool": [tool: RecipeTool];
default: false, }>();
},
},
setup(props, context) {
const { $globals } = useNuxtApp(); const { $globals } = useNuxtApp();
const missingOrganizers = computed(() => [ const missingOrganizers = computed(() => [
{ {
@ -105,17 +101,20 @@ export default defineNuxtComponent({
organizer.selected = !organizer.selected; organizer.selected = !organizer.selected;
if (organizer.selected) { if (organizer.selected) {
context.emit(`add-${organizer.type}`, organizer.item); if (organizer.type === "food") {
emit("add-food", organizer.item as IngredientFood);
} }
else { else {
context.emit(`remove-${organizer.type}`, organizer.item); emit("add-tool", organizer.item as RecipeTool);
}
}
else {
if (organizer.type === "food") {
emit("remove-food", organizer.item as IngredientFood);
}
else {
emit("remove-tool", organizer.item as RecipeTool);
}
} }
} }
return {
missingOrganizers,
handleCheckbox,
};
},
});
</script> </script>

View file

@ -1,4 +1,4 @@
<template v-if="showCards"> <template v-if="_showCards">
<div class="text-center"> <div class="text-center">
<!-- Total Time --> <!-- Total Time -->
<div <div
@ -78,38 +78,29 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
export default defineNuxtComponent({ interface Props {
props: { prepTime?: string | null;
prepTime: { totalTime?: string | null;
type: String, performTime?: string | null;
default: null, color?: string;
}, small?: boolean;
totalTime: { }
type: String, const props = withDefaults(defineProps<Props>(), {
default: null, prepTime: null,
}, totalTime: null,
performTime: { performTime: null,
type: String, color: "accent custom-transparent",
default: null, small: false,
}, });
color: {
type: String,
default: "accent custom-transparent",
},
small: {
type: Boolean,
default: false,
},
},
setup(props) {
const i18n = useI18n(); const i18n = useI18n();
function isEmpty(str: string | null) { function isEmpty(str: string | null) {
return !str || str.length === 0; return !str || str.length === 0;
} }
const showCards = computed(() => { const _showCards = computed(() => {
return [props.prepTime, props.totalTime, props.performTime].some(x => !isEmpty(x)); return [props.prepTime, props.totalTime, props.performTime].some(x => !isEmpty(x));
}); });
@ -128,16 +119,6 @@ export default defineNuxtComponent({
const fontSize = computed(() => { const fontSize = computed(() => {
return props.small ? { fontSize: "smaller" } : { fontSize: "larger" }; return props.small ? { fontSize: "smaller" } : { fontSize: "larger" };
}); });
return {
showCards,
validateTotalTime,
validatePrepTime,
validatePerformTime,
fontSize,
};
},
});
</script> </script>
<style scoped> <style scoped>

View file

@ -11,7 +11,7 @@
nudge-bottom="3" nudge-bottom="3"
:close-on-content-click="false" :close-on-content-click="false"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-badge <v-badge
:content="filterBadgeCount" :content="filterBadgeCount"
:model-value="filterBadgeCount > 0" :model-value="filterBadgeCount > 0"
@ -21,7 +21,7 @@
class="rounded-circle" class="rounded-circle"
size="small" size="small"
color="info" color="info"
v-bind="props" v-bind="activatorProps"
icon icon
> >
<v-icon> {{ $globals.icons.filter }} </v-icon> <v-icon> {{ $globals.icons.filter }} </v-icon>
@ -105,7 +105,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useThrottleFn, whenever } from "@vueuse/core"; import { useThrottleFn, whenever } from "@vueuse/core";
import RecipeTimelineItem from "./RecipeTimelineItem.vue"; import RecipeTimelineItem from "./RecipeTimelineItem.vue";
import { useTimelinePreferences } from "~/composables/use-users/preferences"; import { useTimelinePreferences } from "~/composables/use-users/preferences";
@ -115,29 +115,19 @@ import { alert } from "~/composables/use-toast";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import type { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate, TimelineEventType } from "~/lib/api/types/recipe"; import type { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate, TimelineEventType } from "~/lib/api/types/recipe";
export default defineNuxtComponent({ interface Props {
components: { RecipeTimelineItem }, modelValue?: boolean;
queryFilter: string;
maxHeight?: number | string;
showRecipeCards?: boolean;
}
props: { const props = withDefaults(defineProps<Props>(), {
modelValue: { modelValue: false,
type: Boolean, maxHeight: undefined,
default: false, showRecipeCards: false,
}, });
queryFilter: {
type: String,
required: true,
},
maxHeight: {
type: [Number, String],
default: undefined,
},
showRecipeCards: {
type: Boolean,
default: false,
},
},
setup(props) {
const api = useUserApi(); const api = useUserApi();
const i18n = useI18n(); const i18n = useI18n();
const preferences = useTimelinePreferences(); const preferences = useTimelinePreferences();
@ -160,25 +150,7 @@ export default defineNuxtComponent({
}; };
}); });
}); });
interface ScrollEvent extends Event {
target: HTMLInputElement;
}
const screenBuffer = 4; const screenBuffer = 4;
const onScroll = (event: ScrollEvent) => {
if (!event.target) {
return;
}
const { scrollTop, offsetHeight, scrollHeight } = event.target;
// trigger when the user is getting close to the bottom
const bottomOfElement = scrollTop + offsetHeight >= scrollHeight - (offsetHeight * screenBuffer);
if (bottomOfElement) {
infiniteScroll();
}
};
whenever( whenever(
() => props.modelValue, () => props.modelValue,
@ -229,7 +201,7 @@ export default defineNuxtComponent({
} }
alert.success(i18n.t("events.event-updated") as string); alert.success(i18n.t("events.event-updated") as string);
}; }
async function deleteTimelineEvent(index: number) { async function deleteTimelineEvent(index: number) {
const { response } = await api.recipes.deleteTimelineEvent(timelineEvents.value[index].id); const { response } = await api.recipes.deleteTimelineEvent(timelineEvents.value[index].id);
@ -240,30 +212,30 @@ export default defineNuxtComponent({
timelineEvents.value.splice(index, 1); timelineEvents.value.splice(index, 1);
alert.success(i18n.t("events.event-deleted") as string); alert.success(i18n.t("events.event-deleted") as string);
}; }
async function getRecipe(recipeId: string): Promise<Recipe | null> { async function getRecipes(recipeIds: string[]): Promise<Recipe[]> {
const { data } = await api.recipes.getOne(recipeId); const qf = "id IN [" + recipeIds.map(id => `"${id}"`).join(", ") + "]";
return data; const { data } = await api.recipes.getAll(1, -1, { queryFilter: qf });
}; return data?.items || [];
}
async function updateRecipes(events: RecipeTimelineEventOut[]) { async function updateRecipes(events: RecipeTimelineEventOut[]) {
const recipePromises: Promise<Recipe | null>[] = []; const recipeIds: string[] = [];
const seenRecipeIds: string[] = [];
events.forEach((event) => { events.forEach((event) => {
if (seenRecipeIds.includes(event.recipeId) || recipes.has(event.recipeId)) { if (recipeIds.includes(event.recipeId) || recipes.has(event.recipeId)) {
return; return;
} }
seenRecipeIds.push(event.recipeId); recipeIds.push(event.recipeId);
recipePromises.push(getRecipe(event.recipeId));
}); });
const results = await Promise.all(recipePromises); const results = await getRecipes(recipeIds);
results.forEach((result) => { results.forEach((result) => {
if (result && result.id) { if (!result?.id) {
recipes.set(result.id, result); return;
} }
recipes.set(result.id, result);
}); });
} }
@ -295,7 +267,7 @@ export default defineNuxtComponent({
// this is set last so Vue knows to re-render // this is set last so Vue knows to re-render
timelineEvents.value.push(...events); timelineEvents.value.push(...events);
}; }
async function initializeTimelineEvents() { async function initializeTimelineEvents() {
loading.value = true; loading.value = true;
@ -347,20 +319,4 @@ export default defineNuxtComponent({
}; };
}, },
); );
return {
deleteTimelineEvent,
filterBadgeCount,
loading,
onScroll,
preferences,
eventTypeFilterState,
recipes,
reverseSort,
toggleEventTypeOption,
timelineEvents,
updateTimelineEvent,
};
},
});
</script> </script>

View file

@ -1,10 +1,10 @@
<template> <template>
<v-tooltip <v-tooltip
bottom location="bottom"
nudge-right="50" nudge-right="50"
:color="buttonStyle ? 'info' : 'secondary'" :color="buttonStyle ? 'info' : 'secondary'"
> >
<template #activator="{ props }"> <template #activator="{ props: activatorProps }">
<v-btn <v-btn
icon icon
:variant="buttonStyle ? 'flat' : undefined" :variant="buttonStyle ? 'flat' : undefined"
@ -12,7 +12,7 @@
size="small" size="small"
:color="buttonStyle ? 'info' : 'secondary'" :color="buttonStyle ? 'info' : 'secondary'"
:fab="buttonStyle" :fab="buttonStyle"
v-bind="{ ...props, ...$attrs }" v-bind="{ ...activatorProps, ...$attrs }"
@click.prevent="toggleTimeline" @click.prevent="toggleTimeline"
> >
<v-icon <v-icon
@ -39,31 +39,24 @@
</v-tooltip> </v-tooltip>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeTimeline from "./RecipeTimeline.vue"; import RecipeTimeline from "./RecipeTimeline.vue";
export default defineNuxtComponent({ interface Props {
components: { RecipeTimeline }, buttonStyle?: boolean;
slug?: string;
recipeName?: string;
}
const props = withDefaults(defineProps<Props>(), {
buttonStyle: false,
slug: "",
recipeName: "",
});
props: {
buttonStyle: {
type: Boolean,
default: false,
},
slug: {
type: String,
default: "",
},
recipeName: {
type: String,
default: "",
},
},
setup(props) {
const i18n = useI18n(); const i18n = useI18n();
const { smAndDown } = useDisplay(); const { smAndDown } = useDisplay();
const showTimeline = ref(false); const showTimeline = ref(false);
function toggleTimeline() { function toggleTimeline() {
showTimeline.value = !showTimeline.value; showTimeline.value = !showTimeline.value;
} }
@ -79,8 +72,4 @@ export default defineNuxtComponent({
queryFilter: `recipe.slug="${props.slug}"`, queryFilter: `recipe.slug="${props.slug}"`,
}; };
}); });
return { showTimeline, timelineAttrs, toggleTimeline };
},
});
</script> </script>

View file

@ -53,6 +53,7 @@
<v-row :class="useMobileFormat ? 'py-3 mx-0' : 'py-3 mx-0'" style="max-width: 100%"> <v-row :class="useMobileFormat ? 'py-3 mx-0' : 'py-3 mx-0'" style="max-width: 100%">
<v-col align-self="center" class="pa-0"> <v-col align-self="center" class="pa-0">
<RecipeCardMobile <RecipeCardMobile
disable-highlight
:vertical="useMobileFormat" :vertical="useMobileFormat"
:name="recipe.name" :name="recipe.name"
:slug="recipe.slug" :slug="recipe.slug"
@ -90,7 +91,7 @@
</v-timeline-item> </v-timeline-item>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue"; import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
import { useStaticRoutes } from "~/composables/api"; import { useStaticRoutes } from "~/composables/api";
@ -99,30 +100,26 @@ import type { Recipe, RecipeTimelineEventOut } from "~/lib/api/types/recipe";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue"; import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
import SafeMarkdown from "~/components/global/SafeMarkdown.vue"; import SafeMarkdown from "~/components/global/SafeMarkdown.vue";
export default defineNuxtComponent({ interface Props {
components: { RecipeCardMobile, RecipeTimelineContextMenu, UserAvatar, SafeMarkdown }, event: RecipeTimelineEventOut;
recipe?: Recipe;
showRecipeCards?: boolean;
}
props: { const props = withDefaults(defineProps<Props>(), {
event: { recipe: undefined,
type: Object as () => RecipeTimelineEventOut, showRecipeCards: false,
required: true, });
},
recipe: { defineEmits<{
type: Object as () => Recipe, selected: [];
default: undefined, update: [];
}, delete: [];
showRecipeCards: { }>();
type: Boolean,
default: false,
},
},
emits: ["selected", "update", "delete"],
setup(props) {
const { $vuetify, $globals } = useNuxtApp(); const { $vuetify, $globals } = useNuxtApp();
const { recipeTimelineEventImage } = useStaticRoutes(); const { recipeTimelineEventImage } = useStaticRoutes();
const { eventTypeOptions } = useTimelineEventTypes(); const { eventTypeOptions } = useTimelineEventTypes();
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
const { user: currentUser } = useMealieAuth(); const { user: currentUser } = useMealieAuth();
@ -177,19 +174,6 @@ export default defineNuxtComponent({
return recipeTimelineEventImage(props.event.recipeId, props.event.id); return recipeTimelineEventImage(props.event.recipeId, props.event.id);
}); });
return {
attrs,
groupSlug,
icon,
eventImageUrl,
hideImage,
timelineEvents,
useMobileFormat,
currentUser,
};
},
});
</script> </script>
<style> <style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
v-if="scaledAmount" v-if="yieldDisplay"
class="d-flex align-center" class="d-flex align-center"
> >
<v-row <v-row
@ -18,36 +18,29 @@
<p class="my-0 opacity-80"> <p class="my-0 opacity-80">
<span class="font-weight-bold">{{ $t("recipe.yield") }}</span><br> <span class="font-weight-bold">{{ $t("recipe.yield") }}</span><br>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="scaledAmount" /> {{ text }} <span v-html="yieldDisplay" />
</p> </p>
</v-row> </v-row>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount"; import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
export default defineNuxtComponent({ interface Props {
props: { yieldQuantity?: number;
yieldQuantity: { yieldText?: string;
type: Number, scale?: number;
default: 0, color?: string;
}, }
yield: { const props = withDefaults(defineProps<Props>(), {
type: String, yieldQuantity: 0,
default: "", yieldText: "",
}, scale: 1,
scale: { color: "accent custom-transparent",
type: Number, });
default: 1,
},
color: {
type: String,
default: "accent custom-transparent",
},
},
setup(props) {
function sanitizeHTML(rawHtml: string) { function sanitizeHTML(rawHtml: string) {
return DOMPurify.sanitize(rawHtml, { return DOMPurify.sanitize(rawHtml, {
USE_PROFILES: { html: true }, USE_PROFILES: { html: true },
@ -55,16 +48,19 @@ export default defineNuxtComponent({
}); });
} }
const scaledAmount = computed(() => { const yieldDisplay = computed<string>(() => {
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale); const components: string[] = [];
return scaledAmountDisplay;
});
const text = sanitizeHTML(props.yield);
return { const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
scaledAmount, if (scaledAmountDisplay) {
text, components.push(scaledAmountDisplay);
}; }
},
const text = props.yieldText;
if (text) {
components.push(text);
}
return sanitizeHTML(components.join(" "));
}); });
</script> </script>

View file

@ -40,7 +40,6 @@
v-if="requireAll != undefined" v-if="requireAll != undefined"
v-model="requireAllValue" v-model="requireAllValue"
density="compact" density="compact"
size="small"
hide-details hide-details
class="my-auto" class="my-auto"
color="primary" color="primary"

View file

@ -8,24 +8,26 @@
class="flex-nowrap align-center" class="flex-nowrap align-center"
> >
<v-col :cols="itemLabelCols"> <v-col :cols="itemLabelCols">
<div class="d-flex align-center flex-nowrap">
<v-checkbox <v-checkbox
v-model="listItem.checked" v-model="listItem.checked"
class="mt-0"
color="null"
hide-details hide-details
density="compact" density="compact"
:label="listItem.note!" class="mt-0"
color="null"
@change="$emit('checked', listItem)" @change="$emit('checked', listItem)"
/>
<div
class="ml-2 text-truncate"
:class="listItem.checked ? 'strike-through' : ''"
style="min-width: 0;"
> >
<template #label>
<div :class="listItem.checked ? 'strike-through' : ''">
<RecipeIngredientListItem <RecipeIngredientListItem
:ingredient="listItem" :ingredient="listItem"
:disable-amount="!(listItem.isFood || listItem.quantity !== 1)" :disable-amount="!(listItem.isFood || listItem.quantity !== 1)"
/> />
</div> </div>
</template> </div>
</v-checkbox>
</v-col> </v-col>
<v-spacer /> <v-spacer />
<v-col <v-col
@ -57,7 +59,7 @@
open-delay="200" open-delay="200"
transition="slide-x-reverse-transition" transition="slide-x-reverse-transition"
density="compact" density="compact"
right location="end"
content-class="text-caption" content-class="text-caption"
> >
<template #activator="{ props: tooltipProps }"> <template #activator="{ props: tooltipProps }">

View file

@ -2,7 +2,7 @@
<v-tooltip <v-tooltip
v-if="userId" v-if="userId"
:disabled="!user || !tooltip" :disabled="!user || !tooltip"
right location="end"
> >
<template #activator="{ props }"> <template #activator="{ props }">
<v-avatar <v-avatar

View file

@ -2,7 +2,7 @@
<v-tooltip <v-tooltip
ref="copyToolTip" ref="copyToolTip"
v-model="show" v-model="show"
top location="top"
:open-on-hover="false" :open-on-hover="false"
:open-on-click="true" :open-on-click="true"
close-delay="500" close-delay="500"

View file

@ -48,7 +48,7 @@
open-delay="200" open-delay="200"
transition="slide-y-reverse-transition" transition="slide-y-reverse-transition"
density="compact" density="compact"
bottom location="bottom"
content-class="text-caption" content-class="text-caption"
> >
<template #activator="{ props }"> <template #activator="{ props }">

View file

@ -82,7 +82,7 @@
<BaseButton <BaseButton
v-if="canSubmit" v-if="canSubmit"
type="submit" type="submit"
:disabled="submitDisabled" :disabled="submitDisabled || loading"
@click="submitEvent" @click="submitEvent"
> >
{{ submitText }} {{ submitText }}

View file

@ -22,10 +22,9 @@
<v-card> <v-card>
<v-card-text> <v-card-text>
<v-checkbox <v-checkbox
v-for="itemValue in headers" v-for="itemValue in localHeaders"
:key="itemValue.text + itemValue.show" :key="itemValue.text + itemValue.show"
v-model="filteredHeaders" v-model="itemValue.show"
:value="itemValue.value"
density="compact" density="compact"
flat flat
inset inset
@ -42,7 +41,7 @@
color="info" color="info"
variant="elevated" variant="elevated"
:items="bulkActions" :items="bulkActions"
v-bind="bulkActionListener" v-on="bulkActionListener"
/> />
<slot name="button-row" /> <slot name="button-row" />
</v-card-actions> </v-card-actions>
@ -55,7 +54,7 @@
</div> </div>
<v-data-table <v-data-table
v-model="selected" v-model="selected"
item-key="id" return-object
:headers="activeHeaders" :headers="activeHeaders"
:show-select="bulkActions.length > 0" :show-select="bulkActions.length > 0"
:sort-by="sortBy" :sort-by="sortBy"
@ -172,12 +171,20 @@ export default defineNuxtComponent({
// =========================================================== // ===========================================================
// Reactive Headers // Reactive Headers
// Create a local reactive copy of headers that we can modify
const localHeaders = ref([...props.headers]);
// Watch for changes in props.headers and update local copy
watch(() => props.headers, (newHeaders) => {
localHeaders.value = [...newHeaders];
}, { deep: true });
const filteredHeaders = computed<string[]>(() => { const filteredHeaders = computed<string[]>(() => {
return props.headers.filter(header => header.show).map(header => header.value); return localHeaders.value.filter(header => header.show).map(header => header.value);
}); });
const headersWithoutActions = computed(() => const headersWithoutActions = computed(() =>
props.headers localHeaders.value
.filter(header => filteredHeaders.value.includes(header.value)) .filter(header => filteredHeaders.value.includes(header.value))
.map(header => ({ .map(header => ({
...header, ...header,
@ -214,6 +221,7 @@ export default defineNuxtComponent({
return { return {
sortBy, sortBy,
selected, selected,
localHeaders,
filteredHeaders, filteredHeaders,
headersWithoutActions, headersWithoutActions,
activeHeaders, activeHeaders,

View file

@ -7,13 +7,14 @@
<v-card-text> <v-card-text>
{{ $t("language-dialog.select-description") }} {{ $t("language-dialog.select-description") }}
<v-autocomplete <v-autocomplete
v-model="locale" v-model="selectedLocale"
:items="locales" :items="locales"
item-title="name" item-title="name"
item-value="value"
class="my-3" class="my-3"
hide-details hide-details
variant="outlined" variant="outlined"
offset @update:model-value="onLocaleSelect"
> >
<template #item="{ item, props }"> <template #item="{ item, props }">
<div <div
@ -59,6 +60,14 @@ export default defineNuxtComponent({
}); });
const { locales: LOCALES, locale, i18n } = useLocales(); const { locales: LOCALES, locale, i18n } = useLocales();
const selectedLocale = ref(locale.value);
const onLocaleSelect = (value: string) => {
if (value && locales.some(l => l.value === value)) {
locale.value = value as any;
}
};
watch(locale, () => { watch(locale, () => {
dialog.value = false; // Close dialog when locale changes dialog.value = false; // Close dialog when locale changes
}); });
@ -72,6 +81,8 @@ export default defineNuxtComponent({
i18n, i18n,
locales, locales,
locale, locale,
selectedLocale,
onLocaleSelect,
}; };
}, },
}); });

View file

@ -26,10 +26,10 @@ export default defineComponent({
}, },
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
setup(_, { emit }) { setup(props, { emit }) {
function parseEvent(event: any): object { function parseEvent(event: any): object {
if (!event) { if (!event) {
return {}; return props.modelValue || {};
} }
try { try {
if (event.json) { if (event.json) {
@ -43,11 +43,14 @@ export default defineComponent({
} }
} }
catch { catch {
return {}; return props.modelValue || {};
} }
} }
function onChange(event: any) { function onChange(event: any) {
emit("update:modelValue", parseEvent(event)); const parsed = parseEvent(event);
if (parsed !== props.modelValue) {
emit("update:modelValue", parsed);
}
} }
return { return {
onChange, onChange,

View file

@ -90,6 +90,8 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
} }
async function getRandom(query: RecipeSearchQuery | null = null, queryFilter: string | null = null) { async function getRandom(query: RecipeSearchQuery | null = null, queryFilter: string | null = null) {
query = query || {};
query._searchSeed = query._searchSeed || Date.now().toString();
const { data } = await api.recipes.getAll(1, 1, getParams("random", "desc", null, query, queryFilter)); const { data } = await api.recipes.getAll(1, 1, getParams("random", "desc", null, query, queryFilter));
if (data?.items.length) { if (data?.items.length) {
return data.items[0]; return data.items[0];

View file

@ -1,6 +1,5 @@
import { useStoreActions } from "./partials/use-actions-factory"; import { useStoreActions } from "./partials/use-actions-factory";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
import type { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household"; import type { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household";
import type { RequestResponse } from "~/lib/api/types/non-generated"; import type { RequestResponse } from "~/lib/api/types/non-generated";
import type { Recipe } from "~/lib/api/types/recipe"; import type { Recipe } from "~/lib/api/types/recipe";
@ -68,7 +67,7 @@ export const useGroupRecipeActions = function (
window.open(url, "_blank")?.focus(); window.open(url, "_blank")?.focus();
return; return;
case "post": case "post":
return await api.groupRecipeActions.triggerAction(action.id, recipe.slug || "", useScaledAmount(recipe.recipeServings || 1, recipeScale).scaledAmount); return await api.groupRecipeActions.triggerAction(action.id, recipe.slug || "", recipeScale);
default: default:
break; break;
} }

View file

@ -38,7 +38,7 @@ export const useGroupWebhooks = function () {
loading.value = true; loading.value = true;
const payload = { const payload = {
enabled: false, enabled: true,
name: "New Webhook", name: "New Webhook",
url: "", url: "",
scheduledTime: "00:00", scheduledTime: "00:00",

View file

@ -1,5 +1,5 @@
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import type { GroupBase, GroupSummary } from "~/lib/api/types/user"; import type { GroupBase, GroupInDB, GroupSummary } from "~/lib/api/types/user";
const groupSelfRef = ref<GroupSummary | null>(null); const groupSelfRef = ref<GroupSummary | null>(null);
const loading = ref(false); const loading = ref(false);
@ -45,28 +45,11 @@ export const useGroupSelf = function () {
export const useGroups = function () { export const useGroups = function () {
const api = useUserApi(); const api = useUserApi();
const loading = ref(false); const loading = ref(false);
const groups = ref<GroupInDB[] | null>(null);
function getAllGroups() { async function getAllGroups() {
loading.value = true; loading.value = true;
const asyncKey = String(Date.now()); const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
const { data: groups } = useAsyncData(asyncKey, async () => {
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); ;
if (data) {
return data.items;
}
else {
return null;
}
});
loading.value = false;
return groups;
}
async function refreshAllGroups() {
loading.value = true;
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); ;
if (data) { if (data) {
groups.value = data.items; groups.value = data.items;
@ -78,11 +61,15 @@ export const useGroups = function () {
loading.value = false; loading.value = false;
} }
async function refreshAllGroups() {
await getAllGroups();
}
async function deleteGroup(id: string | number) { async function deleteGroup(id: string | number) {
loading.value = true; loading.value = true;
const { data } = await api.groups.deleteOne(id); const { data } = await api.groups.deleteOne(id);
loading.value = false; loading.value = false;
refreshAllGroups(); await refreshAllGroups();
return data; return data;
} }
@ -93,9 +80,13 @@ export const useGroups = function () {
if (data && groups.value) { if (data && groups.value) {
groups.value.push(data); groups.value.push(data);
} }
loading.value = false;
} }
const groups = getAllGroups(); // Initialize data on first call
if (!groups.value) {
getAllGroups();
}
return { groups, getAllGroups, refreshAllGroups, deleteGroup, createGroup }; return { groups, getAllGroups, refreshAllGroups, deleteGroup, createGroup };
}; };

View file

@ -48,29 +48,12 @@ export const useHouseholdSelf = function () {
export const useAdminHouseholds = function () { export const useAdminHouseholds = function () {
const api = useAdminApi(); const api = useAdminApi();
const loading = ref(false); const loading = ref(false);
const households = ref<HouseholdInDB[] | null>(null);
function getAllHouseholds() { async function getAllHouseholds() {
loading.value = true; loading.value = true;
const asyncKey = String(Date.now());
const { data: households } = useAsyncData(asyncKey, async () => {
const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" }); const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" });
if (data) {
return data.items;
}
else {
return null;
}
});
loading.value = false;
return households;
}
async function refreshAllHouseholds() {
loading.value = true;
const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" }); ;
if (data) { if (data) {
households.value = data.items; households.value = data.items;
} }
@ -81,11 +64,15 @@ export const useAdminHouseholds = function () {
loading.value = false; loading.value = false;
} }
async function refreshAllHouseholds() {
await getAllHouseholds();
}
async function deleteHousehold(id: string | number) { async function deleteHousehold(id: string | number) {
loading.value = true; loading.value = true;
const { data } = await api.households.deleteOne(id); const { data } = await api.households.deleteOne(id);
loading.value = false; loading.value = false;
refreshAllHouseholds(); await refreshAllHouseholds();
return data; return data;
} }
@ -96,9 +83,9 @@ export const useAdminHouseholds = function () {
if (data && households.value) { if (data && households.value) {
households.value.push(data); households.value.push(data);
} }
loading.value = false;
} }
const households = getAllHouseholds();
function useHouseholdsInGroup(groupIdRef: Ref<string>) { function useHouseholdsInGroup(groupIdRef: Ref<string>) {
return computed( return computed(
() => { () => {
@ -109,6 +96,10 @@ export const useAdminHouseholds = function () {
); );
} }
if (!households.value) {
getAllHouseholds();
}
return { return {
households, households,
useHouseholdsInGroup, useHouseholdsInGroup,

View file

@ -3,13 +3,13 @@ export const LOCALES = [
{ {
name: "繁體中文 (Chinese traditional)", name: "繁體中文 (Chinese traditional)",
value: "zh-TW", value: "zh-TW",
progress: 8, progress: 9,
dir: "ltr", dir: "ltr",
}, },
{ {
name: "简体中文 (Chinese simplified)", name: "简体中文 (Chinese simplified)",
value: "zh-CN", value: "zh-CN",
progress: 33, progress: 35,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -33,7 +33,7 @@ export const LOCALES = [
{ {
name: "Svenska (Swedish)", name: "Svenska (Swedish)",
value: "sv-SE", value: "sv-SE",
progress: 39, progress: 50,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -57,7 +57,7 @@ export const LOCALES = [
{ {
name: "Pусский (Russian)", name: "Pусский (Russian)",
value: "ru-RU", value: "ru-RU",
progress: 35, progress: 37,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -75,7 +75,7 @@ export const LOCALES = [
{ {
name: "Português do Brasil (Brazilian Portuguese)", name: "Português do Brasil (Brazilian Portuguese)",
value: "pt-BR", value: "pt-BR",
progress: 34, progress: 36,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -87,13 +87,13 @@ export const LOCALES = [
{ {
name: "Norsk (Norwegian)", name: "Norsk (Norwegian)",
value: "no-NO", value: "no-NO",
progress: 37, progress: 39,
dir: "ltr", dir: "ltr",
}, },
{ {
name: "Nederlands (Dutch)", name: "Nederlands (Dutch)",
value: "nl-NL", value: "nl-NL",
progress: 39, progress: 45,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -123,7 +123,7 @@ export const LOCALES = [
{ {
name: "Italiano (Italian)", name: "Italiano (Italian)",
value: "it-IT", value: "it-IT",
progress: 37, progress: 38,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -135,7 +135,7 @@ export const LOCALES = [
{ {
name: "Magyar (Hungarian)", name: "Magyar (Hungarian)",
value: "hu-HU", value: "hu-HU",
progress: 38, progress: 40,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -147,19 +147,19 @@ export const LOCALES = [
{ {
name: "עברית (Hebrew)", name: "עברית (Hebrew)",
value: "he-IL", value: "he-IL",
progress: 37, progress: 67,
dir: "rtl", dir: "rtl",
}, },
{ {
name: "Galego (Galician)", name: "Galego (Galician)",
value: "gl-ES", value: "gl-ES",
progress: 37, progress: 38,
dir: "ltr", dir: "ltr",
}, },
{ {
name: "Français (French)", name: "Français (French)",
value: "fr-FR", value: "fr-FR",
progress: 38, progress: 49,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -189,7 +189,7 @@ export const LOCALES = [
{ {
name: "Español (Spanish)", name: "Español (Spanish)",
value: "es-ES", value: "es-ES",
progress: 37, progress: 40,
dir: "ltr", dir: "ltr",
}, },
{ {
@ -201,31 +201,31 @@ export const LOCALES = [
{ {
name: "British English", name: "British English",
value: "en-GB", value: "en-GB",
progress: 22, progress: 23,
dir: "ltr", dir: "ltr",
}, },
{ {
name: "Ελληνικά (Greek)", name: "Ελληνικά (Greek)",
value: "el-GR", value: "el-GR",
progress: 37, progress: 38,
dir: "ltr", dir: "ltr",
}, },
{ {
name: "Deutsch (German)", name: "Deutsch (German)",
value: "de-DE", value: "de-DE",
progress: 46, progress: 64,
dir: "ltr", dir: "ltr",
}, },
{ {
name: "Dansk (Danish)", name: "Dansk (Danish)",
value: "da-DK", value: "da-DK",
progress: 37, progress: 38,
dir: "ltr", dir: "ltr",
}, },
{ {
name: "Čeština (Czech)", name: "Čeština (Czech)",
value: "cs-CZ", value: "cs-CZ",
progress: 37, progress: 39,
dir: "ltr", dir: "ltr",
}, },
{ {

View file

@ -3,8 +3,6 @@ import { LOCALES } from "./available-locales";
export const useLocales = () => { export const useLocales = () => {
const i18n = useI18n(); const i18n = useI18n();
const { isRtl } = useRtl();
const { current: vuetifyLocale } = useLocale(); const { current: vuetifyLocale } = useLocale();
const locale = computed<LocaleObject["code"]>({ const locale = computed<LocaleObject["code"]>({
@ -13,18 +11,21 @@ export const useLocales = () => {
i18n.setLocale(value); i18n.setLocale(value);
}, },
}); });
function updateLocale(lc: LocaleObject["code"]) {
vuetifyLocale.value = lc;
}
// auto update vuetify locale // auto update vuetify locale
watch(locale, (lc) => { watch(locale, (lc) => {
vuetifyLocale.value = lc; updateLocale(lc);
});
// auto update rtl
watch(vuetifyLocale, (vl) => {
const currentLocale = LOCALES.find(lc => lc.value === vl);
if (currentLocale) {
isRtl.value = currentLocale.dir === "rtl";
}
}); });
// set initial locale
if (i18n.locale.value) {
updateLocale(i18n.locale.value);
};
return { return {
locale, locale,
locales: LOCALES, locales: LOCALES,

View file

@ -1,47 +1,48 @@
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const datetimeFormats = { const datetimeFormats = {
// CODE_GEN_ID: DATE_LOCALES // CODE_GEN_ID: DATE_LOCALES
"hu-HU": require("./lang/dateTimeFormats/hu-HU.json"), "af-ZA": require("./lang/dateTimeFormats/af-ZA.json"),
"no-NO": require("./lang/dateTimeFormats/no-NO.json"), "ar-SA": require("./lang/dateTimeFormats/ar-SA.json"),
"nl-NL": require("./lang/dateTimeFormats/nl-NL.json"), "bg-BG": require("./lang/dateTimeFormats/bg-BG.json"),
"pl-PL": require("./lang/dateTimeFormats/pl-PL.json"), "ca-ES": require("./lang/dateTimeFormats/ca-ES.json"),
"cs-CZ": require("./lang/dateTimeFormats/cs-CZ.json"),
"da-DK": require("./lang/dateTimeFormats/da-DK.json"), "da-DK": require("./lang/dateTimeFormats/da-DK.json"),
"fr-CA": require("./lang/dateTimeFormats/fr-CA.json"), "de-DE": require("./lang/dateTimeFormats/de-DE.json"),
"el-GR": require("./lang/dateTimeFormats/el-GR.json"),
"en-GB": require("./lang/dateTimeFormats/en-GB.json"),
"en-US": require("./lang/dateTimeFormats/en-US.json"),
"es-ES": require("./lang/dateTimeFormats/es-ES.json"),
"et-EE": require("./lang/dateTimeFormats/et-EE.json"),
"fi-FI": require("./lang/dateTimeFormats/fi-FI.json"),
"fr-BE": require("./lang/dateTimeFormats/fr-BE.json"), "fr-BE": require("./lang/dateTimeFormats/fr-BE.json"),
"fr-CA": require("./lang/dateTimeFormats/fr-CA.json"),
"fr-FR": require("./lang/dateTimeFormats/fr-FR.json"),
"gl-ES": require("./lang/dateTimeFormats/gl-ES.json"),
"he-IL": require("./lang/dateTimeFormats/he-IL.json"),
"hr-HR": require("./lang/dateTimeFormats/hr-HR.json"),
"hu-HU": require("./lang/dateTimeFormats/hu-HU.json"),
"is-IS": require("./lang/dateTimeFormats/is-IS.json"),
"it-IT": require("./lang/dateTimeFormats/it-IT.json"), "it-IT": require("./lang/dateTimeFormats/it-IT.json"),
"ja-JP": require("./lang/dateTimeFormats/ja-JP.json"),
"ko-KR": require("./lang/dateTimeFormats/ko-KR.json"),
"lt-LT": require("./lang/dateTimeFormats/lt-LT.json"),
"lv-LV": require("./lang/dateTimeFormats/lv-LV.json"),
"nl-NL": require("./lang/dateTimeFormats/nl-NL.json"),
"no-NO": require("./lang/dateTimeFormats/no-NO.json"),
"pl-PL": require("./lang/dateTimeFormats/pl-PL.json"),
"pt-BR": require("./lang/dateTimeFormats/pt-BR.json"),
"pt-PT": require("./lang/dateTimeFormats/pt-PT.json"),
"ro-RO": require("./lang/dateTimeFormats/ro-RO.json"),
"ru-RU": require("./lang/dateTimeFormats/ru-RU.json"),
"sk-SK": require("./lang/dateTimeFormats/sk-SK.json"),
"sl-SI": require("./lang/dateTimeFormats/sl-SI.json"), "sl-SI": require("./lang/dateTimeFormats/sl-SI.json"),
"sr-SP": require("./lang/dateTimeFormats/sr-SP.json"), "sr-SP": require("./lang/dateTimeFormats/sr-SP.json"),
"is-IS": require("./lang/dateTimeFormats/is-IS.json"),
"ja-JP": require("./lang/dateTimeFormats/ja-JP.json"),
"fr-FR": require("./lang/dateTimeFormats/fr-FR.json"),
"ca-ES": require("./lang/dateTimeFormats/ca-ES.json"),
"tr-TR": require("./lang/dateTimeFormats/tr-TR.json"),
"fi-FI": require("./lang/dateTimeFormats/fi-FI.json"),
"hr-HR": require("./lang/dateTimeFormats/hr-HR.json"),
"pt-BR": require("./lang/dateTimeFormats/pt-BR.json"),
"sk-SK": require("./lang/dateTimeFormats/sk-SK.json"),
"zh-CN": require("./lang/dateTimeFormats/zh-CN.json"),
"pt-PT": require("./lang/dateTimeFormats/pt-PT.json"),
"en-GB": require("./lang/dateTimeFormats/en-GB.json"),
"ro-RO": require("./lang/dateTimeFormats/ro-RO.json"),
"cs-CZ": require("./lang/dateTimeFormats/cs-CZ.json"),
"en-US": require("./lang/dateTimeFormats/en-US.json"),
"lv-LV": require("./lang/dateTimeFormats/lv-LV.json"),
"ko-KR": require("./lang/dateTimeFormats/ko-KR.json"),
"bg-BG": require("./lang/dateTimeFormats/bg-BG.json"),
"gl-ES": require("./lang/dateTimeFormats/gl-ES.json"),
"de-DE": require("./lang/dateTimeFormats/de-DE.json"),
"lt-LT": require("./lang/dateTimeFormats/lt-LT.json"),
"ru-RU": require("./lang/dateTimeFormats/ru-RU.json"),
"he-IL": require("./lang/dateTimeFormats/he-IL.json"),
"el-GR": require("./lang/dateTimeFormats/el-GR.json"),
"zh-TW": require("./lang/dateTimeFormats/zh-TW.json"),
"af-ZA": require("./lang/dateTimeFormats/af-ZA.json"),
"es-ES": require("./lang/dateTimeFormats/es-ES.json"),
"sv-SE": require("./lang/dateTimeFormats/sv-SE.json"), "sv-SE": require("./lang/dateTimeFormats/sv-SE.json"),
"ar-SA": require("./lang/dateTimeFormats/ar-SA.json"), "tr-TR": require("./lang/dateTimeFormats/tr-TR.json"),
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
"uk-UA": require("./lang/dateTimeFormats/uk-UA.json"), "uk-UA": require("./lang/dateTimeFormats/uk-UA.json"),
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
"zh-CN": require("./lang/dateTimeFormats/zh-CN.json"),
"zh-TW": require("./lang/dateTimeFormats/zh-TW.json"),
// END: DATE_LOCALES // END: DATE_LOCALES
}; };

View file

@ -0,0 +1,21 @@
{
"short": {
"month": "short",
"day": "numeric",
"weekday": "long"
},
"medium": {
"month": "long",
"day": "numeric",
"weekday": "long",
"year": "numeric"
},
"long": {
"year": "numeric",
"month": "long",
"day": "numeric",
"weekday": "long",
"hour": "numeric",
"minute": "numeric"
}
}

View file

@ -472,6 +472,7 @@
"comment": "Lewer kommentaar", "comment": "Lewer kommentaar",
"comments": "Kommentaar", "comments": "Kommentaar",
"delete-confirmation": "Is jy seker jy wil hierdie resep uitvee?", "delete-confirmation": "Is jy seker jy wil hierdie resep uitvee?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Verwyder resep", "delete-recipe": "Verwyder resep",
"description": "Beskrywing", "description": "Beskrywing",
"disable-amount": "Skakel bestanddeelhoeveelhede af", "disable-amount": "Skakel bestanddeelhoeveelhede af",
@ -579,6 +580,10 @@
"made-this": "Ek het dit gemaak", "made-this": "Ek het dit gemaak",
"how-did-it-turn-out": "Hoe het dit uitgedraai?", "how-did-it-turn-out": "Hoe het dit uitgedraai?",
"user-made-this": "{user} het dit gemaak", "user-made-this": "{user} het dit gemaak",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Boodskap sleutel", "message-key": "Boodskap sleutel",
"parse": "Verwerk", "parse": "Verwerk",
@ -658,6 +663,8 @@
"no-unit": "No unit", "no-unit": "No unit",
"missing-unit": "Create missing unit: {unit}", "missing-unit": "Create missing unit: {unit}",
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",

View file

@ -472,6 +472,7 @@
"comment": "أضف تعليق ", "comment": "أضف تعليق ",
"comments": "التعليقات", "comments": "التعليقات",
"delete-confirmation": "هل انت متأكد من رغبتك بحذف هذه الوصفة؟", "delete-confirmation": "هل انت متأكد من رغبتك بحذف هذه الوصفة؟",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "حذف الوصفة", "delete-recipe": "حذف الوصفة",
"description": "الوصف", "description": "الوصف",
"disable-amount": "إيقاف إظهار كميات المكونات", "disable-amount": "إيقاف إظهار كميات المكونات",
@ -579,6 +580,10 @@
"made-this": "لقد طبخت هذا", "made-this": "لقد طبخت هذا",
"how-did-it-turn-out": "كيف كانت النتيجة؟", "how-did-it-turn-out": "كيف كانت النتيجة؟",
"user-made-this": "{user} طبخ هذه", "user-made-this": "{user} طبخ هذه",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "مفتاح الرساله", "message-key": "مفتاح الرساله",
"parse": "تحليل", "parse": "تحليل",
@ -658,6 +663,8 @@
"no-unit": "لا توجد وحدة", "no-unit": "لا توجد وحدة",
"missing-unit": "إنشاء وحدة مفقودة: {unit}", "missing-unit": "إنشاء وحدة مفقودة: {unit}",
"missing-food": "إنشاء طعام مفقود: {food}", "missing-food": "إنشاء طعام مفقود: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "لا يوجد طعام" "no-food": "لا يوجد طعام"
}, },
"reset-servings-count": "إعادة تعيين عدد الحصص", "reset-servings-count": "إعادة تعيين عدد الحصص",

View file

@ -472,6 +472,7 @@
"comment": "Коментар", "comment": "Коментар",
"comments": "Коментари", "comments": "Коментари",
"delete-confirmation": "Сигурни ли сте, че желаете да изтриете тази рецепта?", "delete-confirmation": "Сигурни ли сте, че желаете да изтриете тази рецепта?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Изтрий рецептата", "delete-recipe": "Изтрий рецептата",
"description": "Описание", "description": "Описание",
"disable-amount": "Изключи количествата за съставките", "disable-amount": "Изключи количествата за съставките",
@ -579,6 +580,10 @@
"made-this": "Сготвих рецептата", "made-this": "Сготвих рецептата",
"how-did-it-turn-out": "Как се получи?", "how-did-it-turn-out": "Как се получи?",
"user-made-this": "{user} направи това", "user-made-this": "{user} направи това",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Екстрите за рецепти са ключова характеристика на Mealie API. Те Ви позволяват да създавате персонализирани JSON двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.", "api-extras-description": "Екстрите за рецепти са ключова характеристика на Mealie API. Те Ви позволяват да създавате персонализирани JSON двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.",
"message-key": "Ключ на съобщението", "message-key": "Ключ на съобщението",
"parse": "Анализирай", "parse": "Анализирай",
@ -658,6 +663,8 @@
"no-unit": "No unit", "no-unit": "No unit",
"missing-unit": "Create missing unit: {unit}", "missing-unit": "Create missing unit: {unit}",
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",

View file

@ -472,6 +472,7 @@
"comment": "Comentari", "comment": "Comentari",
"comments": "Comentaris", "comments": "Comentaris",
"delete-confirmation": "Estàs segur que vols suprimir-la?", "delete-confirmation": "Estàs segur que vols suprimir-la?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Suprimeix la recepta", "delete-recipe": "Suprimeix la recepta",
"description": "Descripció", "description": "Descripció",
"disable-amount": "Oculta les quantitats", "disable-amount": "Oculta les quantitats",
@ -579,6 +580,10 @@
"made-this": "Ho he fet", "made-this": "Ho he fet",
"how-did-it-turn-out": "Com ha sortit?", "how-did-it-turn-out": "Com ha sortit?",
"user-made-this": "{user} ha fet això", "user-made-this": "{user} ha fet això",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.", "api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.",
"message-key": "Clau del missatge", "message-key": "Clau del missatge",
"parse": "Analitzar", "parse": "Analitzar",
@ -658,6 +663,8 @@
"no-unit": "Sense unitat", "no-unit": "Sense unitat",
"missing-unit": "Crear unitat que manca: {unit}", "missing-unit": "Crear unitat que manca: {unit}",
"missing-food": "Crear menjar que manca: {food}", "missing-food": "Crear menjar que manca: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Sense menjar" "no-food": "Sense menjar"
}, },
"reset-servings-count": "Reiniciar racions servides", "reset-servings-count": "Reiniciar racions servides",

View file

@ -5,7 +5,7 @@
"api-docs": "Dokumentace API", "api-docs": "Dokumentace API",
"api-port": "API port", "api-port": "API port",
"application-mode": "Režim aplikace", "application-mode": "Režim aplikace",
"database-type": "Typ databáze", "database-type": "Database Type",
"database-url": "URL databáze", "database-url": "URL databáze",
"default-group": "Výchozí skupina", "default-group": "Výchozí skupina",
"default-household": "Výchozí domácnost", "default-household": "Výchozí domácnost",
@ -472,6 +472,7 @@
"comment": "Komentář", "comment": "Komentář",
"comments": "Komentáře", "comments": "Komentáře",
"delete-confirmation": "Opravdu chcete smazat tento recept?", "delete-confirmation": "Opravdu chcete smazat tento recept?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Smazat recept", "delete-recipe": "Smazat recept",
"description": "Popis", "description": "Popis",
"disable-amount": "Nezobrazovat množství ingrediencí", "disable-amount": "Nezobrazovat množství ingrediencí",
@ -579,6 +580,10 @@
"made-this": "Toto jsem uvařil", "made-this": "Toto jsem uvařil",
"how-did-it-turn-out": "Jak to dopadlo?", "how-did-it-turn-out": "Jak to dopadlo?",
"user-made-this": "{user} udělal toto", "user-made-this": "{user} udělal toto",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Recepty jsou klíčovým rysem rozhraní pro API Mealie. Umožňují vytvářet vlastní klíče/hodnoty JSON v rámci receptu pro odkazy na aplikace třetích stran. Tyto klíče můžete použít pro poskytnutí informací, například pro aktivaci automatizace nebo vlastních zpráv pro přenos do požadovaného zařízení.", "api-extras-description": "Recepty jsou klíčovým rysem rozhraní pro API Mealie. Umožňují vytvářet vlastní klíče/hodnoty JSON v rámci receptu pro odkazy na aplikace třetích stran. Tyto klíče můžete použít pro poskytnutí informací, například pro aktivaci automatizace nebo vlastních zpráv pro přenos do požadovaného zařízení.",
"message-key": "Klíč zprávy", "message-key": "Klíč zprávy",
"parse": "Analyzovat", "parse": "Analyzovat",
@ -658,13 +663,15 @@
"no-unit": "Žádná jednotka", "no-unit": "Žádná jednotka",
"missing-unit": "Vytvořit chybějící jednotku: {unit}", "missing-unit": "Vytvořit chybějící jednotku: {unit}",
"missing-food": "Vytvořit chybějící jídlo: {food}", "missing-food": "Vytvořit chybějící jídlo: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Žádné jídlo" "no-food": "Žádné jídlo"
}, },
"reset-servings-count": "Resetovat počet porcí", "reset-servings-count": "Resetovat počet porcí",
"not-linked-ingredients": "Další ingredience", "not-linked-ingredients": "Další ingredience",
"upload-another-image": "Upload another image", "upload-another-image": "Nahrát další obrázek",
"upload-images": "Upload images", "upload-images": "Nahrát obrázky",
"upload-more-images": "Upload more images" "upload-more-images": "Nahrát více obrázků"
}, },
"recipe-finder": { "recipe-finder": {
"recipe-finder": "Vyhledávač receptů", "recipe-finder": "Vyhledávač receptů",

View file

@ -472,6 +472,7 @@
"comment": "Kommentar", "comment": "Kommentar",
"comments": "Kommentarer", "comments": "Kommentarer",
"delete-confirmation": "Er du sikker på, du vil slette denne opskrift?", "delete-confirmation": "Er du sikker på, du vil slette denne opskrift?",
"admin-delete-confirmation": "Du er ved at slette en opskrift, som du ikke er ejer af ved at bruge administrator rettigheder. Er du sikker?",
"delete-recipe": "Slet opskrift", "delete-recipe": "Slet opskrift",
"description": "Beskrivelse", "description": "Beskrivelse",
"disable-amount": "Slå ingrediensmængder fra", "disable-amount": "Slå ingrediensmængder fra",
@ -579,6 +580,10 @@
"made-this": "Jeg har lavet denne", "made-this": "Jeg har lavet denne",
"how-did-it-turn-out": "Hvordan blev det?", "how-did-it-turn-out": "Hvordan blev det?",
"user-made-this": "{user} lavede denne", "user-made-this": "{user} lavede denne",
"added-to-timeline": "Tilføjet til tidslinjen",
"failed-to-add-to-timeline": "Kunne ikke tilføje til tidslinjen",
"failed-to-update-recipe": "Kunne ikke opdatere opskrift",
"added-to-timeline-but-failed-to-add-image": "Tilføjet til tidslinjen, men kunne ikke tilføje billede",
"api-extras-description": "Opskrifter ekstra er en central feature i Mealie API. De giver dig mulighed for at oprette brugerdefinerede JSON nøgle / værdi par inden for en opskrift, at henvise til fra 3. parts applikationer. Du kan bruge disse nøgler til at give oplysninger, for eksempel til at udløse automatiseringer eller brugerdefinerede beskeder til at videresende til din ønskede enhed.", "api-extras-description": "Opskrifter ekstra er en central feature i Mealie API. De giver dig mulighed for at oprette brugerdefinerede JSON nøgle / værdi par inden for en opskrift, at henvise til fra 3. parts applikationer. Du kan bruge disse nøgler til at give oplysninger, for eksempel til at udløse automatiseringer eller brugerdefinerede beskeder til at videresende til din ønskede enhed.",
"message-key": "Beskednøgle", "message-key": "Beskednøgle",
"parse": "Behandl data", "parse": "Behandl data",
@ -599,10 +604,10 @@
"create-recipe-from-an-image": "Opret opskrift fra et billede", "create-recipe-from-an-image": "Opret opskrift fra et billede",
"create-recipe-from-an-image-description": "Opret en opskrift ved at overføre et billede af den. Mealie vil forsøge at udtrække teksten fra billedet med AI og oprette en opskrift fra det.", "create-recipe-from-an-image-description": "Opret en opskrift ved at overføre et billede af den. Mealie vil forsøge at udtrække teksten fra billedet med AI og oprette en opskrift fra det.",
"crop-and-rotate-the-image": "Beskær og roter billedet, så kun teksten er synlig, og det vises i den rigtige retning.", "crop-and-rotate-the-image": "Beskær og roter billedet, så kun teksten er synlig, og det vises i den rigtige retning.",
"create-from-images": "Create from Images", "create-from-images": "Opret fra billede",
"should-translate-description": "Oversæt opskriften til mit sprog", "should-translate-description": "Oversæt opskriften til mit sprog",
"please-wait-image-procesing": "Vent venligst, billedet behandles. Dette kan tage lidt tid.", "please-wait-image-procesing": "Vent venligst, billedet behandles. Dette kan tage lidt tid.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.", "please-wait-images-processing": "Vent venligst, billedet behandles. Dette kan tage lidt tid.",
"bulk-url-import": "Import fra flere URL-adresser", "bulk-url-import": "Import fra flere URL-adresser",
"debug-scraper": "Fejlsøg indlæser", "debug-scraper": "Fejlsøg indlæser",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Opret en opskrift ved at angive navnet. Alle opskrifter skal have unikke navne.", "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Opret en opskrift ved at angive navnet. Alle opskrifter skal have unikke navne.",
@ -658,13 +663,15 @@
"no-unit": "Ingen enhed", "no-unit": "Ingen enhed",
"missing-unit": "Opret manglende måleenhed: {unit}", "missing-unit": "Opret manglende måleenhed: {unit}",
"missing-food": "Opret manglende fødevare: {food}", "missing-food": "Opret manglende fødevare: {food}",
"this-unit-could-not-be-parsed-automatically": "Denne enhed kunne ikke fortolkes automatisk",
"this-food-could-not-be-parsed-automatically": "Denne fødevare kunne ikke fortolkes automatisk",
"no-food": "Ingen fødevarer" "no-food": "Ingen fødevarer"
}, },
"reset-servings-count": "Nulstil antal serveringer", "reset-servings-count": "Nulstil antal serveringer",
"not-linked-ingredients": "Yderligere ingredienser", "not-linked-ingredients": "Yderligere ingredienser",
"upload-another-image": "Upload another image", "upload-another-image": "Upload et andet billede",
"upload-images": "Upload images", "upload-images": "Upload billeder",
"upload-more-images": "Upload more images" "upload-more-images": "Upload flere billeder"
}, },
"recipe-finder": { "recipe-finder": {
"recipe-finder": "Opskriftssøger", "recipe-finder": "Opskriftssøger",
@ -724,7 +731,7 @@
"backup-restore": "Backup / gendannelse", "backup-restore": "Backup / gendannelse",
"back-restore-description": "Gendannelse af denne sikkerhedskopi vil overskrive alle de aktuelle data i din database og i datamappen og erstatte dem med indholdet af denne sikkerhedskopi. {cannot-be-undone} Hvis gendannelsen lykkes, vil du blive logget ud.", "back-restore-description": "Gendannelse af denne sikkerhedskopi vil overskrive alle de aktuelle data i din database og i datamappen og erstatte dem med indholdet af denne sikkerhedskopi. {cannot-be-undone} Hvis gendannelsen lykkes, vil du blive logget ud.",
"cannot-be-undone": "Denne handling kan ikke fortrydes - brug med forsigtighed.", "cannot-be-undone": "Denne handling kan ikke fortrydes - brug med forsigtighed.",
"postgresql-note": "If you are using PostgreSQL, please review the {backup-restore-process} prior to restoring.", "postgresql-note": "Hvis du bruger PostgreSQL, så gennemse venligst {backup-restore-process} før du gendanner.",
"backup-restore-process-in-the-documentation": "backup/restoreproces i dokumentationen", "backup-restore-process-in-the-documentation": "backup/restoreproces i dokumentationen",
"irreversible-acknowledgment": "Jeg forstår, at denne handling er irreversibel, destruktiv og kan forårsage tab af data", "irreversible-acknowledgment": "Jeg forstår, at denne handling er irreversibel, destruktiv og kan forårsage tab af data",
"restore-backup": "Gendan sikkerhedskopi" "restore-backup": "Gendan sikkerhedskopi"

View file

@ -472,6 +472,7 @@
"comment": "Kommentar", "comment": "Kommentar",
"comments": "Kommentare", "comments": "Kommentare",
"delete-confirmation": "Bist du dir sicher, dass du dieses Rezept löschen möchtest?", "delete-confirmation": "Bist du dir sicher, dass du dieses Rezept löschen möchtest?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Rezept löschen", "delete-recipe": "Rezept löschen",
"description": "Beschreibung", "description": "Beschreibung",
"disable-amount": "Zutatenmenge deaktivieren", "disable-amount": "Zutatenmenge deaktivieren",
@ -579,6 +580,10 @@
"made-this": "Ich hab's gemacht", "made-this": "Ich hab's gemacht",
"how-did-it-turn-out": "Wie ist es geworden?", "how-did-it-turn-out": "Wie ist es geworden?",
"user-made-this": "{user} hat's gemacht", "user-made-this": "{user} hat's gemacht",
"added-to-timeline": "Zur Zeitleiste hinzugefügt",
"failed-to-add-to-timeline": "Fehler beim Hinzufügen zur Zeitleiste",
"failed-to-update-recipe": "Fehler beim Aktualisieren des Rezepts",
"added-to-timeline-but-failed-to-add-image": "Zur Zeitleiste hinzugefügt, Bild hinzufügen fehlgeschlagen",
"api-extras-description": "Rezepte-Extras sind ein Hauptmerkmal der Mealie API. Sie ermöglichen es dir, benutzerdefinierte JSON Key-Value-Paare zu einem Rezept zu erstellen, um Drittanbieter-Anwendungen zu steuern. Du kannst diese dazu verwenden, um Automatisierungen auszulösen oder benutzerdefinierte Nachrichten an bestimmte Geräte zu senden.", "api-extras-description": "Rezepte-Extras sind ein Hauptmerkmal der Mealie API. Sie ermöglichen es dir, benutzerdefinierte JSON Key-Value-Paare zu einem Rezept zu erstellen, um Drittanbieter-Anwendungen zu steuern. Du kannst diese dazu verwenden, um Automatisierungen auszulösen oder benutzerdefinierte Nachrichten an bestimmte Geräte zu senden.",
"message-key": "Nachrichten-Schlüssel", "message-key": "Nachrichten-Schlüssel",
"parse": "Parsen", "parse": "Parsen",
@ -658,6 +663,8 @@
"no-unit": "Keine Einheit", "no-unit": "Keine Einheit",
"missing-unit": "Fehlende Einheit erstellen: {unit}", "missing-unit": "Fehlende Einheit erstellen: {unit}",
"missing-food": "Fehlendes Lebensmittel erstellen: {food}", "missing-food": "Fehlendes Lebensmittel erstellen: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Kein Lebensmittel" "no-food": "Kein Lebensmittel"
}, },
"reset-servings-count": "Portionen zurücksetzen", "reset-servings-count": "Portionen zurücksetzen",
@ -1005,7 +1012,7 @@
"webhooks-enabled": "Webhooks aktiviert", "webhooks-enabled": "Webhooks aktiviert",
"you-are-not-allowed-to-create-a-user": "Du bist nicht berechtigt, einen Benutzer anzulegen", "you-are-not-allowed-to-create-a-user": "Du bist nicht berechtigt, einen Benutzer anzulegen",
"you-are-not-allowed-to-delete-this-user": "Du bist nicht berechtigt, diesen Benutzer zu entfernen", "you-are-not-allowed-to-delete-this-user": "Du bist nicht berechtigt, diesen Benutzer zu entfernen",
"enable-advanced-content": "Erweiterten Inhalt aktivieren", "enable-advanced-content": "Erweiterte Inhalte aktivieren",
"enable-advanced-content-description": "Aktiviert zusätzliche Funktionen wie Rezept-Skalierung, API-Schlüssel, Webhooks und Datenverwaltung. Keine Sorge, das kann später noch geändert werden.", "enable-advanced-content-description": "Aktiviert zusätzliche Funktionen wie Rezept-Skalierung, API-Schlüssel, Webhooks und Datenverwaltung. Keine Sorge, das kann später noch geändert werden.",
"favorite-recipes": "Favoriten", "favorite-recipes": "Favoriten",
"email-or-username": "E-Mail oder Benutzername", "email-or-username": "E-Mail oder Benutzername",

View file

@ -472,6 +472,7 @@
"comment": "Σχόλιο", "comment": "Σχόλιο",
"comments": "Σχόλια", "comments": "Σχόλια",
"delete-confirmation": "Θέλετε σίγουρα να διαγράψετε αυτή τη συνταγή;", "delete-confirmation": "Θέλετε σίγουρα να διαγράψετε αυτή τη συνταγή;",
"admin-delete-confirmation": "Πρόκειται να διαγράψετε μια συνταγή που δεν είναι δική σας χρησιμοποιώντας δικαιώματα διαχειριστή. Είστε σίγουρος/η;",
"delete-recipe": "Διαγραφή Συνταγής", "delete-recipe": "Διαγραφή Συνταγής",
"description": "Περιγραφή", "description": "Περιγραφή",
"disable-amount": "Απενεργοποίηση Ποσοτήτων Συστατικών", "disable-amount": "Απενεργοποίηση Ποσοτήτων Συστατικών",
@ -538,7 +539,7 @@
"date-format-hint-yyyy-mm-dd": "Μορφή ΕΕΕΕ-ΜΜ-ΗΗ", "date-format-hint-yyyy-mm-dd": "Μορφή ΕΕΕΕ-ΜΜ-ΗΗ",
"add-to-list": "Προσθήκη σε λίστα", "add-to-list": "Προσθήκη σε λίστα",
"add-to-plan": "Προσθήκη σε πρόγραμμα γευμάτων", "add-to-plan": "Προσθήκη σε πρόγραμμα γευμάτων",
"add-to-timeline": "Προσθήκη στο χρονοδιάγραμμα", "add-to-timeline": "Προσθήκη στο χρονολόγιο",
"recipe-added-to-list": "Η συνταγή προστέθηκε στη λίστα", "recipe-added-to-list": "Η συνταγή προστέθηκε στη λίστα",
"recipes-added-to-list": "Οι συνταγές προστέθηκαν στη λίστα", "recipes-added-to-list": "Οι συνταγές προστέθηκαν στη λίστα",
"successfully-added-to-list": "Επιτυχής προσθήκη στη λίστα", "successfully-added-to-list": "Επιτυχής προσθήκη στη λίστα",
@ -570,15 +571,19 @@
"increase-scale-label": "Αύξηση κλίμακας κατά 1", "increase-scale-label": "Αύξηση κλίμακας κατά 1",
"locked": "Κλειδωμένο", "locked": "Κλειδωμένο",
"public-link": "Δημόσιος σύνδεσμος", "public-link": "Δημόσιος σύνδεσμος",
"edit-timeline-event": "Επεξεργασία συμβάντος χρονοδιαγράμματος", "edit-timeline-event": "Επεξεργασία συμβάντος χρονολόγιου",
"timeline": "Χρονοδιάγραμμα", "timeline": "Χρονολόγιο",
"timeline-is-empty": "Δεν υπάρχει τίποτα ακόμα στο χρονοδιάγραμμα. Δοκιμάστε να κάνετε αυτή τη συνταγή!", "timeline-is-empty": "Δεν υπάρχει τίποτα ακόμα στο χρονολόγιο. Δοκιμάστε να κάνετε αυτή τη συνταγή!",
"timeline-no-events-found-try-adjusting-filters": "Δεν βρέθηκαν συμβαντα. Δοκιμάστε να προσαρμόσετε τα φίλτρα αναζήτησης.", "timeline-no-events-found-try-adjusting-filters": "Δεν βρέθηκαν συμβαντα. Δοκιμάστε να προσαρμόσετε τα φίλτρα αναζήτησης.",
"group-global-timeline": "Συνολικό χρονοδιάγραμμα {groupName}", "group-global-timeline": "Συνολικό χρονολόγιο {groupName}",
"open-timeline": "Ανοιγμα χρονοδιαγράμματος", "open-timeline": "Ανοιγμα χρονολόγιου",
"made-this": "Το έφτιαξα", "made-this": "Το έφτιαξα",
"how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;", "how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;",
"user-made-this": "Ο/η {user} το έφτιαξε αυτό", "user-made-this": "Ο/η {user} το έφτιαξε αυτό",
"added-to-timeline": "Προστέθηκε στο χρονολόγιο",
"failed-to-add-to-timeline": "Αποτυχία προσθήκης στο χρονολόγιο",
"failed-to-update-recipe": "Αποτυχία ενημέρωσης συνταγής",
"added-to-timeline-but-failed-to-add-image": "Προστέθηκε στο χρονολόγιο, αλλά απέτυχε η προσθήκη εικόνας",
"api-extras-description": "Τα extras συνταγών αποτελούν βασικό χαρακτηριστικό του Mealie API. Σας επιτρέπουν να δημιουργήσετε προσαρμοσμένα ζεύγη κλειδιού/τιμής JSON μέσα σε μια συνταγή, να παραπέμψετε σε εφαρμογές τρίτων. Μπορείτε να χρησιμοποιήσετε αυτά τα κλειδιά για την παροχή πληροφοριών, για παράδειγμα πυροδότηση αυτοματισμών ή μετάδοση προσαρμοσμένων μηνυμάτων στη συσκευή που επιθυμείτε.", "api-extras-description": "Τα extras συνταγών αποτελούν βασικό χαρακτηριστικό του Mealie API. Σας επιτρέπουν να δημιουργήσετε προσαρμοσμένα ζεύγη κλειδιού/τιμής JSON μέσα σε μια συνταγή, να παραπέμψετε σε εφαρμογές τρίτων. Μπορείτε να χρησιμοποιήσετε αυτά τα κλειδιά για την παροχή πληροφοριών, για παράδειγμα πυροδότηση αυτοματισμών ή μετάδοση προσαρμοσμένων μηνυμάτων στη συσκευή που επιθυμείτε.",
"message-key": "Κλειδί Μηνύματος", "message-key": "Κλειδί Μηνύματος",
"parse": "Ανάλυση", "parse": "Ανάλυση",
@ -658,6 +663,8 @@
"no-unit": "Καμία μονάδα", "no-unit": "Καμία μονάδα",
"missing-unit": "Δημιουργία μονάδας που λείπει: {unit}", "missing-unit": "Δημιουργία μονάδας που λείπει: {unit}",
"missing-food": "Δημιουργία τροφίμου που λείπει: {food}", "missing-food": "Δημιουργία τροφίμου που λείπει: {food}",
"this-unit-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτής της μονάδας",
"this-food-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτού του φαγητού",
"no-food": "Χωρίς Τρόφιμο" "no-food": "Χωρίς Τρόφιμο"
}, },
"reset-servings-count": "Επαναφορά μέτρησης μερίδων", "reset-servings-count": "Επαναφορά μέτρησης μερίδων",

View file

@ -472,6 +472,7 @@
"comment": "Comment", "comment": "Comment",
"comments": "Comments", "comments": "Comments",
"delete-confirmation": "Are you sure you want to delete this recipe?", "delete-confirmation": "Are you sure you want to delete this recipe?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Delete Recipe", "delete-recipe": "Delete Recipe",
"description": "Description", "description": "Description",
"disable-amount": "Disable Ingredient Amounts", "disable-amount": "Disable Ingredient Amounts",
@ -579,6 +580,10 @@
"made-this": "I Made This", "made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?", "how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this", "user-made-this": "{user} made this",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key", "message-key": "Message Key",
"parse": "Parse", "parse": "Parse",
@ -658,6 +663,8 @@
"no-unit": "No unit", "no-unit": "No unit",
"missing-unit": "Create missing unit: {unit}", "missing-unit": "Create missing unit: {unit}",
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",

View file

@ -472,6 +472,7 @@
"comment": "Comment", "comment": "Comment",
"comments": "Comments", "comments": "Comments",
"delete-confirmation": "Are you sure you want to delete this recipe?", "delete-confirmation": "Are you sure you want to delete this recipe?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Delete Recipe", "delete-recipe": "Delete Recipe",
"description": "Description", "description": "Description",
"disable-amount": "Disable Ingredient Amounts", "disable-amount": "Disable Ingredient Amounts",
@ -579,6 +580,10 @@
"made-this": "I Made This", "made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?", "how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this", "user-made-this": "{user} made this",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key", "message-key": "Message Key",
"parse": "Parse", "parse": "Parse",
@ -658,6 +663,8 @@
"no-unit": "No unit", "no-unit": "No unit",
"missing-unit": "Create missing unit: {unit}", "missing-unit": "Create missing unit: {unit}",
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",

View file

@ -2,8 +2,8 @@
"about": { "about": {
"about": "Acerca de", "about": "Acerca de",
"about-mealie": "Acerca de Mealie", "about-mealie": "Acerca de Mealie",
"api-docs": "Documentación API", "api-docs": "Documentación de API",
"api-port": "Puerto API", "api-port": "Puerto de API",
"application-mode": "Modo de Aplicación", "application-mode": "Modo de Aplicación",
"database-type": "Tipo de base de datos", "database-type": "Tipo de base de datos",
"database-url": "URL de base de datos", "database-url": "URL de base de datos",
@ -472,6 +472,7 @@
"comment": "Comentario", "comment": "Comentario",
"comments": "Comentarios", "comments": "Comentarios",
"delete-confirmation": "¿Estás seguro de eliminar esta receta?", "delete-confirmation": "¿Estás seguro de eliminar esta receta?",
"admin-delete-confirmation": "Estás a punto de eliminar una receta que no es tuya usando permisos de administrador. ¿Estás seguro?",
"delete-recipe": "Borrar receta", "delete-recipe": "Borrar receta",
"description": "Descripción", "description": "Descripción",
"disable-amount": "Desactivar cantidades de ingredientes", "disable-amount": "Desactivar cantidades de ingredientes",
@ -579,6 +580,10 @@
"made-this": "Lo hice", "made-this": "Lo hice",
"how-did-it-turn-out": "¿Cómo resultó esto?", "how-did-it-turn-out": "¿Cómo resultó esto?",
"user-made-this": "{user} hizo esto", "user-made-this": "{user} hizo esto",
"added-to-timeline": "Añadido a la línea de tiempo",
"failed-to-add-to-timeline": "No se pudo agregar a la línea de tiempo",
"failed-to-update-recipe": "Error al actualizar la receta",
"added-to-timeline-but-failed-to-add-image": "Añadido a la línea de tiempo, pero no se pudo agregar la imagen",
"api-extras-description": "Los extras de las recetas son una característica clave de la API de Mealie. Permiten crear pares json clave/valor personalizados dentro de una receta para acceder desde aplicaciones de terceros. Puede utilizar estas claves para almacenar información, para activar la automatización o mensajes personalizados para transmitir al dispositivo deseado.", "api-extras-description": "Los extras de las recetas son una característica clave de la API de Mealie. Permiten crear pares json clave/valor personalizados dentro de una receta para acceder desde aplicaciones de terceros. Puede utilizar estas claves para almacenar información, para activar la automatización o mensajes personalizados para transmitir al dispositivo deseado.",
"message-key": "Clave de mensaje", "message-key": "Clave de mensaje",
"parse": "Analizar", "parse": "Analizar",
@ -599,10 +604,10 @@
"create-recipe-from-an-image": "Crear receta a partir de una imagen", "create-recipe-from-an-image": "Crear receta a partir de una imagen",
"create-recipe-from-an-image-description": "Crea una receta cargando una imagen de ella. Mealie intentará extraer el texto de la imagen usando IA y crear una receta de ella.", "create-recipe-from-an-image-description": "Crea una receta cargando una imagen de ella. Mealie intentará extraer el texto de la imagen usando IA y crear una receta de ella.",
"crop-and-rotate-the-image": "Recortar y rotar la imagen de manera que sólo el texto sea visible, y esté en la orientación correcta.", "crop-and-rotate-the-image": "Recortar y rotar la imagen de manera que sólo el texto sea visible, y esté en la orientación correcta.",
"create-from-images": "Create from Images", "create-from-images": "Crear a partir de imágenes",
"should-translate-description": "Traducir la receta a mi idioma", "should-translate-description": "Traducir la receta a mi idioma",
"please-wait-image-procesing": "Por favor, espere, la imagen se está procesando. Esto puede tardar un tiempo.", "please-wait-image-procesing": "Por favor, espere, la imagen se está procesando. Esto puede tardar un tiempo.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.", "please-wait-images-processing": "Por favor, espere, las imágenes se están procesando. Esto puede tardar algún tiempo.",
"bulk-url-import": "Importación masiva desde URL", "bulk-url-import": "Importación masiva desde URL",
"debug-scraper": "Depurar analizador", "debug-scraper": "Depurar analizador",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crear una receta proporcionando el nombre. Todas las recetas deben tener nombres únicos.", "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crear una receta proporcionando el nombre. Todas las recetas deben tener nombres únicos.",
@ -658,13 +663,15 @@
"no-unit": "Sin unidad", "no-unit": "Sin unidad",
"missing-unit": "Crear unidad faltante: {unit}", "missing-unit": "Crear unidad faltante: {unit}",
"missing-food": "Crear comida faltante: {food}", "missing-food": "Crear comida faltante: {food}",
"this-unit-could-not-be-parsed-automatically": "Esta unidad no pudo ser procesada automáticamente",
"this-food-could-not-be-parsed-automatically": "Esta comida no pudo ser procesada automáticamente",
"no-food": "Sin Comida" "no-food": "Sin Comida"
}, },
"reset-servings-count": "Restablecer contador de porciones", "reset-servings-count": "Restablecer contador de porciones",
"not-linked-ingredients": "Ingredientes adicionales", "not-linked-ingredients": "Ingredientes adicionales",
"upload-another-image": "Upload another image", "upload-another-image": "Subir otra imagen",
"upload-images": "Upload images", "upload-images": "Subir imágenes",
"upload-more-images": "Upload more images" "upload-more-images": "Subir más imágenes"
}, },
"recipe-finder": { "recipe-finder": {
"recipe-finder": "Buscador de recetas", "recipe-finder": "Buscador de recetas",

View file

@ -472,6 +472,7 @@
"comment": "Kommentaar", "comment": "Kommentaar",
"comments": "Kommentaarid", "comments": "Kommentaarid",
"delete-confirmation": "Kas sa oled kindel, et tahad seda retsepti kustutada?", "delete-confirmation": "Kas sa oled kindel, et tahad seda retsepti kustutada?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Kustuta retsept", "delete-recipe": "Kustuta retsept",
"description": "Kirjeldus", "description": "Kirjeldus",
"disable-amount": "Deaktiveeri koostisosade kogused", "disable-amount": "Deaktiveeri koostisosade kogused",
@ -579,6 +580,10 @@
"made-this": "Olen seda valmistanud", "made-this": "Olen seda valmistanud",
"how-did-it-turn-out": "Kuidas tuli see välja?", "how-did-it-turn-out": "Kuidas tuli see välja?",
"user-made-this": "{user} on seda valmistanud", "user-made-this": "{user} on seda valmistanud",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Retsepti väljavõtted on Meali API oluline funktsioon. Neid saab kasutada kohandatud JSON-võtme/väärtuse paaride loomiseks retseptis, et viidata kolmandate osapoolte rakendustele. Neid klahve saab kasutada teabe edastamiseks, näiteks automaatse toimingu või kohandatud sõnumi käivitamiseks teie valitud seadmele.", "api-extras-description": "Retsepti väljavõtted on Meali API oluline funktsioon. Neid saab kasutada kohandatud JSON-võtme/väärtuse paaride loomiseks retseptis, et viidata kolmandate osapoolte rakendustele. Neid klahve saab kasutada teabe edastamiseks, näiteks automaatse toimingu või kohandatud sõnumi käivitamiseks teie valitud seadmele.",
"message-key": "Sõnumi võti", "message-key": "Sõnumi võti",
"parse": "Analüüsi", "parse": "Analüüsi",
@ -658,6 +663,8 @@
"no-unit": "Ilma ühikuta", "no-unit": "Ilma ühikuta",
"missing-unit": "Loo puuduv ühik: {unit}", "missing-unit": "Loo puuduv ühik: {unit}",
"missing-food": "Loo puuduv toit: {food}", "missing-food": "Loo puuduv toit: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Toit puudub" "no-food": "Toit puudub"
}, },
"reset-servings-count": "Lähtesta portsionite arv", "reset-servings-count": "Lähtesta portsionite arv",

View file

@ -472,6 +472,7 @@
"comment": "Kommentti", "comment": "Kommentti",
"comments": "Kommentit", "comments": "Kommentit",
"delete-confirmation": "Haluatko varmasti poistaa reseptin?", "delete-confirmation": "Haluatko varmasti poistaa reseptin?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Poista resepti", "delete-recipe": "Poista resepti",
"description": "Kuvaus", "description": "Kuvaus",
"disable-amount": "Poista ainesosien määrät käytöstä", "disable-amount": "Poista ainesosien määrät käytöstä",
@ -579,6 +580,10 @@
"made-this": "Tein tämän", "made-this": "Tein tämän",
"how-did-it-turn-out": "Miten se onnistui?", "how-did-it-turn-out": "Miten se onnistui?",
"user-made-this": "{user} teki tämän", "user-made-this": "{user} teki tämän",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Reseptiekstrat ovat Mealien API:n tärkeä ominaisuus. Niiden avulla voidaan luoda mukautettuja JSON-avain/arvo-pareja reseptin sisällä viitaten kolmannen osapuolen sovelluksiin. Näitä avaimia voi käyttää tiedon antamiseksi, esimerkiksi automaattisen toiminnon tai mukautetun viestin käynnistämiseksi haluamaasi laitteeseen.", "api-extras-description": "Reseptiekstrat ovat Mealien API:n tärkeä ominaisuus. Niiden avulla voidaan luoda mukautettuja JSON-avain/arvo-pareja reseptin sisällä viitaten kolmannen osapuolen sovelluksiin. Näitä avaimia voi käyttää tiedon antamiseksi, esimerkiksi automaattisen toiminnon tai mukautetun viestin käynnistämiseksi haluamaasi laitteeseen.",
"message-key": "Viestiavain", "message-key": "Viestiavain",
"parse": "Jäsennä", "parse": "Jäsennä",
@ -658,6 +663,8 @@
"no-unit": "Ei yksikköä", "no-unit": "Ei yksikköä",
"missing-unit": "Luo puuttuva yksikkö: {unit}", "missing-unit": "Luo puuttuva yksikkö: {unit}",
"missing-food": "Luo puuttuva ruoka: {food}", "missing-food": "Luo puuttuva ruoka: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Ei ruokaa" "no-food": "Ei ruokaa"
}, },
"reset-servings-count": "Palauta Annoksien Määrä", "reset-servings-count": "Palauta Annoksien Määrä",

View file

@ -472,6 +472,7 @@
"comment": "Commentaire", "comment": "Commentaire",
"comments": "Commentaires", "comments": "Commentaires",
"delete-confirmation": "Voulez-vous vraiment supprimer cette recette?", "delete-confirmation": "Voulez-vous vraiment supprimer cette recette?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Supprimer la recette", "delete-recipe": "Supprimer la recette",
"description": "Description", "description": "Description",
"disable-amount": "Désactiver les quantités des ingrédients", "disable-amount": "Désactiver les quantités des ingrédients",
@ -579,6 +580,10 @@
"made-this": "Je lai cuisiné", "made-this": "Je lai cuisiné",
"how-did-it-turn-out": "Cétait bon?", "how-did-it-turn-out": "Cétait bon?",
"user-made-this": "{user} la cuisiné", "user-made-this": "{user} la cuisiné",
"added-to-timeline": "Ajouté à lhistorique",
"failed-to-add-to-timeline": "Ajout dans lhistorique en échec",
"failed-to-update-recipe": "Impossible de mettre à jour la recette",
"added-to-timeline-but-failed-to-add-image": "Ajouté à lhistorique, mais impossible dajouter limage",
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.", "api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.",
"message-key": "Clé de message", "message-key": "Clé de message",
"parse": "Analyser", "parse": "Analyser",
@ -599,10 +604,10 @@
"create-recipe-from-an-image": "Créer une recette à partir dune image", "create-recipe-from-an-image": "Créer une recette à partir dune image",
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera lIA pour tenter dextraire le texte et de créer une recette.", "create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera lIA pour tenter dextraire le texte et de créer une recette.",
"crop-and-rotate-the-image": "Rogner et pivoter limage pour que seul le texte soit visible, et quil soit dans la bonne orientation.", "crop-and-rotate-the-image": "Rogner et pivoter limage pour que seul le texte soit visible, et quil soit dans la bonne orientation.",
"create-from-images": "Create from Images", "create-from-images": "Créer à partir dimages",
"should-translate-description": "Traduire la recette dans ma langue", "should-translate-description": "Traduire la recette dans ma langue",
"please-wait-image-procesing": "Veuillez patienter, limage est en cours de traitement. Cela peut prendre du temps.", "please-wait-image-procesing": "Veuillez patienter, limage est en cours de traitement. Cela peut prendre du temps.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.", "please-wait-images-processing": "Veuillez patienter, les images sont en cours de traitement. Cela peut prendre un certain temps.",
"bulk-url-import": "Importation en masse d'URL", "bulk-url-import": "Importation en masse d'URL",
"debug-scraper": "Déboguer le récupérateur", "debug-scraper": "Déboguer le récupérateur",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Créer une recette en fournissant le nom. Toutes les recettes doivent avoir des noms uniques.", "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Créer une recette en fournissant le nom. Toutes les recettes doivent avoir des noms uniques.",
@ -658,13 +663,15 @@
"no-unit": "Pas d'unité", "no-unit": "Pas d'unité",
"missing-unit": "Créer une unité manquante : {unit}", "missing-unit": "Créer une unité manquante : {unit}",
"missing-food": "Créer un aliment manquant : {food}", "missing-food": "Créer un aliment manquant : {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Aucun aliment" "no-food": "Aucun aliment"
}, },
"reset-servings-count": "Réinitialiser le nombre de portions", "reset-servings-count": "Réinitialiser le nombre de portions",
"not-linked-ingredients": "Ingrédients supplémentaires", "not-linked-ingredients": "Ingrédients supplémentaires",
"upload-another-image": "Upload another image", "upload-another-image": "Télécharger une autre image",
"upload-images": "Upload images", "upload-images": "Télécharger des images",
"upload-more-images": "Upload more images" "upload-more-images": "Télécharger d'autres images"
}, },
"recipe-finder": { "recipe-finder": {
"recipe-finder": "Recherche de recette", "recipe-finder": "Recherche de recette",

View file

@ -472,6 +472,7 @@
"comment": "Commentaire", "comment": "Commentaire",
"comments": "Commentaires", "comments": "Commentaires",
"delete-confirmation": "Êtes-vous sûr(e) de vouloir supprimer cette recette?", "delete-confirmation": "Êtes-vous sûr(e) de vouloir supprimer cette recette?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Supprimer la recette", "delete-recipe": "Supprimer la recette",
"description": "Description", "description": "Description",
"disable-amount": "Désactiver les quantités d'ingrédients", "disable-amount": "Désactiver les quantités d'ingrédients",
@ -579,6 +580,10 @@
"made-this": "Je lai cuisiné", "made-this": "Je lai cuisiné",
"how-did-it-turn-out": "Cétait bon?", "how-did-it-turn-out": "Cétait bon?",
"user-made-this": "{user} la cuisiné", "user-made-this": "{user} la cuisiné",
"added-to-timeline": "Ajouté à lhistorique",
"failed-to-add-to-timeline": "Ajout dans lhistorique en échec",
"failed-to-update-recipe": "Impossible de mettre à jour la recette",
"added-to-timeline-but-failed-to-add-image": "Ajouté à lhistorique, mais impossible dajouter limage",
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.", "api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.",
"message-key": "Clé de message", "message-key": "Clé de message",
"parse": "Analyser", "parse": "Analyser",
@ -599,10 +604,10 @@
"create-recipe-from-an-image": "Créer une recette à partir dune image", "create-recipe-from-an-image": "Créer une recette à partir dune image",
"create-recipe-from-an-image-description": "Créez une recette en téléversant une image de celle-ci. Mealie utilisera lIA pour tenter dextraire le texte et de créer une recette.", "create-recipe-from-an-image-description": "Créez une recette en téléversant une image de celle-ci. Mealie utilisera lIA pour tenter dextraire le texte et de créer une recette.",
"crop-and-rotate-the-image": "Rogner et pivoter limage pour que seul le texte soit visible et quil soit dans la bonne orientation.", "crop-and-rotate-the-image": "Rogner et pivoter limage pour que seul le texte soit visible et quil soit dans la bonne orientation.",
"create-from-images": "Create from Images", "create-from-images": "Créer à partir dimages",
"should-translate-description": "Traduire la recette dans ma langue", "should-translate-description": "Traduire la recette dans ma langue",
"please-wait-image-procesing": "Veuillez patienter, l'image est en cours de traitement. Cela peut prendre un certain temps.", "please-wait-image-procesing": "Veuillez patienter, l'image est en cours de traitement. Cela peut prendre un certain temps.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.", "please-wait-images-processing": "Veuillez patienter, les images sont en cours de traitement. Cela peut prendre un certain temps.",
"bulk-url-import": "Importation en masse d'URL", "bulk-url-import": "Importation en masse d'URL",
"debug-scraper": "Déboguer le récupérateur", "debug-scraper": "Déboguer le récupérateur",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Créer une recette en fournissant le nom. Toutes les recettes doivent avoir des noms uniques.", "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Créer une recette en fournissant le nom. Toutes les recettes doivent avoir des noms uniques.",
@ -658,13 +663,15 @@
"no-unit": "Pas d'unité", "no-unit": "Pas d'unité",
"missing-unit": "Créer une unité manquante : {unit}", "missing-unit": "Créer une unité manquante : {unit}",
"missing-food": "Créer un aliment manquant : {food}", "missing-food": "Créer un aliment manquant : {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Aucun aliment" "no-food": "Aucun aliment"
}, },
"reset-servings-count": "Réinitialiser le nombre de portions", "reset-servings-count": "Réinitialiser le nombre de portions",
"not-linked-ingredients": "Ingrédients supplémentaires", "not-linked-ingredients": "Ingrédients supplémentaires",
"upload-another-image": "Upload another image", "upload-another-image": "Télécharger une autre image",
"upload-images": "Upload images", "upload-images": "Télécharger des images",
"upload-more-images": "Upload more images" "upload-more-images": "Télécharger d'autres images"
}, },
"recipe-finder": { "recipe-finder": {
"recipe-finder": "Recherche de recette", "recipe-finder": "Recherche de recette",

View file

@ -19,7 +19,7 @@
"log-lines": "Lignes de log", "log-lines": "Lignes de log",
"not-demo": "Non", "not-demo": "Non",
"portfolio": "Portfolio", "portfolio": "Portfolio",
"production": "Réalisation", "production": "Production",
"support": "Soutenir", "support": "Soutenir",
"version": "Version", "version": "Version",
"unknown-version": "inconnu", "unknown-version": "inconnu",
@ -111,7 +111,7 @@
"friday": "Vendredi", "friday": "Vendredi",
"general": "Général", "general": "Général",
"get": "Envoyer", "get": "Envoyer",
"home": "Page daccueil", "home": "Accueil",
"image": "Image", "image": "Image",
"image-upload-failed": "Le téléchargement de limage a échoué", "image-upload-failed": "Le téléchargement de limage a échoué",
"import": "Importer", "import": "Importer",
@ -472,6 +472,7 @@
"comment": "Commentaire", "comment": "Commentaire",
"comments": "Commentaires", "comments": "Commentaires",
"delete-confirmation": "Voulez-vous vraiment supprimer cette recette?", "delete-confirmation": "Voulez-vous vraiment supprimer cette recette?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Supprimer la recette", "delete-recipe": "Supprimer la recette",
"description": "Description", "description": "Description",
"disable-amount": "Désactiver les quantités des ingrédients", "disable-amount": "Désactiver les quantités des ingrédients",
@ -579,6 +580,10 @@
"made-this": "Je lai cuisiné", "made-this": "Je lai cuisiné",
"how-did-it-turn-out": "Cétait bon?", "how-did-it-turn-out": "Cétait bon?",
"user-made-this": "{user} la cuisiné", "user-made-this": "{user} la cuisiné",
"added-to-timeline": "Ajouté à lhistorique",
"failed-to-add-to-timeline": "Ajout dans lhistorique en échec",
"failed-to-update-recipe": "Impossible de mettre à jour la recette",
"added-to-timeline-but-failed-to-add-image": "Ajouté à lhistorique, mais impossible dajouter limage",
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.", "api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.",
"message-key": "Clé de message", "message-key": "Clé de message",
"parse": "Analyser", "parse": "Analyser",
@ -599,10 +604,10 @@
"create-recipe-from-an-image": "Créer une recette à partir dune image", "create-recipe-from-an-image": "Créer une recette à partir dune image",
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera lIA pour tenter dextraire le texte et de créer une recette.", "create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera lIA pour tenter dextraire le texte et de créer une recette.",
"crop-and-rotate-the-image": "Rogner et pivoter limage pour que seul le texte soit visible, et quil soit dans la bonne orientation.", "crop-and-rotate-the-image": "Rogner et pivoter limage pour que seul le texte soit visible, et quil soit dans la bonne orientation.",
"create-from-images": "Create from Images", "create-from-images": "Créer à partir dimages",
"should-translate-description": "Traduire la recette dans ma langue", "should-translate-description": "Traduire la recette dans ma langue",
"please-wait-image-procesing": "Veuillez patienter, limage est en cours de traitement. Cela peut prendre du temps.", "please-wait-image-procesing": "Veuillez patienter, limage est en cours de traitement. Cela peut prendre du temps.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.", "please-wait-images-processing": "Veuillez patienter, les images sont en cours de traitement. Cela peut prendre un certain temps.",
"bulk-url-import": "Importation en masse d'URL", "bulk-url-import": "Importation en masse d'URL",
"debug-scraper": "Déboguer le récupérateur", "debug-scraper": "Déboguer le récupérateur",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Créer une recette en fournissant le nom. Toutes les recettes doivent avoir des noms uniques.", "create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Créer une recette en fournissant le nom. Toutes les recettes doivent avoir des noms uniques.",
@ -658,13 +663,15 @@
"no-unit": "Pas d'unité", "no-unit": "Pas d'unité",
"missing-unit": "Créer une unité manquante : {unit}", "missing-unit": "Créer une unité manquante : {unit}",
"missing-food": "Créer un aliment manquant : {food}", "missing-food": "Créer un aliment manquant : {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Aucun aliment" "no-food": "Aucun aliment"
}, },
"reset-servings-count": "Réinitialiser le nombre de portions", "reset-servings-count": "Réinitialiser le nombre de portions",
"not-linked-ingredients": "Ingrédients supplémentaires", "not-linked-ingredients": "Ingrédients supplémentaires",
"upload-another-image": "Upload another image", "upload-another-image": "Télécharger une autre image",
"upload-images": "Upload images", "upload-images": "Télécharger des images",
"upload-more-images": "Upload more images" "upload-more-images": "Télécharger d'autres images"
}, },
"recipe-finder": { "recipe-finder": {
"recipe-finder": "Recherche de recette", "recipe-finder": "Recherche de recette",

View file

@ -472,6 +472,7 @@
"comment": "Comentario", "comment": "Comentario",
"comments": "Comentarios", "comments": "Comentarios",
"delete-confirmation": "Estás seguro de que queres eliminar esta receita?", "delete-confirmation": "Estás seguro de que queres eliminar esta receita?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Eliminar Receita", "delete-recipe": "Eliminar Receita",
"description": "Descrición", "description": "Descrición",
"disable-amount": "Desactivar as Cantidades de Ingredientes", "disable-amount": "Desactivar as Cantidades de Ingredientes",
@ -579,6 +580,10 @@
"made-this": "Eu fixen isto", "made-this": "Eu fixen isto",
"how-did-it-turn-out": "Que tal ficou?", "how-did-it-turn-out": "Que tal ficou?",
"user-made-this": "{user} fixo isto", "user-made-this": "{user} fixo isto",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Os extras de receitas son unha característica clave da API de Mealie. Permítenche crear pares de clave/valor JSON personalizados dentro dunha receita, para facer referencia desde aplicacións de terceiros. Podes usar estas teclas para proporcionar información, por exemplo, para activar automatizacións ou mensaxes personalizadas para transmitir ao dispositivo que desexes.", "api-extras-description": "Os extras de receitas son unha característica clave da API de Mealie. Permítenche crear pares de clave/valor JSON personalizados dentro dunha receita, para facer referencia desde aplicacións de terceiros. Podes usar estas teclas para proporcionar información, por exemplo, para activar automatizacións ou mensaxes personalizadas para transmitir ao dispositivo que desexes.",
"message-key": "Chave de Mensaxen", "message-key": "Chave de Mensaxen",
"parse": "Interpretar", "parse": "Interpretar",
@ -658,6 +663,8 @@
"no-unit": "Sen unidades", "no-unit": "Sen unidades",
"missing-unit": "Crear a unidade que falta: {unit}", "missing-unit": "Crear a unidade que falta: {unit}",
"missing-food": "Crear a comida que falta: {food}", "missing-food": "Crear a comida que falta: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "Sen Comida" "no-food": "Sen Comida"
}, },
"reset-servings-count": "Reiniciar Contador de Porcións", "reset-servings-count": "Reiniciar Contador de Porcións",

View file

@ -45,7 +45,7 @@
"category-filter": "סינון קטגוריות", "category-filter": "סינון קטגוריות",
"category-update-failed": "עדכון קטגוריה נכשל", "category-update-failed": "עדכון קטגוריה נכשל",
"category-updated": "קטגוריה עודכנה", "category-updated": "קטגוריה עודכנה",
"uncategorized-count": "{count} לא קיבלו קטגוריה", "uncategorized-count": "{count} ללא קטגוריה",
"create-a-category": "יצירת קטגוריה", "create-a-category": "יצירת קטגוריה",
"category-name": "שם קטגוריה", "category-name": "שם קטגוריה",
"category": "קטגוריה" "category": "קטגוריה"
@ -472,6 +472,7 @@
"comment": "הערה", "comment": "הערה",
"comments": "הערות", "comments": "הערות",
"delete-confirmation": "למחוק את המתכון הזה?", "delete-confirmation": "למחוק את המתכון הזה?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "מחיקת מתכון", "delete-recipe": "מחיקת מתכון",
"description": "תיאור", "description": "תיאור",
"disable-amount": "ביטול כמויות מרכיבים", "disable-amount": "ביטול כמויות מרכיבים",
@ -579,6 +580,10 @@
"made-this": "הכנתי את זה", "made-this": "הכנתי את זה",
"how-did-it-turn-out": "איך יצא?", "how-did-it-turn-out": "איך יצא?",
"user-made-this": "{user} הכין את זה", "user-made-this": "{user} הכין את זה",
"added-to-timeline": "נוסף לציר הזמן",
"failed-to-add-to-timeline": "כישלון בהוספה לציר הזמן",
"failed-to-update-recipe": "כישלון בעדכון מתכון",
"added-to-timeline-but-failed-to-add-image": "נוסף לציר הזמן עם כישלון בהוספת תמונה",
"api-extras-description": "מתכונים נוספים הם יכולת מפתח של Mealie API. הם מאפשרים ליצור צמדי key/value בצורת JSON על מנת לקרוא אותם בתוכנת צד שלישית. תוכלו להשתמש בצמדים האלה כדי לספק מידע, לדוגמא להפעיל אוטומציות או הודעות מותאמות אישית למכשירים מסויימים.", "api-extras-description": "מתכונים נוספים הם יכולת מפתח של Mealie API. הם מאפשרים ליצור צמדי key/value בצורת JSON על מנת לקרוא אותם בתוכנת צד שלישית. תוכלו להשתמש בצמדים האלה כדי לספק מידע, לדוגמא להפעיל אוטומציות או הודעות מותאמות אישית למכשירים מסויימים.",
"message-key": "מפתח הודעה", "message-key": "מפתח הודעה",
"parse": "ניתוח", "parse": "ניתוח",
@ -658,6 +663,8 @@
"no-unit": "ללא יחידות", "no-unit": "ללא יחידות",
"missing-unit": "יצירת יחידה חסרה: {unit}", "missing-unit": "יצירת יחידה חסרה: {unit}",
"missing-food": "יצירת אוכל חסר: {food}", "missing-food": "יצירת אוכל חסר: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "אין אוכל" "no-food": "אין אוכל"
}, },
"reset-servings-count": "איפוס מספר המנות", "reset-servings-count": "איפוס מספר המנות",

View file

@ -472,6 +472,7 @@
"comment": "Komentar", "comment": "Komentar",
"comments": "Komentari", "comments": "Komentari",
"delete-confirmation": "Jeste li sigurni da želite izbrisati ovaj recept?", "delete-confirmation": "Jeste li sigurni da želite izbrisati ovaj recept?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Obriši Recept", "delete-recipe": "Obriši Recept",
"description": "Opis", "description": "Opis",
"disable-amount": "Onemogući količine sastojaka", "disable-amount": "Onemogući količine sastojaka",
@ -579,6 +580,10 @@
"made-this": "Napravio/la sam ovo", "made-this": "Napravio/la sam ovo",
"how-did-it-turn-out": "Kako je ispalo?", "how-did-it-turn-out": "Kako je ispalo?",
"user-made-this": "{user} je napravio/la ovo", "user-made-this": "{user} je napravio/la ovo",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Ključ poruke", "message-key": "Ključ poruke",
"parse": "Razluči (parsiraj)", "parse": "Razluči (parsiraj)",
@ -658,6 +663,8 @@
"no-unit": "No unit", "no-unit": "No unit",
"missing-unit": "Create missing unit: {unit}", "missing-unit": "Create missing unit: {unit}",
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",

View file

@ -472,6 +472,7 @@
"comment": "Megjegyzés", "comment": "Megjegyzés",
"comments": "Megjegyzések", "comments": "Megjegyzések",
"delete-confirmation": "Biztosan törli ezt a receptet?", "delete-confirmation": "Biztosan törli ezt a receptet?",
"admin-delete-confirmation": "Adminisztrátori jogosultságokkal törölni készül egy receptet, amely nem a tiéd. Biztosan törölni szeretné?",
"delete-recipe": "Recept törlése", "delete-recipe": "Recept törlése",
"description": "Leírás", "description": "Leírás",
"disable-amount": "Hozzávalók mennyiségének letiltása", "disable-amount": "Hozzávalók mennyiségének letiltása",
@ -579,6 +580,10 @@
"made-this": "Elkészítettem ezt", "made-this": "Elkészítettem ezt",
"how-did-it-turn-out": "Hogyan sikerült?", "how-did-it-turn-out": "Hogyan sikerült?",
"user-made-this": "ezt {user} készítette el", "user-made-this": "ezt {user} készítette el",
"added-to-timeline": "Idővonalhoz hozzáadva",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Nem sikerült frissíteni a receptet",
"added-to-timeline-but-failed-to-add-image": "Idővonalhoz hozzáadva, azonban a kép hozzáadása sikertelen",
"api-extras-description": "A receptek extrái a Mealie API egyik legfontosabb szolgáltatása. Lehetővé teszik, hogy egyéni JSON kulcs/érték párokat hozzon létre egy receptben, amelyekre harmadik féltől származó alkalmazásokból hivatkozhat. Ezeket a kulcsokat információszolgáltatásra használhatja, például automatizmusok vagy egyéni üzenetek indítására, amelyeket a kívánt eszközre küldhet.", "api-extras-description": "A receptek extrái a Mealie API egyik legfontosabb szolgáltatása. Lehetővé teszik, hogy egyéni JSON kulcs/érték párokat hozzon létre egy receptben, amelyekre harmadik féltől származó alkalmazásokból hivatkozhat. Ezeket a kulcsokat információszolgáltatásra használhatja, például automatizmusok vagy egyéni üzenetek indítására, amelyeket a kívánt eszközre küldhet.",
"message-key": "Üzenetkulcs", "message-key": "Üzenetkulcs",
"parse": "Előkészítés", "parse": "Előkészítés",
@ -658,6 +663,8 @@
"no-unit": "Mértékegység nélkül", "no-unit": "Mértékegység nélkül",
"missing-unit": "Hiányzó mértékegység létrehozása: {unit}", "missing-unit": "Hiányzó mértékegység létrehozása: {unit}",
"missing-food": "Hiányzó élelmiszer létrehozása: {food}", "missing-food": "Hiányzó élelmiszer létrehozása: {food}",
"this-unit-could-not-be-parsed-automatically": "Ez az egység nem tudja automatikusan értelmezni",
"this-food-could-not-be-parsed-automatically": "Ezt az ételt nem lehetett automatikusan feldolgozni",
"no-food": "Élelmiszer nélküli" "no-food": "Élelmiszer nélküli"
}, },
"reset-servings-count": "Adagok számának visszaállítása", "reset-servings-count": "Adagok számának visszaállítása",

View file

@ -472,6 +472,7 @@
"comment": "Comment", "comment": "Comment",
"comments": "Comments", "comments": "Comments",
"delete-confirmation": "Are you sure you want to delete this recipe?", "delete-confirmation": "Are you sure you want to delete this recipe?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"delete-recipe": "Delete Recipe", "delete-recipe": "Delete Recipe",
"description": "Description", "description": "Description",
"disable-amount": "Disable Ingredient Amounts", "disable-amount": "Disable Ingredient Amounts",
@ -579,6 +580,10 @@
"made-this": "I Made This", "made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?", "how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this", "user-made-this": "{user} made this",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.", "api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key", "message-key": "Message Key",
"parse": "Parse", "parse": "Parse",
@ -658,6 +663,8 @@
"no-unit": "No unit", "no-unit": "No unit",
"missing-unit": "Create missing unit: {unit}", "missing-unit": "Create missing unit: {unit}",
"missing-food": "Create missing food: {food}", "missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"no-food": "No Food" "no-food": "No Food"
}, },
"reset-servings-count": "Reset Servings Count", "reset-servings-count": "Reset Servings Count",

Some files were not shown because too many files have changed in this diff Show more