Compare commits

..

28 Commits

Author SHA1 Message Date
Hayden
b981cf62bf chore: bump version (#1307)
* bump version

* add release notes
2022-05-28 17:13:36 -08:00
Hayden
ee93d77ace Update stale.yml
exclude tasks
2022-05-28 17:04:40 -08:00
Hayden
3dcfcc1fa9 feat: rewrite print implementation to support new ing (#1305) 2022-05-28 17:00:37 -08:00
Hayden
80f1a9add8 New Crowdin updates (#1304)
* New translations en-US.json (French)

* New translations en-US.json (German)

* New translations en-US.json (German)

* New translations en-US.json (German)
2022-05-28 16:38:59 -08:00
Hayden
137bf9de91 bump recipe scrapers version (#1303) 2022-05-28 16:38:47 -08:00
Drumstickx
1534f0df77 docs: docker-compose.dev.yml is currently not functional (#1300) 2022-05-27 16:17:47 -08:00
Drumstickx
d751e3b35b docs: add references for VSCode dev containers (#1299) 2022-05-27 16:17:39 -08:00
Hayden
07bf5be3ec improve touch support with icon and handle (#1302) 2022-05-27 16:17:25 -08:00
Hayden
a96f94a149 fix: add touch support for mealplanner delete (#1298)
* add touch support for mealplanner delete

* remove click.stop
2022-05-26 19:31:54 -08:00
Hayden
78a8204b58 New translations en-US.json (German) (#1296) 2022-05-26 09:01:17 -08:00
Hayden
649e34f66e add touch support for mealplanner (#1295) 2022-05-25 20:13:13 -08:00
Hayden
010aafa69b feat: add reports to bulk recipe import (url) (#1294)
* remove unused docker and caddy configs

* add experimental nested configs

* switch to nest under docker-compose

* remove v-card

* bulk parser backend re-implementation

* refactor UI for bulk importer

* remove migration specific report text
2022-05-25 19:33:58 -08:00
Hayden
d66d6c55ae fix recipe assets build (#1286) 2022-05-25 11:50:45 -08:00
Hayden
7609715d9e remove explicity typescript version (#1285)
* remove explicity typescript version

* i hate javascript
2022-05-25 10:14:24 -08:00
Hayden
921fceddea chore: update dev dependencies (#1282)
* update dev dependencies

* upgrade eslint

* resolve several errors

* resolve eslint errors
2022-05-25 09:38:21 -08:00
Hayden
01f3fef21f New Crowdin updates (#1284)
* New translations en-US.json (German)

* New translations en-US.json (German)
2022-05-25 09:08:41 -08:00
Philipp Fischbeck
8f7c7c39bb refactor: split up recipe create page (#1283)
* refactor: split up recipe create page

* add flat card

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-05-25 09:08:32 -08:00
Hayden
30d19c6503 fix: bad dev dependency (#1281)
* revert bad dev dependency

* fix concurrent builds
2022-05-24 21:19:50 -08:00
Hayden
ea503a0235 add conccureny config (#1280) 2022-05-24 20:24:33 -08:00
dependabot[bot]
c05c123880 fix(deps): bump @nuxtjs/auth-next in /frontend (#1265)
Bumps [@nuxtjs/auth-next](https://github.com/nuxt-community/auth-module) from 5.0.0-1624817847.21691f1 to 5.0.0-1648802546.c9880dc.
- [Release notes](https://github.com/nuxt-community/auth-module/releases)
- [Changelog](https://github.com/nuxt-community/auth-module/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/nuxt-community/auth-module/commits)

---
updated-dependencies:
- dependency-name: "@nuxtjs/auth-next"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:02:43 -08:00
dependabot[bot]
6f45de6167 chore(deps-dev): bump vue2-script-setup-transform in /frontend (#1263)
Bumps [vue2-script-setup-transform](https://github.com/antfu/vue2-script-setup-transform) from 0.2.6 to 0.3.5.
- [Release notes](https://github.com/antfu/vue2-script-setup-transform/releases)
- [Commits](https://github.com/antfu/vue2-script-setup-transform/compare/v0.2.6...v0.3.5)

---
updated-dependencies:
- dependency-name: vue2-script-setup-transform
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:02:00 -08:00
dependabot[bot]
d634e2fbe1 chore(deps-dev): bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend (#1260)
Bumps [nuxt-vite](https://github.com/nuxt/vite) from 0.1.3 to 0.3.5.
- [Release notes](https://github.com/nuxt/vite/releases)
- [Changelog](https://github.com/nuxt/vite/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nuxt/vite/compare/v0.1.3...v0.3.5)

---
updated-dependencies:
- dependency-name: nuxt-vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:00:50 -08:00
dependabot[bot]
43a566339a chore(deps-dev): bump @vue/runtime-dom in /frontend (#1259)
Bumps [@vue/runtime-dom](https://github.com/vuejs/core/tree/HEAD/packages/runtime-dom) from 3.2.35 to 3.2.36.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.2.36/packages/runtime-dom)

---
updated-dependencies:
- dependency-name: "@vue/runtime-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 20:00:16 -08:00
dependabot[bot]
cc284a0ceb chore(deps-dev): bump eslint-plugin-nuxt in /frontend (#1258)
Bumps [eslint-plugin-nuxt](https://github.com/nuxt/eslint-plugin-nuxt) from 2.0.0 to 3.2.0.
- [Release notes](https://github.com/nuxt/eslint-plugin-nuxt/releases)
- [Changelog](https://github.com/nuxt/eslint-plugin-nuxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nuxt/eslint-plugin-nuxt/compare/v2.0.0...v3.2.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-nuxt
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 19:59:49 -08:00
dependabot[bot]
3c19105d8b fix(deps): bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend (#1257)
Bumps [isomorphic-dompurify](https://github.com/kkomelin/isomorphic-dompurify) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/kkomelin/isomorphic-dompurify/releases)
- [Commits](https://github.com/kkomelin/isomorphic-dompurify/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: isomorphic-dompurify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-24 19:58:14 -08:00
Hayden
b8ee1a4bd8 fix #1270 migration not capture settings (#1272) 2022-05-24 09:55:15 -08:00
Hayden
c30ffbc851 update readme 2022-05-24 09:51:39 -08:00
Hayden
3ddbc033b2 chore: github stalebot changes (#1271)
* add exempt labels

* update tags for docker-compose
2022-05-24 08:30:07 -08:00
65 changed files with 3234 additions and 2380 deletions

6
.github/stale.yml vendored
View File

@@ -6,8 +6,12 @@ daysUntilClose: 7
exemptLabels:
- pinned
- security
- early-stages
- "bug: confirmed"
- feedback
- task
# 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
markComment: >
This issue has been automatically marked as stale because it has not had

View File

@@ -5,6 +5,10 @@ on:
branches:
- mealie-next
concurrency:
group: backend-nightly-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest

View File

@@ -5,6 +5,10 @@ on:
branches:
- mealie-next
concurrency:
group: frontend-nightly-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest

View File

@@ -46,5 +46,12 @@
"python.linting.mypyEnabled": true,
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
"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"
}
}

View File

@@ -5,12 +5,6 @@
[![MIT License][license-shield]][license-url]
[![Docker Pulls][docker-pull]][docker-pull]
[![CodeFactor](https://www.codefactor.io/repository/github/hay-kot/mealie/badge)](https://www.codefactor.io/repository/github/hay-kot/mealie)
[![Docker Build Production](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.release.yml/badge.svg)](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.release.yml)
[![Project Tests Production](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml/badge.svg)](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
[![Docker Build Dev](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.dev.yml/badge.svg?branch=dev)](https://github.com/hay-kot/mealie/actions/workflows/dockerbuild.dev.yml)
[![Project Tests Dev](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml/badge.svg?branch=dev)](https://github.com/hay-kot/mealie/actions/workflows/test-all.yml)
<!-- PROJECT LOGO -->
<br />
@@ -26,20 +20,14 @@
<p align="center">
A Place for All Your Recipes
<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>
<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://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>
</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.
- 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.

View File

@@ -1,3 +1,4 @@
# WARNING: currently not functional, see #756, #1072
# Use root/example as user/password credentials
version: "3.4"
services:

View File

@@ -1,15 +0,0 @@
{
auto_https off
}
:80 {
root * /srv
encode gzip
uri strip_suffix /
handle {
try_files {path} {path}/ /index.html
file_server
}
}

View File

@@ -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

View File

@@ -1,11 +0,0 @@
version: "3"
services:
wiki:
container_name: mealie-docs
image: mealie-docs
ports:
- 8888:80
build:
context: .
dockerfile: Dockerfile
restart: always

View 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))

View File

@@ -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.
## With VS Code Dev Containers
## With [VSCode Dev Containers](https://code.visualstudio.com/docs/remote/containers)
Prerequisites
@@ -110,7 +110,7 @@ frontend 🎬 Start Mealie Frontend Development Server
frontend-build 🏗 Build Frontend in frontend/dist
frontend-generate 🏗 Generate Code for Frontend
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
```

View File

@@ -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.
[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

View File

@@ -10,7 +10,7 @@
version: "3.7"
services:
mealie-frontend:
image: hkotel/mealie:frontend-nightly
image: hkotel/mealie:frontend-v1.0.0-beta-1
container_name: mealie-frontend
depends_on:
- mealie-api
@@ -23,7 +23,7 @@ services:
volumes:
- mealie-data:/app/data/ # (3)
mealie-api:
image: hkotel/mealie:api-nightly
image: hkotel/mealie:api-v1.0.0-beta-1
container_name: mealie-api
depends_on:
- postgres

View File

@@ -12,7 +12,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
version: "3.7"
services:
mealie-frontend:
image: hkotel/mealie:frontend-nightly
image: hkotel/mealie:frontend-v1.0.0-beta-1
container_name: mealie-frontend
environment:
# Set Frontend ENV Variables Here
@@ -23,7 +23,7 @@ services:
volumes:
- mealie-data:/app/data/ # (3)
mealie-api:
image: hkotel/mealie:api-nightly
image: hkotel/mealie:api-v1.0.0-beta-1
container_name: mealie-api
volumes:
- mealie-data:/app/data/

File diff suppressed because one or more lines are too long

View File

@@ -88,6 +88,7 @@ nav:
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
- Change Log:
- v1.0.0beta-2: "changelog/v1.0.0beta-2.md"
- v1.0.0 Beta: "changelog/v1.0.0.md"
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"
- v0.5.1 Bug Fixes: "changelog/v0.5.1.md"

View File

@@ -1,5 +1,8 @@
module.exports = {
root: true,
settings: {
"import/ignore": ["@vueuse*"],
},
env: {
browser: true,
node: true,
@@ -35,6 +38,7 @@ module.exports = {
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline": "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/valid-v-slot": [
"error",

View File

@@ -28,9 +28,13 @@
</v-list-item-avatar>
<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>
{{ $d(Date.parse(item.timeStamp), "long") }}
</v-list-item-subtitle>
@@ -103,5 +107,4 @@ export default defineComponent({
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -10,13 +10,17 @@
<v-list-item-icon class="ma-auto">
<v-tooltip bottom>
<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>
<span>{{ getIconDefinition(item.icon).title }}</span>
</v-tooltip>
</v-list-item-icon>
<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-action>
<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">
<v-spacer></v-spacer>
<BaseDialog
v-model="newAssetDialog"
:title="$t('asset.new-asset')"
:icon="getIconDefinition(newAsset.icon).icon"
v-model="state.newAssetDialog"
:title="$tc('asset.new-asset')"
:icon="getIconDefinition(state.newAsset.icon).icon"
@submit="addAsset"
>
<template #activator>
<BaseButton v-if="edit" small create @click="newAssetDialog = true" />
</template>
<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">
<v-select
v-model="newAsset.icon"
v-model="state.newAsset.icon"
dense
:prepend-icon="getIconDefinition(newAsset.icon).icon"
:prepend-icon="getIconDefinition(state.newAsset.icon).icon"
:items="iconOptions"
item-text="title"
item-value="name"
@@ -66,7 +70,7 @@
</v-select>
<AppButtonUpload :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
</div>
{{ fileObject.name }}
{{ state.fileObject.name }}
</v-card-text>
</BaseDialog>
</div>
@@ -74,9 +78,10 @@
</template>
<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 { alert } from "~/composables/use-toast";
import { RecipeAsset } from "~/types/api-types/recipe";
const BASE_URL = window.location.origin;
@@ -91,7 +96,7 @@ export default defineComponent({
required: true,
},
value: {
type: Array,
type: Array as () => RecipeAsset[],
required: true,
},
edit: {
@@ -181,7 +186,7 @@ export default defineComponent({
}
return {
...toRefs(state),
state,
addAsset,
assetURL,
assetEmbed,

View File

@@ -29,7 +29,9 @@
<v-list dense>
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
<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-title>{{ item.title }}</v-list-item-title>
</v-list-item>

View File

@@ -81,7 +81,7 @@
<v-list dense>
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
<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-title>{{ item.title }}</v-list-item-title>
</v-list-item>
@@ -307,7 +307,7 @@ export default defineComponent({
}
// Note: Print is handled as an event in the parent component
const eventHandlers: { [key: string]: () => void } = {
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
delete: () => {
state.recipeDeleteDialog = true;
},

View File

@@ -32,7 +32,9 @@
<img src="https://i.pravatar.cc/300" alt="John" />
</v-list-item-avatar>
<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>
</template>
@@ -95,31 +97,30 @@ export default defineComponent({
context.emit(INPUT_EVENT, value);
}
const show = props.showHeaders;
const headers = computed(() => {
const hdrs = [];
if (show.id) {
if (props.showHeaders.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: "Name", value: "name" });
if (show.categories) {
if (props.showHeaders.categories) {
hdrs.push({ text: "Categories", value: "recipeCategory" });
}
if (show.tags) {
if (props.showHeaders.tags) {
hdrs.push({ text: "Tags", value: "tags" });
}
if (show.tools) {
if (props.showHeaders.tools) {
hdrs.push({ text: "Tools", value: "tools" });
}
if (show.recipeYield) {
if (props.showHeaders.recipeYield) {
hdrs.push({ text: "Yield", value: "recipeYield" });
}
if (show.dateAdded) {
if (props.showHeaders.dateAdded) {
hdrs.push({ text: "Date Added", value: "dateAdded" });
}

View File

@@ -57,8 +57,7 @@
<script lang="ts">
import { defineComponent, computed, toRefs, reactive, useContext } from "@nuxtjs/composition-api";
import { whenever } from "@vueuse/shared";
import { useClipboard, useShare } from "@vueuse/core";
import { useClipboard, useShare, whenever } from "@vueuse/core";
import { RecipeShareToken } from "~/types/api-types/recipe";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";

View File

@@ -134,8 +134,6 @@ export default defineComponent({
},
},
setup(props) {
const { value } = props;
// ==================================================
// Foods
const { foods, workingFoodData, actions: foodActions } = useFoods();
@@ -144,7 +142,7 @@ export default defineComponent({
async function createAssignFood() {
workingFoodData.name = foodSearch.value;
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() {
workingUnitData.name = unitSearch.value;
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({
@@ -165,7 +163,7 @@ export default defineComponent({
function toggleTitle() {
if (state.showTitle) {
value.title = "";
props.value.title = "";
}
state.showTitle = !state.showTitle;
}
@@ -175,13 +173,21 @@ export default defineComponent({
}
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();
}
}
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();
}
}
@@ -202,7 +208,7 @@ export default defineComponent({
// });
// }
if (value.originalText) {
if (props.value.originalText) {
options.push({
text: "See Original Text",
event: "toggle-original",

View File

@@ -92,7 +92,7 @@
@click="toggleCollapseSection(index)"
>
<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-text-field
v-if="edit"

View File

@@ -1,68 +1,58 @@
<template>
<div>
<div v-if="recipe" class="container print">
<div>
<h1>
<svg class="icon" viewBox="0 0 24 24">
<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>
<p>{{ ingredient.note }}</p>
</li>
</ul>
</div>
<div>
<h2>{{ $t("recipe.instructions") }}</h2>
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
<h2 v-if="step.title">{{ step.title }}</h2>
<div class="ml-5">
<h3>{{ $t("recipe.step-index", { step: index + 1 }) }}</h3>
<VueMarkdown :source="step.text"> </VueMarkdown>
</div>
<div class="print-container">
<section>
<v-card-title class="headline pl-0">
<v-icon left color="primary">
{{ $globals.icons.primary }}
</v-icon>
{{ 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>
</ul>
</div>
<br />
<v-divider v-if="recipe.notes.length > 0" class="mb-5 mt-0"></v-divider>
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
<h3>{{ note.title }}</h3>
<VueMarkdown :source="note.text"> </VueMarkdown>
<div class="ingredient-col-2">
<ul>
<li v-for="(text, index) in splitIngredients.value.secondHalf" :key="index">
{{ text }}
</li>
</ul>
</div>
</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">
<h3 v-if="step.title" class="mb-2">{{ step.title }}</h3>
<div class="ml-5">
<h4>{{ $t("recipe.step-index", { step: index + 1 }) }}</h4>
<VueMarkdown :source="step.text" />
</div>
</div>
</section>
<v-divider v-if="hasNotes" class="grey my-4"></v-divider>
<section>
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
<h4>{{ note.title }}</h4>
<VueMarkdown :source="note.text" />
</div>
</section>
</div>
</template>
@@ -70,8 +60,16 @@
import { defineComponent } from "@nuxtjs/composition-api";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { computed } from "@vue/reactivity";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import { Recipe } from "~/types/api-types/recipe";
import { parseIngredientText } from "~/composables/recipes";
type SplitIngredients = {
firstHalf: string[];
secondHalf: string[];
};
export default defineComponent({
components: {
RecipeTimeCard,
@@ -83,6 +81,36 @@ export default defineComponent({
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>
@@ -90,26 +118,46 @@ export default defineComponent({
@media print {
body,
html {
margin-top: -40px !important;
margin-top: 0 !important;
}
.print-container {
display: block !important;
}
.v-main {
display: block;
}
.v-main__wrap {
position: absolute;
top: 0;
left: 0;
}
}
</style>
h1 {
margin-top: 0 !important;
display: -webkit-box;
display: flex;
font-size: 2rem;
letter-spacing: -0.015625em;
font-weight: 300;
padding: 0;
<style scoped>
.print-container {
display: none;
background-color: white;
color: black !important;
}
h2 {
margin-bottom: 0.25rem;
p {
padding-bottom: 0 !important;
margin-bottom: 0 !important;
}
h3 {
margin-bottom: 0.25rem;
.v-card__text {
padding-bottom: 0;
margin-bottom: 0;
}
.ingredient-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
ul {
@@ -117,51 +165,7 @@ ul {
}
li {
display: -webkit-box;
display: -webkit-flex;
margin-left: 0;
margin-bottom: 0.5rem;
list-style-type: none;
margin-bottom: 0.25rem;
}
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>

View File

@@ -145,6 +145,8 @@ import { AutoFormItems } from "~/types/auto-forms";
const BLUR_EVENT = "blur";
type ValidatorKey = keyof typeof validators;
export default defineComponent({
name: "AutoForm",
props: {
@@ -178,7 +180,7 @@ export default defineComponent({
},
},
setup(props, context) {
function rulesByKey(keys?: string[] | null) {
function rulesByKey(keys?: ValidatorKey[] | null) {
if (keys === undefined || keys === null) {
return [];
}
@@ -193,7 +195,7 @@ export default defineComponent({
return list;
}
const defaultRules = computed(() => rulesByKey(props.globalRules));
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
function removeByIndex(list: never[], index: number) {
// Removes the item at the index

View File

@@ -10,7 +10,7 @@
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
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" />
</v-sheet>
</slot>

View File

@@ -2,18 +2,10 @@
<BaseDialog v-model="dialog" :icon="$globals.icons.translate" :title="$tc('language-dialog.choose-language')">
<v-card-text>
{{ $t("language-dialog.select-description") }}
<v-autocomplete
v-model="locale"
:items="locales"
item-text="name"
class="my-3"
hide-details
outlined
offset
>
<v-autocomplete v-model="locale" :items="locales" item-text="name" class="my-3" hide-details outlined offset>
<template #item="{ item }">
<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-content>
</template>

View File

@@ -4,10 +4,10 @@
<slot v-bind="{ state, toggle }"></slot>
</component>
</template>
<script lang="ts">
import { defineComponent, watch } from "@nuxtjs/composition-api";
import { useToggle } from "@vueuse/shared";
import { useToggle } from "@vueuse/core";
export default defineComponent({
props: {
@@ -34,4 +34,3 @@ export default defineComponent({
},
});
</script>

View File

@@ -1,6 +1,6 @@
import { AxiosResponse } from "axios";
import { useContext } from "@nuxtjs/composition-api";
import { NuxtAxiosInstance } from "@nuxtjs/axios";
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
import { AdminAPI, Api } from "~/api";
import { ApiRequestInstance, RequestResponse } from "~/types/api";
import { PublicApi } from "~/api/public-api";

View File

@@ -3,7 +3,7 @@
"about": "Über",
"about-mealie": "Über Mealie",
"api-docs": "API Dokumentation",
"api-port": "API Port",
"api-port": "API-Port",
"application-mode": "Anwendungsmodus",
"database-type": "Datenbanktyp",
"database-url": "Datenbank URL",
@@ -33,7 +33,7 @@
"show-assets": "Anhänge anzeigen"
},
"category": {
"categories": "Categories",
"categories": "Kategorien",
"category-created": "Kategorie angelegt",
"category-creation-failed": "Anlegen der Kategorie fehlgeschlagen",
"category-deleted": "Kategorie entfernt",
@@ -44,7 +44,7 @@
"uncategorized-count": "{count} nicht kategorisierte"
},
"events": {
"apprise-url": "Apprise URL",
"apprise-url": "Apprise-URL",
"database": "Datenbank",
"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.",
@@ -66,7 +66,7 @@
"create": "Erstellen",
"created": "Erstellt",
"custom": "Benutzerdefiniert",
"dashboard": "Dashboard",
"dashboard": "Übersicht",
"delete": "Löschen",
"disabled": "Deaktiviert",
"download": "Herunterladen",
@@ -88,7 +88,7 @@
"image-upload-failed": "Das Bild konnte nicht hochgeladen werden",
"import": "Importieren",
"json": "JSON",
"keyword": "Keyword",
"keyword": "Schlüsselwort",
"link-copied": "Link kopiert",
"loading-recipes": "Lade Rezepte",
"monday": "Montag",
@@ -118,7 +118,7 @@
"success-count": "Erfolgreich: {count}",
"sunday": "Sonntag",
"templates": "Vorlagen:",
"test": "Test",
"test": "Testen",
"themes": "Themen",
"thursday": "Donnerstag",
"token": "Token",
@@ -131,10 +131,10 @@
"view": "Ansicht",
"wednesday": "Mittwoch",
"yes": "Ja",
"foods": "Foods",
"units": "Units",
"back": "Back",
"next": "Next"
"foods": "Speisen",
"units": "Einheiten",
"back": "Zurück",
"next": "Weiter"
},
"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?",
@@ -156,8 +156,8 @@
"user-group-created": "Benutzergruppe angelegt",
"user-group-creation-failed": "Anlegen der Benutzergruppe fehlgeschlagen",
"settings": {
"keep-my-recipes-private": "Keep My Recipes Private",
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
"keep-my-recipes-private": "Meine Rezepte privat halten",
"keep-my-recipes-private-description": "Setzt Ihre Gruppe und alle Rezepte standardmäßig privat. Sie können dies später jederzeit ändern."
}
},
"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-title": "Anscheinend konnten wir nichts finden",
"from-url": "Von URL",
"github-issues": "GitHub Issues",
"github-issues": "GitHub Fehlermeldungen",
"google-ld-json-info": "Google ld+json Info",
"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",
@@ -216,9 +216,9 @@
"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",
"view-scraped-data": "Gesammelte Daten anzeigen",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
"trim-whitespace-description": "Leerzeichen am Anfang und Ende sowie leere Zeilen entfernen",
"trim-prefix-description": "Erste Zeichen aus jeder Zeile entfernen",
"split-by-numbered-line-description": "Absätze nach dem Schema '1)' oder '1.' aufzuteilen versuchen"
},
"page": {
"404-page-not-found": "404 Seite nicht gefunden",
@@ -291,7 +291,7 @@
"title": "Titel",
"total-time": "Gesamtzeit",
"unable-to-delete-recipe": "Rezept kann nicht gelöscht werden",
"no-recipe": "No Recipe"
"no-recipe": "Kein Rezept"
},
"search": {
"advanced-search": "Erweiterte Suche",
@@ -396,7 +396,7 @@
"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",
"webhook-url": "Webhook URL",
"webhook-url": "Webhook-URL",
"webhooks-caps": "WEBHOOKS",
"webhooks": "Webhooks"
}
@@ -413,9 +413,9 @@
},
"sidebar": {
"all-recipes": "Alle Rezepte",
"backups": "Backups",
"backups": "Sicherungen",
"categories": "Kategorien",
"cookbooks": "Cookbooks",
"cookbooks": "Kochbücher",
"dashboard": "Übersicht",
"home-page": "Startseite",
"manage-users": "Benutzer",
@@ -425,7 +425,7 @@
"site-settings": "Einstellungen",
"tags": "Schlagworte",
"toolbox": "Werkzeuge",
"language": "Language"
"language": "Sprache"
},
"signup": {
"error-signing-up": "Fehler beim Registrieren",
@@ -448,7 +448,7 @@
"untagged-count": "{count} ohne Schlagworte"
},
"tool": {
"tools": "Tools"
"tools": "Werkzeuge"
},
"user": {
"admin": "Admin",
@@ -467,7 +467,7 @@
"error-cannot-delete-super-user": "Fehler! Super Benutzer kann nicht gelöscht werden",
"existing-password-does-not-match": "Bestehendes Passwort stimmt nicht überein",
"full-name": "Vollständiger Name",
"invite-only": "Invite Only",
"invite-only": "Nur auf Einladung",
"link-id": "Linkkennung",
"link-name": "Linkname",
"login": "Anmeldung",
@@ -480,8 +480,8 @@
"password-reset-failed": "Zurücksetzen des Passworts fehlgeschlagen",
"password-updated": "Passwort aktualisiert",
"password": "Passwort",
"password-strength": "Password is {strength}",
"register": "Register",
"password-strength": "Das Passwort ist {strength}",
"register": "Registrieren",
"reset-password": "Passwort zurücksetzen",
"sign-in": "Einloggen",
"total-mealplans": "Alle Essenspläne",
@@ -505,45 +505,45 @@
"webhooks-enabled": "Webhooks aktiviert",
"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",
"enable-advanced-content": "Enable Advanced Content",
"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": "Erweiterten Inhalt aktivieren",
"enable-advanced-content-description": "Aktiviert zusätzliche Funktionen wie Rezept-Skalierung, API-Schlüssel, Webhooks und Datenverwaltung. Keine Sorge, das kann später noch geändert werden."
},
"language-dialog": {
"translated": "translated",
"choose-language": "Choose Language",
"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!",
"read-the-docs": "Read the docs"
"translated": "übersetzt",
"choose-language": "Sprache wählen",
"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": "Ist etwas noch nicht übersetzt, falsch oder deine Sprache fehlt in der Liste? {read-the-docs-link} wie man beiträgt!",
"read-the-docs": "Dokumentation lesen"
},
"data-pages": {
"seed-data": "Seed Data",
"seed-data": "Musterdaten",
"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-food-example": "Merging {food1} into {food2}",
"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-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
"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": "{food1} wird zu {food2} zusammengeführt",
"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": "Sie haben bereits einige Elemente in Ihrer Datenbank. Diese Aktion wird Duplikate nicht ausgleichen, Sie müssen sie manuell verwalten."
},
"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": {
"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",
"join-a-group": "Join a Group",
"create-a-new-group": "Create a New Group",
"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-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-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"
"user-registration": "Benutzerregistrierung",
"join-a-group": "Gruppe beitreten",
"create-a-new-group": "Neue Gruppe erstellen",
"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": "Gruppendetails",
"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": "Musterdaten",
"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": "Kontoinformationen"
},
"validation": {
"group-name-is-taken": "Group name is taken",
"username-is-taken": "Username is taken",
"email-is-taken": "Email is taken"
"group-name-is-taken": "Gruppenname ist schon vergeben",
"username-is-taken": "Benutzername ist schon vergeben",
"email-is-taken": "E-Mail-Adresse ist schon vergeben"
}
}

View File

@@ -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"
},
"language-dialog": {
"translated": "translated",
"choose-language": "Choose Language",
"translated": "traduit",
"choose-language": "Choisir la langue",
"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!",
"read-the-docs": "Read the docs"
@@ -532,18 +532,18 @@
},
"user-registration": {
"user-registration": "User Registration",
"join-a-group": "Join a Group",
"create-a-new-group": "Create a New Group",
"join-a-group": "Rejoindre un groupe",
"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.",
"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!",
"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.",
"account-details": "Account Details"
"account-details": "Détails du compte"
},
"validation": {
"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"
}
}

View File

@@ -25,11 +25,17 @@
<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-avatar>
<v-icon v-text="item.icon"></v-icon>
<v-icon>
{{ item.icon }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.title"></v-list-item-title>
<v-list-item-subtitle v-text="item.subtitle"></v-list-item-subtitle>
<v-list-item-title>
{{ item.title }}
</v-list-item-title>
<v-list-item-subtitle>
{{ item.subtitle }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
@@ -116,7 +122,7 @@ export default defineComponent({
icon: this.$globals.icons.link,
title: "Import",
subtitle: "Import a recipe by URL",
to: "/recipe/create?tab=url",
to: "/recipe/create/url",
restricted: true,
},
{ divider: true },
@@ -124,7 +130,7 @@ export default defineComponent({
icon: this.$globals.icons.edit,
title: "Create",
subtitle: "Create a recipe manually",
to: "/recipe/create?tab=new",
to: "/recipe/create/new",
restricted: true,
},
{ divider: true },

View File

@@ -22,38 +22,35 @@
"@nuxtjs/proxy": "^2.1.0",
"@nuxtjs/pwa": "^3.3.5",
"@vue/composition-api": "^1.0.5",
"@vueuse/core": "^6.8.0",
"@vueuse/core": "^8.5.0",
"core-js": "^3.15.1",
"date-fns": "^2.23.0",
"isomorphic-dompurify": "^0.18.0",
"fuse.js": "^6.5.3",
"isomorphic-dompurify": "^0.19.0",
"nuxt": "^2.15.8",
"v-jsoneditor": "^1.4.5",
"vuedraggable": "^2.24.3",
"vuetify": "^2.5.5"
"vuetify": "^2.6.6"
},
"devDependencies": {
"@babel/eslint-parser": "^7.14.7",
"@nuxt/types": "^2.15.7",
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/composition-api": "^0.31.0",
"@nuxtjs/eslint-config-typescript": "^6.0.1",
"@nuxtjs/composition-api": "^0.32.0",
"@nuxtjs/eslint-config-typescript": "^10.0.0",
"@nuxtjs/eslint-module": "^3.0.2",
"@nuxtjs/google-fonts": "^1.3.0",
"@nuxtjs/vuetify": "^1.12.1",
"@types/sortablejs": "^1.10.7",
"@vue/runtime-dom": "^3.2.9",
"eslint": "^7.29.0",
"@vue/runtime-dom": "^3.2.36",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.12.1",
"lint-staged": "^10.5.4",
"nuxt-vite": "^0.1.1",
"eslint-plugin-nuxt": "^3.2.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.0.1",
"lint-staged": "^12.4.2",
"nuxt-vite": "0.2.3",
"prettier": "^2.3.2",
"vue2-script-setup-transform": "^0.2.0"
},
"resolutions": {
"vite": "2.3.8"
"vue2-script-setup-transform": "^0.3.5"
}
}

View File

@@ -40,7 +40,7 @@
>
<template #item="{ item }">
<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>

View File

@@ -65,7 +65,7 @@
>
<template #item="{ item }">
<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>

View File

@@ -315,8 +315,10 @@ export default defineComponent({
title: "Tag Recipes",
mode: MODES.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,
});

View File

@@ -69,7 +69,7 @@
>
<template #item="{ item }">
<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>

View File

@@ -95,6 +95,7 @@
<template v-if="edit">
<draggable
tag="div"
handle=".handle"
:value="plan.meals"
group="meals"
:data-index="index"
@@ -102,7 +103,13 @@
style="min-height: 150px"
@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-avatar :rounded="false">
<RecipeCardImage
@@ -126,7 +133,13 @@
</v-list-item-content>
</v-list-item>
<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>
<template #activator="{ on, attrs }">
<v-chip v-bind="attrs" label small color="accent" v-on="on" @click.prevent>
@@ -146,8 +159,8 @@
</v-list-item>
</v-list>
</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-btn>
</div>
@@ -291,10 +304,12 @@ export default defineComponent({
}
function onMoveCallback(evt: SortableEvent) {
const supportedEvents = ["drop", "touchend"];
// Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029
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?
console.log("Cancel Move Event");
} else {

View File

@@ -4,9 +4,7 @@
<template #header>
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/data-reports.svg')"></v-img>
</template>
<template #title> Recipe Data Migrations</template>
Recipes can be migrated from another supported application to Mealie. This is a great way to get started with
Mealie.
<template #title> Report </template>
</BasePageTitle>
<v-container v-if="report">
<BaseCardSectionTitle :title="report.name"> </BaseCardSectionTitle>
@@ -31,8 +29,9 @@
</template>
<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 { ReportOut } from "~/types/api-types/reports";
export default defineComponent({
setup() {
@@ -41,16 +40,11 @@ export default defineComponent({
const api = useUserApi();
const state = reactive({
report: {},
});
const report = ref<ReportOut | null>(null);
async function getReport() {
const { data } = await api.groupReports.getOne(id);
if (data) {
state.report = data;
}
report.value = data ?? null;
}
onMounted(async () => {
@@ -64,7 +58,7 @@ export default defineComponent({
];
return {
...toRefs(state),
report,
id,
itemHeaders,
};
@@ -72,5 +66,4 @@ export default defineComponent({
});
</script>
<style lang="scss" scoped>
</style>
<style lang="scss" scoped></style>

View File

@@ -454,12 +454,13 @@
>
<v-switch v-model="wakeLock" small label="Keep Screen Awake" />
</div>
<RecipeComments
v-if="recipe && !recipe.settings.disableComments && !form"
v-model="recipe.comments"
:slug="recipe.slug"
:recipe-id="recipe.id"
class="px-1 my-4"
class="px-1 my-4 d-print-none"
/>
<RecipePrintView v-if="recipe" :recipe="recipe" />
</v-container>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<v-container class="narrow-container flex-column pa-0">
<v-container class="flex-column">
<BasePageTitle divider>
<template #header>
<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
<template #content>
<div class="ml-auto">
<BaseOverflowButton v-model="tab" rounded :items="tabs"> </BaseOverflowButton>
<BaseOverflowButton v-model="subpage" rounded :items="subpages"> </BaseOverflowButton>
</div>
</template>
</BasePageTitle>
<section>
<v-tabs-items v-model="tab" class="mt-2">
<!-- 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>
<NuxtChild />
</section>
</v-container>
<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-container>
</AdvancedOnly>
@@ -321,39 +27,16 @@
</template>
<script lang="ts">
import {
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 { defineComponent, useRouter, useContext, computed, useRoute } from "@nuxtjs/composition-api";
import { MenuItem } from "~/components/global/BaseOverflowButton.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({
components: { AdvancedOnly, RecipeOrganizerSelector },
components: { AdvancedOnly },
setup() {
const state = reactive({
error: false,
loading: false,
});
const { $globals } = useContext();
const tabs: MenuItem[] = [
const subpages: MenuItem[] = [
{
icon: $globals.icons.link,
text: "Import with URL",
@@ -381,185 +64,21 @@ export default defineComponent({
},
];
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 tab = computed({
set(tab: string) {
router.replace({ query: { ...route.value.query, tab } });
const subpage = computed({
set(subpage: string) {
router.push({ path: `/recipe/create/${subpage}`, query: route.value.query });
},
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 {
allTags,
allCategories,
tab,
recipeUrl,
importKeywordsAsTags,
bulkCreate,
bulkUrls,
lockBulkImport,
debugTreeView,
tabs,
domCreateByName,
domUrlForm,
newRecipeName,
newRecipeZip,
debugUrl,
debugData,
createByName,
createByUrl,
createByZip,
...toRefs(state),
validators,
subpages,
subpage,
};
},
head() {
@@ -569,9 +88,3 @@ export default defineComponent({
},
});
</script>
<style>
.force-white > a {
color: white !important;
}
</style>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -216,7 +216,7 @@ export default defineComponent({
if (searchString.value.trim() === "") {
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);
});

View File

@@ -1,16 +1,16 @@
import { Plugin } from "@nuxt/types";
import { NuxtAxiosInstance } from "@nuxtjs/axios";
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
import { alert } from "~/composables/use-toast";
const toastPlugin: Plugin = ({ $axios }: { $axios: NuxtAxiosInstance }) => {
$axios.onResponse((response) => {
if (response?.data?.message) {
alert.info(response.data.message);
alert.info(response.data.message as string);
}
});
$axios.onError((error) => {
if (error.response?.data?.detail?.message) {
alert.error(error.response.data.detail.message);
alert.error(error.response.data.detail.message as string);
}
});
};

View File

@@ -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"],
"exclude": ["node_modules", ".nuxt", "dist"],

View File

@@ -5,7 +5,7 @@
/* 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 interface ReportCreate {

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,9 @@ import dotenv
from mealie.core.settings import app_settings_constructor
from .settings import AppDirectories, AppSettings
from .settings.static import APP_VERSION, DB_VERSION
from .settings.static import APP_VERSION
APP_VERSION
DB_VERSION
CWD = Path(__file__).parent
BASE_DIR = CWD.parent.parent

View File

@@ -1,7 +1,6 @@
from pathlib import Path
APP_VERSION = "v1.0.0b"
DB_VERSION = "v1.0.0b"
APP_VERSION = "v1.0.0beta-2"
CWD = Path(__file__).parent
BASE_DIR = CWD.parent.parent.parent

View File

@@ -13,10 +13,10 @@
"email-conflict-error": "Diese E-Mail-Adresse wird bereits verwendet"
},
"notifications": {
"generic-created": "{name} was created",
"generic-updated": "{name} was updated",
"generic-created-with-url": "{name} has been created, {url}",
"generic-updated-with-url": "{name} has been updated, {url}",
"generic-deleted": "{name} has been created"
"generic-created": "{name} wurde erstellt",
"generic-updated": "{name} wurde aktualisiert",
"generic-created-with-url": "{name} wurde erstellt, {url}",
"generic-updated-with-url": "{name} wurde aktualisiert, {url}",
"generic-deleted": "{name} wurde erstellt"
}
}

View File

@@ -42,7 +42,7 @@
"liter": {
"name": "Liter",
"description": "",
"abbreviation": "l"
"abbreviation": "L"
},
"pound": {
"name": "Pfund",

View File

@@ -1,6 +1,6 @@
from functools import cached_property
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from pydantic import UUID4
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.mixins import HttpRepo
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"])
@@ -39,6 +40,10 @@ class GroupReportsController(BaseUserController):
def get_one(self, item_id: UUID4):
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):
self.mixins.delete_one(item_id) # type: ignore
try:
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

View File

@@ -9,7 +9,6 @@ from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from slugify import slugify
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
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.security import create_recipe_slug_token
from mealie.pkgs import cache
from mealie.repos.all_repositories import get_repositories
from mealie.repos.repository_recipes import RepositoryRecipes
from mealie.routes._base import BaseUserController, controller
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.request_helpers import RecipeZipTokenResponse, UpdateImageResponse
from mealie.schema.response.responses import ErrorResponse
from mealie.schema.server.tasks import ServerTaskNames
from mealie.services import urls
from mealie.services.event_bus_service.event_bus_service import EventBusService
from mealie.services.event_bus_service.message_types import EventTypes
from mealie.services.recipe.recipe_data_service import RecipeDataService
from mealie.services.recipe.recipe_service import RecipeService
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.scraper import create_from_url
from mealie.services.scraper.scraper_strategies import RecipeScraperPackage
from mealie.services.server_tasks.background_executory import BackgroundExecutor
class BaseRecipeController(BaseUserController):
@@ -172,39 +169,11 @@ class RecipeController(BaseRecipeController):
@router.post("/create-url/bulk", status_code=202)
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"""
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:
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"}
return {"reportId": report_id}
@router.post("/test-scrape-url")
def test_parse_recipe_url(self, url: ScrapeRecipeTest):

View File

@@ -11,6 +11,7 @@ class ReportCategory(str, enum.Enum):
backup = "backup"
restore = "restore"
migration = "migration"
bulk_import = "bulk_import"
class ReportSummaryStatus(str, enum.Enum):

View File

@@ -1,3 +1,4 @@
import contextlib
from pathlib import Path
from uuid import UUID
@@ -6,6 +7,7 @@ from pydantic import UUID4
from mealie.core import root_logger
from mealie.repos.all_repositories import AllRepositories
from mealie.schema.recipe import Recipe
from mealie.schema.recipe.recipe_settings import RecipeSettings
from mealie.schema.reports.reports import (
ReportCategory,
ReportCreate,
@@ -25,7 +27,7 @@ class BaseMigrator(BaseService):
key_aliases: list[MigrationAlias]
report_entries: list[ReportEntryCreate]
report_id: int
report_id: UUID4
report: ReportOut
helpers: DatabaseMigrationHelpers
@@ -111,7 +113,19 @@ class BaseMigrator(BaseService):
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:
recipe.settings = default_settings
recipe.user_id = self.user_id
recipe.group_id = self.group_id
@@ -125,7 +139,7 @@ class BaseMigrator(BaseService):
if self.add_migration_tag:
recipe.tags.append(migration_tag)
exception = ""
exception: str | Exception = ""
status = False
try:
recipe = self.db.recipes.create(recipe)
@@ -189,10 +203,8 @@ class BaseMigrator(BaseService):
"""
recipe_dict = self.rewrite_alias(recipe_dict)
try:
with contextlib.suppress(KeyError):
del recipe_dict["id"]
except KeyError:
pass
recipe_dict = cleaner.clean(recipe_dict, url=recipe_dict.get("org_url", None))

View 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
View File

@@ -1188,7 +1188,7 @@ rdflib = ">=5.0.0"
[[package]]
name = "recipe-scrapers"
version = "13.33.0"
version = "14.1.0"
description = "Python package, scraping recipes from all over the internet"
category = "main"
optional = false
@@ -1545,7 +1545,7 @@ pgsql = ["psycopg2-binary"]
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "a84820195816740ce9200a8a9bf1cc8568cc30c1f97264cc2506b1f6289d1883"
content-hash = "45c28207b80dd8ecd82030410c132be32e8f2e46925c92641d4dd1626fec7786"
[metadata.files]
aiofiles = [
@@ -2437,8 +2437,8 @@ rdflib-jsonld = [
{file = "rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b"},
]
recipe-scrapers = [
{file = "recipe_scrapers-13.33.0-py3-none-any.whl", hash = "sha256:c350ee2407167ec62327a1db9e8864f49a51cae06907689f9095885444549293"},
{file = "recipe_scrapers-13.33.0.tar.gz", hash = "sha256:be1742077bca55638392446b55bf7d2e80a9f9a9625285dc30efd02c763461ec"},
{file = "recipe_scrapers-14.1.0-py3-none-any.whl", hash = "sha256:fc4bf1d5bd142e63a81b1b734a874e2fd6686b22287b41a9bc1355c2004bd5f7"},
{file = "recipe_scrapers-14.1.0.tar.gz", hash = "sha256:5c6931dc13cdb458f7ce52c2fae6a63348ee826a9e0f71ba7679a3d3f7a9257b"},
]
requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},

View File

@@ -30,7 +30,7 @@ passlib = "^1.7.4"
lxml = "^4.7.1"
Pillow = "^8.2.0"
apprise = "^0.9.6"
recipe-scrapers = "^13.33.0"
recipe-scrapers = "^14.1.0"
psycopg2-binary = {version = "^2.9.1", optional = true}
gunicorn = "^20.1.0"
emails = "^0.6"