mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 22:43:34 -07:00
Merge branch 'mealie-next' into feat/consolidate-admin-apis
This commit is contained in:
commit
7f5880ea64
84 changed files with 855 additions and 2363 deletions
|
@ -12,7 +12,7 @@ repos:
|
||||||
exclude: ^tests/data/
|
exclude: ^tests/data/
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.11.13
|
rev: v0.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
|
@ -156,8 +156,6 @@ Setting the following environmental variables will change the theme of the front
|
||||||
|
|
||||||
### Docker Secrets
|
### Docker Secrets
|
||||||
|
|
||||||
### Docker Secrets
|
|
||||||
|
|
||||||
> <super>†</super> Starting in version `2.4.2`, any environment variable in the preceding lists with a dagger
|
> <super>†</super> Starting in version `2.4.2`, any environment variable in the preceding lists with a dagger
|
||||||
> symbol next to them support the Docker Compose secrets pattern, below.
|
> symbol next to them support the Docker Compose secrets pattern, below.
|
||||||
[Docker Compose secrets][docker-secrets] can be used to secure sensitive information regarding the Mealie implementation
|
[Docker Compose secrets][docker-secrets] can be used to secure sensitive information regarding the Mealie implementation
|
||||||
|
|
|
@ -24,8 +24,7 @@
|
||||||
|
|
||||||
<v-container
|
<v-container
|
||||||
v-if="book"
|
v-if="book"
|
||||||
fluid
|
class="my-0"
|
||||||
class="py-0 my-0"
|
|
||||||
>
|
>
|
||||||
<v-sheet
|
<v-sheet
|
||||||
color="transparent"
|
color="transparent"
|
||||||
|
@ -33,13 +32,12 @@
|
||||||
elevation="0"
|
elevation="0"
|
||||||
>
|
>
|
||||||
<div class="d-flex align-center w-100 mb-2">
|
<div class="d-flex align-center w-100 mb-2">
|
||||||
|
<v-toolbar-title class="headline mb-0">
|
||||||
<v-icon size="large" class="mr-3">
|
<v-icon size="large" class="mr-3">
|
||||||
{{ $globals.icons.pages }}
|
{{ $globals.icons.pages }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<v-toolbar-title class="headline mb-0">
|
|
||||||
{{ book.name }}
|
{{ book.name }}
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<v-spacer />
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
class="mx-1"
|
class="mx-1"
|
||||||
|
@ -70,7 +68,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useLazyRecipes } from "~/composables/recipes";
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useCookbook, useCookbooks } from "~/composables/use-group-cookbooks";
|
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
|
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 { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||||
|
@ -87,7 +86,7 @@ export default defineNuxtComponent({
|
||||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const slug = route.params.slug as string;
|
const slug = route.params.slug as string;
|
||||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const { actions } = useCookbooks();
|
const { actions } = useCookbookStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const tab = ref(null);
|
const tab = ref(null);
|
||||||
|
|
|
@ -69,22 +69,22 @@ export default defineNuxtComponent({
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const MEAL_TYPE_OPTIONS = [
|
const MEAL_TYPE_OPTIONS = [
|
||||||
{ text: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
{ title: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||||
{ text: i18n.t("meal-plan.lunch"), value: "lunch" },
|
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||||
{ text: i18n.t("meal-plan.dinner"), value: "dinner" },
|
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||||
{ text: i18n.t("meal-plan.side"), value: "side" },
|
{ title: i18n.t("meal-plan.side"), value: "side" },
|
||||||
{ text: i18n.t("meal-plan.type-any"), value: "unset" },
|
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const MEAL_DAY_OPTIONS = [
|
const MEAL_DAY_OPTIONS = [
|
||||||
{ text: i18n.t("general.monday"), value: "monday" },
|
{ title: i18n.t("general.monday"), value: "monday" },
|
||||||
{ text: i18n.t("general.tuesday"), value: "tuesday" },
|
{ title: i18n.t("general.tuesday"), value: "tuesday" },
|
||||||
{ text: i18n.t("general.wednesday"), value: "wednesday" },
|
{ title: i18n.t("general.wednesday"), value: "wednesday" },
|
||||||
{ text: i18n.t("general.thursday"), value: "thursday" },
|
{ title: i18n.t("general.thursday"), value: "thursday" },
|
||||||
{ text: i18n.t("general.friday"), value: "friday" },
|
{ title: i18n.t("general.friday"), value: "friday" },
|
||||||
{ text: i18n.t("general.saturday"), value: "saturday" },
|
{ title: i18n.t("general.saturday"), value: "saturday" },
|
||||||
{ text: i18n.t("general.sunday"), value: "sunday" },
|
{ title: i18n.t("general.sunday"), value: "sunday" },
|
||||||
{ text: i18n.t("meal-plan.day-any"), value: "unset" },
|
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const inputDay = computed({
|
const inputDay = computed({
|
||||||
|
|
|
@ -189,6 +189,7 @@
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
|
@ -198,6 +199,7 @@
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
|
@ -207,6 +209,7 @@
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
|
@ -216,6 +219,7 @@
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
|
@ -225,6 +229,7 @@
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
:size="$vuetify.display.xs ? 'small' : undefined"
|
:size="$vuetify.display.xs ? 'small' : undefined"
|
||||||
:color="btn.color"
|
:color="btn.color"
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
|
:icon="$vuetify.display.xs"
|
||||||
@click="emitHandler(btn.event)"
|
@click="emitHandler(btn.event)"
|
||||||
>
|
>
|
||||||
<v-icon :left="!$vuetify.display.xs">
|
<v-icon :left="!$vuetify.display.xs">
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
v-for="category in items.slice(0, limit)"
|
v-for="category in items.slice(0, limit)"
|
||||||
:key="category.name"
|
:key="category.name"
|
||||||
label
|
label
|
||||||
class="ma-1"
|
class="mr-1 mt-1"
|
||||||
color="accent"
|
color="accent"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
:size="small ? 'small' : 'default'"
|
:size="small ? 'small' : 'default'"
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
id="arrow-search"
|
id="arrow-search"
|
||||||
v-model="search.query.value"
|
v-model="search.query.value"
|
||||||
autofocus
|
autofocus
|
||||||
variant="solo-filled"
|
variant="solo"
|
||||||
flat
|
flat
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
bg-color="primary-lighten-1"
|
bg-color="primary-lighten-1"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container
|
<v-container
|
||||||
fluid
|
fluid
|
||||||
class="pa-0"
|
class="px-0"
|
||||||
>
|
>
|
||||||
<div class="search-container pb-8">
|
<div class="search-container pb-8">
|
||||||
<form
|
<form
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
clearable
|
clearable
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
>
|
>
|
||||||
<template #append-outer>
|
<template #append>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ma-0 pa-0 text-subtitle-1 dense-markdown ingredient-item">
|
<div class="text-subtitle-1 dense-markdown ingredient-item">
|
||||||
<SafeMarkdown
|
<SafeMarkdown
|
||||||
v-if="parsedIng.quantity"
|
v-if="parsedIng.quantity"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
</template>
|
</template>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
density="compact"
|
density="compact"
|
||||||
|
class="pa-0"
|
||||||
@click.stop="toggleChecked(index)"
|
@click.stop="toggleChecked(index)"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
<v-app-bar
|
<v-app-bar
|
||||||
color="transparent"
|
color="transparent"
|
||||||
flat
|
flat
|
||||||
class="mt-n1 rounded align-center px-4 position-relative w-100 left-0 top-0"
|
class="mt-n1 rounded align-center position-relative w-100 left-0 top-0"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
size="large"
|
size="large"
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
closable-chips
|
closable-chips
|
||||||
item-title="name"
|
item-title="name"
|
||||||
multiple
|
multiple
|
||||||
variant="underlined"
|
:variant="variant"
|
||||||
:prepend-inner-icon="icon"
|
:prepend-inner-icon="icon"
|
||||||
:append-icon="$globals.icons.create"
|
:append-icon="showAdd ? $globals.icons.create : undefined"
|
||||||
return-object
|
return-object
|
||||||
auto-select-first
|
auto-select-first
|
||||||
class="pa-0"
|
class="pa-0"
|
||||||
|
@ -93,6 +93,10 @@ export default defineNuxtComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
variant: {
|
||||||
|
type: String as () => "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled",
|
||||||
|
default: "outlined",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-container v-show="!isCookMode" key="recipe-page" class="pt-0" :class="{ 'pa-0': $vuetify.display.smAndDown.value }">
|
<v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown.value }">
|
||||||
<v-card :flat="$vuetify.display.smAndDown.value" class="d-print-none">
|
<v-card :flat="$vuetify.display.smAndDown.value" class="d-print-none">
|
||||||
<RecipePageHeader
|
<RecipePageHeader
|
||||||
:recipe="recipe"
|
:recipe="recipe"
|
||||||
|
@ -87,11 +87,10 @@
|
||||||
/>
|
/>
|
||||||
<RecipePrintContainer :recipe="recipe" :scale="scale" />
|
<RecipePrintContainer :recipe="recipe" :scale="scale" />
|
||||||
</v-container>
|
</v-container>
|
||||||
<!-- Cook mode displayes two columns with ingredients and instructions side by side, each being scrolled individually, allowing to view both at the same timer -->
|
<!-- Cook mode displayes two columns with ingredients and instructions side by side, each being scrolled individually, allowing to view both at the same time -->
|
||||||
<v-sheet
|
<v-sheet
|
||||||
v-show="isCookMode && !hasLinkedIngredients"
|
v-show="isCookMode && !hasLinkedIngredients"
|
||||||
key="cookmode"
|
key="cookmode"
|
||||||
:style="{ height: $vuetify.display.smAndUp ? 'calc(100vh - 48px)' : '' }"
|
|
||||||
>
|
>
|
||||||
<!-- the calc is to account for the toolbar a more dynamic solution could be needed -->
|
<!-- the calc is to account for the toolbar a more dynamic solution could be needed -->
|
||||||
<v-row style="height: 100%" no-gutters class="overflow-hidden">
|
<v-row style="height: 100%" no-gutters class="overflow-hidden">
|
||||||
|
@ -107,7 +106,12 @@
|
||||||
/>
|
/>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="overflow-y-auto py-2" style="height: 100%" cols="12" sm="7">
|
<v-col class="overflow-y-auto"
|
||||||
|
:class="$vuetify.display.smAndDown.value ? 'py-2': 'py-6'"
|
||||||
|
style="height: 100%" cols="12" sm="7">
|
||||||
|
<h2 class="text-h5 px-4 font-weight-medium opacity-80">
|
||||||
|
{{ $t('recipe.instructions') }}
|
||||||
|
</h2>
|
||||||
<RecipePageInstructions
|
<RecipePageInstructions
|
||||||
v-model="recipe.recipeInstructions"
|
v-model="recipe.recipeInstructions"
|
||||||
v-model:assets="recipe.assets"
|
v-model:assets="recipe.assets"
|
||||||
|
|
|
@ -1,31 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex justify-start align-top py-2">
|
<div class="d-flex justify-start align-top flex-wrap">
|
||||||
<RecipeImageUploadBtn
|
<RecipeImageUploadBtn
|
||||||
class="my-1"
|
class="my-2"
|
||||||
:slug="recipe.slug"
|
:slug="recipe.slug"
|
||||||
@upload="uploadImage"
|
@upload="uploadImage"
|
||||||
@refresh="imageKey++"
|
@refresh="imageKey++"
|
||||||
/>
|
/>
|
||||||
<RecipeSettingsMenu
|
<RecipeSettingsMenu
|
||||||
v-model="recipe.settings"
|
v-model="recipe.settings"
|
||||||
class="my-1 mx-1"
|
class="my-2 mx-1"
|
||||||
:is-owner="recipe.userId == user.id"
|
:is-owner="recipe.userId == user.id"
|
||||||
@upload="uploadImage"
|
@upload="uploadImage"
|
||||||
/>
|
/>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-container
|
|
||||||
class="py-0"
|
|
||||||
style="width: 40%;"
|
|
||||||
>
|
|
||||||
<v-select
|
<v-select
|
||||||
v-model="recipe.userId"
|
v-model="recipe.userId"
|
||||||
|
class="my-2"
|
||||||
|
max-width="300"
|
||||||
:items="allUsers"
|
:items="allUsers"
|
||||||
item-title="fullName"
|
:item-props="itemsProps"
|
||||||
item-value="id"
|
|
||||||
:label="$t('general.owner')"
|
:label="$t('general.owner')"
|
||||||
hide-details
|
|
||||||
:disabled="!canEditOwner"
|
:disabled="!canEditOwner"
|
||||||
variant="underlined"
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
|
@ -34,17 +31,7 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-select>
|
</v-select>
|
||||||
<v-card-text
|
</div>
|
||||||
v-if="ownerHousehold"
|
|
||||||
class="pa-0 d-flex"
|
|
||||||
style="align-items: flex-end;"
|
|
||||||
>
|
|
||||||
<v-spacer />
|
|
||||||
<v-icon>{{ $globals.icons.household }}</v-icon>
|
|
||||||
<span class="pl-1">{{ ownerHousehold.name }}</span>
|
|
||||||
</v-card-text>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -71,13 +58,15 @@ const canEditOwner = computed(() => {
|
||||||
|
|
||||||
const { store: allUsers } = useUserStore();
|
const { store: allUsers } = useUserStore();
|
||||||
const { store: households } = useHouseholdStore();
|
const { store: households } = useHouseholdStore();
|
||||||
const ownerHousehold = computed(() => {
|
|
||||||
const owner = allUsers.value.find(u => u.id === recipe.value.userId);
|
function itemsProps(item: any) {
|
||||||
if (!owner) {
|
const owner = allUsers.value.find(u => u.id === item.id);
|
||||||
return null;
|
return {
|
||||||
}
|
value: item.id,
|
||||||
return households.value.find(h => h.id === owner.householdId);
|
title: item.fullName,
|
||||||
});
|
subtitle: owner ? households.value.find(h => h.id === owner.householdId)?.name || "" : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadImage(fileObject: File) {
|
async function uploadImage(fileObject: File) {
|
||||||
if (!recipe.value || !recipe.value.slug) {
|
if (!recipe.value || !recipe.value.slug) {
|
||||||
|
|
|
@ -11,15 +11,17 @@
|
||||||
class="d-flex flex-column justify-center align-center"
|
class="d-flex flex-column justify-center align-center"
|
||||||
>
|
>
|
||||||
<v-card-text class="w-100">
|
<v-card-text class="w-100">
|
||||||
<v-card-title class="text-h5 font-weight-regular pa-0 d-flex flex-column align-center justify-center opacity-80">
|
<div class="d-flex flex-column align-center">
|
||||||
|
<v-card-title class="text-h5 font-weight-regular pa-0 text-wrap text-center opacity-80">
|
||||||
{{ recipe.name }}
|
{{ recipe.name }}
|
||||||
|
</v-card-title>
|
||||||
<RecipeRating
|
<RecipeRating
|
||||||
:key="recipe.slug"
|
:key="recipe.slug"
|
||||||
:value="recipe.rating"
|
:value="recipe.rating"
|
||||||
:recipe-id="recipe.id"
|
:recipe-id="recipe.id"
|
||||||
:slug="recipe.slug"
|
:slug="recipe.slug"
|
||||||
/>
|
/>
|
||||||
</v-card-title>
|
</div>
|
||||||
<v-divider class="my-2" />
|
<v-divider class="my-2" />
|
||||||
<SafeMarkdown :source="recipe.description" class="my-3" />
|
<SafeMarkdown :source="recipe.description" class="my-3" />
|
||||||
<v-divider v-if="recipe.description" />
|
<v-divider v-if="recipe.description" />
|
||||||
|
@ -52,7 +54,7 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-6">
|
<div v-if="recipe.prepTime || recipe.totalTime || recipe.performTime" class="mx-6">
|
||||||
<RecipeTimeCard
|
<RecipeTimeCard
|
||||||
container-class="d-flex flex-wrap justify-center"
|
container-class="d-flex flex-wrap justify-center"
|
||||||
:prep-time="recipe.prepTime"
|
:prep-time="recipe.prepTime"
|
||||||
|
|
|
@ -15,12 +15,13 @@
|
||||||
v-for="(tool, index) in recipe.tools"
|
v-for="(tool, index) in recipe.tools"
|
||||||
:key="index"
|
:key="index"
|
||||||
density="compact"
|
density="compact"
|
||||||
|
class="px-1"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="recipeTools[index].onHand"
|
v-model="recipeTools[index].onHand"
|
||||||
hide-details
|
hide-details
|
||||||
class="pt-0 my-auto py-auto"
|
class="pt-0 py-auto"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
density="compact"
|
density="compact"
|
||||||
@change="updateTool(index)"
|
@change="updateTool(index)"
|
||||||
|
|
|
@ -356,8 +356,9 @@
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||||
import RecipeIngredientHtml from "../../RecipeIngredientHtml.vue";
|
import RecipeIngredientHtml from "../../RecipeIngredientHtml.vue";
|
||||||
import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe";
|
import type { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe";
|
||||||
import { parseIngredientText } from "~/composables/recipes";
|
import { parseIngredientText } from "~/composables/recipes";
|
||||||
|
@ -376,121 +377,92 @@ interface MergerHistory {
|
||||||
sourceText: string;
|
sourceText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const instructionList = defineModel<RecipeStep[]>("modelValue", { required: true, default: () => [] });
|
||||||
components: {
|
const assets = defineModel<RecipeAsset[]>("assets", { required: true, default: () => [] });
|
||||||
VueDraggable,
|
|
||||||
RecipeIngredientHtml,
|
const props = defineProps({
|
||||||
DropZone,
|
|
||||||
RecipeIngredients,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Array as () => RecipeStep[],
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
recipe: {
|
recipe: {
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
type: Object as () => NoUndefinedField<Recipe>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
assets: {
|
|
||||||
type: Array as () => RecipeAsset[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
scale: {
|
scale: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
emits: ["update:modelValue", "click-instruction-field", "update:assets"],
|
|
||||||
|
|
||||||
setup(props, context) {
|
const emit = defineEmits(["click-instruction-field", "update:assets"]);
|
||||||
const i18n = useI18n();
|
|
||||||
const BASE_URL = useRequestURL().origin;
|
|
||||||
|
|
||||||
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
const BASE_URL = useRequestURL().origin;
|
||||||
|
|
||||||
const state = reactive({
|
const { isCookMode, toggleCookMode, isEditForm } = usePageState(props.recipe.slug);
|
||||||
dialog: false,
|
|
||||||
disabledSteps: [] as number[],
|
|
||||||
unusedIngredients: [] as RecipeIngredient[],
|
|
||||||
usedIngredients: [] as RecipeIngredient[],
|
|
||||||
});
|
|
||||||
|
|
||||||
const showTitleEditor = ref<{ [key: string]: boolean }>({});
|
const dialog = ref(false);
|
||||||
|
const disabledSteps = ref<number[]>([]);
|
||||||
|
const unusedIngredients = ref<RecipeIngredient[]>([]);
|
||||||
|
const usedIngredients = ref<RecipeIngredient[]>([]);
|
||||||
|
|
||||||
const actionEvents = [
|
const showTitleEditor = ref<{ [key: string]: boolean }>({});
|
||||||
{
|
|
||||||
text: i18n.t("recipe.toggle-section") as string,
|
|
||||||
event: "toggle-section",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("recipe.link-ingredients") as string,
|
|
||||||
event: "link-ingredients",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("recipe.merge-above") as string,
|
|
||||||
event: "merge-above",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
// UI State Helpers
|
// UI State Helpers
|
||||||
|
|
||||||
function hasSectionTitle(title: string | undefined) {
|
function hasSectionTitle(title: string | undefined) {
|
||||||
return !(title === null || title === "" || title === undefined);
|
return !(title === null || title === "" || title === undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(props.modelValue, (v) => {
|
watch(instructionList, (v) => {
|
||||||
state.disabledSteps = [];
|
disabledSteps.value = [];
|
||||||
|
|
||||||
v.forEach((element: RecipeStep) => {
|
v.forEach((element: RecipeStep) => {
|
||||||
if (element.id !== undefined) {
|
if (element.id !== undefined) {
|
||||||
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, { deep: true });
|
||||||
|
|
||||||
const showCookMode = ref(false);
|
const showCookMode = ref(false);
|
||||||
|
|
||||||
// Eliminate state with an eager call to watcher?
|
onMounted(() => {
|
||||||
onMounted(() => {
|
instructionList.value.forEach((element: RecipeStep) => {
|
||||||
props.modelValue.forEach((element: RecipeStep) => {
|
|
||||||
if (element.id !== undefined) {
|
if (element.id !== undefined) {
|
||||||
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
showTitleEditor.value[element.id!] = hasSectionTitle(element.title!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// showCookMode.value = false;
|
|
||||||
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
||||||
showCookMode.value = true;
|
showCookMode.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
showTitleEditor.value = { ...showTitleEditor.value };
|
showTitleEditor.value = { ...showTitleEditor.value };
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
function toggleDisabled(stepIndex: number) {
|
if (assets.value === undefined) {
|
||||||
|
emit("update:assets", []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleDisabled(stepIndex: number) {
|
||||||
if (isEditForm.value) {
|
if (isEditForm.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.disabledSteps.includes(stepIndex)) {
|
if (disabledSteps.value.includes(stepIndex)) {
|
||||||
const index = state.disabledSteps.indexOf(stepIndex);
|
const index = disabledSteps.value.indexOf(stepIndex);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
state.disabledSteps.splice(index, 1);
|
disabledSteps.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state.disabledSteps.push(stepIndex);
|
disabledSteps.value.push(stepIndex);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isChecked(stepIndex: number) {
|
function isChecked(stepIndex: number) {
|
||||||
if (state.disabledSteps.includes(stepIndex) && !isEditForm.value) {
|
if (disabledSteps.value.includes(stepIndex) && !isEditForm.value) {
|
||||||
return "disabled-card";
|
return "disabled-card";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleShowTitle(id?: string) {
|
function toggleShowTitle(id?: string) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -499,46 +471,35 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
const temp = { ...showTitleEditor.value };
|
const temp = { ...showTitleEditor.value };
|
||||||
showTitleEditor.value = temp;
|
showTitleEditor.value = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instructionList = ref<RecipeStep[]>([...props.modelValue]);
|
function onDragEnd() {
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newVal) => {
|
|
||||||
instructionList.value = [...newVal];
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
function onDragEnd() {
|
|
||||||
context.emit("update:modelValue", [...instructionList.value]);
|
|
||||||
drag.value = false;
|
drag.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
// Ingredient Linker
|
// Ingredient Linker
|
||||||
const activeRefs = ref<string[]>([]);
|
const activeRefs = ref<string[]>([]);
|
||||||
const activeIndex = ref(0);
|
const activeIndex = ref(0);
|
||||||
const activeText = ref("");
|
const activeText = ref("");
|
||||||
|
|
||||||
function openDialog(idx: number, text: string, refs?: IngredientReferences[]) {
|
function openDialog(idx: number, text: string, refs?: IngredientReferences[]) {
|
||||||
if (!refs) {
|
if (!refs) {
|
||||||
instructionList.value[idx].ingredientReferences = [];
|
instructionList.value[idx].ingredientReferences = [];
|
||||||
refs = props.modelValue[idx].ingredientReferences as IngredientReferences[];
|
refs = instructionList.value[idx].ingredientReferences as IngredientReferences[];
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsedIngredients();
|
setUsedIngredients();
|
||||||
activeText.value = text;
|
activeText.value = text;
|
||||||
activeIndex.value = idx;
|
activeIndex.value = idx;
|
||||||
state.dialog = true;
|
dialog.value = true;
|
||||||
activeRefs.value = refs.map(ref => ref.referenceId ?? "");
|
activeRefs.value = refs.map(ref => ref.referenceId ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableNextStep = computed(() => activeIndex.value < props.modelValue.length - 1);
|
const availableNextStep = computed(() => activeIndex.value < instructionList.value.length - 1);
|
||||||
|
|
||||||
function setIngredientIds() {
|
function setIngredientIds() {
|
||||||
const instruction = props.modelValue[activeIndex.value];
|
const instruction = instructionList.value[activeIndex.value];
|
||||||
instruction.ingredientReferences = activeRefs.value.map((ref) => {
|
instruction.ingredientReferences = activeRefs.value.map((ref) => {
|
||||||
return {
|
return {
|
||||||
referenceId: ref,
|
referenceId: ref,
|
||||||
|
@ -547,15 +508,15 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
// Update the visibility of the cook mode button
|
// Update the visibility of the cook mode button
|
||||||
showCookMode.value = false;
|
showCookMode.value = false;
|
||||||
props.modelValue.forEach((element) => {
|
instructionList.value.forEach((element) => {
|
||||||
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
|
||||||
showCookMode.value = true;
|
showCookMode.value = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
state.dialog = false;
|
dialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAndOpenNextLinkIngredients() {
|
function saveAndOpenNextLinkIngredients() {
|
||||||
const currentStepIndex = activeIndex.value;
|
const currentStepIndex = activeIndex.value;
|
||||||
|
|
||||||
if (!availableNextStep.value) {
|
if (!availableNextStep.value) {
|
||||||
|
@ -563,15 +524,15 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
setIngredientIds();
|
setIngredientIds();
|
||||||
const nextStep = props.modelValue[currentStepIndex + 1];
|
const nextStep = instructionList.value[currentStepIndex + 1];
|
||||||
// close dialog before opening to reset the scroll position
|
// close dialog before opening to reset the scroll position
|
||||||
nextTick(() => openDialog(currentStepIndex + 1, nextStep.text, nextStep.ingredientReferences));
|
nextTick(() => openDialog(currentStepIndex + 1, nextStep.text, nextStep.ingredientReferences));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUsedIngredients() {
|
function setUsedIngredients() {
|
||||||
const usedRefs: { [key: string]: boolean } = {};
|
const usedRefs: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
props.modelValue.forEach((element) => {
|
instructionList.value.forEach((element) => {
|
||||||
element.ingredientReferences?.forEach((ref) => {
|
element.ingredientReferences?.forEach((ref) => {
|
||||||
if (ref.referenceId !== undefined) {
|
if (ref.referenceId !== undefined) {
|
||||||
usedRefs[ref.referenceId!] = true;
|
usedRefs[ref.referenceId!] = true;
|
||||||
|
@ -579,25 +540,25 @@ export default defineNuxtComponent({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
state.usedIngredients = props.recipe.recipeIngredient.filter((ing) => {
|
usedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
|
||||||
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
|
return ing.referenceId !== undefined && ing.referenceId in usedRefs;
|
||||||
});
|
});
|
||||||
|
|
||||||
state.unusedIngredients = props.recipe.recipeIngredient.filter((ing) => {
|
unusedIngredients.value = props.recipe.recipeIngredient.filter((ing) => {
|
||||||
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
|
return !(ing.referenceId !== undefined && ing.referenceId in usedRefs);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoSetReferences() {
|
function autoSetReferences() {
|
||||||
useExtractIngredientReferences(
|
useExtractIngredientReferences(
|
||||||
props.recipe.recipeIngredient,
|
props.recipe.recipeIngredient,
|
||||||
activeRefs.value,
|
activeRefs.value,
|
||||||
activeText.value,
|
activeText.value,
|
||||||
props.recipe.settings.disableAmount,
|
props.recipe.settings.disableAmount,
|
||||||
).forEach((ingredient: string) => activeRefs.value.push(ingredient));
|
).forEach((ingredient: string) => activeRefs.value.push(ingredient));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ingredientLookup = computed(() => {
|
const ingredientLookup = computed(() => {
|
||||||
const results: { [key: string]: RecipeIngredient } = {};
|
const results: { [key: string]: RecipeIngredient } = {};
|
||||||
return props.recipe.recipeIngredient.reduce((prev, ing) => {
|
return props.recipe.recipeIngredient.reduce((prev, ing) => {
|
||||||
if (ing.referenceId === undefined) {
|
if (ing.referenceId === undefined) {
|
||||||
|
@ -606,9 +567,9 @@ export default defineNuxtComponent({
|
||||||
prev[ing.referenceId] = ing;
|
prev[ing.referenceId] = ing;
|
||||||
return prev;
|
return prev;
|
||||||
}, results);
|
}, results);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getIngredientByRefId(refId: string | undefined) {
|
function getIngredientByRefId(refId: string | undefined) {
|
||||||
if (refId === undefined) {
|
if (refId === undefined) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -616,13 +577,13 @@ export default defineNuxtComponent({
|
||||||
const ing = ingredientLookup.value[refId];
|
const ing = ingredientLookup.value[refId];
|
||||||
if (!ing) return "";
|
if (!ing) return "";
|
||||||
return parseIngredientText(ing, props.recipe.settings.disableAmount, props.scale);
|
return parseIngredientText(ing, props.recipe.settings.disableAmount, props.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
// Instruction Merger
|
// Instruction Merger
|
||||||
const mergeHistory = ref<MergerHistory[]>([]);
|
const mergeHistory = ref<MergerHistory[]>([]);
|
||||||
|
|
||||||
function mergeAbove(target: number, source: number) {
|
function mergeAbove(target: number, source: number) {
|
||||||
if (target < 0) {
|
if (target < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -630,15 +591,15 @@ export default defineNuxtComponent({
|
||||||
mergeHistory.value.push({
|
mergeHistory.value.push({
|
||||||
target,
|
target,
|
||||||
source,
|
source,
|
||||||
targetText: props.modelValue[target].text,
|
targetText: instructionList.value[target].text,
|
||||||
sourceText: props.modelValue[source].text,
|
sourceText: instructionList.value[source].text,
|
||||||
});
|
});
|
||||||
|
|
||||||
instructionList.value[target].text += " " + props.modelValue[source].text;
|
instructionList.value[target].text += " " + instructionList.value[source].text;
|
||||||
instructionList.value.splice(source, 1);
|
instructionList.value.splice(source, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoMerge(event: KeyboardEvent) {
|
function undoMerge(event: KeyboardEvent) {
|
||||||
if (event.ctrlKey && event.code === "KeyZ") {
|
if (event.ctrlKey && event.code === "KeyZ") {
|
||||||
if (!(mergeHistory.value?.length > 0)) {
|
if (!(mergeHistory.value?.length > 0)) {
|
||||||
return;
|
return;
|
||||||
|
@ -657,30 +618,30 @@ export default defineNuxtComponent({
|
||||||
ingredientReferences: [],
|
ingredientReferences: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveTo(dest: string, source: number) {
|
function moveTo(dest: string, source: number) {
|
||||||
if (dest === "top") {
|
if (dest === "top") {
|
||||||
instructionList.value.unshift(instructionList.value.splice(source, 1)[0]);
|
instructionList.value.unshift(instructionList.value.splice(source, 1)[0]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
instructionList.value.push(instructionList.value.splice(source, 1)[0]);
|
instructionList.value.push(instructionList.value.splice(source, 1)[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert(dest: number) {
|
function insert(dest: number) {
|
||||||
instructionList.value.splice(dest, 0, { id: uuid4(), text: "", title: "", ingredientReferences: [] });
|
instructionList.value.splice(dest, 0, { id: uuid4(), text: "", title: "", ingredientReferences: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewStates = ref<boolean[]>([]);
|
const previewStates = ref<boolean[]>([]);
|
||||||
|
|
||||||
function togglePreviewState(index: number) {
|
function togglePreviewState(index: number) {
|
||||||
const temp = [...previewStates.value];
|
const temp = [...previewStates.value];
|
||||||
temp[index] = !temp[index];
|
temp[index] = !temp[index];
|
||||||
previewStates.value = temp;
|
previewStates.value = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCollapseSection(index: number) {
|
function toggleCollapseSection(index: number) {
|
||||||
const sectionSteps: number[] = [];
|
const sectionSteps: number[] = [];
|
||||||
|
|
||||||
for (let i = index; i < instructionList.value.length; i++) {
|
for (let i = index; i < instructionList.value.length; i++) {
|
||||||
|
@ -692,39 +653,26 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allCollapsed = sectionSteps.every(idx => state.disabledSteps.includes(idx));
|
const allCollapsed = sectionSteps.every(idx => disabledSteps.value.includes(idx));
|
||||||
|
|
||||||
if (allCollapsed) {
|
if (allCollapsed) {
|
||||||
state.disabledSteps = state.disabledSteps.filter(idx => !sectionSteps.includes(idx));
|
disabledSteps.value = disabledSteps.value.filter(idx => !sectionSteps.includes(idx));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state.disabledSteps = [...state.disabledSteps, ...sectionSteps];
|
disabledSteps.value = [...disabledSteps.value, ...sectionSteps];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const drag = ref(false);
|
const drag = ref(false);
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
// Image Uploader
|
// Image Uploader
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const { recipeAssetPath } = useStaticRoutes();
|
const { recipeAssetPath } = useStaticRoutes();
|
||||||
|
|
||||||
const imageUploadMode = ref(false);
|
const loadingStates = ref<{ [key: number]: boolean }>({});
|
||||||
|
|
||||||
function toggleDragMode() {
|
async function handleImageDrop(index: number, files: File[]) {
|
||||||
console.log("Toggling Drag Mode");
|
|
||||||
imageUploadMode.value = !imageUploadMode.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.assets === undefined) {
|
|
||||||
context.emit("update:assets", []);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadingStates = ref<{ [key: number]: boolean }>({});
|
|
||||||
|
|
||||||
async function handleImageDrop(index: number, files: File[]) {
|
|
||||||
if (!files) {
|
if (!files) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -750,13 +698,13 @@ export default defineNuxtComponent({
|
||||||
return; // TODO: Handle error
|
return; // TODO: Handle error
|
||||||
}
|
}
|
||||||
|
|
||||||
context.emit("update:assets", [...props.assets, data]);
|
emit("update:assets", [...assets.value, data]);
|
||||||
const assetUrl = BASE_URL + recipeAssetPath(props.recipe.id, data.fileName as string);
|
const assetUrl = BASE_URL + 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openImageUpload(index: number) {
|
function openImageUpload(index: number) {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = "image/*";
|
input.accept = "image/*";
|
||||||
|
@ -767,52 +715,7 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
const breakpoint = useDisplay();
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Image Uploader
|
|
||||||
toggleDragMode,
|
|
||||||
handleImageDrop,
|
|
||||||
imageUploadMode,
|
|
||||||
openImageUpload,
|
|
||||||
loadingStates,
|
|
||||||
|
|
||||||
// Rest
|
|
||||||
onDragEnd,
|
|
||||||
drag,
|
|
||||||
togglePreviewState,
|
|
||||||
toggleCollapseSection,
|
|
||||||
previewStates,
|
|
||||||
...toRefs(state),
|
|
||||||
actionEvents,
|
|
||||||
activeRefs,
|
|
||||||
activeText,
|
|
||||||
getIngredientByRefId,
|
|
||||||
showTitleEditor,
|
|
||||||
mergeAbove,
|
|
||||||
moveTo,
|
|
||||||
openDialog,
|
|
||||||
setIngredientIds,
|
|
||||||
availableNextStep,
|
|
||||||
saveAndOpenNextLinkIngredients,
|
|
||||||
undoMerge,
|
|
||||||
toggleDisabled,
|
|
||||||
isChecked,
|
|
||||||
toggleShowTitle,
|
|
||||||
instructionList,
|
|
||||||
autoSetReferences,
|
|
||||||
parseIngredientText,
|
|
||||||
toggleCookMode,
|
|
||||||
showCookMode,
|
|
||||||
isCookMode,
|
|
||||||
isEditForm,
|
|
||||||
insert,
|
|
||||||
breakpoint,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
{{ $t('tool.required-tools') }}
|
{{ $t('tool.required-tools') }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="mx-2" />
|
<v-divider class="mx-2" />
|
||||||
<v-card-text class="pt-0">
|
<v-card-text>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-model="recipe.tools"
|
v-model="recipe.tools"
|
||||||
selector-type="tools"
|
selector-type="tools"
|
||||||
|
|
|
@ -141,6 +141,9 @@ export default defineNuxtComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.text-center {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
.time-card-flex {
|
.time-card-flex {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,7 @@
|
||||||
<v-alert
|
<v-alert
|
||||||
type="info"
|
type="info"
|
||||||
:text="$t('search.no-results')"
|
:text="$t('search.no-results')"
|
||||||
|
class="mb-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
|
@ -32,11 +32,11 @@
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="9">
|
<v-col cols="9">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
v-model="generatedSignupLink"
|
||||||
:label="$t('profile.invite-link')"
|
:label="$t('profile.invite-link')"
|
||||||
type="text"
|
type="text"
|
||||||
readonly
|
readonly
|
||||||
variant="filled"
|
variant="filled"
|
||||||
:value="generatedSignupLink"
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<div style="flex-basis: 500px">
|
<div style="flex-basis: 500px">
|
||||||
<strong> {{ $t("user.password-strength", { strength: pwStrength.strength.value }) }}</strong>
|
<strong> {{ $t("user.password-strength", { strength: pwStrength.strength.value }) }}</strong>
|
||||||
<v-progress-linear
|
<v-progress-linear
|
||||||
:value="pwStrength.score.value"
|
v-model="pwStrength.score.value"
|
||||||
class="rounded-lg"
|
rounded
|
||||||
:color="pwStrength.color.value"
|
:color="pwStrength.color.value"
|
||||||
height="15"
|
height="15"
|
||||||
/>
|
/>
|
||||||
|
@ -12,27 +12,11 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePasswordStrength } from "~/composables/use-passwords";
|
import { usePasswordStrength } from "~/composables/use-passwords";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const modelValue = defineModel<string>({ default: "" });
|
||||||
props: {
|
const i18n = useI18n();
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const asRef = toRef(props, "modelValue");
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const pwStrength = usePasswordStrength(asRef, i18n);
|
const pwStrength = usePasswordStrength(modelValue, i18n);
|
||||||
|
|
||||||
return {
|
|
||||||
pwStrength,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
@click:append-inner="pwFields.togglePasswordShow"
|
@click:append-inner="pwFields.togglePasswordShow"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserPasswordStrength :value="credentials.password1.value" />
|
<UserPasswordStrength v-model="credentials.password1.value" />
|
||||||
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="credentials.password2.value"
|
v-model="credentials.password2.value"
|
||||||
|
@ -86,9 +86,8 @@ import { usePasswordField } from "~/composables/use-passwords";
|
||||||
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
|
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
|
||||||
|
|
||||||
const inputAttrs = {
|
const inputAttrs = {
|
||||||
rounded: true,
|
|
||||||
validateOnBlur: true,
|
validateOnBlur: true,
|
||||||
class: "rounded-lg pb-1",
|
class: "pb-1",
|
||||||
variant: "solo-filled" as any,
|
variant: "solo-filled" as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app dark>
|
<v-app dark>
|
||||||
|
<NuxtPwaManifest />
|
||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
|
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
|
@ -104,7 +105,7 @@
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
<v-main class="pt-16">
|
<v-main class="pt-12">
|
||||||
<v-scroll-x-transition>
|
<v-scroll-x-transition>
|
||||||
<div>
|
<div>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
@ -118,8 +119,8 @@
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import type { SideBarLink } from "~/types/application-types";
|
import type { SideBarLink } from "~/types/application-types";
|
||||||
import { useAppInfo } from "~/composables/api";
|
import { useAppInfo } from "~/composables/api";
|
||||||
import { useCookbooks, usePublicCookbooks } from "~/composables/use-group-cookbooks";
|
|
||||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||||
|
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
|
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
|
||||||
import { useToggleDarkMode } from "~/composables/use-utils";
|
import { useToggleDarkMode } from "~/composables/use-utils";
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
|
@ -136,13 +137,14 @@ export default defineNuxtComponent({
|
||||||
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 loggedInCookbooks = useCookbooks();
|
|
||||||
const publicCookbooks = usePublicCookbooks(groupSlug.value || "");
|
|
||||||
const cookbooks = computed(() =>
|
|
||||||
isOwnGroup.value ? loggedInCookbooks.cookbooks.value : publicCookbooks.cookbooks.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
const cookbookPreferences = useCookbookPreferences();
|
const cookbookPreferences = useCookbookPreferences();
|
||||||
|
const { store: cookbooks, actions: cookbooksActions } = isOwnGroup.value ? useCookbookStore() : usePublicCookbookStore(groupSlug.value || "");
|
||||||
|
onMounted(() => {
|
||||||
|
if (!cookbooks.value.length) {
|
||||||
|
cookbooksActions.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value || "");
|
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value || "");
|
||||||
|
|
||||||
const householdsById = computed(() => {
|
const householdsById = computed(() => {
|
||||||
|
@ -176,10 +178,6 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
const currentUserHouseholdId = computed(() => $auth.user.value?.householdId);
|
const currentUserHouseholdId = computed(() => $auth.user.value?.householdId);
|
||||||
const cookbookLinks = computed<SideBarLink[]>(() => {
|
const cookbookLinks = computed<SideBarLink[]>(() => {
|
||||||
if (!cookbooks.value || !households.value) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedCookbooks = [...cookbooks.value].sort((a, b) => (a.position || 0) - (b.position || 0));
|
const sortedCookbooks = [...cookbooks.value].sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||||
|
|
||||||
const ownLinks: SideBarLink[] = [];
|
const ownLinks: SideBarLink[] = [];
|
||||||
|
@ -233,7 +231,7 @@ export default defineNuxtComponent({
|
||||||
{
|
{
|
||||||
insertDivider: false,
|
insertDivider: false,
|
||||||
icon: $globals.icons.fileImage,
|
icon: $globals.icons.fileImage,
|
||||||
title: i18n.t("recipe.create-from-image"),
|
title: i18n.t("recipe.create-from-images"),
|
||||||
subtitle: i18n.t("recipe.create-recipe-from-an-image"),
|
subtitle: i18n.t("recipe.create-recipe-from-an-image"),
|
||||||
to: `/g/${groupSlug.value}/r/create/image`,
|
to: `/g/${groupSlug.value}/r/create/image`,
|
||||||
restricted: true,
|
restricted: true,
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<v-form ref="file">
|
<v-form ref="files">
|
||||||
<input
|
<input
|
||||||
ref="uploader"
|
ref="uploader"
|
||||||
class="d-none"
|
class="d-none"
|
||||||
type="file"
|
type="file"
|
||||||
:accept="accept"
|
:accept="accept"
|
||||||
|
:multiple="multiple"
|
||||||
@change="onFileChanged"
|
@change="onFileChanged"
|
||||||
>
|
>
|
||||||
<slot v-bind="{ isSelecting, onButtonClick }">
|
<slot v-bind="{ isSelecting, onButtonClick }">
|
||||||
|
@ -72,9 +73,13 @@ export default defineNuxtComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const file = ref<File | null>(null);
|
const files = ref<File[]>([]);
|
||||||
const uploader = ref<HTMLInputElement | null>(null);
|
const uploader = ref<HTMLInputElement | null>(null);
|
||||||
const isSelecting = ref(false);
|
const isSelecting = ref(false);
|
||||||
|
|
||||||
|
@ -86,17 +91,33 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
async function upload() {
|
async function upload() {
|
||||||
if (file.value != null) {
|
if (files.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isSelecting.value = true;
|
isSelecting.value = true;
|
||||||
|
|
||||||
if (!props.post) {
|
if (!props.post) {
|
||||||
context.emit(UPLOAD_EVENT, file.value);
|
// NOTE: To preserve behaviour for other parents of this component,
|
||||||
|
// we emit a single File if !props.multiple.
|
||||||
|
context.emit(UPLOAD_EVENT, props.multiple ? files.value : files.value[0]);
|
||||||
isSelecting.value = false;
|
isSelecting.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WARN: My change is only for !props.post.
|
||||||
|
// I have not added support for multiple files in the API.
|
||||||
|
// Existing call-sites never passed the `multiple` prop,
|
||||||
|
// so this case will only be hit if the prop is set to true.
|
||||||
|
if (props.multiple && files.value.length > 1) {
|
||||||
|
console.warn("Multiple file uploads are not supported by the API.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = files.value[0];
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append(props.fileName, file.value);
|
formData.append(props.fileName, file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.upload.file(props.url, formData);
|
const response = await api.upload.file(props.url, formData);
|
||||||
if (response) {
|
if (response) {
|
||||||
|
@ -107,14 +128,15 @@ export default defineNuxtComponent({
|
||||||
console.error(e);
|
console.error(e);
|
||||||
context.emit(UPLOAD_EVENT, null);
|
context.emit(UPLOAD_EVENT, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelecting.value = false;
|
isSelecting.value = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function onFileChanged(e: Event) {
|
function onFileChanged(e: Event) {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
if (target.files !== null && target.files.length > 0 && file.value !== null) {
|
|
||||||
file.value = target.files[0];
|
if (target.files !== null && target.files.length > 0) {
|
||||||
|
files.value = Array.from(target.files);
|
||||||
upload();
|
upload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +154,7 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
file,
|
files,
|
||||||
uploader,
|
uploader,
|
||||||
isSelecting,
|
isSelecting,
|
||||||
effIcon,
|
effIcon,
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
<v-col
|
<v-col
|
||||||
v-for="(inputField, index) in items"
|
v-for="(inputField, index) in items"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="py-0"
|
|
||||||
cols="12"
|
cols="12"
|
||||||
sm="12"
|
sm="12"
|
||||||
>
|
>
|
||||||
|
@ -75,7 +74,6 @@
|
||||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
flat
|
flat
|
||||||
class="rounded-lg"
|
|
||||||
rows="3"
|
rows="3"
|
||||||
auto-grow
|
auto-grow
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
|
@ -95,7 +93,6 @@
|
||||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
flat
|
flat
|
||||||
class="rounded-lg"
|
|
||||||
:prepend-icon="inputField.icons ? modelValue[inputField.varName] : null"
|
:prepend-icon="inputField.icons ? modelValue[inputField.varName] : null"
|
||||||
:label="inputField.label"
|
:label="inputField.label"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<v-card-title class="text-h5 pl-0 py-0" style="font-weight: normal;">
|
<v-card-title class="text-h5 pl-0 py-0" style="font-weight: normal;">
|
||||||
<v-icon
|
<v-icon
|
||||||
v-if="icon"
|
v-if="icon"
|
||||||
|
size="small"
|
||||||
start
|
start
|
||||||
>
|
>
|
||||||
{{ icon }}
|
{{ icon }}
|
||||||
|
@ -24,7 +25,7 @@
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider class="mb-3" />
|
<v-divider class="mt-1 mb-3" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
<v-toolbar-title class="headline">
|
<v-toolbar-title class="headline">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<v-spacer />
|
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-progress-linear
|
<v-progress-linear
|
||||||
v-if="loading"
|
v-if="loading"
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
style="margin-bottom: 4rem"
|
style="margin-bottom: 4rem"
|
||||||
dark
|
dark
|
||||||
>
|
>
|
||||||
<v-toolbar-title class="headline text-h4">
|
<v-toolbar-title class="headline text-h4 text-center mx-0">
|
||||||
Mealie
|
Mealie
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
:min="min"
|
:min="min"
|
||||||
:max="max"
|
:max="max"
|
||||||
type="number"
|
type="number"
|
||||||
class="rounded-xl"
|
|
||||||
size="small"
|
size="small"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<MDC
|
<!-- eslint-disable-next-line vue/no-v-html is safe here because all HTML is sanitized with DOMPurify in setup() -->
|
||||||
:value="value"
|
<div v-html="value" />
|
||||||
tag="article"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -40,7 +39,8 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = computed(() => {
|
const value = computed(() => {
|
||||||
return sanitizeMarkdown(props.source) || "";
|
const rawHtml = marked.parse(props.source || "", { async: false });
|
||||||
|
return sanitizeMarkdown(rawHtml);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -56,7 +56,8 @@ export default defineNuxtComponent({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(th, td) {
|
:deep(th),
|
||||||
|
:deep(td) {
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -65,4 +66,10 @@ export default defineNuxtComponent({
|
||||||
:deep(th) {
|
:deep(th) {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(ul),
|
||||||
|
:deep(ol) {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import type { AxiosInstance, AxiosResponse } from "axios";
|
import type { AxiosInstance, AxiosResponse, AxiosRequestConfig } from "axios";
|
||||||
import type { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
|
import type { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
|
||||||
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
|
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
|
||||||
import { PublicExploreApi } from "~/lib/api/client-public";
|
import { PublicExploreApi } from "~/lib/api/client-public";
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
async safe<T, U>(
|
async safe<T, U>(
|
||||||
funcCall: (url: string, data: U) => Promise<AxiosResponse<T>>,
|
funcCall: (url: string, data: U, config?: AxiosRequestConfig) => Promise<AxiosResponse<T>>,
|
||||||
url: string,
|
url: string,
|
||||||
data: U,
|
data: U,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
): Promise<RequestResponse<T>> {
|
): Promise<RequestResponse<T>> {
|
||||||
let error = null;
|
let error = null;
|
||||||
const response = await funcCall(url, data).catch(function (e) {
|
const response = await funcCall(url, data, config).catch(function (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
// Insert Generic Error Handling Here
|
// Insert Generic Error Handling Here
|
||||||
error = e;
|
error = e;
|
||||||
|
@ -22,9 +23,9 @@ const request = {
|
||||||
|
|
||||||
function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
|
function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
|
||||||
return {
|
return {
|
||||||
async get<T>(url: string, params = {}): Promise<RequestResponse<T>> {
|
async get<T>(url: string, params = {}, config?: AxiosRequestConfig): Promise<RequestResponse<T>> {
|
||||||
let error = null;
|
let error = null;
|
||||||
const response = await axiosInstance.get<T>(url, params).catch((e) => {
|
const response = await axiosInstance.get<T>(url, { ...config, params }).catch((e) => {
|
||||||
error = e;
|
error = e;
|
||||||
});
|
});
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
|
@ -33,20 +34,20 @@ function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
|
||||||
return { response: null, error, data: null };
|
return { response: null, error, data: null };
|
||||||
},
|
},
|
||||||
|
|
||||||
async post<T, U>(url: string, data: U) {
|
async post<T, U>(url: string, data: U, config?: AxiosRequestConfig) {
|
||||||
return await request.safe<T, U>(axiosInstance.post, url, data);
|
return await request.safe<T, U>(axiosInstance.post, url, data, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
async put<T, U = T>(url: string, data: U) {
|
async put<T, U = T>(url: string, data: U, config?: AxiosRequestConfig) {
|
||||||
return await request.safe<T, U>(axiosInstance.put, url, data);
|
return await request.safe<T, U>(axiosInstance.put, url, data, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
async patch<T, U = Partial<T>>(url: string, data: U) {
|
async patch<T, U = Partial<T>>(url: string, data: U, config?: AxiosRequestConfig) {
|
||||||
return await request.safe<T, U>(axiosInstance.patch, url, data);
|
return await request.safe<T, U>(axiosInstance.patch, url, data, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
async delete<T>(url: string) {
|
async delete<T>(url: string, config?: AxiosRequestConfig) {
|
||||||
return await request.safe<T, undefined>(axiosInstance.delete, url, undefined);
|
return await request.safe<T, undefined>(axiosInstance.delete, url, undefined, config);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
17
frontend/composables/store/use-cookbook-store.ts
Normal file
17
frontend/composables/store/use-cookbook-store.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||||
|
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||||
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
|
const store: Ref<RecipeCookBook[]> = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export const useCookbookStore = function () {
|
||||||
|
const api = useUserApi();
|
||||||
|
return useStore<RecipeCookBook>(store, loading, api.cookbooks);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePublicCookbookStore = function (groupSlug: string) {
|
||||||
|
const api = usePublicExploreApi(groupSlug).explore;
|
||||||
|
return useReadOnlyStore<RecipeCookBook>(store, publicLoading, api.cookbooks);
|
||||||
|
};
|
|
@ -1,10 +1,6 @@
|
||||||
import { useAsyncKey } from "./use-utils";
|
import { useAsyncKey } from "./use-utils";
|
||||||
import { usePublicExploreApi } from "./api/api-client";
|
import { usePublicExploreApi } from "./api/api-client";
|
||||||
import { useHouseholdSelf } from "./use-households";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import type { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
|
|
||||||
|
|
||||||
let cookbookStore: Ref<ReadCookBook[] | null> | null = null;
|
|
||||||
|
|
||||||
export const useCookbook = function (publicGroupSlug: string | null = null) {
|
export const useCookbook = function (publicGroupSlug: string | null = null) {
|
||||||
function getOne(id: string | number) {
|
function getOne(id: string | number) {
|
||||||
|
@ -22,149 +18,3 @@ export const useCookbook = function (publicGroupSlug: string | null = null) {
|
||||||
|
|
||||||
return { getOne };
|
return { getOne };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePublicCookbooks = function (groupSlug: string) {
|
|
||||||
const api = usePublicExploreApi(groupSlug).explore;
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
getAll() {
|
|
||||||
loading.value = true;
|
|
||||||
const { data: units } = useAsyncData(useAsyncKey(), async () => {
|
|
||||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data.items;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
return units;
|
|
||||||
},
|
|
||||||
async refreshAll() {
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
|
||||||
|
|
||||||
if (data && data.items && cookbookStore) {
|
|
||||||
cookbookStore.value = data.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
},
|
|
||||||
flushStore() {
|
|
||||||
cookbookStore = null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!cookbookStore) {
|
|
||||||
cookbookStore = actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { cookbooks: cookbookStore, actions };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCookbooks = function () {
|
|
||||||
const api = useUserApi();
|
|
||||||
const { household } = useHouseholdSelf();
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
getAll() {
|
|
||||||
loading.value = true;
|
|
||||||
const { data: units } = useAsyncData(useAsyncKey(), async () => {
|
|
||||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data.items;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
return units;
|
|
||||||
},
|
|
||||||
async refreshAll() {
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
|
|
||||||
|
|
||||||
if (data && data.items && cookbookStore) {
|
|
||||||
cookbookStore.value = data.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
},
|
|
||||||
async createOne(name: string | null = null) {
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.cookbooks.createOne({
|
|
||||||
name: name || i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
|
|
||||||
position: (cookbookStore?.value?.length ?? 0) + 1,
|
|
||||||
queryFilterString: "",
|
|
||||||
});
|
|
||||||
if (data && cookbookStore?.value) {
|
|
||||||
cookbookStore.value.push(data);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.refreshAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
async updateOne(updateData: UpdateCookBook) {
|
|
||||||
if (!updateData.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.cookbooks.updateOne(updateData.id, updateData);
|
|
||||||
if (data && cookbookStore?.value) {
|
|
||||||
this.refreshAll();
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateOrder(cookbooks: ReadCookBook[]) {
|
|
||||||
if (!cookbooks?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
cookbooks.forEach((element, index) => {
|
|
||||||
element.position = index + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = await api.cookbooks.updateAll(cookbooks);
|
|
||||||
|
|
||||||
if (data && cookbookStore?.value) {
|
|
||||||
this.refreshAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
},
|
|
||||||
async deleteOne(id: string | number) {
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.cookbooks.deleteOne(id);
|
|
||||||
if (data && cookbookStore?.value) {
|
|
||||||
this.refreshAll();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
flushStore() {
|
|
||||||
cookbookStore = null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!cookbookStore) {
|
|
||||||
cookbookStore = actions.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { cookbooks: cookbookStore, actions };
|
|
||||||
};
|
|
||||||
|
|
|
@ -596,12 +596,13 @@
|
||||||
"create-recipe-description": "Create a new recipe from scratch.",
|
"create-recipe-description": "Create a new recipe from scratch.",
|
||||||
"create-recipes": "Create Recipes",
|
"create-recipes": "Create Recipes",
|
||||||
"import-with-zip": "Import with .zip",
|
"import-with-zip": "Import with .zip",
|
||||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
"create-recipe-from-an-image": "Create Recipe from Images",
|
||||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
"create-recipe-from-an-image-description": "Create a recipe by uploading images of the recipe text. Mealie will attempt to extract the text from the images using AI and create a new recipe from it.",
|
||||||
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
||||||
"create-from-image": "Create from Image",
|
"create-from-images": "Create from Images",
|
||||||
"should-translate-description": "Translate the recipe into my language",
|
"should-translate-description": "Translate the recipe into my language",
|
||||||
"please-wait-image-procesing": "Please wait, the image is processing. This may take some time.",
|
"please-wait-image-procesing": "Please wait, the image is processing. This may take some time.",
|
||||||
|
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
|
||||||
"bulk-url-import": "Bulk URL Import",
|
"bulk-url-import": "Bulk URL Import",
|
||||||
"debug-scraper": "Debug Scraper",
|
"debug-scraper": "Debug Scraper",
|
||||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Create a recipe by providing the name. All recipes must have unique names.",
|
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Create a recipe by providing the name. All recipes must have unique names.",
|
||||||
|
@ -660,7 +661,10 @@
|
||||||
"no-food": "No Food"
|
"no-food": "No Food"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reset Servings Count",
|
"reset-servings-count": "Reset Servings Count",
|
||||||
"not-linked-ingredients": "Additional Ingredients"
|
"not-linked-ingredients": "Additional Ingredients",
|
||||||
|
"upload-another-image": "Upload another image",
|
||||||
|
"upload-images": "Upload images",
|
||||||
|
"upload-more-images": "Upload more images"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Recipe Finder",
|
"recipe-finder": "Recipe Finder",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app dark>
|
<v-app dark>
|
||||||
|
<NuxtPwaManifest />
|
||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
|
|
||||||
<AppHeader :menu="false" />
|
<AppHeader :menu="false" />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app dark>
|
<v-app dark>
|
||||||
|
<NuxtPwaManifest />
|
||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
|
|
||||||
<v-banner
|
<v-banner
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
|
||||||
export type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
|
export type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
|
||||||
|
|
||||||
|
@ -9,11 +9,11 @@ export interface RequestResponse<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiRequestInstance {
|
export interface ApiRequestInstance {
|
||||||
get<T>(url: string, data?: unknown): Promise<RequestResponse<T>>;
|
get<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||||
post<T>(url: string, data: unknown): Promise<RequestResponse<T>>;
|
post<T>(url: string, data: unknown, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||||
put<T, U = T>(url: string, data: U): Promise<RequestResponse<T>>;
|
put<T, U = T>(url: string, data: U, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||||
patch<T, U = Partial<T>>(url: string, data: U): Promise<RequestResponse<T>>;
|
patch<T, U = Partial<T>>(url: string, data: U, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||||
delete<T>(url: string): Promise<RequestResponse<T>>;
|
delete<T>(url: string, config?: AxiosRequestConfig): Promise<RequestResponse<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginationData<T> {
|
export interface PaginationData<T> {
|
||||||
|
|
|
@ -157,17 +157,19 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
||||||
return await this.requests.post<string>(routes.recipesCreateUrlBulk, payload);
|
return await this.requests.post<string>(routes.recipesCreateUrlBulk, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneFromImage(fileObject: Blob | File, fileName: string, translateLanguage: string | null = null) {
|
async createOneFromImages(fileObjects: (Blob | File)[], translateLanguage: string | null = null) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("images", fileObject);
|
|
||||||
formData.append("extension", fileName.split(".").pop() ?? "");
|
fileObjects.forEach((file) => {
|
||||||
|
formData.append("images", file);
|
||||||
|
});
|
||||||
|
|
||||||
let apiRoute = routes.recipesCreateFromImage;
|
let apiRoute = routes.recipesCreateFromImage;
|
||||||
if (translateLanguage) {
|
if (translateLanguage) {
|
||||||
apiRoute = `${apiRoute}?translateLanguage=${translateLanguage}`;
|
apiRoute = `${apiRoute}?translateLanguage=${translateLanguage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.requests.post<string>(apiRoute, formData);
|
return await this.requests.post<string>(apiRoute, formData, { timeout: 120000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseIngredients(parser: Parser, ingredients: Array<string>) {
|
async parseIngredients(parser: Parser, ingredients: Array<string>) {
|
||||||
|
|
|
@ -150,6 +150,7 @@ import {
|
||||||
mdiCodeTags,
|
mdiCodeTags,
|
||||||
mdiKnife,
|
mdiKnife,
|
||||||
mdiCookie,
|
mdiCookie,
|
||||||
|
mdiBellPlus,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
|
@ -174,6 +175,7 @@ export const icons = {
|
||||||
arrowUpDown: mdiDrag,
|
arrowUpDown: mdiDrag,
|
||||||
backupRestore: mdiBackupRestore,
|
backupRestore: mdiBackupRestore,
|
||||||
bellAlert: mdiBellAlert,
|
bellAlert: mdiBellAlert,
|
||||||
|
bellPlus: mdiBellPlus,
|
||||||
broom: mdiBroom,
|
broom: mdiBroom,
|
||||||
calendar: mdiCalendar,
|
calendar: mdiCalendar,
|
||||||
calendarMinus: mdiCalendarMinus,
|
calendarMinus: mdiCalendarMinus,
|
||||||
|
|
10
frontend/middleware/pwa-share-target-redirect.global.ts
Normal file
10
frontend/middleware/pwa-share-target-redirect.global.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
|
if (to.path === "/r/create/url") {
|
||||||
|
const { user } = useMealieAuth();
|
||||||
|
const groupSlug = user.value?.groupSlug;
|
||||||
|
if (!groupSlug) {
|
||||||
|
return navigateTo("/login", { redirectCode: 301 });
|
||||||
|
}
|
||||||
|
return navigateTo(`/g/${groupSlug}${to.fullPath}`, { redirectCode: 301 });
|
||||||
|
}
|
||||||
|
});
|
|
@ -13,7 +13,6 @@ export default defineNuxtConfig({
|
||||||
"@sidebase/nuxt-auth",
|
"@sidebase/nuxt-auth",
|
||||||
"@nuxtjs/google-fonts",
|
"@nuxtjs/google-fonts",
|
||||||
"vuetify-nuxt-module",
|
"vuetify-nuxt-module",
|
||||||
"@nuxtjs/mdc",
|
|
||||||
"@nuxt/eslint",
|
"@nuxt/eslint",
|
||||||
],
|
],
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
@ -67,7 +66,7 @@ export default defineNuxtConfig({
|
||||||
viewTransition: true,
|
viewTransition: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
css: ["~/assets/css/main.css", "~/assets/style-overrides.scss"],
|
css: ["~/assets/css/main.css", "~/assets/css/fonts.css", "~/assets/style-overrides.scss"],
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
sessionPassword: process.env.SESSION_PASSWORD || "password-with-at-least-32-characters",
|
sessionPassword: process.env.SESSION_PASSWORD || "password-with-at-least-32-characters",
|
||||||
|
@ -172,7 +171,7 @@ export default defineNuxtConfig({
|
||||||
|
|
||||||
googleFonts: {
|
googleFonts: {
|
||||||
fontsPath: "/assets/fonts",
|
fontsPath: "/assets/fonts",
|
||||||
download: true,
|
download: false, // Disable automatic downloading
|
||||||
families: {
|
families: {
|
||||||
Roboto: [100, 300, 400, 500, 700, 900],
|
Roboto: [100, 300, 400, 500, 700, 900],
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@nuxt/eslint": "1.2.0",
|
"@nuxt/eslint": "1.2.0",
|
||||||
"@nuxtjs/i18n": "^9.2.1",
|
"@nuxtjs/i18n": "^9.2.1",
|
||||||
"@nuxtjs/mdc": "0.14.0",
|
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"@sidebase/nuxt-auth": "0.10.0",
|
"@sidebase/nuxt-auth": "0.10.0",
|
||||||
"@vite-pwa/nuxt": "0.10.6",
|
"@vite-pwa/nuxt": "0.10.6",
|
||||||
|
@ -31,6 +30,7 @@
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"isomorphic-dompurify": "^2.22.0",
|
"isomorphic-dompurify": "^2.22.0",
|
||||||
"json-editor-vue": "^0.18.1",
|
"json-editor-vue": "^0.18.1",
|
||||||
|
"marked": "^15.0.12",
|
||||||
"next-auth": "~4.21.1",
|
"next-auth": "~4.21.1",
|
||||||
"nuxt": "^3.15.4",
|
"nuxt": "^3.15.4",
|
||||||
"typescript": "5.3",
|
"typescript": "5.3",
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" :title="$t('admin.maintenance.summary-title')" />
|
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" :title="$t('admin.maintenance.summary-title')" />
|
||||||
<div class="mb-6 ml-2 d-flex" style="gap: 0.3rem">
|
<div class="mb-6 d-flex" style="gap: 0.3rem">
|
||||||
<BaseButton color="info" @click="getSummary">
|
<BaseButton color="info" @click="getSummary">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
{{ $globals.icons.tools }}
|
{{ $globals.icons.tools }}
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
{{ $t("admin.maintenance.button-label-open-details") }}
|
{{ $t("admin.maintenance.button-label-open-details") }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
<v-card class="ma-2" :loading="state.fetchingInfo">
|
<v-card class="" :loading="state.fetchingInfo">
|
||||||
<template v-for="(value, idx) in info" :key="`item-${idx}`">
|
<template v-for="(value, idx) in info" :key="`item-${idx}`">
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-list-item-title class="py-2">
|
<v-list-item-title class="py-2">
|
||||||
|
@ -67,21 +67,23 @@
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</BaseCardSectionTitle>
|
</BaseCardSectionTitle>
|
||||||
<v-card class="ma-2" :loading="state.actionLoading">
|
<v-card class="ma-0" flat :loading="state.actionLoading">
|
||||||
<template v-for="(action, idx) in actions" :key="`item-${idx}`">
|
<template v-for="(action, idx) in actions" :key="`item-${idx}`">
|
||||||
<v-list-item class="py-1">
|
<v-list-item class="py-2 px-0">
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
<div>{{ action.name }}</div>
|
<div>{{ action.name }}</div>
|
||||||
<v-list-item-subtitle class="wrap-word">
|
<v-list-item-subtitle class="wrap-word">
|
||||||
{{ action.subtitle }}
|
{{ action.subtitle }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
|
<template #append>
|
||||||
<BaseButton color="info" @click="action.handler">
|
<BaseButton color="info" @click="action.handler">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
{{ $globals.icons.robot }}
|
{{ $globals.icons.robot }}
|
||||||
</template>
|
</template>
|
||||||
{{ $t("general.run") }}
|
{{ $t("general.run") }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider class="mx-2" />
|
<v-divider class="mx-2" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
:items="groups"
|
:items="groups"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
flat
|
flat
|
||||||
class="rounded-lg"
|
|
||||||
item-title="name"
|
item-title="name"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
|
|
|
@ -12,8 +12,6 @@
|
||||||
v-if="groups"
|
v-if="groups"
|
||||||
v-model="createHouseholdForm.data.groupId"
|
v-model="createHouseholdForm.data.groupId"
|
||||||
:items="groups"
|
:items="groups"
|
||||||
rounded
|
|
||||||
class="rounded-lg"
|
|
||||||
item-title="name"
|
item-title="name"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
v-if="groups"
|
v-if="groups"
|
||||||
v-model="selectedGroupId"
|
v-model="selectedGroupId"
|
||||||
:items="groups"
|
:items="groups"
|
||||||
rounded
|
|
||||||
class="rounded-lg"
|
|
||||||
item-title="name"
|
item-title="name"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
|
@ -37,8 +35,6 @@
|
||||||
v-model="newUserData.household"
|
v-model="newUserData.household"
|
||||||
:disabled="!selectedGroupId"
|
:disabled="!selectedGroupId"
|
||||||
:items="households"
|
:items="households"
|
||||||
rounded
|
|
||||||
class="rounded-lg"
|
|
||||||
item-title="name"
|
item-title="name"
|
||||||
item-value="name"
|
item-value="name"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
|
|
|
@ -251,6 +251,12 @@ export default defineNuxtComponent({
|
||||||
layout: "admin",
|
layout: "admin",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For some reason the layout is not set automatically, so we set it here,
|
||||||
|
// even though it's defined above in the page meta.
|
||||||
|
onMounted(() => {
|
||||||
|
setPageLayout("admin");
|
||||||
|
});
|
||||||
|
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
<v-form @submit.prevent="requestLink()">
|
<v-form @submit.prevent="requestLink()">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="email"
|
v-model="email"
|
||||||
variant="filled"
|
:prepend-inner-icon="$globals.icons.email"
|
||||||
rounded
|
variant="solo-filled"
|
||||||
|
flat
|
||||||
autofocus
|
autofocus
|
||||||
class="rounded-lg"
|
|
||||||
name="login"
|
name="login"
|
||||||
:label="$t('user.email')"
|
:label="$t('user.email')"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
@ -139,10 +139,10 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
import { useHouseholdSelf } from "@/composables/use-households";
|
import { useHouseholdSelf } from "@/composables/use-households";
|
||||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
import type { CreateCookBook, ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
|
@ -162,7 +162,7 @@ export default defineNuxtComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { cookbooks: allCookbooks, actions } = useCookbooks();
|
const { store: allCookbooks, actions } = useCookbookStore();
|
||||||
|
|
||||||
// Make a local reactive copy of myCookbooks
|
// Make a local reactive copy of myCookbooks
|
||||||
const myCookbooks = ref<ReadCookBook[]>([]);
|
const myCookbooks = ref<ReadCookBook[]>([]);
|
||||||
|
@ -188,7 +188,9 @@ export default defineNuxtComponent({
|
||||||
household.value?.name || "",
|
household.value?.name || "",
|
||||||
String((myCookbooks.value?.length ?? 0) + 1),
|
String((myCookbooks.value?.length ?? 0) + 1),
|
||||||
]) as string;
|
]) as string;
|
||||||
await actions.createOne(name).then((cookbook) => {
|
|
||||||
|
const data = { name } as CreateCookBook;
|
||||||
|
await actions.createOne(data).then((cookbook) => {
|
||||||
if (!cookbook) {
|
if (!cookbook) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ export default defineNuxtComponent({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: $globals.icons.fileImage,
|
icon: $globals.icons.fileImage,
|
||||||
text: i18n.t("recipe.create-from-image"),
|
text: i18n.t("recipe.create-from-images"),
|
||||||
value: "image",
|
value: "image",
|
||||||
hide: !enableOpenAIImages.value,
|
hide: !enableOpenAIImages.value,
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
{{ $t('recipe.recipe-bulk-importer-description') }}
|
{{ $t('recipe.recipe-bulk-importer-description') }}
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</div>
|
<div class="px-4">
|
||||||
<section class="mt-2">
|
<section class="mt-2">
|
||||||
<v-row
|
<v-row
|
||||||
v-for="(_, idx) in bulkUrls"
|
v-for="(_, idx) in bulkUrls"
|
||||||
|
@ -54,6 +54,7 @@
|
||||||
cols="12"
|
cols="12"
|
||||||
xs="12"
|
xs="12"
|
||||||
sm="6"
|
sm="6"
|
||||||
|
class="py-0"
|
||||||
>
|
>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-model="bulkUrls[idx].categories"
|
v-model="bulkUrls[idx].categories"
|
||||||
|
@ -73,6 +74,7 @@
|
||||||
cols="12"
|
cols="12"
|
||||||
xs="12"
|
xs="12"
|
||||||
sm="6"
|
sm="6"
|
||||||
|
class="pt-0 pb-4"
|
||||||
>
|
>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-model="bulkUrls[idx].tags"
|
v-model="bulkUrls[idx].tags"
|
||||||
|
@ -90,8 +92,9 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
</template>
|
</template>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-card-actions class="justify-end flex-wrap mb-1">
|
<v-card-actions class="justify-end flex-wrap mt-3 pa-0">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
|
class="mt-1 pr-4"
|
||||||
delete
|
delete
|
||||||
@click="
|
@click="
|
||||||
bulkUrls = [];
|
bulkUrls = [];
|
||||||
|
@ -117,23 +120,26 @@
|
||||||
@bulk-data="assignUrls"
|
@bulk-data="assignUrls"
|
||||||
/>
|
/>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
<div class="px-1">
|
<div class="px-0">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="showCatTags"
|
v-model="showCatTags"
|
||||||
hide-details
|
hide-details
|
||||||
:label="$t('recipe.set-categories-and-tags')"
|
:label="$t('recipe.set-categories-and-tags')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<v-card-actions class="justify-end">
|
<v-card-actions class="justify-center">
|
||||||
|
<div style="width: 250px">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
:disabled="bulkUrls.length === 0 || lockBulkImport"
|
:disabled="bulkUrls.length === 0 || lockBulkImport"
|
||||||
|
rounded
|
||||||
|
block
|
||||||
@click="bulkCreate"
|
@click="bulkCreate"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
{{ $globals.icons.check }}
|
{{ $globals.icons.check }}
|
||||||
</template>
|
</template>
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
</div>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</section>
|
</section>
|
||||||
<section class="mt-12">
|
<section class="mt-12">
|
||||||
|
@ -144,6 +150,8 @@
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -61,7 +61,6 @@
|
||||||
<div style="width: 250px">
|
<div style="width: 250px">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
:disabled="!newRecipeData"
|
:disabled="!newRecipeData"
|
||||||
large
|
|
||||||
rounded
|
rounded
|
||||||
block
|
block
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -1,86 +1,70 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-form
|
<v-form ref="domUrlForm" @submit.prevent="createRecipe">
|
||||||
ref="domUrlForm"
|
|
||||||
@submit.prevent="createRecipe"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<v-card-title class="headline">
|
<v-card-title class="headline">
|
||||||
{{ $t('recipe.create-recipe-from-an-image') }}
|
{{ $t("recipe.create-recipe-from-an-image") }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<p>{{ $t('recipe.create-recipe-from-an-image-description') }}</p>
|
<p>{{ $t("recipe.create-recipe-from-an-image-description") }}</p>
|
||||||
<v-container class="pa-0">
|
<v-container class="pa-0">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col cols="auto" align-self="center">
|
||||||
cols="auto"
|
|
||||||
align-self="center"
|
|
||||||
>
|
|
||||||
<AppButtonUpload
|
<AppButtonUpload
|
||||||
v-if="!uploadedImage"
|
|
||||||
class="ml-auto"
|
class="ml-auto"
|
||||||
url="none"
|
url="none"
|
||||||
file-name="image"
|
file-name="images"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
:text="$t('recipe.upload-image')"
|
:text="uploadedImages.length ? $t('recipe.upload-more-images') : $t('recipe.upload-images')"
|
||||||
:text-btn="false"
|
:text-btn="false"
|
||||||
:post="false"
|
:post="false"
|
||||||
@uploaded="uploadImage"
|
:multiple="true"
|
||||||
|
@uploaded="uploadImages"
|
||||||
/>
|
/>
|
||||||
<v-btn
|
|
||||||
v-if="!!uploadedImage"
|
|
||||||
color="error"
|
|
||||||
@click="clearImage"
|
|
||||||
>
|
|
||||||
<v-icon start>
|
|
||||||
{{ $globals.icons.close }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t('recipe.remove-image') }}
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<div
|
<div v-if="uploadedImages.length" class="mt-3">
|
||||||
v-if="uploadedImage && uploadedImagePreviewUrl"
|
|
||||||
class="mt-3"
|
|
||||||
>
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col cols="12" class="pb-0">
|
||||||
cols="12"
|
|
||||||
class="pb-0"
|
|
||||||
>
|
|
||||||
<v-card-text class="pa-0">
|
<v-card-text class="pa-0">
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
{{ $t('recipe.crop-and-rotate-the-image') }}
|
{{ $t("recipe.crop-and-rotate-the-image") }}
|
||||||
</p>
|
</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row style="max-width: 600px;">
|
<v-row style="max-width: 600px">
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-col cols="12">
|
<v-col v-for="(imageUrl, index) in uploadedImagesPreviewUrls" :key="index" cols="12">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="auto" align-self="center">
|
||||||
<ImageCropper
|
<ImageCropper
|
||||||
:img="uploadedImagePreviewUrl"
|
:img="imageUrl"
|
||||||
cropper-height="50vh"
|
cropper-height="100%"
|
||||||
cropper-width="100%"
|
cropper-width="100%"
|
||||||
@save="updateUploadedImage"
|
@save="(croppedImage) => updateUploadedImage(index, croppedImage)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-btn color="error" @click="() => clearImage(index)">
|
||||||
|
<v-icon start>
|
||||||
|
{{ $globals.icons.close }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("recipe.remove-image") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions v-if="uploadedImage">
|
<v-card-actions v-if="uploadedImages.length">
|
||||||
<div>
|
<div>
|
||||||
<p style="width: 250px">
|
<p style="width: 250px">
|
||||||
<BaseButton
|
<BaseButton rounded block type="submit" :loading="loading" />
|
||||||
rounded
|
|
||||||
block
|
|
||||||
type="submit"
|
|
||||||
:loading="loading"
|
|
||||||
/>
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
|
@ -90,11 +74,12 @@
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p v-if="loading" class="mb-0">
|
||||||
v-if="loading"
|
{{
|
||||||
class="mb-0"
|
uploadedImages.length > 1
|
||||||
>
|
? $t("recipe.please-wait-images-processing")
|
||||||
{{ $t('recipe.please-wait-image-procesing') }}
|
: $t("recipe.please-wait-image-procesing")
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
@ -121,55 +106,59 @@ export default defineNuxtComponent({
|
||||||
const groupSlug = computed(() => route.params.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug || "");
|
||||||
|
|
||||||
const domUrlForm = ref<VForm | null>(null);
|
const domUrlForm = ref<VForm | null>(null);
|
||||||
const uploadedImage = ref<Blob | File>();
|
const uploadedImages = ref<(Blob | File)[]>([]);
|
||||||
const uploadedImageName = ref<string>("");
|
const uploadedImageNames = ref<string[]>([]);
|
||||||
const uploadedImagePreviewUrl = ref<string>();
|
const uploadedImagesPreviewUrls = ref<string[]>([]);
|
||||||
const shouldTranslate = ref(true);
|
const shouldTranslate = ref(true);
|
||||||
|
|
||||||
function uploadImage(fileObject: File) {
|
function uploadImages(files: File[]) {
|
||||||
uploadedImage.value = fileObject;
|
uploadedImages.value = [...uploadedImages.value, ...files];
|
||||||
uploadedImageName.value = fileObject.name;
|
uploadedImageNames.value = [...uploadedImageNames.value, ...files.map(file => file.name)];
|
||||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
uploadedImagesPreviewUrls.value = [
|
||||||
|
...uploadedImagesPreviewUrls.value,
|
||||||
|
...files.map(file => URL.createObjectURL(file)),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUploadedImage(fileObject: Blob) {
|
function clearImage(index: number) {
|
||||||
uploadedImage.value = fileObject;
|
URL.revokeObjectURL(uploadedImagesPreviewUrls.value[index]);
|
||||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearImage() {
|
uploadedImages.value = uploadedImages.value.filter((_, i) => i !== index);
|
||||||
uploadedImage.value = undefined;
|
uploadedImageNames.value = uploadedImageNames.value.filter((_, i) => i !== index);
|
||||||
uploadedImageName.value = "";
|
uploadedImagesPreviewUrls.value = uploadedImagesPreviewUrls.value.filter((_, i) => i !== index);
|
||||||
uploadedImagePreviewUrl.value = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createRecipe() {
|
async function createRecipe() {
|
||||||
if (!uploadedImage.value) {
|
if (uploadedImages.value.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
const translateLanguage = shouldTranslate.value ? i18n.locale : undefined;
|
const translateLanguage = shouldTranslate.value ? i18n.locale : undefined;
|
||||||
const { data, error } = await api.recipes.createOneFromImage(uploadedImage.value, uploadedImageName.value, translateLanguage);
|
const { data, error } = await api.recipes.createOneFromImages(uploadedImages.value, translateLanguage?.value);
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
alert.error(i18n.t("events.something-went-wrong"));
|
alert.error(i18n.t("events.something-went-wrong"));
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
router.push(`/g/${groupSlug.value}/r/${data}`);
|
router.push(`/g/${groupSlug.value}/r/${data}`);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUploadedImage(index: number, croppedImage: Blob) {
|
||||||
|
uploadedImages.value[index] = croppedImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
domUrlForm,
|
domUrlForm,
|
||||||
uploadedImage,
|
uploadedImages,
|
||||||
uploadedImagePreviewUrl,
|
uploadedImagesPreviewUrls,
|
||||||
shouldTranslate,
|
shouldTranslate,
|
||||||
uploadImage,
|
uploadImages,
|
||||||
updateUploadedImage,
|
|
||||||
clearImage,
|
clearImage,
|
||||||
createRecipe,
|
createRecipe,
|
||||||
|
updateUploadedImage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
<div style="width: 250px">
|
<div style="width: 250px">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
:disabled="newRecipeZip === null"
|
:disabled="newRecipeZip === null"
|
||||||
large
|
|
||||||
rounded
|
rounded
|
||||||
block
|
block
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<BasePageTitle
|
<BasePageTitle
|
||||||
v-if="groupName"
|
v-if="groupName"
|
||||||
class="bg-grey-darken-4 mt-n4 pt-8"
|
class="mt-n4 pt-8"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<v-img
|
<v-img
|
||||||
|
|
|
@ -15,11 +15,12 @@
|
||||||
</template>
|
</template>
|
||||||
{{ $t('migration.recipe-data-migrations-explanation') }}
|
{{ $t('migration.recipe-data-migrations-explanation') }}
|
||||||
</BasePageTitle>
|
</BasePageTitle>
|
||||||
<v-container>
|
<v-container :class="$vuetify.display.smAndDown ? 'px-0': ''">
|
||||||
<BaseCardSectionTitle :title="$t('migration.new-migration')" />
|
<BaseCardSectionTitle :title="$t('migration.new-migration')" />
|
||||||
<v-card
|
<v-card
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
style="border-color: lightgrey;"
|
||||||
>
|
>
|
||||||
<v-card-title> {{ $t('migration.choose-migration-type') }} </v-card-title>
|
<v-card-title> {{ $t('migration.choose-migration-type') }} </v-card-title>
|
||||||
<v-card-text
|
<v-card-text
|
||||||
|
@ -83,7 +84,7 @@
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
<v-container>
|
<v-container class="$vuetify.display.smAndDown ? 'px-0': ''">
|
||||||
<BaseCardSectionTitle :title="$t('migration.previous-migrations')" />
|
<BaseCardSectionTitle :title="$t('migration.previous-migrations')" />
|
||||||
<ReportTable
|
<ReportTable
|
||||||
:items="reports"
|
:items="reports"
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
<v-col v-for="(day, index) in plan" :key="index" cols="12" sm="12" md="4" lg="4" xl="2"
|
<v-col v-for="(day, index) in plan" :key="index" cols="12" sm="12" md="4" lg="4" xl="2"
|
||||||
class="col-borders my-1 d-flex flex-column">
|
class="col-borders my-1 d-flex flex-column">
|
||||||
<v-card class="mb-2 border-left-primary rounded-sm px-2">
|
<v-card class="mb-2 border-left-primary rounded-sm px-2">
|
||||||
<v-container class="px-0">
|
<v-container class="px-0 d-flex align-center" height="56px">
|
||||||
<v-row no-gutters style="width: 100%;">
|
<v-row no-gutters style="width: 100%;">
|
||||||
<v-col cols="10">
|
<v-col cols="10" class="d-flex align-center">
|
||||||
<p class="pl-2 my-1">
|
<p class="pl-2 my-1">
|
||||||
{{ $d(day.date, "short") }}
|
{{ $d(day.date, "short") }}
|
||||||
</p>
|
</p>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="d-flex justify-top" cols="2">
|
<v-col class="d-flex align-center" cols="2">
|
||||||
<GroupMealPlanDayContextMenu v-if="day.recipes.length" :recipes="day.recipes" />
|
<GroupMealPlanDayContextMenu v-if="day.recipes.length" :recipes="day.recipes" />
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -38,39 +38,31 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { MealsByDate } from "./types";
|
import type { MealsByDate } from "./types";
|
||||||
import type { ReadPlanEntry } from "~/lib/api/types/meal-plan";
|
import type { ReadPlanEntry } from "~/lib/api/types/meal-plan";
|
||||||
import GroupMealPlanDayContextMenu from "~/components/Domain/Household/GroupMealPlanDayContextMenu.vue";
|
import GroupMealPlanDayContextMenu from "~/components/Domain/Household/GroupMealPlanDayContextMenu.vue";
|
||||||
import RecipeCardMobile from "~/components/Domain/Recipe/RecipeCardMobile.vue";
|
import RecipeCardMobile from "~/components/Domain/Recipe/RecipeCardMobile.vue";
|
||||||
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps<{
|
||||||
components: {
|
mealplans: MealsByDate[];
|
||||||
GroupMealPlanDayContextMenu,
|
}>();
|
||||||
RecipeCardMobile,
|
|
||||||
},
|
type DaySection = {
|
||||||
props: {
|
|
||||||
mealplans: {
|
|
||||||
type: Array as () => MealsByDate[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
type DaySection = {
|
|
||||||
title: string;
|
title: string;
|
||||||
meals: ReadPlanEntry[];
|
meals: ReadPlanEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Days = {
|
type Days = {
|
||||||
date: Date;
|
date: Date;
|
||||||
sections: DaySection[];
|
sections: DaySection[];
|
||||||
recipes: RecipeSummary[];
|
recipes: RecipeSummary[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const plan = computed<Days[]>(() => {
|
const plan = computed<Days[]>(() => {
|
||||||
return props.mealplans.reduce((acc, day) => {
|
return props.mealplans.reduce((acc, day) => {
|
||||||
const out: Days = {
|
const out: Days = {
|
||||||
date: day.date,
|
date: day.date,
|
||||||
|
@ -109,11 +101,5 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as Days[]);
|
}, [] as Days[]);
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
plan,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="createDialog"
|
v-model="createDialog"
|
||||||
:title="$t('events.new-notification')"
|
:title="$t('events.new-notification')"
|
||||||
|
:icon="$globals.icons.bellPlus"
|
||||||
can-submit
|
can-submit
|
||||||
@submit="createNewNotifier"
|
@submit="createNewNotifier"
|
||||||
>
|
>
|
||||||
|
@ -95,7 +96,7 @@
|
||||||
>
|
>
|
||||||
<v-expansion-panel-title
|
<v-expansion-panel-title
|
||||||
disable-icon-rotate
|
disable-icon-rotate
|
||||||
class="headline"
|
class="text-h6"
|
||||||
>
|
>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
{{ notifier.name }}
|
{{ notifier.name }}
|
||||||
|
@ -103,6 +104,7 @@
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
|
flat
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
>
|
>
|
||||||
<v-icon>
|
<v-icon>
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<!-- Form Container -->
|
<!-- Form Container -->
|
||||||
<div class="d-flex justify-center grow items-center my-4">
|
<div class="d-flex justify-center grow items-center my-4">
|
||||||
<template v-if="state.ctx.state === States.Initial">
|
<template v-if="state.ctx.state === States.Initial">
|
||||||
<div width="600px">
|
<v-container>
|
||||||
<v-card-title class="text-h5 my-4 mb-5 pb-0 text-center">
|
<v-card-title class="text-h5 my-4 mb-5 pb-0 text-center">
|
||||||
{{ $t("user-registration.user-registration") }}
|
{{ $t("user-registration.user-registration") }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
dark
|
dark
|
||||||
hover
|
hover
|
||||||
width="300px"
|
width="320px"
|
||||||
@click="initial.joinGroup"
|
@click="initial.joinGroup"
|
||||||
>
|
>
|
||||||
<v-card-title class="d-flex align-center justify-center py-3">
|
<v-card-title class="d-flex align-center justify-center py-3">
|
||||||
|
@ -77,8 +77,8 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
dark
|
dark
|
||||||
hover
|
hover
|
||||||
width="300px"
|
width="320px"
|
||||||
@click="initial.joinGroup"
|
@click="initial.createGroup"
|
||||||
>
|
>
|
||||||
<v-card-title class="d-flex align-center justify-center py-3">
|
<v-card-title class="d-flex align-center justify-center py-3">
|
||||||
<v-icon
|
<v-icon
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="state.ctx.state === States.ProvideToken">
|
<template v-else-if="state.ctx.state === States.ProvideToken">
|
||||||
|
@ -333,9 +333,7 @@ import type { VForm } from "~/types/auto-forms";
|
||||||
|
|
||||||
const inputAttrs = {
|
const inputAttrs = {
|
||||||
variant: "filled",
|
variant: "filled",
|
||||||
rounded: true,
|
|
||||||
validateOnBlur: true,
|
validateOnBlur: true,
|
||||||
class: "rounded-lg",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
|
|
|
@ -17,21 +17,19 @@
|
||||||
<v-form @submit.prevent="requestLink()">
|
<v-form @submit.prevent="requestLink()">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="email"
|
v-model="email"
|
||||||
:prepend-icon="$globals.icons.email"
|
:prepend-inner-icon="$globals.icons.email"
|
||||||
variant="filled"
|
variant="solo-filled"
|
||||||
rounded
|
flat
|
||||||
autofocus
|
autofocus
|
||||||
class="rounded-lg"
|
|
||||||
name="login"
|
name="login"
|
||||||
:label="$t('user.email')"
|
:label="$t('user.email')"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="password"
|
v-model="password"
|
||||||
variant="filled"
|
variant="solo-filled"
|
||||||
rounded
|
flat
|
||||||
class="rounded-lg"
|
:prepend-inner-icon="$globals.icons.lock"
|
||||||
:prepend-icon="$globals.icons.lock"
|
|
||||||
name="password"
|
name="password"
|
||||||
:label="$t('user.password')"
|
:label="$t('user.password')"
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -39,11 +37,10 @@
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="passwordConfirm"
|
v-model="passwordConfirm"
|
||||||
variant="filled"
|
variant="solo-filled"
|
||||||
rounded
|
flat
|
||||||
validate-on="blur"
|
validate-on="blur"
|
||||||
class="rounded-lg"
|
:prepend-inner-icon="$globals.icons.lock"
|
||||||
:prepend-icon="$globals.icons.lock"
|
|
||||||
name="password"
|
name="password"
|
||||||
:label="$t('user.confirm-password')"
|
:label="$t('user.confirm-password')"
|
||||||
type="password"
|
type="password"
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col
|
||||||
cols="3"
|
|
||||||
class="text-left"
|
class="text-left"
|
||||||
>
|
>
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
|
@ -51,8 +50,9 @@
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
|
v-if="mdAndUp"
|
||||||
cols="6"
|
cols="6"
|
||||||
class="d-none d-lg-flex justify-center"
|
class="d-none d-sm-flex justify-center"
|
||||||
>
|
>
|
||||||
<v-img
|
<v-img
|
||||||
max-height="100"
|
max-height="100"
|
||||||
|
@ -434,6 +434,7 @@ export default defineNuxtComponent({
|
||||||
},
|
},
|
||||||
// middleware: "sidebase-auth",
|
// middleware: "sidebase-auth",
|
||||||
setup() {
|
setup() {
|
||||||
|
const { mdAndUp } = useDisplay();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const preferences = useShoppingListPreferences();
|
const preferences = useShoppingListPreferences();
|
||||||
|
@ -1251,6 +1252,7 @@ export default defineNuxtComponent({
|
||||||
allFoods,
|
allFoods,
|
||||||
getTextColor,
|
getTextColor,
|
||||||
isOffline,
|
isOffline,
|
||||||
|
mdAndUp,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,9 +17,10 @@
|
||||||
<section class="d-flex justify-center">
|
<section class="d-flex justify-center">
|
||||||
<v-card
|
<v-card
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
width="500px"
|
width="100%"
|
||||||
|
flat
|
||||||
>
|
>
|
||||||
<v-card-text>
|
<v-card-text class="px-0">
|
||||||
<v-form
|
<v-form
|
||||||
ref="domNewTokenForm"
|
ref="domNewTokenForm"
|
||||||
@submit.prevent
|
@submit.prevent
|
||||||
|
@ -38,16 +39,16 @@
|
||||||
readonly
|
readonly
|
||||||
rows="3"
|
rows="3"
|
||||||
/>
|
/>
|
||||||
<v-list-subheader class="text-center">
|
<p>
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
"settings.token.copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again",
|
"settings.token.copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again",
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</v-list-subheader>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions class="px-0">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="createdToken"
|
v-if="createdToken"
|
||||||
cancel
|
cancel
|
||||||
|
@ -78,13 +79,10 @@
|
||||||
:title="$t('settings.token.active-tokens')"
|
:title="$t('settings.token.active-tokens')"
|
||||||
/>
|
/>
|
||||||
<section class="d-flex flex-column">
|
<section class="d-flex flex-column">
|
||||||
|
<v-list>
|
||||||
<div
|
<div
|
||||||
v-for="(token, index) in user.tokens"
|
v-for="(token, index) in user.tokens"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
|
||||||
<v-card
|
|
||||||
variant="outlined"
|
|
||||||
class="mb-2"
|
|
||||||
>
|
>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
|
@ -93,16 +91,17 @@
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ $t('general.created-on-date', [$d(new Date(token.createdAt!))]) }}
|
{{ $t('general.created-on-date', [$d(new Date(token.createdAt!))]) }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
<v-list-item-action>
|
<template #append>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
delete
|
delete
|
||||||
small
|
small
|
||||||
@click="deleteToken(token.id)"
|
@click="deleteToken(token.id)"
|
||||||
/>
|
/>
|
||||||
</v-list-item-action>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-card>
|
<v-divider class="mx-2 my-2" />
|
||||||
</div>
|
</div>
|
||||||
|
</v-list>
|
||||||
</section>
|
</section>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -149,7 +149,7 @@
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@click:append="showPassword = !showPassword"
|
@click:append="showPassword = !showPassword"
|
||||||
/>
|
/>
|
||||||
<UserPasswordStrength :value="password.newOne" />
|
<UserPasswordStrength v-model="password.newOne" />
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container v-if="user">
|
<v-container v-if="user" class="mb-8">
|
||||||
<section class="d-flex flex-column align-center mt-4">
|
<section class="d-flex flex-column align-center mt-4">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
:tooltip="false"
|
:tooltip="false"
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
:user-id="user.id"
|
:user-id="user.id"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="text-h4">
|
<h2 class="text-h4 text-center">
|
||||||
{{ $t('profile.welcome-user', [user.fullName]) }}
|
{{ $t('profile.welcome-user', [user.fullName]) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="subtitle-1 mb-0 text-center">
|
<p class="subtitle-1 mb-0 text-center">
|
||||||
|
|
|
@ -7,8 +7,7 @@ export default defineNuxtPlugin(() => {
|
||||||
baseURL: "/", // api calls already pass with /api
|
baseURL: "/", // api calls already pass with /api
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
Authorization: "Bearer " + useCookie(tokenName).value,
|
||||||
"Authorization": "Bearer " + useCookie(tokenName).value,
|
|
||||||
},
|
},
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
1286
frontend/yarn.lock
1286
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
||||||
import abc
|
import abc
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Generic, TypeVar
|
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
@ -13,10 +12,8 @@ ALGORITHM = "HS256"
|
||||||
ISS = "mealie"
|
ISS = "mealie"
|
||||||
remember_me_duration = timedelta(days=14)
|
remember_me_duration = timedelta(days=14)
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
class AuthProvider[T](metaclass=abc.ABCMeta):
|
||||||
class AuthProvider(Generic[T], metaclass=abc.ABCMeta):
|
|
||||||
"""Base Authentication Provider interface"""
|
"""Base Authentication Provider interface"""
|
||||||
|
|
||||||
def __init__(self, session: Session, data: T) -> None:
|
def __init__(self, session: Session, data: T) -> None:
|
||||||
|
|
|
@ -449,7 +449,7 @@ class AppSettings(AppLoggingSettings):
|
||||||
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
||||||
"""
|
"""
|
||||||
app_settings_constructor is a factory function that returns an AppSettings object. It is used to inject the
|
app_settings_constructor is a factory function that returns an AppSettings object. It is used to inject the
|
||||||
required dependencies into the AppSettings object and nested child objects. AppSettings should not be substantiated
|
required dependencies into the AppSettings object and nested child objects. AppSettings should not be instantiated
|
||||||
directly, but rather through this factory function.
|
directly, but rather through this factory function.
|
||||||
"""
|
"""
|
||||||
secret_settings = {
|
secret_settings = {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import random
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any, Generic, TypeVar
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
|
@ -28,18 +28,13 @@ from mealie.schema.response.query_search import SearchFilter
|
||||||
|
|
||||||
from ._utils import NOT_SET, NotSet
|
from ._utils import NOT_SET, NotSet
|
||||||
|
|
||||||
Schema = TypeVar("Schema", bound=MealieModel)
|
|
||||||
Model = TypeVar("Model", bound=SqlAlchemyBase)
|
|
||||||
|
|
||||||
T = TypeVar("T", bound="RepositoryGeneric")
|
class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
|
||||||
|
|
||||||
|
|
||||||
class RepositoryGeneric(Generic[Schema, Model]):
|
|
||||||
"""A Generic BaseAccess Model method to perform common operations on the database
|
"""A Generic BaseAccess Model method to perform common operations on the database
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
Generic ([Schema]): Represents the Pydantic Model
|
Schema: Represents the Pydantic Model
|
||||||
Generic ([Model]): Represents the SqlAlchemyModel Model
|
Model: Represents the SqlAlchemyModel Model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
session: Session
|
session: Session
|
||||||
|
@ -467,7 +462,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||||
return search_filter.filter_query_by_search(query, schema, self.model)
|
return search_filter.filter_query_by_search(query, schema, self.model)
|
||||||
|
|
||||||
|
|
||||||
class GroupRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
class GroupRepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase](RepositoryGeneric[Schema, Model]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
@ -483,7 +478,7 @@ class GroupRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
||||||
self._group_id = group_id if group_id else None
|
self._group_id = group_id if group_id else None
|
||||||
|
|
||||||
|
|
||||||
class HouseholdRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
class HouseholdRepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase](RepositoryGeneric[Schema, Model]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
|
|
@ -6,20 +6,18 @@ See their repository for details -> https://github.com/dmontagu/fastapi-utils
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any, ClassVar, ForwardRef, TypeVar, cast, get_origin, get_type_hints
|
from typing import Any, ClassVar, ForwardRef, cast, get_origin, get_type_hints
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from starlette.routing import Route, WebSocketRoute
|
from starlette.routing import Route, WebSocketRoute
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
CBV_CLASS_KEY = "__cbv_class__"
|
CBV_CLASS_KEY = "__cbv_class__"
|
||||||
INCLUDE_INIT_PARAMS_KEY = "__include_init_params__"
|
INCLUDE_INIT_PARAMS_KEY = "__include_init_params__"
|
||||||
RETURN_TYPES_FUNC_KEY = "__return_types_func__"
|
RETURN_TYPES_FUNC_KEY = "__return_types_func__"
|
||||||
|
|
||||||
|
|
||||||
def controller(router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]:
|
def controller[T](router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]:
|
||||||
"""
|
"""
|
||||||
This function returns a decorator that converts the decorated into a class-based view for the provided router.
|
This function returns a decorator that converts the decorated into a class-based view for the provided router.
|
||||||
Any methods of the decorated class that are decorated as endpoints using the router provided to this function
|
Any methods of the decorated class that are decorated as endpoints using the router provided to this function
|
||||||
|
@ -36,7 +34,7 @@ def controller(router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]:
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def _cbv(router: APIRouter, cls: type[T], *urls: str, instance: Any | None = None) -> type[T]:
|
def _cbv[T](router: APIRouter, cls: type[T], *urls: str, instance: Any | None = None) -> type[T]:
|
||||||
"""
|
"""
|
||||||
Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated
|
Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated
|
||||||
function calls that will properly inject an instance of `cls`.
|
function calls that will properly inject an instance of `cls`.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from typing import Generic, TypeVar
|
|
||||||
|
|
||||||
import sqlalchemy.exc
|
import sqlalchemy.exc
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
@ -9,12 +8,8 @@ from pydantic import UUID4, BaseModel
|
||||||
from mealie.repos.repository_generic import RepositoryGeneric
|
from mealie.repos.repository_generic import RepositoryGeneric
|
||||||
from mealie.schema.response import ErrorResponse
|
from mealie.schema.response import ErrorResponse
|
||||||
|
|
||||||
C = TypeVar("C", bound=BaseModel)
|
|
||||||
R = TypeVar("R", bound=BaseModel)
|
|
||||||
U = TypeVar("U", bound=BaseModel)
|
|
||||||
|
|
||||||
|
class HttpRepo[C: BaseModel, R: BaseModel, U: BaseModel]:
|
||||||
class HttpRepo(Generic[C, R, U]):
|
|
||||||
"""
|
"""
|
||||||
The HttpRepo[C, R, U] class is a mixin class that provides a common set of methods for CRUD operations.
|
The HttpRepo[C, R, U] class is a mixin class that provides a common set of methods for CRUD operations.
|
||||||
This class is intended to be used in a composition pattern where a class has a mixin property. For example:
|
This class is intended to be used in a composition pattern where a class has a mixin property. For example:
|
||||||
|
|
|
@ -4,7 +4,7 @@ import re
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import ClassVar, Protocol, Self, TypeVar
|
from typing import ClassVar, Protocol, Self
|
||||||
|
|
||||||
from humps.main import camelize
|
from humps.main import camelize
|
||||||
from pydantic import UUID4, AliasChoices, BaseModel, ConfigDict, Field, model_validator
|
from pydantic import UUID4, AliasChoices, BaseModel, ConfigDict, Field, model_validator
|
||||||
|
@ -14,8 +14,6 @@ from sqlalchemy.orm.interfaces import LoaderOption
|
||||||
|
|
||||||
from mealie.db.models._model_base import SqlAlchemyBase
|
from mealie.db.models._model_base import SqlAlchemyBase
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
|
||||||
|
|
||||||
HOUR_ONLY_TZ_PATTERN = re.compile(r"[+-]\d{2}$")
|
HOUR_ONLY_TZ_PATTERN = re.compile(r"[+-]\d{2}$")
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +54,7 @@ class MealieModel(BaseModel):
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def fix_hour_only_tz(cls, data: T) -> T:
|
def fix_hour_only_tz[T: BaseModel](cls, data: T) -> T:
|
||||||
"""
|
"""
|
||||||
Fixes datetimes with timezones that only have the hour portion.
|
Fixes datetimes with timezones that only have the hour portion.
|
||||||
|
|
||||||
|
@ -82,7 +80,7 @@ class MealieModel(BaseModel):
|
||||||
Adds UTC timezone information to all datetimes in the model.
|
Adds UTC timezone information to all datetimes in the model.
|
||||||
The server stores everything in UTC without timezone info.
|
The server stores everything in UTC without timezone info.
|
||||||
"""
|
"""
|
||||||
for field in self.model_fields:
|
for field in self.__class__.model_fields:
|
||||||
val = getattr(self, field)
|
val = getattr(self, field)
|
||||||
if not isinstance(val, datetime):
|
if not isinstance(val, datetime):
|
||||||
continue
|
continue
|
||||||
|
@ -91,23 +89,25 @@ class MealieModel(BaseModel):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def cast(self, cls: type[T], **kwargs) -> T:
|
def cast[T: BaseModel](self, cls: type[T], **kwargs) -> T:
|
||||||
"""
|
"""
|
||||||
Cast the current model to another with additional arguments. Useful for
|
Cast the current model to another with additional arguments. Useful for
|
||||||
transforming DTOs into models that are saved to a database
|
transforming DTOs into models that are saved to a database
|
||||||
"""
|
"""
|
||||||
create_data = {field: getattr(self, field) for field in self.model_fields if field in cls.model_fields}
|
create_data = {
|
||||||
|
field: getattr(self, field) for field in self.__class__.model_fields if field in cls.model_fields
|
||||||
|
}
|
||||||
create_data.update(kwargs or {})
|
create_data.update(kwargs or {})
|
||||||
return cls(**create_data)
|
return cls(**create_data)
|
||||||
|
|
||||||
def map_to(self, dest: T) -> T:
|
def map_to[T: BaseModel](self, dest: T) -> T:
|
||||||
"""
|
"""
|
||||||
Map matching values from the current model to another model. Model returned
|
Map matching values from the current model to another model. Model returned
|
||||||
for method chaining.
|
for method chaining.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for field in self.model_fields:
|
for field in self.__class__.model_fields:
|
||||||
if field in dest.model_fields:
|
if field in dest.__class__.model_fields:
|
||||||
setattr(dest, field, getattr(self, field))
|
setattr(dest, field, getattr(self, field))
|
||||||
|
|
||||||
return dest
|
return dest
|
||||||
|
@ -117,18 +117,18 @@ class MealieModel(BaseModel):
|
||||||
Map matching values from another model to the current model.
|
Map matching values from another model to the current model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for field in src.model_fields:
|
for field in src.__class__.model_fields:
|
||||||
if field in self.model_fields:
|
if field in self.__class__.model_fields:
|
||||||
setattr(self, field, getattr(src, field))
|
setattr(self, field, getattr(src, field))
|
||||||
|
|
||||||
def merge(self, src: T, replace_null=False):
|
def merge[T: BaseModel](self, src: T, replace_null=False):
|
||||||
"""
|
"""
|
||||||
Replace matching values from another instance to the current instance.
|
Replace matching values from another instance to the current instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for field in src.model_fields:
|
for field in src.__class__.model_fields:
|
||||||
val = getattr(src, field)
|
val = getattr(src, field)
|
||||||
if field in self.model_fields and (val is not None or replace_null):
|
if field in self.__class__.model_fields and (val is not None or replace_null):
|
||||||
setattr(self, field, val)
|
setattr(self, field, val)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
|
||||||
U = TypeVar("U", bound=BaseModel)
|
|
||||||
|
|
||||||
|
def mapper[U: BaseModel, T: BaseModel](source: U, dest: T, **_) -> T:
|
||||||
def mapper(source: U, dest: T, **_) -> T:
|
|
||||||
"""
|
"""
|
||||||
Map a source model to a destination model. Only top-level fields are mapped.
|
Map a source model to a destination model. Only top-level fields are mapped.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for field in source.model_fields:
|
for field in source.__class__.model_fields:
|
||||||
if field in dest.model_fields:
|
if field in dest.__class__.model_fields:
|
||||||
setattr(dest, field, getattr(source, field))
|
setattr(dest, field, getattr(source, field))
|
||||||
|
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
|
|
||||||
def cast(source: U, dest: type[T], **kwargs) -> T:
|
def cast[U: BaseModel, T: BaseModel](source: U, dest: type[T], **kwargs) -> T:
|
||||||
create_data = {field: getattr(source, field) for field in source.model_fields if field in dest.model_fields}
|
create_data = {
|
||||||
|
field: getattr(source, field) for field in source.__class__.model_fields if field in dest.model_fields
|
||||||
|
}
|
||||||
create_data.update(kwargs or {})
|
create_data.update(kwargs or {})
|
||||||
return dest(**create_data)
|
return dest(**create_data)
|
||||||
|
|
|
@ -184,8 +184,9 @@ class OpenAIRecipe(OpenAIBase):
|
||||||
[],
|
[],
|
||||||
description=dedent(
|
description=dedent(
|
||||||
"""
|
"""
|
||||||
A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the
|
A list of instructions for the recipe. Each instruction should represent one step in the recipe,
|
||||||
recipe. If the recipe has no ingredients, you should return an empty list.
|
and should be inserted in the order they appear in the recipe. If the recipe has no instructions,
|
||||||
|
you should return an empty list.
|
||||||
|
|
||||||
Often times, but not always, instructions are separated by line breaks and/or separated by paragraphs.
|
Often times, but not always, instructions are separated by line breaks and/or separated by paragraphs.
|
||||||
Use these as a guide to separate instructions. They also may be separated by numbers or words, such as
|
Use these as a guide to separate instructions. They also may be separated by numbers or words, such as
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import enum
|
import enum
|
||||||
from typing import Annotated, Any, Generic, TypeVar
|
from typing import Annotated, Any
|
||||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||||
|
|
||||||
from humps import camelize
|
from humps import camelize
|
||||||
|
@ -8,8 +8,6 @@ from pydantic_core.core_schema import ValidationInfo
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
|
||||||
DataT = TypeVar("DataT", bound=BaseModel)
|
|
||||||
|
|
||||||
|
|
||||||
class OrderDirection(str, enum.Enum):
|
class OrderDirection(str, enum.Enum):
|
||||||
asc = "asc"
|
asc = "asc"
|
||||||
|
@ -50,7 +48,7 @@ class PaginationQuery(RequestQuery):
|
||||||
per_page: int = 50
|
per_page: int = 50
|
||||||
|
|
||||||
|
|
||||||
class PaginationBase(BaseModel, Generic[DataT]):
|
class PaginationBase[DataT: BaseModel](BaseModel):
|
||||||
page: int = 1
|
page: int = 1
|
||||||
per_page: int = 10
|
per_page: int = 10
|
||||||
total: int = 0
|
total: int = 0
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
import re
|
import re
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, TypeVar, cast
|
from typing import Any, cast
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
@ -19,8 +19,6 @@ from mealie.db.models._model_utils.datetime import NaiveDateTime
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
from mealie.db.models._model_utils.guid import GUID
|
||||||
from mealie.schema._mealie.mealie_model import MealieModel
|
from mealie.schema._mealie.mealie_model import MealieModel
|
||||||
|
|
||||||
Model = TypeVar("Model", bound=SqlAlchemyBase)
|
|
||||||
|
|
||||||
|
|
||||||
class RelationalKeyword(Enum):
|
class RelationalKeyword(Enum):
|
||||||
IS = "IS"
|
IS = "IS"
|
||||||
|
@ -274,7 +272,7 @@ class QueryFilterBuilder:
|
||||||
return consolidated_group_builder.self_group()
|
return consolidated_group_builder.self_group()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_model_and_model_attr_from_attr_string(
|
def get_model_and_model_attr_from_attr_string[Model: SqlAlchemyBase](
|
||||||
cls, attr_string: str, model: type[Model], *, query: sa.Select | None = None
|
cls, attr_string: str, model: type[Model], *, query: sa.Select | None = None
|
||||||
) -> tuple[SqlAlchemyBase, InstrumentedAttribute, sa.Select | None]:
|
) -> tuple[SqlAlchemyBase, InstrumentedAttribute, sa.Select | None]:
|
||||||
"""
|
"""
|
||||||
|
@ -343,7 +341,7 @@ class QueryFilterBuilder:
|
||||||
return model_attr
|
return model_attr
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_filter_element(
|
def _get_filter_element[Model: SqlAlchemyBase](
|
||||||
cls,
|
cls,
|
||||||
query: sa.Select,
|
query: sa.Select,
|
||||||
component: QueryFilterBuilderComponent,
|
component: QueryFilterBuilderComponent,
|
||||||
|
@ -397,7 +395,7 @@ class QueryFilterBuilder:
|
||||||
|
|
||||||
return element
|
return element
|
||||||
|
|
||||||
def filter_query(
|
def filter_query[Model: SqlAlchemyBase](
|
||||||
self, query: sa.Select, model: type[Model], column_aliases: dict[str, sa.ColumnElement] | None = None
|
self, query: sa.Select, model: type[Model], column_aliases: dict[str, sa.ColumnElement] | None = None
|
||||||
) -> sa.Select:
|
) -> sa.Select:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated, Any, Generic, TypeVar
|
from typing import Annotated, Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import UUID4, BaseModel, ConfigDict, Field, StringConstraints, field_validator
|
from pydantic import UUID4, BaseModel, ConfigDict, Field, StringConstraints, field_validator
|
||||||
|
@ -20,7 +20,6 @@ from mealie.schema.response.pagination import PaginationBase
|
||||||
from ...db.models.group import Group
|
from ...db.models.group import Group
|
||||||
from ..recipe import CategoryBase
|
from ..recipe import CategoryBase
|
||||||
|
|
||||||
DataT = TypeVar("DataT", bound=BaseModel)
|
|
||||||
DEFAULT_INTEGRATION_ID = "generic"
|
DEFAULT_INTEGRATION_ID = "generic"
|
||||||
settings = get_app_settings()
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
@ -102,7 +101,7 @@ class UserRatingOut(UserRatingCreate):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class UserRatings(BaseModel, Generic[DataT]):
|
class UserRatings[DataT: BaseModel](BaseModel):
|
||||||
ratings: list[DataT]
|
ratings: list[DataT]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
@ -12,8 +12,6 @@ from mealie.schema.recipe import RecipeCategory
|
||||||
from mealie.schema.recipe.recipe import RecipeTag
|
from mealie.schema.recipe.recipe import RecipeTag
|
||||||
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagOut, TagSave
|
from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagOut, TagSave
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mealie.repos.repository_generic import RepositoryGeneric
|
from mealie.repos.repository_generic import RepositoryGeneric
|
||||||
|
|
||||||
|
@ -23,7 +21,7 @@ class DatabaseMigrationHelpers:
|
||||||
self.session = session
|
self.session = session
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
def _get_or_set_generic(
|
def _get_or_set_generic[T: BaseModel](
|
||||||
self, accessor: RepositoryGeneric, items: Iterable[str], create_model: type[T], out_model: type[T]
|
self, accessor: RepositoryGeneric, items: Iterable[str], create_model: type[T], out_model: type[T]
|
||||||
) -> list[T]:
|
) -> list[T]:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
from rapidfuzz import fuzz, process
|
from rapidfuzz import fuzz, process
|
||||||
|
@ -17,8 +16,6 @@ from mealie.schema.recipe.recipe_ingredient import (
|
||||||
)
|
)
|
||||||
from mealie.schema.response.pagination import PaginationQuery
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
|
||||||
|
|
||||||
|
|
||||||
class DataMatcher:
|
class DataMatcher:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -83,7 +80,9 @@ class DataMatcher:
|
||||||
return self._units_by_alias
|
return self._units_by_alias
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_match(cls, match_value: str, *, store_map: dict[str, T], fuzzy_match_threshold: int = 0) -> T | None:
|
def find_match[T: BaseModel](
|
||||||
|
cls, match_value: str, *, store_map: dict[str, T], fuzzy_match_threshold: int = 0
|
||||||
|
) -> T | None:
|
||||||
# check for literal matches
|
# check for literal matches
|
||||||
if match_value in store_map:
|
if match_value in store_map:
|
||||||
return store_map[match_value]
|
return store_map[match_value]
|
||||||
|
|
65
poetry.lock
generated
65
poetry.lock
generated
|
@ -1855,14 +1855,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "1.88.0"
|
version = "1.92.2"
|
||||||
description = "The official Python library for the openai API"
|
description = "The official Python library for the openai API"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "openai-1.88.0-py3-none-any.whl", hash = "sha256:7edd7826b3b83f5846562a6f310f040c79576278bf8e3687b30ba05bb5dff978"},
|
{file = "openai-1.92.2-py3-none-any.whl", hash = "sha256:abb64bee7f2571709edf9a856f598ffe871730129a7d807a8a4d8d2958f5c842"},
|
||||||
{file = "openai-1.88.0.tar.gz", hash = "sha256:122d35e42998255cf1fc84560f6ee49a844e65c054cd05d3e42fda506b832bb1"},
|
{file = "openai-1.92.2.tar.gz", hash = "sha256:b571a79fc7e165e7d00e6963a8a95eb5f42b60ac89fd316f1dc0a2dac5c6fae1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -1876,6 +1876,7 @@ tqdm = ">4"
|
||||||
typing-extensions = ">=4.11,<5"
|
typing-extensions = ">=4.11,<5"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.6)"]
|
||||||
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
||||||
realtime = ["websockets (>=13,<16)"]
|
realtime = ["websockets (>=13,<16)"]
|
||||||
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
||||||
|
@ -2511,14 +2512,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-settings"
|
name = "pydantic-settings"
|
||||||
version = "2.9.1"
|
version = "2.10.1"
|
||||||
description = "Settings management using Pydantic"
|
description = "Settings management using Pydantic"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"},
|
{file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"},
|
||||||
{file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"},
|
{file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -2675,14 +2676,14 @@ requests = ">=2.32.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.4.0"
|
version = "8.4.1"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"},
|
{file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
|
||||||
{file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"},
|
{file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -2799,14 +2800,14 @@ six = ">=1.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"},
|
{file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"},
|
||||||
{file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"},
|
{file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
@ -3249,30 +3250,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.11.13"
|
version = "0.12.1"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"},
|
{file = "ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b"},
|
||||||
{file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"},
|
{file = "ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0"},
|
||||||
{file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"},
|
{file = "ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be"},
|
||||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"},
|
{file = "ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff"},
|
||||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"},
|
{file = "ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d"},
|
||||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"},
|
{file = "ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd"},
|
||||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"},
|
{file = "ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010"},
|
||||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"},
|
{file = "ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e"},
|
||||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"},
|
{file = "ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed"},
|
||||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"},
|
{file = "ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc"},
|
||||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"},
|
{file = "ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9"},
|
||||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"},
|
{file = "ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13"},
|
||||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"},
|
{file = "ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c"},
|
||||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"},
|
{file = "ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6"},
|
||||||
{file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"},
|
{file = "ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245"},
|
||||||
{file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"},
|
{file = "ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013"},
|
||||||
{file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"},
|
{file = "ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc"},
|
||||||
{file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"},
|
{file = "ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3884,4 +3885,4 @@ pgsql = ["psycopg2-binary"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.12,<3.13"
|
python-versions = ">=3.12,<3.13"
|
||||||
content-hash = "2b8479e18ef741f5254b8c9d64566bf42d597cfb6564c1aa622f6a1afb117402"
|
content-hash = "632cd8ef199c2668bc799a1cf4f370161dc13ff7dcf76ed40f3c94a0896e304f"
|
||||||
|
|
|
@ -69,7 +69,7 @@ pylint = "^3.0.0"
|
||||||
pytest = "^8.0.0"
|
pytest = "^8.0.0"
|
||||||
pytest-asyncio = "^1.0.0"
|
pytest-asyncio = "^1.0.0"
|
||||||
rich = "^14.0.0"
|
rich = "^14.0.0"
|
||||||
ruff = "^0.11.0"
|
ruff = "^0.12.0"
|
||||||
types-PyYAML = "^6.0.4"
|
types-PyYAML = "^6.0.4"
|
||||||
types-python-dateutil = "^2.8.18"
|
types-python-dateutil = "^2.8.18"
|
||||||
types-python-slugify = "^6.0.0"
|
types-python-slugify = "^6.0.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue