mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-21 14:03:32 -07:00
Merge branch 'mealie-next' into mealie-next
This commit is contained in:
commit
2b74119300
180 changed files with 14471 additions and 15145 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
name: Build Package
|
||||
uses: ./.github/workflows/build-package.yml
|
||||
with:
|
||||
tag: release
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
|
||||
publish:
|
||||
permissions:
|
||||
|
|
|
@ -12,7 +12,7 @@ repos:
|
|||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.2
|
||||
rev: v0.12.5
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
|
|
@ -179,9 +179,15 @@ def inject_nuxt_values():
|
|||
|
||||
all_langs = []
|
||||
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.sort()
|
||||
all_date_locales.sort()
|
||||
|
||||
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
||||
|
|
|
@ -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:
|
||||
|
||||
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.
|
||||
4. Restart the container
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
|||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
|
|
@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
|||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
|
|
@ -44,33 +44,17 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
<script setup lang="ts">
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { QueryFilterBuilder },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => ReadCookBook,
|
||||
required: true,
|
||||
},
|
||||
actions: {
|
||||
type: Object as () => any,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const modelValue = defineModel<ReadCookBook>({ required: true });
|
||||
const i18n = useI18n();
|
||||
|
||||
const cookbook = toRef(() => props.modelValue);
|
||||
|
||||
const cookbook = toRef(modelValue);
|
||||
function handleInput(value: string | undefined) {
|
||||
cookbook.value.queryFilterString = value || "";
|
||||
emit("update:modelValue", cookbook.value);
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
|
@ -110,12 +94,4 @@ export default defineNuxtComponent({
|
|||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
cookbook,
|
||||
handleInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<v-card-text>
|
||||
<CookbookEditor
|
||||
v-model="editTarget"
|
||||
:actions="actions"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
@ -65,7 +64,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
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 CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardSection, CookbookEditor },
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
|
@ -89,7 +85,6 @@ export default defineNuxtComponent({
|
|||
const { actions } = useCookbookStore();
|
||||
const router = useRouter();
|
||||
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
|
||||
const isOwnHousehold = computed(() => {
|
||||
|
@ -132,23 +127,4 @@ export default defineNuxtComponent({
|
|||
useSeoMeta({
|
||||
title: book?.value?.name || "Cookbook",
|
||||
});
|
||||
|
||||
return {
|
||||
book,
|
||||
slug,
|
||||
tab,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
canEdit,
|
||||
dialogStates,
|
||||
editTarget,
|
||||
handleEditCookbook,
|
||||
editCookbook,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -20,18 +20,14 @@
|
|||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { parseISO, formatDistanceToNow } from "date-fns";
|
||||
import type { GroupDataExport } from "~/lib/api/types/group";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
exports: {
|
||||
type: Array as () => GroupDataExport[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
defineProps<{
|
||||
exports: GroupDataExport[];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const headers = [
|
||||
|
@ -53,12 +49,4 @@ export default defineNuxtComponent({
|
|||
function downloadData(_: any) {
|
||||
console.log("Downloading data...");
|
||||
}
|
||||
|
||||
return {
|
||||
downloadData,
|
||||
headers,
|
||||
getTimeToExpire,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -9,30 +9,10 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
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);
|
||||
},
|
||||
});
|
||||
<script setup lang="ts">
|
||||
import type { ReadGroupPreferences } from "~/lib/api/types/user";
|
||||
|
||||
return {
|
||||
preferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
const preferences = defineModel<ReadGroupPreferences>({ required: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -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>
|
|
@ -18,7 +18,7 @@
|
|||
:open-on-hover="mdAndUp"
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
:class="{ 'rounded-circle': fab }"
|
||||
:size="fab ? 'small' : undefined"
|
||||
|
@ -26,7 +26,7 @@
|
|||
:icon="!fab"
|
||||
variant="text"
|
||||
dark
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
@click.prevent
|
||||
>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||
import type { ShoppingListSummary } from "~/lib/api/types/household";
|
||||
|
@ -64,33 +64,25 @@ export interface ContextMenuItem {
|
|||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
},
|
||||
props: {
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
menuIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
recipes?: Recipe[];
|
||||
menuTop?: boolean;
|
||||
fab?: boolean;
|
||||
color?: string;
|
||||
menuIcon?: string | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipes: () => [],
|
||||
menuTop: true,
|
||||
fab: false,
|
||||
color: "primary",
|
||||
menuIcon: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
[key: string]: [];
|
||||
}>();
|
||||
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
@ -111,6 +103,8 @@ export default defineNuxtComponent({
|
|||
],
|
||||
});
|
||||
|
||||
const { shoppingListDialog, menuItems } = toRefs(state);
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
|
@ -147,18 +141,7 @@ export default defineNuxtComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
context.emit(eventKey);
|
||||
emit(eventKey);
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
contextMenuEventHandler,
|
||||
icon,
|
||||
recipesWithScales,
|
||||
shoppingLists,
|
||||
mdAndUp,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
style="gap: 10px"
|
||||
>
|
||||
<v-select
|
||||
v-model="inputDay"
|
||||
v-model="day"
|
||||
:items="MEAL_DAY_OPTIONS"
|
||||
:label="$t('meal-plan.rule-day')"
|
||||
/>
|
||||
<v-select
|
||||
v-model="inputEntryType"
|
||||
v-model="entryType"
|
||||
:items="MEAL_TYPE_OPTIONS"
|
||||
:label="$t('meal-plan.meal-type')"
|
||||
/>
|
||||
|
@ -19,53 +19,38 @@
|
|||
<div class="mb-5">
|
||||
<QueryFilterBuilder
|
||||
:field-defs="fieldDefs"
|
||||
:initial-query-filter="queryFilter"
|
||||
:initial-query-filter="props.queryFilter"
|
||||
@input="handleQueryFilterInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO: proper pluralization of inputDay -->
|
||||
{{ $t('meal-plan.this-rule-will-apply', {
|
||||
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
|
||||
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType]),
|
||||
dayCriteria: day === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [day]),
|
||||
mealTypeCriteria: entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [entryType]),
|
||||
}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
QueryFilterBuilder,
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
entryType: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
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) {
|
||||
interface Props {
|
||||
queryFilter?: QueryFilterJSON | null;
|
||||
showHelp?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
queryFilter: null,
|
||||
showHelp: false,
|
||||
});
|
||||
|
||||
const day = defineModel<string>("day", { default: "unset" });
|
||||
const entryType = defineModel<string>("entryType", { default: "unset" });
|
||||
const queryFilterString = defineModel<string>("queryFilterString", { default: "" });
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
|
@ -87,36 +72,10 @@ export default defineNuxtComponent({
|
|||
{ 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) {
|
||||
inputQueryFilterString.value = value || "";
|
||||
};
|
||||
console.warn("handleQueryFilterInput called with value:", value);
|
||||
queryFilterString.value = value || "";
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
|
@ -160,16 +119,4 @@ export default defineNuxtComponent({
|
|||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
MEAL_TYPE_OPTIONS,
|
||||
MEAL_DAY_OPTIONS,
|
||||
inputDay,
|
||||
inputEntryType,
|
||||
inputQueryFilterString,
|
||||
handleQueryFilterInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
:label="$t('settings.webhooks.webhook-url')"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-time-picker
|
||||
<v-text-field
|
||||
v-model="scheduledTime"
|
||||
class="elevation-2"
|
||||
ampm-in-title
|
||||
format="ampm"
|
||||
type="time"
|
||||
clearable
|
||||
variant="underlined"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="py-0 justify-end">
|
||||
|
@ -50,19 +50,20 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ReadWebhook } from "~/lib/api/types/household";
|
||||
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
webhook: {
|
||||
type: Object as () => ReadWebhook,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["delete", "save", "test"],
|
||||
setup(props, { emit }) {
|
||||
const props = defineProps<{
|
||||
webhook: ReadWebhook;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [id: string];
|
||||
save: [webhook: ReadWebhook];
|
||||
test: [id: string];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
|
@ -88,14 +89,4 @@ export default defineNuxtComponent({
|
|||
useSeoMeta({
|
||||
title: i18n.t("settings.webhooks.webhooks"),
|
||||
});
|
||||
|
||||
return {
|
||||
webhookCopy,
|
||||
scheduledTime,
|
||||
handleSave,
|
||||
itemUTC,
|
||||
itemLocal,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -41,18 +41,10 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
|
||||
const i18n = useI18n();
|
||||
|
||||
type Preference = {
|
||||
|
@ -124,23 +116,6 @@ export default defineNuxtComponent({
|
|||
value: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
allDays,
|
||||
preferences,
|
||||
recipePreferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
|
|
@ -147,7 +147,7 @@
|
|||
:model-value="field.value"
|
||||
type="number"
|
||||
variant="underlined"
|
||||
@:model-value="setFieldValue(field, index, $event)"
|
||||
@update:model-value="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-else-if="field.type === 'boolean'"
|
||||
|
@ -163,14 +163,14 @@
|
|||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="field.value"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -184,53 +184,53 @@
|
|||
</v-menu>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Category"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Category"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tag"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tag"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tool"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tool"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Food"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Food"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Household"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Household"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<!-- right parenthesis -->
|
||||
|
@ -297,7 +297,7 @@
|
|||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
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 { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
VueDraggable,
|
||||
RecipeOrganizerSelector,
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
fieldDefs: {
|
||||
type: Array as () => FieldDefinition[],
|
||||
required: true,
|
||||
|
@ -321,9 +316,13 @@ export default defineNuxtComponent({
|
|||
type: Object as () => QueryFilterJSON | 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 { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
|
||||
|
@ -337,6 +336,7 @@ export default defineNuxtComponent({
|
|||
datePickers: [] as boolean[],
|
||||
drag: false,
|
||||
});
|
||||
const { showAdvanced, datePickers, drag } = toRefs(state);
|
||||
|
||||
const storeMap = {
|
||||
[Organizer.Category]: useCategoryStore(),
|
||||
|
@ -369,7 +369,7 @@ export default defineNuxtComponent({
|
|||
id: useUid(),
|
||||
});
|
||||
state.datePickers.push(false);
|
||||
};
|
||||
}
|
||||
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
state.datePickers[index] = false;
|
||||
|
@ -419,15 +419,16 @@ export default defineNuxtComponent({
|
|||
fields.value[index].values = values;
|
||||
}
|
||||
|
||||
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) {
|
||||
setFieldValues(field, index, values.map(value => value.id.toString()));
|
||||
fields.value[index].organizers = values;
|
||||
function setFieldOrganizers(field: FieldWithId, index: number, organizers: OrganizerBase[]) {
|
||||
fields.value[index].organizers = organizers;
|
||||
// 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) {
|
||||
fields.value.splice(index, 1);
|
||||
state.datePickers.splice(index, 1);
|
||||
};
|
||||
}
|
||||
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
/* newFields.forEach((field, index) => {
|
||||
|
@ -441,19 +442,17 @@ export default defineNuxtComponent({
|
|||
}
|
||||
state.qfValid = !!qf;
|
||||
|
||||
context.emit("input", qf || undefined);
|
||||
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
emit("input", qf || undefined);
|
||||
emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
}, 500);
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.organizers = [];
|
||||
|
||||
const { store, actions } = storeMap[field.type];
|
||||
if (!store.value.length) {
|
||||
await actions.refresh();
|
||||
|
@ -467,8 +466,9 @@ export default defineNuxtComponent({
|
|||
}
|
||||
return organizer;
|
||||
});
|
||||
|
||||
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||
setOrganizerValues(field, index, field.organizers);
|
||||
return field;
|
||||
}
|
||||
|
||||
function initFieldsError(error = "") {
|
||||
|
@ -482,14 +482,15 @@ export default defineNuxtComponent({
|
|||
}
|
||||
}
|
||||
|
||||
function initializeFields() {
|
||||
async function initializeFields() {
|
||||
if (!props.initialQueryFilter?.parts?.length) {
|
||||
return initFieldsError();
|
||||
};
|
||||
}
|
||||
|
||||
const initFields: FieldWithId[] = [];
|
||||
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);
|
||||
if (!fieldDef) {
|
||||
error = true;
|
||||
|
@ -522,7 +523,7 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
if (isOrganizerType(field.type)) {
|
||||
hydrateOrganizers(field, index);
|
||||
await hydrateOrganizers(field, index);
|
||||
}
|
||||
}
|
||||
else if (field.type === "boolean") {
|
||||
|
@ -553,7 +554,7 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
initFields.push(field);
|
||||
});
|
||||
}
|
||||
|
||||
if (initFields.length && !error) {
|
||||
fields.value = initFields;
|
||||
|
@ -561,14 +562,16 @@ export default defineNuxtComponent({
|
|||
else {
|
||||
initFieldsError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
initializeFields();
|
||||
await initializeFields();
|
||||
}
|
||||
catch (error) {
|
||||
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
||||
}
|
||||
});
|
||||
|
||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<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!" />
|
||||
<div v-if="loggedIn">
|
||||
<v-tooltip v-if="canEdit" bottom color="info">
|
||||
<template #activator="{ props }">
|
||||
<v-tooltip v-if="canEdit" location="bottom" color="info">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="flat"
|
||||
|
@ -26,7 +26,7 @@
|
|||
size="small"
|
||||
color="info"
|
||||
class="ml-1"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
@click="$emit('edit', true)"
|
||||
>
|
||||
<v-icon size="x-large">
|
||||
|
@ -86,7 +86,7 @@
|
|||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
||||
|
@ -97,48 +97,29 @@ const DELETE_EVENT = "delete";
|
|||
const CLOSE_EVENT = "close";
|
||||
const JSON_EVENT = "json";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimelineBadge },
|
||||
props: {
|
||||
recipe: {
|
||||
required: true,
|
||||
type: Object as () => Recipe,
|
||||
},
|
||||
slug: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
open: {
|
||||
required: true,
|
||||
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) {
|
||||
interface Props {
|
||||
recipe: Recipe;
|
||||
slug: string;
|
||||
recipeScale?: number;
|
||||
open: boolean;
|
||||
name: string;
|
||||
loggedIn?: boolean;
|
||||
recipeId: string;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipeScale: 1,
|
||||
loggedIn: false,
|
||||
canEdit: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
|
||||
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const editorButtons = [
|
||||
{
|
||||
text: i18n.t("general.delete"),
|
||||
|
@ -169,31 +150,22 @@ export default defineNuxtComponent({
|
|||
function emitHandler(event: string) {
|
||||
switch (event) {
|
||||
case CLOSE_EVENT:
|
||||
context.emit(CLOSE_EVENT);
|
||||
context.emit("input", false);
|
||||
emit("close");
|
||||
emit("input", false);
|
||||
break;
|
||||
case DELETE_EVENT:
|
||||
deleteDialog.value = true;
|
||||
break;
|
||||
default:
|
||||
context.emit(event);
|
||||
emit(event as any);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function emitDelete() {
|
||||
context.emit(DELETE_EVENT);
|
||||
context.emit("input", false);
|
||||
emit("delete");
|
||||
emit("input", false);
|
||||
}
|
||||
|
||||
return {
|
||||
deleteDialog,
|
||||
editorButtons,
|
||||
emitHandler,
|
||||
emitDelete,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
>
|
||||
<template #prepend>
|
||||
<div class="ma-auto">
|
||||
<v-tooltip bottom>
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps">
|
||||
{{ getIconDefinition(item.icon).icon }}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
||||
<v-lazy>
|
||||
<div>
|
||||
<v-hover
|
||||
v-slot="{ isHovering, props }"
|
||||
v-slot="{ isHovering, props: hoverProps }"
|
||||
:open-delay="50"
|
||||
>
|
||||
<v-card
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
:class="{ 'on-hover': isHovering }"
|
||||
:style="{ cursor }"
|
||||
:elevation="isHovering ? 12 : 2"
|
||||
|
@ -99,10 +98,9 @@
|
|||
</v-card>
|
||||
</v-hover>
|
||||
</div>
|
||||
</v-lazy>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
|
@ -110,50 +108,31 @@ import RecipeCardImage from "./RecipeCardImage.vue";
|
|||
import RecipeRating from "./RecipeRating.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
ratingColor: {
|
||||
type: String,
|
||||
default: "secondary",
|
||||
},
|
||||
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) {
|
||||
interface Props {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string | null;
|
||||
rating?: number;
|
||||
ratingColor?: string;
|
||||
image?: string;
|
||||
tags?: Array<any>;
|
||||
recipeId: string;
|
||||
imageHeight?: number;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
description: null,
|
||||
rating: 0,
|
||||
ratingColor: "secondary",
|
||||
image: "abc123",
|
||||
tags: () => [],
|
||||
imageHeight: 200,
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
click: [];
|
||||
delete: [slug: string];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
|
@ -164,15 +143,6 @@ export default defineNuxtComponent({
|
|||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
});
|
||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
showRecipeContent,
|
||||
cursor,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -197,6 +167,7 @@ export default defineNuxtComponent({
|
|||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 8;
|
||||
line-clamp: 8;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,47 +28,32 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
<script setup lang="ts">
|
||||
import { useStaticRoutes } from "~/composables/api";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
tiny: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
iconSize: {
|
||||
type: [Number, String],
|
||||
default: 100,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
imageVersion: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: "100%",
|
||||
},
|
||||
},
|
||||
emits: ["click"],
|
||||
setup(props) {
|
||||
const api = useUserApi();
|
||||
interface Props {
|
||||
tiny?: boolean | null;
|
||||
small?: boolean | null;
|
||||
large?: boolean | null;
|
||||
iconSize?: number | string;
|
||||
slug?: string | null;
|
||||
recipeId: string;
|
||||
imageVersion?: string | null;
|
||||
height?: number | string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tiny: null,
|
||||
small: null,
|
||||
large: null,
|
||||
iconSize: 100,
|
||||
slug: null,
|
||||
imageVersion: null,
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
|
||||
|
||||
|
@ -97,15 +82,6 @@ export default defineNuxtComponent({
|
|||
return recipeImage(recipeId, props.imageVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
api,
|
||||
fallBackImage,
|
||||
imageSize,
|
||||
getImage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<v-expand-transition>
|
||||
<v-card
|
||||
:ripple="false"
|
||||
:class="isFlat ? 'mx-auto flat' : 'mx-auto'"
|
||||
:class="[
|
||||
isFlat ? 'mx-auto flat' : 'mx-auto',
|
||||
{ 'disable-highlight': disableHighlight },
|
||||
]"
|
||||
:style="{ cursor }"
|
||||
hover
|
||||
height="100%"
|
||||
|
@ -123,7 +126,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
|
@ -131,59 +134,34 @@ import RecipeRating from "./RecipeRating.vue";
|
|||
import RecipeChips from "./RecipeChips.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeFavoriteBadge,
|
||||
RecipeContextMenu,
|
||||
RecipeRating,
|
||||
RecipeCardImage,
|
||||
RecipeChips,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
image: {
|
||||
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) {
|
||||
interface Props {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
rating?: number;
|
||||
image?: string;
|
||||
tags?: Array<any>;
|
||||
recipeId: string;
|
||||
vertical?: boolean;
|
||||
isFlat?: boolean;
|
||||
height?: number;
|
||||
disableHighlight?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rating: 0,
|
||||
image: "abc123",
|
||||
tags: () => [],
|
||||
vertical: false,
|
||||
isFlat: false,
|
||||
height: 150,
|
||||
disableHighlight: false,
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
selected: [];
|
||||
delete: [slug: string];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
|
@ -194,15 +172,6 @@ export default defineNuxtComponent({
|
|||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
});
|
||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
showRecipeContent,
|
||||
cursor,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -241,4 +210,8 @@ export default defineNuxtComponent({
|
|||
box-shadow: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.disable-highlight :deep(.v-card__overlay) {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,11 +36,11 @@
|
|||
offset-y
|
||||
start
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
variant="text"
|
||||
:icon="$vuetify.display.xs"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
:loading="sortLoading"
|
||||
>
|
||||
<v-icon :start="!$vuetify.display.xs">
|
||||
|
@ -162,7 +162,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import RecipeCard from "./RecipeCard.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 APPEND_RECIPES_EVENT = "appendRecipes";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeCard,
|
||||
RecipeCardMobile,
|
||||
},
|
||||
props: {
|
||||
disableToolbar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disableSort: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
singleColumn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
query: {
|
||||
type: Object as () => RecipeSearchQuery,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
disableToolbar?: boolean;
|
||||
disableSort?: boolean;
|
||||
icon?: string | null;
|
||||
title?: string | null;
|
||||
singleColumn?: boolean;
|
||||
recipes?: Recipe[];
|
||||
query?: RecipeSearchQuery | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disableToolbar: false,
|
||||
disableSort: false,
|
||||
icon: null,
|
||||
title: null,
|
||||
singleColumn: false,
|
||||
recipes: () => [],
|
||||
query: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
replaceRecipes: [recipes: Recipe[]];
|
||||
appendRecipes: [recipes: Recipe[]];
|
||||
}>();
|
||||
|
||||
const { $vuetify } = useNuxtApp();
|
||||
const preferences = useUserSortPreferences();
|
||||
|
||||
|
@ -234,9 +222,7 @@ export default defineNuxtComponent({
|
|||
return props.icon || $globals.icons.tags;
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
sortLoading: false,
|
||||
});
|
||||
const sortLoading = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
|
@ -251,7 +237,7 @@ export default defineNuxtComponent({
|
|||
const router = useRouter();
|
||||
|
||||
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)
|
||||
|
||||
|
@ -290,7 +276,7 @@ export default defineNuxtComponent({
|
|||
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||
watch(
|
||||
() => props.query,
|
||||
async (newValue: RecipeSearchQuery | undefined) => {
|
||||
async (newValue: RecipeSearchQuery | undefined | null) => {
|
||||
const newValueString = JSON.stringify(newValue);
|
||||
if (lastQuery !== newValueString) {
|
||||
lastQuery = newValueString;
|
||||
|
@ -315,7 +301,7 @@ export default defineNuxtComponent({
|
|||
// since we doubled the first call, we also need to advance the page
|
||||
page.value = page.value + 1;
|
||||
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
const infiniteScroll = useThrottleFn(async () => {
|
||||
|
@ -331,14 +317,14 @@ export default defineNuxtComponent({
|
|||
hasMore.value = false;
|
||||
}
|
||||
if (newRecipes.length) {
|
||||
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
|
||||
async function sortRecipes(sortType: string) {
|
||||
if (state.sortLoading || loading.value) {
|
||||
if (sortLoading.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -403,14 +389,14 @@ export default defineNuxtComponent({
|
|||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
|
||||
state.sortLoading = true;
|
||||
sortLoading.value = true;
|
||||
loading.value = true;
|
||||
|
||||
// fetch new recipes
|
||||
const newRecipes = await fetchRecipes();
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
|
||||
state.sortLoading = false;
|
||||
sortLoading.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
|
@ -426,22 +412,6 @@ export default defineNuxtComponent({
|
|||
function toggleMobileCards() {
|
||||
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
displayTitleIcon,
|
||||
EVENTS,
|
||||
infiniteScroll,
|
||||
ready,
|
||||
loading,
|
||||
navigateRandom,
|
||||
preferences,
|
||||
sortRecipes,
|
||||
toggleMobileCards,
|
||||
useMobileCards,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -23,52 +23,31 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||
|
||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array as () => RecipeCategory[] | RecipeTag[] | RecipeTool[],
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
urlPrefix: {
|
||||
type: String as () => UrlPrefixParam,
|
||||
default: "categories",
|
||||
},
|
||||
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}`;
|
||||
interface Props {
|
||||
truncate?: boolean;
|
||||
items?: RecipeCategory[] | RecipeTag[] | RecipeTool[];
|
||||
title?: boolean;
|
||||
urlPrefix?: UrlPrefixParam;
|
||||
limit?: number;
|
||||
small?: boolean;
|
||||
maxWidth?: string | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
truncate: false,
|
||||
items: () => [],
|
||||
title: false,
|
||||
urlPrefix: "categories",
|
||||
limit: 999,
|
||||
small: false,
|
||||
maxWidth: null,
|
||||
});
|
||||
|
||||
defineEmits(["item-selected"]);
|
||||
function truncateText(text: string, length = 20, clamp = "...") {
|
||||
if (!props.truncate) return text;
|
||||
const node = document.createElement("div");
|
||||
|
@ -76,13 +55,6 @@ export default defineNuxtComponent({
|
|||
const content = node.textContent || "";
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
}
|
||||
|
||||
return {
|
||||
baseRecipeRoute,
|
||||
truncateText,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
|
@ -12,7 +12,12 @@
|
|||
@confirm="deleteRecipe()"
|
||||
>
|
||||
<v-card-text>
|
||||
<template v-if="isAdminAndNotOwner">
|
||||
{{ $t("recipe.admin-delete-confirmation") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("recipe.delete-confirmation") }}
|
||||
</template>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
|
@ -50,12 +55,12 @@
|
|||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="newMealdateString"
|
||||
:label="$t('general.date')"
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -95,7 +100,7 @@
|
|||
:open-on-hover="$vuetify.display.mdAndUp"
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
icon
|
||||
:variant="fab ? 'flat' : undefined"
|
||||
|
@ -103,7 +108,7 @@
|
|||
:size="fab ? 'small' : undefined"
|
||||
:color="fab ? 'info' : 'secondary'"
|
||||
:fab="fab"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
@click.prevent
|
||||
>
|
||||
<v-icon
|
||||
|
@ -125,32 +130,27 @@
|
|||
</v-list-item>
|
||||
<div v-if="useItems.recipeActions && recipeActions && recipeActions.length">
|
||||
<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-for="(action, index) in recipeActions"
|
||||
:key="index"
|
||||
class="pl-6"
|
||||
@click="executeRecipeAction(action)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon color="undefined">
|
||||
{{ $globals.icons.linkVariantPlus }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ action.title }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-list-group>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
||||
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||
|
@ -186,16 +186,22 @@ export interface ContextMenuItem {
|
|||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
RecipeDialogPrintPreferences,
|
||||
RecipeDialogShare,
|
||||
},
|
||||
props: {
|
||||
useItems: {
|
||||
type: Object as () => ContextMenuIncludes,
|
||||
default: () => ({
|
||||
interface Props {
|
||||
useItems?: ContextMenuIncludes;
|
||||
appendItems?: ContextMenuItem[];
|
||||
leadingItems?: ContextMenuItem[];
|
||||
menuTop?: boolean;
|
||||
fab?: boolean;
|
||||
color?: string;
|
||||
slug: string;
|
||||
menuIcon?: string | null;
|
||||
name: string;
|
||||
recipe?: Recipe;
|
||||
recipeId: string;
|
||||
recipeScale?: number;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
useItems: () => ({
|
||||
delete: true,
|
||||
edit: true,
|
||||
download: true,
|
||||
|
@ -207,75 +213,42 @@ export default defineNuxtComponent({
|
|||
share: true,
|
||||
recipeActions: true,
|
||||
}),
|
||||
},
|
||||
// Append items are added at the end of the useItems list
|
||||
appendItems: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
default: () => [],
|
||||
},
|
||||
// Append items are added at the beginning of the useItems list
|
||||
leadingItems: {
|
||||
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,
|
||||
appendItems: () => [],
|
||||
leadingItems: () => [],
|
||||
menuTop: true,
|
||||
fab: false,
|
||||
color: "primary",
|
||||
menuIcon: null,
|
||||
recipe: undefined,
|
||||
recipeScale: 1,
|
||||
});
|
||||
|
||||
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(() => {
|
||||
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();
|
||||
|
@ -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
|
||||
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
|
||||
menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
|
@ -383,6 +346,30 @@ export default defineNuxtComponent({
|
|||
const recipeRefWithScale = computed(() =>
|
||||
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() {
|
||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||
|
@ -420,7 +407,7 @@ export default defineNuxtComponent({
|
|||
if (data?.slug) {
|
||||
router.push(`/g/${groupSlug.value}`);
|
||||
}
|
||||
context.emit("delete", props.slug);
|
||||
emit("delete", props.slug);
|
||||
}
|
||||
|
||||
const download = useDownloader();
|
||||
|
@ -436,7 +423,7 @@ export default defineNuxtComponent({
|
|||
async function addRecipeToPlan() {
|
||||
const { response } = await api.mealplans.createOne({
|
||||
date: newMealdateString.value,
|
||||
entryType: state.newMealType,
|
||||
entryType: newMealType.value,
|
||||
title: "",
|
||||
text: "",
|
||||
recipeId: props.recipeId,
|
||||
|
@ -451,7 +438,7 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
delete: () => {
|
||||
state.recipeDeleteDialog = true;
|
||||
recipeDeleteDialog.value = true;
|
||||
},
|
||||
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
||||
download: handleDownloadEvent,
|
||||
duplicate: () => {
|
||||
state.recipeDuplicateDialog = true;
|
||||
recipeDuplicateDialog.value = true;
|
||||
},
|
||||
mealplanner: () => {
|
||||
state.mealplannerDialog = true;
|
||||
mealplannerDialog.value = true;
|
||||
},
|
||||
printPreferences: async () => {
|
||||
if (!recipeRef.value) {
|
||||
await refreshRecipe();
|
||||
}
|
||||
state.printPreferencesDialog = true;
|
||||
printPreferencesDialog.value = true;
|
||||
},
|
||||
shoppingList: () => {
|
||||
const promises: Promise<void>[] = [getShoppingLists()];
|
||||
|
@ -484,11 +471,11 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
Promise.allSettled(promises).then(() => {
|
||||
state.shoppingListDialog = true;
|
||||
shoppingListDialog.value = true;
|
||||
});
|
||||
},
|
||||
share: () => {
|
||||
state.shareDialog = true;
|
||||
shareDialog.value = true;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -497,32 +484,14 @@ export default defineNuxtComponent({
|
|||
|
||||
if (handler && typeof handler === "function") {
|
||||
handler();
|
||||
state.loading = false;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
context.emit(eventKey);
|
||||
state.loading = false;
|
||||
emit(eventKey);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const planTypeOptions = usePlanTypeOptions();
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
newMealdateString,
|
||||
recipeRef,
|
||||
recipeRefWithScale,
|
||||
executeRecipeAction,
|
||||
recipeActions: groupRecipeActionsStore.recipeActions,
|
||||
shoppingLists,
|
||||
duplicateRecipe,
|
||||
contextMenuEventHandler,
|
||||
deleteRecipe,
|
||||
addRecipeToPlan,
|
||||
icon,
|
||||
planTypeOptions,
|
||||
firstDayOfWeek,
|
||||
};
|
||||
},
|
||||
});
|
||||
const recipeActions = groupRecipeActionsStore.recipeActions;
|
||||
</script>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
@ -42,28 +42,19 @@ export interface GenericAlias {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
data: {
|
||||
type: Object as () => IngredientFood | IngredientUnit,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["submit", "update:modelValue", "cancel"],
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
data: IngredientFood | IngredientUnit;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [aliases: GenericAlias[]];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
// V-Model Support
|
||||
const dialog = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
function createAlias() {
|
||||
aliases.value.push({
|
||||
|
@ -85,7 +76,7 @@ export default defineNuxtComponent({
|
|||
|
||||
initAliases();
|
||||
whenever(
|
||||
() => props.modelValue,
|
||||
() => dialog.value,
|
||||
() => {
|
||||
initAliases();
|
||||
},
|
||||
|
@ -111,17 +102,6 @@ export default defineNuxtComponent({
|
|||
});
|
||||
|
||||
aliases.value = keepAliases;
|
||||
context.emit("submit", keepAliases);
|
||||
emit("submit", keepAliases);
|
||||
}
|
||||
|
||||
return {
|
||||
aliases,
|
||||
createAlias,
|
||||
dialog,
|
||||
deleteAlias,
|
||||
saveAliases,
|
||||
validators,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
v-model="selected"
|
||||
item-key="id"
|
||||
show-select
|
||||
:sort-by="[{ key: 'dateAdded', order: 'desc' }]"
|
||||
:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="recipes"
|
||||
:items-per-page="15"
|
||||
class="elevation-0"
|
||||
:loading="loading"
|
||||
return-object
|
||||
>
|
||||
<template #[`item.name`]="{ item }">
|
||||
<a
|
||||
|
@ -61,7 +62,7 @@
|
|||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import UserAvatar from "../User/UserAvatar.vue";
|
||||
import RecipeChip from "./RecipeChips.vue";
|
||||
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 { RecipeTag } from "~/lib/api/types/household";
|
||||
|
||||
const INPUT_EVENT = "update:modelValue";
|
||||
|
||||
interface ShowHeaders {
|
||||
id: boolean;
|
||||
owner: boolean;
|
||||
|
@ -83,53 +82,43 @@ interface ShowHeaders {
|
|||
dateAdded: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeChip, UserAvatar },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Recipe[]>,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
showHeaders: {
|
||||
type: Object as () => ShowHeaders,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {
|
||||
interface Props {
|
||||
loading?: boolean;
|
||||
recipes?: Recipe[];
|
||||
showHeaders?: ShowHeaders;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
recipes: () => [],
|
||||
showHeaders: () => ({
|
||||
id: true,
|
||||
owner: false,
|
||||
tags: true,
|
||||
categories: true,
|
||||
tools: true,
|
||||
recipeServings: true,
|
||||
recipeYieldQuantity: true,
|
||||
recipeYield: true,
|
||||
dateAdded: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ["click"],
|
||||
setup(props, context) {
|
||||
}),
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const selected = defineModel<Recipe[]>({ default: () => [] });
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const groupSlug = $auth.user.value?.groupSlug;
|
||||
const router = useRouter();
|
||||
const selected = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => context.emit(INPUT_EVENT, value),
|
||||
});
|
||||
|
||||
// Initialize sort state with default sorting by dateAdded descending
|
||||
const sortBy = ref([{ key: "dateAdded", order: "desc" as const }]);
|
||||
|
||||
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) {
|
||||
hdrs.push({ title: i18n.t("general.id"), value: "id" });
|
||||
|
@ -203,16 +192,4 @@ export default defineNuxtComponent({
|
|||
|
||||
return i18n.t("general.none");
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
groupSlug,
|
||||
headers,
|
||||
formatDate,
|
||||
members,
|
||||
getMember,
|
||||
filterItems,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<BaseDialog
|
||||
v-if="shoppingListIngredientDialog"
|
||||
v-model="dialog"
|
||||
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
||||
:title="selectedShoppingList?.name || $t('recipe.add-to-list')"
|
||||
:icon="$globals.icons.cartCheck"
|
||||
width="70%"
|
||||
:submit-text="$t('recipe.add-to-list')"
|
||||
|
@ -137,7 +137,7 @@
|
|||
color="secondary"
|
||||
density="compact"
|
||||
/>
|
||||
<div :key="ingredientData.ingredient.quantity">
|
||||
<div :key="`${ingredientData.ingredient.quantity || 'no-qty'}-${i}`">
|
||||
<RecipeIngredientListItem
|
||||
:ingredient="ingredientData.ingredient"
|
||||
:disable-amount="ingredientData.disableAmount"
|
||||
|
@ -172,7 +172,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from "@vueuse/core";
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
@ -203,49 +203,31 @@ export interface ShoppingListRecipeIngredientSection {
|
|||
ingredientSections: ShoppingListIngredientSection[];
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeIngredientListItem,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => RecipeWithScale[],
|
||||
default: undefined,
|
||||
},
|
||||
shoppingLists: {
|
||||
type: Array as () => ShoppingListSummary[],
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
recipes?: RecipeWithScale[];
|
||||
shoppingLists?: ShoppingListSummary[];
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipes: undefined,
|
||||
shoppingLists: () => [],
|
||||
});
|
||||
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const api = useUserApi();
|
||||
const preferences = useShoppingListPreferences();
|
||||
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({
|
||||
shoppingListDialog: true,
|
||||
shoppingListIngredientDialog: false,
|
||||
shoppingListShowAllToggled: false,
|
||||
});
|
||||
|
||||
const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllToggled: _shoppingListShowAllToggled } = toRefs(state);
|
||||
|
||||
const userHousehold = computed(() => {
|
||||
return $auth.user.value?.householdSlug || "";
|
||||
});
|
||||
|
@ -269,6 +251,12 @@ export default defineNuxtComponent({
|
|||
},
|
||||
);
|
||||
|
||||
watch(dialog, (val) => {
|
||||
if (!val) {
|
||||
initState();
|
||||
}
|
||||
});
|
||||
|
||||
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
|
||||
for (const recipe of recipes) {
|
||||
|
@ -277,7 +265,10 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -421,22 +412,6 @@ export default defineNuxtComponent({
|
|||
state.shoppingListIngredientDialog = false;
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
dialog,
|
||||
preferences,
|
||||
ready,
|
||||
shoppingListChoices,
|
||||
...toRefs(state),
|
||||
addRecipesToList,
|
||||
bulkCheckIngredients,
|
||||
openShoppingListIngredientDialog,
|
||||
setShowAllToggled,
|
||||
recipeIngredientSections,
|
||||
selectedShoppingList,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
v-model="dialog"
|
||||
width="800"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<BaseButton
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
@click="inputText = inputTextProp"
|
||||
>
|
||||
{{ $t("new-recipe.bulk-add") }}
|
||||
|
@ -89,28 +89,27 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
inputTextProp: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["bulk-data"],
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
dialog: false,
|
||||
inputText: props.inputTextProp,
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
inputTextProp?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
inputTextProp: "",
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"bulk-data": [data: string[]];
|
||||
}>();
|
||||
|
||||
const dialog = ref(false);
|
||||
const inputText = ref(props.inputTextProp);
|
||||
|
||||
function splitText() {
|
||||
return state.inputText.split("\n").filter(line => !(line === "\n" || !line));
|
||||
return inputText.value.split("\n").filter(line => !(line === "\n" || !line));
|
||||
}
|
||||
|
||||
function removeFirstCharacter() {
|
||||
state.inputText = splitText()
|
||||
inputText.value = splitText()
|
||||
.map(line => line.substring(1))
|
||||
.join("\n");
|
||||
}
|
||||
|
@ -119,11 +118,11 @@ export default defineNuxtComponent({
|
|||
|
||||
function splitByNumberedLine() {
|
||||
// Split inputText by numberedLineRegex
|
||||
const matches = state.inputText.match(numberedLineRegex);
|
||||
const matches = inputText.value.match(numberedLineRegex);
|
||||
|
||||
matches?.forEach((match, idx) => {
|
||||
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();
|
||||
});
|
||||
|
||||
state.inputText = splitLines.join("\n");
|
||||
inputText.value = splitLines.join("\n");
|
||||
}
|
||||
|
||||
function save() {
|
||||
context.emit("bulk-data", splitText());
|
||||
state.dialog = false;
|
||||
emit("bulk-data", splitText());
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
|
@ -161,16 +160,4 @@ export default defineNuxtComponent({
|
|||
action: splitByNumberedLine,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
utilities,
|
||||
splitText,
|
||||
trimAllLines,
|
||||
removeFirstCharacter,
|
||||
splitByNumberedLine,
|
||||
save,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<v-switch
|
||||
v-model="preferences.showDescription"
|
||||
hide-details
|
||||
color="primary"
|
||||
:label="$t('recipe.description')"
|
||||
/>
|
||||
</v-row>
|
||||
|
@ -51,6 +52,7 @@
|
|||
<v-switch
|
||||
v-model="preferences.showNotes"
|
||||
hide-details
|
||||
color="primary"
|
||||
:label="$t('recipe.notes')"
|
||||
/>
|
||||
</v-row>
|
||||
|
@ -63,6 +65,7 @@
|
|||
<v-switch
|
||||
v-model="preferences.showNutrition"
|
||||
hide-details
|
||||
color="primary"
|
||||
:label="$t('recipe.nutrition')"
|
||||
/>
|
||||
</v-row>
|
||||
|
@ -83,45 +86,19 @@
|
|||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipePrintView,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
recipe?: NoUndefinedField<Recipe>;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipe: undefined,
|
||||
});
|
||||
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
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>
|
||||
|
|
|
@ -52,10 +52,6 @@
|
|||
<div class="mr-auto">
|
||||
{{ $t("search.results") }}
|
||||
</div>
|
||||
<!-- <router-link
|
||||
:to="advancedSearchUrl"
|
||||
class="text-primary"
|
||||
> {{ $t("search.advanced-search") }} </router-link> -->
|
||||
</v-card-actions>
|
||||
|
||||
<RecipeCardMobile
|
||||
|
@ -76,7 +72,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
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";
|
||||
|
||||
const SELECTED_EVENT = "selected";
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeCardMobile,
|
||||
},
|
||||
|
||||
setup(_, context) {
|
||||
// Define emits
|
||||
const emit = defineEmits<{
|
||||
selected: [recipe: RecipeSummary];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
selectedIndex: -1,
|
||||
});
|
||||
const loading = ref(false);
|
||||
const selectedIndex = ref(-1);
|
||||
|
||||
// ===========================================================================
|
||||
// Dialog State Management
|
||||
|
@ -105,7 +99,7 @@ export default defineNuxtComponent({
|
|||
watch(dialog, (val) => {
|
||||
if (!val) {
|
||||
search.query.value = "";
|
||||
state.selectedIndex = -1;
|
||||
selectedIndex.value = -1;
|
||||
search.data.value = [];
|
||||
}
|
||||
});
|
||||
|
@ -116,17 +110,17 @@ export default defineNuxtComponent({
|
|||
function selectRecipe() {
|
||||
const recipeCards = document.getElementsByClassName("arrow-nav");
|
||||
if (recipeCards) {
|
||||
if (state.selectedIndex < 0) {
|
||||
state.selectedIndex = -1;
|
||||
if (selectedIndex.value < 0) {
|
||||
selectedIndex.value = -1;
|
||||
document.getElementById("arrow-search")?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.selectedIndex >= recipeCards.length) {
|
||||
state.selectedIndex = recipeCards.length - 1;
|
||||
if (selectedIndex.value >= recipeCards.length) {
|
||||
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") {
|
||||
e.preventDefault();
|
||||
state.selectedIndex--;
|
||||
selectedIndex.value--;
|
||||
}
|
||||
else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
state.selectedIndex++;
|
||||
selectedIndex.value++;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
|
@ -158,9 +152,8 @@ export default defineNuxtComponent({
|
|||
}
|
||||
});
|
||||
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`);
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
watch(route, close);
|
||||
|
||||
function open() {
|
||||
|
@ -177,22 +170,15 @@ export default defineNuxtComponent({
|
|||
const search = useRecipeSearch(api);
|
||||
|
||||
// Select Handler
|
||||
|
||||
function handleSelect(recipe: RecipeSummary) {
|
||||
close();
|
||||
context.emit(SELECTED_EVENT, recipe);
|
||||
emit(SELECTED_EVENT, recipe);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
advancedSearchUrl,
|
||||
dialog,
|
||||
// Expose functions to parent components
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
handleSelect,
|
||||
search,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="expirationDateString"
|
||||
:label="$t('recipe-share.expiration-date')"
|
||||
:hint="$t('recipe-share.default-30-days')"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -92,56 +92,35 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useClipboard, useShare, whenever } from "@vueuse/core";
|
||||
import type { RecipeShareToken } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useHouseholdSelf } from "~/composables/use-households";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
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);
|
||||
},
|
||||
});
|
||||
interface Props {
|
||||
recipeId: string;
|
||||
name: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const state = reactive({
|
||||
datePickerMenu: false,
|
||||
expirationDate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000),
|
||||
tokens: [] as RecipeShareToken[],
|
||||
});
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const datePickerMenu = ref(false);
|
||||
const expirationDate = ref(new Date(Date.now() - new Date().getTimezoneOffset() * 60000));
|
||||
const tokens = ref<RecipeShareToken[]>([]);
|
||||
|
||||
const expirationDateString = computed(() => {
|
||||
return state.expirationDate.toISOString().substring(0, 10);
|
||||
return expirationDate.value.toISOString().substring(0, 10);
|
||||
});
|
||||
|
||||
whenever(
|
||||
() => props.modelValue,
|
||||
() => dialog.value,
|
||||
() => {
|
||||
// Set expiration date to today + 30 Days
|
||||
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();
|
||||
},
|
||||
);
|
||||
|
@ -165,17 +144,17 @@ export default defineNuxtComponent({
|
|||
// Convert expiration date to timestamp
|
||||
const { data } = await userApi.recipes.share.createOne({
|
||||
recipeId: props.recipeId,
|
||||
expiresAt: state.expirationDate.toISOString(),
|
||||
expiresAt: expirationDate.value.toISOString(),
|
||||
});
|
||||
|
||||
if (data) {
|
||||
state.tokens.push(data);
|
||||
tokens.value.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteToken(id: string) {
|
||||
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() {
|
||||
|
@ -183,7 +162,7 @@ export default defineNuxtComponent({
|
|||
|
||||
if (data) {
|
||||
// @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);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
expirationDateString,
|
||||
dialog,
|
||||
createNewToken,
|
||||
deleteToken,
|
||||
firstDayOfWeek,
|
||||
shareRecipe,
|
||||
copyTokenLink,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
nudge-right="50"
|
||||
:color="buttonStyle ? 'info' : 'secondary'"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
v-if="isFavorite || showAlways"
|
||||
icon
|
||||
|
@ -13,7 +13,7 @@
|
|||
size="small"
|
||||
:color="buttonStyle ? 'info' : 'secondary'"
|
||||
:fab="buttonStyle"
|
||||
v-bind="{ ...props, ...$attrs }"
|
||||
v-bind="{ ...tooltipProps, ...$attrs }"
|
||||
@click.prevent="toggleFavorite"
|
||||
>
|
||||
<v-icon
|
||||
|
@ -28,26 +28,21 @@
|
|||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserSelfRatings } from "~/composables/use-users";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipeId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showAlways: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
buttonStyle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
interface Props {
|
||||
recipeId?: string;
|
||||
showAlways?: boolean;
|
||||
buttonStyle?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipeId: "",
|
||||
showAlways: false,
|
||||
buttonStyle: false,
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const $auth = useMealieAuth();
|
||||
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
||||
|
@ -67,8 +62,4 @@ export default defineNuxtComponent({
|
|||
}
|
||||
await refreshUserRatings();
|
||||
}
|
||||
|
||||
return { isFavorite, toggleFavorite };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
nudge-top="6"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
color="accent"
|
||||
dark
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
>
|
||||
<v-icon start>
|
||||
{{ $globals.icons.fileImage }}
|
||||
|
@ -61,52 +61,42 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const REFRESH_EVENT = "refresh";
|
||||
const UPLOAD_EVENT = "upload";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
url: "",
|
||||
loading: false,
|
||||
menu: false,
|
||||
});
|
||||
const props = defineProps<{ slug: string }>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
upload: [fileObject: File];
|
||||
}>();
|
||||
|
||||
const url = ref("");
|
||||
const loading = ref(false);
|
||||
const menu = ref(false);
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
context.emit(UPLOAD_EVENT, fileObject);
|
||||
state.menu = false;
|
||||
emit(UPLOAD_EVENT, fileObject);
|
||||
menu.value = false;
|
||||
}
|
||||
|
||||
const api = useUserApi();
|
||||
async function getImageFromURL() {
|
||||
state.loading = true;
|
||||
if (await api.recipes.updateImagebyURL(props.slug, state.url)) {
|
||||
context.emit(REFRESH_EVENT);
|
||||
loading.value = true;
|
||||
if (await api.recipes.updateImagebyURL(props.slug, url.value)) {
|
||||
emit(REFRESH_EVENT);
|
||||
}
|
||||
state.loading = false;
|
||||
state.menu = false;
|
||||
loading.value = false;
|
||||
menu.value = false;
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
const messages = props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")];
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
uploadImage,
|
||||
getImageFromURL,
|
||||
messages,
|
||||
};
|
||||
},
|
||||
});
|
||||
const messages = computed(() =>
|
||||
props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")],
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -63,6 +63,22 @@
|
|||
clearable
|
||||
@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>
|
||||
<div class="caption text-center pb-2">
|
||||
{{ $t("recipe.press-enter-to-create") }}
|
||||
|
@ -104,6 +120,22 @@
|
|||
clearable
|
||||
@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>
|
||||
<div class="caption text-center pb-2">
|
||||
{{ $t("recipe.press-enter-to-create") }}
|
||||
|
@ -153,7 +185,6 @@
|
|||
@toggle-original="toggleOriginalText"
|
||||
@insert-above="$emit('insert-above')"
|
||||
@insert-below="$emit('insert-below')"
|
||||
@insert-ingredient="$emit('insert-ingredient')"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -184,22 +215,33 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
|||
// defineModel replaces modelValue prop
|
||||
const model = defineModel<RecipeIngredient>({ required: true });
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
disableAmount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowInsertIngredient: {
|
||||
unitError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
unitErrorTooltip: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
foodError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
foodErrorTooltip: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits([
|
||||
"clickIngredientField",
|
||||
"insert-above",
|
||||
"insert-below",
|
||||
"insert-ingredient",
|
||||
"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) {
|
||||
options.push({
|
||||
text: i18n.t("recipe.see-original-text"),
|
||||
|
|
|
@ -3,21 +3,13 @@
|
|||
<div v-html="safeMarkup" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
markup: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
interface Props {
|
||||
markup: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
||||
return {
|
||||
safeMarkup,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -28,34 +28,22 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { RecipeIngredient } from "~/lib/api/types/household";
|
||||
import { useParsedIngredientText } from "~/composables/recipes";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
ingredient: {
|
||||
type: Object as () => RecipeIngredient,
|
||||
required: true,
|
||||
},
|
||||
disableAmount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const parsedIng = computed(() => {
|
||||
return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale);
|
||||
interface Props {
|
||||
ingredient: RecipeIngredient;
|
||||
disableAmount?: boolean;
|
||||
scale?: number;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disableAmount: false,
|
||||
scale: 1,
|
||||
});
|
||||
|
||||
return {
|
||||
parsedIng,
|
||||
};
|
||||
},
|
||||
const parsedIng = computed(() => {
|
||||
return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -53,40 +53,30 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeIngredientListItem },
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => RecipeIngredient[],
|
||||
default: () => [],
|
||||
},
|
||||
disableAmount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
isCookMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
function validateTitle(title?: string) {
|
||||
interface Props {
|
||||
value?: RecipeIngredient[];
|
||||
disableAmount?: boolean;
|
||||
scale?: number;
|
||||
isCookMode?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: () => [],
|
||||
disableAmount: false,
|
||||
scale: 1,
|
||||
isCookMode: false,
|
||||
});
|
||||
|
||||
function validateTitle(title?: string | null) {
|
||||
return !(title === undefined || title === "" || title === null);
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
checked: props.value.map(() => false),
|
||||
showTitleEditor: computed(() => props.value.map(x => validateTitle(x.title))),
|
||||
});
|
||||
const checked = ref(props.value.map(() => false));
|
||||
const showTitleEditor = computed(() => props.value.map(x => validateTitle(x.title)));
|
||||
|
||||
const ingredientCopyText = computed(() => {
|
||||
const components: string[] = [];
|
||||
|
@ -108,16 +98,8 @@ export default defineNuxtComponent({
|
|||
function toggleChecked(index: number) {
|
||||
// TODO Find a better way to do this - $set is not available, and
|
||||
// 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>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<div>
|
||||
<BaseDialog
|
||||
v-model="madeThisDialog"
|
||||
:loading="madeThisFormLoading"
|
||||
:icon="$globals.icons.chefHat"
|
||||
:title="$t('recipe.made-this')"
|
||||
:submit-text="$t('recipe.add-to-timeline')"
|
||||
|
@ -29,11 +30,11 @@
|
|||
offset-y
|
||||
max-width="290px"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="newTimelineEventTimestampString"
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -85,13 +86,13 @@
|
|||
<div>
|
||||
<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-tooltip bottom>
|
||||
<template #activator="{ props }">
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
rounded
|
||||
variant="outlined"
|
||||
size="x-large"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
style="border-color: rgb(var(--v-theme-primary));"
|
||||
@click="madeThisDialog = true"
|
||||
>
|
||||
|
@ -116,22 +117,19 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
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";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => Recipe,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["eventCreated"],
|
||||
setup(props, context) {
|
||||
const props = defineProps<{ recipe: Recipe }>();
|
||||
const emit = defineEmits<{
|
||||
eventCreated: [event: RecipeTimelineEventOut];
|
||||
}>();
|
||||
|
||||
const madeThisDialog = ref(false);
|
||||
const userApi = useUserApi();
|
||||
const { household } = useHouseholdSelf();
|
||||
|
@ -196,12 +194,26 @@ export default defineNuxtComponent({
|
|||
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() {
|
||||
if (!(newTimelineEventTimestampString.value && props.recipe?.id && props.recipe?.slug)) {
|
||||
return;
|
||||
}
|
||||
|
||||
madeThisFormLoading.value = true;
|
||||
|
||||
newTimelineEvent.value.recipeId = props.recipe.id;
|
||||
// Note: $auth.user is now a ref
|
||||
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
|
||||
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 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
|
||||
if (!lastMade.value || newTimelineEvent.value.timestamp > lastMade.value) {
|
||||
try {
|
||||
lastMade.value = 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
|
||||
if (newTimelineEventImage.value && newEvent) {
|
||||
let imageError = false;
|
||||
if (newTimelineEventImage.value) {
|
||||
try {
|
||||
const imageResponse = await userApi.recipes.updateTimelineEventImage(
|
||||
newEvent.id,
|
||||
newTimelineEventImage.value,
|
||||
|
@ -230,34 +262,20 @@ export default defineNuxtComponent({
|
|||
newEvent.image = imageResponse.data.image;
|
||||
}
|
||||
}
|
||||
|
||||
// reset form
|
||||
newTimelineEvent.value.eventMessage = "";
|
||||
newTimelineEvent.value.timestamp = undefined;
|
||||
clearImage();
|
||||
madeThisDialog.value = false;
|
||||
domMadeThisForm.value?.reset();
|
||||
|
||||
context.emit("eventCreated", newEvent);
|
||||
catch (error) {
|
||||
imageError = true;
|
||||
console.error("Failed to upload image for timeline event:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
domMadeThisForm,
|
||||
madeThisDialog,
|
||||
firstDayOfWeek,
|
||||
newTimelineEvent,
|
||||
newTimelineEventImage,
|
||||
newTimelineEventImagePreviewUrl,
|
||||
newTimelineEventTimestamp,
|
||||
newTimelineEventTimestampString,
|
||||
lastMade,
|
||||
lastMadeReady,
|
||||
createTimelineEvent,
|
||||
clearImage,
|
||||
uploadImage,
|
||||
updateUploadedImage,
|
||||
};
|
||||
},
|
||||
});
|
||||
if (imageError) {
|
||||
alert.error(i18n.t("recipe.added-to-timeline-but-failed-to-add-image"));
|
||||
}
|
||||
else {
|
||||
alert.success(i18n.t("recipe.added-to-timeline"));
|
||||
}
|
||||
|
||||
resetMadeThisForm();
|
||||
emit("eventCreated", newEvent);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -51,40 +51,28 @@
|
|||
</v-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import { useFraction } from "~/composables/recipes/use-fraction";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipes: {
|
||||
type: Array as () => RecipeSummary[],
|
||||
required: true,
|
||||
},
|
||||
listItem: {
|
||||
type: Object as () => ShoppingListItemOut | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showDescription: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
interface Props {
|
||||
recipes: RecipeSummary[];
|
||||
listItem?: ShoppingListItemOut;
|
||||
small?: boolean;
|
||||
tile?: boolean;
|
||||
showDescription?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
listItem: undefined,
|
||||
small: false,
|
||||
tile: false,
|
||||
showDescription: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { frac } = useFraction();
|
||||
const route = useRoute();
|
||||
|
@ -180,12 +168,4 @@ export default defineNuxtComponent({
|
|||
|
||||
return listItemDescriptions;
|
||||
});
|
||||
|
||||
return {
|
||||
attrs,
|
||||
groupSlug,
|
||||
listItemDescriptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -45,29 +45,25 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useNutritionLabels } from "~/composables/recipes";
|
||||
import type { Nutrition } from "~/lib/api/types/recipe";
|
||||
import type { NutritionLabelType } from "~/composables/recipes/use-recipe-nutrition";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => Nutrition,
|
||||
required: true,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
edit?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
edit: true,
|
||||
});
|
||||
|
||||
const modelValue = defineModel<Nutrition>({ required: true });
|
||||
|
||||
const { labels } = useNutritionLabels();
|
||||
const valueNotNull = computed(() => {
|
||||
let key: keyof Nutrition;
|
||||
for (key in props.modelValue) {
|
||||
if (props.modelValue[key] !== null) {
|
||||
for (key in modelValue.value) {
|
||||
if (modelValue.value[key] !== null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -77,31 +73,21 @@ export default defineNuxtComponent({
|
|||
const showViewer = computed(() => !props.edit && valueNotNull.value);
|
||||
|
||||
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
|
||||
const renderedList = computed(() => {
|
||||
return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => {
|
||||
if (props.modelValue[key]?.trim()) {
|
||||
if (modelValue.value[key]?.trim()) {
|
||||
item[key] = {
|
||||
...label,
|
||||
value: props.modelValue[key],
|
||||
value: modelValue.value[key],
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}, {});
|
||||
});
|
||||
|
||||
return {
|
||||
labels,
|
||||
valueNotNull,
|
||||
showViewer,
|
||||
updateValue,
|
||||
renderedList,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -60,54 +60,39 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated";
|
||||
|
||||
const CREATED_ITEM_EVENT = "created-item";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
tagDialog: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
itemType: {
|
||||
type: String as () => RecipeOrganizer,
|
||||
default: "category",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
color?: string | null;
|
||||
tagDialog?: boolean;
|
||||
itemType?: RecipeOrganizer;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: null,
|
||||
tagDialog: true,
|
||||
itemType: "category" as RecipeOrganizer,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"created-item": [item: any];
|
||||
}>();
|
||||
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
name: "",
|
||||
onHand: false,
|
||||
});
|
||||
|
||||
const dialog = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
context.emit("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
const name = ref("");
|
||||
const onHand = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
dialog,
|
||||
(val: boolean) => {
|
||||
if (!val) state.name = "";
|
||||
if (!val) name.value = "";
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -154,25 +139,14 @@ export default defineNuxtComponent({
|
|||
async function select() {
|
||||
if (store) {
|
||||
// @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;
|
||||
}
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
...toRefs(state),
|
||||
dialog,
|
||||
properties,
|
||||
rules,
|
||||
select,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
|
@ -122,9 +122,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
import { useContextPresets } from "~/composables/use-context-presents";
|
||||
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||
|
@ -138,26 +137,17 @@ interface GenericItem {
|
|||
onHand: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeOrganizerDialog,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => GenericItem[],
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemType: {
|
||||
type: String as () => RecipeOrganizer,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update", "delete"],
|
||||
setup(props, { emit }) {
|
||||
const props = defineProps<{
|
||||
items: GenericItem[];
|
||||
icon: string;
|
||||
itemType: RecipeOrganizer;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [item: GenericItem];
|
||||
delete: [id: string];
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
// Search Options
|
||||
options: {
|
||||
|
@ -271,23 +261,4 @@ export default defineNuxtComponent({
|
|||
function isTitle(str: number | string) {
|
||||
return typeof str === "string" && str.length === 1;
|
||||
}
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
isTitle,
|
||||
dialogs,
|
||||
confirmDelete,
|
||||
openUpdateDialog,
|
||||
updateOne,
|
||||
updateTarget,
|
||||
deleteOne,
|
||||
deleteTarget,
|
||||
Organizer,
|
||||
presets,
|
||||
itemsSorted,
|
||||
searchString,
|
||||
translationKey,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-model="selected"
|
||||
v-bind="inputAttrs"
|
||||
v-model:search="searchInput"
|
||||
:items="storeItem"
|
||||
:items="items"
|
||||
:label="label"
|
||||
chips
|
||||
closable-chips
|
||||
|
@ -46,67 +46,40 @@
|
|||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
|
||||
import type { RecipeTool } from "~/lib/api/types/admin";
|
||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as () => (
|
||||
interface Props {
|
||||
selectorType: RecipeOrganizer;
|
||||
inputAttrs?: Record<string, any>;
|
||||
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
|
||||
| RecipeTag
|
||||
| RecipeCategory
|
||||
| RecipeTool
|
||||
| IngredientFood
|
||||
| string
|
||||
)[] | undefined,
|
||||
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);
|
||||
},
|
||||
});
|
||||
)[] | undefined>({ required: true });
|
||||
|
||||
onMounted(() => {
|
||||
if (selected.value === undefined) {
|
||||
|
@ -205,21 +178,6 @@ export default defineNuxtComponent({
|
|||
function resetSearchInput() {
|
||||
searchInput.value = "";
|
||||
}
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
appendCreated,
|
||||
dialog,
|
||||
storeItem: items,
|
||||
label,
|
||||
icon,
|
||||
selected,
|
||||
removeByIndex,
|
||||
searchInput,
|
||||
resetSearchInput,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<RecipePageIngredientEditor v-if="isEditForm" v-model="recipe" />
|
||||
</div>
|
||||
<div>
|
||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
||||
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
@ -81,7 +81,7 @@
|
|||
</v-card>
|
||||
<WakelockSwitch />
|
||||
<RecipePageComments
|
||||
v-if="!recipe.settings.disableComments && !isEditForm && !isCookMode"
|
||||
v-if="!recipe.settings?.disableComments && !isEditForm && !isCookMode"
|
||||
v-model="recipe"
|
||||
class="px-1 my-4 d-print-none"
|
||||
/>
|
||||
|
@ -96,7 +96,7 @@
|
|||
<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%">
|
||||
<div class="d-flex align-center">
|
||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
||||
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||
</div>
|
||||
<RecipePageIngredientToolsView
|
||||
v-if="!isEditForm"
|
||||
|
@ -124,7 +124,7 @@
|
|||
</v-sheet>
|
||||
<v-sheet v-show="isCookMode && hasLinkedIngredients">
|
||||
<div class="mt-2 px-2 px-md-4">
|
||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
||||
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||
</div>
|
||||
<RecipePageInstructions
|
||||
v-model="recipe.recipeInstructions"
|
||||
|
@ -278,7 +278,7 @@ async function deleteRecipe() {
|
|||
* View Preferences
|
||||
*/
|
||||
const landscape = computed(() => {
|
||||
const preferLandscape = recipe.value.settings.landscapeView;
|
||||
const preferLandscape = recipe.value.settings?.landscapeView;
|
||||
const smallScreen = !$vuetify.display.smAndUp.value;
|
||||
|
||||
if (preferLandscape) {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useRecipePermissions } from "~/composables/recipes";
|
||||
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 { Recipe } from "~/lib/api/types/recipe";
|
||||
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 { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
|
@ -78,9 +68,6 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
const hideImage = ref(false);
|
||||
const imageHeight = computed(() => {
|
||||
return $vuetify.display.xs.value ? "200" : "400";
|
||||
});
|
||||
|
||||
const recipeImageUrl = computed(() => {
|
||||
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
||||
|
@ -92,25 +79,4 @@ export default defineNuxtComponent({
|
|||
hideImage.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
setMode,
|
||||
toggleEditMode,
|
||||
recipeImage,
|
||||
canEditRecipe,
|
||||
imageKey,
|
||||
user,
|
||||
PageMode,
|
||||
pageMode,
|
||||
EditorMode,
|
||||
editMode,
|
||||
printRecipe,
|
||||
imageHeight,
|
||||
hideImage,
|
||||
isEditMode,
|
||||
recipeImageUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
>
|
||||
<RecipeYield
|
||||
:yield-quantity="recipe.recipeYieldQuantity"
|
||||
:yield="recipe.recipeYield"
|
||||
:yield-text="recipe.recipeYield"
|
||||
:scale="recipeScale"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
@ -76,7 +76,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.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 { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeRating,
|
||||
RecipeLastMade,
|
||||
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();
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
recipeScale?: number;
|
||||
landscape: boolean;
|
||||
}
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
};
|
||||
},
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipeScale: 1,
|
||||
});
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
</script>
|
||||
|
|
|
@ -12,25 +12,21 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
maxWidth?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxWidth: undefined,
|
||||
});
|
||||
|
||||
const { $vuetify } = useNuxtApp();
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
const { imageKey } = usePageState(props.recipe.slug);
|
||||
|
@ -59,13 +55,4 @@ export default defineNuxtComponent({
|
|||
hideImage.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
recipeImageUrl,
|
||||
imageKey,
|
||||
hideImage,
|
||||
imageHeight,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
/>
|
||||
<div class="d-flex flex-wrap justify-center justify-sm-end mt-3">
|
||||
<v-tooltip
|
||||
top
|
||||
location="top"
|
||||
color="accent"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import { useToolStore } from "~/composables/store";
|
||||
|
@ -48,25 +48,15 @@ interface RecipeToolWithOnHand extends RecipeTool {
|
|||
onHand: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeIngredients,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isCookMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
scale: number;
|
||||
isCookMode?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isCookMode: false,
|
||||
});
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
||||
|
@ -106,13 +96,4 @@ export default defineNuxtComponent({
|
|||
console.log("no user, skipping server update");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
toolStore,
|
||||
recipeTools,
|
||||
isEditMode,
|
||||
updateTool,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -29,33 +29,31 @@
|
|||
{{ activeText }}
|
||||
</p>
|
||||
<v-divider class="mb-4" />
|
||||
<v-checkbox
|
||||
<v-checkbox-btn
|
||||
v-for="ing in unusedIngredients"
|
||||
:key="ing.referenceId"
|
||||
v-model="activeRefs"
|
||||
:value="ing.referenceId"
|
||||
class="mb-n2 mt-n2"
|
||||
>
|
||||
<template #label>
|
||||
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" />
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-checkbox-btn>
|
||||
|
||||
<template v-if="usedIngredients.length > 0">
|
||||
<h4 class="py-3 ml-1">
|
||||
{{ $t("recipe.linked-to-other-step") }}
|
||||
</h4>
|
||||
<v-checkbox
|
||||
<v-checkbox-btn
|
||||
v-for="ing in usedIngredients"
|
||||
:key="ing.referenceId"
|
||||
v-model="activeRefs"
|
||||
:value="ing.referenceId"
|
||||
class="mb-n2 mt-n2"
|
||||
>
|
||||
<template #label>
|
||||
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" />
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
</v-card-text>
|
||||
|
||||
|
|
|
@ -2,55 +2,26 @@
|
|||
<div class="d-flex justify-space-between align-center pt-2 pb-3">
|
||||
<RecipeScaleEditButton
|
||||
v-if="!isEditMode"
|
||||
v-model.number="scaleValue"
|
||||
v-model.number="scale"
|
||||
:recipe-servings="recipeServings"
|
||||
:edit-scale="!recipe.settings.disableAmount && !isEditMode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeScaleEditButton from "~/components/Domain/Recipe/RecipeScaleEditButton.vue";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import { usePageState } from "~/composables/recipe-page/shared-state";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeScaleEditButton,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
emits: ["update:scale"],
|
||||
setup(props, { emit }) {
|
||||
const props = defineProps<{ recipe: NoUndefinedField<Recipe> }>();
|
||||
|
||||
const scale = defineModel<number>({ default: 1 });
|
||||
|
||||
const { isEditMode } = usePageState(props.recipe.slug);
|
||||
|
||||
const recipeServings = computed<number>(() => {
|
||||
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>
|
||||
|
|
|
@ -8,24 +8,17 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipePrintView,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => Recipe,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
interface Props {
|
||||
recipe: Recipe;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
scale: 1,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -166,7 +166,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||
import { useStaticRoutes } from "~/composables/api";
|
||||
|
@ -188,25 +188,16 @@ type InstructionSection = {
|
|||
instructions: RecipeStep[];
|
||||
};
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeTimeCard,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
dense: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
scale?: number;
|
||||
dense?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
scale: 1,
|
||||
dense: false,
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const preferences = useUserPrintPreferences();
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
|
@ -219,10 +210,9 @@ export default defineNuxtComponent({
|
|||
ALLOWED_TAGS: ["strong", "sup"],
|
||||
});
|
||||
}
|
||||
|
||||
const servingsDisplay = computed(() => {
|
||||
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale);
|
||||
return scaledAmountDisplay
|
||||
return scaledAmountDisplay || props.recipe.recipeYield
|
||||
? i18n.t("recipe.yields-amount-with-text", {
|
||||
amount: scaledAmountDisplay,
|
||||
text: props.recipe.recipeYield,
|
||||
|
@ -333,22 +323,6 @@ export default defineNuxtComponent({
|
|||
function parseText(ingredient: RecipeIngredient) {
|
||||
return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false, props.scale);
|
||||
}
|
||||
|
||||
return {
|
||||
labels,
|
||||
hasNotes,
|
||||
imageKey,
|
||||
ImagePosition,
|
||||
parseText,
|
||||
parseIngredientText,
|
||||
preferences,
|
||||
recipeImageUrl,
|
||||
recipeYield,
|
||||
ingredientSections,
|
||||
instructionSections,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
nudge-top="6"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-tooltip
|
||||
v-if="canEditScale"
|
||||
size="small"
|
||||
top
|
||||
location="top"
|
||||
color="secondary-darken-1"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
|
@ -23,7 +23,7 @@
|
|||
dark
|
||||
color="secondary-darken-1"
|
||||
size="small"
|
||||
v-bind="{ ...props, ...tooltipProps }"
|
||||
v-bind="{ ...activatorProps, ...tooltipProps }"
|
||||
:style="{ cursor: canEditScale ? '' : 'default' }"
|
||||
>
|
||||
<v-icon
|
||||
|
@ -45,7 +45,7 @@
|
|||
dark
|
||||
color="secondary-darken-1"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
:style="{ cursor: canEditScale ? '' : 'default' }"
|
||||
>
|
||||
<v-icon
|
||||
|
@ -66,21 +66,22 @@
|
|||
<v-card-text class="mt-n5">
|
||||
<div class="mt-4 d-flex align-center">
|
||||
<v-text-field
|
||||
:model-value="yieldQuantityEditorValue"
|
||||
:model-value="yieldQuantity"
|
||||
type="number"
|
||||
:min="0"
|
||||
variant="underlined"
|
||||
hide-spin-buttons
|
||||
@update:model-value="recalculateScale(yieldQuantityEditorValue)"
|
||||
@update:model-value="recalculateScale(parseFloat($event) || 0)"
|
||||
/>
|
||||
<v-tooltip
|
||||
end
|
||||
location="end"
|
||||
color="secondary-darken-1"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: resetTooltipProps }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
v-bind="resetTooltipProps"
|
||||
icon
|
||||
flat
|
||||
class="mx-1"
|
||||
size="small"
|
||||
@click="scale = 1"
|
||||
|
@ -121,38 +122,24 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
recipeServings: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
editScale: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
interface Props {
|
||||
recipeServings?: number;
|
||||
editScale?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipeServings: 0,
|
||||
editScale: false,
|
||||
});
|
||||
|
||||
const scale = defineModel<number>({ required: true });
|
||||
|
||||
const i18n = useI18n();
|
||||
const menu = ref<boolean>(false);
|
||||
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) {
|
||||
if (isNaN(newYield) || newYield <= 0) {
|
||||
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(() => {
|
||||
return recipeYieldAmount.value.scaledAmount <= 1;
|
||||
});
|
||||
|
||||
return {
|
||||
menu,
|
||||
canEditScale,
|
||||
scale,
|
||||
recalculateScale,
|
||||
yieldDisplay,
|
||||
yieldQuantity,
|
||||
yieldQuantityEditorValue,
|
||||
disableDecrement,
|
||||
};
|
||||
},
|
||||
return yieldQuantity.value <= 1;
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -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>
|
|
@ -14,9 +14,7 @@
|
|||
<div v-for="(organizer, idx) in missingOrganizers" :key="idx">
|
||||
<v-col v-if="organizer.show" cols="12">
|
||||
<div class="d-flex flex-row flex-wrap align-center pt-2">
|
||||
<v-icon class="ma-0 pa-0">
|
||||
{{ organizer.icon }}
|
||||
</v-icon>
|
||||
<v-icon class="ma-0 pa-0" />
|
||||
<v-card-text class="mr-0 my-0 pl-1 py-0" style="width: min-content">
|
||||
{{ $t("recipe-finder.missing") }}:
|
||||
</v-card-text>
|
||||
|
@ -41,7 +39,7 @@
|
|||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import type { IngredientFood, RecipeSummary, RecipeTool } from "~/lib/api/types/recipe";
|
||||
|
||||
|
@ -51,27 +49,25 @@ interface Organizer {
|
|||
selected: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardMobile },
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => RecipeSummary,
|
||||
required: true,
|
||||
},
|
||||
missingFoods: {
|
||||
type: Array as () => IngredientFood[] | null,
|
||||
default: null,
|
||||
},
|
||||
missingTools: {
|
||||
type: Array as () => RecipeTool[] | null,
|
||||
default: null,
|
||||
},
|
||||
disableCheckbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
interface Props {
|
||||
recipe: RecipeSummary;
|
||||
missingFoods?: IngredientFood[] | null;
|
||||
missingTools?: RecipeTool[] | null;
|
||||
disableCheckbox?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
missingFoods: null,
|
||||
missingTools: null,
|
||||
disableCheckbox: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"add-food": [food: IngredientFood];
|
||||
"remove-food": [food: IngredientFood];
|
||||
"add-tool": [tool: RecipeTool];
|
||||
"remove-tool": [tool: RecipeTool];
|
||||
}>();
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
const missingOrganizers = computed(() => [
|
||||
{
|
||||
|
@ -105,17 +101,20 @@ export default defineNuxtComponent({
|
|||
|
||||
organizer.selected = !organizer.selected;
|
||||
if (organizer.selected) {
|
||||
context.emit(`add-${organizer.type}`, organizer.item);
|
||||
if (organizer.type === "food") {
|
||||
emit("add-food", organizer.item as IngredientFood);
|
||||
}
|
||||
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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<template v-if="showCards">
|
||||
<template v-if="_showCards">
|
||||
<div class="text-center">
|
||||
<!-- Total Time -->
|
||||
<div
|
||||
|
@ -78,38 +78,29 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
prepTime: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
totalTime: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
performTime: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "accent custom-transparent",
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
prepTime?: string | null;
|
||||
totalTime?: string | null;
|
||||
performTime?: string | null;
|
||||
color?: string;
|
||||
small?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
prepTime: null,
|
||||
totalTime: null,
|
||||
performTime: null,
|
||||
color: "accent custom-transparent",
|
||||
small: false,
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
function isEmpty(str: string | null) {
|
||||
return !str || str.length === 0;
|
||||
}
|
||||
|
||||
const showCards = computed(() => {
|
||||
const _showCards = computed(() => {
|
||||
return [props.prepTime, props.totalTime, props.performTime].some(x => !isEmpty(x));
|
||||
});
|
||||
|
||||
|
@ -128,16 +119,6 @@ export default defineNuxtComponent({
|
|||
const fontSize = computed(() => {
|
||||
return props.small ? { fontSize: "smaller" } : { fontSize: "larger" };
|
||||
});
|
||||
|
||||
return {
|
||||
showCards,
|
||||
validateTotalTime,
|
||||
validatePrepTime,
|
||||
validatePerformTime,
|
||||
fontSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
nudge-bottom="3"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-badge
|
||||
:content="filterBadgeCount"
|
||||
:model-value="filterBadgeCount > 0"
|
||||
|
@ -21,7 +21,7 @@
|
|||
class="rounded-circle"
|
||||
size="small"
|
||||
color="info"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
icon
|
||||
>
|
||||
<v-icon> {{ $globals.icons.filter }} </v-icon>
|
||||
|
@ -105,7 +105,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useThrottleFn, whenever } from "@vueuse/core";
|
||||
import RecipeTimelineItem from "./RecipeTimelineItem.vue";
|
||||
import { useTimelinePreferences } from "~/composables/use-users/preferences";
|
||||
|
@ -115,29 +115,19 @@ import { alert } from "~/composables/use-toast";
|
|||
import { useUserApi } from "~/composables/api";
|
||||
import type { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate, TimelineEventType } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeTimelineItem },
|
||||
interface Props {
|
||||
modelValue?: boolean;
|
||||
queryFilter: string;
|
||||
maxHeight?: number | string;
|
||||
showRecipeCards?: boolean;
|
||||
}
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
queryFilter: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
maxHeight: {
|
||||
type: [Number, String],
|
||||
default: undefined,
|
||||
},
|
||||
showRecipeCards: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
maxHeight: undefined,
|
||||
showRecipeCards: false,
|
||||
});
|
||||
|
||||
setup(props) {
|
||||
const api = useUserApi();
|
||||
const i18n = useI18n();
|
||||
const preferences = useTimelinePreferences();
|
||||
|
@ -160,25 +150,7 @@ export default defineNuxtComponent({
|
|||
};
|
||||
});
|
||||
});
|
||||
|
||||
interface ScrollEvent extends Event {
|
||||
target: HTMLInputElement;
|
||||
}
|
||||
|
||||
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(
|
||||
() => props.modelValue,
|
||||
|
@ -229,7 +201,7 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
alert.success(i18n.t("events.event-updated") as string);
|
||||
};
|
||||
}
|
||||
|
||||
async function deleteTimelineEvent(index: number) {
|
||||
const { response } = await api.recipes.deleteTimelineEvent(timelineEvents.value[index].id);
|
||||
|
@ -240,30 +212,30 @@ export default defineNuxtComponent({
|
|||
|
||||
timelineEvents.value.splice(index, 1);
|
||||
alert.success(i18n.t("events.event-deleted") as string);
|
||||
};
|
||||
}
|
||||
|
||||
async function getRecipe(recipeId: string): Promise<Recipe | null> {
|
||||
const { data } = await api.recipes.getOne(recipeId);
|
||||
return data;
|
||||
};
|
||||
async function getRecipes(recipeIds: string[]): Promise<Recipe[]> {
|
||||
const qf = "id IN [" + recipeIds.map(id => `"${id}"`).join(", ") + "]";
|
||||
const { data } = await api.recipes.getAll(1, -1, { queryFilter: qf });
|
||||
return data?.items || [];
|
||||
}
|
||||
|
||||
async function updateRecipes(events: RecipeTimelineEventOut[]) {
|
||||
const recipePromises: Promise<Recipe | null>[] = [];
|
||||
const seenRecipeIds: string[] = [];
|
||||
const recipeIds: string[] = [];
|
||||
events.forEach((event) => {
|
||||
if (seenRecipeIds.includes(event.recipeId) || recipes.has(event.recipeId)) {
|
||||
if (recipeIds.includes(event.recipeId) || recipes.has(event.recipeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
seenRecipeIds.push(event.recipeId);
|
||||
recipePromises.push(getRecipe(event.recipeId));
|
||||
recipeIds.push(event.recipeId);
|
||||
});
|
||||
|
||||
const results = await Promise.all(recipePromises);
|
||||
const results = await getRecipes(recipeIds);
|
||||
results.forEach((result) => {
|
||||
if (result && result.id) {
|
||||
recipes.set(result.id, result);
|
||||
if (!result?.id) {
|
||||
return;
|
||||
}
|
||||
recipes.set(result.id, result);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -295,7 +267,7 @@ export default defineNuxtComponent({
|
|||
|
||||
// this is set last so Vue knows to re-render
|
||||
timelineEvents.value.push(...events);
|
||||
};
|
||||
}
|
||||
|
||||
async function initializeTimelineEvents() {
|
||||
loading.value = true;
|
||||
|
@ -347,20 +319,4 @@ export default defineNuxtComponent({
|
|||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
deleteTimelineEvent,
|
||||
filterBadgeCount,
|
||||
loading,
|
||||
onScroll,
|
||||
preferences,
|
||||
eventTypeFilterState,
|
||||
recipes,
|
||||
reverseSort,
|
||||
toggleEventTypeOption,
|
||||
timelineEvents,
|
||||
updateTimelineEvent,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<v-tooltip
|
||||
bottom
|
||||
location="bottom"
|
||||
nudge-right="50"
|
||||
:color="buttonStyle ? 'info' : 'secondary'"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
icon
|
||||
:variant="buttonStyle ? 'flat' : undefined"
|
||||
|
@ -12,7 +12,7 @@
|
|||
size="small"
|
||||
:color="buttonStyle ? 'info' : 'secondary'"
|
||||
:fab="buttonStyle"
|
||||
v-bind="{ ...props, ...$attrs }"
|
||||
v-bind="{ ...activatorProps, ...$attrs }"
|
||||
@click.prevent="toggleTimeline"
|
||||
>
|
||||
<v-icon
|
||||
|
@ -39,31 +39,24 @@
|
|||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeTimeline from "./RecipeTimeline.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeTimeline },
|
||||
interface Props {
|
||||
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 { smAndDown } = useDisplay();
|
||||
const showTimeline = ref(false);
|
||||
|
||||
function toggleTimeline() {
|
||||
showTimeline.value = !showTimeline.value;
|
||||
}
|
||||
|
@ -79,8 +72,4 @@ export default defineNuxtComponent({
|
|||
queryFilter: `recipe.slug="${props.slug}"`,
|
||||
};
|
||||
});
|
||||
|
||||
return { showTimeline, timelineAttrs, toggleTimeline };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
<v-row :class="useMobileFormat ? 'py-3 mx-0' : 'py-3 mx-0'" style="max-width: 100%">
|
||||
<v-col align-self="center" class="pa-0">
|
||||
<RecipeCardMobile
|
||||
disable-highlight
|
||||
:vertical="useMobileFormat"
|
||||
:name="recipe.name"
|
||||
:slug="recipe.slug"
|
||||
|
@ -90,7 +91,7 @@
|
|||
</v-timeline-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
|
||||
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 SafeMarkdown from "~/components/global/SafeMarkdown.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardMobile, RecipeTimelineContextMenu, UserAvatar, SafeMarkdown },
|
||||
interface Props {
|
||||
event: RecipeTimelineEventOut;
|
||||
recipe?: Recipe;
|
||||
showRecipeCards?: boolean;
|
||||
}
|
||||
|
||||
props: {
|
||||
event: {
|
||||
type: Object as () => RecipeTimelineEventOut,
|
||||
required: true,
|
||||
},
|
||||
recipe: {
|
||||
type: Object as () => Recipe,
|
||||
default: undefined,
|
||||
},
|
||||
showRecipeCards: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["selected", "update", "delete"],
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipe: undefined,
|
||||
showRecipeCards: false,
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
selected: [];
|
||||
update: [];
|
||||
delete: [];
|
||||
}>();
|
||||
|
||||
setup(props) {
|
||||
const { $vuetify, $globals } = useNuxtApp();
|
||||
const { recipeTimelineEventImage } = useStaticRoutes();
|
||||
const { eventTypeOptions } = useTimelineEventTypes();
|
||||
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
|
||||
|
||||
const { user: currentUser } = useMealieAuth();
|
||||
|
||||
|
@ -177,19 +174,6 @@ export default defineNuxtComponent({
|
|||
|
||||
return recipeTimelineEventImage(props.event.recipeId, props.event.id);
|
||||
});
|
||||
|
||||
return {
|
||||
attrs,
|
||||
groupSlug,
|
||||
icon,
|
||||
eventImageUrl,
|
||||
hideImage,
|
||||
timelineEvents,
|
||||
useMobileFormat,
|
||||
currentUser,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="scaledAmount"
|
||||
v-if="yieldDisplay"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-row
|
||||
|
@ -18,36 +18,29 @@
|
|||
<p class="my-0 opacity-80">
|
||||
<span class="font-weight-bold">{{ $t("recipe.yield") }}</span><br>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="scaledAmount" /> {{ text }}
|
||||
<span v-html="yieldDisplay" />
|
||||
</p>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
yieldQuantity: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
yield: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "accent custom-transparent",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
interface Props {
|
||||
yieldQuantity?: number;
|
||||
yieldText?: string;
|
||||
scale?: number;
|
||||
color?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
yieldQuantity: 0,
|
||||
yieldText: "",
|
||||
scale: 1,
|
||||
color: "accent custom-transparent",
|
||||
});
|
||||
|
||||
function sanitizeHTML(rawHtml: string) {
|
||||
return DOMPurify.sanitize(rawHtml, {
|
||||
USE_PROFILES: { html: true },
|
||||
|
@ -55,16 +48,19 @@ export default defineNuxtComponent({
|
|||
});
|
||||
}
|
||||
|
||||
const scaledAmount = computed(() => {
|
||||
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
|
||||
return scaledAmountDisplay;
|
||||
});
|
||||
const text = sanitizeHTML(props.yield);
|
||||
const yieldDisplay = computed<string>(() => {
|
||||
const components: string[] = [];
|
||||
|
||||
return {
|
||||
scaledAmount,
|
||||
text,
|
||||
};
|
||||
},
|
||||
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
|
||||
if (scaledAmountDisplay) {
|
||||
components.push(scaledAmountDisplay);
|
||||
}
|
||||
|
||||
const text = props.yieldText;
|
||||
if (text) {
|
||||
components.push(text);
|
||||
}
|
||||
|
||||
return sanitizeHTML(components.join(" "));
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
v-if="requireAll != undefined"
|
||||
v-model="requireAllValue"
|
||||
density="compact"
|
||||
size="small"
|
||||
hide-details
|
||||
class="my-auto"
|
||||
color="primary"
|
||||
|
|
|
@ -8,24 +8,26 @@
|
|||
class="flex-nowrap align-center"
|
||||
>
|
||||
<v-col :cols="itemLabelCols">
|
||||
<div class="d-flex align-center flex-nowrap">
|
||||
<v-checkbox
|
||||
v-model="listItem.checked"
|
||||
class="mt-0"
|
||||
color="null"
|
||||
hide-details
|
||||
density="compact"
|
||||
:label="listItem.note!"
|
||||
class="mt-0"
|
||||
color="null"
|
||||
@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
|
||||
:ingredient="listItem"
|
||||
:disable-amount="!(listItem.isFood || listItem.quantity !== 1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col
|
||||
|
@ -57,7 +59,7 @@
|
|||
open-delay="200"
|
||||
transition="slide-x-reverse-transition"
|
||||
density="compact"
|
||||
right
|
||||
location="end"
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-tooltip
|
||||
v-if="userId"
|
||||
:disabled="!user || !tooltip"
|
||||
right
|
||||
location="end"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-avatar
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-tooltip
|
||||
ref="copyToolTip"
|
||||
v-model="show"
|
||||
top
|
||||
location="top"
|
||||
:open-on-hover="false"
|
||||
:open-on-click="true"
|
||||
close-delay="500"
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
open-delay="200"
|
||||
transition="slide-y-reverse-transition"
|
||||
density="compact"
|
||||
bottom
|
||||
location="bottom"
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
<BaseButton
|
||||
v-if="canSubmit"
|
||||
type="submit"
|
||||
:disabled="submitDisabled"
|
||||
:disabled="submitDisabled || loading"
|
||||
@click="submitEvent"
|
||||
>
|
||||
{{ submitText }}
|
||||
|
|
|
@ -22,10 +22,9 @@
|
|||
<v-card>
|
||||
<v-card-text>
|
||||
<v-checkbox
|
||||
v-for="itemValue in headers"
|
||||
v-for="itemValue in localHeaders"
|
||||
:key="itemValue.text + itemValue.show"
|
||||
v-model="filteredHeaders"
|
||||
:value="itemValue.value"
|
||||
v-model="itemValue.show"
|
||||
density="compact"
|
||||
flat
|
||||
inset
|
||||
|
@ -42,7 +41,7 @@
|
|||
color="info"
|
||||
variant="elevated"
|
||||
:items="bulkActions"
|
||||
v-bind="bulkActionListener"
|
||||
v-on="bulkActionListener"
|
||||
/>
|
||||
<slot name="button-row" />
|
||||
</v-card-actions>
|
||||
|
@ -55,7 +54,7 @@
|
|||
</div>
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
item-key="id"
|
||||
return-object
|
||||
:headers="activeHeaders"
|
||||
:show-select="bulkActions.length > 0"
|
||||
:sort-by="sortBy"
|
||||
|
@ -172,12 +171,20 @@ export default defineNuxtComponent({
|
|||
|
||||
// ===========================================================
|
||||
// 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[]>(() => {
|
||||
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(() =>
|
||||
props.headers
|
||||
localHeaders.value
|
||||
.filter(header => filteredHeaders.value.includes(header.value))
|
||||
.map(header => ({
|
||||
...header,
|
||||
|
@ -214,6 +221,7 @@ export default defineNuxtComponent({
|
|||
return {
|
||||
sortBy,
|
||||
selected,
|
||||
localHeaders,
|
||||
filteredHeaders,
|
||||
headersWithoutActions,
|
||||
activeHeaders,
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
<v-card-text>
|
||||
{{ $t("language-dialog.select-description") }}
|
||||
<v-autocomplete
|
||||
v-model="locale"
|
||||
v-model="selectedLocale"
|
||||
:items="locales"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
class="my-3"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
offset
|
||||
@update:model-value="onLocaleSelect"
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
<div
|
||||
|
@ -59,6 +60,14 @@ export default defineNuxtComponent({
|
|||
});
|
||||
|
||||
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, () => {
|
||||
dialog.value = false; // Close dialog when locale changes
|
||||
});
|
||||
|
@ -72,6 +81,8 @@ export default defineNuxtComponent({
|
|||
i18n,
|
||||
locales,
|
||||
locale,
|
||||
selectedLocale,
|
||||
onLocaleSelect,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,10 +26,10 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(_, { emit }) {
|
||||
setup(props, { emit }) {
|
||||
function parseEvent(event: any): object {
|
||||
if (!event) {
|
||||
return {};
|
||||
return props.modelValue || {};
|
||||
}
|
||||
try {
|
||||
if (event.json) {
|
||||
|
@ -43,11 +43,14 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
catch {
|
||||
return {};
|
||||
return props.modelValue || {};
|
||||
}
|
||||
}
|
||||
function onChange(event: any) {
|
||||
emit("update:modelValue", parseEvent(event));
|
||||
const parsed = parseEvent(event);
|
||||
if (parsed !== props.modelValue) {
|
||||
emit("update:modelValue", parsed);
|
||||
}
|
||||
}
|
||||
return {
|
||||
onChange,
|
||||
|
|
|
@ -90,6 +90,8 @@ export const useLazyRecipes = function (publicGroupSlug: 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));
|
||||
if (data?.items.length) {
|
||||
return data.items[0];
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useStoreActions } from "./partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
||||
import type { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household";
|
||||
import type { RequestResponse } from "~/lib/api/types/non-generated";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
|
@ -68,7 +67,7 @@ export const useGroupRecipeActions = function (
|
|||
window.open(url, "_blank")?.focus();
|
||||
return;
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export const useGroupWebhooks = function () {
|
|||
loading.value = true;
|
||||
|
||||
const payload = {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
name: "New Webhook",
|
||||
url: "",
|
||||
scheduledTime: "00:00",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 loading = ref(false);
|
||||
|
@ -45,28 +45,11 @@ export const useGroupSelf = function () {
|
|||
export const useGroups = function () {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
const groups = ref<GroupInDB[] | null>(null);
|
||||
|
||||
function getAllGroups() {
|
||||
async function getAllGroups() {
|
||||
loading.value = true;
|
||||
const asyncKey = String(Date.now());
|
||||
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" }); ;
|
||||
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||
|
||||
if (data) {
|
||||
groups.value = data.items;
|
||||
|
@ -78,11 +61,15 @@ export const useGroups = function () {
|
|||
loading.value = false;
|
||||
}
|
||||
|
||||
async function refreshAllGroups() {
|
||||
await getAllGroups();
|
||||
}
|
||||
|
||||
async function deleteGroup(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.groups.deleteOne(id);
|
||||
loading.value = false;
|
||||
refreshAllGroups();
|
||||
await refreshAllGroups();
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -93,9 +80,13 @@ export const useGroups = function () {
|
|||
if (data && groups.value) {
|
||||
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 };
|
||||
};
|
||||
|
|
|
@ -48,29 +48,12 @@ export const useHouseholdSelf = function () {
|
|||
export const useAdminHouseholds = function () {
|
||||
const api = useAdminApi();
|
||||
const loading = ref(false);
|
||||
const households = ref<HouseholdInDB[] | null>(null);
|
||||
|
||||
function getAllHouseholds() {
|
||||
async function getAllHouseholds() {
|
||||
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" });
|
||||
|
||||
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) {
|
||||
households.value = data.items;
|
||||
}
|
||||
|
@ -81,11 +64,15 @@ export const useAdminHouseholds = function () {
|
|||
loading.value = false;
|
||||
}
|
||||
|
||||
async function refreshAllHouseholds() {
|
||||
await getAllHouseholds();
|
||||
}
|
||||
|
||||
async function deleteHousehold(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.households.deleteOne(id);
|
||||
loading.value = false;
|
||||
refreshAllHouseholds();
|
||||
await refreshAllHouseholds();
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -96,9 +83,9 @@ export const useAdminHouseholds = function () {
|
|||
if (data && households.value) {
|
||||
households.value.push(data);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const households = getAllHouseholds();
|
||||
function useHouseholdsInGroup(groupIdRef: Ref<string>) {
|
||||
return computed(
|
||||
() => {
|
||||
|
@ -109,6 +96,10 @@ export const useAdminHouseholds = function () {
|
|||
);
|
||||
}
|
||||
|
||||
if (!households.value) {
|
||||
getAllHouseholds();
|
||||
}
|
||||
|
||||
return {
|
||||
households,
|
||||
useHouseholdsInGroup,
|
||||
|
|
|
@ -3,13 +3,13 @@ export const LOCALES = [
|
|||
{
|
||||
name: "繁體中文 (Chinese traditional)",
|
||||
value: "zh-TW",
|
||||
progress: 8,
|
||||
progress: 9,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
name: "简体中文 (Chinese simplified)",
|
||||
value: "zh-CN",
|
||||
progress: 33,
|
||||
progress: 35,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -33,7 +33,7 @@ export const LOCALES = [
|
|||
{
|
||||
name: "Svenska (Swedish)",
|
||||
value: "sv-SE",
|
||||
progress: 39,
|
||||
progress: 50,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -57,7 +57,7 @@ export const LOCALES = [
|
|||
{
|
||||
name: "Pусский (Russian)",
|
||||
value: "ru-RU",
|
||||
progress: 35,
|
||||
progress: 37,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -75,7 +75,7 @@ export const LOCALES = [
|
|||
{
|
||||
name: "Português do Brasil (Brazilian Portuguese)",
|
||||
value: "pt-BR",
|
||||
progress: 34,
|
||||
progress: 36,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -87,13 +87,13 @@ export const LOCALES = [
|
|||
{
|
||||
name: "Norsk (Norwegian)",
|
||||
value: "no-NO",
|
||||
progress: 37,
|
||||
progress: 39,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
name: "Nederlands (Dutch)",
|
||||
value: "nl-NL",
|
||||
progress: 39,
|
||||
progress: 45,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -123,7 +123,7 @@ export const LOCALES = [
|
|||
{
|
||||
name: "Italiano (Italian)",
|
||||
value: "it-IT",
|
||||
progress: 37,
|
||||
progress: 38,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -135,7 +135,7 @@ export const LOCALES = [
|
|||
{
|
||||
name: "Magyar (Hungarian)",
|
||||
value: "hu-HU",
|
||||
progress: 38,
|
||||
progress: 40,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -147,19 +147,19 @@ export const LOCALES = [
|
|||
{
|
||||
name: "עברית (Hebrew)",
|
||||
value: "he-IL",
|
||||
progress: 37,
|
||||
progress: 67,
|
||||
dir: "rtl",
|
||||
},
|
||||
{
|
||||
name: "Galego (Galician)",
|
||||
value: "gl-ES",
|
||||
progress: 37,
|
||||
progress: 38,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
name: "Français (French)",
|
||||
value: "fr-FR",
|
||||
progress: 38,
|
||||
progress: 49,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -189,7 +189,7 @@ export const LOCALES = [
|
|||
{
|
||||
name: "Español (Spanish)",
|
||||
value: "es-ES",
|
||||
progress: 37,
|
||||
progress: 40,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
@ -201,31 +201,31 @@ export const LOCALES = [
|
|||
{
|
||||
name: "British English",
|
||||
value: "en-GB",
|
||||
progress: 22,
|
||||
progress: 23,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
name: "Ελληνικά (Greek)",
|
||||
value: "el-GR",
|
||||
progress: 37,
|
||||
progress: 38,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
name: "Deutsch (German)",
|
||||
value: "de-DE",
|
||||
progress: 46,
|
||||
progress: 64,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
name: "Dansk (Danish)",
|
||||
value: "da-DK",
|
||||
progress: 37,
|
||||
progress: 38,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
name: "Čeština (Czech)",
|
||||
value: "cs-CZ",
|
||||
progress: 37,
|
||||
progress: 39,
|
||||
dir: "ltr",
|
||||
},
|
||||
{
|
||||
|
|
|
@ -3,8 +3,6 @@ import { LOCALES } from "./available-locales";
|
|||
|
||||
export const useLocales = () => {
|
||||
const i18n = useI18n();
|
||||
|
||||
const { isRtl } = useRtl();
|
||||
const { current: vuetifyLocale } = useLocale();
|
||||
|
||||
const locale = computed<LocaleObject["code"]>({
|
||||
|
@ -13,18 +11,21 @@ export const useLocales = () => {
|
|||
i18n.setLocale(value);
|
||||
},
|
||||
});
|
||||
|
||||
function updateLocale(lc: LocaleObject["code"]) {
|
||||
vuetifyLocale.value = lc;
|
||||
}
|
||||
|
||||
// auto update vuetify locale
|
||||
watch(locale, (lc) => {
|
||||
vuetifyLocale.value = lc;
|
||||
});
|
||||
// auto update rtl
|
||||
watch(vuetifyLocale, (vl) => {
|
||||
const currentLocale = LOCALES.find(lc => lc.value === vl);
|
||||
if (currentLocale) {
|
||||
isRtl.value = currentLocale.dir === "rtl";
|
||||
}
|
||||
updateLocale(lc);
|
||||
});
|
||||
|
||||
// set initial locale
|
||||
if (i18n.locale.value) {
|
||||
updateLocale(i18n.locale.value);
|
||||
};
|
||||
|
||||
return {
|
||||
locale,
|
||||
locales: LOCALES,
|
||||
|
|
|
@ -1,47 +1,48 @@
|
|||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const datetimeFormats = {
|
||||
// CODE_GEN_ID: DATE_LOCALES
|
||||
"hu-HU": require("./lang/dateTimeFormats/hu-HU.json"),
|
||||
"no-NO": require("./lang/dateTimeFormats/no-NO.json"),
|
||||
"nl-NL": require("./lang/dateTimeFormats/nl-NL.json"),
|
||||
"pl-PL": require("./lang/dateTimeFormats/pl-PL.json"),
|
||||
"af-ZA": require("./lang/dateTimeFormats/af-ZA.json"),
|
||||
"ar-SA": require("./lang/dateTimeFormats/ar-SA.json"),
|
||||
"bg-BG": require("./lang/dateTimeFormats/bg-BG.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"),
|
||||
"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-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"),
|
||||
"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"),
|
||||
"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"),
|
||||
"ar-SA": require("./lang/dateTimeFormats/ar-SA.json"),
|
||||
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
|
||||
"tr-TR": require("./lang/dateTimeFormats/tr-TR.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
|
||||
};
|
||||
|
||||
|
|
21
frontend/lang/dateTimeFormats/et-EE.json
Normal file
21
frontend/lang/dateTimeFormats/et-EE.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Lewer kommentaar",
|
||||
"comments": "Kommentaar",
|
||||
"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",
|
||||
"description": "Beskrywing",
|
||||
"disable-amount": "Skakel bestanddeelhoeveelhede af",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Ek het dit gemaak",
|
||||
"how-did-it-turn-out": "Hoe het dit uitgedraai?",
|
||||
"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.",
|
||||
"message-key": "Boodskap sleutel",
|
||||
"parse": "Verwerk",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "No unit",
|
||||
"missing-unit": "Create missing unit: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reset Servings Count",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "أضف تعليق ",
|
||||
"comments": "التعليقات",
|
||||
"delete-confirmation": "هل انت متأكد من رغبتك بحذف هذه الوصفة؟",
|
||||
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||
"delete-recipe": "حذف الوصفة",
|
||||
"description": "الوصف",
|
||||
"disable-amount": "إيقاف إظهار كميات المكونات",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "لقد طبخت هذا",
|
||||
"how-did-it-turn-out": "كيف كانت النتيجة؟",
|
||||
"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.",
|
||||
"message-key": "مفتاح الرساله",
|
||||
"parse": "تحليل",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "لا توجد وحدة",
|
||||
"missing-unit": "إنشاء وحدة مفقودة: {unit}",
|
||||
"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": "لا يوجد طعام"
|
||||
},
|
||||
"reset-servings-count": "إعادة تعيين عدد الحصص",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Коментар",
|
||||
"comments": "Коментари",
|
||||
"delete-confirmation": "Сигурни ли сте, че желаете да изтриете тази рецепта?",
|
||||
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||
"delete-recipe": "Изтрий рецептата",
|
||||
"description": "Описание",
|
||||
"disable-amount": "Изключи количествата за съставките",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Сготвих рецептата",
|
||||
"how-did-it-turn-out": "Как се получи?",
|
||||
"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 двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.",
|
||||
"message-key": "Ключ на съобщението",
|
||||
"parse": "Анализирай",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "No unit",
|
||||
"missing-unit": "Create missing unit: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reset Servings Count",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Comentari",
|
||||
"comments": "Comentaris",
|
||||
"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",
|
||||
"description": "Descripció",
|
||||
"disable-amount": "Oculta les quantitats",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Ho he fet",
|
||||
"how-did-it-turn-out": "Com ha sortit?",
|
||||
"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.",
|
||||
"message-key": "Clau del missatge",
|
||||
"parse": "Analitzar",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "Sense unitat",
|
||||
"missing-unit": "Crear unitat que manca: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reiniciar racions servides",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"api-docs": "Dokumentace API",
|
||||
"api-port": "API port",
|
||||
"application-mode": "Režim aplikace",
|
||||
"database-type": "Typ databáze",
|
||||
"database-type": "Database Type",
|
||||
"database-url": "URL databáze",
|
||||
"default-group": "Výchozí skupina",
|
||||
"default-household": "Výchozí domácnost",
|
||||
|
@ -472,6 +472,7 @@
|
|||
"comment": "Komentář",
|
||||
"comments": "Komentáře",
|
||||
"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",
|
||||
"description": "Popis",
|
||||
"disable-amount": "Nezobrazovat množství ingrediencí",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Toto jsem uvařil",
|
||||
"how-did-it-turn-out": "Jak to dopadlo?",
|
||||
"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í.",
|
||||
"message-key": "Klíč zprávy",
|
||||
"parse": "Analyzovat",
|
||||
|
@ -658,13 +663,15 @@
|
|||
"no-unit": "Žádná jednotka",
|
||||
"missing-unit": "Vytvořit chybějící jednotku: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Resetovat počet porcí",
|
||||
"not-linked-ingredients": "Další ingredience",
|
||||
"upload-another-image": "Upload another image",
|
||||
"upload-images": "Upload images",
|
||||
"upload-more-images": "Upload more images"
|
||||
"upload-another-image": "Nahrát další obrázek",
|
||||
"upload-images": "Nahrát obrázky",
|
||||
"upload-more-images": "Nahrát více obrázků"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Vyhledávač receptů",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Kommentar",
|
||||
"comments": "Kommentarer",
|
||||
"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",
|
||||
"description": "Beskrivelse",
|
||||
"disable-amount": "Slå ingrediensmængder fra",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Jeg har lavet denne",
|
||||
"how-did-it-turn-out": "Hvordan blev det?",
|
||||
"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.",
|
||||
"message-key": "Beskednøgle",
|
||||
"parse": "Behandl data",
|
||||
|
@ -599,10 +604,10 @@
|
|||
"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.",
|
||||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -658,13 +663,15 @@
|
|||
"no-unit": "Ingen enhed",
|
||||
"missing-unit": "Opret manglende måleenhed: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Nulstil antal serveringer",
|
||||
"not-linked-ingredients": "Yderligere ingredienser",
|
||||
"upload-another-image": "Upload another image",
|
||||
"upload-images": "Upload images",
|
||||
"upload-more-images": "Upload more images"
|
||||
"upload-another-image": "Upload et andet billede",
|
||||
"upload-images": "Upload billeder",
|
||||
"upload-more-images": "Upload flere billeder"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Opskriftssøger",
|
||||
|
@ -724,7 +731,7 @@
|
|||
"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.",
|
||||
"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",
|
||||
"irreversible-acknowledgment": "Jeg forstår, at denne handling er irreversibel, destruktiv og kan forårsage tab af data",
|
||||
"restore-backup": "Gendan sikkerhedskopi"
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Kommentar",
|
||||
"comments": "Kommentare",
|
||||
"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",
|
||||
"description": "Beschreibung",
|
||||
"disable-amount": "Zutatenmenge deaktivieren",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Ich hab's gemacht",
|
||||
"how-did-it-turn-out": "Wie ist es geworden?",
|
||||
"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.",
|
||||
"message-key": "Nachrichten-Schlüssel",
|
||||
"parse": "Parsen",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "Keine Einheit",
|
||||
"missing-unit": "Fehlende Einheit erstellen: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Portionen zurücksetzen",
|
||||
|
@ -1005,7 +1012,7 @@
|
|||
"webhooks-enabled": "Webhooks aktiviert",
|
||||
"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",
|
||||
"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.",
|
||||
"favorite-recipes": "Favoriten",
|
||||
"email-or-username": "E-Mail oder Benutzername",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Σχόλιο",
|
||||
"comments": "Σχόλια",
|
||||
"delete-confirmation": "Θέλετε σίγουρα να διαγράψετε αυτή τη συνταγή;",
|
||||
"admin-delete-confirmation": "Πρόκειται να διαγράψετε μια συνταγή που δεν είναι δική σας χρησιμοποιώντας δικαιώματα διαχειριστή. Είστε σίγουρος/η;",
|
||||
"delete-recipe": "Διαγραφή Συνταγής",
|
||||
"description": "Περιγραφή",
|
||||
"disable-amount": "Απενεργοποίηση Ποσοτήτων Συστατικών",
|
||||
|
@ -538,7 +539,7 @@
|
|||
"date-format-hint-yyyy-mm-dd": "Μορφή ΕΕΕΕ-ΜΜ-ΗΗ",
|
||||
"add-to-list": "Προσθήκη σε λίστα",
|
||||
"add-to-plan": "Προσθήκη σε πρόγραμμα γευμάτων",
|
||||
"add-to-timeline": "Προσθήκη στο χρονοδιάγραμμα",
|
||||
"add-to-timeline": "Προσθήκη στο χρονολόγιο",
|
||||
"recipe-added-to-list": "Η συνταγή προστέθηκε στη λίστα",
|
||||
"recipes-added-to-list": "Οι συνταγές προστέθηκαν στη λίστα",
|
||||
"successfully-added-to-list": "Επιτυχής προσθήκη στη λίστα",
|
||||
|
@ -570,15 +571,19 @@
|
|||
"increase-scale-label": "Αύξηση κλίμακας κατά 1",
|
||||
"locked": "Κλειδωμένο",
|
||||
"public-link": "Δημόσιος σύνδεσμος",
|
||||
"edit-timeline-event": "Επεξεργασία συμβάντος χρονοδιαγράμματος",
|
||||
"timeline": "Χρονοδιάγραμμα",
|
||||
"timeline-is-empty": "Δεν υπάρχει τίποτα ακόμα στο χρονοδιάγραμμα. Δοκιμάστε να κάνετε αυτή τη συνταγή!",
|
||||
"edit-timeline-event": "Επεξεργασία συμβάντος χρονολόγιου",
|
||||
"timeline": "Χρονολόγιο",
|
||||
"timeline-is-empty": "Δεν υπάρχει τίποτα ακόμα στο χρονολόγιο. Δοκιμάστε να κάνετε αυτή τη συνταγή!",
|
||||
"timeline-no-events-found-try-adjusting-filters": "Δεν βρέθηκαν συμβαντα. Δοκιμάστε να προσαρμόσετε τα φίλτρα αναζήτησης.",
|
||||
"group-global-timeline": "Συνολικό χρονοδιάγραμμα {groupName}",
|
||||
"open-timeline": "Ανοιγμα χρονοδιαγράμματος",
|
||||
"group-global-timeline": "Συνολικό χρονολόγιο {groupName}",
|
||||
"open-timeline": "Ανοιγμα χρονολόγιου",
|
||||
"made-this": "Το έφτιαξα",
|
||||
"how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;",
|
||||
"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 μέσα σε μια συνταγή, να παραπέμψετε σε εφαρμογές τρίτων. Μπορείτε να χρησιμοποιήσετε αυτά τα κλειδιά για την παροχή πληροφοριών, για παράδειγμα πυροδότηση αυτοματισμών ή μετάδοση προσαρμοσμένων μηνυμάτων στη συσκευή που επιθυμείτε.",
|
||||
"message-key": "Κλειδί Μηνύματος",
|
||||
"parse": "Ανάλυση",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "Καμία μονάδα",
|
||||
"missing-unit": "Δημιουργία μονάδας που λείπει: {unit}",
|
||||
"missing-food": "Δημιουργία τροφίμου που λείπει: {food}",
|
||||
"this-unit-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτής της μονάδας",
|
||||
"this-food-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτού του φαγητού",
|
||||
"no-food": "Χωρίς Τρόφιμο"
|
||||
},
|
||||
"reset-servings-count": "Επαναφορά μέτρησης μερίδων",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Comment",
|
||||
"comments": "Comments",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"disable-amount": "Disable Ingredient Amounts",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "I Made This",
|
||||
"how-did-it-turn-out": "How did it turn out?",
|
||||
"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.",
|
||||
"message-key": "Message Key",
|
||||
"parse": "Parse",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "No unit",
|
||||
"missing-unit": "Create missing unit: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reset Servings Count",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Comment",
|
||||
"comments": "Comments",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"disable-amount": "Disable Ingredient Amounts",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "I Made This",
|
||||
"how-did-it-turn-out": "How did it turn out?",
|
||||
"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.",
|
||||
"message-key": "Message Key",
|
||||
"parse": "Parse",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "No unit",
|
||||
"missing-unit": "Create missing unit: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reset Servings Count",
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"about": {
|
||||
"about": "Acerca de",
|
||||
"about-mealie": "Acerca de Mealie",
|
||||
"api-docs": "Documentación API",
|
||||
"api-port": "Puerto API",
|
||||
"api-docs": "Documentación de API",
|
||||
"api-port": "Puerto de API",
|
||||
"application-mode": "Modo de Aplicación",
|
||||
"database-type": "Tipo de base de datos",
|
||||
"database-url": "URL de base de datos",
|
||||
|
@ -472,6 +472,7 @@
|
|||
"comment": "Comentario",
|
||||
"comments": "Comentarios",
|
||||
"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",
|
||||
"description": "Descripción",
|
||||
"disable-amount": "Desactivar cantidades de ingredientes",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Lo hice",
|
||||
"how-did-it-turn-out": "¿Cómo resultó 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.",
|
||||
"message-key": "Clave de mensaje",
|
||||
"parse": "Analizar",
|
||||
|
@ -599,10 +604,10 @@
|
|||
"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.",
|
||||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -658,13 +663,15 @@
|
|||
"no-unit": "Sin unidad",
|
||||
"missing-unit": "Crear unidad faltante: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Restablecer contador de porciones",
|
||||
"not-linked-ingredients": "Ingredientes adicionales",
|
||||
"upload-another-image": "Upload another image",
|
||||
"upload-images": "Upload images",
|
||||
"upload-more-images": "Upload more images"
|
||||
"upload-another-image": "Subir otra imagen",
|
||||
"upload-images": "Subir imágenes",
|
||||
"upload-more-images": "Subir más imágenes"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Buscador de recetas",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Kommentaar",
|
||||
"comments": "Kommentaarid",
|
||||
"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",
|
||||
"description": "Kirjeldus",
|
||||
"disable-amount": "Deaktiveeri koostisosade kogused",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Olen seda valmistanud",
|
||||
"how-did-it-turn-out": "Kuidas tuli see välja?",
|
||||
"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.",
|
||||
"message-key": "Sõnumi võti",
|
||||
"parse": "Analüüsi",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "Ilma ühikuta",
|
||||
"missing-unit": "Loo puuduv ühik: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Lähtesta portsionite arv",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Kommentti",
|
||||
"comments": "Kommentit",
|
||||
"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",
|
||||
"description": "Kuvaus",
|
||||
"disable-amount": "Poista ainesosien määrät käytöstä",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Tein tämän",
|
||||
"how-did-it-turn-out": "Miten se onnistui?",
|
||||
"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.",
|
||||
"message-key": "Viestiavain",
|
||||
"parse": "Jäsennä",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "Ei yksikköä",
|
||||
"missing-unit": "Luo puuttuva yksikkö: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Palauta Annoksien Määrä",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Commentaire",
|
||||
"comments": "Commentaires",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"disable-amount": "Désactiver les quantités des ingrédients",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Je l’ai cuisiné",
|
||||
"how-did-it-turn-out": "C’était bon ?",
|
||||
"user-made-this": "{user} l’a cuisiné",
|
||||
"added-to-timeline": "Ajouté à l’historique",
|
||||
"failed-to-add-to-timeline": "Ajout dans l’historique en échec",
|
||||
"failed-to-update-recipe": "Impossible de mettre à jour la recette",
|
||||
"added-to-timeline-but-failed-to-add-image": "Ajouté à l’historique, mais impossible d’ajouter l’image",
|
||||
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de l’API 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 à l’appareil souhaité.",
|
||||
"message-key": "Clé de message",
|
||||
"parse": "Analyser",
|
||||
|
@ -599,10 +604,10 @@
|
|||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible, et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Create from Images",
|
||||
"create-from-images": "Créer à partir d’images",
|
||||
"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 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",
|
||||
"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.",
|
||||
|
@ -658,13 +663,15 @@
|
|||
"no-unit": "Pas d'unité",
|
||||
"missing-unit": "Créer une unité manquante : {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Réinitialiser le nombre de portions",
|
||||
"not-linked-ingredients": "Ingrédients supplémentaires",
|
||||
"upload-another-image": "Upload another image",
|
||||
"upload-images": "Upload images",
|
||||
"upload-more-images": "Upload more images"
|
||||
"upload-another-image": "Télécharger une autre image",
|
||||
"upload-images": "Télécharger des images",
|
||||
"upload-more-images": "Télécharger d'autres images"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Recherche de recette",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Commentaire",
|
||||
"comments": "Commentaires",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"disable-amount": "Désactiver les quantités d'ingrédients",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Je l’ai cuisiné",
|
||||
"how-did-it-turn-out": "C’était bon ?",
|
||||
"user-made-this": "{user} l’a cuisiné",
|
||||
"added-to-timeline": "Ajouté à l’historique",
|
||||
"failed-to-add-to-timeline": "Ajout dans l’historique en échec",
|
||||
"failed-to-update-recipe": "Impossible de mettre à jour la recette",
|
||||
"added-to-timeline-but-failed-to-add-image": "Ajouté à l’historique, mais impossible d’ajouter l’image",
|
||||
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de l’API 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 à l’appareil souhaité.",
|
||||
"message-key": "Clé de message",
|
||||
"parse": "Analyser",
|
||||
|
@ -599,10 +604,10 @@
|
|||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléversant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Create from Images",
|
||||
"create-from-images": "Créer à partir d’images",
|
||||
"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-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",
|
||||
"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.",
|
||||
|
@ -658,13 +663,15 @@
|
|||
"no-unit": "Pas d'unité",
|
||||
"missing-unit": "Créer une unité manquante : {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Réinitialiser le nombre de portions",
|
||||
"not-linked-ingredients": "Ingrédients supplémentaires",
|
||||
"upload-another-image": "Upload another image",
|
||||
"upload-images": "Upload images",
|
||||
"upload-more-images": "Upload more images"
|
||||
"upload-another-image": "Télécharger une autre image",
|
||||
"upload-images": "Télécharger des images",
|
||||
"upload-more-images": "Télécharger d'autres images"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Recherche de recette",
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"log-lines": "Lignes de log",
|
||||
"not-demo": "Non",
|
||||
"portfolio": "Portfolio",
|
||||
"production": "Réalisation",
|
||||
"production": "Production",
|
||||
"support": "Soutenir",
|
||||
"version": "Version",
|
||||
"unknown-version": "inconnu",
|
||||
|
@ -111,7 +111,7 @@
|
|||
"friday": "Vendredi",
|
||||
"general": "Général",
|
||||
"get": "Envoyer",
|
||||
"home": "Page d’accueil",
|
||||
"home": "Accueil",
|
||||
"image": "Image",
|
||||
"image-upload-failed": "Le téléchargement de l’image a échoué",
|
||||
"import": "Importer",
|
||||
|
@ -472,6 +472,7 @@
|
|||
"comment": "Commentaire",
|
||||
"comments": "Commentaires",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"disable-amount": "Désactiver les quantités des ingrédients",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Je l’ai cuisiné",
|
||||
"how-did-it-turn-out": "C’était bon ?",
|
||||
"user-made-this": "{user} l’a cuisiné",
|
||||
"added-to-timeline": "Ajouté à l’historique",
|
||||
"failed-to-add-to-timeline": "Ajout dans l’historique en échec",
|
||||
"failed-to-update-recipe": "Impossible de mettre à jour la recette",
|
||||
"added-to-timeline-but-failed-to-add-image": "Ajouté à l’historique, mais impossible d’ajouter l’image",
|
||||
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de l’API 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 à l’appareil souhaité.",
|
||||
"message-key": "Clé de message",
|
||||
"parse": "Analyser",
|
||||
|
@ -599,10 +604,10 @@
|
|||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible, et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Create from Images",
|
||||
"create-from-images": "Créer à partir d’images",
|
||||
"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 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",
|
||||
"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.",
|
||||
|
@ -658,13 +663,15 @@
|
|||
"no-unit": "Pas d'unité",
|
||||
"missing-unit": "Créer une unité manquante : {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Réinitialiser le nombre de portions",
|
||||
"not-linked-ingredients": "Ingrédients supplémentaires",
|
||||
"upload-another-image": "Upload another image",
|
||||
"upload-images": "Upload images",
|
||||
"upload-more-images": "Upload more images"
|
||||
"upload-another-image": "Télécharger une autre image",
|
||||
"upload-images": "Télécharger des images",
|
||||
"upload-more-images": "Télécharger d'autres images"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Recherche de recette",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Comentario",
|
||||
"comments": "Comentarios",
|
||||
"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",
|
||||
"description": "Descrición",
|
||||
"disable-amount": "Desactivar as Cantidades de Ingredientes",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Eu fixen isto",
|
||||
"how-did-it-turn-out": "Que tal ficou?",
|
||||
"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.",
|
||||
"message-key": "Chave de Mensaxen",
|
||||
"parse": "Interpretar",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "Sen unidades",
|
||||
"missing-unit": "Crear a unidade que falta: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reiniciar Contador de Porcións",
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"category-filter": "סינון קטגוריות",
|
||||
"category-update-failed": "עדכון קטגוריה נכשל",
|
||||
"category-updated": "קטגוריה עודכנה",
|
||||
"uncategorized-count": "{count} לא קיבלו קטגוריה",
|
||||
"uncategorized-count": "{count} ללא קטגוריה",
|
||||
"create-a-category": "יצירת קטגוריה",
|
||||
"category-name": "שם קטגוריה",
|
||||
"category": "קטגוריה"
|
||||
|
@ -472,6 +472,7 @@
|
|||
"comment": "הערה",
|
||||
"comments": "הערות",
|
||||
"delete-confirmation": "למחוק את המתכון הזה?",
|
||||
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||
"delete-recipe": "מחיקת מתכון",
|
||||
"description": "תיאור",
|
||||
"disable-amount": "ביטול כמויות מרכיבים",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "הכנתי את זה",
|
||||
"how-did-it-turn-out": "איך יצא?",
|
||||
"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 על מנת לקרוא אותם בתוכנת צד שלישית. תוכלו להשתמש בצמדים האלה כדי לספק מידע, לדוגמא להפעיל אוטומציות או הודעות מותאמות אישית למכשירים מסויימים.",
|
||||
"message-key": "מפתח הודעה",
|
||||
"parse": "ניתוח",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "ללא יחידות",
|
||||
"missing-unit": "יצירת יחידה חסרה: {unit}",
|
||||
"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": "אין אוכל"
|
||||
},
|
||||
"reset-servings-count": "איפוס מספר המנות",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Komentar",
|
||||
"comments": "Komentari",
|
||||
"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",
|
||||
"description": "Opis",
|
||||
"disable-amount": "Onemogući količine sastojaka",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Napravio/la sam ovo",
|
||||
"how-did-it-turn-out": "Kako je ispalo?",
|
||||
"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.",
|
||||
"message-key": "Ključ poruke",
|
||||
"parse": "Razluči (parsiraj)",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "No unit",
|
||||
"missing-unit": "Create missing unit: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reset Servings Count",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Megjegyzés",
|
||||
"comments": "Megjegyzések",
|
||||
"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",
|
||||
"description": "Leírás",
|
||||
"disable-amount": "Hozzávalók mennyiségének letiltása",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "Elkészítettem ezt",
|
||||
"how-did-it-turn-out": "Hogyan sikerült?",
|
||||
"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.",
|
||||
"message-key": "Üzenetkulcs",
|
||||
"parse": "Előkészítés",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "Mértékegység nélkül",
|
||||
"missing-unit": "Hiányzó mértékegység létrehozása: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Adagok számának visszaállítása",
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
"comment": "Comment",
|
||||
"comments": "Comments",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"disable-amount": "Disable Ingredient Amounts",
|
||||
|
@ -579,6 +580,10 @@
|
|||
"made-this": "I Made This",
|
||||
"how-did-it-turn-out": "How did it turn out?",
|
||||
"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.",
|
||||
"message-key": "Message Key",
|
||||
"parse": "Parse",
|
||||
|
@ -658,6 +663,8 @@
|
|||
"no-unit": "No unit",
|
||||
"missing-unit": "Create missing unit: {unit}",
|
||||
"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"
|
||||
},
|
||||
"reset-servings-count": "Reset Servings Count",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue