mirror of
https://github.com/hay-kot/mealie.git
synced 2025-07-06 04:52:25 -07:00
feat: create recipe from multiple images (#5590)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Docker Nightly Production / Backend Server Tests (push) Waiting to run
Docker Nightly Production / Frontend Tests (push) Waiting to run
Docker Nightly Production / Build Package (push) Waiting to run
Docker Nightly Production / Build Tagged Release (push) Blocked by required conditions
Docker Nightly Production / Notify Discord (push) Blocked by required conditions
Release Drafter / ✏️ Draft release (push) Waiting to run
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Docker Nightly Production / Backend Server Tests (push) Waiting to run
Docker Nightly Production / Frontend Tests (push) Waiting to run
Docker Nightly Production / Build Package (push) Waiting to run
Docker Nightly Production / Build Tagged Release (push) Blocked by required conditions
Docker Nightly Production / Notify Discord (push) Blocked by required conditions
Release Drafter / ✏️ Draft release (push) Waiting to run
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> Co-authored-by: Kuchenpirat <jojow@gmx.net> Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
parent
084f99b0de
commit
95fa0af28a
8 changed files with 149 additions and 131 deletions
|
@ -231,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,35 +91,52 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
async function upload() {
|
async function upload() {
|
||||||
if (file.value != null) {
|
if (files.value.length === 0) {
|
||||||
isSelecting.value = true;
|
return;
|
||||||
|
|
||||||
if (!props.post) {
|
|
||||||
context.emit(UPLOAD_EVENT, file.value);
|
|
||||||
isSelecting.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append(props.fileName, file.value);
|
|
||||||
try {
|
|
||||||
const response = await api.upload.file(props.url, formData);
|
|
||||||
if (response) {
|
|
||||||
context.emit(UPLOAD_EVENT, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
context.emit(UPLOAD_EVENT, null);
|
|
||||||
}
|
|
||||||
isSelecting.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSelecting.value = true;
|
||||||
|
|
||||||
|
if (!props.post) {
|
||||||
|
// 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;
|
||||||
|
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();
|
||||||
|
formData.append(props.fileName, file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.upload.file(props.url, formData);
|
||||||
|
if (response) {
|
||||||
|
context.emit(UPLOAD_EVENT, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
context.emit(UPLOAD_EVENT, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,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>) {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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">
|
||||||
<ImageCropper
|
<v-row>
|
||||||
:img="uploadedImagePreviewUrl"
|
<v-col cols="auto" align-self="center">
|
||||||
cropper-height="50vh"
|
<ImageCropper
|
||||||
cropper-width="100%"
|
:img="imageUrl"
|
||||||
@save="updateUploadedImage"
|
cropper-height="100%"
|
||||||
/>
|
cropper-width="100%"
|
||||||
|
@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?.value);
|
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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue