mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
Merge branch 'mealie-recipes:mealie-next' into mealie-next
This commit is contained in:
commit
e9f59fa4be
116 changed files with 2355 additions and 1642 deletions
|
@ -71,6 +71,7 @@ tasks:
|
||||||
desc: run code generators
|
desc: run code generators
|
||||||
cmds:
|
cmds:
|
||||||
- poetry run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
- poetry run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
||||||
|
- task: docs:gen
|
||||||
- task: py:format
|
- task: py:format
|
||||||
|
|
||||||
dev:services:
|
dev:services:
|
||||||
|
|
|
@ -8,8 +8,8 @@ from utils import log
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
template = """// This Code is auto generated by gen_ts_types.py
|
template = """// This Code is auto generated by gen_ts_types.py
|
||||||
{% for name in global %}import {{ name }} from "@/components/global/{{ name }}.vue";
|
{% for name in global %}import type {{ name }} from "@/components/global/{{ name }}.vue";
|
||||||
{% endfor %}{% for name in layout %}import {{ name }} from "@/components/layout/{{ name }}.vue";
|
{% endfor %}{% for name in layout %}import type {{ name }} from "@/components/layout/{{ name }}.vue";
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
declare module "vue" {
|
declare module "vue" {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
###############################################
|
###############################################
|
||||||
# Frontend Build
|
# Frontend Build
|
||||||
###############################################
|
###############################################
|
||||||
FROM node:20 AS frontend-builder
|
FROM node:20@sha256:572a90df10a58ebb7d3f223d661d964a6c2383a9c2b5763162b4f631c53dc56a \
|
||||||
|
AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
|
@ -20,7 +21,8 @@ RUN yarn generate
|
||||||
###############################################
|
###############################################
|
||||||
# Base Image - Python
|
# Base Image - Python
|
||||||
###############################################
|
###############################################
|
||||||
FROM python:3.12-slim AS python-base
|
FROM python:3.12-slim@sha256:2267adc248a477c1f1a852a07a5a224d42abe54c28aafa572efa157dfb001bba \
|
||||||
|
AS python-base
|
||||||
|
|
||||||
ENV MEALIE_HOME="/app"
|
ENV MEALIE_HOME="/app"
|
||||||
|
|
||||||
|
@ -132,7 +134,7 @@ RUN apt-get update \
|
||||||
gosu \
|
gosu \
|
||||||
iproute2 \
|
iproute2 \
|
||||||
libldap-common \
|
libldap-common \
|
||||||
libldap-2.5 \
|
libldap2 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# create directory used for Docker Secrets
|
# create directory used for Docker Secrets
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -70,7 +70,7 @@ import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue"
|
||||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
|
@ -100,7 +100,7 @@ const dialogStates = reactive({
|
||||||
edit: false,
|
edit: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const editTarget = ref<RecipeCookBook | null>(null);
|
const editTarget = ref<ReadCookBook | null>(null);
|
||||||
function handleEditCookbook() {
|
function handleEditCookbook() {
|
||||||
dialogStates.edit = true;
|
dialogStates.edit = true;
|
||||||
editTarget.value = book.value;
|
editTarget.value = book.value;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<v-list :class="tile ? 'd-flex flex-wrap background' : 'background'">
|
<v-list :class="tile ? 'd-flex flex-wrap background' : 'background'" style="background-color: transparent;">
|
||||||
<v-sheet
|
<v-sheet
|
||||||
v-for="recipe, index in recipes"
|
v-for="recipe, index in recipes"
|
||||||
:key="recipe.id"
|
:key="recipe.id"
|
||||||
|
|
|
@ -390,8 +390,6 @@ const props = defineProps({
|
||||||
|
|
||||||
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
||||||
|
|
||||||
const BASE_URL = useRequestURL().origin;
|
|
||||||
|
|
||||||
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
||||||
|
|
||||||
const dialog = ref(false);
|
const dialog = ref(false);
|
||||||
|
@ -695,7 +693,7 @@ async function handleImageDrop(index: number, files: File[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("update:assets", [...assets.value, data]);
|
emit("update:assets", [...assets.value, data]);
|
||||||
const assetUrl = BASE_URL + recipeAssetPath(props.recipe.id, data.fileName as string);
|
const assetUrl = recipeAssetPath(props.recipe.id, data.fileName as string);
|
||||||
const text = `<img src="${assetUrl}" height="100%" width="100%"/>`;
|
const text = `<img src="${assetUrl}" height="100%" width="100%"/>`;
|
||||||
instructionList.value[index].text += text;
|
instructionList.value[index].text += text;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-btn
|
<v-btn
|
||||||
size="small"
|
size="small"
|
||||||
|
variant="text"
|
||||||
class="ml-2 handle"
|
class="ml-2 handle"
|
||||||
icon
|
icon
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
v-model="listItem.checked"
|
v-model="listItem.checked"
|
||||||
hide-details
|
hide-details
|
||||||
density="compact"
|
density="compact"
|
||||||
class="mt-0"
|
class="mt-0 flex-shrink-0"
|
||||||
color="null"
|
color="null"
|
||||||
@change="$emit('checked', listItem)"
|
@change="$emit('checked', listItem)"
|
||||||
/>
|
/>
|
||||||
|
@ -27,16 +27,6 @@
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-col
|
|
||||||
v-if="label && showLabel"
|
|
||||||
cols="3"
|
|
||||||
class="text-right"
|
|
||||||
>
|
|
||||||
<MultiPurposeLabel
|
|
||||||
:label="label"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
<v-col
|
||||||
cols="auto"
|
cols="auto"
|
||||||
class="text-right"
|
class="text-right"
|
||||||
|
@ -75,27 +65,6 @@
|
||||||
</template>
|
</template>
|
||||||
<span>Toggle Recipes</span>
|
<span>Toggle Recipes</span>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<!-- Dummy button so the spacing is consistent when labels are enabled -->
|
|
||||||
<v-btn
|
|
||||||
v-else
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
class="ml-2"
|
|
||||||
icon
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
class="ml-2 handle"
|
|
||||||
icon
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<v-icon>
|
|
||||||
{{ $globals.icons.arrowUpDown }}
|
|
||||||
</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
|
@ -107,6 +76,17 @@
|
||||||
{{ $globals.icons.edit }}
|
{{ $globals.icons.edit }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
class="handle"
|
||||||
|
icon
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<v-icon>
|
||||||
|
{{ $globals.icons.arrowUpDown }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
|
@ -177,7 +157,6 @@
|
||||||
import { useOnline } from "@vueuse/core";
|
import { useOnline } from "@vueuse/core";
|
||||||
import RecipeIngredientListItem from "../Recipe/RecipeIngredientListItem.vue";
|
import RecipeIngredientListItem from "../Recipe/RecipeIngredientListItem.vue";
|
||||||
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
|
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
|
||||||
import MultiPurposeLabel from "./MultiPurposeLabel.vue";
|
|
||||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||||
import type { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
import type { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||||
import type { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
|
import type { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
|
||||||
|
@ -189,16 +168,12 @@ interface actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: { ShoppingListItemEditor, MultiPurposeLabel, RecipeList, RecipeIngredientListItem },
|
components: { ShoppingListItemEditor, RecipeList, RecipeIngredientListItem },
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object as () => ShoppingListItemOut,
|
type: Object as () => ShoppingListItemOut,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
showLabel: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
labels: {
|
labels: {
|
||||||
type: Array as () => MultiPurposeLabelOut[],
|
type: Array as () => MultiPurposeLabelOut[],
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -220,7 +195,7 @@ export default defineNuxtComponent({
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const displayRecipeRefs = ref(false);
|
const displayRecipeRefs = ref(false);
|
||||||
const itemLabelCols = ref<string>(props.modelValue.checked ? "auto" : props.showLabel ? "4" : "6");
|
const itemLabelCols = ref<string>(props.modelValue.checked ? "auto" : "6");
|
||||||
const isOffline = computed(() => useOnline().value === false);
|
const isOffline = computed(() => useOnline().value === false);
|
||||||
|
|
||||||
const contextMenu: actions[] = [
|
const contextMenu: actions[] = [
|
||||||
|
@ -305,7 +280,7 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
listItem.value.recipeReferences.forEach((ref) => {
|
listItem.value.recipeReferences.forEach((ref) => {
|
||||||
const recipe = props.recipes.get(ref.recipeId);
|
const recipe = props.recipes?.get(ref.recipeId);
|
||||||
if (recipe) {
|
if (recipe) {
|
||||||
recipeList.push(recipe);
|
recipeList.push(recipe);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,8 @@
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||||
:hint="inputField.hint"
|
:hint="inputField.hint"
|
||||||
hide-details="auto"
|
:hide-details="!inputField.hint"
|
||||||
|
:persistent-hint="!!inputField.hint"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
@change="emitBlur">
|
@change="emitBlur">
|
||||||
<template #label>
|
<template #label>
|
||||||
|
@ -97,8 +98,8 @@
|
||||||
:label="inputField.label"
|
:label="inputField.label"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:items="inputField.options"
|
:items="inputField.options"
|
||||||
:item-title="inputField.itemText"
|
item-title="text"
|
||||||
:item-value="inputField.itemValue"
|
item-value="text"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
:hint="inputField.hint"
|
:hint="inputField.hint"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
|
@ -107,10 +108,11 @@
|
||||||
@blur="emitBlur"
|
@blur="emitBlur"
|
||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div>
|
<v-list-item
|
||||||
<v-list-item-title>{{ item.raw.text }}</v-list-item-title>
|
v-bind="props"
|
||||||
<v-list-item-subtitle>{{ item.raw.description }}</v-list-item-subtitle>
|
:title="item.raw.text"
|
||||||
</div>
|
:subtitle="item.raw.description"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-select>
|
</v-select>
|
||||||
|
|
||||||
|
|
|
@ -4,43 +4,39 @@ function UnknownToString(ukn: string | unknown) {
|
||||||
|
|
||||||
export const useStaticRoutes = () => {
|
export const useStaticRoutes = () => {
|
||||||
const { $config } = useNuxtApp();
|
const { $config } = useNuxtApp();
|
||||||
const serverBase = useRequestURL().origin;
|
|
||||||
|
|
||||||
const prefix = `${$config.public.SUB_PATH}/api`.replace("//", "/");
|
const prefix = `${$config.public.SUB_PATH}/api`.replace("//", "/");
|
||||||
|
|
||||||
const fullBase = serverBase + prefix;
|
|
||||||
|
|
||||||
// Methods to Generate reference urls for assets/images *
|
// Methods to Generate reference urls for assets/images *
|
||||||
function recipeImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
function recipeImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${UnknownToString(version)}`;
|
return `${prefix}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${UnknownToString(version)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeSmallImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
function recipeSmallImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${UnknownToString(
|
return `${prefix}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${UnknownToString(
|
||||||
version,
|
version,
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeTinyImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
function recipeTinyImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${UnknownToString(
|
return `${prefix}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${UnknownToString(
|
||||||
version,
|
version,
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeTimelineEventImage(recipeId: string, timelineEventId: string) {
|
function recipeTimelineEventImage(recipeId: string, timelineEventId: string) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/original.webp`;
|
return `${prefix}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/original.webp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeTimelineEventSmallImage(recipeId: string, timelineEventId: string) {
|
function recipeTimelineEventSmallImage(recipeId: string, timelineEventId: string) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/min-original.webp`;
|
return `${prefix}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/min-original.webp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeTimelineEventTinyImage(recipeId: string, timelineEventId: string) {
|
function recipeTimelineEventTinyImage(recipeId: string, timelineEventId: string) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/tiny-original.webp`;
|
return `${prefix}/media/recipes/${recipeId}/images/timeline/${timelineEventId}/tiny-original.webp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recipeAssetPath(recipeId: string, assetName: string) {
|
function recipeAssetPath(recipeId: string, assetName: string) {
|
||||||
return `${fullBase}/media/recipes/${recipeId}/assets/${assetName}`;
|
return `${prefix}/media/recipes/${recipeId}/assets/${assetName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||||
|
import { useCopyList } from "~/composables/use-copy";
|
||||||
|
|
||||||
|
type CopyTypes = "plain" | "markdown";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing shopping list copy functionality
|
||||||
|
*/
|
||||||
|
export function useShoppingListCopy() {
|
||||||
|
const copy = useCopyList();
|
||||||
|
|
||||||
|
function copyListItems(itemsByLabel: { [key: string]: ShoppingListItemOut[] }, copyType: CopyTypes) {
|
||||||
|
const text: string[] = [];
|
||||||
|
Object.entries(itemsByLabel).forEach(([label, items], idx) => {
|
||||||
|
if (idx) {
|
||||||
|
text.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
text.push(formatCopiedLabelHeading(copyType, label));
|
||||||
|
items.forEach(item => text.push(formatCopiedListItem(copyType, item)));
|
||||||
|
});
|
||||||
|
|
||||||
|
copy.copyPlain(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCopiedListItem(copyType: CopyTypes, item: ShoppingListItemOut): string {
|
||||||
|
const display = item.display || "";
|
||||||
|
switch (copyType) {
|
||||||
|
case "markdown":
|
||||||
|
return `- [ ] ${display}`;
|
||||||
|
default:
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCopiedLabelHeading(copyType: CopyTypes, label: string): string {
|
||||||
|
switch (copyType) {
|
||||||
|
case "markdown":
|
||||||
|
return `# ${label}`;
|
||||||
|
default:
|
||||||
|
return `[${label}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
copyListItems,
|
||||||
|
formatCopiedListItem,
|
||||||
|
formatCopiedLabelHeading,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
import type { ShoppingListOut, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/household";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { uuid4 } from "~/composables/use-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing shopping list item CRUD operations
|
||||||
|
*/
|
||||||
|
export function useShoppingListCrud(
|
||||||
|
shoppingList: Ref<ShoppingListOut | null>,
|
||||||
|
loadingCounter: Ref<number>,
|
||||||
|
listItems: { unchecked: ShoppingListItemOut[]; checked: ShoppingListItemOut[] },
|
||||||
|
shoppingListItemActions: any,
|
||||||
|
refresh: () => void,
|
||||||
|
sortCheckedItems: (a: ShoppingListItemOut, b: ShoppingListItemOut) => number,
|
||||||
|
updateListItemOrder: () => void,
|
||||||
|
) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const userApi = useUserApi();
|
||||||
|
|
||||||
|
const createListItemData = ref<ShoppingListItemOut>(listItemFactory());
|
||||||
|
const localLabels = ref<ShoppingListMultiPurposeLabelOut[]>();
|
||||||
|
|
||||||
|
function listItemFactory(): ShoppingListItemOut {
|
||||||
|
return {
|
||||||
|
id: uuid4(),
|
||||||
|
shoppingListId: shoppingList.value?.id || "",
|
||||||
|
checked: false,
|
||||||
|
position: shoppingList.value?.listItems?.length || 1,
|
||||||
|
quantity: 0,
|
||||||
|
note: "",
|
||||||
|
labelId: undefined,
|
||||||
|
unitId: undefined,
|
||||||
|
foodId: undefined,
|
||||||
|
} as ShoppingListItemOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check/Uncheck All operations
|
||||||
|
function checkAllItems() {
|
||||||
|
let hasChanged = false;
|
||||||
|
shoppingList.value?.listItems?.forEach((item) => {
|
||||||
|
if (!item.checked) {
|
||||||
|
hasChanged = true;
|
||||||
|
item.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasChanged) {
|
||||||
|
updateUncheckedListItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uncheckAllItems() {
|
||||||
|
let hasChanged = false;
|
||||||
|
shoppingList.value?.listItems?.forEach((item) => {
|
||||||
|
if (item.checked) {
|
||||||
|
hasChanged = true;
|
||||||
|
item.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasChanged) {
|
||||||
|
listItems.unchecked = [...listItems.unchecked, ...listItems.checked];
|
||||||
|
listItems.checked = [];
|
||||||
|
updateUncheckedListItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCheckedItems() {
|
||||||
|
const checked = shoppingList.value?.listItems?.filter(item => item.checked);
|
||||||
|
|
||||||
|
if (!checked || checked?.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingCounter.value += 1;
|
||||||
|
deleteListItems(checked);
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveListItem(item: ShoppingListItemOut) {
|
||||||
|
if (!shoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set a temporary updatedAt timestamp prior to refresh so it appears at the top of the checked items
|
||||||
|
item.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
// make updates reflect immediately
|
||||||
|
if (shoppingList.value.listItems) {
|
||||||
|
shoppingList.value.listItems.forEach((oldListItem: ShoppingListItemOut, idx: number) => {
|
||||||
|
if (oldListItem.id === item.id && shoppingList.value?.listItems) {
|
||||||
|
shoppingList.value.listItems[idx] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Immediately update checked/unchecked arrays for UI
|
||||||
|
listItems.unchecked = shoppingList.value.listItems.filter(i => !i.checked);
|
||||||
|
listItems.checked = shoppingList.value.listItems.filter(i => i.checked)
|
||||||
|
.sort(sortCheckedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the item if it's checked, otherwise updateUncheckedListItems will handle it
|
||||||
|
if (item.checked) {
|
||||||
|
shoppingListItemActions.updateItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateListItemOrder();
|
||||||
|
updateUncheckedListItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteListItem(item: ShoppingListItemOut) {
|
||||||
|
if (!shoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shoppingListItemActions.deleteItem(item);
|
||||||
|
|
||||||
|
// remove the item from the list immediately so the user sees the change
|
||||||
|
if (shoppingList.value.listItems) {
|
||||||
|
shoppingList.value.listItems = shoppingList.value.listItems.filter(itm => itm.id !== item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteListItems(items: ShoppingListItemOut[]) {
|
||||||
|
if (!shoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
shoppingListItemActions.deleteItem(item);
|
||||||
|
});
|
||||||
|
// remove the items from the list immediately so the user sees the change
|
||||||
|
if (shoppingList.value?.listItems) {
|
||||||
|
const deletedItems = new Set(items.map(item => item.id));
|
||||||
|
shoppingList.value.listItems = shoppingList.value.listItems.filter(itm => !deletedItems.has(itm.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createListItem() {
|
||||||
|
if (!shoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createListItemData.value.foodId && !createListItemData.value.note) {
|
||||||
|
// don't create an empty item
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingCounter.value += 1;
|
||||||
|
|
||||||
|
// make sure it's inserted into the end of the list, which may have been updated
|
||||||
|
createListItemData.value.position = shoppingList.value?.listItems?.length
|
||||||
|
? (shoppingList.value.listItems.reduce((a, b) => (a.position || 0) > (b.position || 0) ? a : b).position || 0) + 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
createListItemData.value.createdAt = new Date().toISOString();
|
||||||
|
createListItemData.value.updatedAt = createListItemData.value.createdAt;
|
||||||
|
|
||||||
|
updateListItemOrder();
|
||||||
|
|
||||||
|
shoppingListItemActions.createItem(createListItemData.value);
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
|
||||||
|
if (shoppingList.value.listItems) {
|
||||||
|
// add the item to the list immediately so the user sees the change
|
||||||
|
shoppingList.value.listItems.push(createListItemData.value);
|
||||||
|
updateListItemOrder();
|
||||||
|
}
|
||||||
|
createListItemData.value = listItemFactory();
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUncheckedListItems() {
|
||||||
|
if (!shoppingList.value?.listItems) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set position for unchecked items
|
||||||
|
listItems.unchecked.forEach((item: ShoppingListItemOut, idx: number) => {
|
||||||
|
item.position = idx;
|
||||||
|
shoppingListItemActions.updateItem(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label management
|
||||||
|
function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) {
|
||||||
|
if (!shoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
labelSettings.forEach((labelSetting, index) => {
|
||||||
|
labelSetting.position = index;
|
||||||
|
return labelSetting;
|
||||||
|
});
|
||||||
|
|
||||||
|
localLabels.value = labelSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelLabelOrder() {
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
if (!shoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// restore original state
|
||||||
|
localLabels.value = shoppingList.value.labelSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveLabelOrder(updateItemsByLabel: () => void) {
|
||||||
|
if (!shoppingList.value || !localLabels.value || (localLabels.value === shoppingList.value.labelSettings)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingCounter.value += 1;
|
||||||
|
const { data } = await userApi.shopping.lists.updateLabelSettings(shoppingList.value.id, localLabels.value);
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
// update shoppingList labels using the API response
|
||||||
|
shoppingList.value.labelSettings = (data as ShoppingListOut).labelSettings;
|
||||||
|
updateItemsByLabel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleReorderLabelsDialog(reorderLabelsDialog: Ref<boolean>) {
|
||||||
|
// stop polling and populate localLabels
|
||||||
|
loadingCounter.value += 1;
|
||||||
|
reorderLabelsDialog.value = !reorderLabelsDialog.value;
|
||||||
|
localLabels.value = shoppingList.value?.labelSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu actions
|
||||||
|
const contextActions = {
|
||||||
|
delete: "delete",
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextMenu = [
|
||||||
|
{ title: t("general.delete"), action: contextActions.delete },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
createListItemData,
|
||||||
|
localLabels,
|
||||||
|
listItemFactory,
|
||||||
|
checkAllItems,
|
||||||
|
uncheckAllItems,
|
||||||
|
deleteCheckedItems,
|
||||||
|
saveListItem,
|
||||||
|
deleteListItem,
|
||||||
|
deleteListItems,
|
||||||
|
createListItem,
|
||||||
|
updateUncheckedListItems,
|
||||||
|
updateLabelOrder,
|
||||||
|
cancelLabelOrder,
|
||||||
|
saveLabelOrder,
|
||||||
|
toggleReorderLabelsDialog,
|
||||||
|
contextActions,
|
||||||
|
contextMenu,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { useOnline, useIdle } from "@vueuse/core";
|
||||||
|
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||||
|
import { useShoppingListItemActions } from "~/composables/use-shopping-list-item-actions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing shopping list data fetching and polling
|
||||||
|
*/
|
||||||
|
export function useShoppingListData(listId: string, shoppingList: Ref<ShoppingListOut | null>, loadingCounter: Ref<number>) {
|
||||||
|
const isOffline = computed(() => useOnline().value === false);
|
||||||
|
const { idle } = useIdle(5 * 60 * 1000); // 5 minutes
|
||||||
|
const shoppingListItemActions = useShoppingListItemActions(listId);
|
||||||
|
|
||||||
|
async function fetchShoppingList() {
|
||||||
|
const data = await shoppingListItemActions.getList();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(updateListItemOrder: () => void) {
|
||||||
|
loadingCounter.value += 1;
|
||||||
|
try {
|
||||||
|
await shoppingListItemActions.process();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newListValue: typeof shoppingList.value = null;
|
||||||
|
try {
|
||||||
|
newListValue = await fetchShoppingList();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
|
||||||
|
// only update the list with the new value if we're not loading, to prevent UI jitter
|
||||||
|
if (loadingCounter.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent overwriting local changes with stale backend data when offline
|
||||||
|
if (isOffline.value) {
|
||||||
|
// Do not update shoppingList.value from backend when offline
|
||||||
|
updateListItemOrder();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're not connected to the network, this will be null, so we don't want to clear the list
|
||||||
|
if (newListValue) {
|
||||||
|
shoppingList.value = newListValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateListItemOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// constantly polls for changes
|
||||||
|
async function pollForChanges(updateListItemOrder: () => void) {
|
||||||
|
// pause polling if the user isn't active or we're busy
|
||||||
|
if (idle.value || loadingCounter.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refresh(updateListItemOrder);
|
||||||
|
|
||||||
|
if (shoppingList.value) {
|
||||||
|
attempts = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the refresh was unsuccessful, the shopping list will be null, so we increment the attempt counter
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we hit too many errors, stop polling
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start polling
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
|
||||||
|
// max poll time = pollFrequency * maxAttempts = 24 hours
|
||||||
|
// we use a long max poll time since polling stops when the user is idle anyway
|
||||||
|
const pollFrequency = 5000;
|
||||||
|
const maxAttempts = 17280;
|
||||||
|
let attempts = 0;
|
||||||
|
let pollTimer: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
function startPolling(updateListItemOrder: () => void) {
|
||||||
|
pollForChanges(updateListItemOrder); // populate initial list
|
||||||
|
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
pollForChanges(updateListItemOrder);
|
||||||
|
}, pollFrequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOffline,
|
||||||
|
fetchShoppingList,
|
||||||
|
refresh,
|
||||||
|
startPolling,
|
||||||
|
stopPolling,
|
||||||
|
shoppingListItemActions,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { useToggle } from "@vueuse/core";
|
||||||
|
import type { ShoppingListOut, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing shopping list label state and operations
|
||||||
|
*/
|
||||||
|
export function useShoppingListLabels(shoppingList: Ref<ShoppingListOut | null>) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const labelOpenState = ref<{ [key: string]: boolean }>({});
|
||||||
|
const [showChecked, toggleShowChecked] = useToggle(false);
|
||||||
|
|
||||||
|
const initializeLabelOpenStates = () => {
|
||||||
|
if (!shoppingList.value?.listItems) return;
|
||||||
|
|
||||||
|
const existingLabels = new Set(Object.keys(labelOpenState.value));
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
for (const item of shoppingList.value.listItems) {
|
||||||
|
const labelName = item.label?.name || t("shopping-list.no-label");
|
||||||
|
if (!existingLabels.has(labelName) && !(labelName in labelOpenState.value)) {
|
||||||
|
labelOpenState.value[labelName] = true;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
labelOpenState.value = { ...labelOpenState.value };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelNames = computed(() => {
|
||||||
|
return new Set(
|
||||||
|
shoppingList.value?.listItems
|
||||||
|
?.map(item => item.label?.name || t("shopping-list.no-label"))
|
||||||
|
.filter(Boolean) ?? [],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(labelNames, initializeLabelOpenStates, { immediate: true });
|
||||||
|
|
||||||
|
function toggleShowLabel(key: string) {
|
||||||
|
labelOpenState.value[key] = !labelOpenState.value[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelColor(item: ShoppingListItemOut | null) {
|
||||||
|
return item?.label?.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentLabels = computed(() => {
|
||||||
|
const labels: Array<{ id: string; name: string }> = [];
|
||||||
|
|
||||||
|
shoppingList.value?.listItems?.forEach((item) => {
|
||||||
|
if (item.labelId && item.label) {
|
||||||
|
labels.push({
|
||||||
|
name: item.label.name,
|
||||||
|
id: item.labelId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
labelOpenState,
|
||||||
|
showChecked,
|
||||||
|
toggleShowChecked,
|
||||||
|
toggleShowLabel,
|
||||||
|
getLabelColor,
|
||||||
|
presentLabels,
|
||||||
|
initializeLabelOpenStates,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing shopping list recipe references
|
||||||
|
*/
|
||||||
|
export function useShoppingListRecipes(
|
||||||
|
shoppingList: Ref<ShoppingListOut | null>,
|
||||||
|
loadingCounter: Ref<number>,
|
||||||
|
recipeReferenceLoading: Ref<boolean>,
|
||||||
|
refresh: () => void,
|
||||||
|
) {
|
||||||
|
const userApi = useUserApi();
|
||||||
|
|
||||||
|
async function addRecipeReferenceToList(recipeId: string) {
|
||||||
|
if (!shoppingList.value || recipeReferenceLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingCounter.value += 1;
|
||||||
|
recipeReferenceLoading.value = true;
|
||||||
|
const { data } = await userApi.shopping.lists.addRecipes(shoppingList.value.id, [{ recipeId }]);
|
||||||
|
recipeReferenceLoading.value = false;
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeRecipeReferenceToList(recipeId: string) {
|
||||||
|
if (!shoppingList.value || recipeReferenceLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingCounter.value += 1;
|
||||||
|
recipeReferenceLoading.value = true;
|
||||||
|
const { data } = await userApi.shopping.lists.removeRecipe(shoppingList.value.id, recipeId);
|
||||||
|
recipeReferenceLoading.value = false;
|
||||||
|
loadingCounter.value -= 1;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addRecipeReferenceToList,
|
||||||
|
removeRecipeReferenceToList,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
import type { ShoppingListOut, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||||
|
|
||||||
|
interface ListItemGroup {
|
||||||
|
position: number;
|
||||||
|
createdAt: string;
|
||||||
|
items: ShoppingListItemOut[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing shopping list item sorting and organization
|
||||||
|
*/
|
||||||
|
export function useShoppingListSorting() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
function sortItems(a: ShoppingListItemOut | ListItemGroup, b: ShoppingListItemOut | ListItemGroup) {
|
||||||
|
// Sort by position ASC, then by createdAt ASC
|
||||||
|
const posA = a.position ?? 0;
|
||||||
|
const posB = b.position ?? 0;
|
||||||
|
if (posA !== posB) {
|
||||||
|
return posA - posB;
|
||||||
|
}
|
||||||
|
const createdA = a.createdAt ?? "";
|
||||||
|
const createdB = b.createdAt ?? "";
|
||||||
|
if (createdA !== createdB) {
|
||||||
|
return createdA < createdB ? -1 : 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupAndSortListItemsByFood(shoppingList: ShoppingListOut) {
|
||||||
|
if (!shoppingList?.listItems?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedItemKey = "__checkedItem";
|
||||||
|
const listItemGroupsMap = new Map<string, ListItemGroup>();
|
||||||
|
listItemGroupsMap.set(checkedItemKey, { position: Number.MAX_SAFE_INTEGER, createdAt: "", items: [] });
|
||||||
|
|
||||||
|
// group items by checked status, food, or note
|
||||||
|
shoppingList.listItems.forEach((item) => {
|
||||||
|
const key = item.checked
|
||||||
|
? checkedItemKey
|
||||||
|
: item.food?.name
|
||||||
|
? item.food.name
|
||||||
|
: item.note || "";
|
||||||
|
|
||||||
|
const group = listItemGroupsMap.get(key);
|
||||||
|
if (!group) {
|
||||||
|
listItemGroupsMap.set(key, { position: item.position || 0, createdAt: item.createdAt || "", items: [item] });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
group.items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const listItemGroups = Array.from(listItemGroupsMap.values());
|
||||||
|
listItemGroups.sort(sortItems);
|
||||||
|
|
||||||
|
// sort group items, then aggregate them
|
||||||
|
const sortedItems: ShoppingListItemOut[] = [];
|
||||||
|
let nextPosition = 0;
|
||||||
|
listItemGroups.forEach((listItemGroup) => {
|
||||||
|
listItemGroup.items.sort(sortItems);
|
||||||
|
listItemGroup.items.forEach((item) => {
|
||||||
|
item.position = nextPosition;
|
||||||
|
nextPosition += 1;
|
||||||
|
sortedItems.push(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
shoppingList.listItems = sortedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortListItems(shoppingList: ShoppingListOut) {
|
||||||
|
if (!shoppingList?.listItems?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shoppingList.listItems.sort(sortItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItemsByLabel(shoppingList: ShoppingListOut) {
|
||||||
|
const items: { [prop: string]: ShoppingListItemOut[] } = {};
|
||||||
|
const noLabelText = t("shopping-list.no-label");
|
||||||
|
const noLabel = [] as ShoppingListItemOut[];
|
||||||
|
|
||||||
|
shoppingList?.listItems?.forEach((item) => {
|
||||||
|
if (item.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.labelId) {
|
||||||
|
if (item.label && item.label.name in items) {
|
||||||
|
items[item.label.name].push(item);
|
||||||
|
}
|
||||||
|
else if (item.label) {
|
||||||
|
items[item.label.name] = [item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
noLabel.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (noLabel.length > 0) {
|
||||||
|
items[noLabelText] = noLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the map by label order
|
||||||
|
const orderedLabelNames = shoppingList?.labelSettings?.map(labelSetting => labelSetting.label.name);
|
||||||
|
if (!orderedLabelNames) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsSorted: { [prop: string]: ShoppingListItemOut[] } = {};
|
||||||
|
if (noLabelText in items) {
|
||||||
|
itemsSorted[noLabelText] = items[noLabelText];
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedLabelNames.forEach((labelName) => {
|
||||||
|
if (labelName in items) {
|
||||||
|
itemsSorted[labelName] = items[labelName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return itemsSorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortItems,
|
||||||
|
groupAndSortListItemsByFood,
|
||||||
|
sortListItems,
|
||||||
|
updateItemsByLabel,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import type { ShoppingListOut, ShoppingListItemOut } from "~/lib/api/types/household";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for managing shopping list state and reactive data
|
||||||
|
*/
|
||||||
|
export function useShoppingListState() {
|
||||||
|
const shoppingList = ref<ShoppingListOut | null>(null);
|
||||||
|
const loadingCounter = ref(1);
|
||||||
|
const recipeReferenceLoading = ref(false);
|
||||||
|
const preserveItemOrder = ref(false);
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
const edit = ref(false);
|
||||||
|
const threeDot = ref(false);
|
||||||
|
const reorderLabelsDialog = ref(false);
|
||||||
|
const createEditorOpen = ref(false);
|
||||||
|
|
||||||
|
// Dialog states
|
||||||
|
const state = reactive({
|
||||||
|
checkAllDialog: false,
|
||||||
|
uncheckAllDialog: false,
|
||||||
|
deleteCheckedDialog: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hydrate listItems from shoppingList.value?.listItems
|
||||||
|
const listItems = reactive({
|
||||||
|
unchecked: [] as ShoppingListItemOut[],
|
||||||
|
checked: [] as ShoppingListItemOut[],
|
||||||
|
});
|
||||||
|
|
||||||
|
function sortCheckedItems(a: ShoppingListItemOut, b: ShoppingListItemOut) {
|
||||||
|
if (a.updatedAt! === b.updatedAt!) {
|
||||||
|
return ((a.position || 0) > (b.position || 0)) ? -1 : 1;
|
||||||
|
}
|
||||||
|
return a.updatedAt! < b.updatedAt! ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => shoppingList.value?.listItems,
|
||||||
|
(items) => {
|
||||||
|
listItems.unchecked = (items?.filter(item => !item.checked) ?? []);
|
||||||
|
listItems.checked = (items?.filter(item => item.checked)
|
||||||
|
.sort(sortCheckedItems) ?? []);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const recipeMap = computed(() => new Map(
|
||||||
|
(shoppingList.value?.recipeReferences?.map(ref => ref.recipe) ?? [])
|
||||||
|
.map(recipe => [recipe.id || "", recipe])),
|
||||||
|
);
|
||||||
|
|
||||||
|
const recipeList = computed(() => Array.from(recipeMap.value.values()));
|
||||||
|
|
||||||
|
return {
|
||||||
|
shoppingList,
|
||||||
|
loadingCounter,
|
||||||
|
recipeReferenceLoading,
|
||||||
|
preserveItemOrder,
|
||||||
|
edit,
|
||||||
|
threeDot,
|
||||||
|
reorderLabelsDialog,
|
||||||
|
createEditorOpen,
|
||||||
|
state,
|
||||||
|
listItems,
|
||||||
|
recipeMap,
|
||||||
|
recipeList,
|
||||||
|
sortCheckedItems,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||||
|
import { useShoppingListState } from "~/composables/shopping-list-page/sub-composables/use-shopping-list-state";
|
||||||
|
import { useShoppingListData } from "~/composables/shopping-list-page/sub-composables/use-shopping-list-data";
|
||||||
|
import { useShoppingListSorting } from "~/composables/shopping-list-page/sub-composables/use-shopping-list-sorting";
|
||||||
|
import { useShoppingListLabels } from "~/composables/shopping-list-page/sub-composables/use-shopping-list-labels";
|
||||||
|
import { useShoppingListCopy } from "~/composables/shopping-list-page/sub-composables/use-shopping-list-copy";
|
||||||
|
import { useShoppingListCrud } from "~/composables/shopping-list-page/sub-composables/use-shopping-list-crud";
|
||||||
|
import { useShoppingListRecipes } from "~/composables/shopping-list-page/sub-composables/use-shopping-list-recipes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main composable that orchestrates all shopping list page functionality
|
||||||
|
*/
|
||||||
|
export function useShoppingListPage(listId: string) {
|
||||||
|
// Initialize state
|
||||||
|
const state = useShoppingListState();
|
||||||
|
const {
|
||||||
|
shoppingList,
|
||||||
|
loadingCounter,
|
||||||
|
recipeReferenceLoading,
|
||||||
|
preserveItemOrder,
|
||||||
|
listItems,
|
||||||
|
sortCheckedItems,
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
// Initialize sorting functionality
|
||||||
|
const sorting = useShoppingListSorting();
|
||||||
|
const { groupAndSortListItemsByFood, sortListItems, updateItemsByLabel } = sorting;
|
||||||
|
|
||||||
|
// Track items organized by label
|
||||||
|
const itemsByLabel = ref<{ [key: string]: ShoppingListItemOut[] }>({});
|
||||||
|
|
||||||
|
function updateListItemOrder() {
|
||||||
|
if (!shoppingList.value) return;
|
||||||
|
|
||||||
|
if (!preserveItemOrder.value) {
|
||||||
|
groupAndSortListItemsByFood(shoppingList.value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sortListItems(shoppingList.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labeledItems = updateItemsByLabel(shoppingList.value);
|
||||||
|
if (labeledItems) {
|
||||||
|
itemsByLabel.value = labeledItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize data management
|
||||||
|
const dataManager = useShoppingListData(listId, shoppingList, loadingCounter);
|
||||||
|
const { isOffline, refresh: baseRefresh, startPolling, stopPolling, shoppingListItemActions } = dataManager;
|
||||||
|
|
||||||
|
const refresh = () => baseRefresh(updateListItemOrder);
|
||||||
|
|
||||||
|
// Initialize shopping list labels
|
||||||
|
const labels = useShoppingListLabels(shoppingList);
|
||||||
|
|
||||||
|
// Initialize copy functionality
|
||||||
|
const copyManager = useShoppingListCopy();
|
||||||
|
|
||||||
|
// Initialize CRUD operations
|
||||||
|
const crud = useShoppingListCrud(
|
||||||
|
shoppingList,
|
||||||
|
loadingCounter,
|
||||||
|
listItems,
|
||||||
|
shoppingListItemActions,
|
||||||
|
refresh,
|
||||||
|
sortCheckedItems,
|
||||||
|
updateListItemOrder,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize recipe management
|
||||||
|
const recipes = useShoppingListRecipes(
|
||||||
|
shoppingList,
|
||||||
|
loadingCounter,
|
||||||
|
recipeReferenceLoading,
|
||||||
|
refresh,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle item reordering by label
|
||||||
|
function updateIndexUncheckedByLabel(labelName: string, labeledUncheckedItems: ShoppingListItemOut[]) {
|
||||||
|
if (!itemsByLabel.value[labelName]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update this label's item order
|
||||||
|
itemsByLabel.value[labelName] = labeledUncheckedItems;
|
||||||
|
|
||||||
|
// reset list order of all items
|
||||||
|
const allUncheckedItems: ShoppingListItemOut[] = [];
|
||||||
|
for (const labelKey in itemsByLabel.value) {
|
||||||
|
allUncheckedItems.push(...itemsByLabel.value[labelKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// since the user has manually reordered the list, we should preserve this order
|
||||||
|
preserveItemOrder.value = true;
|
||||||
|
|
||||||
|
// save changes
|
||||||
|
listItems.unchecked = allUncheckedItems;
|
||||||
|
listItems.checked = shoppingList.value?.listItems?.filter(item => item.checked) || [];
|
||||||
|
crud.updateUncheckedListItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialog helpers
|
||||||
|
function openCheckAll() {
|
||||||
|
if (shoppingList.value?.listItems?.some(item => !item.checked)) {
|
||||||
|
state.state.checkAllDialog = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUncheckAll() {
|
||||||
|
if (shoppingList.value?.listItems?.some(item => item.checked)) {
|
||||||
|
state.state.uncheckAllDialog = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteChecked() {
|
||||||
|
if (shoppingList.value?.listItems?.some(item => item.checked)) {
|
||||||
|
state.state.deleteCheckedDialog = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAll() {
|
||||||
|
state.state.checkAllDialog = false;
|
||||||
|
crud.checkAllItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uncheckAll() {
|
||||||
|
state.state.uncheckAllDialog = false;
|
||||||
|
crud.uncheckAllItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteChecked() {
|
||||||
|
state.state.deleteCheckedDialog = false;
|
||||||
|
crud.deleteCheckedItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy functionality wrapper
|
||||||
|
function copyListItems(copyType: "plain" | "markdown") {
|
||||||
|
copyManager.copyListItems(itemsByLabel.value, copyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label reordering helpers
|
||||||
|
function toggleReorderLabelsDialog() {
|
||||||
|
crud.toggleReorderLabelsDialog(state.reorderLabelsDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveLabelOrder() {
|
||||||
|
await crud.saveLabelOrder(() => {
|
||||||
|
const labeledItems = updateItemsByLabel(shoppingList.value!);
|
||||||
|
if (labeledItems) {
|
||||||
|
itemsByLabel.value = labeledItems;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle management
|
||||||
|
onMounted(() => {
|
||||||
|
startPolling(updateListItemOrder);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
itemsByLabel,
|
||||||
|
isOffline,
|
||||||
|
|
||||||
|
// Sub-composables
|
||||||
|
...state,
|
||||||
|
...labels,
|
||||||
|
...crud,
|
||||||
|
...recipes,
|
||||||
|
|
||||||
|
// Specialized functions
|
||||||
|
updateIndexUncheckedByLabel,
|
||||||
|
copyListItems,
|
||||||
|
|
||||||
|
// Dialog actions
|
||||||
|
openCheckAll,
|
||||||
|
openUncheckAll,
|
||||||
|
openDeleteChecked,
|
||||||
|
checkAll,
|
||||||
|
uncheckAll,
|
||||||
|
deleteChecked,
|
||||||
|
|
||||||
|
// Label management
|
||||||
|
toggleReorderLabelsDialog,
|
||||||
|
saveLabelOrder,
|
||||||
|
|
||||||
|
// Data refresh
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,18 +1,18 @@
|
||||||
import type { Composer } from "vue-i18n";
|
import type { Composer } from "vue-i18n";
|
||||||
import { useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
import { useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
const store: Ref<RecipeCookBook[]> = ref([]);
|
const store: Ref<ReadCookBook[]> = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
export const useCookbookStore = function (i18n?: Composer) {
|
export const useCookbookStore = function (i18n?: Composer) {
|
||||||
const api = useUserApi(i18n);
|
const api = useUserApi(i18n);
|
||||||
return useStore<RecipeCookBook>(store, loading, api.cookbooks);
|
return useStore<ReadCookBook>(store, loading, api.cookbooks);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) {
|
export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) {
|
||||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||||
return useReadOnlyStore<RecipeCookBook>(store, publicLoading, api.cookbooks);
|
return useReadOnlyStore<ReadCookBook>(store, publicLoading, api.cookbooks);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { AutoFormItems } from "~/types/auto-forms";
|
||||||
export const useCommonSettingsForm = () => {
|
export const useCommonSettingsForm = () => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const commonSettingsForm: AutoFormItems = [
|
const commonSettingsForm = computed<AutoFormItems>(() => [
|
||||||
{
|
{
|
||||||
section: i18n.t("profile.group-settings"),
|
section: i18n.t("profile.group-settings"),
|
||||||
label: i18n.t("group.enable-public-access"),
|
label: i18n.t("group.enable-public-access"),
|
||||||
|
@ -21,7 +21,7 @@ export const useCommonSettingsForm = () => {
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
rules: ["required"],
|
rules: ["required"],
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commonSettingsForm,
|
commonSettingsForm,
|
||||||
|
|
|
@ -33,7 +33,6 @@ export interface UserRecipePreferences {
|
||||||
|
|
||||||
export interface UserShoppingListPreferences {
|
export interface UserShoppingListPreferences {
|
||||||
viewAllLists: boolean;
|
viewAllLists: boolean;
|
||||||
viewByLabel: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserTimelinePreferences {
|
export interface UserTimelinePreferences {
|
||||||
|
@ -129,7 +128,6 @@ export function useShoppingListPreferences(): Ref<UserShoppingListPreferences> {
|
||||||
"shopping-list-preferences",
|
"shopping-list-preferences",
|
||||||
{
|
{
|
||||||
viewAllLists: false,
|
viewAllLists: false,
|
||||||
viewByLabel: true,
|
|
||||||
},
|
},
|
||||||
{ mergeDefaults: true },
|
{ mergeDefaults: true },
|
||||||
// we cast to a Ref because by default it will return an optional type ref
|
// we cast to a Ref because by default it will return an optional type ref
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAsyncValidator } from "~/composables/use-validators";
|
import { useAsyncValidator } from "~/composables/use-validators";
|
||||||
import type { VForm } from "~/types/vuetify";
|
import type { VForm } from "~/types/auto-forms";
|
||||||
import { usePublicApi } from "~/composables/api/api-client";
|
import { usePublicApi } from "~/composables/api/api-client";
|
||||||
|
|
||||||
const domAccountForm = ref<VForm | null>(null);
|
const domAccountForm = ref<VForm | null>(null);
|
||||||
|
@ -13,12 +13,14 @@ const advancedOptions = ref(false);
|
||||||
export const useUserRegistrationForm = () => {
|
export const useUserRegistrationForm = () => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
function safeValidate(form: Ref<VForm | null>) {
|
async function safeValidate(form: Ref<VForm | null>) {
|
||||||
if (form.value && form.value.validate) {
|
if (!form.value) {
|
||||||
return form.value.validate();
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await form.value.validate();
|
||||||
|
return result.valid;
|
||||||
|
}
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Provide Group Details
|
// Provide Group Details
|
||||||
const publicApi = usePublicApi();
|
const publicApi = usePublicApi();
|
||||||
|
@ -45,11 +47,15 @@ export const useUserRegistrationForm = () => {
|
||||||
email,
|
email,
|
||||||
advancedOptions,
|
advancedOptions,
|
||||||
validate: async () => {
|
validate: async () => {
|
||||||
if (!(validUsername.value && validEmail.value)) {
|
if (!validUsername.value || !validEmail.value) {
|
||||||
await Promise.all([validateUsername(), validateEmail()]);
|
await Promise.all([validateUsername(), validateEmail()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (safeValidate(domAccountForm as Ref<VForm>) && validUsername.value && validEmail.value);
|
if (!validUsername.value || !validEmail.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await safeValidate(domAccountForm as Ref<VForm>);
|
||||||
},
|
},
|
||||||
reset: () => {
|
reset: () => {
|
||||||
accountDetails.username.value = "";
|
accountDetails.username.value = "";
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nuwe kennisgewing",
|
"new-notification": "Nuwe kennisgewing",
|
||||||
"event-notifiers": "Gebeurteniskennisgewers",
|
"event-notifiers": "Gebeurteniskennisgewers",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (oorgeslaan indien leeg)",
|
"apprise-url-skipped-if-blank": "Apprise URL (oorgeslaan indien leeg)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Aktiveer kennisgewer",
|
"enable-notifier": "Aktiveer kennisgewer",
|
||||||
"what-events": "Op watter gebeurtenisse moet hierdie kennisgewing inteken?",
|
"what-events": "Op watter gebeurtenisse moet hierdie kennisgewing inteken?",
|
||||||
"user-events": "Gebruikersgebeurtenisse",
|
"user-events": "Gebruikersgebeurtenisse",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Groep besonderhede",
|
"group-details": "Groep besonderhede",
|
||||||
"group-details-description": "Voordat jy 'n rekening skep, moet jy eers 'n groep skep. Jy sal die enigste lid van die groep wees, maar jy kan later ander nooi. Lede van jou groep kan maaltydplanne, inkopielyste, resepte en meer deel!",
|
"group-details-description": "Voordat jy 'n rekening skep, moet jy eers 'n groep skep. Jy sal die enigste lid van die groep wees, maar jy kan later ander nooi. Lede van jou groep kan maaltydplanne, inkopielyste, resepte en meer deel!",
|
||||||
"use-seed-data": "Gebruik voorbeelddata",
|
"use-seed-data": "Gebruik voorbeelddata",
|
||||||
"use-seed-data-description": "Mealie bevat 'n versameling bestanddele, eenhede en etikette wat gebruik kan word om jou groep met nuttige data te vul om jou resepte te organiseer.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Rekening besonderhede"
|
"account-details": "Rekening besonderhede"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "إشعار جديد",
|
"new-notification": "إشعار جديد",
|
||||||
"event-notifiers": "إشعار الحدث",
|
"event-notifiers": "إشعار الحدث",
|
||||||
"apprise-url-skipped-if-blank": "الرابط Apprise (يتم تجاهله إذا ما كان فارغً)",
|
"apprise-url-skipped-if-blank": "الرابط Apprise (يتم تجاهله إذا ما كان فارغً)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "تفعيل الإشعارات",
|
"enable-notifier": "تفعيل الإشعارات",
|
||||||
"what-events": "ما هي الأحداث التي يجب على هذا المخدم أن يستجيب لها؟",
|
"what-events": "ما هي الأحداث التي يجب على هذا المخدم أن يستجيب لها؟",
|
||||||
"user-events": "أحداث المستخدمين",
|
"user-events": "أحداث المستخدمين",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "تفاصيل المجموعة",
|
"group-details": "تفاصيل المجموعة",
|
||||||
"group-details-description": "قبل إنشاء حساب ستحتاج إلى إنشاء مجموعة. المجموعة الخاصة بك سوف تحتوي عليك فقط، ولكن ستتمكن من دعوة الآخرين لاحقاً. يمكن لأعضاء مجموعتك مشاركة خطط الوجبات وقوائم التسوق والوصفات، والمزيد!",
|
"group-details-description": "قبل إنشاء حساب ستحتاج إلى إنشاء مجموعة. المجموعة الخاصة بك سوف تحتوي عليك فقط، ولكن ستتمكن من دعوة الآخرين لاحقاً. يمكن لأعضاء مجموعتك مشاركة خطط الوجبات وقوائم التسوق والوصفات، والمزيد!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie يأتي بمجموعة من الأطعمة والوحدات والعلامات التي يمكن استخدامها لتزويد مجموعتك ببيانات مفيدة لتنظيم وصفاتك.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "تفاصيل الحساب"
|
"account-details": "تفاصيل الحساب"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Ново известие",
|
"new-notification": "Ново известие",
|
||||||
"event-notifiers": "Известия за събитие",
|
"event-notifiers": "Известия за събитие",
|
||||||
"apprise-url-skipped-if-blank": "URL за известяване (пропуска се ако е празно)",
|
"apprise-url-skipped-if-blank": "URL за известяване (пропуска се ако е празно)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Включи известията",
|
"enable-notifier": "Включи известията",
|
||||||
"what-events": "За кои събития трябва да се получават известия?",
|
"what-events": "За кои събития трябва да се получават известия?",
|
||||||
"user-events": "Потребителски събития",
|
"user-events": "Потребителски събития",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Подробности за групата",
|
"group-details": "Подробности за групата",
|
||||||
"group-details-description": "Преди да създадете акаунт, ще трябва да създадете група. Вашата група ще съдържа само Вас, но ще можете да поканите други по-късно. Членовете във вашата група могат да споделят планове за хранене, списъци за пазаруване, рецепти и други!",
|
"group-details-description": "Преди да създадете акаунт, ще трябва да създадете група. Вашата група ще съдържа само Вас, но ще можете да поканите други по-късно. Членовете във вашата група могат да споделят планове за хранене, списъци за пазаруване, рецепти и други!",
|
||||||
"use-seed-data": "Използвай предварителни данни",
|
"use-seed-data": "Използвай предварителни данни",
|
||||||
"use-seed-data-description": "Mealie се доставя с колекция от продукти, мерни единици и етикети за попълване на Вашата група с полезни данни за организиране на рецептите.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Подробни данни за акаунта"
|
"account-details": "Подробни данни за акаунта"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nova notificació",
|
"new-notification": "Nova notificació",
|
||||||
"event-notifiers": "Notificacions d'esdeveniments",
|
"event-notifiers": "Notificacions d'esdeveniments",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (si es deixa buit, s'ignorarà)",
|
"apprise-url-skipped-if-blank": "Apprise URL (si es deixa buit, s'ignorarà)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Habilita la notificació",
|
"enable-notifier": "Habilita la notificació",
|
||||||
"what-events": "Què esdeveniments vols que utilitzen aquest notificador?",
|
"what-events": "Què esdeveniments vols que utilitzen aquest notificador?",
|
||||||
"user-events": "Esdeveniments d'usuari",
|
"user-events": "Esdeveniments d'usuari",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detalls del grup",
|
"group-details": "Detalls del grup",
|
||||||
"group-details-description": "Abans de crear un compte heu de crear un grup. Al grup només hi serà vostè, però després podeu convidar d'altres. Els membres d'un grup poden compartir menús, llistes de la compra, receptes i molt més!",
|
"group-details-description": "Abans de crear un compte heu de crear un grup. Al grup només hi serà vostè, però després podeu convidar d'altres. Els membres d'un grup poden compartir menús, llistes de la compra, receptes i molt més!",
|
||||||
"use-seed-data": "Afegiu dades predeterminades",
|
"use-seed-data": "Afegiu dades predeterminades",
|
||||||
"use-seed-data-description": "Mealie ve configurat amb una col·lecció d'aliments, unitats i etiquetes que poden ser emprades pel vostre grup per a ajudar-vos a organitzar les vostres receptes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Detalls del compte"
|
"account-details": "Detalls del compte"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nové oznámení",
|
"new-notification": "Nové oznámení",
|
||||||
"event-notifiers": "Notifikace událostí",
|
"event-notifiers": "Notifikace událostí",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (přeskočeno pokud je prázdné)",
|
"apprise-url-skipped-if-blank": "Apprise URL (přeskočeno pokud je prázdné)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Povolit notifikaci",
|
"enable-notifier": "Povolit notifikaci",
|
||||||
"what-events": "K jakým událostem by se měl tento oznamovatel přihlásit?",
|
"what-events": "K jakým událostem by se měl tento oznamovatel přihlásit?",
|
||||||
"user-events": "Uživatelské události",
|
"user-events": "Uživatelské události",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Podrobnosti o skupině",
|
"group-details": "Podrobnosti o skupině",
|
||||||
"group-details-description": "Než vytvoříte svůj účet, musíte vytvořit skupinu. Vaše skupina bude obsahovat pouze vás, ale později budete moct přizvat jiné uživatele. Členové vaší skupiny mohou sdílet jídelníčky, nákupní seznamy, recepty a další!",
|
"group-details-description": "Než vytvoříte svůj účet, musíte vytvořit skupinu. Vaše skupina bude obsahovat pouze vás, ale později budete moct přizvat jiné uživatele. Členové vaší skupiny mohou sdílet jídelníčky, nákupní seznamy, recepty a další!",
|
||||||
"use-seed-data": "Použít Seed Data",
|
"use-seed-data": "Použít Seed Data",
|
||||||
"use-seed-data-description": "Mealie obsahuje kolekci potravin, jednotek a popisků, které můžete použít ve své skupině pro organizování svých receptů.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Podrobnosti účtu"
|
"account-details": "Podrobnosti účtu"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Ny notifikation",
|
"new-notification": "Ny notifikation",
|
||||||
"event-notifiers": "Notifikation om begivenheder",
|
"event-notifiers": "Notifikation om begivenheder",
|
||||||
"apprise-url-skipped-if-blank": "Informations link (sprunget over hvis ladet være tomt)",
|
"apprise-url-skipped-if-blank": "Informations link (sprunget over hvis ladet være tomt)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Aktiver Notifikationer",
|
"enable-notifier": "Aktiver Notifikationer",
|
||||||
"what-events": "Hvilke begivenheder skal denne anmelder abonnere på?",
|
"what-events": "Hvilke begivenheder skal denne anmelder abonnere på?",
|
||||||
"user-events": "Brugerhændelser",
|
"user-events": "Brugerhændelser",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Gruppeoplysninger",
|
"group-details": "Gruppeoplysninger",
|
||||||
"group-details-description": "Før du opretter en konto, skal du oprette en gruppe. Din gruppe vil kun indeholde dig, men du vil kunne invitere andre senere. Medlemmer i din gruppe kan dele madplaner, indkøbslister, opskrifter og meget mere!",
|
"group-details-description": "Før du opretter en konto, skal du oprette en gruppe. Din gruppe vil kun indeholde dig, men du vil kunne invitere andre senere. Medlemmer i din gruppe kan dele madplaner, indkøbslister, opskrifter og meget mere!",
|
||||||
"use-seed-data": "Anved standard data",
|
"use-seed-data": "Anved standard data",
|
||||||
"use-seed-data-description": "Mealie indeholder som standard en samling af fødevarer, enheder og etiketter, som du kan bruge til at oprette og organisere dine opskrifter.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Kontodetaljer"
|
"account-details": "Kontodetaljer"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Neue Benachrichtigung",
|
"new-notification": "Neue Benachrichtigung",
|
||||||
"event-notifiers": "Ereignis-Benachrichtigungen",
|
"event-notifiers": "Ereignis-Benachrichtigungen",
|
||||||
"apprise-url-skipped-if-blank": "Apprise-URL (wird übersprungen, wenn leer)",
|
"apprise-url-skipped-if-blank": "Apprise-URL (wird übersprungen, wenn leer)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Benachrichtigen aktivieren",
|
"enable-notifier": "Benachrichtigen aktivieren",
|
||||||
"what-events": "Welche Ereignisse soll diese Benachrichtigung abonnieren?",
|
"what-events": "Welche Ereignisse soll diese Benachrichtigung abonnieren?",
|
||||||
"user-events": "Benutzer-Ereignisse",
|
"user-events": "Benutzer-Ereignisse",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Gruppendetails",
|
"group-details": "Gruppendetails",
|
||||||
"group-details-description": "Bevor du ein Konto erstellst, musst du eine Gruppe erstellen. Deine Gruppe wird nur dich enthalten, aber du kannst andere später einladen. Mitglieder in deiner Gruppe können Essenspläne, Einkaufslisten, Rezepte und vieles mehr teilen!",
|
"group-details-description": "Bevor du ein Konto erstellst, musst du eine Gruppe erstellen. Deine Gruppe wird nur dich enthalten, aber du kannst andere später einladen. Mitglieder in deiner Gruppe können Essenspläne, Einkaufslisten, Rezepte und vieles mehr teilen!",
|
||||||
"use-seed-data": "Musterdaten",
|
"use-seed-data": "Musterdaten",
|
||||||
"use-seed-data-description": "Mealie enthält eine Sammlung von Lebensmitteln, Maßeinheiten und Kategorien, die verwendet werden können, um deine Gruppe mit hilfreichen Daten für die Organisation deiner Rezepte zu füllen.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Kontoinformationen"
|
"account-details": "Kontoinformationen"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Νέα ειδοποίηση",
|
"new-notification": "Νέα ειδοποίηση",
|
||||||
"event-notifiers": "Ειδοποιητές Συμβάντος",
|
"event-notifiers": "Ειδοποιητές Συμβάντος",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (παραλείπεται αν είναι κενό)",
|
"apprise-url-skipped-if-blank": "Apprise URL (παραλείπεται αν είναι κενό)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Δεδομένου ότι οι διευθύνσεις URL Apprise περιέχουν συνήθως ευαίσθητες πληροφορίες, το πεδίο αυτό παραμένει σκόπιμα κενό κατά την επεξεργασία. Αν θέλετε να ενημερώσετε το URL, παρακαλώ εισάγετε το νέο εδώ, αλλιώς αφήστε το κενό για να διατηρήσετε την τρέχουσα διεύθυνση URL.",
|
||||||
"enable-notifier": "Ενεργοποίηση ειδοποιητή",
|
"enable-notifier": "Ενεργοποίηση ειδοποιητή",
|
||||||
"what-events": "Σε ποια συμβάντα θα πρέπει να εγγραφεί αυτός ο ειδοποιητής;",
|
"what-events": "Σε ποια συμβάντα θα πρέπει να εγγραφεί αυτός ο ειδοποιητής;",
|
||||||
"user-events": "Συμβάντα Χρήστη",
|
"user-events": "Συμβάντα Χρήστη",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Λεπτομέρειες ομάδας",
|
"group-details": "Λεπτομέρειες ομάδας",
|
||||||
"group-details-description": "Πριν δημιουργήσετε ένα λογαριασμό θα πρέπει να δημιουργήσετε μια ομάδα. Η ομάδα σας θα περιέχει μόνο εσάς, αλλά θα μπορείτε να προσκαλέσετε άλλους αργότερα. Μέλη της ομάδας σας μπορούν να μοιραστούν προγράμματα γευμάτων, λίστες για ψώνια, συνταγές και πολλά άλλα!",
|
"group-details-description": "Πριν δημιουργήσετε ένα λογαριασμό θα πρέπει να δημιουργήσετε μια ομάδα. Η ομάδα σας θα περιέχει μόνο εσάς, αλλά θα μπορείτε να προσκαλέσετε άλλους αργότερα. Μέλη της ομάδας σας μπορούν να μοιραστούν προγράμματα γευμάτων, λίστες για ψώνια, συνταγές και πολλά άλλα!",
|
||||||
"use-seed-data": "Χρήση δεδομένων από τροφοδοσία",
|
"use-seed-data": "Χρήση δεδομένων από τροφοδοσία",
|
||||||
"use-seed-data-description": "Το Mealie έρχεται με μια συλλογή Τροφίμων, Μονάδων και Ετικετών που μπορούν να χρησιμοποιηθούν για τη συμπλήρωση της ομάδας σας με χρήσιμα δεδομένα για την οργάνωση των συνταγών σας.",
|
"use-seed-data-description": "Το Mealie έρχεται με μια συλλογή Τροφίμων, Μονάδων και Ετικετών που μπορούν να χρησιμοποιηθούν για τη συμπλήρωση της ομάδας σας με χρήσιμα δεδομένα για την οργάνωση των συνταγών σας. Αυτά είναι μεταφρασμένα στη γλώσσα που έχετε επιλέξει. Μπορείτε πάντα να προσθέσετε ή να τροποποιήσετε αυτά τα δεδομένα αργότερα.",
|
||||||
"account-details": "Λεπτομέρειες λογαριασμού"
|
"account-details": "Λεπτομέρειες λογαριασμού"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "New Notification",
|
"new-notification": "New Notification",
|
||||||
"event-notifiers": "Event Notifiers",
|
"event-notifiers": "Event Notifiers",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (skipped if blank)",
|
"apprise-url-skipped-if-blank": "Apprise URL (skipped if blank)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Enable Notifier",
|
"enable-notifier": "Enable Notifier",
|
||||||
"what-events": "What events should this notifier subscribe to?",
|
"what-events": "What events should this notifier subscribe to?",
|
||||||
"user-events": "User Events",
|
"user-events": "User Events",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "New Notification",
|
"new-notification": "New Notification",
|
||||||
"event-notifiers": "Event Notifiers",
|
"event-notifiers": "Event Notifiers",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (skipped if blank)",
|
"apprise-url-skipped-if-blank": "Apprise URL (skipped if blank)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Enable Notifier",
|
"enable-notifier": "Enable Notifier",
|
||||||
"what-events": "What events should this notifier subscribe to?",
|
"what-events": "What events should this notifier subscribe to?",
|
||||||
"user-events": "User Events",
|
"user-events": "User Events",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nueva notificación",
|
"new-notification": "Nueva notificación",
|
||||||
"event-notifiers": "Notificaciones de eventos",
|
"event-notifiers": "Notificaciones de eventos",
|
||||||
"apprise-url-skipped-if-blank": "URL de Apprise (omitida si está en blanco)",
|
"apprise-url-skipped-if-blank": "URL de Apprise (omitida si está en blanco)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Habilitar notificador",
|
"enable-notifier": "Habilitar notificador",
|
||||||
"what-events": "¿A qué eventos debe suscribirse este notificador?",
|
"what-events": "¿A qué eventos debe suscribirse este notificador?",
|
||||||
"user-events": "Eventos de los usuarios",
|
"user-events": "Eventos de los usuarios",
|
||||||
|
@ -80,7 +81,7 @@
|
||||||
"category-events": "Eventos de Categoría",
|
"category-events": "Eventos de Categoría",
|
||||||
"when-a-new-user-joins-your-group": "Cuando un nuevo usuario se une a tu grupo",
|
"when-a-new-user-joins-your-group": "Cuando un nuevo usuario se une a tu grupo",
|
||||||
"recipe-events": "Eventos de receta",
|
"recipe-events": "Eventos de receta",
|
||||||
"label-events": "Label Events"
|
"label-events": "Eventos de etiqueta"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"add": "Agregar",
|
"add": "Agregar",
|
||||||
|
@ -674,8 +675,8 @@
|
||||||
"upload-another-image": "Subir otra imagen",
|
"upload-another-image": "Subir otra imagen",
|
||||||
"upload-images": "Subir imágenes",
|
"upload-images": "Subir imágenes",
|
||||||
"upload-more-images": "Subir más imágenes",
|
"upload-more-images": "Subir más imágenes",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Establecer como imagen de portada de receta",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Imagen de portada"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Buscador de recetas",
|
"recipe-finder": "Buscador de recetas",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detalles del grupo",
|
"group-details": "Detalles del grupo",
|
||||||
"group-details-description": "Antes de crear una cuenta, debe crear un grupo. En el grupo sólo estará usted, pero puede invitar a otros más tarde. Los miembros de un grupo pueden compartir menús, listas de la compra, recetas y más...",
|
"group-details-description": "Antes de crear una cuenta, debe crear un grupo. En el grupo sólo estará usted, pero puede invitar a otros más tarde. Los miembros de un grupo pueden compartir menús, listas de la compra, recetas y más...",
|
||||||
"use-seed-data": "Utilizar datos de ejemplo",
|
"use-seed-data": "Utilizar datos de ejemplo",
|
||||||
"use-seed-data-description": "Mealie incluye una colección de alimentos, unidades y etiquetas, que puede utilizar como ejemplo en su grupo, para ayudarle a organizar sus recetas.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Información de la cuenta"
|
"account-details": "Información de la cuenta"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Uus teade",
|
"new-notification": "Uus teade",
|
||||||
"event-notifiers": "Sündmuste märguanded",
|
"event-notifiers": "Sündmuste märguanded",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (kui on tühi, jäetakse vahele)",
|
"apprise-url-skipped-if-blank": "Apprise URL (kui on tühi, jäetakse vahele)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Luba teavitaja",
|
"enable-notifier": "Luba teavitaja",
|
||||||
"what-events": "Millised sündmused peaks see teavitaja tellimaa?",
|
"what-events": "Millised sündmused peaks see teavitaja tellimaa?",
|
||||||
"user-events": "Kasutaja sündmused",
|
"user-events": "Kasutaja sündmused",
|
||||||
|
@ -80,7 +81,7 @@
|
||||||
"category-events": "Kategooria sündmused",
|
"category-events": "Kategooria sündmused",
|
||||||
"when-a-new-user-joins-your-group": "Kui uus kasutaja liitub sinu grupiga",
|
"when-a-new-user-joins-your-group": "Kui uus kasutaja liitub sinu grupiga",
|
||||||
"recipe-events": "Retsepti sündmused",
|
"recipe-events": "Retsepti sündmused",
|
||||||
"label-events": "Label Events"
|
"label-events": "Sildista sündmused"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"add": "Lisa",
|
"add": "Lisa",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Grupi detailid",
|
"group-details": "Grupi detailid",
|
||||||
"group-details-description": "Sa pead looma grupi enne konto loomist. Sinu grupis oled vaid sina, kuid sa saad kutsuda teisi sinna hiljem. Su grupi liikmed saavad jagada toitumisplaane, ostunimekirju, retsepte ja muud!",
|
"group-details-description": "Sa pead looma grupi enne konto loomist. Sinu grupis oled vaid sina, kuid sa saad kutsuda teisi sinna hiljem. Su grupi liikmed saavad jagada toitumisplaane, ostunimekirju, retsepte ja muud!",
|
||||||
"use-seed-data": "Kasuta baasandmete infot.",
|
"use-seed-data": "Kasuta baasandmete infot.",
|
||||||
"use-seed-data-description": "Mealsiga on kaasas toiduainete, ühikute ja siltide kogu, mida saate kasutada oma rühma täitmiseks kasuliku teabega retseptide korraldamiseks.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Konto üksikasjad"
|
"account-details": "Konto üksikasjad"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Uusi ilmoitus",
|
"new-notification": "Uusi ilmoitus",
|
||||||
"event-notifiers": "Tapahtumien ilmoitukset",
|
"event-notifiers": "Tapahtumien ilmoitukset",
|
||||||
"apprise-url-skipped-if-blank": "Ilmoitusverkko-osoite (voi jättää tyhjäksi)",
|
"apprise-url-skipped-if-blank": "Ilmoitusverkko-osoite (voi jättää tyhjäksi)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Ota ilmoittaja käyttöön",
|
"enable-notifier": "Ota ilmoittaja käyttöön",
|
||||||
"what-events": "Mistä tapahtumista tulisi ilmoittaa?",
|
"what-events": "Mistä tapahtumista tulisi ilmoittaa?",
|
||||||
"user-events": "Käyttäjän tapahtumat",
|
"user-events": "Käyttäjän tapahtumat",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Ryhmän tiedot",
|
"group-details": "Ryhmän tiedot",
|
||||||
"group-details-description": "Ennen kuin luot tilin, sinun on luotava ryhmä. Ryhmässäsi on vain sinä, mutta voit kutsua muita myöhemmin. Ryhmäsi jäsenet voivat jakaa ateriasuunnitelmia, ostoslistoja, reseptejä ja paljon muuta!",
|
"group-details-description": "Ennen kuin luot tilin, sinun on luotava ryhmä. Ryhmässäsi on vain sinä, mutta voit kutsua muita myöhemmin. Ryhmäsi jäsenet voivat jakaa ateriasuunnitelmia, ostoslistoja, reseptejä ja paljon muuta!",
|
||||||
"use-seed-data": "Käytä pohjatietoja",
|
"use-seed-data": "Käytä pohjatietoja",
|
||||||
"use-seed-data-description": "Mealien mukana toimitetaan kokoelma elintarvikkeita, yksiköitä ja tarroja, joiden avulla voit täyttää ryhmäsi hyödyllisillä tiedoilla reseptien järjestämiseen.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Tilitiedot"
|
"account-details": "Tilitiedot"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nouvelle notification",
|
"new-notification": "Nouvelle notification",
|
||||||
"event-notifiers": "Notifications d'événements",
|
"event-notifiers": "Notifications d'événements",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Activer la notification",
|
"enable-notifier": "Activer la notification",
|
||||||
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
||||||
"user-events": "Evénements utilisateur",
|
"user-events": "Evénements utilisateur",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Détails du groupe",
|
"group-details": "Détails du groupe",
|
||||||
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
||||||
"use-seed-data": "Utiliser l'initialisation de données",
|
"use-seed-data": "Utiliser l'initialisation de données",
|
||||||
"use-seed-data-description": "Mealie inclut avec une liste d’aliments, d’unités et d’étiquettes qui peut être utilisée pour initialiser votre groupe avec des données utiles pour organiser vos recettes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Détails du compte"
|
"account-details": "Détails du compte"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nouvelle notification",
|
"new-notification": "Nouvelle notification",
|
||||||
"event-notifiers": "Notifications d'événements",
|
"event-notifiers": "Notifications d'événements",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Activer la notification",
|
"enable-notifier": "Activer la notification",
|
||||||
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
||||||
"user-events": "Événements de l'utilisateur",
|
"user-events": "Événements de l'utilisateur",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Détails du groupe",
|
"group-details": "Détails du groupe",
|
||||||
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
||||||
"use-seed-data": "Utiliser l'initialisation de données",
|
"use-seed-data": "Utiliser l'initialisation de données",
|
||||||
"use-seed-data-description": "Mealie inclut avec une liste d’aliments, d’unités et d’étiquettes qui peut être utilisée pour initialiser votre groupe avec des données utiles pour organiser vos recettes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Détails du compte"
|
"account-details": "Détails du compte"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nouvelle notification",
|
"new-notification": "Nouvelle notification",
|
||||||
"event-notifiers": "Notifications d'événements",
|
"event-notifiers": "Notifications d'événements",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Activer la notification",
|
"enable-notifier": "Activer la notification",
|
||||||
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
||||||
"user-events": "Événements utilisateur",
|
"user-events": "Événements utilisateur",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Détails du groupe",
|
"group-details": "Détails du groupe",
|
||||||
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
||||||
"use-seed-data": "Utiliser l'initialisation de données",
|
"use-seed-data": "Utiliser l'initialisation de données",
|
||||||
"use-seed-data-description": "Mealie inclut avec une liste d’aliments, d’unités et d’étiquettes qui peut être utilisée pour initialiser votre groupe avec des données utiles pour organiser vos recettes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Détails du compte"
|
"account-details": "Détails du compte"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nova Notificación",
|
"new-notification": "Nova Notificación",
|
||||||
"event-notifiers": "Notificadores de Eventos",
|
"event-notifiers": "Notificadores de Eventos",
|
||||||
"apprise-url-skipped-if-blank": "URL de Apprise (omitido se está en branco)",
|
"apprise-url-skipped-if-blank": "URL de Apprise (omitido se está en branco)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Activar o Notificador",
|
"enable-notifier": "Activar o Notificador",
|
||||||
"what-events": "A que eventos debería subscribirse este notificador?",
|
"what-events": "A que eventos debería subscribirse este notificador?",
|
||||||
"user-events": "Eventos de Usuario",
|
"user-events": "Eventos de Usuario",
|
||||||
|
@ -639,7 +640,7 @@
|
||||||
"bulk-import-process-has-failed": "Erro no proceso de importación en masa",
|
"bulk-import-process-has-failed": "Erro no proceso de importación en masa",
|
||||||
"report-deletion-failed": "Erro ao eliminar relatorio",
|
"report-deletion-failed": "Erro ao eliminar relatorio",
|
||||||
"recipe-debugger": "Depurador de Receitas",
|
"recipe-debugger": "Depurador de Receitas",
|
||||||
"recipe-debugger-description": "Copie o URL da receita que quer depurar e pégueo aqui. O URL será lido polo lector de receitas e os resultados serán mostrados. Se non ves negún dato devolto, a páxina que está a tentar ler non é suportada polo Mealie ou pola sua biblioteca de 'scrapping'.",
|
"recipe-debugger-description": "Copie o URL da receita que quer depurar e pégueo aqui. O URL será lido polo lector de receitas e os resultados serán mostrados. Se non ve nengún dato devolto, a páxina que está a tentar ler non é suportada polo Mealie ou pola sua biblioteca de 'scrapping'.",
|
||||||
"use-openai": "Utilizar OpenAI",
|
"use-openai": "Utilizar OpenAI",
|
||||||
"recipe-debugger-use-openai-description": "Utilize o OpenAI para analisar os resultados en vez de depender da biblioteca de scrapers. Ao crear unha receita através dun URL, isto é feito automaticamente se a biblioteca de scrapers falla, mas pode provala manualmente aqui.",
|
"recipe-debugger-use-openai-description": "Utilize o OpenAI para analisar os resultados en vez de depender da biblioteca de scrapers. Ao crear unha receita através dun URL, isto é feito automaticamente se a biblioteca de scrapers falla, mas pode provala manualmente aqui.",
|
||||||
"debug": "Depurar",
|
"debug": "Depurar",
|
||||||
|
@ -665,7 +666,7 @@
|
||||||
"no-unit": "Sen unidades",
|
"no-unit": "Sen unidades",
|
||||||
"missing-unit": "Crear a unidade que falta: {unit}",
|
"missing-unit": "Crear a unidade que falta: {unit}",
|
||||||
"missing-food": "Crear a comida que falta: {food}",
|
"missing-food": "Crear a comida que falta: {food}",
|
||||||
"this-unit-could-not-be-parsed-automatically": "Non foi posível procesar automaticamente esta unidade",
|
"this-unit-could-not-be-parsed-automatically": "Non foi posíbel procesar automaticamente esta unidade",
|
||||||
"this-food-could-not-be-parsed-automatically": "Non foi posíbel procesar automaticamente este alimento",
|
"this-food-could-not-be-parsed-automatically": "Non foi posíbel procesar automaticamente este alimento",
|
||||||
"no-food": "Sen Comida"
|
"no-food": "Sen Comida"
|
||||||
},
|
},
|
||||||
|
@ -679,7 +680,7 @@
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Localizador de Receitas",
|
"recipe-finder": "Localizador de Receitas",
|
||||||
"recipe-finder-description": "Procure receitas con base nos ingredientes que teñas a man. Pode tamén filtrar polas ferramentas disponíveis e definir un número máximo de ingredientes ou ferramentas que faltan.",
|
"recipe-finder-description": "Procure receitas con base nos ingredientes que teña a man. Pode tamén filtrar polas ferramentas disponíbeis e definir un número máximo de ingredientes ou ferramentas que faltan.",
|
||||||
"selected-ingredients": "Ingredientes Selecionados",
|
"selected-ingredients": "Ingredientes Selecionados",
|
||||||
"no-ingredients-selected": "Nengun ingrediente selecionado",
|
"no-ingredients-selected": "Nengun ingrediente selecionado",
|
||||||
"missing": "En falta",
|
"missing": "En falta",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detalles do Grupo",
|
"group-details": "Detalles do Grupo",
|
||||||
"group-details-description": "Antes de crear unha conta é necesario crear un grupo. Será o único membro do seu grupo, mas poderá convidar outros mais tarde. Os membros do seu grupo poden compartir menús, listas de compras, receitas e moito mais!",
|
"group-details-description": "Antes de crear unha conta é necesario crear un grupo. Será o único membro do seu grupo, mas poderá convidar outros mais tarde. Os membros do seu grupo poden compartir menús, listas de compras, receitas e moito mais!",
|
||||||
"use-seed-data": "Utilizar datos xerados",
|
"use-seed-data": "Utilizar datos xerados",
|
||||||
"use-seed-data-description": "O Mealie ven cunha coleción de Alimentos, Unidades e Rótulos que poden ser usados para preencher o seu grupo con datos úteis para organizar as suas receitas.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Detalles da Conta"
|
"account-details": "Detalles da Conta"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "התראה חדשה",
|
"new-notification": "התראה חדשה",
|
||||||
"event-notifiers": "מנגנוני התרעה על אירועים",
|
"event-notifiers": "מנגנוני התרעה על אירועים",
|
||||||
"apprise-url-skipped-if-blank": "כתובת Apprise (דלג אם ריק)",
|
"apprise-url-skipped-if-blank": "כתובת Apprise (דלג אם ריק)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "הפעלת מתריע",
|
"enable-notifier": "הפעלת מתריע",
|
||||||
"what-events": "לאילו אירועים לרשום את מתריע זה?",
|
"what-events": "לאילו אירועים לרשום את מתריע זה?",
|
||||||
"user-events": "אירועי משתמש",
|
"user-events": "אירועי משתמש",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "פרטי הקבוצה",
|
"group-details": "פרטי הקבוצה",
|
||||||
"group-details-description": "לפני יצירת חשבון יש צורך ליצור קבוצה. הקבוצה תכיל רק אותך אבל תוכל להזמין אחרים בשלב מאוחר יותר. חברים בקבוצה יכולים לשתף תוכנית ארוחות, רשימות קניות, מתכונים ועוד!",
|
"group-details-description": "לפני יצירת חשבון יש צורך ליצור קבוצה. הקבוצה תכיל רק אותך אבל תוכל להזמין אחרים בשלב מאוחר יותר. חברים בקבוצה יכולים לשתף תוכנית ארוחות, רשימות קניות, מתכונים ועוד!",
|
||||||
"use-seed-data": "השתמש בנתוני האכלוס",
|
"use-seed-data": "השתמש בנתוני האכלוס",
|
||||||
"use-seed-data-description": "Mealie מגיעה עם אוסף של מאכלים, יחידות מדידה ותוויות שניתן להשתמש לאכלוס הקבוצות עם מידע שימושי לארגון המתכונים.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "פרטי חשבון"
|
"account-details": "פרטי חשבון"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nova Obavijest",
|
"new-notification": "Nova Obavijest",
|
||||||
"event-notifiers": "Obavještavatelji Događaja",
|
"event-notifiers": "Obavještavatelji Događaja",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (preskočeno ako je prazno)",
|
"apprise-url-skipped-if-blank": "Apprise URL (preskočeno ako je prazno)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Omogući obavještavanje",
|
"enable-notifier": "Omogući obavještavanje",
|
||||||
"what-events": "Na koje događaje bi ovaj obavještavatelj trebao biti pretplaćen?",
|
"what-events": "Na koje događaje bi ovaj obavještavatelj trebao biti pretplaćen?",
|
||||||
"user-events": "Događaji Korisnika",
|
"user-events": "Događaji Korisnika",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detalji o Grupi",
|
"group-details": "Detalji o Grupi",
|
||||||
"group-details-description": "Prije nego što kreirate korisnički račun, morat ćete stvoriti grupu. Vaša grupa će sadržavati samo vas, ali kasnije ćete moći pozvati druge članove. Članovi vaše grupe mogu dijeliti planove obroka, popise za kupovinu, recepte i još mnogo toga!",
|
"group-details-description": "Prije nego što kreirate korisnički račun, morat ćete stvoriti grupu. Vaša grupa će sadržavati samo vas, ali kasnije ćete moći pozvati druge članove. Članovi vaše grupe mogu dijeliti planove obroka, popise za kupovinu, recepte i još mnogo toga!",
|
||||||
"use-seed-data": "Koristi Pridržane Podatke",
|
"use-seed-data": "Koristi Pridržane Podatke",
|
||||||
"use-seed-data-description": "Mealie dolazi s kolekcijom hrane, jedinica i oznaka koje se mogu koristiti za popunjavanje vaše grupe korisnim podacima za organiziranje vaših recepata.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Detalji Računa"
|
"account-details": "Detalji Računa"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Új értesítés",
|
"new-notification": "Új értesítés",
|
||||||
"event-notifiers": "Esemény értesítők",
|
"event-notifiers": "Esemény értesítők",
|
||||||
"apprise-url-skipped-if-blank": "Értesítendő URL (kihagy, ha üres)",
|
"apprise-url-skipped-if-blank": "Értesítendő URL (kihagy, ha üres)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Értesítés engedélyezése",
|
"enable-notifier": "Értesítés engedélyezése",
|
||||||
"what-events": "Milyen eseményekre figyeljen ez az értesítés?",
|
"what-events": "Milyen eseményekre figyeljen ez az értesítés?",
|
||||||
"user-events": "Felhasználói Események",
|
"user-events": "Felhasználói Események",
|
||||||
|
@ -674,8 +675,8 @@
|
||||||
"upload-another-image": "Másik kép feltöltése",
|
"upload-another-image": "Másik kép feltöltése",
|
||||||
"upload-images": "Képek feltöltése",
|
"upload-images": "Képek feltöltése",
|
||||||
"upload-more-images": "További képek feltöltése",
|
"upload-more-images": "További képek feltöltése",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Beállítás a recept borítóképének",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Borítókép"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Receptkereső",
|
"recipe-finder": "Receptkereső",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Csoport részletek",
|
"group-details": "Csoport részletek",
|
||||||
"group-details-description": "Mielőtt létrehozna egy fiókot, létre kell hoznia egy csoportot. A csoportban csak ön lesz, de később másokat is meghívhat. A csoport tagjai menüterveket, bevásárlólistákat, recepteket és még sok mást is megoszthatnak egymással!",
|
"group-details-description": "Mielőtt létrehozna egy fiókot, létre kell hoznia egy csoportot. A csoportban csak ön lesz, de később másokat is meghívhat. A csoport tagjai menüterveket, bevásárlólistákat, recepteket és még sok mást is megoszthatnak egymással!",
|
||||||
"use-seed-data": "Mintaadatok használata",
|
"use-seed-data": "Mintaadatok használata",
|
||||||
"use-seed-data-description": "Mealie az alapanyagok, a mennyiségi egységek és a címkék gyűjteményét tartalmazza, amelyek megoszthatók a csoporttal és hasznos adataival segítségül szolgálhat a receptek szervezéséhez.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "A fiók részletei"
|
"account-details": "A fiók részletei"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Ný tilkynning",
|
"new-notification": "Ný tilkynning",
|
||||||
"event-notifiers": "Viðburðar tilkynningar",
|
"event-notifiers": "Viðburðar tilkynningar",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (sleppt ef tómt)",
|
"apprise-url-skipped-if-blank": "Apprise URL (sleppt ef tómt)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Virkja tilkynningar",
|
"enable-notifier": "Virkja tilkynningar",
|
||||||
"what-events": "Hvaða viðburði ætti þessi tilkynnir að vera áskrifandi að?",
|
"what-events": "Hvaða viðburði ætti þessi tilkynnir að vera áskrifandi að?",
|
||||||
"user-events": "Notenda viðburðir",
|
"user-events": "Notenda viðburðir",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nuova Notifica",
|
"new-notification": "Nuova Notifica",
|
||||||
"event-notifiers": "Notifiche Evento",
|
"event-notifiers": "Notifiche Evento",
|
||||||
"apprise-url-skipped-if-blank": "Url di Apprise (ignorato se vuoto)",
|
"apprise-url-skipped-if-blank": "Url di Apprise (ignorato se vuoto)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Poiché gli URL Apprise contengono in genere informazioni sensibili, questo campo viene lasciato intenzionalmente vuoto durante la modifica. Se si desidera aggiornare l'URL, inserire qui il nuovo URL, altrimenti lasciarlo vuoto per mantenere l'URL corrente.",
|
||||||
"enable-notifier": "Abilita Notificatore",
|
"enable-notifier": "Abilita Notificatore",
|
||||||
"what-events": "Quali eventi dovrebbe sottoscrivere questo notificatore?",
|
"what-events": "Quali eventi dovrebbe sottoscrivere questo notificatore?",
|
||||||
"user-events": "Eventi Utente",
|
"user-events": "Eventi Utente",
|
||||||
|
@ -80,7 +81,7 @@
|
||||||
"category-events": "Categoria Eventi",
|
"category-events": "Categoria Eventi",
|
||||||
"when-a-new-user-joins-your-group": "Quando un nuovo utente entra nel tuo gruppo",
|
"when-a-new-user-joins-your-group": "Quando un nuovo utente entra nel tuo gruppo",
|
||||||
"recipe-events": "Eventi di ricette",
|
"recipe-events": "Eventi di ricette",
|
||||||
"label-events": "Label Events"
|
"label-events": "Eventi Etichetta"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"add": "Aggiungi",
|
"add": "Aggiungi",
|
||||||
|
@ -473,7 +474,7 @@
|
||||||
"comment": "Commento",
|
"comment": "Commento",
|
||||||
"comments": "Commenti",
|
"comments": "Commenti",
|
||||||
"delete-confirmation": "Sei sicuro di voler eliminare questa ricetta?",
|
"delete-confirmation": "Sei sicuro di voler eliminare questa ricetta?",
|
||||||
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
"admin-delete-confirmation": "Stai per eliminare una ricetta che non è tua usando i permessi di amministrazione. Sei sicuro?",
|
||||||
"delete-recipe": "Elimina Ricetta",
|
"delete-recipe": "Elimina Ricetta",
|
||||||
"description": "Descrizione",
|
"description": "Descrizione",
|
||||||
"disable-amount": "Disabilita Quantità Ingredienti",
|
"disable-amount": "Disabilita Quantità Ingredienti",
|
||||||
|
@ -581,10 +582,10 @@
|
||||||
"made-this": "L'Ho Preparato",
|
"made-this": "L'Ho Preparato",
|
||||||
"how-did-it-turn-out": "Come è venuto?",
|
"how-did-it-turn-out": "Come è venuto?",
|
||||||
"user-made-this": "{user} l'ha preparato",
|
"user-made-this": "{user} l'ha preparato",
|
||||||
"added-to-timeline": "Added to timeline",
|
"added-to-timeline": "Aggiunto alla cronologia",
|
||||||
"failed-to-add-to-timeline": "Failed to add to timeline",
|
"failed-to-add-to-timeline": "Impossibile aggiungere alla cronologia",
|
||||||
"failed-to-update-recipe": "Failed to update recipe",
|
"failed-to-update-recipe": "Impossibile aggiornare la ricetta",
|
||||||
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
"added-to-timeline-but-failed-to-add-image": "Aggiunto alla cronologia, ma non è stato possibile aggiungere l'immagine",
|
||||||
"api-extras-description": "Le opzioni extra delle ricette sono una caratteristica fondamentale dell'API Mealie. Consentono di creare json personalizzati con coppie di chiavi/valore all'interno di una ricetta a cui fare riferimento tramite applicazioni terze. È possibile utilizzare queste chiavi per inserire informazioni, per esempio per attivare automazioni oppure per inoltrare messaggi personalizzati al dispositivo desiderato.",
|
"api-extras-description": "Le opzioni extra delle ricette sono una caratteristica fondamentale dell'API Mealie. Consentono di creare json personalizzati con coppie di chiavi/valore all'interno di una ricetta a cui fare riferimento tramite applicazioni terze. È possibile utilizzare queste chiavi per inserire informazioni, per esempio per attivare automazioni oppure per inoltrare messaggi personalizzati al dispositivo desiderato.",
|
||||||
"message-key": "Chiave Messaggio",
|
"message-key": "Chiave Messaggio",
|
||||||
"parse": "Analizza",
|
"parse": "Analizza",
|
||||||
|
@ -606,10 +607,10 @@
|
||||||
"create-recipe-from-an-image": "Crea ricetta da un'immagine",
|
"create-recipe-from-an-image": "Crea ricetta da un'immagine",
|
||||||
"create-recipe-from-an-image-description": "Crea una ricetta caricando un'immagine di essa. Mealie tenterà di estrarre il testo dall'immagine usando l'IA e creare una ricetta da esso.",
|
"create-recipe-from-an-image-description": "Crea una ricetta caricando un'immagine di essa. Mealie tenterà di estrarre il testo dall'immagine usando l'IA e creare una ricetta da esso.",
|
||||||
"crop-and-rotate-the-image": "Ritaglia e ruota l'immagine in modo che solo il testo sia visibile e che sia orientato correttamente.",
|
"crop-and-rotate-the-image": "Ritaglia e ruota l'immagine in modo che solo il testo sia visibile e che sia orientato correttamente.",
|
||||||
"create-from-images": "Create from Images",
|
"create-from-images": "Crea da immagini",
|
||||||
"should-translate-description": "Traduci la ricetta nella mia lingua",
|
"should-translate-description": "Traduci la ricetta nella mia lingua",
|
||||||
"please-wait-image-procesing": "Attendere, l'immagine è in fase di elaborazione. Potrebbe volerci un po' di tempo.",
|
"please-wait-image-procesing": "Attendere, l'immagine è in fase di elaborazione. Potrebbe volerci un po' di tempo.",
|
||||||
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
|
"please-wait-images-processing": "Attendere, le immagini sono in fase di elaborazione. Potrebbe volerci un po' di tempo.",
|
||||||
"bulk-url-import": "Importazione multipla URL",
|
"bulk-url-import": "Importazione multipla URL",
|
||||||
"debug-scraper": "Debug Scraper",
|
"debug-scraper": "Debug Scraper",
|
||||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crea una ricetta fornendo il nome. Tutte le ricette devono avere nomi univoci.",
|
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crea una ricetta fornendo il nome. Tutte le ricette devono avere nomi univoci.",
|
||||||
|
@ -665,17 +666,17 @@
|
||||||
"no-unit": "Nessuna unità",
|
"no-unit": "Nessuna unità",
|
||||||
"missing-unit": "Crea unità mancante: {unit}",
|
"missing-unit": "Crea unità mancante: {unit}",
|
||||||
"missing-food": "Crea cibo mancante: {food}",
|
"missing-food": "Crea cibo mancante: {food}",
|
||||||
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
"this-unit-could-not-be-parsed-automatically": "Questa unità non può essere analizzata automaticamente",
|
||||||
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
"this-food-could-not-be-parsed-automatically": "Questo alimento non può essere analizzato automaticamente",
|
||||||
"no-food": "Nessun Alimento"
|
"no-food": "Nessun Alimento"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reimposta conteggio porzioni",
|
"reset-servings-count": "Reimposta conteggio porzioni",
|
||||||
"not-linked-ingredients": "Ingredienti Aggiuntivi",
|
"not-linked-ingredients": "Ingredienti Aggiuntivi",
|
||||||
"upload-another-image": "Upload another image",
|
"upload-another-image": "Carica un'altra immagine",
|
||||||
"upload-images": "Upload images",
|
"upload-images": "Carica immagini",
|
||||||
"upload-more-images": "Upload more images",
|
"upload-more-images": "Carica altre immagini",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Imposta come immagine di copertina della ricetta",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Immagine di copertina"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Trova ricette",
|
"recipe-finder": "Trova ricette",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Dettagli Gruppo",
|
"group-details": "Dettagli Gruppo",
|
||||||
"group-details-description": "Prima di creare un account, è necessario creare un gruppo. Il gruppo conterrà solo voi, ma potrete invitare altre persone in seguito. I membri del gruppo possono condividere piani alimentari, liste della spesa, ricette e molto altro!",
|
"group-details-description": "Prima di creare un account, è necessario creare un gruppo. Il gruppo conterrà solo voi, ma potrete invitare altre persone in seguito. I membri del gruppo possono condividere piani alimentari, liste della spesa, ricette e molto altro!",
|
||||||
"use-seed-data": "Utilizzo Dati Generati",
|
"use-seed-data": "Utilizzo Dati Generati",
|
||||||
"use-seed-data-description": "Mealie viene fornito con una raccolta di alimenti, unità ed etichette che possono essere utilizzate per popolare il tuo gruppo con dati utili per organizzare le tue ricette.",
|
"use-seed-data-description": "Mealie include una raccolta di Alimenti, Unità ed Etichette che possono essere utilizzate per arricchire il proprio gruppo con dati utili per organizzare le proprie ricette. Questi dati vengono tradotti nella lingua selezionata. Si può sempre aggiungere o modificare questi dati in seguito.",
|
||||||
"account-details": "Dettagli dell'Account"
|
"account-details": "Dettagli dell'Account"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "新着通知",
|
"new-notification": "新着通知",
|
||||||
"event-notifiers": "イベント通知",
|
"event-notifiers": "イベント通知",
|
||||||
"apprise-url-skipped-if-blank": "通知用URL (空欄の場合はスキップ)",
|
"apprise-url-skipped-if-blank": "通知用URL (空欄の場合はスキップ)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "通知を有効にする",
|
"enable-notifier": "通知を有効にする",
|
||||||
"what-events": "この通知はどのイベントを購読すべきですか?",
|
"what-events": "この通知はどのイベントを購読すべきですか?",
|
||||||
"user-events": "ユーザーイベント",
|
"user-events": "ユーザーイベント",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "グループの詳細",
|
"group-details": "グループの詳細",
|
||||||
"group-details-description": "アカウントを作成する前に、グループを作成する必要があります。グループにはあなたしか含まれませんが、後で他の人を招待できます。グループのメンバーは、食事計画、買い物リスト、レシピなどを共有できます!",
|
"group-details-description": "アカウントを作成する前に、グループを作成する必要があります。グループにはあなたしか含まれませんが、後で他の人を招待できます。グループのメンバーは、食事計画、買い物リスト、レシピなどを共有できます!",
|
||||||
"use-seed-data": "シードデータを使用",
|
"use-seed-data": "シードデータを使用",
|
||||||
"use-seed-data-description": "Mealieには、レシピを整理するために役立つデータをグループに追加するために使用できる、食品、単位、ラベルのコレクションが付属しています。",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "アカウントの詳細"
|
"account-details": "アカウントの詳細"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "새 알림",
|
"new-notification": "새 알림",
|
||||||
"event-notifiers": "이벤트 알림이",
|
"event-notifiers": "이벤트 알림이",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (비워두면 생략합니다)",
|
"apprise-url-skipped-if-blank": "Apprise URL (비워두면 생략합니다)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "알림 활성화",
|
"enable-notifier": "알림 활성화",
|
||||||
"what-events": "이 알리미는 어떤 이벤트를 구독해야 합니까?",
|
"what-events": "이 알리미는 어떤 이벤트를 구독해야 합니까?",
|
||||||
"user-events": "사용자 이벤트",
|
"user-events": "사용자 이벤트",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Naujas pranešimas",
|
"new-notification": "Naujas pranešimas",
|
||||||
"event-notifiers": "Įvykių pranešimai",
|
"event-notifiers": "Įvykių pranešimai",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (praleidžiama, jei tuščia)",
|
"apprise-url-skipped-if-blank": "Apprise URL (praleidžiama, jei tuščia)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Įjungti pranešiklį",
|
"enable-notifier": "Įjungti pranešiklį",
|
||||||
"what-events": "Kokie įvykiai turėtų būti sekami?",
|
"what-events": "Kokie įvykiai turėtų būti sekami?",
|
||||||
"user-events": "Naudotojų įvykiai",
|
"user-events": "Naudotojų įvykiai",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Grupės informacija",
|
"group-details": "Grupės informacija",
|
||||||
"group-details-description": "Prieš kurdami paskyrą turite sukurti grupę. Jūsų grupėje būsite tik jūs, tačiau vėliau galėsite pakviesti ir kitus. Jūsų grupės nariai galės dalintis maitinimo planais, pirkinių sąrašais, receptais ir kita!",
|
"group-details-description": "Prieš kurdami paskyrą turite sukurti grupę. Jūsų grupėje būsite tik jūs, tačiau vėliau galėsite pakviesti ir kitus. Jūsų grupės nariai galės dalintis maitinimo planais, pirkinių sąrašais, receptais ir kita!",
|
||||||
"use-seed-data": "Naudoti pradinius duomenis",
|
"use-seed-data": "Naudoti pradinius duomenis",
|
||||||
"use-seed-data-description": "\"Mealie\" sistemoje jau yra pradinis duomenų rinkinys su produktais, vienetais ir etiketėmis. Jį galite panaudoti savo grupės užpildymui naudinga informacija, kuri padės organizuoti jūsų receptus.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Paskyros informacija"
|
"account-details": "Paskyros informacija"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Jauns paziņojums",
|
"new-notification": "Jauns paziņojums",
|
||||||
"event-notifiers": "Notikumu paziņotāji",
|
"event-notifiers": "Notikumu paziņotāji",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (izlaists, ja tukšs)",
|
"apprise-url-skipped-if-blank": "Apprise URL (izlaists, ja tukšs)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Iespējot paziņotāju",
|
"enable-notifier": "Iespējot paziņotāju",
|
||||||
"what-events": "Kādus notikumus šim paziņotājam vajadzētu abonēt?",
|
"what-events": "Kādus notikumus šim paziņotājam vajadzētu abonēt?",
|
||||||
"user-events": "Lietotāju notikumi",
|
"user-events": "Lietotāju notikumi",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Grupas informācija",
|
"group-details": "Grupas informācija",
|
||||||
"group-details-description": "Pirms konta izveides jums būs jāizveido grupa. Jūsu grupā būs tikai jūs, bet vēlāk varēsiet uzaicināt citus. Jūsu grupas dalībnieki var dalīties maltīšu plānos, iepirkumu sarakstos, receptēs un daudz ko citu!",
|
"group-details-description": "Pirms konta izveides jums būs jāizveido grupa. Jūsu grupā būs tikai jūs, bet vēlāk varēsiet uzaicināt citus. Jūsu grupas dalībnieki var dalīties maltīšu plānos, iepirkumu sarakstos, receptēs un daudz ko citu!",
|
||||||
"use-seed-data": "Izmantojiet sēklu datus",
|
"use-seed-data": "Izmantojiet sēklu datus",
|
||||||
"use-seed-data-description": "Mealie piegādā kopā ar pārtikas produktu, vienību un etiķešu kolekciju, ko var izmantot, lai papildinātu grupu ar noderīgiem datiem recepšu sakārtošanai.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Konta informācija"
|
"account-details": "Konta informācija"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nieuwe melding",
|
"new-notification": "Nieuwe melding",
|
||||||
"event-notifiers": "Meldingen van gebeurtenissen",
|
"event-notifiers": "Meldingen van gebeurtenissen",
|
||||||
"apprise-url-skipped-if-blank": "URL van Apprise (overgeslagen als veld leeg is)",
|
"apprise-url-skipped-if-blank": "URL van Apprise (overgeslagen als veld leeg is)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Activeer melding",
|
"enable-notifier": "Activeer melding",
|
||||||
"what-events": "Op welke gebeurtenissen moet deze melding zich abonneren?",
|
"what-events": "Op welke gebeurtenissen moet deze melding zich abonneren?",
|
||||||
"user-events": "Gebeurtenissen van gebruiker",
|
"user-events": "Gebeurtenissen van gebruiker",
|
||||||
|
@ -674,8 +675,8 @@
|
||||||
"upload-another-image": "Een andere afbeelding uploaden",
|
"upload-another-image": "Een andere afbeelding uploaden",
|
||||||
"upload-images": "Afbeelding uploaden",
|
"upload-images": "Afbeelding uploaden",
|
||||||
"upload-more-images": "Meer afbeeldingen uploaden",
|
"upload-more-images": "Meer afbeeldingen uploaden",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Als recept omslagfoto instellen",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Omslagfoto"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Recept zoeker",
|
"recipe-finder": "Recept zoeker",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Groepsdetails",
|
"group-details": "Groepsdetails",
|
||||||
"group-details-description": "Voordat je een account aanmaakt moet je eerst een groep aanmaken. Jij bent het enige lid van de groep, maar je kunt later anderen uitnodigen. Leden van je groep kunnen maaltijdplannen, boodschappenlijstjes, recepten en nog veel meer delen!",
|
"group-details-description": "Voordat je een account aanmaakt moet je eerst een groep aanmaken. Jij bent het enige lid van de groep, maar je kunt later anderen uitnodigen. Leden van je groep kunnen maaltijdplannen, boodschappenlijstjes, recepten en nog veel meer delen!",
|
||||||
"use-seed-data": "Gebruik voorbeeldgegevens",
|
"use-seed-data": "Gebruik voorbeeldgegevens",
|
||||||
"use-seed-data-description": "Mealie bevat een verzameling ingrediënten, eenheden en labels die gebruikt kunnen worden om je groep te vullen met handige gegevens voor het organiseren van je recepten.",
|
"use-seed-data-description": "Mealie komt standaard met lijsten voor Voedsel, Eenheden en Labels. Die gebruik je om je recepten handig in te delen. Of om je groep handige informatie te geven. Ze zijn vertaald in de taal die je voor Mealie hebt ingesteld. Je kunt deze lijsten altijd aanvullen of aanpassen.",
|
||||||
"account-details": "Accountgegevens"
|
"account-details": "Accountgegevens"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nytt varsel",
|
"new-notification": "Nytt varsel",
|
||||||
"event-notifiers": "Hendelsesvarsler",
|
"event-notifiers": "Hendelsesvarsler",
|
||||||
"apprise-url-skipped-if-blank": "Apprise-URL (hoppes over hvis tom)",
|
"apprise-url-skipped-if-blank": "Apprise-URL (hoppes over hvis tom)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Aktiver varslingsagenten",
|
"enable-notifier": "Aktiver varslingsagenten",
|
||||||
"what-events": "Hvilke hendelser skal denne varslingsagenten abonnere på?",
|
"what-events": "Hvilke hendelser skal denne varslingsagenten abonnere på?",
|
||||||
"user-events": "Brukerhendelser",
|
"user-events": "Brukerhendelser",
|
||||||
|
@ -473,7 +474,7 @@
|
||||||
"comment": "Kommentar",
|
"comment": "Kommentar",
|
||||||
"comments": "Kommentarer",
|
"comments": "Kommentarer",
|
||||||
"delete-confirmation": "Er du sikker på at du vil slette denne oppskriften?",
|
"delete-confirmation": "Er du sikker på at du vil slette denne oppskriften?",
|
||||||
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
"admin-delete-confirmation": "Du er i ferd med å slette en oppskrift som ikke er din ved å bruke administratortillatelser. Er du sikker?",
|
||||||
"delete-recipe": "Slett oppskrift",
|
"delete-recipe": "Slett oppskrift",
|
||||||
"description": "Beskrivelse",
|
"description": "Beskrivelse",
|
||||||
"disable-amount": "Deaktiver ingrediensmengde",
|
"disable-amount": "Deaktiver ingrediensmengde",
|
||||||
|
@ -582,7 +583,7 @@
|
||||||
"how-did-it-turn-out": "Hvordan ble det?",
|
"how-did-it-turn-out": "Hvordan ble det?",
|
||||||
"user-made-this": "{user} har laget dette",
|
"user-made-this": "{user} har laget dette",
|
||||||
"added-to-timeline": "Legg til tidslinje",
|
"added-to-timeline": "Legg til tidslinje",
|
||||||
"failed-to-add-to-timeline": "Failed to add to timeline",
|
"failed-to-add-to-timeline": "Kunne ikke legge til på tidslinjen",
|
||||||
"failed-to-update-recipe": "Kunne ikke oppdatere oppskriften",
|
"failed-to-update-recipe": "Kunne ikke oppdatere oppskriften",
|
||||||
"added-to-timeline-but-failed-to-add-image": "Lagt til i tidslinjen, men klarte ikke å legge til bilde",
|
"added-to-timeline-but-failed-to-add-image": "Lagt til i tidslinjen, men klarte ikke å legge til bilde",
|
||||||
"api-extras-description": "Ekstramaterialer til oppskrifter er en viktig funksjon i Mealie API-en. De lar deg opprette egendefinerte JSON-nøkkel/verdi-par innenfor en oppskrift for å referere fra tredjepartsapplikasjoner. Du kan bruke disse nøklene til å gi informasjon for eksempel for å utløse automatiseringer eller egendefinerte meldinger som skal videreformidles til ønsket enhet.",
|
"api-extras-description": "Ekstramaterialer til oppskrifter er en viktig funksjon i Mealie API-en. De lar deg opprette egendefinerte JSON-nøkkel/verdi-par innenfor en oppskrift for å referere fra tredjepartsapplikasjoner. Du kan bruke disse nøklene til å gi informasjon for eksempel for å utløse automatiseringer eller egendefinerte meldinger som skal videreformidles til ønsket enhet.",
|
||||||
|
@ -674,8 +675,8 @@
|
||||||
"upload-another-image": "Last opp nytt bilde",
|
"upload-another-image": "Last opp nytt bilde",
|
||||||
"upload-images": "Last opp bilder",
|
"upload-images": "Last opp bilder",
|
||||||
"upload-more-images": "Last opp flere bilder",
|
"upload-more-images": "Last opp flere bilder",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Bruk som forsidebilde for oppskriften",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Forsidebilde"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Oppskriftsfinner",
|
"recipe-finder": "Oppskriftsfinner",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Gruppedetaljer",
|
"group-details": "Gruppedetaljer",
|
||||||
"group-details-description": "Før du oppretter en konto må du opprette en gruppe. Gruppen din vil bare inneholde deg, men du vil kunne invitere andre senere. Medlemmer i gruppen din kan dele måltider, handlelister, oppskrifter med mer!",
|
"group-details-description": "Før du oppretter en konto må du opprette en gruppe. Gruppen din vil bare inneholde deg, men du vil kunne invitere andre senere. Medlemmer i gruppen din kan dele måltider, handlelister, oppskrifter med mer!",
|
||||||
"use-seed-data": "Bruk tilføringsdata",
|
"use-seed-data": "Bruk tilføringsdata",
|
||||||
"use-seed-data-description": "Mealie kommer med en samling av matvarer, enheter og etiketter som kan brukes til å fylle gruppen din med nyttige data for å organisere oppskriftene dine.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Kontodetaljer"
|
"account-details": "Kontodetaljer"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nowe powiadomienie",
|
"new-notification": "Nowe powiadomienie",
|
||||||
"event-notifiers": "Powiadomienia o zdarzeniach",
|
"event-notifiers": "Powiadomienia o zdarzeniach",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (pominięty, jeśli puste)",
|
"apprise-url-skipped-if-blank": "URL Apprise (pominięty, jeśli puste)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Ponieważ adresy URL Apprise zawierają zazwyczaj poufne informacje, pole to pozostaje celowo puste podczas edycji. Jeśli chcesz zaktualizować adres URL, wprowadź ten nowy tutaj, w przeciwnym razie pozostaw puste, aby zachować bieżący adres URL.",
|
||||||
"enable-notifier": "Włącz Powiadomienie",
|
"enable-notifier": "Włącz Powiadomienie",
|
||||||
"what-events": "Jakie zdarzenia powinien subskrybować ten powiadamiający?",
|
"what-events": "Jakie zdarzenia powinien subskrybować ten powiadamiający?",
|
||||||
"user-events": "Zdarzenia użytkownika",
|
"user-events": "Zdarzenia użytkownika",
|
||||||
|
@ -80,7 +81,7 @@
|
||||||
"category-events": "Wydarzenia kategorii",
|
"category-events": "Wydarzenia kategorii",
|
||||||
"when-a-new-user-joins-your-group": "Kiedy nowy użytkownik dołączy do Twojej grupy",
|
"when-a-new-user-joins-your-group": "Kiedy nowy użytkownik dołączy do Twojej grupy",
|
||||||
"recipe-events": "Zdarzenia Przepisów",
|
"recipe-events": "Zdarzenia Przepisów",
|
||||||
"label-events": "Label Events"
|
"label-events": "Etykieta wydarzeń"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"add": "Dodaj",
|
"add": "Dodaj",
|
||||||
|
@ -674,8 +675,8 @@
|
||||||
"upload-another-image": "Prześlij kolejny obraz",
|
"upload-another-image": "Prześlij kolejny obraz",
|
||||||
"upload-images": "Prześlij obraz",
|
"upload-images": "Prześlij obraz",
|
||||||
"upload-more-images": "Prześlij więcej obrazów",
|
"upload-more-images": "Prześlij więcej obrazów",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Ustaw jako okładkę przepisu",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Okładka"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Wyszukiwarka przepisów",
|
"recipe-finder": "Wyszukiwarka przepisów",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Szczegóły grupy",
|
"group-details": "Szczegóły grupy",
|
||||||
"group-details-description": "Zanim utworzysz konto musisz stworzyć grupę. Twoja grupa zawierać będzie tylko Ciebie, ale będziesz istniała możlwiość zaproszenia do niej innych. Użytkownicy Twojej grupy mogą współdzielić plany posiłków, listy zakupów, przepisy i więcej!",
|
"group-details-description": "Zanim utworzysz konto musisz stworzyć grupę. Twoja grupa zawierać będzie tylko Ciebie, ale będziesz istniała możlwiość zaproszenia do niej innych. Użytkownicy Twojej grupy mogą współdzielić plany posiłków, listy zakupów, przepisy i więcej!",
|
||||||
"use-seed-data": "Użyj przykładowych danych",
|
"use-seed-data": "Użyj przykładowych danych",
|
||||||
"use-seed-data-description": "Mealie dostarcza zestaw posiłków, jednostek i opisów które mogą zostać użyte do zapełnienia Twojej grupy przydatnymi danymi do ogranizacji Twoich przepisów.",
|
"use-seed-data-description": "Wysyłka posiłków z kolekcją żywności, jednostek i etykiet, które mogą być użyte do wypełnienia Twojej grupy pomocnymi danymi do organizacji twoich przepisów. Są one tłumaczone na wybrany język. Zawsze możesz dodać lub zmodyfikować te dane później.",
|
||||||
"account-details": "Szczegóły konta"
|
"account-details": "Szczegóły konta"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nova Notificação",
|
"new-notification": "Nova Notificação",
|
||||||
"event-notifiers": "Notificações de Eventos",
|
"event-notifiers": "Notificações de Eventos",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignorado se estiver em branco)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignorado se estiver em branco)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Habilitar Notificador",
|
"enable-notifier": "Habilitar Notificador",
|
||||||
"what-events": "A quais eventos este notificador deve subscrever?",
|
"what-events": "A quais eventos este notificador deve subscrever?",
|
||||||
"user-events": "Eventos do usuário",
|
"user-events": "Eventos do usuário",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detalhes do Grupo",
|
"group-details": "Detalhes do Grupo",
|
||||||
"group-details-description": "Antes de criar uma conta é necessário criar um grupo. O seu grupo só conterá você, mas você poderá convidar os outros mais tarde. Os membros do seu grupo podem compartilhar planos de refeição, listas de compras, receitas e muito mais!",
|
"group-details-description": "Antes de criar uma conta é necessário criar um grupo. O seu grupo só conterá você, mas você poderá convidar os outros mais tarde. Os membros do seu grupo podem compartilhar planos de refeição, listas de compras, receitas e muito mais!",
|
||||||
"use-seed-data": "Usar dados semeados",
|
"use-seed-data": "Usar dados semeados",
|
||||||
"use-seed-data-description": "O Mealie é fornecido com uma coleção de alimentos, unidades e rótulos que podem ser usados para preencher seu grupo com dados úteis para organizar suas receitas.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Detalhes da Conta"
|
"account-details": "Detalhes da Conta"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nova Notificação",
|
"new-notification": "Nova Notificação",
|
||||||
"event-notifiers": "Notificadores de eventos",
|
"event-notifiers": "Notificadores de eventos",
|
||||||
"apprise-url-skipped-if-blank": "URL da Apprise (ignorado se vazio)",
|
"apprise-url-skipped-if-blank": "URL da Apprise (ignorado se vazio)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Ativar Notificador",
|
"enable-notifier": "Ativar Notificador",
|
||||||
"what-events": "Que eventos este notificador deve subscrever?",
|
"what-events": "Que eventos este notificador deve subscrever?",
|
||||||
"user-events": "Eventos do utilizador",
|
"user-events": "Eventos do utilizador",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detalhes do Grupo",
|
"group-details": "Detalhes do Grupo",
|
||||||
"group-details-description": "Antes de criar uma conta é necessário criar um grupo. Será o único membro do seu grupo, mas poderá convidar outros mais tarde. Os membros do seu grupo podem partilhar planos de refeição, listas de compras, receitas e muito mais!",
|
"group-details-description": "Antes de criar uma conta é necessário criar um grupo. Será o único membro do seu grupo, mas poderá convidar outros mais tarde. Os membros do seu grupo podem partilhar planos de refeição, listas de compras, receitas e muito mais!",
|
||||||
"use-seed-data": "Utilizar dados gerados",
|
"use-seed-data": "Utilizar dados gerados",
|
||||||
"use-seed-data-description": "O Mealie vem com uma coleção de Alimentos, Unidades e Rótulos que podem ser usados para popular o seu grupo com dados úteis para organizar as suas receitas.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Detalhes da Conta"
|
"account-details": "Detalhes da Conta"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Notificare nouă",
|
"new-notification": "Notificare nouă",
|
||||||
"event-notifiers": "Notificatori de evenimente",
|
"event-notifiers": "Notificatori de evenimente",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignorat daca e gol)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignorat daca e gol)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Activare notificator",
|
"enable-notifier": "Activare notificator",
|
||||||
"what-events": "La ce evenimente ar trebui să se înscrie acest notificator?",
|
"what-events": "La ce evenimente ar trebui să se înscrie acest notificator?",
|
||||||
"user-events": "Evenimente Utilizator",
|
"user-events": "Evenimente Utilizator",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detalii grup",
|
"group-details": "Detalii grup",
|
||||||
"group-details-description": "Înainte de a crea un cont, va trebui să creezi un grup. Grupul tău va conține inițial doar pe tine, dar vei putea invita și alte persoane ulterior. Membrii din grupul tău vor putea să partajeze planuri de mese, liste de cumpărături, rețete și multe altele!",
|
"group-details-description": "Înainte de a crea un cont, va trebui să creezi un grup. Grupul tău va conține inițial doar pe tine, dar vei putea invita și alte persoane ulterior. Membrii din grupul tău vor putea să partajeze planuri de mese, liste de cumpărături, rețete și multe altele!",
|
||||||
"use-seed-data": "Utilizează setul de date a populării",
|
"use-seed-data": "Utilizează setul de date a populării",
|
||||||
"use-seed-data-description": "Mealie vine cu o colecție de Alimente, Unități, și Etichete care pot fi utilizate pentru a popula grupul tău cu date utile pentru organizarea rețetelor.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Detalii Cont"
|
"account-details": "Detalii Cont"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Новое уведомление",
|
"new-notification": "Новое уведомление",
|
||||||
"event-notifiers": "Уведомления о событии",
|
"event-notifiers": "Уведомления о событии",
|
||||||
"apprise-url-skipped-if-blank": "URL-адрес (пропущен, если пусто)",
|
"apprise-url-skipped-if-blank": "URL-адрес (пропущен, если пусто)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Включить уведомления",
|
"enable-notifier": "Включить уведомления",
|
||||||
"what-events": "На какие события следует настроить уведомления?",
|
"what-events": "На какие события следует настроить уведомления?",
|
||||||
"user-events": "События пользователя",
|
"user-events": "События пользователя",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Сведения о группе",
|
"group-details": "Сведения о группе",
|
||||||
"group-details-description": "Прежде чем создать учетную запись, вам нужно создать группу. В вашей группе будете только вы, но вы сможете пригласить других позже. Участники группы могут обмениваться планами питания, списками покупок, рецептами и многим другим!",
|
"group-details-description": "Прежде чем создать учетную запись, вам нужно создать группу. В вашей группе будете только вы, но вы сможете пригласить других позже. Участники группы могут обмениваться планами питания, списками покупок, рецептами и многим другим!",
|
||||||
"use-seed-data": "Использовать дефолтные значения",
|
"use-seed-data": "Использовать дефолтные значения",
|
||||||
"use-seed-data-description": "Mealie идёт с коллекцией продуктов, единиц измерения и меток, которые могут быть использованы для заполнения вашей группы полезными данными для организации ваших рецептов.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Параметры учетной записи"
|
"account-details": "Параметры учетной записи"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Nové upozornenie",
|
"new-notification": "Nové upozornenie",
|
||||||
"event-notifiers": "Upozornenia udalostí",
|
"event-notifiers": "Upozornenia udalostí",
|
||||||
"apprise-url-skipped-if-blank": "Informačná URL (preskočená, ak je prázdna)",
|
"apprise-url-skipped-if-blank": "Informačná URL (preskočená, ak je prázdna)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Zapnúť notifikátor",
|
"enable-notifier": "Zapnúť notifikátor",
|
||||||
"what-events": "Pre ktoré udalosti si želáte zapnúť notifikátor?",
|
"what-events": "Pre ktoré udalosti si želáte zapnúť notifikátor?",
|
||||||
"user-events": "Udalosti používateľa",
|
"user-events": "Udalosti používateľa",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Podrobnosti o skupine",
|
"group-details": "Podrobnosti o skupine",
|
||||||
"group-details-description": "Pred vytvorením účtu musíte vytvoriť skupinu. Vaša skupina bude obsahovať iba vás, ale neskôr budete môcť pozvať ostatných. Členovia vašej skupiny môžu zdieľať stravovacie plány, nákupné zoznamy, recepty a ďalšie!",
|
"group-details-description": "Pred vytvorením účtu musíte vytvoriť skupinu. Vaša skupina bude obsahovať iba vás, ale neskôr budete môcť pozvať ostatných. Členovia vašej skupiny môžu zdieľať stravovacie plány, nákupné zoznamy, recepty a ďalšie!",
|
||||||
"use-seed-data": "Použiť predvolené dáta",
|
"use-seed-data": "Použiť predvolené dáta",
|
||||||
"use-seed-data-description": "Mealie prichádza so zbierkou potravín, jednotiek a štítkov, ktoré možno použiť na naplnenie vašej skupiny užitočnými údajmi na organizáciu vašich receptov.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Detaily účtu"
|
"account-details": "Detaily účtu"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Novo obvestilo",
|
"new-notification": "Novo obvestilo",
|
||||||
"event-notifiers": "Obvestila o dogodkih",
|
"event-notifiers": "Obvestila o dogodkih",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (preskočeno, če je prazno)",
|
"apprise-url-skipped-if-blank": "Apprise URL (preskočeno, če je prazno)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Vključi obvestila",
|
"enable-notifier": "Vključi obvestila",
|
||||||
"what-events": "Katere dogodke naj spremlja obveščevalni sistem?",
|
"what-events": "Katere dogodke naj spremlja obveščevalni sistem?",
|
||||||
"user-events": "Dogodki uporabnika",
|
"user-events": "Dogodki uporabnika",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Detajli skupine",
|
"group-details": "Detajli skupine",
|
||||||
"group-details-description": "Preden kreirate račun, morate kreirati skupino. V skupini boste sprva samo vi, vendar imate možnost povabiti še ostale člane. Člani v vaši skupini lahko delijo načrte obrokov, nakupovalne sezname, recepte in še več!",
|
"group-details-description": "Preden kreirate račun, morate kreirati skupino. V skupini boste sprva samo vi, vendar imate možnost povabiti še ostale člane. Člani v vaši skupini lahko delijo načrte obrokov, nakupovalne sezname, recepte in še več!",
|
||||||
"use-seed-data": "Uporabi privzete podatke",
|
"use-seed-data": "Uporabi privzete podatke",
|
||||||
"use-seed-data-description": "Meali vključuje zbirko jedi, enot in oznak, ki se lahko uporabno uporabijo v vaši skupini za organizacijo receptov.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Podatki o računu"
|
"account-details": "Podatki o računu"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Ново обавештење",
|
"new-notification": "Ново обавештење",
|
||||||
"event-notifiers": "Обавештавач о догађају",
|
"event-notifiers": "Обавештавач о догађају",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (прескочено ако је празно)",
|
"apprise-url-skipped-if-blank": "Apprise URL (прескочено ако је празно)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Омогући обавештење",
|
"enable-notifier": "Омогући обавештење",
|
||||||
"what-events": "На које догађаје би требао да се претплати овај обавештавач?",
|
"what-events": "На које догађаје би требао да се претплати овај обавештавач?",
|
||||||
"user-events": "Догађаји корисника",
|
"user-events": "Догађаји корисника",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Пре него што креирате налог, морате креирати групу. Ваша група ће садржавати само вас, али касније ћете моћи позвати друге. Чланови ваше групе могу делити јеловнике, спискове за куповину, рецепте и још много тога!",
|
"group-details-description": "Пре него што креирате налог, морате креирати групу. Ваша група ће садржавати само вас, али касније ћете моћи позвати друге. Чланови ваше групе могу делити јеловнике, спискове за куповину, рецепте и још много тога!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Ny avisering",
|
"new-notification": "Ny avisering",
|
||||||
"event-notifiers": "Händelseavisering",
|
"event-notifiers": "Händelseavisering",
|
||||||
"apprise-url-skipped-if-blank": "Apprise-URL (hoppa över om tom)",
|
"apprise-url-skipped-if-blank": "Apprise-URL (hoppa över om tom)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Aktivera avisering",
|
"enable-notifier": "Aktivera avisering",
|
||||||
"what-events": "Vilka händelser ska denna avisering prenumerera på?",
|
"what-events": "Vilka händelser ska denna avisering prenumerera på?",
|
||||||
"user-events": "Användarhändelser",
|
"user-events": "Användarhändelser",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Gruppuppgifter",
|
"group-details": "Gruppuppgifter",
|
||||||
"group-details-description": "Innan du skapar ett konto måste du skapa en grupp. Din grupp kommer bara att innehålla dig, men du kommer att kunna bjuda in andra senare. Medlemmarna i din grupp kan dela måltidsplaner, inköpslistor, recept och mycket mer!",
|
"group-details-description": "Innan du skapar ett konto måste du skapa en grupp. Din grupp kommer bara att innehålla dig, men du kommer att kunna bjuda in andra senare. Medlemmarna i din grupp kan dela måltidsplaner, inköpslistor, recept och mycket mer!",
|
||||||
"use-seed-data": "Använd exempeldata",
|
"use-seed-data": "Använd exempeldata",
|
||||||
"use-seed-data-description": "Mealie innehåller en samling av livsmedel, enheter och etiketter som kan användas för att fylla din grupp med användbara data för att organisera dina recept.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Kontouppgifter"
|
"account-details": "Kontouppgifter"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Yeni bildirim",
|
"new-notification": "Yeni bildirim",
|
||||||
"event-notifiers": "Etkinlik Bildirimleri",
|
"event-notifiers": "Etkinlik Bildirimleri",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL'si (boşsa geçilir)",
|
"apprise-url-skipped-if-blank": "Apprise URL'si (boşsa geçilir)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Bildiriciyi Etkinleştir",
|
"enable-notifier": "Bildiriciyi Etkinleştir",
|
||||||
"what-events": "Bu bildirimci hangi olaylara abone olmalıdır?",
|
"what-events": "Bu bildirimci hangi olaylara abone olmalıdır?",
|
||||||
"user-events": "Kullanıcı Etkinlikleri",
|
"user-events": "Kullanıcı Etkinlikleri",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Grup Detayları",
|
"group-details": "Grup Detayları",
|
||||||
"group-details-description": "Hesap oluşturmadan önce bir grup oluşturmanız gerekir. Grubunuzda yalnızca siz yer alacaksınız ancak daha sonra başkalarını da davet edebileceksiniz. Grubunuzdaki üyeler yemek planlarını, alışveriş listelerini, tarifleri ve daha fazlasını paylaşabilir!",
|
"group-details-description": "Hesap oluşturmadan önce bir grup oluşturmanız gerekir. Grubunuzda yalnızca siz yer alacaksınız ancak daha sonra başkalarını da davet edebileceksiniz. Grubunuzdaki üyeler yemek planlarını, alışveriş listelerini, tarifleri ve daha fazlasını paylaşabilir!",
|
||||||
"use-seed-data": "Tohum Verisi Kullan",
|
"use-seed-data": "Tohum Verisi Kullan",
|
||||||
"use-seed-data-description": "Mealie, grubunuzu tariflerinizi düzenlemenize yardımcı olacak yararlı verilerle doldurmak için kullanılabilecek bir Yiyecek, Birim ve Etiket koleksiyonuyla birlikte gelir.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Hesap Detayları"
|
"account-details": "Hesap Detayları"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Нове сповіщення",
|
"new-notification": "Нове сповіщення",
|
||||||
"event-notifiers": "Сповіщувачі",
|
"event-notifiers": "Сповіщувачі",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (пропущено якщо порожній)",
|
"apprise-url-skipped-if-blank": "Apprise URL (пропущено якщо порожній)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Увімкнути сповіщувач",
|
"enable-notifier": "Увімкнути сповіщувач",
|
||||||
"what-events": "На які події цей сповіщувач має бути підписаний?",
|
"what-events": "На які події цей сповіщувач має бути підписаний?",
|
||||||
"user-events": "Події користувача",
|
"user-events": "Події користувача",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Деталі групи",
|
"group-details": "Деталі групи",
|
||||||
"group-details-description": "Перед створенням облікового запису вам потрібно створити групу. Спочатку ваша група буде містити тільки вас, але ви зможете запрошувати інших пізніше. Учасники вашої групи можуть обмінюватися планами харчування, списками покупок, рецептами і багато чим іншим!",
|
"group-details-description": "Перед створенням облікового запису вам потрібно створити групу. Спочатку ваша група буде містити тільки вас, але ви зможете запрошувати інших пізніше. Учасники вашої групи можуть обмінюватися планами харчування, списками покупок, рецептами і багато чим іншим!",
|
||||||
"use-seed-data": "Використати початкові дані",
|
"use-seed-data": "Використати початкові дані",
|
||||||
"use-seed-data-description": "Mealie має вбудований набір продуктів, одиниць виміру, та етикеток що можуть бути додані до вашої групи для допомоги в організації рецептів.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Деталі акаунта"
|
"account-details": "Деталі акаунта"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "Thông báo mới",
|
"new-notification": "Thông báo mới",
|
||||||
"event-notifiers": "Event Notifiers",
|
"event-notifiers": "Event Notifiers",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (skipped if blank)",
|
"apprise-url-skipped-if-blank": "Apprise URL (skipped if blank)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "Enable Notifier",
|
"enable-notifier": "Enable Notifier",
|
||||||
"what-events": "What events should this notifier subscribe to?",
|
"what-events": "What events should this notifier subscribe to?",
|
||||||
"user-events": "User Events",
|
"user-events": "User Events",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "新通知",
|
"new-notification": "新通知",
|
||||||
"event-notifiers": "事件通知器",
|
"event-notifiers": "事件通知器",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (如果为空则跳过)",
|
"apprise-url-skipped-if-blank": "Apprise URL (如果为空则跳过)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "打开消息通知",
|
"enable-notifier": "打开消息通知",
|
||||||
"what-events": "该通知器需要订阅哪些事件?",
|
"what-events": "该通知器需要订阅哪些事件?",
|
||||||
"user-events": "用户事件",
|
"user-events": "用户事件",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "群组详情",
|
"group-details": "群组详情",
|
||||||
"group-details-description": "在你创建账户之前,需要先创建一个群组。此时群组将只包含你自己,但稍后你便可邀请其他人。 你的群组成员可以分享食谱、饮食计划、购物清单等!",
|
"group-details-description": "在你创建账户之前,需要先创建一个群组。此时群组将只包含你自己,但稍后你便可邀请其他人。 你的群组成员可以分享食谱、饮食计划、购物清单等!",
|
||||||
"use-seed-data": "使用初始数据",
|
"use-seed-data": "使用初始数据",
|
||||||
"use-seed-data-description": "Mealie附带一套现成的“食品”、“单位”、“标签”数据,可以帮助你的群组管理食谱。",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "账户详情"
|
"account-details": "账户详情"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"new-notification": "新通知",
|
"new-notification": "新通知",
|
||||||
"event-notifiers": "事件通知",
|
"event-notifiers": "事件通知",
|
||||||
"apprise-url-skipped-if-blank": "Apprise 網址(空白則略過)",
|
"apprise-url-skipped-if-blank": "Apprise 網址(空白則略過)",
|
||||||
|
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
||||||
"enable-notifier": "啟用通知功能",
|
"enable-notifier": "啟用通知功能",
|
||||||
"what-events": "要訂閱哪些事件通知?",
|
"what-events": "要訂閱哪些事件通知?",
|
||||||
"user-events": "用戶相關事件",
|
"user-events": "用戶相關事件",
|
||||||
|
@ -1168,7 +1169,7 @@
|
||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||||
import { RecipeCookBook } from "~/lib/api/types/cookbook";
|
import { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
|
@ -10,7 +10,7 @@ const routes = {
|
||||||
cookbooksGroupSlugCookbookId: (groupSlug: string | number, cookbookId: string | number) => `${exploreGroupSlug(groupSlug)}/cookbooks/${cookbookId}`,
|
cookbooksGroupSlugCookbookId: (groupSlug: string | number, cookbookId: string | number) => `${exploreGroupSlug(groupSlug)}/cookbooks/${cookbookId}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PublicCookbooksApi extends BaseCRUDAPIReadOnly<RecipeCookBook> {
|
export class PublicCookbooksApi extends BaseCRUDAPIReadOnly<ReadCookBook> {
|
||||||
constructor(requests: ApiRequestInstance, groupSlug: string) {
|
constructor(requests: ApiRequestInstance, groupSlug: string) {
|
||||||
super(
|
super(
|
||||||
requests,
|
requests,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
@ -38,67 +39,6 @@ export interface QueryFilterJSONPart {
|
||||||
attributeName?: string | null;
|
attributeName?: string | null;
|
||||||
relationalOperator?: RelationalKeyword | RelationalOperator | null;
|
relationalOperator?: RelationalKeyword | RelationalOperator | null;
|
||||||
value?: string | string[] | null;
|
value?: string | string[] | null;
|
||||||
}
|
|
||||||
export interface RecipeCookBook {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
slug?: string | null;
|
|
||||||
position?: number;
|
|
||||||
public?: boolean;
|
|
||||||
queryFilterString?: string;
|
|
||||||
groupId: string;
|
|
||||||
householdId: string;
|
|
||||||
id: string;
|
|
||||||
queryFilter?: QueryFilterJSON;
|
|
||||||
recipes: RecipeSummary[];
|
|
||||||
}
|
|
||||||
export interface RecipeSummary {
|
|
||||||
id?: string | null;
|
|
||||||
userId?: string;
|
|
||||||
householdId?: string;
|
|
||||||
groupId?: string;
|
|
||||||
name?: string | null;
|
|
||||||
slug?: string;
|
|
||||||
image?: unknown;
|
|
||||||
recipeServings?: number;
|
|
||||||
recipeYieldQuantity?: number;
|
|
||||||
recipeYield?: string | null;
|
|
||||||
totalTime?: string | null;
|
|
||||||
prepTime?: string | null;
|
|
||||||
cookTime?: string | null;
|
|
||||||
performTime?: string | null;
|
|
||||||
description?: string | null;
|
|
||||||
recipeCategory?: RecipeCategory[] | null;
|
|
||||||
tags?: RecipeTag[] | null;
|
|
||||||
tools?: RecipeTool[];
|
|
||||||
rating?: number | null;
|
|
||||||
orgURL?: string | null;
|
|
||||||
dateAdded?: string | null;
|
|
||||||
dateUpdated?: string | null;
|
|
||||||
createdAt?: string | null;
|
|
||||||
updatedAt?: string | null;
|
|
||||||
lastMade?: string | null;
|
|
||||||
}
|
|
||||||
export interface RecipeCategory {
|
|
||||||
id?: string | null;
|
|
||||||
groupId?: string | null;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
|
||||||
export interface RecipeTag {
|
|
||||||
id?: string | null;
|
|
||||||
groupId?: string | null;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
|
||||||
export interface RecipeTool {
|
|
||||||
id: string;
|
|
||||||
groupId?: string | null;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
householdsWithTool?: string[];
|
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
export interface SaveCookBook {
|
export interface SaveCookBook {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BaseCRUDAPI } from "../base/base-clients";
|
import { BaseCRUDAPI } from "../base/base-clients";
|
||||||
import type { CreateCookBook, RecipeCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
|
import type { CreateCookBook, ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ const routes = {
|
||||||
cookbooksId: (id: number) => `${prefix}/households/cookbooks/${id}`,
|
cookbooksId: (id: number) => `${prefix}/households/cookbooks/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CookbookAPI extends BaseCRUDAPI<CreateCookBook, RecipeCookBook, UpdateCookBook> {
|
export class CookbookAPI extends BaseCRUDAPI<CreateCookBook, ReadCookBook, UpdateCookBook> {
|
||||||
baseRoute: string = routes.cookbooks;
|
baseRoute: string = routes.cookbooks;
|
||||||
itemRoute = routes.cookbooksId;
|
itemRoute = routes.cookbooksId;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container fill-height
|
<v-container
|
||||||
|
fill-height
|
||||||
fluid
|
fluid
|
||||||
class="d-flex justify-center align-center"
|
class="d-flex justify-center align-center"
|
||||||
width="1200px"
|
width="1200px"
|
||||||
|
@ -8,7 +9,8 @@
|
||||||
'bg-off-white': !$vuetify.theme.current.dark,
|
'bg-off-white': !$vuetify.theme.current.dark,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<BaseWizard v-model="currentPage"
|
<BaseWizard
|
||||||
|
v-model="currentPage"
|
||||||
:max-page-number="totalPages"
|
:max-page-number="totalPages"
|
||||||
:title="$t('admin.setup.first-time-setup')"
|
:title="$t('admin.setup.first-time-setup')"
|
||||||
:prev-button-show="activeConfig.showPrevButton"
|
:prev-button-show="activeConfig.showPrevButton"
|
||||||
|
@ -20,13 +22,15 @@
|
||||||
:is-submitting="isSubmitting"
|
:is-submitting="isSubmitting"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
>
|
>
|
||||||
<v-container v-if="currentPage === Pages.LANDING"
|
<v-container
|
||||||
|
v-if="currentPage === Pages.LANDING"
|
||||||
class="mb-12"
|
class="mb-12"
|
||||||
>
|
>
|
||||||
<v-card-title class="text-h4 justify-center text-center">
|
<v-card-title class="text-h4 justify-center text-center">
|
||||||
{{ $t('admin.setup.welcome-to-mealie-get-started') }}
|
{{ $t('admin.setup.welcome-to-mealie-get-started') }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-btn :to="groupSlug ? `/g/${groupSlug}` : '/login'"
|
<v-btn
|
||||||
|
:to="groupSlug ? `/g/${groupSlug}` : '/login'"
|
||||||
rounded
|
rounded
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="grey-lighten-1"
|
color="grey-lighten-1"
|
||||||
|
@ -36,35 +40,42 @@
|
||||||
{{ $t('admin.setup.already-set-up-bring-to-homepage') }}
|
{{ $t('admin.setup.already-set-up-bring-to-homepage') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<v-container v-if="currentPage === Pages.USER_INFO">
|
<v-container v-if="currentPage === Pages.USER_INFO">
|
||||||
<UserRegistrationForm />
|
<UserRegistrationForm />
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<v-container v-if="currentPage === Pages.PAGE_2">
|
<v-container v-if="currentPage === Pages.PAGE_2">
|
||||||
<v-card-title class="headline justify-center">
|
<v-card-title class="headline justify-center pa-0">
|
||||||
{{ $t('admin.setup.common-settings-for-new-sites') }}
|
{{ $t('admin.setup.common-settings-for-new-sites') }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<AutoForm v-model="commonSettings"
|
<AutoForm
|
||||||
|
v-model="commonSettings"
|
||||||
:items="commonSettingsForm"
|
:items="commonSettingsForm"
|
||||||
/>
|
/>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<v-container v-if="currentPage === Pages.CONFIRM">
|
<v-container v-if="currentPage === Pages.CONFIRM">
|
||||||
<v-card-title class="headline justify-center">
|
<v-card-title class="headline justify-center">
|
||||||
{{ $t("general.confirm-how-does-everything-look") }}
|
{{ $t("general.confirm-how-does-everything-look") }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-list>
|
<v-list>
|
||||||
<template v-for="(item, idx) in confirmationData">
|
<template v-for="(item, idx) in confirmationData">
|
||||||
<v-list-item v-if="item.display"
|
<v-list-item
|
||||||
|
v-if="item.display"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
>
|
>
|
||||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ item.value }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ item.value }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider v-if="idx !== confirmationData.length - 1"
|
<v-divider
|
||||||
|
v-if="idx !== confirmationData.length - 1"
|
||||||
:key="`divider-${idx}`"
|
:key="`divider-${idx}`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<v-container v-if="currentPage === Pages.END">
|
<v-container v-if="currentPage === Pages.END">
|
||||||
<v-card-title class="text-h4 justify-center">
|
<v-card-title class="text-h4 justify-center">
|
||||||
{{ $t('admin.setup.setup-complete') }}
|
{{ $t('admin.setup.setup-complete') }}
|
||||||
|
@ -72,7 +83,8 @@
|
||||||
<v-card-title class="text-h6 justify-center">
|
<v-card-title class="text-h6 justify-center">
|
||||||
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
|
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<div v-for="link, idx in setupCompleteLinks"
|
<div
|
||||||
|
v-for="link, idx in setupCompleteLinks"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="px-4 pt-4"
|
class="px-4 pt-4"
|
||||||
>
|
>
|
||||||
|
@ -82,7 +94,8 @@
|
||||||
{{ link.section }}
|
{{ link.section }}
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</div>
|
</div>
|
||||||
<v-btn :to="link.to"
|
<v-btn
|
||||||
|
:to="link.to"
|
||||||
color="info"
|
color="info"
|
||||||
>
|
>
|
||||||
{{ link.text }}
|
{{ link.text }}
|
||||||
|
|
|
@ -121,6 +121,7 @@
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="notifiers[index].appriseUrl"
|
v-model="notifiers[index].appriseUrl"
|
||||||
:label="$t('events.apprise-url-skipped-if-blank')"
|
:label="$t('events.apprise-url-skipped-if-blank')"
|
||||||
|
:hint="$t('events.apprise-url-is-left-intentionally-blank')"
|
||||||
/>
|
/>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="notifiers[index].enabled"
|
v-model="notifiers[index].enabled"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
class="md-container"
|
class="md-container"
|
||||||
>
|
>
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="checkAllDialog"
|
v-model="state.checkAllDialog"
|
||||||
:title="$t('general.confirm')"
|
:title="$t('general.confirm')"
|
||||||
can-confirm
|
can-confirm
|
||||||
@confirm="checkAll"
|
@confirm="checkAll"
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="uncheckAllDialog"
|
v-model="state.uncheckAllDialog"
|
||||||
:title="$t('general.confirm')"
|
:title="$t('general.confirm')"
|
||||||
can-confirm
|
can-confirm
|
||||||
@confirm="uncheckAll"
|
@confirm="uncheckAll"
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="deleteCheckedDialog"
|
v-model="state.deleteCheckedDialog"
|
||||||
:title="$t('general.confirm')"
|
:title="$t('general.confirm')"
|
||||||
can-confirm
|
can-confirm
|
||||||
@confirm="deleteChecked"
|
@confirm="deleteChecked"
|
||||||
|
@ -90,11 +90,6 @@
|
||||||
text: '',
|
text: '',
|
||||||
event: 'three-dot',
|
event: 'three-dot',
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
icon: $globals.icons.tags,
|
|
||||||
text: $t('shopping-list.toggle-label-sort'),
|
|
||||||
event: 'sort-by-labels',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: $globals.icons.tags,
|
icon: $globals.icons.tags,
|
||||||
text: $t('shopping-list.reorder-labels'),
|
text: $t('shopping-list.reorder-labels'),
|
||||||
|
@ -111,7 +106,6 @@
|
||||||
@edit="edit = true"
|
@edit="edit = true"
|
||||||
@three-dot="threeDot = true"
|
@three-dot="threeDot = true"
|
||||||
@check="openCheckAll"
|
@check="openCheckAll"
|
||||||
@sort-by-labels="sortByLabels"
|
|
||||||
@copy-plain="copyListItems('plain')"
|
@copy-plain="copyListItems('plain')"
|
||||||
@copy-markdown="copyListItems('markdown')"
|
@copy-markdown="copyListItems('markdown')"
|
||||||
@reorder-labels="toggleReorderLabelsDialog()"
|
@reorder-labels="toggleReorderLabelsDialog()"
|
||||||
|
@ -159,40 +153,6 @@
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- View without Label grouping -->
|
|
||||||
<div v-if="!preferences.viewByLabel">
|
|
||||||
<VueDraggable
|
|
||||||
v-model="listItems.unchecked"
|
|
||||||
handle=".handle"
|
|
||||||
:delay="250"
|
|
||||||
:delay-on-touch-only="true"
|
|
||||||
@start="loadingCounter += 1"
|
|
||||||
@end="loadingCounter -= 1"
|
|
||||||
@update:model-value="updateIndexUnchecked"
|
|
||||||
>
|
|
||||||
<v-lazy
|
|
||||||
v-for="(item, index) in listItems.unchecked"
|
|
||||||
:key="item.id"
|
|
||||||
class="my-2"
|
|
||||||
>
|
|
||||||
<ShoppingListItem
|
|
||||||
v-model="listItems.unchecked[index]"
|
|
||||||
class="my-2 my-sm-0"
|
|
||||||
:show-label="true"
|
|
||||||
:labels="allLabels || []"
|
|
||||||
:units="allUnits || []"
|
|
||||||
:foods="allFoods || []"
|
|
||||||
:recipes="recipeMap"
|
|
||||||
@checked="saveListItem"
|
|
||||||
@save="saveListItem"
|
|
||||||
@delete="deleteListItem(item)"
|
|
||||||
/>
|
|
||||||
</v-lazy>
|
|
||||||
</VueDraggable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- View By Label -->
|
|
||||||
<div v-else>
|
|
||||||
<div
|
<div
|
||||||
v-for="(value, key) in itemsByLabel"
|
v-for="(value, key) in itemsByLabel"
|
||||||
:key="key"
|
:key="key"
|
||||||
|
@ -230,7 +190,6 @@
|
||||||
>
|
>
|
||||||
<ShoppingListItem
|
<ShoppingListItem
|
||||||
v-model="value[index]"
|
v-model="value[index]"
|
||||||
:show-label="false"
|
|
||||||
:labels="allLabels || []"
|
:labels="allLabels || []"
|
||||||
:units="allUnits || []"
|
:units="allUnits || []"
|
||||||
:foods="allFoods || []"
|
:foods="allFoods || []"
|
||||||
|
@ -244,7 +203,6 @@
|
||||||
</div>
|
</div>
|
||||||
</v-expand-transition>
|
</v-expand-transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reorder Labels -->
|
<!-- Reorder Labels -->
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
|
@ -359,12 +317,12 @@
|
||||||
</div>
|
</div>
|
||||||
<v-divider class="my-4" />
|
<v-divider class="my-4" />
|
||||||
<RecipeList
|
<RecipeList
|
||||||
:recipes="Array.from(recipeMap.values())"
|
:recipes="recipeList"
|
||||||
show-description
|
show-description
|
||||||
:disabled="isOffline"
|
:disabled="isOffline"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(recipe, index) in recipeMap.values()"
|
v-for="(recipe, index) in recipeList"
|
||||||
#[`actions-${recipe.id}`]
|
#[`actions-${recipe.id}`]
|
||||||
:key="'item-actions-decrease' + recipe.id"
|
:key="'item-actions-decrease' + recipe.id"
|
||||||
>
|
>
|
||||||
|
@ -408,26 +366,14 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { useIdle, useOnline, useToggle } from "@vueuse/core";
|
|
||||||
import { useCopyList } from "~/composables/use-copy";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue";
|
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue";
|
||||||
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
|
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
|
||||||
import type { ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household";
|
|
||||||
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
||||||
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
|
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
|
||||||
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
|
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
|
||||||
import { useShoppingListItemActions } from "~/composables/use-shopping-list-item-actions";
|
|
||||||
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
|
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
|
||||||
import { getTextColor } from "~/composables/use-text-color";
|
import { getTextColor } from "~/composables/use-text-color";
|
||||||
import { uuid4 } from "~/composables/use-utils";
|
import { useShoppingListPage } from "~/composables/shopping-list-page/use-shopping-list-page";
|
||||||
|
|
||||||
type CopyTypes = "plain" | "markdown";
|
|
||||||
|
|
||||||
interface PresentLabel {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -437,809 +383,34 @@ export default defineNuxtComponent({
|
||||||
RecipeList,
|
RecipeList,
|
||||||
ShoppingListItemEditor,
|
ShoppingListItemEditor,
|
||||||
},
|
},
|
||||||
// middleware: "sidebase-auth",
|
|
||||||
setup() {
|
setup() {
|
||||||
const { mdAndUp } = useDisplay();
|
const { mdAndUp } = useDisplay();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const preferences = useShoppingListPreferences();
|
const preferences = useShoppingListPreferences();
|
||||||
|
|
||||||
const isOffline = computed(() => useOnline().value === false);
|
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: i18n.t("shopping-list.shopping-list"),
|
title: i18n.t("shopping-list.shopping-list"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { idle } = useIdle(5 * 60 * 1000); // 5 minutes
|
|
||||||
const loadingCounter = ref(1);
|
|
||||||
const recipeReferenceLoading = ref(false);
|
|
||||||
const userApi = useUserApi();
|
|
||||||
|
|
||||||
const edit = ref(false);
|
|
||||||
const threeDot = ref(false);
|
|
||||||
const reorderLabelsDialog = ref(false);
|
|
||||||
const preserveItemOrder = ref(false);
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
const id = route.params.id as string;
|
const id = route.params.id as string;
|
||||||
const shoppingListItemActions = useShoppingListItemActions(id);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
checkAllDialog: false,
|
|
||||||
uncheckAllDialog: false,
|
|
||||||
deleteCheckedDialog: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===============================================================
|
|
||||||
// Shopping List Actions
|
|
||||||
|
|
||||||
const shoppingList = ref<ShoppingListOut | null>(null);
|
|
||||||
async function fetchShoppingList() {
|
|
||||||
const data = await shoppingListItemActions.getList();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
loadingCounter.value += 1;
|
|
||||||
try {
|
|
||||||
await shoppingListItemActions.process();
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let newListValue: typeof shoppingList.value = null;
|
|
||||||
try {
|
|
||||||
newListValue = await fetchShoppingList();
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
|
|
||||||
// only update the list with the new value if we're not loading, to prevent UI jitter
|
|
||||||
if (loadingCounter.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent overwriting local changes with stale backend data when offline
|
|
||||||
if (isOffline.value) {
|
|
||||||
// Do not update shoppingList.value from backend when offline
|
|
||||||
updateListItemOrder();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we're not connected to the network, this will be null, so we don't want to clear the list
|
|
||||||
if (newListValue) {
|
|
||||||
shoppingList.value = newListValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateListItemOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateListItemOrder() {
|
|
||||||
if (!preserveItemOrder.value) {
|
|
||||||
groupAndSortListItemsByFood();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sortListItems();
|
|
||||||
}
|
|
||||||
updateItemsByLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// constantly polls for changes
|
|
||||||
async function pollForChanges() {
|
|
||||||
// pause polling if the user isn't active or we're busy
|
|
||||||
if (idle.value || loadingCounter.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await refresh();
|
|
||||||
|
|
||||||
if (shoppingList.value) {
|
|
||||||
attempts = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the refresh was unsuccessful, the shopping list will be null, so we increment the attempt counter
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
catch {
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we hit too many errors, stop polling
|
|
||||||
if (attempts >= maxAttempts) {
|
|
||||||
clearInterval(pollTimer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start polling
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
pollForChanges(); // populate initial list
|
|
||||||
|
|
||||||
// max poll time = pollFrequency * maxAttempts = 24 hours
|
|
||||||
// we use a long max poll time since polling stops when the user is idle anyway
|
|
||||||
const pollFrequency = 5000;
|
|
||||||
const maxAttempts = 17280;
|
|
||||||
let attempts = 0;
|
|
||||||
|
|
||||||
const pollTimer: ReturnType<typeof setInterval> = setInterval(() => {
|
|
||||||
pollForChanges();
|
|
||||||
}, pollFrequency);
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(pollTimer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// List Item CRUD
|
|
||||||
|
|
||||||
// Hydrate listItems from shoppingList.value?.listItems
|
|
||||||
const listItems = reactive({
|
|
||||||
unchecked: [] as ShoppingListItemOut[],
|
|
||||||
checked: [] as ShoppingListItemOut[],
|
|
||||||
});
|
|
||||||
|
|
||||||
function sortCheckedItems(a: ShoppingListItemOut, b: ShoppingListItemOut) {
|
|
||||||
if (a.updatedAt! === b.updatedAt!) {
|
|
||||||
return ((a.position || 0) > (b.position || 0)) ? -1 : 1;
|
|
||||||
}
|
|
||||||
return a.updatedAt! < b.updatedAt! ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => shoppingList.value?.listItems,
|
|
||||||
(items) => {
|
|
||||||
listItems.unchecked = (items?.filter(item => !item.checked) ?? []);
|
|
||||||
listItems.checked = (items?.filter(item => item.checked)
|
|
||||||
.sort(sortCheckedItems) ?? []);
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// Collapsable Labels
|
|
||||||
const labelOpenState = ref<{ [key: string]: boolean }>({});
|
|
||||||
|
|
||||||
const initializeLabelOpenStates = () => {
|
|
||||||
if (!shoppingList.value?.listItems) return;
|
|
||||||
|
|
||||||
const existingLabels = new Set(Object.keys(labelOpenState.value));
|
|
||||||
let hasChanges = false;
|
|
||||||
|
|
||||||
for (const item of shoppingList.value.listItems) {
|
|
||||||
const labelName = item.label?.name || i18n.t("shopping-list.no-label");
|
|
||||||
if (!existingLabels.has(labelName) && !(labelName in labelOpenState.value)) {
|
|
||||||
labelOpenState.value[labelName] = true;
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChanges) {
|
|
||||||
labelOpenState.value = { ...labelOpenState.value };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const labelNames = computed(() => {
|
|
||||||
return new Set(
|
|
||||||
shoppingList.value?.listItems
|
|
||||||
?.map(item => item.label?.name || i18n.t("shopping-list.no-label"))
|
|
||||||
.filter(Boolean) ?? [],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(labelNames, initializeLabelOpenStates, { immediate: true });
|
|
||||||
|
|
||||||
function toggleShowLabel(key: string) {
|
|
||||||
labelOpenState.value[key] = !labelOpenState.value[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [showChecked, toggleShowChecked] = useToggle(false);
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// Copy List Items
|
|
||||||
|
|
||||||
const copy = useCopyList();
|
|
||||||
|
|
||||||
function copyListItems(copyType: CopyTypes) {
|
|
||||||
const text: string[] = [];
|
|
||||||
|
|
||||||
if (preferences.value.viewByLabel) {
|
|
||||||
// if we're sorting by label, we want the copied text in subsections
|
|
||||||
Object.entries(itemsByLabel.value).forEach(([label, items], idx) => {
|
|
||||||
// for every group except the first, add a blank line
|
|
||||||
if (idx) {
|
|
||||||
text.push("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// add an appropriate heading for the label depending on the copy format
|
|
||||||
text.push(formatCopiedLabelHeading(copyType, label));
|
|
||||||
|
|
||||||
// now add the appropriately formatted list items with the given label
|
|
||||||
items.forEach(item => text.push(formatCopiedListItem(copyType, item)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// labels are toggled off, so just copy in the order they come in
|
|
||||||
const items = shoppingList.value?.listItems?.filter(item => !item.checked);
|
|
||||||
|
|
||||||
items?.forEach((item) => {
|
|
||||||
text.push(formatCopiedListItem(copyType, item));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
copy.copyPlain(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCopiedListItem(copyType: CopyTypes, item: ShoppingListItemOut): string {
|
|
||||||
const display = item.display || "";
|
|
||||||
switch (copyType) {
|
|
||||||
case "markdown":
|
|
||||||
return `- [ ] ${display}`;
|
|
||||||
default:
|
|
||||||
return display;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCopiedLabelHeading(copyType: CopyTypes, label: string): string {
|
|
||||||
switch (copyType) {
|
|
||||||
case "markdown":
|
|
||||||
return `# ${label}`;
|
|
||||||
default:
|
|
||||||
return `[${label}]`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// Check / Uncheck All
|
|
||||||
function openCheckAll() {
|
|
||||||
if (shoppingList.value?.listItems?.some(item => !item.checked)) {
|
|
||||||
state.checkAllDialog = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAll() {
|
|
||||||
state.checkAllDialog = false;
|
|
||||||
let hasChanged = false;
|
|
||||||
shoppingList.value?.listItems?.forEach((item) => {
|
|
||||||
if (!item.checked) {
|
|
||||||
hasChanged = true;
|
|
||||||
item.checked = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (hasChanged) {
|
|
||||||
updateUncheckedListItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openUncheckAll() {
|
|
||||||
if (shoppingList.value?.listItems?.some(item => item.checked)) {
|
|
||||||
state.uncheckAllDialog = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function uncheckAll() {
|
|
||||||
state.uncheckAllDialog = false;
|
|
||||||
let hasChanged = false;
|
|
||||||
shoppingList.value?.listItems?.forEach((item) => {
|
|
||||||
if (item.checked) {
|
|
||||||
hasChanged = true;
|
|
||||||
item.checked = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (hasChanged) {
|
|
||||||
listItems.unchecked = [...listItems.unchecked, ...listItems.checked];
|
|
||||||
listItems.checked = [];
|
|
||||||
updateUncheckedListItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDeleteChecked() {
|
|
||||||
if (shoppingList.value?.listItems?.some(item => item.checked)) {
|
|
||||||
state.deleteCheckedDialog = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteChecked() {
|
|
||||||
const checked = shoppingList.value?.listItems?.filter(item => item.checked);
|
|
||||||
|
|
||||||
if (!checked || checked?.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingCounter.value += 1;
|
|
||||||
deleteListItems(checked);
|
|
||||||
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// List Item Context Menu
|
|
||||||
|
|
||||||
const contextActions = {
|
|
||||||
delete: "delete",
|
|
||||||
};
|
|
||||||
|
|
||||||
const contextMenu = [
|
|
||||||
{ title: i18n.t("general.delete"), action: contextActions.delete },
|
|
||||||
];
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// Labels, Units, Foods
|
|
||||||
// TODO: Extract to Composable
|
|
||||||
|
|
||||||
const localLabels = ref<ShoppingListMultiPurposeLabelOut[]>();
|
|
||||||
|
|
||||||
|
const shoppingListPage = useShoppingListPage(id);
|
||||||
const { store: allLabels } = useLabelStore();
|
const { store: allLabels } = useLabelStore();
|
||||||
const { store: allUnits } = useUnitStore();
|
const { store: allUnits } = useUnitStore();
|
||||||
const { store: allFoods } = useFoodStore();
|
const { store: allFoods } = useFoodStore();
|
||||||
|
|
||||||
function getLabelColor(item: ShoppingListItemOut | null) {
|
|
||||||
return item?.label?.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByLabels() {
|
|
||||||
preferences.value.viewByLabel = !preferences.value.viewByLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleReorderLabelsDialog() {
|
|
||||||
// stop polling and populate localLabels
|
|
||||||
loadingCounter.value += 1;
|
|
||||||
reorderLabelsDialog.value = !reorderLabelsDialog.value;
|
|
||||||
localLabels.value = shoppingList.value?.labelSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) {
|
|
||||||
if (!shoppingList.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
labelSettings.forEach((labelSetting, index) => {
|
|
||||||
labelSetting.position = index;
|
|
||||||
return labelSetting;
|
|
||||||
});
|
|
||||||
|
|
||||||
localLabels.value = labelSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelLabelOrder() {
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
if (!shoppingList.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// restore original state
|
|
||||||
localLabels.value = shoppingList.value.labelSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveLabelOrder() {
|
|
||||||
if (!shoppingList.value || !localLabels.value || (localLabels.value === shoppingList.value.labelSettings)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingCounter.value += 1;
|
|
||||||
const { data } = await userApi.shopping.lists.updateLabelSettings(shoppingList.value.id, localLabels.value);
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
// update shoppingList labels using the API response
|
|
||||||
shoppingList.value.labelSettings = (data as ShoppingListOut).labelSettings;
|
|
||||||
updateItemsByLabel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const presentLabels = computed(() => {
|
|
||||||
const labels: PresentLabel[] = [];
|
|
||||||
|
|
||||||
shoppingList.value?.listItems?.forEach((item) => {
|
|
||||||
if (item.labelId && item.label) {
|
|
||||||
labels.push({
|
|
||||||
name: item.label.name,
|
|
||||||
id: item.labelId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
});
|
|
||||||
|
|
||||||
const itemsByLabel = ref<{ [key: string]: ShoppingListItemOut[] }>({});
|
|
||||||
|
|
||||||
interface ListItemGroup {
|
|
||||||
position: number;
|
|
||||||
createdAt: string;
|
|
||||||
items: ShoppingListItemOut[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortItems(a: ShoppingListItemOut | ListItemGroup, b: ShoppingListItemOut | ListItemGroup) {
|
|
||||||
// Sort by position ASC, then by createdAt ASC
|
|
||||||
const posA = a.position ?? 0;
|
|
||||||
const posB = b.position ?? 0;
|
|
||||||
if (posA !== posB) {
|
|
||||||
return posA - posB;
|
|
||||||
}
|
|
||||||
const createdA = a.createdAt ?? "";
|
|
||||||
const createdB = b.createdAt ?? "";
|
|
||||||
if (createdA !== createdB) {
|
|
||||||
return createdA < createdB ? -1 : 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupAndSortListItemsByFood() {
|
|
||||||
if (!shoppingList.value?.listItems?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkedItemKey = "__checkedItem";
|
|
||||||
const listItemGroupsMap = new Map<string, ListItemGroup>();
|
|
||||||
listItemGroupsMap.set(checkedItemKey, { position: Number.MAX_SAFE_INTEGER, createdAt: "", items: [] });
|
|
||||||
|
|
||||||
// group items by checked status, food, or note
|
|
||||||
shoppingList.value.listItems.forEach((item) => {
|
|
||||||
const key = item.checked
|
|
||||||
? checkedItemKey
|
|
||||||
: item.food?.name
|
|
||||||
? item.food.name
|
|
||||||
: item.note || "";
|
|
||||||
|
|
||||||
const group = listItemGroupsMap.get(key);
|
|
||||||
if (!group) {
|
|
||||||
listItemGroupsMap.set(key, { position: item.position || 0, createdAt: item.createdAt || "", items: [item] });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
group.items.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const listItemGroups = Array.from(listItemGroupsMap.values());
|
|
||||||
listItemGroups.sort(sortItems);
|
|
||||||
|
|
||||||
// sort group items, then aggregate them
|
|
||||||
const sortedItems: ShoppingListItemOut[] = [];
|
|
||||||
let nextPosition = 0;
|
|
||||||
listItemGroups.forEach((listItemGroup) => {
|
|
||||||
listItemGroup.items.sort(sortItems);
|
|
||||||
listItemGroup.items.forEach((item) => {
|
|
||||||
item.position = nextPosition;
|
|
||||||
nextPosition += 1;
|
|
||||||
sortedItems.push(item);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
shoppingList.value.listItems = sortedItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortListItems() {
|
|
||||||
if (!shoppingList.value?.listItems?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shoppingList.value.listItems.sort(sortItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateItemsByLabel() {
|
|
||||||
const items: { [prop: string]: ShoppingListItemOut[] } = {};
|
|
||||||
const noLabelText = i18n.t("shopping-list.no-label");
|
|
||||||
const noLabel = [] as ShoppingListItemOut[];
|
|
||||||
|
|
||||||
shoppingList.value?.listItems?.forEach((item) => {
|
|
||||||
if (item.checked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.labelId) {
|
|
||||||
if (item.label && item.label.name in items) {
|
|
||||||
items[item.label.name].push(item);
|
|
||||||
}
|
|
||||||
else if (item.label) {
|
|
||||||
items[item.label.name] = [item];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
noLabel.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (noLabel.length > 0) {
|
|
||||||
items[noLabelText] = noLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort the map by label order
|
|
||||||
const orderedLabelNames = shoppingList.value?.labelSettings?.map(labelSetting => labelSetting.label.name);
|
|
||||||
if (!orderedLabelNames) {
|
|
||||||
itemsByLabel.value = items;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsSorted: { [prop: string]: ShoppingListItemOut[] } = {};
|
|
||||||
if (noLabelText in items) {
|
|
||||||
itemsSorted[noLabelText] = items[noLabelText];
|
|
||||||
}
|
|
||||||
|
|
||||||
orderedLabelNames.forEach((labelName) => {
|
|
||||||
if (labelName in items) {
|
|
||||||
itemsSorted[labelName] = items[labelName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
itemsByLabel.value = itemsSorted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// Add/Remove Recipe References
|
|
||||||
|
|
||||||
const recipeMap = computed(() => new Map(
|
|
||||||
(shoppingList.value?.recipeReferences?.map(ref => ref.recipe) ?? [])
|
|
||||||
.map(recipe => [recipe.id || "", recipe])),
|
|
||||||
);
|
|
||||||
|
|
||||||
async function addRecipeReferenceToList(recipeId: string) {
|
|
||||||
if (!shoppingList.value || recipeReferenceLoading.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingCounter.value += 1;
|
|
||||||
recipeReferenceLoading.value = true;
|
|
||||||
const { data } = await userApi.shopping.lists.addRecipes(shoppingList.value.id, [{ recipeId }]);
|
|
||||||
recipeReferenceLoading.value = false;
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeRecipeReferenceToList(recipeId: string) {
|
|
||||||
if (!shoppingList.value || recipeReferenceLoading.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingCounter.value += 1;
|
|
||||||
recipeReferenceLoading.value = true;
|
|
||||||
const { data } = await userApi.shopping.lists.removeRecipe(shoppingList.value.id, recipeId);
|
|
||||||
recipeReferenceLoading.value = false;
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// List Item CRUD
|
|
||||||
|
|
||||||
/*
|
|
||||||
* saveListItem updates and update on the backend server. Additionally, if the item is
|
|
||||||
* checked it will also append that item to the end of the list so that the unchecked items
|
|
||||||
* are at the top of the list.
|
|
||||||
*/
|
|
||||||
function saveListItem(item: ShoppingListItemOut) {
|
|
||||||
if (!shoppingList.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set a temporary updatedAt timestamp prior to refresh so it appears at the top of the checked items
|
|
||||||
item.updatedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
// make updates reflect immediately
|
|
||||||
if (shoppingList.value.listItems) {
|
|
||||||
shoppingList.value.listItems.forEach((oldListItem: ShoppingListItemOut, idx: number) => {
|
|
||||||
if (oldListItem.id === item.id && shoppingList.value?.listItems) {
|
|
||||||
shoppingList.value.listItems[idx] = item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Immediately update checked/unchecked arrays for UI
|
|
||||||
listItems.unchecked = shoppingList.value.listItems.filter(i => !i.checked);
|
|
||||||
listItems.checked = shoppingList.value.listItems.filter(i => i.checked)
|
|
||||||
.sort(sortCheckedItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the item if it's checked, otherwise updateUncheckedListItems will handle it
|
|
||||||
if (item.checked) {
|
|
||||||
shoppingListItemActions.updateItem(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateListItemOrder();
|
|
||||||
updateUncheckedListItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteListItem(item: ShoppingListItemOut) {
|
|
||||||
if (!shoppingList.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shoppingListItemActions.deleteItem(item);
|
|
||||||
|
|
||||||
// remove the item from the list immediately so the user sees the change
|
|
||||||
if (shoppingList.value.listItems) {
|
|
||||||
shoppingList.value.listItems = shoppingList.value.listItems.filter(itm => itm.id !== item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// Create New Item
|
|
||||||
|
|
||||||
const createEditorOpen = ref(false);
|
|
||||||
const createListItemData = ref<ShoppingListItemOut>(listItemFactory());
|
|
||||||
|
|
||||||
function listItemFactory(): ShoppingListItemOut {
|
|
||||||
return {
|
return {
|
||||||
id: uuid4(),
|
|
||||||
shoppingListId: id,
|
|
||||||
checked: false,
|
|
||||||
position: shoppingList.value?.listItems?.length || 1,
|
|
||||||
quantity: 0,
|
|
||||||
note: "",
|
|
||||||
labelId: undefined,
|
|
||||||
unitId: undefined,
|
|
||||||
foodId: undefined,
|
|
||||||
} as ShoppingListItemOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* const newMeal = reactive({
|
|
||||||
date: "",
|
|
||||||
title: "",
|
|
||||||
text: "",
|
|
||||||
recipeId: undefined as string | undefined,
|
|
||||||
entryType: "dinner" as PlanEntryType,
|
|
||||||
existing: false,
|
|
||||||
id: 0,
|
|
||||||
groupId: "",
|
|
||||||
userId: $auth.user.value?.id || "",
|
|
||||||
}); */
|
|
||||||
|
|
||||||
function createListItem() {
|
|
||||||
if (!shoppingList.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!createListItemData.value.foodId && !createListItemData.value.note) {
|
|
||||||
// don't create an empty item
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingCounter.value += 1;
|
|
||||||
|
|
||||||
// make sure it's inserted into the end of the list, which may have been updated
|
|
||||||
createListItemData.value.position = shoppingList.value?.listItems?.length
|
|
||||||
? (shoppingList.value.listItems.reduce((a, b) => (a.position || 0) > (b.position || 0) ? a : b).position || 0) + 1
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
createListItemData.value.createdAt = new Date().toISOString();
|
|
||||||
createListItemData.value.updatedAt = createListItemData.value.createdAt;
|
|
||||||
|
|
||||||
updateListItemOrder();
|
|
||||||
|
|
||||||
shoppingListItemActions.createItem(createListItemData.value);
|
|
||||||
loadingCounter.value -= 1;
|
|
||||||
|
|
||||||
if (shoppingList.value.listItems) {
|
|
||||||
// add the item to the list immediately so the user sees the change
|
|
||||||
shoppingList.value.listItems.push(createListItemData.value);
|
|
||||||
updateListItemOrder();
|
|
||||||
}
|
|
||||||
createListItemData.value = listItemFactory();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateIndexUnchecked(uncheckedItems: ShoppingListItemOut[]) {
|
|
||||||
listItems.unchecked = uncheckedItems;
|
|
||||||
listItems.checked = shoppingList.value?.listItems?.filter(item => item.checked) || [];
|
|
||||||
|
|
||||||
// since the user has manually reordered the list, we should preserve this order
|
|
||||||
preserveItemOrder.value = true;
|
|
||||||
|
|
||||||
updateUncheckedListItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateIndexUncheckedByLabel(labelName: string, labeledUncheckedItems: ShoppingListItemOut[]) {
|
|
||||||
if (!itemsByLabel.value[labelName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update this label's item order
|
|
||||||
itemsByLabel.value[labelName] = labeledUncheckedItems;
|
|
||||||
|
|
||||||
// reset list order of all items
|
|
||||||
const allUncheckedItems: ShoppingListItemOut[] = [];
|
|
||||||
for (labelName in itemsByLabel.value) {
|
|
||||||
allUncheckedItems.push(...itemsByLabel.value[labelName]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// since the user has manually reordered the list, we should preserve this order
|
|
||||||
preserveItemOrder.value = true;
|
|
||||||
|
|
||||||
// save changes
|
|
||||||
listItems.unchecked = allUncheckedItems;
|
|
||||||
listItems.checked = shoppingList.value?.listItems?.filter(item => item.checked) || [];
|
|
||||||
updateUncheckedListItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteListItems(items: ShoppingListItemOut[]) {
|
|
||||||
if (!shoppingList.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
items.forEach((item) => {
|
|
||||||
shoppingListItemActions.deleteItem(item);
|
|
||||||
});
|
|
||||||
// remove the items from the list immediately so the user sees the change
|
|
||||||
if (shoppingList.value?.listItems) {
|
|
||||||
const deletedItems = new Set(items.map(item => item.id));
|
|
||||||
shoppingList.value.listItems = shoppingList.value.listItems.filter(itm => !deletedItems.has(itm.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUncheckedListItems() {
|
|
||||||
if (!shoppingList.value?.listItems) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set position for unchecked items
|
|
||||||
listItems.unchecked.forEach((item: ShoppingListItemOut, idx: number) => {
|
|
||||||
item.position = idx;
|
|
||||||
shoppingListItemActions.updateItem(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
addRecipeReferenceToList,
|
|
||||||
allLabels,
|
|
||||||
contextMenu,
|
|
||||||
copyListItems,
|
|
||||||
createEditorOpen,
|
|
||||||
createListItem,
|
|
||||||
createListItemData,
|
|
||||||
deleteChecked,
|
|
||||||
openDeleteChecked,
|
|
||||||
deleteListItem,
|
|
||||||
edit,
|
|
||||||
threeDot,
|
|
||||||
getLabelColor,
|
|
||||||
groupSlug,
|
groupSlug,
|
||||||
itemsByLabel,
|
|
||||||
listItems,
|
|
||||||
loadingCounter,
|
|
||||||
preferences,
|
preferences,
|
||||||
presentLabels,
|
allLabels,
|
||||||
recipeMap,
|
|
||||||
removeRecipeReferenceToList,
|
|
||||||
reorderLabelsDialog,
|
|
||||||
toggleReorderLabelsDialog,
|
|
||||||
localLabels,
|
|
||||||
updateLabelOrder,
|
|
||||||
cancelLabelOrder,
|
|
||||||
saveLabelOrder,
|
|
||||||
saveListItem,
|
|
||||||
shoppingList,
|
|
||||||
showChecked,
|
|
||||||
sortByLabels,
|
|
||||||
labelOpenState,
|
|
||||||
toggleShowLabel,
|
|
||||||
toggleShowChecked,
|
|
||||||
uncheckAll,
|
|
||||||
openUncheckAll,
|
|
||||||
checkAll,
|
|
||||||
openCheckAll,
|
|
||||||
updateIndexUnchecked,
|
|
||||||
updateIndexUncheckedByLabel,
|
|
||||||
allUnits,
|
allUnits,
|
||||||
allFoods,
|
allFoods,
|
||||||
getTextColor,
|
getTextColor,
|
||||||
isOffline,
|
|
||||||
mdAndUp,
|
mdAndUp,
|
||||||
|
...shoppingListPage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,7 +85,6 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
|
||||||
|
|
||||||
def _filter_builder(self, **kwargs) -> dict[str, Any]:
|
def _filter_builder(self, **kwargs) -> dict[str, Any]:
|
||||||
dct = {}
|
dct = {}
|
||||||
|
|
||||||
if self.group_id:
|
if self.group_id:
|
||||||
dct["group_id"] = self.group_id
|
dct["group_id"] = self.group_id
|
||||||
if self.household_id:
|
if self.household_id:
|
||||||
|
@ -146,7 +145,11 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
|
||||||
return self.session.execute(self._query().filter_by(**fltr)).unique().scalars().one()
|
return self.session.execute(self._query().filter_by(**fltr)).unique().scalars().one()
|
||||||
|
|
||||||
def get_one(
|
def get_one(
|
||||||
self, value: str | int | UUID4, key: str | None = None, any_case=False, override_schema=None
|
self,
|
||||||
|
value: str | int | UUID4,
|
||||||
|
key: str | None = None,
|
||||||
|
any_case=False,
|
||||||
|
override_schema=None,
|
||||||
) -> Schema | None:
|
) -> Schema | None:
|
||||||
key = key or self.primary_key
|
key = key or self.primary_key
|
||||||
eff_schema = override_schema or self.schema
|
eff_schema = override_schema or self.schema
|
||||||
|
|
|
@ -660,7 +660,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "orange",
|
"name": "orange",
|
||||||
"plural_name": "oranges"
|
"plural_name": "appelsiner"
|
||||||
},
|
},
|
||||||
"raisin": {
|
"raisin": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1055,7 +1055,7 @@
|
||||||
"dried lemon": {
|
"dried lemon": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried lemon",
|
"name": "tørret citron",
|
||||||
"plural_name": "dried lemons"
|
"plural_name": "dried lemons"
|
||||||
},
|
},
|
||||||
"young jackfruit": {
|
"young jackfruit": {
|
||||||
|
|
|
@ -4709,8 +4709,8 @@
|
||||||
"duck confit": {
|
"duck confit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "duck confit",
|
"name": "Entenconfit",
|
||||||
"plural_name": "duck confits"
|
"plural_name": "Entenconfite"
|
||||||
},
|
},
|
||||||
"roast duck": {
|
"roast duck": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -4740,13 +4740,13 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "geräucherte Putenflügel",
|
"name": "geräucherte Putenflügel",
|
||||||
"plural_name": "smoked turkey wings"
|
"plural_name": "Geräucherte Truthahnflügel"
|
||||||
},
|
},
|
||||||
"chicken curry-cut": {
|
"chicken curry-cut": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "chicken curry-cut",
|
"name": "Curry-geschnittenes Hähnchen",
|
||||||
"plural_name": "chicken curry-cuts"
|
"plural_name": "Curry-geschnittene Hähnchenteile"
|
||||||
},
|
},
|
||||||
"chicken schnitzel": {
|
"chicken schnitzel": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -6131,8 +6131,8 @@
|
||||||
"dried parsley flake": {
|
"dried parsley flake": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried parsley flake",
|
"name": "Petersilie, gerebelt",
|
||||||
"plural_name": "dried parsley flakes"
|
"plural_name": "Petersilie, gerebelt"
|
||||||
},
|
},
|
||||||
"fenugreek seed": {
|
"fenugreek seed": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -6233,8 +6233,8 @@
|
||||||
"guajillo pepper": {
|
"guajillo pepper": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "guajillo pepper",
|
"name": "Guajillo-Chili",
|
||||||
"plural_name": "guajillo peppers"
|
"plural_name": "Guajillo-Chilis"
|
||||||
},
|
},
|
||||||
"pink peppercorn": {
|
"pink peppercorn": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|
|
@ -881,8 +881,8 @@
|
||||||
"dried mango": {
|
"dried mango": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried mango",
|
"name": "manga seca",
|
||||||
"plural_name": "dried mangoes"
|
"plural_name": "mangas secas"
|
||||||
},
|
},
|
||||||
"dried apple": {
|
"dried apple": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -905,8 +905,8 @@
|
||||||
"banana chip": {
|
"banana chip": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "banana chip",
|
"name": "chip de banana",
|
||||||
"plural_name": "banana chips"
|
"plural_name": "chips de banana"
|
||||||
},
|
},
|
||||||
"kumquat": {
|
"kumquat": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -935,8 +935,8 @@
|
||||||
"asian pear": {
|
"asian pear": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "asian pear",
|
"name": "pera asiática",
|
||||||
"plural_name": "asian pears"
|
"plural_name": "peras asiáticas"
|
||||||
},
|
},
|
||||||
"lychee": {
|
"lychee": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -972,7 +972,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "pomelo",
|
"name": "pomelo",
|
||||||
"plural_name": "pomeloes"
|
"plural_name": "pomelos"
|
||||||
},
|
},
|
||||||
"chestnut puree": {
|
"chestnut puree": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1007,8 +1007,8 @@
|
||||||
"apple chip": {
|
"apple chip": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "apple chip",
|
"name": "chip de mazá",
|
||||||
"plural_name": "apple chips"
|
"plural_name": "chips de mazá"
|
||||||
},
|
},
|
||||||
"mixed peel": {
|
"mixed peel": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1055,8 +1055,8 @@
|
||||||
"dried lemon": {
|
"dried lemon": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried lemon",
|
"name": "limón seco",
|
||||||
"plural_name": "dried lemons"
|
"plural_name": "limóns secos"
|
||||||
},
|
},
|
||||||
"young jackfruit": {
|
"young jackfruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1465,8 +1465,8 @@
|
||||||
"raspberry": {
|
"raspberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "raspberry",
|
"name": "framboesa",
|
||||||
"plural_name": "raspberries"
|
"plural_name": "framboesas"
|
||||||
},
|
},
|
||||||
"cranberry": {
|
"cranberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1483,8 +1483,8 @@
|
||||||
"blackberry": {
|
"blackberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "blackberry",
|
"name": "amora",
|
||||||
"plural_name": "blackberries"
|
"plural_name": "amoras"
|
||||||
},
|
},
|
||||||
"berry mix": {
|
"berry mix": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|
|
@ -100,12 +100,12 @@
|
||||||
"sweet corn": {
|
"sweet corn": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "sweet corn",
|
"name": "mais dolce",
|
||||||
"plural_name": "sweet corns"
|
"plural_name": "mais dolci"
|
||||||
},
|
},
|
||||||
"chile pepper": {
|
"chile pepper": {
|
||||||
"aliases": [
|
"aliases": [
|
||||||
"capsicum"
|
"peperoncino"
|
||||||
],
|
],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "peperoncino",
|
"name": "peperoncino",
|
||||||
|
@ -132,8 +132,8 @@
|
||||||
"baby green": {
|
"baby green": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "baby green",
|
"name": "insalatina",
|
||||||
"plural_name": "baby greens"
|
"plural_name": "insalatine"
|
||||||
},
|
},
|
||||||
"pumpkin": {
|
"pumpkin": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -151,112 +151,112 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "cavolo",
|
"name": "cavolo",
|
||||||
"plural_name": "cabbages"
|
"plural_name": "cavoli"
|
||||||
},
|
},
|
||||||
"asparagu": {
|
"asparagu": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "asparagu",
|
"name": "asparago",
|
||||||
"plural_name": "asparagus"
|
"plural_name": "asparagi"
|
||||||
},
|
},
|
||||||
"kale": {
|
"kale": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "kale",
|
"name": "cavolo riccio",
|
||||||
"plural_name": "kales"
|
"plural_name": "cavoli ricci"
|
||||||
},
|
},
|
||||||
"arugula": {
|
"arugula": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "arugula",
|
"name": "rucola",
|
||||||
"plural_name": "arugulas"
|
"plural_name": "rucole"
|
||||||
},
|
},
|
||||||
"leek": {
|
"leek": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "leek",
|
"name": "porro",
|
||||||
"plural_name": "leeks"
|
"plural_name": "porri"
|
||||||
},
|
},
|
||||||
"eggplant": {
|
"eggplant": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "eggplant",
|
"name": "melanzana",
|
||||||
"plural_name": "eggplants"
|
"plural_name": "melanzane"
|
||||||
},
|
},
|
||||||
"lettuce": {
|
"lettuce": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "lettuce",
|
"name": "lattuga",
|
||||||
"plural_name": "lettuces"
|
"plural_name": "lattughe"
|
||||||
},
|
},
|
||||||
"butternut squash": {
|
"butternut squash": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "butternut squash",
|
"name": "zucca violina",
|
||||||
"plural_name": "butternut squashes"
|
"plural_name": "zucche violine"
|
||||||
},
|
},
|
||||||
"romaine": {
|
"romaine": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "romaine",
|
"name": "lattuga romana",
|
||||||
"plural_name": "romaines"
|
"plural_name": "lattughe romane"
|
||||||
},
|
},
|
||||||
"beetroot": {
|
"beetroot": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "beetroot",
|
"name": "barbabietola",
|
||||||
"plural_name": "beetroots"
|
"plural_name": "barbabietole"
|
||||||
},
|
},
|
||||||
"brussels sprout": {
|
"brussels sprout": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "brussels sprout",
|
"name": "cavoletto di Bruxelles",
|
||||||
"plural_name": "brussels sprouts"
|
"plural_name": "cavoletti di Bruxelles"
|
||||||
},
|
},
|
||||||
"fennel": {
|
"fennel": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "fennel",
|
"name": "finocchio",
|
||||||
"plural_name": "fennels"
|
"plural_name": "finocchi"
|
||||||
},
|
},
|
||||||
"sun dried tomato": {
|
"sun dried tomato": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "sun dried tomato",
|
"name": "pomodoro essiccato al sole",
|
||||||
"plural_name": "sun dried tomatoes"
|
"plural_name": "pomodori essiccati al sole"
|
||||||
},
|
},
|
||||||
"radish": {
|
"radish": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "radish",
|
"name": "ravanello",
|
||||||
"plural_name": "radishes"
|
"plural_name": "ravanelli"
|
||||||
},
|
},
|
||||||
"red cabbage": {
|
"red cabbage": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "red cabbage",
|
"name": "cavolo rosso",
|
||||||
"plural_name": "red cabbages"
|
"plural_name": "cavoli rossi"
|
||||||
},
|
},
|
||||||
"artichoke": {
|
"artichoke": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "artichoke",
|
"name": "carciofo",
|
||||||
"plural_name": "artichokes"
|
"plural_name": "carciofi"
|
||||||
},
|
},
|
||||||
"new potato": {
|
"new potato": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "new potato",
|
"name": "patata nuova",
|
||||||
"plural_name": "new potatoes"
|
"plural_name": "patate nuove"
|
||||||
},
|
},
|
||||||
"summer squash": {
|
"summer squash": {
|
||||||
"aliases": [
|
"aliases": [
|
||||||
"courgette",
|
"zucchina",
|
||||||
"gem squash"
|
"gem squash"
|
||||||
],
|
],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "summer squash",
|
"name": "zucca estiva",
|
||||||
"plural_name": "summer squashes"
|
"plural_name": "zucche estive"
|
||||||
},
|
},
|
||||||
"mixed green": {
|
"mixed green": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -267,20 +267,20 @@
|
||||||
"parsnip": {
|
"parsnip": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "parsnip",
|
"name": "pastinaca",
|
||||||
"plural_name": "parsnips"
|
"plural_name": "pastinache"
|
||||||
},
|
},
|
||||||
"baby carrot": {
|
"baby carrot": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "baby carrot",
|
"name": "carotina",
|
||||||
"plural_name": "baby carrots"
|
"plural_name": "carotine"
|
||||||
},
|
},
|
||||||
"mixed vegetable": {
|
"mixed vegetable": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "mixed vegetable",
|
"name": "ortaggi misti",
|
||||||
"plural_name": "mixed vegetables"
|
"plural_name": "ortaggi misti"
|
||||||
},
|
},
|
||||||
"poblano pepper": {
|
"poblano pepper": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -303,50 +303,50 @@
|
||||||
"cayenne pepper": {
|
"cayenne pepper": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "cayenne pepper",
|
"name": "pepe di Caienna",
|
||||||
"plural_name": "cayenne peppers"
|
"plural_name": "pepi di Caienna"
|
||||||
},
|
},
|
||||||
"green tomato": {
|
"green tomato": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "green tomato",
|
"name": "pomodoro verde",
|
||||||
"plural_name": "green tomatoes"
|
"plural_name": "pomodori verdi"
|
||||||
},
|
},
|
||||||
"watercress": {
|
"watercress": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "watercress",
|
"name": "crescione",
|
||||||
"plural_name": "watercress"
|
"plural_name": "crescioni"
|
||||||
},
|
},
|
||||||
"iceberg": {
|
"iceberg": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "iceberg",
|
"name": "iceberg",
|
||||||
"plural_name": "icebergs"
|
"plural_name": "iceberg"
|
||||||
},
|
},
|
||||||
"mashed potato": {
|
"mashed potato": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "mashed potato",
|
"name": "purè di patate",
|
||||||
"plural_name": "mashed potatoes"
|
"plural_name": "purè di patate"
|
||||||
},
|
},
|
||||||
"horseradish": {
|
"horseradish": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "horseradish",
|
"name": "rafano",
|
||||||
"plural_name": "horseradishes"
|
"plural_name": "rafani"
|
||||||
},
|
},
|
||||||
"chard": {
|
"chard": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "chard",
|
"name": "bietola",
|
||||||
"plural_name": "chards"
|
"plural_name": "bietole"
|
||||||
},
|
},
|
||||||
"pimiento": {
|
"pimiento": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "pimiento",
|
"name": "pimiento",
|
||||||
"plural_name": "pimientoes"
|
"plural_name": "pimienti"
|
||||||
},
|
},
|
||||||
"spaghetti squash": {
|
"spaghetti squash": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -389,8 +389,8 @@
|
||||||
"turnip": {
|
"turnip": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "turnip",
|
"name": "rapa",
|
||||||
"plural_name": "turnips"
|
"plural_name": "rape"
|
||||||
},
|
},
|
||||||
"thai chile pepper": {
|
"thai chile pepper": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -443,8 +443,8 @@
|
||||||
"plantain": {
|
"plantain": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "plantain",
|
"name": "platano",
|
||||||
"plural_name": "plantains"
|
"plural_name": "platani"
|
||||||
},
|
},
|
||||||
"leaf lettuce": {
|
"leaf lettuce": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|
|
@ -1890,7 +1890,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "trail mix",
|
"name": "trail mix",
|
||||||
"plural_name": "trail mixes"
|
"plural_name": "studentenhavers"
|
||||||
},
|
},
|
||||||
"basil seed": {
|
"basil seed": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -2105,8 +2105,8 @@
|
||||||
"cottage cheese": {
|
"cottage cheese": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "cottage cheese",
|
"name": "huttenkaas",
|
||||||
"plural_name": "cottage cheeses"
|
"plural_name": "huttenkazen"
|
||||||
},
|
},
|
||||||
"american cheese": {
|
"american cheese": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|
|
@ -251,7 +251,7 @@
|
||||||
},
|
},
|
||||||
"summer squash": {
|
"summer squash": {
|
||||||
"aliases": [
|
"aliases": [
|
||||||
"courgette",
|
"squash",
|
||||||
"gem squash"
|
"gem squash"
|
||||||
],
|
],
|
||||||
"description": "",
|
"description": "",
|
||||||
|
@ -339,7 +339,7 @@
|
||||||
"chard": {
|
"chard": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "chard",
|
"name": "bladbete",
|
||||||
"plural_name": "chards"
|
"plural_name": "chards"
|
||||||
},
|
},
|
||||||
"pimiento": {
|
"pimiento": {
|
||||||
|
@ -402,7 +402,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "bok choy",
|
"name": "bok choy",
|
||||||
"plural_name": "bok choy"
|
"plural_name": "pak choi"
|
||||||
},
|
},
|
||||||
"okra": {
|
"okra": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -511,8 +511,8 @@
|
||||||
"baby corn": {
|
"baby corn": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "baby corn",
|
"name": "baby mais",
|
||||||
"plural_name": "baby corns"
|
"plural_name": "baby mais"
|
||||||
},
|
},
|
||||||
"broccoli rabe": {
|
"broccoli rabe": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -642,7 +642,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "lime",
|
"name": "lime",
|
||||||
"plural_name": "limes"
|
"plural_name": "lime"
|
||||||
},
|
},
|
||||||
"apple": {
|
"apple": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -719,7 +719,7 @@
|
||||||
"pomegranate": {
|
"pomegranate": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "pomegranate",
|
"name": "granateple",
|
||||||
"plural_name": "pomegranates"
|
"plural_name": "pomegranates"
|
||||||
},
|
},
|
||||||
"watermelon": {
|
"watermelon": {
|
||||||
|
@ -749,7 +749,7 @@
|
||||||
"grapefruit": {
|
"grapefruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "grapefruit",
|
"name": "grapefrukt",
|
||||||
"plural_name": "grapefrukt"
|
"plural_name": "grapefrukt"
|
||||||
},
|
},
|
||||||
"plum": {
|
"plum": {
|
||||||
|
@ -762,13 +762,13 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "fig",
|
"name": "fig",
|
||||||
"plural_name": "figs"
|
"plural_name": "fikener"
|
||||||
},
|
},
|
||||||
"apricot": {
|
"apricot": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "aprikos",
|
"name": "aprikos",
|
||||||
"plural_name": "apricots"
|
"plural_name": "aprikos"
|
||||||
},
|
},
|
||||||
"currant": {
|
"currant": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -803,8 +803,8 @@
|
||||||
"passion fruit": {
|
"passion fruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "passion fruit",
|
"name": "pasjonsfrukt",
|
||||||
"plural_name": "passion fruits"
|
"plural_name": "pasjonsfrukter"
|
||||||
},
|
},
|
||||||
"papaya": {
|
"papaya": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -821,20 +821,20 @@
|
||||||
"nectarine": {
|
"nectarine": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "nectarine",
|
"name": "nektarin",
|
||||||
"plural_name": "nectarines"
|
"plural_name": "nektariner"
|
||||||
},
|
},
|
||||||
"dried fig": {
|
"dried fig": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried fig",
|
"name": "dried fig",
|
||||||
"plural_name": "dried figs"
|
"plural_name": "tørkede fikener"
|
||||||
},
|
},
|
||||||
"chestnut": {
|
"chestnut": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "chestnut",
|
"name": "kastanje",
|
||||||
"plural_name": "chestnuts"
|
"plural_name": "kastanjer"
|
||||||
},
|
},
|
||||||
"meyer lemon": {
|
"meyer lemon": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -845,14 +845,14 @@
|
||||||
"honeydew melon": {
|
"honeydew melon": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "honeydew melon",
|
"name": "honning melon",
|
||||||
"plural_name": "honeydew melons"
|
"plural_name": "honeydew melons"
|
||||||
},
|
},
|
||||||
"dried fruit": {
|
"dried fruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried fruit",
|
"name": "tørket frukt",
|
||||||
"plural_name": "dried fruits"
|
"plural_name": "tørket frukt"
|
||||||
},
|
},
|
||||||
"clementine": {
|
"clementine": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1056,7 +1056,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried lemon",
|
"name": "dried lemon",
|
||||||
"plural_name": "dried lemons"
|
"plural_name": "tørkede sitroner"
|
||||||
},
|
},
|
||||||
"young jackfruit": {
|
"young jackfruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|
|
@ -310,7 +310,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "green tomato",
|
"name": "green tomato",
|
||||||
"plural_name": "green tomatoes"
|
"plural_name": "zielone pomidory"
|
||||||
},
|
},
|
||||||
"watercress": {
|
"watercress": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -357,8 +357,8 @@
|
||||||
"butter lettuce": {
|
"butter lettuce": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "butter lettuce",
|
"name": "sałata masłowa",
|
||||||
"plural_name": "butter lettuces"
|
"plural_name": "sałaty masłowe"
|
||||||
},
|
},
|
||||||
"hash brown": {
|
"hash brown": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -389,8 +389,8 @@
|
||||||
"turnip": {
|
"turnip": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "turnip",
|
"name": "rzepa",
|
||||||
"plural_name": "turnips"
|
"plural_name": "rzepy"
|
||||||
},
|
},
|
||||||
"thai chile pepper": {
|
"thai chile pepper": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -851,8 +851,8 @@
|
||||||
"dried fruit": {
|
"dried fruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried fruit",
|
"name": "Suszony owoc",
|
||||||
"plural_name": "dried fruits"
|
"plural_name": "Suszone owoce"
|
||||||
},
|
},
|
||||||
"clementine": {
|
"clementine": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -881,14 +881,14 @@
|
||||||
"dried mango": {
|
"dried mango": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried mango",
|
"name": "Suszone mango",
|
||||||
"plural_name": "dried mangoes"
|
"plural_name": "Suszone mango"
|
||||||
},
|
},
|
||||||
"dried apple": {
|
"dried apple": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried apple",
|
"name": "Suszone jabłko",
|
||||||
"plural_name": "dried apples"
|
"plural_name": "Suszone jabłka"
|
||||||
},
|
},
|
||||||
"quince": {
|
"quince": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -912,7 +912,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "kumquat",
|
"name": "kumquat",
|
||||||
"plural_name": "kumquats"
|
"plural_name": "kumkwat"
|
||||||
},
|
},
|
||||||
"jackfruit": {
|
"jackfruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -923,8 +923,8 @@
|
||||||
"dragon fruit": {
|
"dragon fruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dragon fruit",
|
"name": "smoczy owoc",
|
||||||
"plural_name": "dragon fruits"
|
"plural_name": "smocze owoce"
|
||||||
},
|
},
|
||||||
"mixed fruit": {
|
"mixed fruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -959,20 +959,20 @@
|
||||||
"star fruit": {
|
"star fruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "star fruit",
|
"name": "karambola",
|
||||||
"plural_name": "star fruits"
|
"plural_name": "karambole"
|
||||||
},
|
},
|
||||||
"green papaya": {
|
"green papaya": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "green papaya",
|
"name": "zielona papaja",
|
||||||
"plural_name": "green papayas"
|
"plural_name": "zielone papaje"
|
||||||
},
|
},
|
||||||
"pomelo": {
|
"pomelo": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "pomelo",
|
"name": "pomelo",
|
||||||
"plural_name": "pomeloes"
|
"plural_name": "pomelo"
|
||||||
},
|
},
|
||||||
"chestnut puree": {
|
"chestnut puree": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1055,8 +1055,8 @@
|
||||||
"dried lemon": {
|
"dried lemon": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried lemon",
|
"name": "Suszona cytryna",
|
||||||
"plural_name": "dried lemons"
|
"plural_name": "Suszone cytryny"
|
||||||
},
|
},
|
||||||
"young jackfruit": {
|
"young jackfruit": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1068,7 +1068,7 @@
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "durian",
|
"name": "durian",
|
||||||
"plural_name": "durians"
|
"plural_name": "duriany"
|
||||||
},
|
},
|
||||||
"freeze-dried apple": {
|
"freeze-dried apple": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1245,8 +1245,8 @@
|
||||||
"shiitake mushroom": {
|
"shiitake mushroom": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "shiitake mushroom",
|
"name": "Grzyb Shiitake",
|
||||||
"plural_name": "shiitake mushrooms"
|
"plural_name": "Grzyby Shiitake"
|
||||||
},
|
},
|
||||||
"portobello mushroom": {
|
"portobello mushroom": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1299,8 +1299,8 @@
|
||||||
"black truffle": {
|
"black truffle": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "black truffle",
|
"name": "czarna trufla",
|
||||||
"plural_name": "black truffles"
|
"plural_name": "czarne trufle"
|
||||||
},
|
},
|
||||||
"morel mushroom": {
|
"morel mushroom": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1353,8 +1353,8 @@
|
||||||
"white truffle": {
|
"white truffle": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "white truffle",
|
"name": "biała trufla",
|
||||||
"plural_name": "white truffles"
|
"plural_name": "białe trufle"
|
||||||
},
|
},
|
||||||
"white fungu": {
|
"white fungu": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1453,26 +1453,26 @@
|
||||||
"strawberry": {
|
"strawberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "strawberry",
|
"name": "truskawka",
|
||||||
"plural_name": "strawberries"
|
"plural_name": "truskawki"
|
||||||
},
|
},
|
||||||
"blueberry": {
|
"blueberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "blueberry",
|
"name": "borówka",
|
||||||
"plural_name": "blueberries"
|
"plural_name": "borówki"
|
||||||
},
|
},
|
||||||
"raspberry": {
|
"raspberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "raspberry",
|
"name": "malina",
|
||||||
"plural_name": "raspberries"
|
"plural_name": "maliny"
|
||||||
},
|
},
|
||||||
"cranberry": {
|
"cranberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "cranberry",
|
"name": "żurawina",
|
||||||
"plural_name": "cranberries"
|
"plural_name": "żurawiny"
|
||||||
},
|
},
|
||||||
"cherry": {
|
"cherry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1483,8 +1483,8 @@
|
||||||
"blackberry": {
|
"blackberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "blackberry",
|
"name": "jeżyna",
|
||||||
"plural_name": "blackberries"
|
"plural_name": "jeżyny"
|
||||||
},
|
},
|
||||||
"berry mix": {
|
"berry mix": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1519,14 +1519,14 @@
|
||||||
"goji berry": {
|
"goji berry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "goji berry",
|
"name": "jagoda goji",
|
||||||
"plural_name": "goji berries"
|
"plural_name": "jagody goji"
|
||||||
},
|
},
|
||||||
"dried blueberry": {
|
"dried blueberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried blueberry",
|
"name": "Suszona borówka",
|
||||||
"plural_name": "dried blueberries"
|
"plural_name": "Suszone borówki"
|
||||||
},
|
},
|
||||||
"freeze-dried strawberry": {
|
"freeze-dried strawberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1537,8 +1537,8 @@
|
||||||
"gooseberry": {
|
"gooseberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "gooseberry",
|
"name": "agrest",
|
||||||
"plural_name": "gooseberries"
|
"plural_name": "agresty"
|
||||||
},
|
},
|
||||||
"freeze-dried raspberry": {
|
"freeze-dried raspberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1561,14 +1561,14 @@
|
||||||
"mulberry": {
|
"mulberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "mulberry",
|
"name": "morwa",
|
||||||
"plural_name": "mulberries"
|
"plural_name": "morwy"
|
||||||
},
|
},
|
||||||
"acai berry": {
|
"acai berry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "acai berry",
|
"name": "jagoda acai",
|
||||||
"plural_name": "acai berries"
|
"plural_name": "jagody acai"
|
||||||
},
|
},
|
||||||
"canned cherry": {
|
"canned cherry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1585,8 +1585,8 @@
|
||||||
"elderberry": {
|
"elderberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "elderberry",
|
"name": "czarny bez",
|
||||||
"plural_name": "elderberries"
|
"plural_name": "czarny bez"
|
||||||
},
|
},
|
||||||
"freeze-dried blueberry": {
|
"freeze-dried blueberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -1663,8 +1663,8 @@
|
||||||
"aronia berry": {
|
"aronia berry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "aronia berry",
|
"name": "jagoda aronii",
|
||||||
"plural_name": "aronia berries"
|
"plural_name": "jagody aronii"
|
||||||
},
|
},
|
||||||
"chokeberry": {
|
"chokeberry": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -5181,8 +5181,8 @@
|
||||||
"dried anchovy": {
|
"dried anchovy": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried anchovy",
|
"name": "Suszona anszua",
|
||||||
"plural_name": "dried anchovies"
|
"plural_name": "Suszone anszua"
|
||||||
},
|
},
|
||||||
"arctic char": {
|
"arctic char": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -5331,8 +5331,8 @@
|
||||||
"dried fish": {
|
"dried fish": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried fish",
|
"name": "suszona ryba",
|
||||||
"plural_name": "dried fish"
|
"plural_name": "suszone ryby"
|
||||||
},
|
},
|
||||||
"flathead": {
|
"flathead": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -5587,8 +5587,8 @@
|
||||||
"dried shrimp": {
|
"dried shrimp": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried shrimp",
|
"name": "suszona garnela",
|
||||||
"plural_name": "dried shrimps"
|
"plural_name": "suszone garnele"
|
||||||
},
|
},
|
||||||
"bay scallop": {
|
"bay scallop": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -5641,8 +5641,8 @@
|
||||||
"dried prawn": {
|
"dried prawn": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried prawn",
|
"name": "suszona krewetka",
|
||||||
"plural_name": "dried prawns"
|
"plural_name": "suszone krewetki"
|
||||||
},
|
},
|
||||||
"dulse seaweed": {
|
"dulse seaweed": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -6215,8 +6215,8 @@
|
||||||
"dried chili": {
|
"dried chili": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried chili",
|
"name": "suszone chili",
|
||||||
"plural_name": "dried chilies"
|
"plural_name": "suszone chili"
|
||||||
},
|
},
|
||||||
"black cardamom": {
|
"black cardamom": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
@ -9332,8 +9332,8 @@
|
||||||
"dried pea": {
|
"dried pea": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "dried pea",
|
"name": "suszony groch",
|
||||||
"plural_name": "dried peas"
|
"plural_name": "suszony groch"
|
||||||
},
|
},
|
||||||
"pink bean": {
|
"pink bean": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|
|
@ -19,6 +19,8 @@ from mealie.routes._base.routers import UserAPIRouter
|
||||||
from mealie.schema.user import PrivateUser
|
from mealie.schema.user import PrivateUser
|
||||||
from mealie.schema.user.auth import CredentialsRequestForm
|
from mealie.schema.user.auth import CredentialsRequestForm
|
||||||
|
|
||||||
|
from .auth_cache import AuthCache
|
||||||
|
|
||||||
public_router = APIRouter(tags=["Users: Authentication"])
|
public_router = APIRouter(tags=["Users: Authentication"])
|
||||||
user_router = UserAPIRouter(tags=["Users: Authentication"])
|
user_router = UserAPIRouter(tags=["Users: Authentication"])
|
||||||
logger = root_logger.get_logger("auth")
|
logger = root_logger.get_logger("auth")
|
||||||
|
@ -27,7 +29,7 @@ remember_me_duration = timedelta(days=14)
|
||||||
|
|
||||||
settings = get_app_settings()
|
settings = get_app_settings()
|
||||||
if settings.OIDC_READY:
|
if settings.OIDC_READY:
|
||||||
oauth = OAuth()
|
oauth = OAuth(cache=AuthCache())
|
||||||
scope = None
|
scope = None
|
||||||
if settings.OIDC_SCOPES_OVERRIDE:
|
if settings.OIDC_SCOPES_OVERRIDE:
|
||||||
scope = settings.OIDC_SCOPES_OVERRIDE
|
scope = settings.OIDC_SCOPES_OVERRIDE
|
||||||
|
|
51
mealie/routes/auth/auth_cache.py
Normal file
51
mealie/routes/auth/auth_cache.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class AuthCache:
|
||||||
|
def __init__(self, threshold: int = 500, default_timeout: float = 300):
|
||||||
|
self.default_timeout = default_timeout
|
||||||
|
self._cache: dict[str, tuple[float, Any]] = {}
|
||||||
|
self.clear = self._cache.clear
|
||||||
|
self._threshold = threshold
|
||||||
|
|
||||||
|
def _prune(self):
|
||||||
|
if len(self._cache) > self._threshold:
|
||||||
|
now = time.time()
|
||||||
|
toremove = []
|
||||||
|
for idx, (key, (expires, _)) in enumerate(self._cache.items()):
|
||||||
|
if (expires != 0 and expires <= now) or idx % 3 == 0:
|
||||||
|
toremove.append(key)
|
||||||
|
for key in toremove:
|
||||||
|
self._cache.pop(key, None)
|
||||||
|
|
||||||
|
def _normalize_timeout(self, timeout: float | None) -> float:
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.default_timeout
|
||||||
|
if timeout > 0:
|
||||||
|
timeout = time.time() + timeout
|
||||||
|
return timeout
|
||||||
|
|
||||||
|
async def get(self, key: str) -> Any:
|
||||||
|
try:
|
||||||
|
expires, value = self._cache[key]
|
||||||
|
if expires == 0 or expires > time.time():
|
||||||
|
return value
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def set(self, key: str, value: Any, timeout: float | None = None) -> bool:
|
||||||
|
expires = self._normalize_timeout(timeout)
|
||||||
|
self._prune()
|
||||||
|
self._cache[key] = (expires, value)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def delete(self, key: str) -> bool:
|
||||||
|
return self._cache.pop(key, None) is not None
|
||||||
|
|
||||||
|
async def has(self, key: str) -> bool:
|
||||||
|
try:
|
||||||
|
expires, value = self._cache[key]
|
||||||
|
return expires == 0 or expires > time.time()
|
||||||
|
except KeyError:
|
||||||
|
return False
|
|
@ -5,7 +5,7 @@ from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.routes._base import controller
|
from mealie.routes._base import controller
|
||||||
from mealie.routes._base.base_controllers import BasePublicHouseholdExploreController
|
from mealie.routes._base.base_controllers import BasePublicHouseholdExploreController
|
||||||
from mealie.schema.cookbook.cookbook import ReadCookBook, RecipeCookBook
|
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||||
from mealie.schema.make_dependable import make_dependable
|
from mealie.schema.make_dependable import make_dependable
|
||||||
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@ class PublicCookbooksController(BasePublicHouseholdExploreController):
|
||||||
response.set_pagination_guides(self.get_explore_url_path(router.url_path_for("get_all")), q.model_dump())
|
response.set_pagination_guides(self.get_explore_url_path(router.url_path_for("get_all")), q.model_dump())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
@router.get("/{item_id}", response_model=ReadCookBook)
|
||||||
def get_one(self, item_id: UUID4 | str) -> RecipeCookBook:
|
def get_one(self, item_id: UUID4 | str) -> ReadCookBook:
|
||||||
NOT_FOUND_EXCEPTION = HTTPException(404, "cookbook not found")
|
NOT_FOUND_EXCEPTION = HTTPException(404, "cookbook not found")
|
||||||
if isinstance(item_id, UUID):
|
if isinstance(item_id, UUID):
|
||||||
match_attr = "id"
|
match_attr = "id"
|
||||||
|
@ -58,13 +58,4 @@ class PublicCookbooksController(BasePublicHouseholdExploreController):
|
||||||
if not household or household.preferences.private_household:
|
if not household or household.preferences.private_household:
|
||||||
raise NOT_FOUND_EXCEPTION
|
raise NOT_FOUND_EXCEPTION
|
||||||
|
|
||||||
cross_household_recipes = self.cross_household_repos.recipes
|
return cookbook
|
||||||
recipes = cross_household_recipes.page_all(
|
|
||||||
PaginationQuery(
|
|
||||||
page=1,
|
|
||||||
per_page=-1,
|
|
||||||
query_filter="settings.public = TRUE AND household.preferences.privateHousehold = FALSE",
|
|
||||||
),
|
|
||||||
cookbook=cookbook,
|
|
||||||
)
|
|
||||||
return cookbook.cast(RecipeCookBook, recipes=recipes.items)
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from mealie.routes._base import BaseCrudController, controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import MealieCrudRoute
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, SaveCookBook, UpdateCookBook
|
||||||
from mealie.schema.cookbook.cookbook import CookBookPagination
|
from mealie.schema.cookbook.cookbook import CookBookPagination
|
||||||
from mealie.schema.response.pagination import PaginationQuery
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.services.event_bus_service.event_types import (
|
from mealie.services.event_bus_service.event_types import (
|
||||||
|
@ -101,7 +101,7 @@ class GroupCookbookController(BaseCrudController):
|
||||||
|
|
||||||
return all_updated
|
return all_updated
|
||||||
|
|
||||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
@router.get("/{item_id}", response_model=ReadCookBook)
|
||||||
def get_one(self, item_id: UUID4 | str):
|
def get_one(self, item_id: UUID4 | str):
|
||||||
if isinstance(item_id, UUID):
|
if isinstance(item_id, UUID):
|
||||||
match_attr = "id"
|
match_attr = "id"
|
||||||
|
@ -114,12 +114,10 @@ class GroupCookbookController(BaseCrudController):
|
||||||
|
|
||||||
# Allow fetching other households' cookbooks
|
# Allow fetching other households' cookbooks
|
||||||
cookbook = self.group_cookbooks.get_one(item_id, match_attr)
|
cookbook = self.group_cookbooks.get_one(item_id, match_attr)
|
||||||
|
|
||||||
if cookbook is None:
|
if cookbook is None:
|
||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
|
|
||||||
recipe_pagination = self.repos.recipes.page_all(PaginationQuery(page=1, per_page=-1, cookbook=cookbook))
|
return cookbook
|
||||||
return cookbook.cast(RecipeCookBook, recipes=recipe_pagination.items)
|
|
||||||
|
|
||||||
@router.put("/{item_id}", response_model=ReadCookBook)
|
@router.put("/{item_id}", response_model=ReadCookBook)
|
||||||
def update_one(self, item_id: str, data: CreateCookBook):
|
def update_one(self, item_id: str, data: CreateCookBook):
|
||||||
|
|
|
@ -523,12 +523,12 @@ class RecipeController(BaseRecipeController):
|
||||||
|
|
||||||
@router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"])
|
@router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"])
|
||||||
def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)):
|
def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)):
|
||||||
recipe = self.mixins.get_one(slug)
|
try:
|
||||||
data_service = RecipeDataService(recipe.id)
|
new_version = self.service.update_recipe_image(slug, image, extension)
|
||||||
data_service.write_image(image, extension)
|
|
||||||
|
|
||||||
new_version = self.recipes.update_image(slug, extension)
|
|
||||||
return UpdateImageResponse(image=new_version)
|
return UpdateImageResponse(image=new_version)
|
||||||
|
except Exception as e:
|
||||||
|
self.handle_exceptions(e)
|
||||||
|
return None
|
||||||
|
|
||||||
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
|
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
|
||||||
def upload_recipe_asset(
|
def upload_recipe_asset(
|
||||||
|
@ -550,7 +550,7 @@ class RecipeController(BaseRecipeController):
|
||||||
file_name = f"{file_slug}.{extension}"
|
file_name = f"{file_slug}.{extension}"
|
||||||
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
||||||
|
|
||||||
recipe = self.mixins.get_one(slug)
|
recipe = self.service.get_one(slug)
|
||||||
|
|
||||||
dest = recipe.asset_dir / file_name
|
dest = recipe.asset_dir / file_name
|
||||||
|
|
||||||
|
@ -567,9 +567,9 @@ class RecipeController(BaseRecipeController):
|
||||||
if not dest.is_file():
|
if not dest.is_file():
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
recipe = self.mixins.get_one(slug)
|
if recipe.assets is not None:
|
||||||
recipe.assets.append(asset_in)
|
recipe.assets.append(asset_in)
|
||||||
|
|
||||||
self.mixins.update_one(recipe, slug)
|
self.service.update_one(slug, recipe)
|
||||||
|
|
||||||
return asset_in
|
return asset_in
|
||||||
|
|
|
@ -3,11 +3,11 @@ from .datetime_parse import DateError, DateTimeError, DurationError, TimeError
|
||||||
from .mealie_model import HasUUID, MealieModel, SearchType
|
from .mealie_model import HasUUID, MealieModel, SearchType
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"HasUUID",
|
|
||||||
"MealieModel",
|
|
||||||
"SearchType",
|
|
||||||
"DateError",
|
"DateError",
|
||||||
"DateTimeError",
|
"DateTimeError",
|
||||||
"DurationError",
|
"DurationError",
|
||||||
"TimeError",
|
"TimeError",
|
||||||
|
"HasUUID",
|
||||||
|
"MealieModel",
|
||||||
|
"SearchType",
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,10 +18,28 @@ from .restore import (
|
||||||
from .settings import CustomPageBase, CustomPageOut
|
from .settings import CustomPageBase, CustomPageOut
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"MaintenanceLogs",
|
||||||
|
"MaintenanceStorageDetails",
|
||||||
|
"MaintenanceSummary",
|
||||||
"ChowdownURL",
|
"ChowdownURL",
|
||||||
"MigrationFile",
|
"MigrationFile",
|
||||||
"MigrationImport",
|
"MigrationImport",
|
||||||
"Migrations",
|
"Migrations",
|
||||||
|
"CustomPageBase",
|
||||||
|
"CustomPageOut",
|
||||||
|
"CommentImport",
|
||||||
|
"CustomPageImport",
|
||||||
|
"GroupImport",
|
||||||
|
"ImportBase",
|
||||||
|
"NotificationImport",
|
||||||
|
"RecipeImport",
|
||||||
|
"SettingsImport",
|
||||||
|
"UserImport",
|
||||||
|
"AllBackups",
|
||||||
|
"BackupFile",
|
||||||
|
"BackupOptions",
|
||||||
|
"CreateBackup",
|
||||||
|
"ImportJob",
|
||||||
"AdminAboutInfo",
|
"AdminAboutInfo",
|
||||||
"AppInfo",
|
"AppInfo",
|
||||||
"AppStartupInfo",
|
"AppStartupInfo",
|
||||||
|
@ -31,23 +49,5 @@ __all__ = [
|
||||||
"EmailReady",
|
"EmailReady",
|
||||||
"EmailSuccess",
|
"EmailSuccess",
|
||||||
"EmailTest",
|
"EmailTest",
|
||||||
"CustomPageBase",
|
|
||||||
"CustomPageOut",
|
|
||||||
"AllBackups",
|
|
||||||
"BackupFile",
|
|
||||||
"BackupOptions",
|
|
||||||
"CreateBackup",
|
|
||||||
"ImportJob",
|
|
||||||
"MaintenanceLogs",
|
|
||||||
"MaintenanceStorageDetails",
|
|
||||||
"MaintenanceSummary",
|
|
||||||
"DebugResponse",
|
"DebugResponse",
|
||||||
"CommentImport",
|
|
||||||
"CustomPageImport",
|
|
||||||
"GroupImport",
|
|
||||||
"ImportBase",
|
|
||||||
"NotificationImport",
|
|
||||||
"RecipeImport",
|
|
||||||
"SettingsImport",
|
|
||||||
"UserImport",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
# This file is auto-generated by gen_schema_exports.py
|
# This file is auto-generated by gen_schema_exports.py
|
||||||
from .cookbook import CookBookPagination, CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
from .cookbook import CookBookPagination, CreateCookBook, ReadCookBook, SaveCookBook, UpdateCookBook
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CookBookPagination",
|
"CookBookPagination",
|
||||||
"CreateCookBook",
|
"CreateCookBook",
|
||||||
"ReadCookBook",
|
"ReadCookBook",
|
||||||
"RecipeCookBook",
|
|
||||||
"SaveCookBook",
|
"SaveCookBook",
|
||||||
"UpdateCookBook",
|
"UpdateCookBook",
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,7 +7,6 @@ from slugify import slugify
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.models.recipe import RecipeModel
|
from mealie.db.models.recipe import RecipeModel
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema.recipe.recipe import RecipeSummary
|
|
||||||
from mealie.schema.response.pagination import PaginationBase
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
from mealie.schema.response.query_filter import QueryFilterBuilder, QueryFilterJSON
|
from mealie.schema.response.query_filter import QueryFilterBuilder, QueryFilterJSON
|
||||||
|
|
||||||
|
@ -84,10 +83,3 @@ class ReadCookBook(UpdateCookBook):
|
||||||
|
|
||||||
class CookBookPagination(PaginationBase):
|
class CookBookPagination(PaginationBase):
|
||||||
items: list[ReadCookBook]
|
items: list[ReadCookBook]
|
||||||
|
|
||||||
|
|
||||||
class RecipeCookBook(ReadCookBook):
|
|
||||||
group_id: UUID4
|
|
||||||
household_id: UUID4
|
|
||||||
recipes: list[RecipeSummary]
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
|
@ -7,13 +7,13 @@ from .group_seeder import SeederConfig
|
||||||
from .group_statistics import GroupStorage
|
from .group_statistics import GroupStorage
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GroupAdminUpdate",
|
|
||||||
"GroupStorage",
|
|
||||||
"GroupDataExport",
|
"GroupDataExport",
|
||||||
"SeederConfig",
|
|
||||||
"CreateGroupPreferences",
|
"CreateGroupPreferences",
|
||||||
"ReadGroupPreferences",
|
"ReadGroupPreferences",
|
||||||
"UpdateGroupPreferences",
|
"UpdateGroupPreferences",
|
||||||
|
"GroupStorage",
|
||||||
"DataMigrationCreate",
|
"DataMigrationCreate",
|
||||||
"SupportedMigrations",
|
"SupportedMigrations",
|
||||||
|
"SeederConfig",
|
||||||
|
"GroupAdminUpdate",
|
||||||
]
|
]
|
||||||
|
|
|
@ -70,6 +70,49 @@ from .invite_token import CreateInviteToken, EmailInitationResponse, EmailInvita
|
||||||
from .webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination, WebhookType
|
from .webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination, WebhookType
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"GroupEventNotifierCreate",
|
||||||
|
"GroupEventNotifierOptions",
|
||||||
|
"GroupEventNotifierOptionsOut",
|
||||||
|
"GroupEventNotifierOptionsSave",
|
||||||
|
"GroupEventNotifierOut",
|
||||||
|
"GroupEventNotifierPrivate",
|
||||||
|
"GroupEventNotifierSave",
|
||||||
|
"GroupEventNotifierUpdate",
|
||||||
|
"GroupEventPagination",
|
||||||
|
"CreateGroupRecipeAction",
|
||||||
|
"GroupRecipeActionOut",
|
||||||
|
"GroupRecipeActionPagination",
|
||||||
|
"GroupRecipeActionPayload",
|
||||||
|
"GroupRecipeActionType",
|
||||||
|
"SaveGroupRecipeAction",
|
||||||
|
"CreateWebhook",
|
||||||
|
"ReadWebhook",
|
||||||
|
"SaveWebhook",
|
||||||
|
"WebhookPagination",
|
||||||
|
"WebhookType",
|
||||||
|
"CreateHouseholdPreferences",
|
||||||
|
"ReadHouseholdPreferences",
|
||||||
|
"SaveHouseholdPreferences",
|
||||||
|
"UpdateHouseholdPreferences",
|
||||||
|
"HouseholdCreate",
|
||||||
|
"HouseholdInDB",
|
||||||
|
"HouseholdPagination",
|
||||||
|
"HouseholdRecipeBase",
|
||||||
|
"HouseholdRecipeCreate",
|
||||||
|
"HouseholdRecipeOut",
|
||||||
|
"HouseholdRecipeSummary",
|
||||||
|
"HouseholdRecipeUpdate",
|
||||||
|
"HouseholdSave",
|
||||||
|
"HouseholdSummary",
|
||||||
|
"HouseholdUserSummary",
|
||||||
|
"UpdateHousehold",
|
||||||
|
"UpdateHouseholdAdmin",
|
||||||
|
"HouseholdStatistics",
|
||||||
|
"CreateInviteToken",
|
||||||
|
"EmailInitationResponse",
|
||||||
|
"EmailInvitation",
|
||||||
|
"ReadInviteToken",
|
||||||
|
"SaveInviteToken",
|
||||||
"ShoppingListAddRecipeParams",
|
"ShoppingListAddRecipeParams",
|
||||||
"ShoppingListAddRecipeParamsBulk",
|
"ShoppingListAddRecipeParamsBulk",
|
||||||
"ShoppingListCreate",
|
"ShoppingListCreate",
|
||||||
|
@ -93,48 +136,5 @@ __all__ = [
|
||||||
"ShoppingListSave",
|
"ShoppingListSave",
|
||||||
"ShoppingListSummary",
|
"ShoppingListSummary",
|
||||||
"ShoppingListUpdate",
|
"ShoppingListUpdate",
|
||||||
"GroupEventNotifierCreate",
|
|
||||||
"GroupEventNotifierOptions",
|
|
||||||
"GroupEventNotifierOptionsOut",
|
|
||||||
"GroupEventNotifierOptionsSave",
|
|
||||||
"GroupEventNotifierOut",
|
|
||||||
"GroupEventNotifierPrivate",
|
|
||||||
"GroupEventNotifierSave",
|
|
||||||
"GroupEventNotifierUpdate",
|
|
||||||
"GroupEventPagination",
|
|
||||||
"CreateGroupRecipeAction",
|
|
||||||
"GroupRecipeActionOut",
|
|
||||||
"GroupRecipeActionPagination",
|
|
||||||
"GroupRecipeActionPayload",
|
|
||||||
"GroupRecipeActionType",
|
|
||||||
"SaveGroupRecipeAction",
|
|
||||||
"CreateHouseholdPreferences",
|
|
||||||
"ReadHouseholdPreferences",
|
|
||||||
"SaveHouseholdPreferences",
|
|
||||||
"UpdateHouseholdPreferences",
|
|
||||||
"SetPermissions",
|
"SetPermissions",
|
||||||
"CreateInviteToken",
|
|
||||||
"EmailInitationResponse",
|
|
||||||
"EmailInvitation",
|
|
||||||
"ReadInviteToken",
|
|
||||||
"SaveInviteToken",
|
|
||||||
"HouseholdStatistics",
|
|
||||||
"CreateWebhook",
|
|
||||||
"ReadWebhook",
|
|
||||||
"SaveWebhook",
|
|
||||||
"WebhookPagination",
|
|
||||||
"WebhookType",
|
|
||||||
"HouseholdCreate",
|
|
||||||
"HouseholdInDB",
|
|
||||||
"HouseholdPagination",
|
|
||||||
"HouseholdRecipeBase",
|
|
||||||
"HouseholdRecipeCreate",
|
|
||||||
"HouseholdRecipeOut",
|
|
||||||
"HouseholdRecipeSummary",
|
|
||||||
"HouseholdRecipeUpdate",
|
|
||||||
"HouseholdSave",
|
|
||||||
"HouseholdSummary",
|
|
||||||
"HouseholdUserSummary",
|
|
||||||
"UpdateHousehold",
|
|
||||||
"UpdateHouseholdAdmin",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,6 +12,9 @@ from .plan_rules import PlanRulesCreate, PlanRulesDay, PlanRulesOut, PlanRulesPa
|
||||||
from .shopping_list import ListItem, ShoppingListIn, ShoppingListOut
|
from .shopping_list import ListItem, ShoppingListIn, ShoppingListOut
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"ListItem",
|
||||||
|
"ShoppingListIn",
|
||||||
|
"ShoppingListOut",
|
||||||
"CreatePlanEntry",
|
"CreatePlanEntry",
|
||||||
"CreateRandomEntry",
|
"CreateRandomEntry",
|
||||||
"PlanEntryPagination",
|
"PlanEntryPagination",
|
||||||
|
@ -19,9 +22,6 @@ __all__ = [
|
||||||
"ReadPlanEntry",
|
"ReadPlanEntry",
|
||||||
"SavePlanEntry",
|
"SavePlanEntry",
|
||||||
"UpdatePlanEntry",
|
"UpdatePlanEntry",
|
||||||
"ListItem",
|
|
||||||
"ShoppingListIn",
|
|
||||||
"ShoppingListOut",
|
|
||||||
"PlanRulesCreate",
|
"PlanRulesCreate",
|
||||||
"PlanRulesDay",
|
"PlanRulesDay",
|
||||||
"PlanRulesOut",
|
"PlanRulesOut",
|
||||||
|
|
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