mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-20 07:50:16 -04:00
Compare commits
28 Commits
v1.0.0-bet
...
v1.0.0beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b981cf62bf | ||
|
|
ee93d77ace | ||
|
|
3dcfcc1fa9 | ||
|
|
80f1a9add8 | ||
|
|
137bf9de91 | ||
|
|
1534f0df77 | ||
|
|
d751e3b35b | ||
|
|
07bf5be3ec | ||
|
|
a96f94a149 | ||
|
|
78a8204b58 | ||
|
|
649e34f66e | ||
|
|
010aafa69b | ||
|
|
d66d6c55ae | ||
|
|
7609715d9e | ||
|
|
921fceddea | ||
|
|
01f3fef21f | ||
|
|
8f7c7c39bb | ||
|
|
30d19c6503 | ||
|
|
ea503a0235 | ||
|
|
c05c123880 | ||
|
|
6f45de6167 | ||
|
|
d634e2fbe1 | ||
|
|
43a566339a | ||
|
|
cc284a0ceb | ||
|
|
3c19105d8b | ||
|
|
b8ee1a4bd8 | ||
|
|
c30ffbc851 | ||
|
|
3ddbc033b2 |
6
.github/stale.yml
vendored
6
.github/stale.yml
vendored
@@ -6,8 +6,12 @@ daysUntilClose: 7
|
|||||||
exemptLabels:
|
exemptLabels:
|
||||||
- pinned
|
- pinned
|
||||||
- security
|
- security
|
||||||
|
- early-stages
|
||||||
|
- "bug: confirmed"
|
||||||
|
- feedback
|
||||||
|
- task
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: stale
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
|||||||
4
.github/workflows/backend-docker-nightly.yml
vendored
4
.github/workflows/backend-docker-nightly.yml
vendored
@@ -5,6 +5,10 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- mealie-next
|
- mealie-next
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: backend-nightly-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- mealie-next
|
- mealie-next
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: frontend-nightly-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -46,5 +46,12 @@
|
|||||||
"python.linting.mypyEnabled": true,
|
"python.linting.mypyEnabled": true,
|
||||||
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
|
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
|
||||||
"search.mode": "reuseEditor",
|
"search.mode": "reuseEditor",
|
||||||
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test_*.py"]
|
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test_*.py"],
|
||||||
|
"explorer.fileNesting.enabled": true,
|
||||||
|
"explorer.fileNesting.patterns": {
|
||||||
|
"package.json": "package-lock.json, yarn.lock, .eslintrc.js, tsconfig.json, .prettierrc, .editorconfig",
|
||||||
|
"pyproject.toml": "poetry.lock, alembic.ini, .pylintrc, .flake8",
|
||||||
|
"netlify.toml": "runtime.txt",
|
||||||
|
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -5,12 +5,6 @@
|
|||||||
[![MIT License][license-shield]][license-url]
|
[![MIT License][license-shield]][license-url]
|
||||||
[![Docker Pulls][docker-pull]][docker-pull]
|
[![Docker Pulls][docker-pull]][docker-pull]
|
||||||
[](https://www.codefactor.io/repository/github/hay-kot/mealie)
|
[](https://www.codefactor.io/repository/github/hay-kot/mealie)
|
||||||
[](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.release.yml)
|
|
||||||
[](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
|
|
||||||
[](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.dev.yml)
|
|
||||||
[](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- PROJECT LOGO -->
|
<!-- PROJECT LOGO -->
|
||||||
<br />
|
<br />
|
||||||
@@ -26,20 +20,14 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
A Place for All Your Recipes
|
A Place for All Your Recipes
|
||||||
<br />
|
<br />
|
||||||
<a href="https://hay-kot.github.io/mealie/"><strong>Explore the docs »</strong></a>
|
<a href="https://nightly.mealie.io"><strong>Explore the docs »</strong></a>
|
||||||
<a href="https://github.com/hay-kot/mealie">
|
<a href="https://github.com/hay-kot/mealie">
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
<a href="https://mealie-demo.hay-kot.dev/">View Demo</a>
|
<a href="https://demo.mealie.io/">View Demo</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||||
·
|
·
|
||||||
<a href="https://hay-kot.github.io/mealie/api/redoc/">API</a>
|
|
||||||
·
|
|
||||||
<a href="https://github.com/hay-kot/mealie/issues">
|
|
||||||
Request Feature
|
|
||||||
</a>
|
|
||||||
·
|
|
||||||
<a href="https://hub.docker.com/r/hkotel/mealie"> Docker Hub
|
<a href="https://hub.docker.com/r/hkotel/mealie"> Docker Hub
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -63,7 +51,7 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
|||||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you're going to be working on the code-base you'll want to use the nightly documentation to ensure you get the latest information.
|
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you're going to be working on the code-base you'll want to use the nightly documentation to ensure you get the latest information.
|
||||||
|
|
||||||
- See the [Contributors Guide](https://nightly.mealie.io/contributors/developers-guide/code-contributions/) for help getting started.
|
- See the [Contributors Guide](https://nightly.mealie.io/contributors/developers-guide/code-contributions/) for help getting started.
|
||||||
- We use VSCode Dev Contains to make it easy for contributors to get started!
|
- We use [VSCode Dev Containers](https://code.visualstudio.com/docs/remote/containers) to make it easy for contributors to get started!
|
||||||
|
|
||||||
If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
|
If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# WARNING: currently not functional, see #756, #1072
|
||||||
# Use root/example as user/password credentials
|
# Use root/example as user/password credentials
|
||||||
version: "3.4"
|
version: "3.4"
|
||||||
services:
|
services:
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
auto_https off
|
|
||||||
}
|
|
||||||
|
|
||||||
:80 {
|
|
||||||
root * /srv
|
|
||||||
encode gzip
|
|
||||||
uri strip_suffix /
|
|
||||||
|
|
||||||
handle {
|
|
||||||
try_files {path} {path}/ /index.html
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM python:3.8-slim as build-stage
|
|
||||||
WORKDIR /app
|
|
||||||
RUN pip install --no-cache-dir mkdocs mkdocs-material
|
|
||||||
COPY . .
|
|
||||||
RUN mkdocs build
|
|
||||||
|
|
||||||
FROM caddy:alpine
|
|
||||||
WORKDIR /app
|
|
||||||
COPY ./Caddyfile /etc/caddy/Caddyfile
|
|
||||||
COPY --from=build-stage /app/site /srv
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
services:
|
|
||||||
wiki:
|
|
||||||
container_name: mealie-docs
|
|
||||||
image: mealie-docs
|
|
||||||
ports:
|
|
||||||
- 8888:80
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
restart: always
|
|
||||||
29
docs/docs/changelog/v1.0.0beta-2.md
Normal file
29
docs/docs/changelog/v1.0.0beta-2.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend ([#1257](https://github.com/orhun/git-cliff/issues/1257))
|
||||||
|
- Bump @nuxtjs/auth-next in /frontend ([#1265](https://github.com/orhun/git-cliff/issues/1265))
|
||||||
|
- Bad dev dependency ([#1281](https://github.com/orhun/git-cliff/issues/1281))
|
||||||
|
- Add touch support for mealplanner delete ([#1298](https://github.com/orhun/git-cliff/issues/1298))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Add references for VSCode dev containers ([#1299](https://github.com/orhun/git-cliff/issues/1299))
|
||||||
|
- Docker-compose.dev.yml is currently not functional ([#1300](https://github.com/orhun/git-cliff/issues/1300))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add reports to bulk recipe import (url) ([#1294](https://github.com/orhun/git-cliff/issues/1294))
|
||||||
|
- Rewrite print implementation to support new ing ([#1305](https://github.com/orhun/git-cliff/issues/1305))
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Github stalebot changes ([#1271](https://github.com/orhun/git-cliff/issues/1271))
|
||||||
|
- Bump eslint-plugin-nuxt in /frontend ([#1258](https://github.com/orhun/git-cliff/issues/1258))
|
||||||
|
- Bump @vue/runtime-dom in /frontend ([#1259](https://github.com/orhun/git-cliff/issues/1259))
|
||||||
|
- Bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend ([#1260](https://github.com/orhun/git-cliff/issues/1260))
|
||||||
|
- Bump vue2-script-setup-transform in /frontend ([#1263](https://github.com/orhun/git-cliff/issues/1263))
|
||||||
|
- Update dev dependencies ([#1282](https://github.com/orhun/git-cliff/issues/1282))
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Split up recipe create page ([#1283](https://github.com/orhun/git-cliff/issues/1283))
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
After reading through the [Code Contributions Guide](../developers-guide/code-contributions.md) and forking the repo you can start working. This project is developed with :whale: docker and as such you will be greatly aided by using docker for development. It's not necessary but it is helpful.
|
After reading through the [Code Contributions Guide](../developers-guide/code-contributions.md) and forking the repo you can start working. This project is developed with :whale: docker and as such you will be greatly aided by using docker for development. It's not necessary but it is helpful.
|
||||||
|
|
||||||
## With VS Code Dev Containers
|
## With [VSCode Dev Containers](https://code.visualstudio.com/docs/remote/containers)
|
||||||
|
|
||||||
Prerequisites
|
Prerequisites
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ frontend 🎬 Start Mealie Frontend Development Server
|
|||||||
frontend-build 🏗 Build Frontend in frontend/dist
|
frontend-build 🏗 Build Frontend in frontend/dist
|
||||||
frontend-generate 🏗 Generate Code for Frontend
|
frontend-generate 🏗 Generate Code for Frontend
|
||||||
frontend-lint 🧺 Run yarn lint
|
frontend-lint 🧺 Run yarn lint
|
||||||
docker-dev 🐳 Build and Start Docker Development Stack
|
docker-dev 🐳 Build and Start Docker Development Stack (currently not functional, see #756, #1072)
|
||||||
docker-prod 🐳 Build and Start Docker Production Stack
|
docker-prod 🐳 Build and Start Docker Production Stack
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor.
|
Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor.
|
||||||
|
|
||||||
[Demo](https://beta.mealie.io/recipe/create?tab=url){ .md-button .md-button--primary .align-right }
|
[Demo](https://beta.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right }
|
||||||
|
|
||||||
### Importing Recipes
|
### Importing Recipes
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
version: "3.7"
|
version: "3.7"
|
||||||
services:
|
services:
|
||||||
mealie-frontend:
|
mealie-frontend:
|
||||||
image: hkotel/mealie:frontend-nightly
|
image: hkotel/mealie:frontend-v1.0.0-beta-1
|
||||||
container_name: mealie-frontend
|
container_name: mealie-frontend
|
||||||
depends_on:
|
depends_on:
|
||||||
- mealie-api
|
- mealie-api
|
||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- mealie-data:/app/data/ # (3)
|
- mealie-data:/app/data/ # (3)
|
||||||
mealie-api:
|
mealie-api:
|
||||||
image: hkotel/mealie:api-nightly
|
image: hkotel/mealie:api-v1.0.0-beta-1
|
||||||
container_name: mealie-api
|
container_name: mealie-api
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
|||||||
version: "3.7"
|
version: "3.7"
|
||||||
services:
|
services:
|
||||||
mealie-frontend:
|
mealie-frontend:
|
||||||
image: hkotel/mealie:frontend-nightly
|
image: hkotel/mealie:frontend-v1.0.0-beta-1
|
||||||
container_name: mealie-frontend
|
container_name: mealie-frontend
|
||||||
environment:
|
environment:
|
||||||
# Set Frontend ENV Variables Here
|
# Set Frontend ENV Variables Here
|
||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- mealie-data:/app/data/ # (3)
|
- mealie-data:/app/data/ # (3)
|
||||||
mealie-api:
|
mealie-api:
|
||||||
image: hkotel/mealie:api-nightly
|
image: hkotel/mealie:api-v1.0.0-beta-1
|
||||||
container_name: mealie-api
|
container_name: mealie-api
|
||||||
volumes:
|
volumes:
|
||||||
- mealie-data:/app/data/
|
- mealie-data:/app/data/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -88,6 +88,7 @@ nav:
|
|||||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||||
|
|
||||||
- Change Log:
|
- Change Log:
|
||||||
|
- v1.0.0beta-2: "changelog/v1.0.0beta-2.md"
|
||||||
- v1.0.0 Beta: "changelog/v1.0.0.md"
|
- v1.0.0 Beta: "changelog/v1.0.0.md"
|
||||||
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"
|
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"
|
||||||
- v0.5.1 Bug Fixes: "changelog/v0.5.1.md"
|
- v0.5.1 Bug Fixes: "changelog/v0.5.1.md"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
settings: {
|
||||||
|
"import/ignore": ["@vueuse*"],
|
||||||
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
@@ -35,6 +38,7 @@ module.exports = {
|
|||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
"vue/multiline-html-element-content-newline": "off",
|
"vue/multiline-html-element-content-newline": "off",
|
||||||
"vue/no-mutating-props": "off",
|
"vue/no-mutating-props": "off",
|
||||||
|
"vue/no-v-text-v-html-on-component": "warn",
|
||||||
"vue/no-v-for-template-key-on-child": "off",
|
"vue/no-v-for-template-key-on-child": "off",
|
||||||
"vue/valid-v-slot": [
|
"vue/valid-v-slot": [
|
||||||
"error",
|
"error",
|
||||||
|
|||||||
@@ -28,9 +28,13 @@
|
|||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
|
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-text="item.title"></v-list-item-title>
|
<v-list-item-title>
|
||||||
|
{{ item.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
|
||||||
<v-list-item-subtitle v-text="item.text"></v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
|
{{ item.text }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ $d(Date.parse(item.timeStamp), "long") }}
|
{{ $d(Date.parse(item.timeStamp), "long") }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
@@ -103,5 +107,4 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -10,13 +10,17 @@
|
|||||||
<v-list-item-icon class="ma-auto">
|
<v-list-item-icon class="ma-auto">
|
||||||
<v-tooltip bottom>
|
<v-tooltip bottom>
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-icon v-bind="attrs" v-on="on" v-text="getIconDefinition(item.icon).icon"></v-icon>
|
<v-icon v-bind="attrs" v-on="on">
|
||||||
|
{{ getIconDefinition(item.icon).icon }}
|
||||||
|
</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<span>{{ getIconDefinition(item.icon).title }}</span>
|
<span>{{ getIconDefinition(item.icon).title }}</span>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title class="pl-2" v-text="item.name"></v-list-item-title>
|
<v-list-item-title class="pl-2">
|
||||||
|
{{ item.name }}
|
||||||
|
</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
|
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
|
||||||
@@ -35,21 +39,21 @@
|
|||||||
<div class="d-flex ml-auto mt-2">
|
<div class="d-flex ml-auto mt-2">
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="newAssetDialog"
|
v-model="state.newAssetDialog"
|
||||||
:title="$t('asset.new-asset')"
|
:title="$tc('asset.new-asset')"
|
||||||
:icon="getIconDefinition(newAsset.icon).icon"
|
:icon="getIconDefinition(state.newAsset.icon).icon"
|
||||||
@submit="addAsset"
|
@submit="addAsset"
|
||||||
>
|
>
|
||||||
<template #activator>
|
<template #activator>
|
||||||
<BaseButton v-if="edit" small create @click="newAssetDialog = true" />
|
<BaseButton v-if="edit" small create @click="newAssetDialog = true" />
|
||||||
</template>
|
</template>
|
||||||
<v-card-text class="pt-4">
|
<v-card-text class="pt-4">
|
||||||
<v-text-field v-model="newAsset.name" dense :label="$t('general.name')"></v-text-field>
|
<v-text-field v-model="state.newAsset.name" dense :label="$t('general.name')"></v-text-field>
|
||||||
<div class="d-flex justify-space-between">
|
<div class="d-flex justify-space-between">
|
||||||
<v-select
|
<v-select
|
||||||
v-model="newAsset.icon"
|
v-model="state.newAsset.icon"
|
||||||
dense
|
dense
|
||||||
:prepend-icon="getIconDefinition(newAsset.icon).icon"
|
:prepend-icon="getIconDefinition(state.newAsset.icon).icon"
|
||||||
:items="iconOptions"
|
:items="iconOptions"
|
||||||
item-text="title"
|
item-text="title"
|
||||||
item-value="name"
|
item-value="name"
|
||||||
@@ -66,7 +70,7 @@
|
|||||||
</v-select>
|
</v-select>
|
||||||
<AppButtonUpload :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
|
<AppButtonUpload :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
|
||||||
</div>
|
</div>
|
||||||
{{ fileObject.name }}
|
{{ state.fileObject.name }}
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,9 +78,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, reactive, useContext } from "@nuxtjs/composition-api";
|
||||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
|
import { RecipeAsset } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
const BASE_URL = window.location.origin;
|
const BASE_URL = window.location.origin;
|
||||||
|
|
||||||
@@ -91,7 +96,7 @@ export default defineComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: Array,
|
type: Array as () => RecipeAsset[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
@@ -181,7 +186,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...toRefs(state),
|
state,
|
||||||
addAsset,
|
addAsset,
|
||||||
assetURL,
|
assetURL,
|
||||||
assetEmbed,
|
assetEmbed,
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
<v-list dense>
|
<v-list dense>
|
||||||
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon :color="item.color" v-text="item.icon"></v-icon>
|
<v-icon :color="item.color">
|
||||||
|
{{ item.icon }}
|
||||||
|
</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
<v-list dense>
|
<v-list dense>
|
||||||
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon :color="item.color" v-text="item.icon"></v-icon>
|
<v-icon :color="item.color"> {{ item.icon }} </v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
@@ -307,7 +307,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Note: Print is handled as an event in the parent component
|
// Note: Print is handled as an event in the parent component
|
||||||
const eventHandlers: { [key: string]: () => void } = {
|
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||||
delete: () => {
|
delete: () => {
|
||||||
state.recipeDeleteDialog = true;
|
state.recipeDeleteDialog = true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
<img src="https://i.pravatar.cc/300" alt="John" />
|
<img src="https://i.pravatar.cc/300" alt="John" />
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-text="getMember(item.userId)"></v-list-item-title>
|
<v-list-item-title>
|
||||||
|
{{ getMember(item.userId) }}
|
||||||
|
</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
@@ -95,31 +97,30 @@ export default defineComponent({
|
|||||||
context.emit(INPUT_EVENT, value);
|
context.emit(INPUT_EVENT, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = props.showHeaders;
|
|
||||||
const headers = computed(() => {
|
const headers = computed(() => {
|
||||||
const hdrs = [];
|
const hdrs = [];
|
||||||
|
|
||||||
if (show.id) {
|
if (props.showHeaders.id) {
|
||||||
hdrs.push({ text: "Id", value: "id" });
|
hdrs.push({ text: "Id", value: "id" });
|
||||||
}
|
}
|
||||||
if (show.owner) {
|
if (props.showHeaders.owner) {
|
||||||
hdrs.push({ text: "Owner", value: "userId", align: "center" });
|
hdrs.push({ text: "Owner", value: "userId", align: "center" });
|
||||||
}
|
}
|
||||||
hdrs.push({ text: "Name", value: "name" });
|
hdrs.push({ text: "Name", value: "name" });
|
||||||
if (show.categories) {
|
if (props.showHeaders.categories) {
|
||||||
hdrs.push({ text: "Categories", value: "recipeCategory" });
|
hdrs.push({ text: "Categories", value: "recipeCategory" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show.tags) {
|
if (props.showHeaders.tags) {
|
||||||
hdrs.push({ text: "Tags", value: "tags" });
|
hdrs.push({ text: "Tags", value: "tags" });
|
||||||
}
|
}
|
||||||
if (show.tools) {
|
if (props.showHeaders.tools) {
|
||||||
hdrs.push({ text: "Tools", value: "tools" });
|
hdrs.push({ text: "Tools", value: "tools" });
|
||||||
}
|
}
|
||||||
if (show.recipeYield) {
|
if (props.showHeaders.recipeYield) {
|
||||||
hdrs.push({ text: "Yield", value: "recipeYield" });
|
hdrs.push({ text: "Yield", value: "recipeYield" });
|
||||||
}
|
}
|
||||||
if (show.dateAdded) {
|
if (props.showHeaders.dateAdded) {
|
||||||
hdrs.push({ text: "Date Added", value: "dateAdded" });
|
hdrs.push({ text: "Date Added", value: "dateAdded" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,8 +57,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, toRefs, reactive, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, computed, toRefs, reactive, useContext } from "@nuxtjs/composition-api";
|
||||||
import { whenever } from "@vueuse/shared";
|
import { useClipboard, useShare, whenever } from "@vueuse/core";
|
||||||
import { useClipboard, useShare } from "@vueuse/core";
|
|
||||||
import { RecipeShareToken } from "~/types/api-types/recipe";
|
import { RecipeShareToken } from "~/types/api-types/recipe";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
|
|||||||
@@ -134,8 +134,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { value } = props;
|
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Foods
|
// Foods
|
||||||
const { foods, workingFoodData, actions: foodActions } = useFoods();
|
const { foods, workingFoodData, actions: foodActions } = useFoods();
|
||||||
@@ -144,7 +142,7 @@ export default defineComponent({
|
|||||||
async function createAssignFood() {
|
async function createAssignFood() {
|
||||||
workingFoodData.name = foodSearch.value;
|
workingFoodData.name = foodSearch.value;
|
||||||
await foodActions.createOne();
|
await foodActions.createOne();
|
||||||
value.food = foods.value?.find((food) => food.name === foodSearch.value);
|
props.value.food = foods.value?.find((food) => food.name === foodSearch.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
@@ -155,7 +153,7 @@ export default defineComponent({
|
|||||||
async function createAssignUnit() {
|
async function createAssignUnit() {
|
||||||
workingUnitData.name = unitSearch.value;
|
workingUnitData.name = unitSearch.value;
|
||||||
await unitActions.createOne();
|
await unitActions.createOne();
|
||||||
value.unit = units.value?.find((unit) => unit.name === unitSearch.value);
|
props.value.unit = units.value?.find((unit) => unit.name === unitSearch.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -165,7 +163,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
function toggleTitle() {
|
function toggleTitle() {
|
||||||
if (state.showTitle) {
|
if (state.showTitle) {
|
||||||
value.title = "";
|
props.value.title = "";
|
||||||
}
|
}
|
||||||
state.showTitle = !state.showTitle;
|
state.showTitle = !state.showTitle;
|
||||||
}
|
}
|
||||||
@@ -175,13 +173,21 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleUnitEnter() {
|
function handleUnitEnter() {
|
||||||
if (value.unit === undefined || value.unit === null || !value.unit.name.includes(unitSearch.value)) {
|
if (
|
||||||
|
props.value.unit === undefined ||
|
||||||
|
props.value.unit === null ||
|
||||||
|
!props.value.unit.name.includes(unitSearch.value)
|
||||||
|
) {
|
||||||
createAssignUnit();
|
createAssignUnit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFoodEnter() {
|
function handleFoodEnter() {
|
||||||
if (value.food === undefined || value.food === null || !value.food.name.includes(foodSearch.value)) {
|
if (
|
||||||
|
props.value.food === undefined ||
|
||||||
|
props.value.food === null ||
|
||||||
|
!props.value.food.name.includes(foodSearch.value)
|
||||||
|
) {
|
||||||
createAssignFood();
|
createAssignFood();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,7 +208,7 @@ export default defineComponent({
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (value.originalText) {
|
if (props.value.originalText) {
|
||||||
options.push({
|
options.push({
|
||||||
text: "See Original Text",
|
text: "See Original Text",
|
||||||
event: "toggle-original",
|
event: "toggle-original",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
@click="toggleCollapseSection(index)"
|
@click="toggleCollapseSection(index)"
|
||||||
>
|
>
|
||||||
<v-toolbar-title v-if="!edit" class="headline">
|
<v-toolbar-title v-if="!edit" class="headline">
|
||||||
<v-app-bar-title v-text="step.title"> </v-app-bar-title>
|
<v-app-bar-title> {{ step.title }} </v-app-bar-title>
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
|
|||||||
@@ -1,68 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="print-container">
|
||||||
<div v-if="recipe" class="container print">
|
<section>
|
||||||
<div>
|
<v-card-title class="headline pl-0">
|
||||||
<h1>
|
<v-icon left color="primary">
|
||||||
<svg class="icon" viewBox="0 0 24 24">
|
{{ $globals.icons.primary }}
|
||||||
<path
|
|
||||||
fill="#E58325"
|
|
||||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{{ recipe.name }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div class="time-container">
|
|
||||||
<RecipeTimeCard
|
|
||||||
:prep-time="recipe.prepTime"
|
|
||||||
:total-time="recipe.totalTime"
|
|
||||||
:perform-time="recipe.performTime"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<v-btn
|
|
||||||
v-if="recipe.recipeYield"
|
|
||||||
dense
|
|
||||||
small
|
|
||||||
:hover="false"
|
|
||||||
type="label"
|
|
||||||
:ripple="false"
|
|
||||||
elevation="0"
|
|
||||||
color="secondary darken-1"
|
|
||||||
class="rounded-sm static"
|
|
||||||
>
|
|
||||||
{{ recipe.recipeYield }}
|
|
||||||
</v-btn>
|
|
||||||
<div>
|
|
||||||
<VueMarkdown :source="recipe.description"> </VueMarkdown>
|
|
||||||
<h2>{{ $t("recipe.ingredients") }}</h2>
|
|
||||||
<ul>
|
|
||||||
<li v-for="(ingredient, index) in recipe.recipeIngredient" :key="index">
|
|
||||||
<v-icon>
|
|
||||||
{{ $globals.icons.checkboxBlankOutline }}
|
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<p>{{ ingredient.note }}</p>
|
{{ recipe.name }}
|
||||||
|
</v-card-title>
|
||||||
|
<RecipeTimeCard :prep-time="recipe.prepTime" :total-time="recipe.totalTime" :perform-time="recipe.performTime" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<v-card-text class="px-0">
|
||||||
|
<VueMarkdown :source="recipe.description" />
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<v-card-title class="headline pl-0"> {{ $t("recipe.ingredients") }} </v-card-title>
|
||||||
|
<div class="ingredient-grid">
|
||||||
|
<div class="ingredient-col-1">
|
||||||
|
<ul>
|
||||||
|
<li v-for="(text, index) in splitIngredients.value.firstHalf" :key="index">
|
||||||
|
{{ text }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="ingredient-col-2">
|
||||||
<h2>{{ $t("recipe.instructions") }}</h2>
|
<ul>
|
||||||
|
<li v-for="(text, index) in splitIngredients.value.secondHalf" :key="index">
|
||||||
|
{{ text }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<v-card-title class="headline pl-0">{{ $t("recipe.instructions") }}</v-card-title>
|
||||||
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
|
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
|
||||||
<h2 v-if="step.title">{{ step.title }}</h2>
|
<h3 v-if="step.title" class="mb-2">{{ step.title }}</h3>
|
||||||
<div class="ml-5">
|
<div class="ml-5">
|
||||||
<h3>{{ $t("recipe.step-index", { step: index + 1 }) }}</h3>
|
<h4>{{ $t("recipe.step-index", { step: index + 1 }) }}</h4>
|
||||||
<VueMarkdown :source="step.text"> </VueMarkdown>
|
<VueMarkdown :source="step.text" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<br />
|
<v-divider v-if="hasNotes" class="grey my-4"></v-divider>
|
||||||
<v-divider v-if="recipe.notes.length > 0" class="mb-5 mt-0"></v-divider>
|
|
||||||
|
|
||||||
|
<section>
|
||||||
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
||||||
<h3>{{ note.title }}</h3>
|
<h4>{{ note.title }}</h4>
|
||||||
<VueMarkdown :source="note.text"> </VueMarkdown>
|
<VueMarkdown :source="note.text" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -70,8 +60,16 @@
|
|||||||
import { defineComponent } from "@nuxtjs/composition-api";
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
// @ts-ignore vue-markdown has no types
|
// @ts-ignore vue-markdown has no types
|
||||||
import VueMarkdown from "@adapttive/vue-markdown";
|
import VueMarkdown from "@adapttive/vue-markdown";
|
||||||
|
import { computed } from "@vue/reactivity";
|
||||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
import { parseIngredientText } from "~/composables/recipes";
|
||||||
|
|
||||||
|
type SplitIngredients = {
|
||||||
|
firstHalf: string[];
|
||||||
|
secondHalf: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeTimeCard,
|
RecipeTimeCard,
|
||||||
@@ -83,6 +81,36 @@ export default defineComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup(props) {
|
||||||
|
const splitIngredients = computed<SplitIngredients>(() => {
|
||||||
|
const firstHalf = props.recipe.recipeIngredient
|
||||||
|
?.slice(0, Math.ceil(props.recipe.recipeIngredient.length / 2))
|
||||||
|
.map((ingredient) => {
|
||||||
|
return parseIngredientText(ingredient, props.recipe?.settings?.disableAmount || false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondHalf = props.recipe.recipeIngredient
|
||||||
|
?.slice(Math.ceil(props.recipe.recipeIngredient.length / 2))
|
||||||
|
.map((ingredient) => {
|
||||||
|
return parseIngredientText(ingredient, props.recipe?.settings?.disableAmount || false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstHalf: firstHalf || [],
|
||||||
|
secondHalf: secondHalf || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNotes = computed(() => {
|
||||||
|
return props.recipe.notes && props.recipe.notes.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasNotes,
|
||||||
|
splitIngredients,
|
||||||
|
parseIngredientText,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -90,26 +118,46 @@ export default defineComponent({
|
|||||||
@media print {
|
@media print {
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
margin-top: -40px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
display: -webkit-box;
|
|
||||||
display: flex;
|
|
||||||
font-size: 2rem;
|
|
||||||
letter-spacing: -0.015625em;
|
|
||||||
font-weight: 300;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.print-container {
|
||||||
margin-bottom: 0.25rem;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.v-main {
|
||||||
margin-bottom: 0.25rem;
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main__wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.print-container {
|
||||||
|
display: none;
|
||||||
|
background-color: white;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card__text {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredient-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@@ -117,51 +165,7 @@ ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: -webkit-box;
|
list-style-type: none;
|
||||||
display: -webkit-flex;
|
margin-bottom: 0.25rem;
|
||||||
margin-left: 0;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li p {
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
letter-spacing: 0.03125em;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
height: 3rem;
|
|
||||||
width: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-chip {
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
border-color: black;
|
|
||||||
border: 1px;
|
|
||||||
border-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.print {
|
|
||||||
display: initial;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -145,6 +145,8 @@ import { AutoFormItems } from "~/types/auto-forms";
|
|||||||
|
|
||||||
const BLUR_EVENT = "blur";
|
const BLUR_EVENT = "blur";
|
||||||
|
|
||||||
|
type ValidatorKey = keyof typeof validators;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "AutoForm",
|
name: "AutoForm",
|
||||||
props: {
|
props: {
|
||||||
@@ -178,7 +180,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
function rulesByKey(keys?: string[] | null) {
|
function rulesByKey(keys?: ValidatorKey[] | null) {
|
||||||
if (keys === undefined || keys === null) {
|
if (keys === undefined || keys === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -193,7 +195,7 @@ export default defineComponent({
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultRules = computed(() => rulesByKey(props.globalRules));
|
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
|
||||||
|
|
||||||
function removeByIndex(list: never[], index: number) {
|
function removeByIndex(list: never[], index: number) {
|
||||||
// Removes the item at the index
|
// Removes the item at the index
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
|
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
|
||||||
dark
|
dark
|
||||||
>
|
>
|
||||||
<v-icon v-if="icon" size="40" v-text="icon" />
|
<v-icon v-if="icon" size="40"> {{ icon }} </v-icon>
|
||||||
<div v-if="text" class="headline font-weight-thin" v-text="text" />
|
<div v-if="text" class="headline font-weight-thin" v-text="text" />
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -2,18 +2,10 @@
|
|||||||
<BaseDialog v-model="dialog" :icon="$globals.icons.translate" :title="$tc('language-dialog.choose-language')">
|
<BaseDialog v-model="dialog" :icon="$globals.icons.translate" :title="$tc('language-dialog.choose-language')">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
{{ $t("language-dialog.select-description") }}
|
{{ $t("language-dialog.select-description") }}
|
||||||
<v-autocomplete
|
<v-autocomplete v-model="locale" :items="locales" item-text="name" class="my-3" hide-details outlined offset>
|
||||||
v-model="locale"
|
|
||||||
:items="locales"
|
|
||||||
item-text="name"
|
|
||||||
class="my-3"
|
|
||||||
hide-details
|
|
||||||
outlined
|
|
||||||
offset
|
|
||||||
>
|
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||||
<v-list-item-subtitle> {{ item.progress }}% {{ $tc("language-dialog.translated") }} </v-list-item-subtitle>
|
<v-list-item-subtitle> {{ item.progress }}% {{ $tc("language-dialog.translated") }} </v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, watch } from "@nuxtjs/composition-api";
|
import { defineComponent, watch } from "@nuxtjs/composition-api";
|
||||||
import { useToggle } from "@vueuse/shared";
|
import { useToggle } from "@vueuse/core";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -34,4 +34,3 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useContext } from "@nuxtjs/composition-api";
|
import { useContext } from "@nuxtjs/composition-api";
|
||||||
import { NuxtAxiosInstance } from "@nuxtjs/axios";
|
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||||
import { AdminAPI, Api } from "~/api";
|
import { AdminAPI, Api } from "~/api";
|
||||||
import { ApiRequestInstance, RequestResponse } from "~/types/api";
|
import { ApiRequestInstance, RequestResponse } from "~/types/api";
|
||||||
import { PublicApi } from "~/api/public-api";
|
import { PublicApi } from "~/api/public-api";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"about": "Über",
|
"about": "Über",
|
||||||
"about-mealie": "Über Mealie",
|
"about-mealie": "Über Mealie",
|
||||||
"api-docs": "API Dokumentation",
|
"api-docs": "API Dokumentation",
|
||||||
"api-port": "API Port",
|
"api-port": "API-Port",
|
||||||
"application-mode": "Anwendungsmodus",
|
"application-mode": "Anwendungsmodus",
|
||||||
"database-type": "Datenbanktyp",
|
"database-type": "Datenbanktyp",
|
||||||
"database-url": "Datenbank URL",
|
"database-url": "Datenbank URL",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"show-assets": "Anhänge anzeigen"
|
"show-assets": "Anhänge anzeigen"
|
||||||
},
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"categories": "Categories",
|
"categories": "Kategorien",
|
||||||
"category-created": "Kategorie angelegt",
|
"category-created": "Kategorie angelegt",
|
||||||
"category-creation-failed": "Anlegen der Kategorie fehlgeschlagen",
|
"category-creation-failed": "Anlegen der Kategorie fehlgeschlagen",
|
||||||
"category-deleted": "Kategorie entfernt",
|
"category-deleted": "Kategorie entfernt",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"uncategorized-count": "{count} nicht kategorisierte"
|
"uncategorized-count": "{count} nicht kategorisierte"
|
||||||
},
|
},
|
||||||
"events": {
|
"events": {
|
||||||
"apprise-url": "Apprise URL",
|
"apprise-url": "Apprise-URL",
|
||||||
"database": "Datenbank",
|
"database": "Datenbank",
|
||||||
"delete-event": "Ereignis löschen",
|
"delete-event": "Ereignis löschen",
|
||||||
"new-notification-form-description": "Mealie verwendet die Apprise-Bibliothek, um Benachrichtigungen zu erzeugen. Sie bietet viele Optionen für Dienste an, die für Benachrichtigungen genutzt werden können. Werfe einen Blick in ihr Wiki für eine umfassende Anleitung zum Erstellen der URL für Ihren Dienst. Falls verfügbar, kann die Auswahl des Benachrichtigungstyps zusätzliche Funktionen enthalten.",
|
"new-notification-form-description": "Mealie verwendet die Apprise-Bibliothek, um Benachrichtigungen zu erzeugen. Sie bietet viele Optionen für Dienste an, die für Benachrichtigungen genutzt werden können. Werfe einen Blick in ihr Wiki für eine umfassende Anleitung zum Erstellen der URL für Ihren Dienst. Falls verfügbar, kann die Auswahl des Benachrichtigungstyps zusätzliche Funktionen enthalten.",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"created": "Erstellt",
|
"created": "Erstellt",
|
||||||
"custom": "Benutzerdefiniert",
|
"custom": "Benutzerdefiniert",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Übersicht",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"disabled": "Deaktiviert",
|
"disabled": "Deaktiviert",
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"image-upload-failed": "Das Bild konnte nicht hochgeladen werden",
|
"image-upload-failed": "Das Bild konnte nicht hochgeladen werden",
|
||||||
"import": "Importieren",
|
"import": "Importieren",
|
||||||
"json": "JSON",
|
"json": "JSON",
|
||||||
"keyword": "Keyword",
|
"keyword": "Schlüsselwort",
|
||||||
"link-copied": "Link kopiert",
|
"link-copied": "Link kopiert",
|
||||||
"loading-recipes": "Lade Rezepte",
|
"loading-recipes": "Lade Rezepte",
|
||||||
"monday": "Montag",
|
"monday": "Montag",
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"success-count": "Erfolgreich: {count}",
|
"success-count": "Erfolgreich: {count}",
|
||||||
"sunday": "Sonntag",
|
"sunday": "Sonntag",
|
||||||
"templates": "Vorlagen:",
|
"templates": "Vorlagen:",
|
||||||
"test": "Test",
|
"test": "Testen",
|
||||||
"themes": "Themen",
|
"themes": "Themen",
|
||||||
"thursday": "Donnerstag",
|
"thursday": "Donnerstag",
|
||||||
"token": "Token",
|
"token": "Token",
|
||||||
@@ -131,10 +131,10 @@
|
|||||||
"view": "Ansicht",
|
"view": "Ansicht",
|
||||||
"wednesday": "Mittwoch",
|
"wednesday": "Mittwoch",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"foods": "Foods",
|
"foods": "Speisen",
|
||||||
"units": "Units",
|
"units": "Einheiten",
|
||||||
"back": "Back",
|
"back": "Zurück",
|
||||||
"next": "Next"
|
"next": "Weiter"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"are-you-sure-you-want-to-delete-the-group": "Bist du dir sicher, dass du die Gruppe <b>{groupName}<b/> löschen möchtest?",
|
"are-you-sure-you-want-to-delete-the-group": "Bist du dir sicher, dass du die Gruppe <b>{groupName}<b/> löschen möchtest?",
|
||||||
@@ -156,8 +156,8 @@
|
|||||||
"user-group-created": "Benutzergruppe angelegt",
|
"user-group-created": "Benutzergruppe angelegt",
|
||||||
"user-group-creation-failed": "Anlegen der Benutzergruppe fehlgeschlagen",
|
"user-group-creation-failed": "Anlegen der Benutzergruppe fehlgeschlagen",
|
||||||
"settings": {
|
"settings": {
|
||||||
"keep-my-recipes-private": "Keep My Recipes Private",
|
"keep-my-recipes-private": "Meine Rezepte privat halten",
|
||||||
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
|
"keep-my-recipes-private-description": "Setzt Ihre Gruppe und alle Rezepte standardmäßig privat. Sie können dies später jederzeit ändern."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"meal-plan": {
|
"meal-plan": {
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
"error-details": "Mealie kann Rezepte nur von Webseiten importieren, die Id+json oder Mikrodaten enthalten. Die meisten großen Rezeptwebseiten unterstützen diese Datenstruktur. Wenn das Rezept nicht importiert werden kann, aber JSON-Daten im Log vorhanden sind, melde es bitte mit der URL und diesen Daten auf GitHub.",
|
"error-details": "Mealie kann Rezepte nur von Webseiten importieren, die Id+json oder Mikrodaten enthalten. Die meisten großen Rezeptwebseiten unterstützen diese Datenstruktur. Wenn das Rezept nicht importiert werden kann, aber JSON-Daten im Log vorhanden sind, melde es bitte mit der URL und diesen Daten auf GitHub.",
|
||||||
"error-title": "Anscheinend konnten wir nichts finden",
|
"error-title": "Anscheinend konnten wir nichts finden",
|
||||||
"from-url": "Von URL",
|
"from-url": "Von URL",
|
||||||
"github-issues": "GitHub Issues",
|
"github-issues": "GitHub Fehlermeldungen",
|
||||||
"google-ld-json-info": "Google ld+json Info",
|
"google-ld-json-info": "Google ld+json Info",
|
||||||
"must-be-a-valid-url": "Muss eine gültige URL sein",
|
"must-be-a-valid-url": "Muss eine gültige URL sein",
|
||||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Füge deine Rezeptdaten ein. Jede Zeile wird als Eintrag in einer Liste dargestellt",
|
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Füge deine Rezeptdaten ein. Jede Zeile wird als Eintrag in einer Liste dargestellt",
|
||||||
@@ -216,9 +216,9 @@
|
|||||||
"upload-individual-zip-file": "Lade eine individuelle .zip-Datei hoch, die von einer anderen Mealie-Instanz exportiert wird.",
|
"upload-individual-zip-file": "Lade eine individuelle .zip-Datei hoch, die von einer anderen Mealie-Instanz exportiert wird.",
|
||||||
"url-form-hint": "Kopiere einen Link von deiner Lieblingsrezept-Website und füge ihn ein",
|
"url-form-hint": "Kopiere einen Link von deiner Lieblingsrezept-Website und füge ihn ein",
|
||||||
"view-scraped-data": "Gesammelte Daten anzeigen",
|
"view-scraped-data": "Gesammelte Daten anzeigen",
|
||||||
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
|
"trim-whitespace-description": "Leerzeichen am Anfang und Ende sowie leere Zeilen entfernen",
|
||||||
"trim-prefix-description": "Trim first character from each line",
|
"trim-prefix-description": "Erste Zeichen aus jeder Zeile entfernen",
|
||||||
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
|
"split-by-numbered-line-description": "Absätze nach dem Schema '1)' oder '1.' aufzuteilen versuchen"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"404-page-not-found": "404 Seite nicht gefunden",
|
"404-page-not-found": "404 Seite nicht gefunden",
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
"title": "Titel",
|
"title": "Titel",
|
||||||
"total-time": "Gesamtzeit",
|
"total-time": "Gesamtzeit",
|
||||||
"unable-to-delete-recipe": "Rezept kann nicht gelöscht werden",
|
"unable-to-delete-recipe": "Rezept kann nicht gelöscht werden",
|
||||||
"no-recipe": "No Recipe"
|
"no-recipe": "Kein Rezept"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"advanced-search": "Erweiterte Suche",
|
"advanced-search": "Erweiterte Suche",
|
||||||
@@ -396,7 +396,7 @@
|
|||||||
"webhooks": {
|
"webhooks": {
|
||||||
"test-webhooks": "Teste Webhooks",
|
"test-webhooks": "Teste Webhooks",
|
||||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Die unten stehenden URL's erhalten Webhooks welche die Rezeptdaten für den Menüplan am geplanten Tag enthalten. Derzeit werden die Webhooks ausgeführt um",
|
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Die unten stehenden URL's erhalten Webhooks welche die Rezeptdaten für den Menüplan am geplanten Tag enthalten. Derzeit werden die Webhooks ausgeführt um",
|
||||||
"webhook-url": "Webhook URL",
|
"webhook-url": "Webhook-URL",
|
||||||
"webhooks-caps": "WEBHOOKS",
|
"webhooks-caps": "WEBHOOKS",
|
||||||
"webhooks": "Webhooks"
|
"webhooks": "Webhooks"
|
||||||
}
|
}
|
||||||
@@ -413,9 +413,9 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"all-recipes": "Alle Rezepte",
|
"all-recipes": "Alle Rezepte",
|
||||||
"backups": "Backups",
|
"backups": "Sicherungen",
|
||||||
"categories": "Kategorien",
|
"categories": "Kategorien",
|
||||||
"cookbooks": "Cookbooks",
|
"cookbooks": "Kochbücher",
|
||||||
"dashboard": "Übersicht",
|
"dashboard": "Übersicht",
|
||||||
"home-page": "Startseite",
|
"home-page": "Startseite",
|
||||||
"manage-users": "Benutzer",
|
"manage-users": "Benutzer",
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
"site-settings": "Einstellungen",
|
"site-settings": "Einstellungen",
|
||||||
"tags": "Schlagworte",
|
"tags": "Schlagworte",
|
||||||
"toolbox": "Werkzeuge",
|
"toolbox": "Werkzeuge",
|
||||||
"language": "Language"
|
"language": "Sprache"
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"error-signing-up": "Fehler beim Registrieren",
|
"error-signing-up": "Fehler beim Registrieren",
|
||||||
@@ -448,7 +448,7 @@
|
|||||||
"untagged-count": "{count} ohne Schlagworte"
|
"untagged-count": "{count} ohne Schlagworte"
|
||||||
},
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"tools": "Tools"
|
"tools": "Werkzeuge"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
@@ -467,7 +467,7 @@
|
|||||||
"error-cannot-delete-super-user": "Fehler! Super Benutzer kann nicht gelöscht werden",
|
"error-cannot-delete-super-user": "Fehler! Super Benutzer kann nicht gelöscht werden",
|
||||||
"existing-password-does-not-match": "Bestehendes Passwort stimmt nicht überein",
|
"existing-password-does-not-match": "Bestehendes Passwort stimmt nicht überein",
|
||||||
"full-name": "Vollständiger Name",
|
"full-name": "Vollständiger Name",
|
||||||
"invite-only": "Invite Only",
|
"invite-only": "Nur auf Einladung",
|
||||||
"link-id": "Linkkennung",
|
"link-id": "Linkkennung",
|
||||||
"link-name": "Linkname",
|
"link-name": "Linkname",
|
||||||
"login": "Anmeldung",
|
"login": "Anmeldung",
|
||||||
@@ -480,8 +480,8 @@
|
|||||||
"password-reset-failed": "Zurücksetzen des Passworts fehlgeschlagen",
|
"password-reset-failed": "Zurücksetzen des Passworts fehlgeschlagen",
|
||||||
"password-updated": "Passwort aktualisiert",
|
"password-updated": "Passwort aktualisiert",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"password-strength": "Password is {strength}",
|
"password-strength": "Das Passwort ist {strength}",
|
||||||
"register": "Register",
|
"register": "Registrieren",
|
||||||
"reset-password": "Passwort zurücksetzen",
|
"reset-password": "Passwort zurücksetzen",
|
||||||
"sign-in": "Einloggen",
|
"sign-in": "Einloggen",
|
||||||
"total-mealplans": "Alle Essenspläne",
|
"total-mealplans": "Alle Essenspläne",
|
||||||
@@ -505,45 +505,45 @@
|
|||||||
"webhooks-enabled": "Webhooks aktiviert",
|
"webhooks-enabled": "Webhooks aktiviert",
|
||||||
"you-are-not-allowed-to-create-a-user": "Sie sind nicht berechtigt, einen Benutzer anzulegen",
|
"you-are-not-allowed-to-create-a-user": "Sie sind nicht berechtigt, einen Benutzer anzulegen",
|
||||||
"you-are-not-allowed-to-delete-this-user": "Sie sind nicht berechtigt, diesen Benutzer zu entfernen",
|
"you-are-not-allowed-to-delete-this-user": "Sie sind nicht berechtigt, diesen Benutzer zu entfernen",
|
||||||
"enable-advanced-content": "Enable Advanced Content",
|
"enable-advanced-content": "Erweiterten Inhalt aktivieren",
|
||||||
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
|
"enable-advanced-content-description": "Aktiviert zusätzliche Funktionen wie Rezept-Skalierung, API-Schlüssel, Webhooks und Datenverwaltung. Keine Sorge, das kann später noch geändert werden."
|
||||||
},
|
},
|
||||||
"language-dialog": {
|
"language-dialog": {
|
||||||
"translated": "translated",
|
"translated": "übersetzt",
|
||||||
"choose-language": "Choose Language",
|
"choose-language": "Sprache wählen",
|
||||||
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
|
"select-description": "Wählen Sie die Sprache für die Mealie-Benutzeroberfläche. Die Einstellung gilt nur für Sie, nicht für andere Benutzer.",
|
||||||
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
|
"how-to-contribute-description": "Ist etwas noch nicht übersetzt, falsch oder deine Sprache fehlt in der Liste? {read-the-docs-link} wie man beiträgt!",
|
||||||
"read-the-docs": "Read the docs"
|
"read-the-docs": "Dokumentation lesen"
|
||||||
},
|
},
|
||||||
"data-pages": {
|
"data-pages": {
|
||||||
"seed-data": "Seed Data",
|
"seed-data": "Musterdaten",
|
||||||
"foods": {
|
"foods": {
|
||||||
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
"merge-dialog-text": "Zusammenführen der ausgewählten Lebensmittel führt diese zusammen in ein einzelnes Lebensmittel. Die Ausgangslebensmittel werden gelöscht und alle Verweise werden auf das zusammengeführte Lebensmittel angepasst.",
|
||||||
"merge-food-example": "Merging {food1} into {food2}",
|
"merge-food-example": "{food1} wird zu {food2} zusammengeführt",
|
||||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
|
"seed-dialog-text": "Füllt die Datenbank mit Lebensmitteln basierend auf Ihrer Landessprache. Dadurch werden mehr als 200 gängige Lebensmittel eingetragen, die verwendet werden können, um die Datenbank zu organisieren. Die Speisen werden über einen Gemeinschaftsanstrengung übersetzt.",
|
||||||
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
|
"seed-dialog-warning": "Sie haben bereits einige Elemente in Ihrer Datenbank. Diese Aktion wird Duplikate nicht ausgleichen, Sie müssen sie manuell verwalten."
|
||||||
},
|
},
|
||||||
"units": {
|
"units": {
|
||||||
"seed-dialog-text": "Seed the database with common units based on your local language."
|
"seed-dialog-text": "Füllt die Datenbank mit gängigen Maßeinheiten basierend auf Ihrer Sprache."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"seed-dialog-text": "Seed the database with common labels based on your local language."
|
"seed-dialog-text": "Füllt die Datenbank mit gängigen Etiketten basierend auf Ihrer Sprache."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user-registration": {
|
"user-registration": {
|
||||||
"user-registration": "User Registration",
|
"user-registration": "Benutzerregistrierung",
|
||||||
"join-a-group": "Join a Group",
|
"join-a-group": "Gruppe beitreten",
|
||||||
"create-a-new-group": "Create a New Group",
|
"create-a-new-group": "Neue Gruppe erstellen",
|
||||||
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
|
"provide-registration-token-description": "Bitte gib den Registrierungstoken für die Gruppe ein, der du beitreten möchtest. Du kannst ihn von einem bestehenden Gruppenmitglied erhalten.",
|
||||||
"group-details": "Group Details",
|
"group-details": "Gruppendetails",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Bevor Sie ein Konto erstellen, müssen Sie eine Gruppe erstellen. Ihre Gruppe wird nur Sie enthalten, aber Sie können andere später einladen. Mitglieder in Ihrer Gruppe können Essenspläne, Einkaufslisten, Rezepte und vieles mehr teilen!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Musterdaten",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie enthält eine Sammlung von Lebensmitteln, Maßeinheiten und Etiketten, die verwendet werden können, um deine Gruppe mit hilfreichen Daten für die Organisation deiner Rezepte zu füllen.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Kontoinformationen"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"group-name-is-taken": "Group name is taken",
|
"group-name-is-taken": "Gruppenname ist schon vergeben",
|
||||||
"username-is-taken": "Username is taken",
|
"username-is-taken": "Benutzername ist schon vergeben",
|
||||||
"email-is-taken": "Email is taken"
|
"email-is-taken": "E-Mail-Adresse ist schon vergeben"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,8 +509,8 @@
|
|||||||
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
|
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
|
||||||
},
|
},
|
||||||
"language-dialog": {
|
"language-dialog": {
|
||||||
"translated": "translated",
|
"translated": "traduit",
|
||||||
"choose-language": "Choose Language",
|
"choose-language": "Choisir la langue",
|
||||||
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
|
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
|
||||||
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
|
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
|
||||||
"read-the-docs": "Read the docs"
|
"read-the-docs": "Read the docs"
|
||||||
@@ -532,18 +532,18 @@
|
|||||||
},
|
},
|
||||||
"user-registration": {
|
"user-registration": {
|
||||||
"user-registration": "User Registration",
|
"user-registration": "User Registration",
|
||||||
"join-a-group": "Join a Group",
|
"join-a-group": "Rejoindre un groupe",
|
||||||
"create-a-new-group": "Create a New Group",
|
"create-a-new-group": "Créer un nouveau groupe",
|
||||||
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
|
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
|
||||||
"group-details": "Group Details",
|
"group-details": "Détails du groupe",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Détails du compte"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"group-name-is-taken": "Group name is taken",
|
"group-name-is-taken": "Group name is taken",
|
||||||
"username-is-taken": "Username is taken",
|
"username-is-taken": "Nom d'utilisateur déjà utilisé",
|
||||||
"email-is-taken": "Email is taken"
|
"email-is-taken": "Email is taken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,17 @@
|
|||||||
<v-divider v-if="item.divider" :key="index" class="mx-2"></v-divider>
|
<v-divider v-if="item.divider" :key="index" class="mx-2"></v-divider>
|
||||||
<v-list-item v-else :key="item.title" :to="item.to" exact>
|
<v-list-item v-else :key="item.title" :to="item.to" exact>
|
||||||
<v-list-item-avatar>
|
<v-list-item-avatar>
|
||||||
<v-icon v-text="item.icon"></v-icon>
|
<v-icon>
|
||||||
|
{{ item.icon }}
|
||||||
|
</v-icon>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-text="item.title"></v-list-item-title>
|
<v-list-item-title>
|
||||||
<v-list-item-subtitle v-text="item.subtitle"></v-list-item-subtitle>
|
{{ item.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ item.subtitle }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
@@ -116,7 +122,7 @@ export default defineComponent({
|
|||||||
icon: this.$globals.icons.link,
|
icon: this.$globals.icons.link,
|
||||||
title: "Import",
|
title: "Import",
|
||||||
subtitle: "Import a recipe by URL",
|
subtitle: "Import a recipe by URL",
|
||||||
to: "/recipe/create?tab=url",
|
to: "/recipe/create/url",
|
||||||
restricted: true,
|
restricted: true,
|
||||||
},
|
},
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
@@ -124,7 +130,7 @@ export default defineComponent({
|
|||||||
icon: this.$globals.icons.edit,
|
icon: this.$globals.icons.edit,
|
||||||
title: "Create",
|
title: "Create",
|
||||||
subtitle: "Create a recipe manually",
|
subtitle: "Create a recipe manually",
|
||||||
to: "/recipe/create?tab=new",
|
to: "/recipe/create/new",
|
||||||
restricted: true,
|
restricted: true,
|
||||||
},
|
},
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
|
|||||||
@@ -22,38 +22,35 @@
|
|||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"@vue/composition-api": "^1.0.5",
|
"@vue/composition-api": "^1.0.5",
|
||||||
"@vueuse/core": "^6.8.0",
|
"@vueuse/core": "^8.5.0",
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
"date-fns": "^2.23.0",
|
"date-fns": "^2.23.0",
|
||||||
"isomorphic-dompurify": "^0.18.0",
|
|
||||||
"fuse.js": "^6.5.3",
|
"fuse.js": "^6.5.3",
|
||||||
|
"isomorphic-dompurify": "^0.19.0",
|
||||||
"nuxt": "^2.15.8",
|
"nuxt": "^2.15.8",
|
||||||
"v-jsoneditor": "^1.4.5",
|
"v-jsoneditor": "^1.4.5",
|
||||||
"vuedraggable": "^2.24.3",
|
"vuedraggable": "^2.24.3",
|
||||||
"vuetify": "^2.5.5"
|
"vuetify": "^2.6.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.14.7",
|
"@babel/eslint-parser": "^7.14.7",
|
||||||
"@nuxt/types": "^2.15.7",
|
"@nuxt/types": "^2.15.7",
|
||||||
"@nuxt/typescript-build": "^2.1.0",
|
"@nuxt/typescript-build": "^2.1.0",
|
||||||
"@nuxtjs/composition-api": "^0.31.0",
|
"@nuxtjs/composition-api": "^0.32.0",
|
||||||
"@nuxtjs/eslint-config-typescript": "^6.0.1",
|
"@nuxtjs/eslint-config-typescript": "^10.0.0",
|
||||||
"@nuxtjs/eslint-module": "^3.0.2",
|
"@nuxtjs/eslint-module": "^3.0.2",
|
||||||
"@nuxtjs/google-fonts": "^1.3.0",
|
"@nuxtjs/google-fonts": "^1.3.0",
|
||||||
"@nuxtjs/vuetify": "^1.12.1",
|
"@nuxtjs/vuetify": "^1.12.1",
|
||||||
"@types/sortablejs": "^1.10.7",
|
"@types/sortablejs": "^1.10.7",
|
||||||
"@vue/runtime-dom": "^3.2.9",
|
"@vue/runtime-dom": "^3.2.36",
|
||||||
"eslint": "^7.29.0",
|
"eslint": "^8.16.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-nuxt": "^2.0.0",
|
"eslint-plugin-nuxt": "^3.2.0",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^7.12.1",
|
"eslint-plugin-vue": "^9.0.1",
|
||||||
"lint-staged": "^10.5.4",
|
"lint-staged": "^12.4.2",
|
||||||
"nuxt-vite": "^0.1.1",
|
"nuxt-vite": "0.2.3",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"vue2-script-setup-transform": "^0.2.0"
|
"vue2-script-setup-transform": "^0.3.5"
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"vite": "2.3.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
|
|||||||
@@ -315,8 +315,10 @@ export default defineComponent({
|
|||||||
title: "Tag Recipes",
|
title: "Tag Recipes",
|
||||||
mode: MODES.tag,
|
mode: MODES.tag,
|
||||||
tag: "",
|
tag: "",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
callback: () => {
|
||||||
callback: () => {},
|
// Stub function to be overwritten
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
icon: $globals.icons.tags,
|
icon: $globals.icons.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<draggable
|
<draggable
|
||||||
tag="div"
|
tag="div"
|
||||||
|
handle=".handle"
|
||||||
:value="plan.meals"
|
:value="plan.meals"
|
||||||
group="meals"
|
group="meals"
|
||||||
:data-index="index"
|
:data-index="index"
|
||||||
@@ -102,7 +103,13 @@
|
|||||||
style="min-height: 150px"
|
style="min-height: 150px"
|
||||||
@end="onMoveCallback"
|
@end="onMoveCallback"
|
||||||
>
|
>
|
||||||
<v-card v-for="mealplan in plan.meals" :key="mealplan.id" v-model="hover[mealplan.id]" class="my-1">
|
<v-card
|
||||||
|
v-for="mealplan in plan.meals"
|
||||||
|
:key="mealplan.id"
|
||||||
|
v-model="hover[mealplan.id]"
|
||||||
|
class="my-1"
|
||||||
|
:class="{ handle: $vuetify.breakpoint.smAndUp }"
|
||||||
|
>
|
||||||
<v-list-item :to="edit || !mealplan.recipe ? null : `/recipe/${mealplan.recipe.slug}`">
|
<v-list-item :to="edit || !mealplan.recipe ? null : `/recipe/${mealplan.recipe.slug}`">
|
||||||
<v-list-item-avatar :rounded="false">
|
<v-list-item-avatar :rounded="false">
|
||||||
<RecipeCardImage
|
<RecipeCardImage
|
||||||
@@ -126,7 +133,13 @@
|
|||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider class="mx-2"></v-divider>
|
||||||
<div class="py-2 px-2 d-flex">
|
<div class="py-2 px-2 d-flex" style="align-items: center">
|
||||||
|
<v-btn small icon :class="{ handle: !$vuetify.breakpoint.smAndUp }">
|
||||||
|
<v-icon>
|
||||||
|
{{ $globals.icons.arrowUpDown }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<v-menu offset-y>
|
<v-menu offset-y>
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-chip v-bind="attrs" label small color="accent" v-on="on" @click.prevent>
|
<v-chip v-bind="attrs" label small color="accent" v-on="on" @click.prevent>
|
||||||
@@ -146,8 +159,8 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn color="error" small icon @click="actions.deleteOne(mealplan.id)">
|
<v-btn class="ml-auto" small icon @click="actions.deleteOne(mealplan.id)">
|
||||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,10 +304,12 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMoveCallback(evt: SortableEvent) {
|
function onMoveCallback(evt: SortableEvent) {
|
||||||
|
const supportedEvents = ["drop", "touchend"];
|
||||||
|
|
||||||
// Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029
|
// Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029
|
||||||
const ogEvent: DragEvent = (evt as any).originalEvent;
|
const ogEvent: DragEvent = (evt as any).originalEvent;
|
||||||
|
|
||||||
if (ogEvent && ogEvent.type !== "drop") {
|
if (ogEvent && ogEvent.type in supportedEvents) {
|
||||||
// The drop was cancelled, unsure if anything needs to be done?
|
// The drop was cancelled, unsure if anything needs to be done?
|
||||||
console.log("Cancel Move Event");
|
console.log("Cancel Move Event");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,9 +4,7 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/data-reports.svg')"></v-img>
|
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/data-reports.svg')"></v-img>
|
||||||
</template>
|
</template>
|
||||||
<template #title> Recipe Data Migrations</template>
|
<template #title> Report </template>
|
||||||
Recipes can be migrated from another supported application to Mealie. This is a great way to get started with
|
|
||||||
Mealie.
|
|
||||||
</BasePageTitle>
|
</BasePageTitle>
|
||||||
<v-container v-if="report">
|
<v-container v-if="report">
|
||||||
<BaseCardSectionTitle :title="report.name"> </BaseCardSectionTitle>
|
<BaseCardSectionTitle :title="report.name"> </BaseCardSectionTitle>
|
||||||
@@ -31,8 +29,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useRoute, reactive, toRefs, onMounted } from "@nuxtjs/composition-api";
|
import { defineComponent, useRoute, ref, onMounted } from "@nuxtjs/composition-api";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { ReportOut } from "~/types/api-types/reports";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
@@ -41,16 +40,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|
||||||
const state = reactive({
|
const report = ref<ReportOut | null>(null);
|
||||||
report: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function getReport() {
|
async function getReport() {
|
||||||
const { data } = await api.groupReports.getOne(id);
|
const { data } = await api.groupReports.getOne(id);
|
||||||
|
report.value = data ?? null;
|
||||||
if (data) {
|
|
||||||
state.report = data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -64,7 +58,7 @@ export default defineComponent({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...toRefs(state),
|
report,
|
||||||
id,
|
id,
|
||||||
itemHeaders,
|
itemHeaders,
|
||||||
};
|
};
|
||||||
@@ -72,5 +66,4 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -454,12 +454,13 @@
|
|||||||
>
|
>
|
||||||
<v-switch v-model="wakeLock" small label="Keep Screen Awake" />
|
<v-switch v-model="wakeLock" small label="Keep Screen Awake" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RecipeComments
|
<RecipeComments
|
||||||
v-if="recipe && !recipe.settings.disableComments && !form"
|
v-if="recipe && !recipe.settings.disableComments && !form"
|
||||||
v-model="recipe.comments"
|
v-model="recipe.comments"
|
||||||
:slug="recipe.slug"
|
:slug="recipe.slug"
|
||||||
:recipe-id="recipe.id"
|
:recipe-id="recipe.id"
|
||||||
class="px-1 my-4"
|
class="px-1 my-4 d-print-none"
|
||||||
/>
|
/>
|
||||||
<RecipePrintView v-if="recipe" :recipe="recipe" />
|
<RecipePrintView v-if="recipe" :recipe="recipe" />
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-container class="narrow-container flex-column pa-0">
|
<v-container class="flex-column">
|
||||||
<BasePageTitle divider>
|
<BasePageTitle divider>
|
||||||
<template #header>
|
<template #header>
|
||||||
<v-img max-height="175" max-width="175" :src="require('~/static/svgs/recipes-create.svg')"></v-img>
|
<v-img max-height="175" max-width="175" :src="require('~/static/svgs/recipes-create.svg')"></v-img>
|
||||||
@@ -9,311 +9,17 @@
|
|||||||
Select one of the various ways to create a recipe
|
Select one of the various ways to create a recipe
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<BaseOverflowButton v-model="tab" rounded :items="tabs"> </BaseOverflowButton>
|
<BaseOverflowButton v-model="subpage" rounded :items="subpages"> </BaseOverflowButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BasePageTitle>
|
</BasePageTitle>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<v-tabs-items v-model="tab" class="mt-2">
|
<NuxtChild />
|
||||||
<!-- Create From URL -->
|
|
||||||
<v-tab-item value="url" eager>
|
|
||||||
<v-form ref="domUrlForm" @submit.prevent="createByUrl(recipeUrl, importKeywordsAsTags)">
|
|
||||||
<v-card flat>
|
|
||||||
<v-card-title class="headline"> Scrape Recipe </v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to
|
|
||||||
scrape the recipe from that site and add it to your collection.
|
|
||||||
<v-text-field
|
|
||||||
v-model="recipeUrl"
|
|
||||||
:label="$t('new-recipe.recipe-url')"
|
|
||||||
:prepend-inner-icon="$globals.icons.link"
|
|
||||||
validate-on-blur
|
|
||||||
autofocus
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
class="rounded-lg mt-2"
|
|
||||||
rounded
|
|
||||||
:rules="[validators.url]"
|
|
||||||
:hint="$t('new-recipe.url-form-hint')"
|
|
||||||
persistent-hint
|
|
||||||
></v-text-field>
|
|
||||||
<v-checkbox v-model="importKeywordsAsTags" label="Import original keywords as tags">
|
|
||||||
</v-checkbox>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions class="justify-center">
|
|
||||||
<div style="width: 250px">
|
|
||||||
<BaseButton :disabled="recipeUrl === null" rounded block type="submit" :loading="loading" />
|
|
||||||
</div>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-form>
|
|
||||||
<v-expand-transition>
|
|
||||||
<v-alert v-show="error" color="error" class="mt-6 white--text">
|
|
||||||
<v-card-title class="ma-0 pa-0">
|
|
||||||
<v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon>
|
|
||||||
{{ $t("new-recipe.error-title") }}
|
|
||||||
</v-card-title>
|
|
||||||
<v-divider class="my-3 mx-2"></v-divider>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{ $t("new-recipe.error-details") }}
|
|
||||||
</p>
|
|
||||||
<div class="d-flex row justify-space-around my-3 force-white">
|
|
||||||
<a
|
|
||||||
class="dark"
|
|
||||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer nofollow"
|
|
||||||
>
|
|
||||||
{{ $t("new-recipe.google-ld-json-info") }}
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
|
||||||
{{ $t("new-recipe.github-issues") }}
|
|
||||||
</a>
|
|
||||||
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
|
||||||
{{ $t("new-recipe.recipe-markup-specification") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</v-alert>
|
|
||||||
</v-expand-transition>
|
|
||||||
</v-tab-item>
|
|
||||||
|
|
||||||
<!-- Create By Name -->
|
|
||||||
<v-tab-item value="new" eager>
|
|
||||||
<v-card flat>
|
|
||||||
<v-card-title class="headline"> Create Recipe </v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
Create a recipe by providing the name. All recipes must have unique names.
|
|
||||||
<v-form ref="domCreateByName">
|
|
||||||
<v-text-field
|
|
||||||
v-model="newRecipeName"
|
|
||||||
:label="$t('recipe.recipe-name')"
|
|
||||||
:prepend-inner-icon="$globals.icons.primary"
|
|
||||||
validate-on-blur
|
|
||||||
autofocus
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
class="rounded-lg mt-2"
|
|
||||||
rounded
|
|
||||||
:rules="[validators.required]"
|
|
||||||
hint="New recipe names must be unique"
|
|
||||||
persistent-hint
|
|
||||||
></v-text-field>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions class="justify-center">
|
|
||||||
<div style="width: 250px">
|
|
||||||
<BaseButton
|
|
||||||
:disabled="newRecipeName === ''"
|
|
||||||
rounded
|
|
||||||
block
|
|
||||||
:loading="loading"
|
|
||||||
@click="createByName(newRecipeName)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-tab-item>
|
|
||||||
|
|
||||||
<!-- Create By Zip -->
|
|
||||||
<v-tab-item value="zip" eager>
|
|
||||||
<v-form>
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="headline"> Import from Zip </v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
Import a single recipe that was exported from another Mealie instance.
|
|
||||||
<v-file-input
|
|
||||||
v-model="newRecipeZip"
|
|
||||||
accept=".zip"
|
|
||||||
label=".zip"
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
class="rounded-lg mt-2"
|
|
||||||
rounded
|
|
||||||
truncate-length="100"
|
|
||||||
hint=".zip files must have been exported from Mealie"
|
|
||||||
persistent-hint
|
|
||||||
prepend-icon=""
|
|
||||||
:prepend-inner-icon="$globals.icons.zip"
|
|
||||||
>
|
|
||||||
</v-file-input>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions class="justify-center">
|
|
||||||
<div style="width: 250px">
|
|
||||||
<BaseButton
|
|
||||||
:disabled="newRecipeZip === null"
|
|
||||||
large
|
|
||||||
rounded
|
|
||||||
block
|
|
||||||
:loading="loading"
|
|
||||||
@click="createByZip"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-form>
|
|
||||||
</v-tab-item>
|
|
||||||
|
|
||||||
<!-- Create By Zip -->
|
|
||||||
<v-tab-item value="debug" eager>
|
|
||||||
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
|
||||||
<v-card flat>
|
|
||||||
<v-card-title class="headline"> Recipe Debugger </v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe
|
|
||||||
scraper and the results will be displayed. If you don't see any data returned, the site you are trying
|
|
||||||
to scrape is not supported by Mealie or it's scraper library.
|
|
||||||
<v-text-field
|
|
||||||
v-model="recipeUrl"
|
|
||||||
:label="$t('new-recipe.recipe-url')"
|
|
||||||
validate-on-blur
|
|
||||||
:prepend-inner-icon="$globals.icons.link"
|
|
||||||
autofocus
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
rounded
|
|
||||||
class="rounded-lg mt-2"
|
|
||||||
:rules="[validators.url]"
|
|
||||||
:hint="$t('new-recipe.url-form-hint')"
|
|
||||||
persistent-hint
|
|
||||||
></v-text-field>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions class="justify-center">
|
|
||||||
<div style="width: 250px">
|
|
||||||
<BaseButton
|
|
||||||
:disabled="recipeUrl === null"
|
|
||||||
rounded
|
|
||||||
block
|
|
||||||
type="submit"
|
|
||||||
color="info"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
{{ $globals.icons.robot }}
|
|
||||||
</template>
|
|
||||||
Debug
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-form>
|
|
||||||
</v-tab-item>
|
|
||||||
|
|
||||||
<v-tab-item value="bulk" eager>
|
|
||||||
<v-card flat>
|
|
||||||
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the
|
|
||||||
backend and running the task in the background. This can be useful when initially migrating to Mealie,
|
|
||||||
or when you want to import a large number of recipes.
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-tab-item>
|
|
||||||
</v-tabs-items>
|
|
||||||
</section>
|
|
||||||
<v-divider class="mt-5"></v-divider>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<v-container tag="section">
|
|
||||||
<!-- Debug Extras -->
|
|
||||||
<section v-if="debugData && tab === 'debug'">
|
|
||||||
<v-checkbox v-model="debugTreeView" label="Tree View"></v-checkbox>
|
|
||||||
<LazyRecipeJsonEditor
|
|
||||||
v-model="debugData"
|
|
||||||
class="primary"
|
|
||||||
:options="{
|
|
||||||
mode: debugTreeView ? 'tree' : 'code',
|
|
||||||
search: false,
|
|
||||||
indentation: 4,
|
|
||||||
mainMenuBar: false,
|
|
||||||
}"
|
|
||||||
height="700px"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<!-- Debug Extras -->
|
|
||||||
<section v-else-if="tab === 'bulk'" class="mt-2">
|
|
||||||
<v-row v-for="(bulkUrl, idx) in bulkUrls" :key="'bulk-url' + idx" class="my-1" dense>
|
|
||||||
<v-col cols="12" xs="12" sm="12" md="12">
|
|
||||||
<v-text-field
|
|
||||||
v-model="bulkUrls[idx].url"
|
|
||||||
:label="$t('new-recipe.recipe-url')"
|
|
||||||
dense
|
|
||||||
single-line
|
|
||||||
validate-on-blur
|
|
||||||
autofocus
|
|
||||||
filled
|
|
||||||
hide-details
|
|
||||||
clearable
|
|
||||||
:prepend-inner-icon="$globals.icons.link"
|
|
||||||
rounded
|
|
||||||
class="rounded-lg"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<v-btn color="error" icon x-small @click="bulkUrls.splice(idx, 1)">
|
|
||||||
<v-icon>
|
|
||||||
{{ $globals.icons.delete }}
|
|
||||||
</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" xs="12" sm="6">
|
|
||||||
<RecipeOrganizerSelector
|
|
||||||
v-model="bulkUrls[idx].categories"
|
|
||||||
:items="allCategories || []"
|
|
||||||
selector-type="category"
|
|
||||||
:input-attrs="{
|
|
||||||
filled: true,
|
|
||||||
singleLine: true,
|
|
||||||
dense: true,
|
|
||||||
rounded: true,
|
|
||||||
class: 'rounded-lg',
|
|
||||||
hideDetails: true,
|
|
||||||
clearable: true,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" xs="12" sm="6">
|
|
||||||
<RecipeOrganizerSelector
|
|
||||||
v-model="bulkUrls[idx].tags"
|
|
||||||
:items="allTags || []"
|
|
||||||
selector-type="tag"
|
|
||||||
:input-attrs="{
|
|
||||||
filled: true,
|
|
||||||
singleLine: true,
|
|
||||||
dense: true,
|
|
||||||
rounded: true,
|
|
||||||
class: 'rounded-lg',
|
|
||||||
hideDetails: true,
|
|
||||||
clearable: true,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-card-actions class="justify-end">
|
|
||||||
<BaseButton
|
|
||||||
delete
|
|
||||||
@click="
|
|
||||||
bulkUrls = [];
|
|
||||||
lockBulkImport = false;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</BaseButton>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<BaseButton color="info" @click="bulkUrls.push({ url: '', categories: [], tags: [] })">
|
|
||||||
<template #icon> {{ $globals.icons.createAlt }} </template> New
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton :disabled="bulkUrls.length === 0 || lockBulkImport" @click="bulkCreate">
|
|
||||||
<template #icon> {{ $globals.icons.check }} </template> Submit
|
|
||||||
</BaseButton>
|
|
||||||
</v-card-actions>
|
|
||||||
</section>
|
</section>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<AdvancedOnly>
|
<AdvancedOnly>
|
||||||
<v-container class="narrow-container d-flex justify-end">
|
<v-container class="d-flex justify-end">
|
||||||
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
|
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
|
||||||
</v-container>
|
</v-container>
|
||||||
</AdvancedOnly>
|
</AdvancedOnly>
|
||||||
@@ -321,39 +27,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { defineComponent, useRouter, useContext, computed, useRoute } from "@nuxtjs/composition-api";
|
||||||
defineComponent,
|
|
||||||
reactive,
|
|
||||||
toRefs,
|
|
||||||
ref,
|
|
||||||
useRouter,
|
|
||||||
useContext,
|
|
||||||
computed,
|
|
||||||
useRoute,
|
|
||||||
} from "@nuxtjs/composition-api";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { onMounted } from "vue-demi";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { validators } from "~/composables/use-validators";
|
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
|
||||||
import { alert } from "~/composables/use-toast";
|
|
||||||
import { VForm } from "~/types/vuetify";
|
|
||||||
import { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
import { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
||||||
import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
|
import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
|
||||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
|
||||||
import { useCategories, useTags } from "~/composables/recipes";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { AdvancedOnly, RecipeOrganizerSelector },
|
components: { AdvancedOnly },
|
||||||
setup() {
|
setup() {
|
||||||
const state = reactive({
|
|
||||||
error: false,
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { $globals } = useContext();
|
const { $globals } = useContext();
|
||||||
|
|
||||||
const tabs: MenuItem[] = [
|
const subpages: MenuItem[] = [
|
||||||
{
|
{
|
||||||
icon: $globals.icons.link,
|
icon: $globals.icons.link,
|
||||||
text: "Import with URL",
|
text: "Import with URL",
|
||||||
@@ -381,185 +64,21 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const api = useUserApi();
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
const subpage = computed({
|
||||||
if (response?.status !== 201) {
|
set(subpage: string) {
|
||||||
state.error = true;
|
router.push({ path: `/recipe/create/${subpage}`, query: route.value.query });
|
||||||
state.loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tab = computed({
|
|
||||||
set(tab: string) {
|
|
||||||
router.replace({ query: { ...route.value.query, tab } });
|
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
return route.value.query.tab as string;
|
return route.value.path.split("/").pop() ?? "url";
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeUrl = computed({
|
|
||||||
set(recipe_import_url: string | null) {
|
|
||||||
if (recipe_import_url !== null) {
|
|
||||||
recipe_import_url = recipe_import_url.trim();
|
|
||||||
router.replace({ query: { ...route.value.query, recipe_import_url } });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get() {
|
|
||||||
return route.value.query.recipe_import_url as string | null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const importKeywordsAsTags = computed({
|
|
||||||
get() {
|
|
||||||
return route.value.query.import_keywords_as_tags === "1";
|
|
||||||
},
|
|
||||||
set(keywordsAsTags: boolean) {
|
|
||||||
let import_keywords_as_tags = "0"
|
|
||||||
if (keywordsAsTags) {
|
|
||||||
import_keywords_as_tags = "1"
|
|
||||||
}
|
|
||||||
router.replace({query: {...route.value.query, import_keywords_as_tags}})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!recipeUrl.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipeUrl.value.includes("https")) {
|
|
||||||
createByUrl(recipeUrl.value, importKeywordsAsTags.value);
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ===================================================
|
|
||||||
// Recipe Debug URL Scraper
|
|
||||||
|
|
||||||
const debugTreeView = ref(false);
|
|
||||||
|
|
||||||
const debugData = ref<Recipe | null>(null);
|
|
||||||
|
|
||||||
async function debugUrl(url: string | null) {
|
|
||||||
if (url === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.loading = true;
|
|
||||||
|
|
||||||
const { data } = await api.recipes.testCreateOneUrl(url);
|
|
||||||
|
|
||||||
state.loading = false;
|
|
||||||
debugData.value = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================
|
|
||||||
// Recipe URL Import
|
|
||||||
const domUrlForm = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
async function createByUrl(url: string, importKeywordsAsTags: boolean) {
|
|
||||||
|
|
||||||
if (url === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!domUrlForm.value?.validate() || url === "") {
|
|
||||||
console.log("Invalid URL", url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.loading = true;
|
|
||||||
const { response } = await api.recipes.createOneByUrl(url, importKeywordsAsTags);
|
|
||||||
handleResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================
|
|
||||||
// Recipe Create By Name
|
|
||||||
const newRecipeName = ref("");
|
|
||||||
const domCreateByName = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
async function createByName(name: string) {
|
|
||||||
if (!domCreateByName.value?.validate() || name === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { response } = await api.recipes.createOne({ name });
|
|
||||||
// TODO createOne claims to return a Recipe, but actually the API only returns a string
|
|
||||||
// @ts-ignore See above
|
|
||||||
handleResponse(response, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================
|
|
||||||
// Recipe Import From Zip File
|
|
||||||
const newRecipeZip = ref<File | null>(null);
|
|
||||||
const newRecipeZipFileName = "archive";
|
|
||||||
|
|
||||||
async function createByZip() {
|
|
||||||
if (!newRecipeZip.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
|
||||||
|
|
||||||
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
|
||||||
handleResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================
|
|
||||||
// Bulk Importer
|
|
||||||
|
|
||||||
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
|
|
||||||
const lockBulkImport = ref(false);
|
|
||||||
|
|
||||||
async function bulkCreate() {
|
|
||||||
if (bulkUrls.value.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { response } = await api.recipes.createManyByUrl({ imports: bulkUrls.value });
|
|
||||||
|
|
||||||
if (response?.status === 202) {
|
|
||||||
alert.success("Bulk Import process has started");
|
|
||||||
lockBulkImport.value = true;
|
|
||||||
} else {
|
|
||||||
alert.error("Bulk import process has failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
|
||||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
|
||||||
|
|
||||||
getAllTags();
|
|
||||||
getAllCategories();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allTags,
|
subpages,
|
||||||
allCategories,
|
subpage,
|
||||||
tab,
|
|
||||||
recipeUrl,
|
|
||||||
importKeywordsAsTags,
|
|
||||||
bulkCreate,
|
|
||||||
bulkUrls,
|
|
||||||
lockBulkImport,
|
|
||||||
debugTreeView,
|
|
||||||
tabs,
|
|
||||||
domCreateByName,
|
|
||||||
domUrlForm,
|
|
||||||
newRecipeName,
|
|
||||||
newRecipeZip,
|
|
||||||
debugUrl,
|
|
||||||
debugData,
|
|
||||||
createByName,
|
|
||||||
createByUrl,
|
|
||||||
createByZip,
|
|
||||||
...toRefs(state),
|
|
||||||
validators,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
@@ -569,9 +88,3 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.force-white > a {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
201
frontend/pages/recipe/create/bulk.vue
Normal file
201
frontend/pages/recipe/create/bulk.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div flat>
|
||||||
|
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the backend and
|
||||||
|
running the task in the background. This can be useful when initially migrating to Mealie, or when you want to
|
||||||
|
import a large number of recipes.
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
<section class="mt-2">
|
||||||
|
<v-row v-for="(_, idx) in bulkUrls" :key="'bulk-url' + idx" class="my-1" dense>
|
||||||
|
<v-col cols="12" xs="12" sm="12" md="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="bulkUrls[idx].url"
|
||||||
|
:label="$t('new-recipe.recipe-url')"
|
||||||
|
dense
|
||||||
|
single-line
|
||||||
|
validate-on-blur
|
||||||
|
autofocus
|
||||||
|
filled
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
|
:prepend-inner-icon="$globals.icons.link"
|
||||||
|
rounded
|
||||||
|
class="rounded-lg"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<v-btn style="margin-top: -2px" icon small @click="bulkUrls.splice(idx, 1)">
|
||||||
|
<v-icon>
|
||||||
|
{{ $globals.icons.delete }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<template v-if="showCatTags">
|
||||||
|
<v-col cols="12" xs="12" sm="6">
|
||||||
|
<RecipeOrganizerSelector
|
||||||
|
v-model="bulkUrls[idx].categories"
|
||||||
|
:items="allCategories || []"
|
||||||
|
selector-type="category"
|
||||||
|
:input-attrs="{
|
||||||
|
filled: true,
|
||||||
|
singleLine: true,
|
||||||
|
dense: true,
|
||||||
|
rounded: true,
|
||||||
|
class: 'rounded-lg',
|
||||||
|
hideDetails: true,
|
||||||
|
clearable: true,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" xs="12" sm="6">
|
||||||
|
<RecipeOrganizerSelector
|
||||||
|
v-model="bulkUrls[idx].tags"
|
||||||
|
:items="allTags || []"
|
||||||
|
selector-type="tag"
|
||||||
|
:input-attrs="{
|
||||||
|
filled: true,
|
||||||
|
singleLine: true,
|
||||||
|
dense: true,
|
||||||
|
rounded: true,
|
||||||
|
class: 'rounded-lg',
|
||||||
|
hideDetails: true,
|
||||||
|
clearable: true,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</template>
|
||||||
|
</v-row>
|
||||||
|
<v-card-actions class="justify-end">
|
||||||
|
<BaseButton
|
||||||
|
delete
|
||||||
|
@click="
|
||||||
|
bulkUrls = [];
|
||||||
|
lockBulkImport = false;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</BaseButton>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseButton class="mr-1" color="info" @click="bulkUrls.push({ url: '', categories: [], tags: [] })">
|
||||||
|
<template #icon> {{ $globals.icons.createAlt }} </template>
|
||||||
|
New
|
||||||
|
</BaseButton>
|
||||||
|
<RecipeDialogBulkAdd v-model="bulkDialog" @bulk-data="assignUrls" />
|
||||||
|
</v-card-actions>
|
||||||
|
<div class="px-1">
|
||||||
|
<v-checkbox v-model="showCatTags" hide-details label="Set Categories and Tags " />
|
||||||
|
</div>
|
||||||
|
<v-card-actions class="justify-end">
|
||||||
|
<BaseButton :disabled="bulkUrls.length === 0 || lockBulkImport" @click="bulkCreate">
|
||||||
|
<template #icon> {{ $globals.icons.check }} </template>
|
||||||
|
Submit
|
||||||
|
</BaseButton>
|
||||||
|
</v-card-actions>
|
||||||
|
</section>
|
||||||
|
<section class="mt-12">
|
||||||
|
<BaseCardSectionTitle title="Bulk Imports"> </BaseCardSectionTitle>
|
||||||
|
<ReportTable :items="reports" @delete="deleteReport" />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, ref } from "@nuxtjs/composition-api";
|
||||||
|
import { whenever } from "@vueuse/shared";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { alert } from "~/composables/use-toast";
|
||||||
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
|
import { useCategories, useTags } from "~/composables/recipes";
|
||||||
|
import { ReportSummary } from "~/types/api-types/reports";
|
||||||
|
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { RecipeOrganizerSelector, RecipeDialogBulkAdd },
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
error: false,
|
||||||
|
loading: false,
|
||||||
|
showCatTags: false,
|
||||||
|
bulkDialog: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => !state.showCatTags,
|
||||||
|
() => {
|
||||||
|
console.log("showCatTags changed");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const api = useUserApi();
|
||||||
|
|
||||||
|
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
|
||||||
|
const lockBulkImport = ref(false);
|
||||||
|
|
||||||
|
async function bulkCreate() {
|
||||||
|
if (bulkUrls.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response } = await api.recipes.createManyByUrl({ imports: bulkUrls.value });
|
||||||
|
|
||||||
|
if (response?.status === 202) {
|
||||||
|
alert.success("Bulk Import process has started");
|
||||||
|
lockBulkImport.value = true;
|
||||||
|
} else {
|
||||||
|
alert.error("Bulk import process has failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchReports();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
||||||
|
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
||||||
|
|
||||||
|
getAllTags();
|
||||||
|
getAllCategories();
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Reports
|
||||||
|
|
||||||
|
const reports = ref<ReportSummary[]>([]);
|
||||||
|
|
||||||
|
async function fetchReports() {
|
||||||
|
const { data } = await api.groupReports.getAll("bulk_import");
|
||||||
|
reports.value = data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteReport(id: string) {
|
||||||
|
console.log(id);
|
||||||
|
const { response } = await api.groupReports.deleteOne(id);
|
||||||
|
|
||||||
|
if (response?.status === 200) {
|
||||||
|
fetchReports();
|
||||||
|
} else {
|
||||||
|
alert.error("Report deletion failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchReports();
|
||||||
|
|
||||||
|
function assignUrls(urls: string[]) {
|
||||||
|
bulkUrls.value = urls.map((url) => ({ url, categories: [], tags: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
assignUrls,
|
||||||
|
reports,
|
||||||
|
deleteReport,
|
||||||
|
allTags,
|
||||||
|
allCategories,
|
||||||
|
bulkCreate,
|
||||||
|
bulkUrls,
|
||||||
|
lockBulkImport,
|
||||||
|
...toRefs(state),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
110
frontend/pages/recipe/create/debug.vue
Normal file
110
frontend/pages/recipe/create/debug.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
|
||||||
|
<div>
|
||||||
|
<v-card-title class="headline"> Recipe Debugger </v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper
|
||||||
|
and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is
|
||||||
|
not supported by Mealie or it's scraper library.
|
||||||
|
<v-text-field
|
||||||
|
v-model="recipeUrl"
|
||||||
|
:label="$t('new-recipe.recipe-url')"
|
||||||
|
validate-on-blur
|
||||||
|
:prepend-inner-icon="$globals.icons.link"
|
||||||
|
autofocus
|
||||||
|
filled
|
||||||
|
clearable
|
||||||
|
rounded
|
||||||
|
class="rounded-lg mt-2"
|
||||||
|
:rules="[validators.url]"
|
||||||
|
:hint="$t('new-recipe.url-form-hint')"
|
||||||
|
persistent-hint
|
||||||
|
></v-text-field>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions class="justify-center">
|
||||||
|
<div style="width: 250px">
|
||||||
|
<BaseButton :disabled="recipeUrl === null" rounded block type="submit" color="info" :loading="loading">
|
||||||
|
<template #icon>
|
||||||
|
{{ $globals.icons.robot }}
|
||||||
|
</template>
|
||||||
|
Debug
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</v-card-actions>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
<section v-if="debugData">
|
||||||
|
<v-checkbox v-model="debugTreeView" label="Tree View"></v-checkbox>
|
||||||
|
<LazyRecipeJsonEditor
|
||||||
|
v-model="debugData"
|
||||||
|
class="primary"
|
||||||
|
:options="{
|
||||||
|
mode: debugTreeView ? 'tree' : 'code',
|
||||||
|
search: false,
|
||||||
|
indentation: 4,
|
||||||
|
mainMenuBar: false,
|
||||||
|
}"
|
||||||
|
height="700px"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, ref, useRouter, computed, useRoute } from "@nuxtjs/composition-api";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
error: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = useUserApi();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const recipeUrl = computed({
|
||||||
|
set(recipe_import_url: string | null) {
|
||||||
|
if (recipe_import_url !== null) {
|
||||||
|
recipe_import_url = recipe_import_url.trim();
|
||||||
|
router.replace({ query: { ...route.value.query, recipe_import_url } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return route.value.query.recipe_import_url as string | null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const debugTreeView = ref(false);
|
||||||
|
|
||||||
|
const debugData = ref<Recipe | null>(null);
|
||||||
|
|
||||||
|
async function debugUrl(url: string | null) {
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.loading = true;
|
||||||
|
|
||||||
|
const { data } = await api.recipes.testCreateOneUrl(url);
|
||||||
|
|
||||||
|
state.loading = false;
|
||||||
|
debugData.value = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
recipeUrl,
|
||||||
|
debugTreeView,
|
||||||
|
debugUrl,
|
||||||
|
debugData,
|
||||||
|
...toRefs(state),
|
||||||
|
validators,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
18
frontend/pages/recipe/create/index.vue
Normal file
18
frontend/pages/recipe/create/index.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const router = useRouter();
|
||||||
|
onMounted(() => {
|
||||||
|
// Force redirect to first valid page
|
||||||
|
router.replace("/recipe/create/url");
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
82
frontend/pages/recipe/create/new.vue
Normal file
82
frontend/pages/recipe/create/new.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-card-title class="headline"> Create Recipe </v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Create a recipe by providing the name. All recipes must have unique names.
|
||||||
|
<v-form ref="domCreateByName">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newRecipeName"
|
||||||
|
:label="$t('recipe.recipe-name')"
|
||||||
|
:prepend-inner-icon="$globals.icons.primary"
|
||||||
|
validate-on-blur
|
||||||
|
autofocus
|
||||||
|
filled
|
||||||
|
clearable
|
||||||
|
class="rounded-lg mt-2"
|
||||||
|
rounded
|
||||||
|
:rules="[validators.required]"
|
||||||
|
hint="New recipe names must be unique"
|
||||||
|
persistent-hint
|
||||||
|
></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions class="justify-center">
|
||||||
|
<div style="width: 250px">
|
||||||
|
<BaseButton
|
||||||
|
:disabled="newRecipeName === ''"
|
||||||
|
rounded
|
||||||
|
block
|
||||||
|
:loading="loading"
|
||||||
|
@click="createByName(newRecipeName)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card-actions>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
import { VForm } from "~/types/vuetify";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
error: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
const api = useUserApi();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
||||||
|
if (response?.status !== 201) {
|
||||||
|
state.error = true;
|
||||||
|
state.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRecipeName = ref("");
|
||||||
|
const domCreateByName = ref<VForm | null>(null);
|
||||||
|
|
||||||
|
async function createByName(name: string) {
|
||||||
|
if (!domCreateByName.value?.validate() || name === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { response } = await api.recipes.createOne({ name });
|
||||||
|
// TODO createOne claims to return a Recipe, but actually the API only returns a string
|
||||||
|
// @ts-ignore See above
|
||||||
|
handleResponse(response, true);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
domCreateByName,
|
||||||
|
newRecipeName,
|
||||||
|
createByName,
|
||||||
|
...toRefs(state),
|
||||||
|
validators,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
159
frontend/pages/recipe/create/url.vue
Normal file
159
frontend/pages/recipe/create/url.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-form ref="domUrlForm" @submit.prevent="createByUrl(recipeUrl, importKeywordsAsTags)">
|
||||||
|
<div>
|
||||||
|
<v-card-title class="headline"> Scrape Recipe </v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Scrape a recipe by url. Provide the url for the site you want to scrape, and Mealie will attempt to scrape the
|
||||||
|
recipe from that site and add it to your collection.
|
||||||
|
<v-text-field
|
||||||
|
v-model="recipeUrl"
|
||||||
|
:label="$t('new-recipe.recipe-url')"
|
||||||
|
:prepend-inner-icon="$globals.icons.link"
|
||||||
|
validate-on-blur
|
||||||
|
autofocus
|
||||||
|
filled
|
||||||
|
clearable
|
||||||
|
class="rounded-lg mt-2"
|
||||||
|
rounded
|
||||||
|
:rules="[validators.url]"
|
||||||
|
:hint="$t('new-recipe.url-form-hint')"
|
||||||
|
persistent-hint
|
||||||
|
></v-text-field>
|
||||||
|
<v-checkbox v-model="importKeywordsAsTags" label="Import original keywords as tags"> </v-checkbox>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions class="justify-center">
|
||||||
|
<div style="width: 250px">
|
||||||
|
<BaseButton :disabled="recipeUrl === null" rounded block type="submit" :loading="loading" />
|
||||||
|
</div>
|
||||||
|
</v-card-actions>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
<v-expand-transition>
|
||||||
|
<v-alert v-show="error" color="error" class="mt-6 white--text">
|
||||||
|
<v-card-title class="ma-0 pa-0">
|
||||||
|
<v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon>
|
||||||
|
{{ $t("new-recipe.error-title") }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-divider class="my-3 mx-2"></v-divider>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ $t("new-recipe.error-details") }}
|
||||||
|
</p>
|
||||||
|
<div class="d-flex row justify-space-around my-3 force-white">
|
||||||
|
<a
|
||||||
|
class="dark"
|
||||||
|
href="https://developers.google.com/search/docs/data-types/recipe"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer nofollow"
|
||||||
|
>
|
||||||
|
{{ $t("new-recipe.google-ld-json-info") }}
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
||||||
|
{{ $t("new-recipe.github-issues") }}
|
||||||
|
</a>
|
||||||
|
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
||||||
|
{{ $t("new-recipe.recipe-markup-specification") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
|
</v-expand-transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, ref, useRouter, computed, useRoute } from "@nuxtjs/composition-api";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { onMounted } from "vue-demi";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
import { VForm } from "~/types/vuetify";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
error: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = useUserApi();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
||||||
|
if (response?.status !== 201) {
|
||||||
|
state.error = true;
|
||||||
|
state.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipeUrl = computed({
|
||||||
|
set(recipe_import_url: string | null) {
|
||||||
|
if (recipe_import_url !== null) {
|
||||||
|
recipe_import_url = recipe_import_url.trim();
|
||||||
|
router.replace({ query: { ...route.value.query, recipe_import_url } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return route.value.query.recipe_import_url as string | null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const importKeywordsAsTags = computed({
|
||||||
|
get() {
|
||||||
|
return route.value.query.import_keywords_as_tags === "1";
|
||||||
|
},
|
||||||
|
set(keywordsAsTags: boolean) {
|
||||||
|
let import_keywords_as_tags = "0";
|
||||||
|
if (keywordsAsTags) {
|
||||||
|
import_keywords_as_tags = "1";
|
||||||
|
}
|
||||||
|
router.replace({ query: { ...route.value.query, import_keywords_as_tags } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!recipeUrl.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipeUrl.value.includes("https")) {
|
||||||
|
createByUrl(recipeUrl.value, importKeywordsAsTags.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const domUrlForm = ref<VForm | null>(null);
|
||||||
|
|
||||||
|
async function createByUrl(url: string, importKeywordsAsTags: boolean) {
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domUrlForm.value?.validate() || url === "") {
|
||||||
|
console.log("Invalid URL", url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.loading = true;
|
||||||
|
const { response } = await api.recipes.createOneByUrl(url, importKeywordsAsTags);
|
||||||
|
handleResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
recipeUrl,
|
||||||
|
importKeywordsAsTags,
|
||||||
|
domUrlForm,
|
||||||
|
createByUrl,
|
||||||
|
...toRefs(state),
|
||||||
|
validators,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.force-white > a {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
78
frontend/pages/recipe/create/zip.vue
Normal file
78
frontend/pages/recipe/create/zip.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<v-form>
|
||||||
|
<div>
|
||||||
|
<v-card-title class="headline"> Import from Zip </v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Import a single recipe that was exported from another Mealie instance.
|
||||||
|
<v-file-input
|
||||||
|
v-model="newRecipeZip"
|
||||||
|
accept=".zip"
|
||||||
|
label=".zip"
|
||||||
|
filled
|
||||||
|
clearable
|
||||||
|
class="rounded-lg mt-2"
|
||||||
|
rounded
|
||||||
|
truncate-length="100"
|
||||||
|
hint=".zip files must have been exported from Mealie"
|
||||||
|
persistent-hint
|
||||||
|
prepend-icon=""
|
||||||
|
:prepend-inner-icon="$globals.icons.zip"
|
||||||
|
>
|
||||||
|
</v-file-input>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions class="justify-center">
|
||||||
|
<div style="width: 250px">
|
||||||
|
<BaseButton :disabled="newRecipeZip === null" large rounded block :loading="loading" @click="createByZip" />
|
||||||
|
</div>
|
||||||
|
</v-card-actions>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
error: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
const api = useUserApi();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
|
||||||
|
if (response?.status !== 201) {
|
||||||
|
state.error = true;
|
||||||
|
state.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRecipeZip = ref<File | null>(null);
|
||||||
|
const newRecipeZipFileName = "archive";
|
||||||
|
|
||||||
|
async function createByZip() {
|
||||||
|
if (!newRecipeZip.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append(newRecipeZipFileName, newRecipeZip.value);
|
||||||
|
|
||||||
|
const { response } = await api.upload.file("/api/recipes/create-from-zip", formData);
|
||||||
|
handleResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
newRecipeZip,
|
||||||
|
createByZip,
|
||||||
|
...toRefs(state),
|
||||||
|
validators,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -216,7 +216,7 @@ export default defineComponent({
|
|||||||
if (searchString.value.trim() === "") {
|
if (searchString.value.trim() === "") {
|
||||||
return filteredRecipes.value;
|
return filteredRecipes.value;
|
||||||
}
|
}
|
||||||
const result = fuse.value.search(searchString.value.trim());
|
const result = fuse.value.search(searchString.value.trim() as string);
|
||||||
return result.map((x) => x.item);
|
return result.map((x) => x.item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Plugin } from "@nuxt/types";
|
import { Plugin } from "@nuxt/types";
|
||||||
import { NuxtAxiosInstance } from "@nuxtjs/axios";
|
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
|
|
||||||
const toastPlugin: Plugin = ({ $axios }: { $axios: NuxtAxiosInstance }) => {
|
const toastPlugin: Plugin = ({ $axios }: { $axios: NuxtAxiosInstance }) => {
|
||||||
$axios.onResponse((response) => {
|
$axios.onResponse((response) => {
|
||||||
if (response?.data?.message) {
|
if (response?.data?.message) {
|
||||||
alert.info(response.data.message);
|
alert.info(response.data.message as string);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$axios.onError((error) => {
|
$axios.onError((error) => {
|
||||||
if (error.response?.data?.detail?.message) {
|
if (error.response?.data?.detail?.message) {
|
||||||
alert.error(error.response.data.detail.message);
|
alert.error(error.response.data.detail.message as string);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,15 @@
|
|||||||
"~/*": ["./*"],
|
"~/*": ["./*"],
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
},
|
},
|
||||||
"types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "@nuxtjs/i18n", "@nuxtjs/auth-next", "@nuxtjs/vuetify", "@types/sortablejs"]
|
"types": [
|
||||||
|
"@nuxt/types",
|
||||||
|
"@nuxtjs/axios",
|
||||||
|
"@types/node",
|
||||||
|
"@nuxtjs/i18n",
|
||||||
|
"@nuxtjs/auth-next",
|
||||||
|
"@nuxtjs/vuetify",
|
||||||
|
"@types/sortablejs"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["**/*", ".eslintrc.js"],
|
"include": ["**/*", ".eslintrc.js"],
|
||||||
"exclude": ["node_modules", ".nuxt", "dist"],
|
"exclude": ["node_modules", ".nuxt", "dist"],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ReportCategory = "backup" | "restore" | "migration";
|
export type ReportCategory = "backup" | "restore" | "migration" | "bulk_import";
|
||||||
export type ReportSummaryStatus = "in-progress" | "success" | "failure" | "partial";
|
export type ReportSummaryStatus = "in-progress" | "success" | "failure" | "partial";
|
||||||
|
|
||||||
export interface ReportCreate {
|
export interface ReportCreate {
|
||||||
|
|||||||
3509
frontend/yarn.lock
3509
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,9 @@ import dotenv
|
|||||||
from mealie.core.settings import app_settings_constructor
|
from mealie.core.settings import app_settings_constructor
|
||||||
|
|
||||||
from .settings import AppDirectories, AppSettings
|
from .settings import AppDirectories, AppSettings
|
||||||
from .settings.static import APP_VERSION, DB_VERSION
|
from .settings.static import APP_VERSION
|
||||||
|
|
||||||
APP_VERSION
|
APP_VERSION
|
||||||
DB_VERSION
|
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
BASE_DIR = CWD.parent.parent
|
BASE_DIR = CWD.parent.parent
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
APP_VERSION = "v1.0.0b"
|
APP_VERSION = "v1.0.0beta-2"
|
||||||
DB_VERSION = "v1.0.0b"
|
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
BASE_DIR = CWD.parent.parent.parent
|
BASE_DIR = CWD.parent.parent.parent
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
"email-conflict-error": "Diese E-Mail-Adresse wird bereits verwendet"
|
"email-conflict-error": "Diese E-Mail-Adresse wird bereits verwendet"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"generic-created": "{name} was created",
|
"generic-created": "{name} wurde erstellt",
|
||||||
"generic-updated": "{name} was updated",
|
"generic-updated": "{name} wurde aktualisiert",
|
||||||
"generic-created-with-url": "{name} has been created, {url}",
|
"generic-created-with-url": "{name} wurde erstellt, {url}",
|
||||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
"generic-updated-with-url": "{name} wurde aktualisiert, {url}",
|
||||||
"generic-deleted": "{name} has been created"
|
"generic-deleted": "{name} wurde erstellt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"liter": {
|
"liter": {
|
||||||
"name": "Liter",
|
"name": "Liter",
|
||||||
"description": "",
|
"description": "",
|
||||||
"abbreviation": "l"
|
"abbreviation": "L"
|
||||||
},
|
},
|
||||||
"pound": {
|
"pound": {
|
||||||
"name": "Pfund",
|
"name": "Pfund",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.core.exceptions import mealie_registered_exceptions
|
from mealie.core.exceptions import mealie_registered_exceptions
|
||||||
@@ -8,6 +8,7 @@ from mealie.routes._base.base_controllers import BaseUserController
|
|||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.schema.reports.reports import ReportCategory, ReportCreate, ReportOut, ReportSummary
|
from mealie.schema.reports.reports import ReportCategory, ReportCreate, ReportOut, ReportSummary
|
||||||
|
from mealie.schema.response.responses import ErrorResponse, SuccessResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/reports", tags=["Groups: Reports"])
|
router = APIRouter(prefix="/groups/reports", tags=["Groups: Reports"])
|
||||||
|
|
||||||
@@ -39,6 +40,10 @@ class GroupReportsController(BaseUserController):
|
|||||||
def get_one(self, item_id: UUID4):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
|
|
||||||
@router.delete("/{item_id}", status_code=204)
|
@router.delete("/{item_id}", status_code=200)
|
||||||
def delete_one(self, item_id: UUID4):
|
def delete_one(self, item_id: UUID4):
|
||||||
|
try:
|
||||||
self.mixins.delete_one(item_id) # type: ignore
|
self.mixins.delete_one(item_id) # type: ignore
|
||||||
|
return SuccessResponse.respond("Report deleted.")
|
||||||
|
except Exception as ex:
|
||||||
|
raise HTTPException(500, ErrorResponse.respond("Failed to delete report")) from ex
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from fastapi.encoders import jsonable_encoder
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
from mealie.core import exceptions
|
from mealie.core import exceptions
|
||||||
@@ -17,7 +16,6 @@ from mealie.core.dependencies import temporary_zip_path
|
|||||||
from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token
|
from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token
|
||||||
from mealie.core.security import create_recipe_slug_token
|
from mealie.core.security import create_recipe_slug_token
|
||||||
from mealie.pkgs import cache
|
from mealie.pkgs import cache
|
||||||
from mealie.repos.all_repositories import get_repositories
|
|
||||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||||
from mealie.routes._base import BaseUserController, controller
|
from mealie.routes._base import BaseUserController, controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
@@ -29,17 +27,16 @@ from mealie.schema.recipe.recipe_asset import RecipeAsset
|
|||||||
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
||||||
from mealie.schema.recipe.request_helpers import RecipeZipTokenResponse, UpdateImageResponse
|
from mealie.schema.recipe.request_helpers import RecipeZipTokenResponse, UpdateImageResponse
|
||||||
from mealie.schema.response.responses import ErrorResponse
|
from mealie.schema.response.responses import ErrorResponse
|
||||||
from mealie.schema.server.tasks import ServerTaskNames
|
|
||||||
from mealie.services import urls
|
from mealie.services import urls
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
||||||
from mealie.services.event_bus_service.message_types import EventTypes
|
from mealie.services.event_bus_service.message_types import EventTypes
|
||||||
from mealie.services.recipe.recipe_data_service import RecipeDataService
|
from mealie.services.recipe.recipe_data_service import RecipeDataService
|
||||||
from mealie.services.recipe.recipe_service import RecipeService
|
from mealie.services.recipe.recipe_service import RecipeService
|
||||||
from mealie.services.recipe.template_service import TemplateService
|
from mealie.services.recipe.template_service import TemplateService
|
||||||
|
from mealie.services.scraper.recipe_bulk_scraper import RecipeBulkScraperService
|
||||||
from mealie.services.scraper.scraped_extras import ScraperContext
|
from mealie.services.scraper.scraped_extras import ScraperContext
|
||||||
from mealie.services.scraper.scraper import create_from_url
|
from mealie.services.scraper.scraper import create_from_url
|
||||||
from mealie.services.scraper.scraper_strategies import RecipeScraperPackage
|
from mealie.services.scraper.scraper_strategies import RecipeScraperPackage
|
||||||
from mealie.services.server_tasks.background_executory import BackgroundExecutor
|
|
||||||
|
|
||||||
|
|
||||||
class BaseRecipeController(BaseUserController):
|
class BaseRecipeController(BaseUserController):
|
||||||
@@ -172,39 +169,11 @@ class RecipeController(BaseRecipeController):
|
|||||||
@router.post("/create-url/bulk", status_code=202)
|
@router.post("/create-url/bulk", status_code=202)
|
||||||
def parse_recipe_url_bulk(self, bulk: CreateRecipeByUrlBulk, bg_tasks: BackgroundTasks):
|
def parse_recipe_url_bulk(self, bulk: CreateRecipeByUrlBulk, bg_tasks: BackgroundTasks):
|
||||||
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
||||||
bg_executor = BackgroundExecutor(self.group.id, self.repos, bg_tasks)
|
bulk_scraper = RecipeBulkScraperService(self.service, self.repos, self.group)
|
||||||
|
report_id = bulk_scraper.get_report_id()
|
||||||
|
bg_tasks.add_task(bulk_scraper.scrape, bulk)
|
||||||
|
|
||||||
def bulk_import_func(task_id: int, session: Session) -> None:
|
return {"reportId": report_id}
|
||||||
database = get_repositories(session)
|
|
||||||
task = database.server_tasks.get_one(task_id)
|
|
||||||
|
|
||||||
task.append_log("test task has started")
|
|
||||||
|
|
||||||
for b in bulk.imports:
|
|
||||||
try:
|
|
||||||
recipe, _ = create_from_url(b.url)
|
|
||||||
|
|
||||||
if b.tags:
|
|
||||||
recipe.tags = b.tags
|
|
||||||
|
|
||||||
if b.categories:
|
|
||||||
recipe.recipe_category = b.categories
|
|
||||||
|
|
||||||
self.service.create_one(recipe)
|
|
||||||
task.append_log(f"INFO: Created recipe from url: {b.url}")
|
|
||||||
except Exception as e:
|
|
||||||
task.append_log(f"Error: Failed to create recipe from url: {b.url}")
|
|
||||||
task.append_log(f"Error: {e}")
|
|
||||||
self.deps.logger.error(f"Failed to create recipe from url: {b.url}")
|
|
||||||
self.deps.logger.error(e)
|
|
||||||
database.server_tasks.update(task.id, task)
|
|
||||||
|
|
||||||
task.set_finished()
|
|
||||||
database.server_tasks.update(task.id, task)
|
|
||||||
|
|
||||||
bg_executor.dispatch(ServerTaskNames.bulk_recipe_import, bulk_import_func)
|
|
||||||
|
|
||||||
return {"details": "task has been started"}
|
|
||||||
|
|
||||||
@router.post("/test-scrape-url")
|
@router.post("/test-scrape-url")
|
||||||
def test_parse_recipe_url(self, url: ScrapeRecipeTest):
|
def test_parse_recipe_url(self, url: ScrapeRecipeTest):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class ReportCategory(str, enum.Enum):
|
|||||||
backup = "backup"
|
backup = "backup"
|
||||||
restore = "restore"
|
restore = "restore"
|
||||||
migration = "migration"
|
migration = "migration"
|
||||||
|
bulk_import = "bulk_import"
|
||||||
|
|
||||||
|
|
||||||
class ReportSummaryStatus(str, enum.Enum):
|
class ReportSummaryStatus(str, enum.Enum):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import contextlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ from pydantic import UUID4
|
|||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.repos.all_repositories import AllRepositories
|
from mealie.repos.all_repositories import AllRepositories
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
|
from mealie.schema.recipe.recipe_settings import RecipeSettings
|
||||||
from mealie.schema.reports.reports import (
|
from mealie.schema.reports.reports import (
|
||||||
ReportCategory,
|
ReportCategory,
|
||||||
ReportCreate,
|
ReportCreate,
|
||||||
@@ -25,7 +27,7 @@ class BaseMigrator(BaseService):
|
|||||||
key_aliases: list[MigrationAlias]
|
key_aliases: list[MigrationAlias]
|
||||||
|
|
||||||
report_entries: list[ReportEntryCreate]
|
report_entries: list[ReportEntryCreate]
|
||||||
report_id: int
|
report_id: UUID4
|
||||||
report: ReportOut
|
report: ReportOut
|
||||||
|
|
||||||
helpers: DatabaseMigrationHelpers
|
helpers: DatabaseMigrationHelpers
|
||||||
@@ -111,7 +113,19 @@ class BaseMigrator(BaseService):
|
|||||||
|
|
||||||
return_vars = []
|
return_vars = []
|
||||||
|
|
||||||
|
group = self.db.groups.get_one(self.group_id)
|
||||||
|
|
||||||
|
default_settings = RecipeSettings(
|
||||||
|
public=group.preferences.recipe_public,
|
||||||
|
show_nutrition=group.preferences.recipe_show_nutrition,
|
||||||
|
show_assets=group.preferences.recipe_show_assets,
|
||||||
|
landscape_view=group.preferences.recipe_landscape_view,
|
||||||
|
disable_comments=group.preferences.recipe_disable_comments,
|
||||||
|
disable_amount=group.preferences.recipe_disable_amount,
|
||||||
|
)
|
||||||
|
|
||||||
for recipe in validated_recipes:
|
for recipe in validated_recipes:
|
||||||
|
recipe.settings = default_settings
|
||||||
|
|
||||||
recipe.user_id = self.user_id
|
recipe.user_id = self.user_id
|
||||||
recipe.group_id = self.group_id
|
recipe.group_id = self.group_id
|
||||||
@@ -125,7 +139,7 @@ class BaseMigrator(BaseService):
|
|||||||
if self.add_migration_tag:
|
if self.add_migration_tag:
|
||||||
recipe.tags.append(migration_tag)
|
recipe.tags.append(migration_tag)
|
||||||
|
|
||||||
exception = ""
|
exception: str | Exception = ""
|
||||||
status = False
|
status = False
|
||||||
try:
|
try:
|
||||||
recipe = self.db.recipes.create(recipe)
|
recipe = self.db.recipes.create(recipe)
|
||||||
@@ -189,10 +203,8 @@ class BaseMigrator(BaseService):
|
|||||||
"""
|
"""
|
||||||
recipe_dict = self.rewrite_alias(recipe_dict)
|
recipe_dict = self.rewrite_alias(recipe_dict)
|
||||||
|
|
||||||
try:
|
with contextlib.suppress(KeyError):
|
||||||
del recipe_dict["id"]
|
del recipe_dict["id"]
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
recipe_dict = cleaner.clean(recipe_dict, url=recipe_dict.get("org_url", None))
|
recipe_dict = cleaner.clean(recipe_dict, url=recipe_dict.get("org_url", None))
|
||||||
|
|
||||||
|
|||||||
104
mealie/services/scraper/recipe_bulk_scraper.py
Normal file
104
mealie/services/scraper/recipe_bulk_scraper.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from pydantic import UUID4
|
||||||
|
|
||||||
|
from mealie.repos.repository_factory import AllRepositories
|
||||||
|
from mealie.schema.recipe.recipe import CreateRecipeByUrlBulk
|
||||||
|
from mealie.schema.reports.reports import ReportCategory, ReportCreate, ReportEntryCreate, ReportSummaryStatus
|
||||||
|
from mealie.schema.user.user import GroupInDB
|
||||||
|
from mealie.services._base_service import BaseService
|
||||||
|
from mealie.services.recipe.recipe_service import RecipeService
|
||||||
|
from mealie.services.scraper.scraper import create_from_url
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeBulkScraperService(BaseService):
|
||||||
|
report_entries: list[ReportEntryCreate]
|
||||||
|
|
||||||
|
def __init__(self, service: RecipeService, repos: AllRepositories, group: GroupInDB) -> None:
|
||||||
|
self.service = service
|
||||||
|
self.repos = repos
|
||||||
|
self.group = group
|
||||||
|
self.report_entries = []
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def get_report_id(self) -> UUID4:
|
||||||
|
import_report = ReportCreate(
|
||||||
|
name="Bulk Import",
|
||||||
|
category=ReportCategory.bulk_import,
|
||||||
|
status=ReportSummaryStatus.in_progress,
|
||||||
|
group_id=self.group.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.report = self.repos.group_reports.create(import_report)
|
||||||
|
return self.report.id
|
||||||
|
|
||||||
|
def _add_error_entry(self, message: str, exception: str = "") -> None:
|
||||||
|
self.report_entries.append(
|
||||||
|
ReportEntryCreate(
|
||||||
|
report_id=self.report.id,
|
||||||
|
success=False,
|
||||||
|
message=message,
|
||||||
|
exception=exception,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _save_all_entries(self) -> None:
|
||||||
|
is_success = True
|
||||||
|
is_failure = True
|
||||||
|
|
||||||
|
for entry in self.report_entries:
|
||||||
|
if is_failure and entry.success:
|
||||||
|
is_failure = False
|
||||||
|
|
||||||
|
if is_success and not entry.success:
|
||||||
|
is_success = False
|
||||||
|
|
||||||
|
self.repos.group_report_entries.create(entry)
|
||||||
|
|
||||||
|
if is_success:
|
||||||
|
self.report.status = ReportSummaryStatus.success
|
||||||
|
|
||||||
|
if is_failure:
|
||||||
|
self.report.status = ReportSummaryStatus.failure
|
||||||
|
|
||||||
|
if not is_success and not is_failure:
|
||||||
|
self.report.status = ReportSummaryStatus.partial
|
||||||
|
|
||||||
|
self.repos.group_reports.update(self.report.id, self.report)
|
||||||
|
|
||||||
|
def scrape(self, urls: CreateRecipeByUrlBulk) -> None:
|
||||||
|
if self.report is None:
|
||||||
|
self.get_report_id()
|
||||||
|
|
||||||
|
for b in urls.imports:
|
||||||
|
|
||||||
|
try:
|
||||||
|
recipe, _ = create_from_url(b.url)
|
||||||
|
except Exception as e:
|
||||||
|
self.service.logger.error(f"failed to scrape url during bulk url import {b.url}")
|
||||||
|
self.service.logger.exception(e)
|
||||||
|
self._add_error_entry(f"failed to scrape url {b.url}", str(e))
|
||||||
|
break
|
||||||
|
|
||||||
|
if b.tags:
|
||||||
|
recipe.tags = b.tags
|
||||||
|
|
||||||
|
if b.categories:
|
||||||
|
recipe.recipe_category = b.categories
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.service.create_one(recipe)
|
||||||
|
except Exception as e:
|
||||||
|
self.service.logger.error(f"Failed to save recipe to database during bulk url import {b.url}")
|
||||||
|
self.service.logger.exception(e)
|
||||||
|
self._add_error_entry("Failed to save recipe to database during bulk url import", str(e))
|
||||||
|
|
||||||
|
self.report_entries.append(
|
||||||
|
ReportEntryCreate(
|
||||||
|
report_id=self.report.id,
|
||||||
|
success=True,
|
||||||
|
message=f"Successfully imported recipe {recipe.name}",
|
||||||
|
exception="",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._save_all_entries()
|
||||||
8
poetry.lock
generated
8
poetry.lock
generated
@@ -1188,7 +1188,7 @@ rdflib = ">=5.0.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "recipe-scrapers"
|
name = "recipe-scrapers"
|
||||||
version = "13.33.0"
|
version = "14.1.0"
|
||||||
description = "Python package, scraping recipes from all over the internet"
|
description = "Python package, scraping recipes from all over the internet"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -1545,7 +1545,7 @@ pgsql = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "a84820195816740ce9200a8a9bf1cc8568cc30c1f97264cc2506b1f6289d1883"
|
content-hash = "45c28207b80dd8ecd82030410c132be32e8f2e46925c92641d4dd1626fec7786"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
@@ -2437,8 +2437,8 @@ rdflib-jsonld = [
|
|||||||
{file = "rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b"},
|
{file = "rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b"},
|
||||||
]
|
]
|
||||||
recipe-scrapers = [
|
recipe-scrapers = [
|
||||||
{file = "recipe_scrapers-13.33.0-py3-none-any.whl", hash = "sha256:c350ee2407167ec62327a1db9e8864f49a51cae06907689f9095885444549293"},
|
{file = "recipe_scrapers-14.1.0-py3-none-any.whl", hash = "sha256:fc4bf1d5bd142e63a81b1b734a874e2fd6686b22287b41a9bc1355c2004bd5f7"},
|
||||||
{file = "recipe_scrapers-13.33.0.tar.gz", hash = "sha256:be1742077bca55638392446b55bf7d2e80a9f9a9625285dc30efd02c763461ec"},
|
{file = "recipe_scrapers-14.1.0.tar.gz", hash = "sha256:5c6931dc13cdb458f7ce52c2fae6a63348ee826a9e0f71ba7679a3d3f7a9257b"},
|
||||||
]
|
]
|
||||||
requests = [
|
requests = [
|
||||||
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ passlib = "^1.7.4"
|
|||||||
lxml = "^4.7.1"
|
lxml = "^4.7.1"
|
||||||
Pillow = "^8.2.0"
|
Pillow = "^8.2.0"
|
||||||
apprise = "^0.9.6"
|
apprise = "^0.9.6"
|
||||||
recipe-scrapers = "^13.33.0"
|
recipe-scrapers = "^14.1.0"
|
||||||
psycopg2-binary = {version = "^2.9.1", optional = true}
|
psycopg2-binary = {version = "^2.9.1", optional = true}
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
emails = "^0.6"
|
emails = "^0.6"
|
||||||
|
|||||||
Reference in New Issue
Block a user