Compare commits

..

52 Commits

Author SHA1 Message Date
Michael Genson
d24a518bac fix: Remove br encoding from scraper (#5115)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2025-02-28 09:41:31 +00:00
renovate[bot]
46b821d832 fix(deps): update dependency fastapi to v0.115.9 (#5122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 23:48:45 +00:00
renovate[bot]
637bb30e13 chore(deps): update dependency ruff to v0.9.8 (#5112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 23:38:37 +00:00
renovate[bot]
b930ebfb20 fix(deps): update dependency openai to v1.65.1 (#5123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 17:28:11 -06:00
github-actions[bot]
5e2c40731c docs(auto): Update image tag, for release v2.7.0 (#5111)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2025-02-27 16:33:35 +00:00
renovate[bot]
54ae810acc fix(deps): update dependency pydantic-settings to v2.8.1 (#5108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 17:23:50 +01:00
Michael Genson
716c85cc3b fix: Bulk Add Recipes to Shopping List (#5054) 2025-02-27 13:58:40 +00:00
Kuchenpirat
3d1b76bcad fix: update recipe time row direction on small screens (#5107) 2025-02-27 07:48:29 -06:00
Hayden
4843a9a74a chore(l10n): New Crowdin updates (#5106) 2025-02-27 08:12:19 +01:00
Hayden
12aec943dc chore(l10n): New Crowdin updates (#5105) 2025-02-26 17:46:57 +00:00
Kuchenpirat
3b0d6050a2 feat: redesign recipe info card (#5026)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-02-26 16:07:12 +00:00
renovate[bot]
3fd3661206 fix(deps): update dependency authlib to v1.5.0 (#5103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 07:54:59 -06:00
Michael Genson
df8dd3fe4a fix: Invalidate Expired Shared Links (#5065) 2025-02-25 13:01:32 +00:00
Chip
a2c6b3f69b docs: Add additional information and tips to Backup & Restore Usage Documentation Page & Shopping List (#4843)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2025-02-25 12:50:44 +00:00
renovate[bot]
c01593e918 fix(deps): update dependency beautifulsoup4 to v4.13.3 (#5090)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 12:59:22 +01:00
Michael Genson
48484e5b1a feat: Better Scraping/More User Agents (#5091) 2025-02-25 10:57:05 +00:00
Hayden
173e8792a6 chore(l10n): New Crowdin updates (#5102) 2025-02-25 11:46:37 +01:00
Hayden
28047d9b58 chore(l10n): New Crowdin updates (#5101) 2025-02-25 09:51:24 +01:00
Hayden
82393b0cd1 chore(l10n): New Crowdin updates (#5100) 2025-02-24 21:13:21 +00:00
Hayden
eea9a6ae16 chore(l10n): New Crowdin updates (#5099) 2025-02-24 17:08:51 +01:00
Hayden
ca05c25b61 chore(l10n): New Crowdin updates (#5098) 2025-02-24 14:42:03 +00:00
github-actions[bot]
af912ebefb chore(auto): Update pre-commit hooks (#5069)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2025-02-24 14:07:49 +00:00
renovate[bot]
e6b46b21d9 chore(deps): update dependency ruff to v0.9.7 (#5079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 14:58:10 +01:00
miah
a41f8b31f1 feat: Improve Shopping List UI (#4608)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2025-02-24 08:41:04 +00:00
Michael Genson
6271b33b1b fix: Only run migration data fixes on migrations (#5038) 2025-02-24 08:29:12 +00:00
renovate[bot]
09234e3bf0 fix(deps): update dependency openai to v1.64.0 (#5092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 08:06:07 +00:00
Hayden
9f467b702e chore(l10n): New Crowdin updates (#5093) 2025-02-24 08:55:36 +01:00
renovate[bot]
6c156e0e14 fix(deps): update dependency pydantic-settings to v2.8.0 (#5086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 10:52:20 -06:00
renovate[bot]
10818ab0ba fix(deps): update dependency recipe-scrapers to v15.5.1 (#5089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 16:22:27 +00:00
renovate[bot]
0778919134 fix(deps): update dependency recipe-scrapers to v15.5.0 (#5087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 10:11:59 -06:00
Hayden
fb8746e7b8 chore(l10n): New Crowdin updates (#5080) 2025-02-20 20:44:12 +01:00
renovate[bot]
c82d08c0d9 chore(deps): update dependency mkdocs-material to v9.6.5 (#5078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 21:18:47 -06:00
Hayden
be1dc69be6 chore(l10n): New Crowdin updates (#5073) 2025-02-19 22:35:43 +00:00
renovate[bot]
8ea932ef7c chore(deps): update dependency mkdocs-material to v9.6.4 (#5051)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 10:27:52 +00:00
renovate[bot]
70a6bc4769 fix(deps): update dependency lxml to v5.3.1 (#5048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 10:17:29 +00:00
renovate[bot]
c765401ac5 fix(deps): update dependency openai to v1.63.2 (#5070)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 21:06:59 +11:00
Michael Genson
3b12a62fc6 fix(deps): update dependency openai to v1.63.0 (#5067) 2025-02-16 21:24:59 +01:00
renovate[bot]
c351cf7bd5 chore(deps): update dependency coverage to v7.6.12 (#5042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-16 13:45:03 -06:00
RMI78
aea5eb3419 feat: support _FILE suffix for docker secrets (again) (#4958)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-02-16 10:59:50 -06:00
Michael Genson
cb9008bb5c fix: shorten indexes to fix issues with index limits (#5045) 2025-02-11 15:54:32 +00:00
renovate[bot]
3534e445d8 chore(deps): update dependency ruff to v0.9.6 (#5049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 09:44:49 -06:00
Michael Chisholm
c0ab7673ba dev: Create a Python package, build Docker images from it (#4551)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2025-02-11 09:28:40 -06:00
github-actions[bot]
abf73e08ec chore(auto): Update pre-commit hooks (#5047)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-02-10 13:29:35 +00:00
Hayden
c4df8f0611 chore(l10n): New Crowdin updates (#5046) 2025-02-10 11:06:55 +02:00
renovate[bot]
b28eefab77 fix(deps): update dependency sqlalchemy to v2.0.38 (#5030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 08:11:00 -06:00
renovate[bot]
6f3a139efd chore(deps): update dependency ruff to v0.9.5 (#5029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 11:17:13 +00:00
renovate[bot]
790f4a9b9a chore(deps): update dependency mkdocs-material to v9.6.3 (#5031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 22:07:12 +11:00
Michael Genson
c7c87068bf chore: Remove Warnings (#5039) 2025-02-07 23:42:43 +01:00
renovate[bot]
f48dafd855 fix(deps): update dependency pydantic to v2.10.6 (#4940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-07 13:51:27 -06:00
Hayden
273f628acd chore(l10n): New Crowdin updates (#5032) 2025-02-07 14:58:28 +01:00
github-actions[bot]
a8653ea904 docs(auto): Update image tag, for release v2.6.0 (#5022)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2025-02-06 08:38:46 +00:00
renovate[bot]
0093627adb fix(deps): update dependency beautifulsoup4 to v4.13.3 (#5009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 09:29:10 +01:00
121 changed files with 2090 additions and 1009 deletions

View File

@@ -6,7 +6,7 @@
.idea
.vscode
__pycache__/
**/__pycache__/
*.py[cod]
*$py.class
*.so
@@ -25,9 +25,11 @@ venv
*/node_modules
*/dist
/dist/
*/data/db
*/mealie/test
*/mealie/.temp
/mealie/frontend/
model.crfmodel

View File

@@ -3,8 +3,15 @@ on:
workflow_call:
jobs:
build-package:
name: "Build Python package"
uses: ./.github/workflows/partial-package.yml
with:
tag: e2e
test:
timeout-minutes: 60
needs: build-package
runs-on: ubuntu-latest
defaults:
run:
@@ -18,11 +25,18 @@ jobs:
cache-dependency-path: ./tests/e2e/yarn.lock
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Retrieve Python package
uses: actions/download-artifact@v4
with:
name: backend-dist
path: dist
- name: Build Image
uses: docker/build-push-action@v5
with:
file: ./docker/Dockerfile
context: .
build-contexts: |
packages=dist
push: false
load: true
tags: mealie:e2e

View File

@@ -21,7 +21,7 @@ jobs:
uses: ./.github/workflows/partial-backend.yml
frontend-tests:
name: "Frontend and End-to-End Tests"
name: "Frontend Tests"
uses: ./.github/workflows/partial-frontend.yml
build-release:

View File

@@ -1,4 +1,4 @@
name: Backend Test/Lint
name: Backend Lint and Test
on:
workflow_call:

View File

@@ -16,7 +16,14 @@ on:
required: true
jobs:
build-package:
name: "Build Python package"
uses: ./.github/workflows/partial-package.yml
with:
tag: ${{ inputs.tag }}
publish:
needs: build-package
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -35,18 +42,22 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Override __init__.py
run: |
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
- uses: depot/setup-action@v1
- name: Retrieve Python package
uses: actions/download-artifact@v4
with:
name: backend-dist
path: dist
- name: Build and push Docker image, via Depot.dev
uses: depot/build-push-action@v1
with:
project: srzjb6mhzm
file: ./docker/Dockerfile
context: .
build-contexts: |
packages=dist
platforms: linux/amd64,linux/arm64
push: true
tags: |

View File

@@ -1,4 +1,4 @@
name: Frontend Build/Lin
name: Frontend Lint and Test
on:
workflow_call:
@@ -41,37 +41,3 @@ jobs:
- name: Run tests 🧪
run: yarn test:ci
working-directory: "frontend"
build:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
- name: Setup node env 🏗
uses: actions/setup-node@v4.0.0
with:
node-version: 16
check-latest: true
- name: Get yarn cache directory path 🛠
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache node_modules 📦
uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies 👨🏻‍💻
run: yarn
working-directory: "frontend"
- name: Run Build 🚚
run: yarn build
working-directory: "frontend"

102
.github/workflows/partial-package.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: Package build
on:
workflow_call:
inputs:
tag:
required: true
type: string
jobs:
build-frontend:
name: Build frontend
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
- name: Setup node env 🏗
uses: actions/setup-node@v4.0.0
with:
node-version: 16
check-latest: true
- name: Get yarn cache directory path 🛠
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache node_modules 📦
uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies 👨🏻‍💻
run: yarn
working-directory: "frontend"
- name: Run Build 🚚
run: yarn generate
working-directory: "frontend"
- name: Archive built frontend
uses: actions/upload-artifact@v4
with:
name: frontend-dist
path: frontend/dist
retention-days: 5
build-package:
name: Build Python package
needs: build-frontend
runs-on: ubuntu-latest
steps:
- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check out repository
uses: actions/checkout@v4
- name: Set up python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
plugins: |
poetry-plugin-export
- name: Retrieve built frontend
uses: actions/download-artifact@v4
with:
name: frontend-dist
path: mealie/frontend
- name: Override __init__.py
run: |
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
- name: Build package and requirements.txt
env:
SKIP_PACKAGE_DEPS: true
run: |
task py:package
- name: Archive built package
uses: actions/upload-artifact@v4
with:
name: backend-dist
path: dist
retention-days: 5

View File

@@ -19,7 +19,7 @@ jobs:
uses: ./.github/workflows/partial-backend.yml
frontend-tests:
name: "Frontend and End-to-End Tests"
name: "Frontend Tests"
uses: ./.github/workflows/partial-frontend.yml
container-scanning:

View File

@@ -10,7 +10,7 @@ jobs:
uses: ./.github/workflows/partial-backend.yml
frontend-tests:
name: "Frontend and End-to-End Tests"
name: "Frontend Tests"
uses: ./.github/workflows/partial-frontend.yml
build-release:

5
.gitignore vendored
View File

@@ -52,7 +52,7 @@ pnpm-debug.log*
env/
build/
develop-eggs/
/dist/
downloads/
eggs/
.eggs/
@@ -66,6 +66,9 @@ wheels/
.installed.cfg
*.egg
# frontend copied into Python module for packaging purposes
/mealie/frontend/
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.

View File

@@ -12,7 +12,7 @@ repos:
exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.4
rev: v0.9.7
hooks:
- id: ruff
- id: ruff-format

View File

@@ -60,5 +60,9 @@
},
"[vue]": {
"editor.formatOnSave": false
},
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
}
}

View File

@@ -41,14 +41,25 @@ tasks:
setup:ui:
desc: setup frontend dependencies
dir: frontend
run: once
cmds:
- yarn install
sources:
- package.json
- yarn.lock
generates:
- node_modules/**
setup:py:
desc: setup python dependencies
run: once
cmds:
- poetry install --with main,dev,postgres
- poetry run pre-commit install
sources:
- poetry.lock
- pyproject.toml
- .pre-commit-config.yaml
setup:model:
desc: setup nlp model
@@ -131,6 +142,63 @@ tasks:
- poetry run coverage html
- open htmlcov/index.html
py:package:copy-frontend:
desc: copy the frontend files into the Python package
internal: true
deps:
- ui:generate
cmds:
- rm -rf mealie/frontend
- cp -a frontend/dist mealie/frontend
sources:
- frontend/dist/**
generates:
- mealie/frontend/**
py:package:generate-requirements:
desc: Generate requirements file to pin all packages, effectively a "pip freeze" before installation begins
internal: true
cmds:
- poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt
# Include mealie in the requirements, hashing the package that was just built to ensure it's the one installed
- echo "mealie[pgsql]=={{.MEALIE_VERSION}} \\" >> dist/requirements.txt
- poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
- echo " \\" >> dist/requirements.txt
- poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}.tar.gz | tail -n1 >> dist/requirements.txt
vars:
MEALIE_VERSION:
sh: poetry version --short
sources:
- poetry.lock
- pyproject.toml
- dist/mealie-*.whl
- dist/mealie-*.tar.gz
generates:
- dist/requirements.txt
py:package:deps-parallel:
desc: Run py:package dependencies in parallel
internal: true
deps:
- setup:py
- py:package:copy-frontend
py:package:deps:
desc: Dependencies of py:package, skippable by setting SKIP_PACKAGE_DEPS=true
internal: true
cmds:
- task: py:package:deps-parallel
status:
- '{{ .SKIP_PACKAGE_DEPS | default "false"}}'
py:package:
desc: builds Python packages (sdist and wheel) in top-level dist directory
deps:
- py:package:deps
cmds:
- poetry build -n --output=dist
- task: py:package:generate-requirements
py:
desc: runs the backend server
cmds:
@@ -160,6 +228,14 @@ tasks:
cmds:
- yarn build
ui:generate:
desc: generates a static version of the frontend in frontend/dist
dir: frontend
deps:
- setup:ui
cmds:
- yarn generate
ui:lint:
desc: runs the frontend linter
dir: frontend
@@ -184,6 +260,16 @@ tasks:
cmds:
- yarn run dev
docker:build-from-package:
desc: Builds the Docker image from the existing Python package in dist/
deps:
- py:package
cmds:
- docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT={{.GIT_COMMIT}} --build-context packages=dist .
vars:
GIT_COMMIT:
sh: git rev-parse HEAD
docker:prod:
desc: builds and runs the production docker image locally
dir: docker

View File

@@ -1,8 +1,11 @@
FROM node:16 as builder
###############################################
# Frontend Build
###############################################
FROM node:16 AS frontend-builder
WORKDIR /app
WORKDIR /frontend
COPY ./frontend .
COPY frontend .
RUN yarn install \
--prefer-offline \
@@ -26,14 +29,10 @@ ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_HOME="/opt/poetry" \
POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv"
VENV_PATH="/opt/mealie"
# prepend poetry and venv to path
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
# prepend venv to path
ENV PATH="$VENV_PATH/bin:$PATH"
# create user account
RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \
@@ -41,31 +40,81 @@ RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \
&& mkdir $MEALIE_HOME
###############################################
# Builder Image
# Backend Package Build
###############################################
FROM python-base as builder-base
FROM python-base AS backend-builder
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV POETRY_HOME="/opt/poetry" \
POETRY_NO_INTERACTION=1
# prepend poetry to path
ENV PATH="$POETRY_HOME/bin:$PATH"
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
ENV POETRY_VERSION=2.0.1
RUN curl -sSL https://install.python-poetry.org | python3 -
# install poetry plugins needed to build the package
RUN poetry self add "poetry-plugin-export>=1.9"
WORKDIR /mealie
# copy project files here to ensure they will be cached.
COPY poetry.lock pyproject.toml ./
COPY mealie ./mealie
# Copy frontend to package it into the wheel
COPY --from=frontend-builder /frontend/dist ./mealie/frontend
# Build the source and binary package
RUN poetry build --output=dist
# Create the requirements file, which is used to install the built package and
# its pinned dependencies later. mealie is included to ensure the built one is
# what's installed.
RUN export MEALIE_VERSION=$(poetry version --short) \
&& poetry export --only=main --extras=pgsql --output=dist/requirements.txt \
&& echo "mealie[pgsql]==$MEALIE_VERSION \\" >> dist/requirements.txt \
&& poetry run pip hash dist/mealie-$MEALIE_VERSION-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \
&& echo " \\" >> dist/requirements.txt \
&& poetry run pip hash dist/mealie-$MEALIE_VERSION.tar.gz | tail -n1 >> dist/requirements.txt
###############################################
# Package Container
# Only role is to hold the packages, or be overriden by a --build-context flag.
###############################################
FROM scratch AS packages
COPY --from=backend-builder /mealie/dist /
###############################################
# Python Virtual Environment Build
###############################################
# Install packages required to build the venv, in parallel to building the wheel
FROM python-base AS venv-builder-base
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
build-essential \
libpq-dev \
libwebp-dev \
# LDAP Dependencies
libsasl2-dev libldap2-dev libssl-dev \
gnupg gnupg2 gnupg1 \
&& rm -rf /var/lib/apt/lists/* \
&& pip install -U --no-cache-dir pip
&& rm -rf /var/lib/apt/lists/*
RUN python3 -m venv --upgrade-deps $VENV_PATH
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
ENV POETRY_VERSION=1.3.1
RUN curl -sSL https://install.python-poetry.org | python3 -
# Install the wheel and all dependencies into the venv
FROM venv-builder-base AS venv-builder
# copy project requirement files here to ensure they will be cached.
WORKDIR $PYSETUP_PATH
COPY ./poetry.lock ./pyproject.toml ./
# Copy built package (wheel) and its dependency requirements
COPY --from=packages * /dist/
# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
RUN poetry install -E pgsql --only main
# Install the wheel with exact versions of dependencies into the venv
RUN . $VENV_PATH/bin/activate \
&& pip install --require-hashes -r /dist/requirements.txt --find-links /dist
###############################################
# CRFPP Image
@@ -96,39 +145,25 @@ RUN apt-get update \
# create directory used for Docker Secrets
RUN mkdir -p /run/secrets
# copying poetry and venv into image
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
# copy CRF++ and add it to the library path
ENV LD_LIBRARY_PATH=/usr/local/lib
COPY --from=crfpp /usr/local/lib/ /usr/local/lib
COPY --from=crfpp /usr/local/bin/crf_learn /usr/local/bin/crf_learn
COPY --from=crfpp /usr/local/bin/crf_test /usr/local/bin/crf_test
# copy backend
COPY ./mealie $MEALIE_HOME/mealie
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
# Copy venv into image. It contains a fully-installed mealie backend and frontend.
COPY --from=venv-builder $VENV_PATH $VENV_PATH
# venv already has runtime deps installed we get a quicker install
WORKDIR $MEALIE_HOME
RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --only main
WORKDIR /
# Grab CRF++ Model Release
RUN python $MEALIE_HOME/mealie/scripts/install_model.py
RUN python -m mealie.scripts.install_model
VOLUME [ "$MEALIE_HOME/data/" ]
ENV APP_PORT=9000
EXPOSE ${APP_PORT}
HEALTHCHECK CMD python $MEALIE_HOME/mealie/scripts/healthcheck.py || exit 1
# ----------------------------------
# Copy Frontend
ENV STATIC_FILES=/spa/static
COPY --from=builder /app/dist ${STATIC_FILES}
HEALTHCHECK CMD python -m mealie.scripts.healthcheck || exit 1
ENV HOST 0.0.0.0

View File

@@ -32,13 +32,51 @@ init() {
cd /app
# Activate our virtual environment here
. /opt/pysetup/.venv/bin/activate
. /opt/mealie/bin/activate
}
load_secrets() {
# Each of these environment variables will support a `_FILE` suffix that allows
# for setting the environment variable through the Docker Compose secret
# pattern.
local -a secret_supported_vars=(
"POSTGRES_USER"
"POSTGRES_PASSWORD"
"POSTGRES_SERVER"
"POSTGRES_PORT"
"POSTGRES_DB"
"POSTGRES_URL_OVERRIDE"
"SMTP_HOST"
"SMTP_PORT"
"SMTP_USER"
"SMTP_PASSWORD"
"LDAP_SERVER_URL"
"LDAP_QUERY_PASSWORD"
"OIDC_CONFIGURATION_URL"
"OIDC_CLIENT_ID"
"OIDC_CLIENT_SECRET"
"OPENAI_BASE_URL"
"OPENAI_API_KEY"
)
# If any secrets are set, prefer them over base environment variables.
for var in "${secret_supported_vars[@]}"; do
file_var="${var}_FILE"
if [ -n "${!file_var}" ]; then
export "$var=$(<"${!file_var}")"
fi
done
}
change_user
init
load_secrets
# Start API
HOST_IP=`/sbin/ip route|awk '/default/ { print $3 }'`
exec python /app/mealie/main.py
exec mealie

View File

@@ -0,0 +1,40 @@
# Building Packages
Released packages are [built and published via GitHub actions](maintainers.md#drafting-releases).
## Python packages
To build Python packages locally for testing, use [`task`](starting-dev-server.md#without-dev-containers). After installing `task`, run `task py:package` to perform all the steps needed to build the package and a requirements file. To do it manually, run:
```sh
pushd frontend
yarnpkg install
yarnpkg generate
popd
rm -r mealie/frontend
cp -a frontend/dist mealie/frontend
poetry build
poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt
MEALIE_VERSION=$(poetry version --short)
echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt
poetry run pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
echo " \\" >> dist/requirements.txt
poetry run pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt
```
The Python package can be installed with all of its dependencies pinned to the versions tested by the developers with:
```sh
pip3 install -r dist/requirements.txt --find-links dist
```
To install with the latest but still compatible dependency versions, instead run `pip3 install dist/mealie-$VERSION-py3-none-any.whl` (where `$VERSION` is the version of mealie to install).
## Docker image
One way to build the Docker image is to run the following command in the project root directory:
```sh
docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT=$(git rev-parse HEAD) .
```
The Docker image can be built from the pre-built Python packages with the task command `task docker:build-from-package`. This is equivalent to:
```sh
docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT=$(git rev-parse HEAD) --build-context packages=dist .
```

View File

@@ -35,7 +35,7 @@ Mealie has a robust and flexible recipe organization system with a few different
#### Categories
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**.
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, **Side**, or **Drinks**.
[Categories Demo](https://demo.mealie.io/g/home/recipes/categories){ .md-button .md-button--primary }
@@ -84,7 +84,30 @@ The meal planner has the concept of plan rules. These offer a flexible way to us
The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list or link a recipe and all of it's ingredients to track meals during the week.
Managing shopping lists can be done from the Sidebar > Shopping Lists.
Here you will be able to:
- See items already on the Shopping List
- See linked recipes with ingredients
- Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it.
- Check off an item
- Add / Change / Remove / Sort Items via the grid icon
- Be sure if you are modifying an ingredient to click the 'Save' icon.
- Add / Change / Remove / Sort Labels
- 'No Label' will always be on the top, others can be Reordered via the 'Reorder Labels' button
!!! tip
If you accidentally checked off an item, you can uncheck it by expanding 'items checked' and unchecking it. This will add it back to the Shopping List.
!!! tip
You can use Labels to categorize your ingredients. You may want to Label by Food Type (Frozen, Fresh, etc), by Store, Tool, Recipe, or more. Play around with this to see what works best for you.
!!! tip
You can toggle 'Food' on items so that if you add multiple of the same food / ingredient, Mealie will automatically combine them together. Do this by editing an item in the Shopping List and clicking the 'Apple' icon. If you then have recipes that contain "1 | cup | cheese" and "2 | cup | cheese" this would be combined to show "3 cups of cheese."
[See FAQ for more information](../getting-started/faq.md)
[Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
## Integrations
@@ -94,9 +117,9 @@ Mealie is designed to integrate with many different external services. There are
### Notifiers
Notifiers are event-driven notifications sent when specific actions are performed within Mealie. Some actions include:
- creating a recipe
- adding items to a shopping list
- creating a new mealplan
- Creating / Updating a recipe
- Adding items to a shopping list
- Creating a new mealplan
Notifiers use the [Apprise library](https://github.com/caronc/apprise/wiki), which integrates with a large number of notification services. In addition, certain custom notifiers send basic event data to the consumer (e.g. the `id` of the resource). These include:

View File

@@ -31,27 +31,27 @@
### Database
| Variables | Default | Description |
| --------------------- | :------: | ----------------------------------------------------------------------- |
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
| POSTGRES_USER | mealie | Postgres database user |
| POSTGRES_PASSWORD | mealie | Postgres database password |
| POSTGRES_SERVER | postgres | Postgres database server address |
| POSTGRES_PORT | 5432 | Postgres database port |
| POSTGRES_DB | mealie | Postgres database name |
| POSTGRES_URL_OVERRIDE | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables |
| Variables | Default | Description |
| ------------------------------------------------------- | :------: | ----------------------------------------------------------------------- |
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
| POSTGRES_USER<super>[&dagger;][secrets]</super> | mealie | Postgres database user |
| POSTGRES_PASSWORD<super>[&dagger;][secrets]</super> | mealie | Postgres database password |
| POSTGRES_SERVER<super>[&dagger;][secrets]</super> | postgres | Postgres database server address |
| POSTGRES_PORT<super>[&dagger;][secrets]</super> | 5432 | Postgres database port |
| POSTGRES_DB<super>[&dagger;][secrets]</super> | mealie | Postgres database name |
| POSTGRES_URL_OVERRIDE<super>[&dagger;][secrets]</super> | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables |
### Email
| Variables | Default | Description |
| ------------------ | :-----: | ------------------------------------------------- |
| SMTP_HOST | None | Required For email |
| SMTP_PORT | 587 | Required For email |
| SMTP_FROM_NAME | Mealie | Required For email |
| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' |
| SMTP_FROM_EMAIL | None | Required For email |
| SMTP_USER | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
| SMTP_PASSWORD | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
| Variables | Default | Description |
| ----------------------------------------------- | :-----: | ------------------------------------------------- |
| SMTP_HOST<super>[&dagger;][secrets]</super> | None | Required For email |
| SMTP_PORT<super>[&dagger;][secrets]</super> | 587 | Required For email |
| SMTP_FROM_NAME | Mealie | Required For email |
| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' |
| SMTP_FROM_EMAIL | None | Required For email |
| SMTP_USER<super>[&dagger;][secrets]</super> | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
| SMTP_PASSWORD<super>[&dagger;][secrets]</super> | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
### Webworker
@@ -72,21 +72,21 @@ Use this only when mealie is run without a webserver or reverse proxy.
### LDAP
| Variables | Default | Description |
| -------------------- | :-----: | ----------------------------------------------------------------------------------------------------------------------------------- |
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) |
| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP |
| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
| LDAP_ENABLE_STARTTLS | False | Optional. Use STARTTLS to connect to the server |
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
| LDAP_QUERY_BIND | None | Optional bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`). If `None` then anonymous bind will be used |
| LDAP_QUERY_PASSWORD | None | Optional password for the bind user used in LDAP_QUERY_BIND |
| LDAP_USER_FILTER | None | Optional LDAP filter to narrow down eligible users (e.g. `(memberOf=cn=mealie_user,dc=example,dc=com)`) |
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id |
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email |
| Variables | Default | Description |
| ----------------------------------------------------- | :-----: | ----------------------------------------------------------------------------------------------------------------------------------- |
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
| LDAP_SERVER_URL<super>[&dagger;][secrets]</super> | None | LDAP server URL (e.g. ldap://ldap.example.com) |
| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP |
| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
| LDAP_ENABLE_STARTTLS | False | Optional. Use STARTTLS to connect to the server |
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
| LDAP_QUERY_BIND | None | Optional bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`). If `None` then anonymous bind will be used |
| LDAP_QUERY_PASSWORD<super>[&dagger;][secrets]</super> | None | Optional password for the bind user used in LDAP_QUERY_BIND |
| LDAP_USER_FILTER | None | Optional LDAP filter to narrow down eligible users (e.g. `(memberOf=cn=mealie_user,dc=example,dc=com)`) |
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id |
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email |
### OpenID Connect (OIDC)
@@ -94,23 +94,22 @@ Use this only when mealie is run without a webserver or reverse proxy.
For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
| Variables | Default | Description |
|---------------------------------------------------|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
| OIDC_CLIENT_SECRET <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider |
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) |
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) |
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed and you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
| OIDC_NAME_CLAIM | name | This is the claim which Mealie will use for the users Full Name |
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. |
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
| Variables | Default | Description |
| ----------------------------------------------------------------------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
| OIDC_CONFIGURATION_URL<super>[&dagger;][secrets]</super> | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
| OIDC_CLIENT_ID<super>[&dagger;][secrets]</super> | None | The client id of your configured client in your provider |
| OIDC_CLIENT_SECRET<super>[&dagger;][secrets]</super> <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider |
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate, regardless of the `OIDC_ADMIN_GROUP`. For more information see [this page](../authentication/oidc.md#groups) |
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be made an admin. For more information see [this page](../authentication/oidc.md#groups) |
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed an you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
| OIDC_SIGNING_ALGORITHM | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
### OpenAI
@@ -119,17 +118,13 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md).
For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`)
| Variables | Default | Description |
| ---------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OPENAI_BASE_URL | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
| OPENAI_API_KEY | None | Your OpenAI API Key. Enables OpenAI-related features |
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
| Variables | Default | Description |
| ------------------------------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------- |
| OPENAI_BASE_URL<super>[&dagger;][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
| OPENAI_API_KEY<super>[&dagger;][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
### Theming
@@ -154,24 +149,80 @@ Setting the following environmental variables will change the theme of the front
### Docker Secrets
Setting a credential can be done using secrets when running in a Docker container.
This can be used to avoid leaking passwords through compose files, environment variables, or command-line history.
For example, to configure the Postgres database password in Docker compose, create a file on the host that contains only the password, and expose that file to the Mealie service as a secret with the correct name.
Note that environment variables take priority over secrets, so any previously defined environment variables should be removed when migrating to secrets.
### Docker Secrets
> <super>&dagger;</super> Starting in version `2.4.2`, any environment variable in the preceding lists with a dagger
> symbol next to them support the Docker Compose secrets pattern, below.
[Docker Compose secrets][docker-secrets] can be used to secure sensitive information regarding the Mealie implementation
by managing control of each secret independently from the single `.env` file. This is helpful for users that may need
different levels of access for various, sensitive environment variables, such as differentiating between hardening
operations (e.g., server endpoints and ports) and user access control (e.g., usernames, passwords, and API keys).
To convert any of these environment variables to a Docker Compose secret, append `_FILE` to the environment variable and
connect it with a Docker Compose secret, per the [Docker documentation][docker-secrets].
If both the base environment variable and the secret pattern of the environment variable are set, the secret will always
take precedence.
For example, a user that wishes to harden their operations by only giving some access to their database URL, but who
wish to place additional security around their user access control, may have a Docker Compose configuration similar to:
```yaml
services:
mealie:
...
environment:
...
POSTGRES_USER: postgres
secrets:
- POSTGRES_PASSWORD
# These secrets will be loaded by Docker into the `/run/secrets` folder within the container.
- postgres-host
- postgres-port
- postgres-db-name
- postgres-user
- postgres-password
environment:
DB_ENGINE: postgres
POSTGRES_SERVER: duplicate.entry.tld # This will be ignored, due to the secret defined, below.
POSTGRES_SERVER_FILE: /run/secrets/postgres-host
POSTGRES_PORT_FILE: /run/secrets/postgres-port
POSTGRES_DB_FILE: /run/secrets/postgres-db-name
POSTGRES_USER_FILE: /run/secrets/postgres-user
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
# Each of these secrets are loaded via these local files. Different patterns are available. See the Docker Compose
# documentation for more information.
secrets:
POSTGRES_PASSWORD:
file: postgrespassword.txt
postgres-host:
file: ./secrets/postgres-host.txt
postgres-port:
file: ./secrets/postgres-port.txt
postgres-db-name:
file: ./secrets/sensitive/postgres-db-name.txt
postgres-user:
file: ./secrets/sensitive/postgres-user.txt
postgres-password:
file: ./secrets/sensitive/postgres-password.txt
```
In the example above, a directory organization and access pattern may look like the following:
```text
.
├── docker-compose.yml
└── secrets # Access restricted to anyone that can manage secrets
├── postgres-host.txt
├── postgres-port.txt
└── sensitive # Access further-restricted to anyone managing service accounts
├── postgres-db-name.txt
├── postgres-password.txt
└── postgres-user.txt
```
How you organize your secrets is ultimately up to you. At minimum, it's highly recommended to use secret patterns for
at least these sensitive environment variables when working within shared environments:
- `POSTGRES_PASSWORD`
- `SMTP_PASSWORD`
- `LDAP_QUERY_PASSWORD`
- `OPENAI_API_KEY`
[docker-secrets]: https://docs.docker.com/compose/use-secrets/
[secrets]: #docker-secrets
[unicorn_workers]: https://www.uvicorn.org/deployment/#built-in

View File

@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.5.0`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.7.0`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container

View File

@@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v2.5.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v2.7.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v2.5.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v2.7.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -1,17 +1,24 @@
# Backups and Restoring
# Backups and Restores
Mealie provides an integrated mechanics for doing full installation backups of the database. Navigate to `/admin/backups` to
Mealie provides an integrated mechanic for doing full installation backups of the database.
Navigate to Settings > Backups or manually by adding `/admin/backups` to your instance URL.
From this page, you will be able to:
- See a list of available backups
- Perform a backups
- Restore a backup
- Create a backup
- Upload a backup
- Delete a backup (Confirmation Required)
- Download a backup
- Perform a restore
!!! tip
If you're using Mealie with SQLite all your data is stored in the /app/data/ folder in the container. You can easily perform entire site backups by stopping the container, and backing up this folder with your chosen tool. This is the **best** way to backup your data.
## Restoring from a Backup
To restore from a backup it needs to be uploaded to your instance, this can be done through the web portal. On the lower left hand corner of the backups data table you'll see an upload button. Click this button and select the backup file you want to upload and it will be available to import shortly.
To restore from a backup it needs to be uploaded to your instance which can be done through the web portal. On the top left of the page you'll see an upload button. Click this button and select the backup file you want to upload and it will be available to import shortly. You can alternatively use one of the backups you see on the screen, if one exists.
Before importing it's critical that you understand the following:
@@ -19,6 +26,9 @@ Before importing it's critical that you understand the following:
- This action cannot be undone
- If this action is successful you will be logged out and you will need to log back in to complete the restore
!!! tip
If for some reason the restore does not succeed, you can review the logs of what the issue may be, download the backup .ZIP and edit the contents of database.json to potentially resolve the issue. For example, if you receive an error restoring 'shopping-list' you can edit out the contents of that list while allowing other sections to restore. If you would like any assistance on this, reach out over Discord.
!!! warning
Prior to beta-v5 using a mis-matched version of the database backup will result in an error that will prevent you from using the instance of Mealie requiring you to remove all data and reinstall. Post beta-v5 performing a mismatched restore will throw an error and alert the user of the issue.

View File

@@ -99,6 +99,7 @@ nav:
- Non-Code: "contributors/non-coders.md"
- Translating: "contributors/translating.md"
- Developers Guide:
- Building Packages: "contributors/developers-guide/building-packages.md"
- Code Contributions: "contributors/developers-guide/code-contributions.md"
- Dev Getting Started: "contributors/developers-guide/starting-dev-server.md"
- Database Changes: "contributors/developers-guide/database-changes.md"

View File

@@ -138,8 +138,8 @@ import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
import { ShoppingListSummary } from "~/lib/api/types/household";
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
import { RecipeIngredient, ShoppingListAddRecipeParamsBulk, ShoppingListSummary } from "~/lib/api/types/household";
import { Recipe } from "~/lib/api/types/recipe";
export interface RecipeWithScale extends Recipe {
scale: number;
@@ -342,12 +342,12 @@ export default defineComponent({
}
async function addRecipesToList() {
const promises: Promise<any>[] = [];
recipeIngredientSections.value.forEach((section) => {
if (!selectedShoppingList.value) {
return;
}
if (!selectedShoppingList.value) {
return;
}
const recipeData: ShoppingListAddRecipeParamsBulk[] = [];
recipeIngredientSections.value.forEach((section) => {
const ingredients: RecipeIngredient[] = [];
section.ingredientSections.forEach((ingSection) => {
ingSection.ingredients.forEach((ing) => {
@@ -361,24 +361,18 @@ export default defineComponent({
return;
}
promises.push(api.shopping.lists.addRecipe(
selectedShoppingList.value.id,
section.recipeId,
section.recipeScale,
ingredients,
));
recipeData.push(
{
recipeId: section.recipeId,
recipeIncrementQuantity: section.recipeScale,
recipeIngredients: ingredients,
}
);
});
let success = true;
const results = await Promise.allSettled(promises);
results.forEach((result) => {
if (result.status === "rejected") {
success = false;
}
})
success ? alert.success(i18n.tc("recipe.successfully-added-to-list"))
: alert.error(i18n.tc("failed-to-add-recipes-to-list"))
const { error } = await api.shopping.lists.addRecipes(selectedShoppingList.value.id, recipeData);
error ? alert.error(i18n.tc("recipe.failed-to-add-recipes-to-list"))
: alert.success(i18n.tc("recipe.successfully-added-to-list"));
state.shoppingListDialog = false;
state.shoppingListIngredientDialog = false;

View File

@@ -86,29 +86,27 @@
</BaseDialog>
</div>
<div>
<div class="d-flex justify-center flex-wrap">
<v-chip
label
:small="$vuetify.breakpoint.smAndDown"
color="accent custom-transparent"
class="ma-1 pa-3"
>
<v-icon left>
{{ $globals.icons.calendar }}
</v-icon>
<div v-if="lastMadeReady">
{{ $t('recipe.last-made-date', { date: lastMade ? new Date(lastMade).toLocaleDateString($i18n.locale) : $t("general.never") } ) }}
</div>
<div v-else>
<AppLoader tiny />
</div>
</v-chip>
</div>
<div class="d-flex justify-center flex-wrap mt-1">
<BaseButton :small="$vuetify.breakpoint.smAndDown" @click="madeThisDialog = true">
<template #icon> {{ $globals.icons.chefHat }} </template>
{{ $t('recipe.made-this') }}
</BaseButton>
<div v-if="lastMadeReady" class="d-flex justify-center flex-wrap">
<v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger;">
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-btn
rounded
outlined
x-large
color="primary"
v-bind="attrs"
v-on="on"
@click="madeThisDialog = true"
>
<v-icon left large>{{ $globals.icons.calendar }}</v-icon>
<span class="text--secondary" style="letter-spacing: normal;"><b>{{ $tc("general.last-made") }}</b><br>{{ lastMade ? new Date(lastMade).toLocaleDateString($i18n.locale) : $tc("general.never") }}</span>
<v-icon right large>{{ $globals.icons.createAlt }}</v-icon>
</v-btn>
</template>
<span>{{ $tc("recipe.made-this") }}</span>
</v-tooltip>
</v-row>
</div>
</div>
</div>

View File

@@ -14,15 +14,16 @@
</v-card-title>
<v-divider class="my-2" />
<SafeMarkdown :source="recipe.description" />
<v-divider />
<v-container class="d-flex flex-row flex-wrap justify-center align-center">
<div class="mx-5">
<v-row no-gutters class="mb-1">
<v-divider v-if="recipe.description" />
<v-container class="d-flex flex-row flex-wrap justify-center">
<div class="mx-6">
<v-row no-gutters>
<v-col v-if="recipe.recipeYieldQuantity || recipe.recipeYield" cols="12" class="d-flex flex-wrap justify-center">
<RecipeYield
:yield-quantity="recipe.recipeYieldQuantity"
:yield="recipe.recipeYield"
:scale="recipeScale"
class="mb-4"
/>
</v-col>
</v-row>
@@ -31,18 +32,18 @@
<RecipeLastMade
v-if="isOwnGroup"
:recipe="recipe"
:class="true ? undefined : 'force-bottom'"
class="mb-4"
/>
</v-col>
</v-row>
</div>
<div class="mx-5">
<div class="mx-6">
<RecipeTimeCard
stacked
container-class="d-flex flex-wrap justify-center"
:prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
class="mb-4"
/>
</div>
</v-container>

View File

@@ -30,12 +30,17 @@
<span v-html="recipeYield"></span>
</v-chip>
</div>
<RecipeTimeCard
:prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
color="white"
/>
<v-row class="d-flex justify-start">
<RecipeTimeCard
:prep-time="recipe.prepTime"
:total-time="recipe.totalTime"
:perform-time="recipe.performTime"
small
color="white"
class="ml-4"
/>
</v-row>
<v-card-text v-if="preferences.showDescription" class="px-0">
<SafeMarkdown :source="recipe.description" />
</v-card-text>

View File

@@ -1,41 +1,37 @@
<template>
<div v-if="stacked">
<v-container>
<v-row v-for="(time, index) in allTimes" :key="`${index}-stacked`" no-gutters>
<v-col cols="12" :class="containerClass">
<v-chip
:small="$vuetify.breakpoint.smAndDown"
label
:color="color"
class="ma-1"
>
<v-icon left>
{{ $globals.icons.clockOutline }}
</v-icon>
{{ time.name }} |
{{ time.value }}
</v-chip>
</v-col>
</v-row>
</v-container>
</div>
<div v-else>
<v-container :class="containerClass">
<v-chip
v-for="(time, index) in allTimes"
:key="index"
:small="$vuetify.breakpoint.smAndDown"
label
:color="color"
class="ma-1"
>
<v-icon left>
<template v-if="showCards">
<div class="text-center">
<!-- Total Time -->
<div v-if="validateTotalTime" class="time-card-flex mx-auto">
<v-row no-gutters class="d-flex flex-no-wrap align-center " :style="fontSize">
<v-icon :x-large="!small" left color="primary">
{{ $globals.icons.clockOutline }}
</v-icon>
{{ time.name }} |
{{ time.value }}
</v-chip>
</v-container>
<p class="my-0"><span class="font-weight-bold">{{ validateTotalTime.name }}</span><br>{{ validateTotalTime.value }}</p>
</v-row>
</div>
<v-divider v-if="validateTotalTime && (validatePrepTime || validatePerformTime)" class="my-2" />
<!-- Prep Time & Perform Time -->
<div v-if="validatePrepTime || validatePerformTime" class="time-card-flex mx-auto">
<v-row
no-gutters
class="d-flex justify-center align-center" :class="{'flex-column': $vuetify.breakpoint.smAndDown}"
style="width: 100%;" :style="fontSize"
>
<div v-if="validatePrepTime" class="d-flex flex-no-wrap my-1">
<v-icon :large="!small" :dense="small" left color="primary">
{{ $globals.icons.knfife }}
</v-icon>
<p class="my-0"><span class="font-weight-bold">{{ validatePrepTime.name }}</span><br>{{ validatePrepTime.value }}</p>
</div>
<v-divider v-if="validatePrepTime && validatePerformTime" vertical class="mx-4" />
<div v-if="validatePerformTime" class="d-flex flex-no-wrap my-1">
<v-icon :large="!small" :dense="small" left color="primary">
{{ $globals.icons.potSteam }}
</v-icon>
<p class="my-0"><span class="font-weight-bold">{{ validatePerformTime.name }}</span><br>{{ validatePerformTime.value }}</p>
</div>
</v-row>
</div>
</div>
</template>
@@ -44,10 +40,6 @@ import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
stacked: {
type: Boolean,
default: false,
},
prepTime: {
type: String,
default: null,
@@ -64,9 +56,9 @@ export default defineComponent({
type: String,
default: "accent custom-transparent"
},
containerClass: {
type: String,
default: undefined,
small: {
type: Boolean,
default: false,
},
},
setup(props) {
@@ -92,13 +84,16 @@ export default defineComponent({
return !isEmpty(props.performTime) ? { name: i18n.t("recipe.perform-time"), value: props.performTime } : null;
});
const allTimes = computed(() => {
return [validateTotalTime.value, validatePrepTime.value, validatePerformTime.value].filter((x) => x !== null);
const fontSize = computed(() => {
return props.small ? { fontSize: "smaller" } : { fontSize: "larger" };
});
return {
showCards,
allTimes,
validateTotalTime,
validatePrepTime,
validatePerformTime,
fontSize,
};
},
});

View File

@@ -1,21 +1,20 @@
<template>
<div v-if="displayText" class="d-flex justify-space-between align-center">
<v-chip
:small="$vuetify.breakpoint.smAndDown"
label
:color="color"
>
<v-icon left>
{{ $globals.icons.potSteam }}
<div v-if="scaledAmount" class="d-flex align-center">
<v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger;">
<v-icon x-large left color="primary">
{{ $globals.icons.bread }}
</v-icon>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="displayText"></span>
</v-chip>
<p class="my-0">
<span class="font-weight-bold">{{ $i18n.tc("recipe.yield") }}</span><br>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="scaledAmount"></span> {{ text }}
</p>
</v-row>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import { defineComponent, computed } from "@nuxtjs/composition-api";
import DOMPurify from "dompurify";
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
@@ -39,7 +38,6 @@ export default defineComponent({
},
},
setup(props) {
const { i18n } = useContext();
function sanitizeHTML(rawHtml: string) {
return DOMPurify.sanitize(rawHtml, {
@@ -48,21 +46,15 @@ export default defineComponent({
});
}
const displayText = computed(() => {
if (!(props.yieldQuantity || props.yield)) {
return "";
}
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
return i18n.t("recipe.yields-amount-with-text", {
amount: scaledAmountDisplay,
text: sanitizeHTML(props.yield),
}) as string;
const scaledAmount = computed(() => {
const {scaledAmountDisplay} = useScaledAmount(props.yieldQuantity, props.scale);
return scaledAmountDisplay;
});
const text = sanitizeHTML(props.yield);
return {
displayText,
scaledAmount,
text,
};
},
});

View File

@@ -34,6 +34,7 @@
:label="$t('shopping-list.note')"
rows="1"
auto-grow
autofocus
@keypress="handleNoteKeyPress"
></v-textarea>
</div>
@@ -80,37 +81,37 @@
<v-spacer />
</div>
</v-card-text>
<v-card-actions class="ma-0 pt-0 pb-1 justify-end">
<BaseButtonGroup
:buttons="[
...(allowDelete ? [{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
}] : []),
{
icon: $globals.icons.close,
text: $t('general.cancel'),
event: 'cancel',
},
{
icon: $globals.icons.foods,
text: $t('shopping-list.toggle-food'),
event: 'toggle-foods',
},
{
icon: $globals.icons.save,
text: $t('general.save'),
event: 'save',
},
]"
@save="$emit('save')"
@cancel="$emit('cancel')"
@delete="$emit('delete')"
@toggle-foods="listItem.isFood = !listItem.isFood"
/>
</v-card-actions>
</v-card>
<v-card-actions class="ma-0 pt-0 pb-1 justify-end">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
},
{
icon: $globals.icons.close,
text: $t('general.cancel'),
event: 'cancel',
},
{
icon: $globals.icons.foods,
text: $t('shopping-list.toggle-food'),
event: 'toggle-foods',
},
{
icon: $globals.icons.save,
text: $t('general.save'),
event: 'save',
},
]"
@save="$emit('save')"
@cancel="$emit('cancel')"
@delete="$emit('delete')"
@toggle-foods="listItem.isFood = !listItem.isFood"
/>
</v-card-actions>
</div>
</template>
@@ -139,6 +140,11 @@ export default defineComponent({
type: Array as () => IngredientFood[],
required: true,
},
allowDelete: {
type: Boolean,
required: false,
default: true,
},
},
setup(props, context) {
const foodStore = useFoodStore();

View File

@@ -579,7 +579,6 @@
"made-this": "Ek het dit gemaak",
"how-did-it-turn-out": "Hoe het dit uitgedraai?",
"user-made-this": "{user} het dit gemaak",
"last-made-date": "Laas gemaak {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Boodskap sleutel",
"parse": "Verwerk",

View File

@@ -550,7 +550,7 @@
"yields-amount-with-text": "Yields {amount} {text}",
"yield-text": "Yield Text",
"quantity": "Quantity",
"choose-unit": "Choose Unit",
"choose-unit": "اختر الوحدة",
"press-enter-to-create": "Press Enter to Create",
"choose-food": "Choose Food",
"notes": "Notes",
@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -579,7 +579,6 @@
"made-this": "Сготвих рецептата",
"how-did-it-turn-out": "Как се получи?",
"user-made-this": "{user} направи това",
"last-made-date": "Последно приготвена на {date}",
"api-extras-description": "Екстрите за рецепти са ключова характеристика на Mealie API. Те Ви позволяват да създавате персонализирани JSON двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.",
"message-key": "Ключ на съобщението",
"parse": "Анализирай",

View File

@@ -579,7 +579,6 @@
"made-this": "Ho he fet",
"how-did-it-turn-out": "Com ha sortit?",
"user-made-this": "{user} ha fet això",
"last-made-date": "Última vegada feta {date}",
"api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.",
"message-key": "Clau del missatge",
"parse": "Analitzar",

View File

@@ -579,7 +579,6 @@
"made-this": "Toto jsem uvařil",
"how-did-it-turn-out": "Jak to dopadlo?",
"user-made-this": "{user} udělal toto",
"last-made-date": "Naposledy uvařeno {date}",
"api-extras-description": "Recepty jsou klíčovým rysem rozhraní pro API Mealie. Umožňují vytvářet vlastní klíče/hodnoty JSON v rámci receptu pro odkazy na aplikace třetích stran. Tyto klíče můžete použít pro poskytnutí informací, například pro aktivaci automatizace nebo vlastních zpráv pro přenos do požadovaného zařízení.",
"message-key": "Klíč zprávy",
"parse": "Analyzovat",

View File

@@ -579,7 +579,6 @@
"made-this": "Jeg har lavet denne",
"how-did-it-turn-out": "Hvordan blev det?",
"user-made-this": "{user} lavede denne",
"last-made-date": "Sidst tilberedt den {date}",
"api-extras-description": "Opskrifter ekstra er en central feature i Mealie API. De giver dig mulighed for at oprette brugerdefinerede JSON nøgle / værdi par inden for en opskrift, at henvise til fra 3. parts applikationer. Du kan bruge disse nøgler til at give oplysninger, for eksempel til at udløse automatiseringer eller brugerdefinerede beskeder til at videresende til din ønskede enhed.",
"message-key": "Beskednøgle",
"parse": "Behandl data",

View File

@@ -579,7 +579,6 @@
"made-this": "Ich hab's gemacht",
"how-did-it-turn-out": "Wie ist es geworden?",
"user-made-this": "{user} hat's gemacht",
"last-made-date": "Zuletzt gemacht {date}",
"api-extras-description": "Rezepte-Extras sind ein Hauptmerkmal der Mealie API. Sie ermöglichen es dir, benutzerdefinierte JSON Key-Value-Paare zu einem Rezept zu erstellen, um Drittanbieter-Anwendungen zu steuern. Du kannst diese dazu verwenden, um Automatisierungen auszulösen oder benutzerdefinierte Nachrichten an bestimmte Geräte zu senden.",
"message-key": "Nachrichten-Schlüssel",
"parse": "Parsen",

View File

@@ -579,7 +579,6 @@
"made-this": "Το έφτιαξα",
"how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;",
"user-made-this": "Ο/η {user} το έφτιαξε αυτό",
"last-made-date": "Τελευταία παρασκευή {date}",
"api-extras-description": "Τα extras συνταγών αποτελούν βασικό χαρακτηριστικό του Mealie API. Σας επιτρέπουν να δημιουργήσετε προσαρμοσμένα ζεύγη κλειδιού/τιμής JSON μέσα σε μια συνταγή, να παραπέμψετε σε εφαρμογές τρίτων. Μπορείτε να χρησιμοποιήσετε αυτά τα κλειδιά για την παροχή πληροφοριών, για παράδειγμα πυροδότηση αυτοματισμών ή μετάδοση προσαρμοσμένων μηνυμάτων στη συσκευή που επιθυμείτε.",
"message-key": "Κλειδί Μηνύματος",
"parse": "Ανάλυση",

View File

@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -579,7 +579,6 @@
"made-this": "Lo hice",
"how-did-it-turn-out": "¿Cómo resultó esto?",
"user-made-this": "{user} hizo esto",
"last-made-date": "Cocinado por última vez el {date}",
"api-extras-description": "Los extras de las recetas son una característica clave de la API de Mealie. Permiten crear pares json clave/valor personalizados dentro de una receta para acceder desde aplicaciones de terceros. Puede utilizar estas claves para almacenar información, para activar la automatización o mensajes personalizados para transmitir al dispositivo deseado.",
"message-key": "Clave de mensaje",
"parse": "Analizar",

View File

@@ -579,7 +579,6 @@
"made-this": "Olen seda valmistanud",
"how-did-it-turn-out": "Kuidas tuli see välja?",
"user-made-this": "{user} on seda valmistanud",
"last-made-date": "Viimati valmistatud: {date}",
"api-extras-description": "Retsepti väljavõtted on Meali API oluline funktsioon. Neid saab kasutada kohandatud JSON-võtme/väärtuse paaride loomiseks retseptis, et viidata kolmandate osapoolte rakendustele. Neid klahve saab kasutada teabe edastamiseks, näiteks automaatse toimingu või kohandatud sõnumi käivitamiseks teie valitud seadmele.",
"message-key": "Sõnumi võti",
"parse": "Analüüsi",

View File

@@ -579,7 +579,6 @@
"made-this": "Tein tämän",
"how-did-it-turn-out": "Miten se onnistui?",
"user-made-this": "{user} teki tämän",
"last-made-date": "Viimeksi valmistettu {date}",
"api-extras-description": "Reseptiekstrat ovat Mealien API:n tärkeä ominaisuus. Niiden avulla voidaan luoda mukautettuja JSON-avain/arvo-pareja reseptin sisällä viitaten kolmannen osapuolen sovelluksiin. Näitä avaimia voi käyttää tiedon antamiseksi, esimerkiksi automaattisen toiminnon tai mukautetun viestin käynnistämiseksi haluamaasi laitteeseen.",
"message-key": "Viestiavain",
"parse": "Jäsennä",

View File

@@ -579,7 +579,6 @@
"made-this": "Je lai cuisiné",
"how-did-it-turn-out": "Cétait bon?",
"user-made-this": "{user} la cuisiné",
"last-made-date": "Cuisiné le {date}",
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.",
"message-key": "Clé de message",
"parse": "Analyser",

View File

@@ -579,7 +579,6 @@
"made-this": "Je lai cuisiné",
"how-did-it-turn-out": "Cétait bon?",
"user-made-this": "{user} la cuisiné",
"last-made-date": "Cuisiné le {date}",
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.",
"message-key": "Clé de message",
"parse": "Analyser",

View File

@@ -579,7 +579,6 @@
"made-this": "Je lai cuisiné",
"how-did-it-turn-out": "Cétait bon?",
"user-made-this": "{user} la cuisiné",
"last-made-date": "Cuisiné le {date}",
"api-extras-description": "Les suppléments des recettes sont une fonctionnalité clé de lAPI Mealie. Ils permettent de créer des paires JSON clé/valeur personnalisées dans une recette, qui peuvent être référencées depuis des applications tierces. Ces clés peuvent être utilisées par exemple pour déclencher des tâches automatisées ou des messages personnalisés à transmettre à lappareil souhaité.",
"message-key": "Clé de message",
"parse": "Analyser",

View File

@@ -182,7 +182,7 @@
"date": "Data",
"id": "Id",
"owner": "Dono",
"change-owner": "Change Owner",
"change-owner": "Mudar Proprietario",
"date-added": "Engadida o",
"none": "Nada",
"run": "Executar",
@@ -214,10 +214,10 @@
"confirm-delete-generic-items": "Estás seguro de que queres eliminar os seguintes elementos?",
"organizers": "Organizadores",
"caution": "Coidado",
"show-advanced": "Show Advanced",
"add-field": "Add Field",
"show-advanced": "Mostrar Avanzadas",
"add-field": "Adicionar Campo",
"date-created": "Date Created",
"date-updated": "Date Updated"
"date-updated": "Data de Atualización"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Estás seguro de que queres eliminar <b>{groupName}<b/>?",
@@ -295,11 +295,11 @@
"private-household-description": "Setting your household to private will disable all public view options. This overrides any individual public view settings",
"lock-recipe-edits-from-other-households": "Lock recipe edits from other households",
"lock-recipe-edits-from-other-households-description": "When enabled only users in your household can edit recipes created by your household",
"household-recipe-preferences": "Household Recipe Preferences",
"household-recipe-preferences": "Preferencias de receitas da casa",
"default-recipe-preferences-description": "These are the default settings when a new recipe is created in your household. These can be changed for individual recipes in the recipe settings menu.",
"allow-users-outside-of-your-household-to-see-your-recipes": "Allow users outside of your household to see your recipes",
"allow-users-outside-of-your-household-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your household or with a pre-generated private link",
"household-preferences": "Household Preferences"
"household-preferences": "Preferencias da Casa"
},
"meal-plan": {
"create-a-new-meal-plan": "Crea un Novo Menú",
@@ -322,9 +322,9 @@
"mealplan-update-failed": "Produciuse un erro na actualización do menú",
"mealplan-updated": "Menú Actualizado",
"mealplan-households-description": "If no household is selected, recipes can be added from any household",
"any-category": "Any Category",
"any-tag": "Any Tag",
"any-household": "Any Household",
"any-category": "Calquer Categoria",
"any-tag": "Calquer Etiqueta",
"any-household": "Calquer Casa",
"no-meal-plan-defined-yet": "Aínda non se definiu ningún menú",
"no-meal-planned-for-today": "Non hai ningunha comida prevista para hoxe",
"numberOfDays-hint": "Número de días ao cargar a páxina",
@@ -395,7 +395,7 @@
},
"tandoor": {
"description-long": "Mealie can import recipes from Tandoor. Export your data in the \"Default\" format, then upload the .zip below.",
"title": "Tandoor Recipes"
"title": "Receitas do Tandoor"
},
"recipe-data-migrations": "Recipe Data Migrations",
"recipe-data-migrations-explanation": "Recipes can be migrated from another supported application to Mealie. This is a great way to get started with Mealie.",
@@ -404,8 +404,8 @@
"tag-all-recipes": "Tag all recipes with {tag-name} tag",
"nextcloud-text": "Nextcloud recipes can be imported from a zip file that contains the data stored in Nextcloud. See the example folder structure below to ensure your recipes are able to be imported.",
"chowdown-text": "Mealie admite de forma nativa o formato do repositorio de chowdown. Descarga o repositorio de códigos como ficheiro .zip e cárgao a continuación.",
"recipe-1": "Recipe 1",
"recipe-2": "Recipe 2",
"recipe-1": "Receita 1",
"recipe-2": "Receita 2",
"paprika-text": "Mealie can import recipes from the Paprika application. Export your recipes from paprika, rename the export extension to .zip and upload it below.",
"mealie-text": "Mealie can import recipes from the Mealie application from a pre v1.0 release. Export your recipes from your old instance, and upload the zip file below. Note that only recipes can be imported from the export.",
"plantoeat": {
@@ -424,15 +424,15 @@
"new-recipe": {
"bulk-add": "Bulk Add",
"error-details": "Only websites containing ld+json or microdata can be imported by Mealie. Most major recipe websites support this data structure. If your site cannot be imported but there is json data in the log, please submit a github issue with the URL and data.",
"error-title": "Looks Like We Couldn't Find Anything",
"from-url": "Import a Recipe",
"github-issues": "GitHub Issues",
"error-title": "Parece que non conseguimos encontrar nada",
"from-url": "Importar unha Receita",
"github-issues": "Problemas no GitHub",
"google-ld-json-info": "Google ld+json Info",
"must-be-a-valid-url": "Must be a Valid URL",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list",
"recipe-markup-specification": "Recipe Markup Specification",
"recipe-url": "Recipe URL",
"recipe-html-or-json": "Recipe HTML or JSON",
"must-be-a-valid-url": "Precisa ser un URL válido",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Pegue os datos da sua receita. Cada liña será tratada como un item nunha lista",
"recipe-markup-specification": "Especificación Markup da Receita",
"recipe-url": "URL da Receita",
"recipe-html-or-json": "Receita en HTML ou JSON",
"upload-a-recipe": "Upload a Recipe",
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
"url-form-hint": "Copy and paste a link from your favorite recipe website",
@@ -440,13 +440,13 @@
"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",
"import-by-url": "Import a recipe by URL",
"import-by-url": "Importar unha receita por URL",
"create-manually": "Create a recipe manually",
"make-recipe-image": "Make this the recipe image"
},
"page": {
"404-page-not-found": "404 Page not found",
"all-recipes": "All Recipes",
"404-page-not-found": "404 Páxina non encontrada",
"all-recipes": "Todas as receitas",
"new-page-created": "New page created",
"page": "Páxina",
"page-creation-failed": "Produciuse un erro ao creala páxina",
@@ -467,7 +467,7 @@
"calories-suffix": "calorías",
"carbohydrate-content": "Carbohidratos",
"categories": "Categorías",
"cholesterol-content": "Cholesterol",
"cholesterol-content": "Colesterol",
"comment-action": "Comentar",
"comment": "Comentario",
"comments": "Comentarios",
@@ -535,7 +535,7 @@
"add-recipe-to-mealplan": "Add Recipe to Mealplan",
"entry-type": "Entry Type",
"date-format-hint": "Formato MM/DD/YYYY",
"date-format-hint-yyyy-mm-dd": "YYYY-MM-DD format",
"date-format-hint-yyyy-mm-dd": "Formato AAAA-MM-DD",
"add-to-list": "Add to List",
"add-to-plan": "Add to Plan",
"add-to-timeline": "Add to Timeline",
@@ -553,7 +553,7 @@
"choose-unit": "Choose Unit",
"press-enter-to-create": "Press Enter to Create",
"choose-food": "Choose Food",
"notes": "Notes",
"notes": "Notas",
"toggle-section": "Toggle Section",
"see-original-text": "See Original Text",
"original-text-with-value": "Original Text: {originalText}",
@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} fixo isto",
"last-made-date": "Feito por Última Vez {date}",
"api-extras-description": "Os extras de receitas son unha característica clave da API de Mealie. Permítenche crear pares de clave/valor JSON personalizados dentro dunha receita, para facer referencia desde aplicacións de terceiros. Podes usar estas teclas para proporcionar información, por exemplo, para activar automatizacións ou mensaxes personalizadas para transmitir ao dispositivo que desexes.",
"message-key": "Message Key",
"parse": "Parse",
@@ -1194,8 +1193,8 @@
"demo_password": "Password: {password}"
},
"ocr-editor": {
"ocr-editor": "Ocr editor",
"toolbar": "Toolbar",
"ocr-editor": "Editor OCR",
"toolbar": "Barra de ferramentas",
"selection-mode": "Selection mode",
"pan-and-zoom-picture": "Pan and zoom picture",
"split-text": "Split text",
@@ -1203,7 +1202,7 @@
"split-by-block": "Split by text block",
"flatten": "Flatten regardless of original formating",
"help": {
"help": "Help",
"help": "Axuda",
"mouse-modes": "Mouse modes",
"selection-mode": "Selection Mode (default)",
"selection-mode-desc": "The selection mode is the main mode that can be used to enter data:",

View File

@@ -579,7 +579,6 @@
"made-this": "הכנתי את זה",
"how-did-it-turn-out": "איך יצא?",
"user-made-this": "{user} הכין את זה",
"last-made-date": "נעשה לאחרונה ב{date}",
"api-extras-description": "מתכונים נוספים הם יכולת מפתח של Mealie API. הם מאפשרים ליצור צמדי key/value בצורת JSON על מנת לקרוא אותם בתוכנת צד שלישית. תוכלו להשתמש בצמדים האלה כדי לספק מידע, לדוגמא להפעיל אוטומציות או הודעות מותאמות אישית למכשירים מסויימים.",
"message-key": "מפתח הודעה",
"parse": "ניתוח",

View File

@@ -579,7 +579,6 @@
"made-this": "Napravio/la sam ovo",
"how-did-it-turn-out": "Kako je ispalo?",
"user-made-this": "{user} je napravio/la ovo",
"last-made-date": "Zadnji put napravljeno {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Ključ poruke",
"parse": "Razluči (parsiraj)",

View File

@@ -579,7 +579,6 @@
"made-this": "Elkészítettem ezt",
"how-did-it-turn-out": "Hogyan sikerült?",
"user-made-this": "ezt {user} készítette el",
"last-made-date": "Utoljára elkészítve {date}",
"api-extras-description": "A receptek extrái a Mealie API egyik legfontosabb szolgáltatása. Lehetővé teszik, hogy egyéni JSON kulcs/érték párokat hozzon létre egy receptben, amelyekre harmadik féltől származó alkalmazásokból hivatkozhat. Ezeket a kulcsokat információszolgáltatásra használhatja, például automatizmusok vagy egyéni üzenetek indítására, amelyeket a kívánt eszközre küldhet.",
"message-key": "Üzenetkulcs",
"parse": "Előkészítés",

View File

@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -579,7 +579,6 @@
"made-this": "L'Ho Preparato",
"how-did-it-turn-out": "Come è venuto?",
"user-made-this": "{user} l'ha preparato",
"last-made-date": "Ultima Preparazione {date}",
"api-extras-description": "Le opzioni extra delle ricette sono una caratteristica fondamentale dell'API Mealie. Consentono di creare json personalizzati con coppie di chiavi/valore all'interno di una ricetta a cui fare riferimento tramite applicazioni terze. È possibile utilizzare queste chiavi per inserire informazioni, per esempio per attivare automazioni oppure per inoltrare messaggi personalizzati al dispositivo desiderato.",
"message-key": "Chiave Messaggio",
"parse": "Analizza",

View File

@@ -579,7 +579,6 @@
"made-this": "これを作りました",
"how-did-it-turn-out": "どうなりましたか?",
"user-made-this": "{user} がこれを作りました",
"last-made-date": "最後は {date} に作りました",
"api-extras-description": "レシピの追加機能はMealie APIの主な機能です。 サードパーティアプリから参照するために、レシピ内にカスタムJSONキー/値のペアを作成することができます。 これらのキーを使用して情報を提供することができます。例えば、自動化をトリガーしたり、カスタムメッセージをお使いのデバイスにリレーするなどです。",
"message-key": "メッセージキー",
"parse": "解析",

View File

@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -579,7 +579,6 @@
"made-this": "Aš tai gaminau",
"how-did-it-turn-out": "Kaip tai pavyko?",
"user-made-this": "{user} gamino šį patiekalą",
"last-made-date": "Paskutinį kartą gaminta {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Žinutės raktas",
"parse": "Nuskaityti",

View File

@@ -579,7 +579,6 @@
"made-this": "Es to pagatavoju",
"how-did-it-turn-out": "Kā tas izrādījās?",
"user-made-this": "{user}izdarīja šo",
"last-made-date": "Pēdējo reizi izgatavots {date}",
"api-extras-description": "Recepšu ekstras ir galvenā Mealie API iezīme. Tie ļauj jums izveidot pielāgotus JSON atslēgu/vērtību pārus receptē, lai atsaucotos no trešo pušu lietojumprogrammām. Varat izmantot šos taustiņus, lai sniegtu informāciju, piemēram, aktivizētu automatizāciju vai pielāgotus ziņojumus, lai tos pārsūtītu uz vēlamo ierīci.",
"message-key": "Ziņojuma atslēga",
"parse": "Parsēšana",

View File

@@ -579,7 +579,6 @@
"made-this": "Ik heb dit gemaakt",
"how-did-it-turn-out": "Hoe was je gerecht?",
"user-made-this": "{user} heeft dit gemaakt",
"last-made-date": "Laatst gemaakt op {date}",
"api-extras-description": "Extra's bij recepten zijn een belangrijke functie van de Mealie API. Hiermee kun je aangepaste JSON key/value paren maken bij een recept om naar te verwijzen vanuit applicaties van derden. Je kunt deze sleutels gebruiken om extra informatie te bieden, bijvoorbeeld om automatisering aan te sturen of aangepaste berichten naar je gewenste apparaat te laten versturen.",
"message-key": "Berichtsleutel",
"parse": "Ontleed",

View File

@@ -579,7 +579,6 @@
"made-this": "Jeg har laget dette",
"how-did-it-turn-out": "Hvordan ble det?",
"user-made-this": "{user} har laget dette",
"last-made-date": "Sist laget {date}",
"api-extras-description": "Ekstramaterialer til oppskrifter er en viktig funksjon i Mealie API-en. De lar deg opprette egendefinerte JSON-nøkkel/verdi-par innenfor en oppskrift for å referere fra tredjepartsapplikasjoner. Du kan bruke disse nøklene til å gi informasjon for eksempel for å utløse automatiseringer eller egendefinerte meldinger som skal videreformidles til ønsket enhet.",
"message-key": "Meldingsnøkkel",
"parse": "Analyser",

View File

@@ -579,7 +579,6 @@
"made-this": "Ugotowałem to",
"how-did-it-turn-out": "Jak się to udało?",
"user-made-this": "{user} ugotował(a) to",
"last-made-date": "Ostatnio ugotowano {date}",
"api-extras-description": "Dodatki w przepisach są kluczową cechą API Mealie. Pozwalają na tworzenie niestandardowych par kluczy/wartości JSON w przepisie do odwoływania się przez zewnętrzne aplikacje. Możesz użyć tych kluczy do wyzwalania automatyzacji lub przekazywania niestandardowych wiadomości do twoich wybranych urządzeń.",
"message-key": "Klucz Wiadomości",
"parse": "Analizuj",

View File

@@ -579,7 +579,6 @@
"made-this": "Eu Fiz Isso",
"how-did-it-turn-out": "Como que ficou?",
"user-made-this": "{user} fez isso",
"last-made-date": "Feito pela última vez em {date}",
"api-extras-description": "Extras de receitas são atributos-chave da API do Mealie. Assim, você pode criar pares chave/valor JSON personalizados dentro de uma receita, referenciando aplicações de terceiros. Você pode usar as chaves para fornecer informações, como por ex. ativar automações ou mensagens que serão enviadas a seus dispositivos.",
"message-key": "Chave de mensagem",
"parse": "Analisar",

View File

@@ -579,7 +579,6 @@
"made-this": "Eu fiz isto",
"how-did-it-turn-out": "Que tal ficou?",
"user-made-this": "{user} fez isto",
"last-made-date": "Última vez {date}",
"api-extras-description": "Extras para receitas são funcionalidades chave da API Mealie. Estas permitem criar, dentro de uma receita, pares personalizados de chave/valor em JSON, para referência a partir de aplicações de terceiros. Pode usar essas chaves para fornecer informações, por exemplo, para acionar automações ou mensagens personalizadas para transmitir a um determinado dispositivo.",
"message-key": "Chave de Mensagem",
"parse": "Interpretar",

View File

@@ -579,7 +579,6 @@
"made-this": "Am făcut asta",
"how-did-it-turn-out": "Cum a ieșit?",
"user-made-this": "{user} a făcut asta",
"last-made-date": "Ultima preparare {date}",
"api-extras-description": "Recipes extras sunt o caracteristică cheie a API-ului Mealie. Îți permit să creezi perechi personalizate de cheie/valoare JSON într-o rețetă, ca să faci referire la aplicații terțe. Puteți utiliza aceste chei pentru a furniza informații, de exemplu pentru a declanșa automatizări sau mesaje personalizate pentru a transmite dispozitivul dorit.",
"message-key": "Cheie mesaj",
"parse": "Parsează",

View File

@@ -579,7 +579,6 @@
"made-this": "Я сделал это",
"how-did-it-turn-out": "Что получилось?",
"user-made-this": "{user} сделал это",
"last-made-date": "Последний раз сделано {date}",
"api-extras-description": "Дополнения к рецептам являются ключевым элементом Mealie API. Они позволяют создавать пользовательские пары json ключ/значение в рецепте для ссылания на другие приложения. Вы можете использовать эти ключи, чтобы сохранить нужную информацию, например, для автоматизаций или уведомлений на ваши устройства.",
"message-key": "Ключ сообщения",
"parse": "Обработать",

View File

@@ -579,7 +579,6 @@
"made-this": "Toto som uvaril",
"how-did-it-turn-out": "Ako to dopadlo?",
"user-made-this": "{user} toto uvaril/-a",
"last-made-date": "Posledne pripravené {date}",
"api-extras-description": "API dolnky receptov sú kľúčovou funkcionalitou Mealie API. Umožňujú užívateľom vytvárať vlastné JSON páry kľúč/hodnota v rámci receptu, a využiť v aplikáciách tretích strán. Údaje uložené pod jednotlivými kľúčmi je možné využiť napríklad ako spúšťač automatizovaných procesov, či pri zasielaní vlastných správ do vami zvolených zariadení.",
"message-key": "Kľúč správy",
"parse": "Analyzovať",

View File

@@ -579,7 +579,6 @@
"made-this": "Naredil sem to",
"how-did-it-turn-out": "Kako se je izkazalo?",
"user-made-this": "{user} je tole pripravil/a",
"last-made-date": "Nazadnje pripravljen {date}",
"api-extras-description": "Dodatni podatki za recepte so ključna funkcionalnost Mealie APIja. Omogočajo ustvarjanje lastnih JSON ključ / vrednost parov v okviru recepta, da lahko do njih dostopajo zunanje aplikacije. Te ključe lahko uporabiš za posredovanje informacij, na primer za sprožanje avtomatike ali sporočanje prilagojenih sporočil na poljubno napravo.",
"message-key": "Ključ sporočila",
"parse": "Razloči",

View File

@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Последњи пут прављено {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -45,7 +45,7 @@
"category-filter": "Kategorifilter",
"category-update-failed": "Kategori gick inte att uppdatera",
"category-updated": "Kategori uppdaterad",
"uncategorized-count": "Ingen Kategori {count}",
"uncategorized-count": "Utan kategori {count}",
"create-a-category": "Skapa kategori",
"category-name": "Kategorinamn",
"category": "Kategori"
@@ -579,7 +579,6 @@
"made-this": "Jag lagade den här",
"how-did-it-turn-out": "Hur blev rätten?",
"user-made-this": "{user} lagade detta",
"last-made-date": "Senast lagad {date}",
"api-extras-description": "Recept API-tillägg är en viktig funktion i Mealie's API. Med hjälp av dem kan du skapa anpassade JSON-nyckel/värdepar i ett recept, som du kan referera till från tredjepartsapplikationer. Du kan använda dessa nycklar för att tillhandahålla information, till exempel för att trigga automatiseringar eller anpassade meddelanden som ska vidarebefordras till önskad enhet.",
"message-key": "Meddelandenyckel",
"parse": "Läs in",

View File

@@ -579,7 +579,6 @@
"made-this": "Bunu ben yaptım",
"how-did-it-turn-out": "Nasıl oldu?",
"user-made-this": "{user} bunu yaptı",
"last-made-date": "En Son {date} Yapıldı",
"api-extras-description": "Tarif ekstraları Mealie API'nin önemli bir özelliğidir. Üçüncü taraf uygulamalardan referans almak üzere bir tarif içinde özel JSON anahtar/değer çiftleri oluşturmanıza olanak tanır. Bu tuşları, örneğin otomasyonları tetiklemek veya istediğiniz cihaza iletilecek özel mesajları bilgi sağlamak için kullanabilirsiniz.",
"message-key": "İleti Anahtarı",
"parse": "Ayrıştırma",

View File

@@ -5,7 +5,7 @@
"api-docs": "Документація API",
"api-port": "Порт API",
"application-mode": "Режим додатку",
"database-type": "Тип бази данних",
"database-type": "Тип бази даних",
"database-url": "URL-адреса бази даних",
"default-group": "Групи за замовчуванням",
"default-household": "Сімʼя за замовчуванням",
@@ -190,7 +190,7 @@
"a-name-is-required": "Необхідно вказати назву",
"delete-with-name": "Видалити {name}",
"confirm-delete-generic-with-name": "Ви дійсно хочете видалити {name}?",
"confirm-delete-own-admin-account": "Зверніть увагу, що ви намагаєтеся видалити свій обліковий запис адміністратора! Цю дію неможливо скасувати і ви остаточно видалите ваш обліковий запис?",
"confirm-delete-own-admin-account": "Зверніть увагу, що ви намагаєтеся видалити свій обліковий запис адміністратора! Цю дію неможливо скасувати й ви остаточно видалите ваш обліковий запис?",
"organizer": "Організатор",
"transfer": "Передача",
"copy": "Скопіювати",
@@ -200,7 +200,7 @@
"learn-more": "Дізнатися більше",
"this-feature-is-currently-inactive": "Ця функція наразі не активна",
"clipboard-not-supported": "Буфер обміну не підтримується",
"copied-to-clipboard": "Скопійовано до буферу обміну",
"copied-to-clipboard": "Скопійовано до буфера обміну",
"your-browser-does-not-support-clipboard": "Ваш браузер не підтримує буфер обміну",
"copied-items-to-clipboard": "Жоден елемент не скопійовано в буфер обміну|Один елемент скопійовано в буфер обміну|Скопійовано {count} елементів в буфер обміну",
"actions": "Дії",
@@ -246,14 +246,14 @@
"manage-members": "Керування Користувачами",
"manage-members-description": "Керуйте дозволами учасників вашої сімʼї. {manage} дозволяє користувачеві отримати доступ до сторінки керування даними {invite} дозволяє користувачеві генерувати посилання запрошення для інших користувачів. Власники групи не можуть змінити власні дозволи.",
"manage": "Керування",
"manage-household": "Manage Household",
"manage-household": "Керувати сімʼєю",
"invite": "Запрошення",
"looking-to-update-your-profile": "Бажаєте оновити свій профіль?",
"default-recipe-preferences-description": "Це типові налаштування, коли створюється новий рецепт у вашій групі. Ці параметри можна змінити для окремих рецептів в меню налаштувань рецептів.",
"default-recipe-preferences": "Параметри за умовчанням",
"group-preferences": "Налаштування групи",
"private-group": "Приватна група",
"private-group-description": "Setting your group to private will disable all public view options. This overrides any individual public view settings",
"private-group-description": "Якщо зробити групу приватною, то всі налаштування публічного перегляду буде скинуто. Це замінить індивідуальні налаштування публічного перегляду",
"enable-public-access": "Дозволити загальний доступ",
"enable-public-access-description": "Робить групові рецепти загальнодоступними за замовчуванням і дозволяє користувачам переглядати рецепти без входу в систему",
"allow-users-outside-of-your-group-to-see-your-recipes": "Дозволити користувачам за межами вашої групи бачити ваші рецепти",
@@ -277,7 +277,7 @@
"admin-group-management-text": "Зміни до цієї групи будуть відображені негайно.",
"group-id-value": "Id групи: {0}",
"total-households": "Всього сімей",
"you-must-select-a-group-before-selecting-a-household": "You must select a group before selecting a household"
"you-must-select-a-group-before-selecting-a-household": "Ви маєте вибрати групу перед тим, як вибирати сім'ю"
},
"household": {
"household": "Сімʼя",
@@ -292,9 +292,9 @@
"admin-household-management-text": "Зміни до цієї сімʼї будуть відображені негайно.",
"household-id-value": "Ідентифікатор сімʼї: {0}",
"private-household": "Приватна сімʼя",
"private-household-description": "Setting your household to private will disable all public view options. This overrides any individual public view settings",
"lock-recipe-edits-from-other-households": "Lock recipe edits from other households",
"lock-recipe-edits-from-other-households-description": "When enabled only users in your household can edit recipes created by your household",
"private-household-description": "Якщо зробити сім'ю приватною, то всі налаштування публічного перегляду буде скинуто. Це замінить індивідуальні налаштування публічного перегляду",
"lock-recipe-edits-from-other-households": "Заблокувати редагування рецептів іншими сім'ями",
"lock-recipe-edits-from-other-households-description": "Якщо увімкнено, тільки члени вашої сім'ї зможуть редагувати рецепти, які були створені вашою сім'єю",
"household-recipe-preferences": "Налаштування рецептів сімʼї",
"default-recipe-preferences-description": "Це типові налаштування для нового рецепта у вашій сімʼї. Ці параметри можна змінити для окремих рецептів в меню налаштувань рецептів.",
"allow-users-outside-of-your-household-to-see-your-recipes": "Дозволити користувачам за межами вашої сімʼї бачити ваші рецепти",
@@ -321,10 +321,10 @@
"mealplan-settings": "Налаштування плану харчування",
"mealplan-update-failed": "Не вдалося оновити план харчування",
"mealplan-updated": "План харчування оновлено",
"mealplan-households-description": "If no household is selected, recipes can be added from any household",
"mealplan-households-description": "Якщо жодної сім'ї не вибрано, рецепти можуть бути доданими з будь-якої сім'ї",
"any-category": "Будь-яка категорія",
"any-tag": "Будь-який тег",
"any-household": "Any Household",
"any-household": "Будь-яка сім'я",
"no-meal-plan-defined-yet": "Не створено жодного плану харчування",
"no-meal-planned-for-today": "Не заплановано харчування на сьогодні",
"numberOfDays-hint": "Скільки днів завантажувати на сторінку",
@@ -357,7 +357,7 @@
"for-type-meal-types": "для {0} типів харчування",
"meal-plan-rules": "Правила планів харчування",
"new-rule": "Нове правило",
"meal-plan-rules-description": "You can create rules for auto selecting recipes for your meal plans. These rules are used by the server to determine the random pool of recipes to select from when creating meal plans. Note that if rules have the same day/type constraints then the rule filters will be merged. In practice, it's unnecessary to create duplicate rules, but it's possible to do so.",
"meal-plan-rules-description": "Ви можете створити правила для автоматичного вибору рецептів для ваших планів харчування. Ці правила використовуються сервером для вибору рецептів при створенні плану харчування. Зверніть увагу, що якщо правила мають обмеження на день/тип, то їх категорії будуть об'єднані. Дублювати правила немає сенсу, але можливо.",
"new-rule-description": "При створенні нового правила для плану харчування, ви можете обмежити правило на певний день тижня та/або певний тип їжі. Щоб застосувати правило до всіх днів або всіх типів їжі, ви можете встановити правило \"Будь-який\", що застосовуватиме його до всіх можливих значень для дня та/або типу їжі.",
"recipe-rules": "Правила рецептів",
"applies-to-all-days": "Застосовується до всіх днів",
@@ -518,7 +518,7 @@
"save-recipe-before-use": "Зберегти рецепт перед використанням",
"section-title": "Назва розділу",
"servings": "Порції",
"serves-amount": "Serves {amount}",
"serves-amount": "Порцій: {amount}",
"share-recipe-message": "Я хотів би поділитися з тобою своїм рецептом {0}.",
"show-nutrition-values": "Показати харчову цінність",
"sodium-content": "Натрій",
@@ -547,8 +547,8 @@
"failed-to-add-recipe-to-mealplan": "Не вдалося додати рецепт до плану харчування",
"failed-to-add-to-list": "Не вдалося додати до списку",
"yield": "Вихід",
"yields-amount-with-text": "Yields {amount} {text}",
"yield-text": "Yield Text",
"yields-amount-with-text": "Вийде: {amount} {text}",
"yield-text": "Текст виходу",
"quantity": "Кількість",
"choose-unit": "Виберіть одиниці вимірювання",
"press-enter-to-create": "Натисніть Enter, щоб створити",
@@ -579,7 +579,6 @@
"made-this": "Я це приготував",
"how-did-it-turn-out": "Як вийшло?",
"user-made-this": "{user} зробив це",
"last-made-date": "Востаннє приготовано {date}",
"api-extras-description": "Додатки в рецептах - ключова функція API Mealie. Вони дозволяють створювати користувацьку пару JSON ключів та значень в рецепті для сторонніх додатків. Це можна використовувати для автоматизації або для створення користувацьких повідомлень для сторонніх сервісів.",
"message-key": "Ключ повідомлення",
"parse": "Проаналізувати",
@@ -662,24 +661,24 @@
"no-food": "Немає їжі"
},
"reset-servings-count": "Скинути кількість порцій",
"not-linked-ingredients": "Additional Ingredients"
"not-linked-ingredients": "Додаткові продукти"
},
"recipe-finder": {
"recipe-finder": "Recipe Finder",
"recipe-finder-description": "Search for recipes based on ingredients you have on hand. You can also filter by tools you have available, and set a maximum number of missing ingredients or tools.",
"selected-ingredients": "Selected Ingredients",
"no-ingredients-selected": "No ingredients selected",
"missing": "Missing",
"no-recipes-found": "No recipes found",
"no-recipes-found-description": "Try adding more ingredients to your search or adjusting your filters",
"include-ingredients-on-hand": "Include Ingredients On Hand",
"include-tools-on-hand": "Include Tools On Hand",
"max-missing-ingredients": "Max Missing Ingredients",
"max-missing-tools": "Max Missing Tools",
"selected-tools": "Selected Tools",
"other-filters": "Other Filters",
"ready-to-make": "Ready to Make",
"almost-ready-to-make": "Almost Ready to Make"
"recipe-finder": "Шукач рецептів",
"recipe-finder-description": "Пошук рецептів базується на продуктах, які ви маєте. Ви також можете фільтрувати за наявними інструментами та встановити максимальну кількість відсутніх продуктів або інструментів.",
"selected-ingredients": "Вибрані продукти",
"no-ingredients-selected": "Жодного продукту не вибрано",
"missing": "Відсутні",
"no-recipes-found": "Рецептів не знайдено",
"no-recipes-found-description": "Спробуйте додати більше продуктів до пошукового списку або підлаштувати фільтри",
"include-ingredients-on-hand": "Включити наявні продукти",
"include-tools-on-hand": "Включити наявні інструменти",
"max-missing-ingredients": "Максимум відсутніх продуктів",
"max-missing-tools": "Максимум відсутніх інструментів",
"selected-tools": "Вибрані інструменти",
"other-filters": "Інші фільтри",
"ready-to-make": "Готове до приготування",
"almost-ready-to-make": "Майже готове до приготування"
},
"search": {
"advanced-search": "Розширений пошук",
@@ -884,7 +883,7 @@
"are-you-sure-you-want-to-check-all-items": "Ви впевнені, що хочете відмітити всі елементи?",
"are-you-sure-you-want-to-uncheck-all-items": "Ви впевнені, що хочете зняти відмітку з усіх елементів?",
"are-you-sure-you-want-to-delete-checked-items": "Ви впевнені, що хочете видалити всі відмічені елементи?",
"no-shopping-lists-found": "No Shopping Lists Found"
"no-shopping-lists-found": "Списків покупок не знайдено"
},
"sidebar": {
"all-recipes": "Всі рецепти",
@@ -1030,7 +1029,7 @@
"administrator": "Адміністратор",
"user-can-invite-other-to-group": "Користувач може запрошувати інших в групу",
"user-can-manage-group": "Користувач може керувати групою",
"user-can-manage-household": "User can manage household",
"user-can-manage-household": "Користувач може управляти сім'єю",
"user-can-organize-group-data": "Користувач може впорядковувати дані групи",
"enable-advanced-features": "Увімкнути додаткові функції",
"it-looks-like-this-is-your-first-time-logging-in": "Схоже, ви заходите вперше.",
@@ -1290,13 +1289,13 @@
"debug-openai-services-description": "Використовуйте цю сторінку, щоб налагодити служби OpenAI. Ви можете перевірити ваше з'єднання з OpenAI й побачити результати тут. Якщо ввімкнено служби зображень, ви також можете надати зображення.",
"run-test": "Запустити перевірку",
"test-results": "Результати перевірки",
"group-delete-note": "Groups with users or households cannot be deleted",
"household-delete-note": "Households with users cannot be deleted"
"group-delete-note": "Не можна видалити групи з користувачами чи сім'ями в ній",
"household-delete-note": "Не можна видалити сім'ю з користувачами в ній"
},
"profile": {
"welcome-user": "👋 Ласкаво просимо, {0}!",
"description": "Керування вашим профілем, рецептами та налаштуваннями групи.",
"invite-link": "Invite Link",
"invite-link": "Посилання-запрошення",
"get-invite-link": "Отримати посилання-запрошення",
"get-public-link": "Отримати публічне посилання",
"account-summary": "Аккаунт",
@@ -1345,9 +1344,9 @@
},
"cookbook": {
"cookbooks": "Кулінарні книги",
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",
"description": "Кулінарні книги - це ще один спосіб організовувати рецепти за допомогою розділів та інших фільтрів. Нова кулінарна книга з'явиться на боковій панелі, і всі рецепти, які відповідають обраним фільтрам, будуть показуватися в кулінарній книзі.",
"hide-cookbooks-from-other-households": "Приховати кулінарні книги від інших сімей",
"hide-cookbooks-from-other-households-description": "Якщо вибрано, тільки кулінарні книги вашої сім'ї буде видно на боковій панелі",
"public-cookbook": "Публічна кулінарна книга",
"public-cookbook-description": "Публічними кулінарними книгами можна поділитися з будь-ким, і вони будуть відображатися на сторінці вашої групи.",
"filter-options": "Параметри фільтра",

View File

@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -579,7 +579,6 @@
"made-this": "我做了这道菜",
"how-did-it-turn-out": "成品怎么样?",
"user-made-this": "{user}做了",
"last-made-date": "上次制作于{date}",
"api-extras-description": "食谱扩展是Mealie API的关键功能之一。它允许你在食谱中添加自定义JSON键值对以供第三方程序使用。你可以利用这些键提供信息实现更多功能例如触发自动化或转发自定义信息到指定的设备上。",
"message-key": "键名",
"parse": "自动解析",

View File

@@ -579,7 +579,6 @@
"made-this": "I Made This",
"how-did-it-turn-out": "How did it turn out?",
"user-made-this": "{user} made this",
"last-made-date": "Last Made {date}",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "Message Key",
"parse": "Parse",

View File

@@ -391,6 +391,11 @@ export interface CreateIngredientFoodAlias {
name: string;
[k: string]: unknown;
}
export interface ShoppingListAddRecipeParamsBulk {
recipeIncrementQuantity?: number;
recipeIngredients?: RecipeIngredient[] | null;
recipeId: string;
}
export interface ShoppingListCreate {
name?: string | null;
extras?: {

View File

@@ -291,6 +291,7 @@ export interface UserBase {
id: string;
username?: string | null;
admin: boolean;
fullName?: string | null;
}
export interface RecipeCategoryResponse {
name: string;

View File

@@ -1,7 +1,7 @@
import { BaseCRUDAPI } from "../base/base-clients";
import { RecipeIngredient } from "../types/recipe";
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
import {
ShoppingListAddRecipeParamsBulk,
ShoppingListCreate,
ShoppingListItemCreate,
ShoppingListItemOut,
@@ -16,7 +16,7 @@ const prefix = "/api";
const routes = {
shoppingLists: `${prefix}/households/shopping/lists`,
shoppingListsId: (id: string) => `${prefix}/households/shopping/lists/${id}`,
shoppingListIdAddRecipe: (id: string, recipeId: string) => `${prefix}/households/shopping/lists/${id}/recipe/${recipeId}`,
shoppingListIdAddRecipe: (id: string) => `${prefix}/households/shopping/lists/${id}/recipe`,
shoppingListIdRemoveRecipe: (id: string, recipeId: string) => `${prefix}/households/shopping/lists/${id}/recipe/${recipeId}/delete`,
shoppingListIdUpdateLabelSettings: (id: string) => `${prefix}/households/shopping/lists/${id}/label-settings`,
@@ -29,8 +29,8 @@ export class ShoppingListsApi extends BaseCRUDAPI<ShoppingListCreate, ShoppingLi
baseRoute = routes.shoppingLists;
itemRoute = routes.shoppingListsId;
async addRecipe(itemId: string, recipeId: string, recipeIncrementQuantity = 1, recipeIngredients: RecipeIngredient[] | null = null) {
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), { recipeIncrementQuantity, recipeIngredients });
async addRecipes(itemId: string, data: ShoppingListAddRecipeParamsBulk[]) {
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId), data);
}
async removeRecipe(itemId: string, recipeId: string, recipeDecrementQuantity = 1) {

View File

@@ -146,6 +146,8 @@ import {
mdiFileCabinet,
mdiSilverwareForkKnife,
mdiCodeTags,
mdiKnife,
mdiCookie
} from "@mdi/js";
export const icons = {
@@ -271,6 +273,8 @@ export const icons = {
windowClose: mdiWindowClose,
zip: mdiFolderZipOutline,
undo: mdiUndo,
knfife: mdiKnife,
bread: mdiCookie,
// Crud
backArrow: mdiArrowLeftBoldOutline,

View File

@@ -1,6 +1,6 @@
{
"name": "mealie",
"version": "2.5.0",
"version": "2.7.0",
"private": true,
"scripts": {
"dev": "nuxt",

View File

@@ -151,6 +151,24 @@
</div>
</div>
<!-- Create Item -->
<div v-if="createEditorOpen">
<ShoppingListItemEditor
v-model="createListItemData"
class="my-4"
:labels="allLabels || []"
:units="allUnits || []"
:foods="allFoods || []"
:allow-delete="false"
@delete="createEditorOpen = false"
@cancel="createEditorOpen = false"
@save="createListItem"
/>
</div>
<div v-else class="d-flex justify-end">
<BaseButton create @click="createEditorOpen = true" > {{ $t('general.add') }} </BaseButton>
</div>
<!-- Reorder Labels -->
<BaseDialog
v-model="reorderLabelsDialog"
@@ -177,23 +195,6 @@
</v-card>
</BaseDialog>
<!-- Create Item -->
<div v-if="createEditorOpen">
<ShoppingListItemEditor
v-model="createListItemData"
class="my-4"
:labels="allLabels || []"
:units="allUnits || []"
:foods="allFoods || []"
@delete="createEditorOpen = false"
@cancel="createEditorOpen = false"
@save="createListItem"
/>
</div>
<div v-else class="mt-4 d-flex justify-end">
<BaseButton create @click="createEditorOpen = true" > {{ $t('general.add') }} </BaseButton>
</div>
<!-- Checked Items -->
<div v-if="listItems.checked && listItems.checked.length > 0" class="mt-6">
<div class="d-flex">
@@ -833,7 +834,7 @@ export default defineComponent({
loadingCounter.value += 1;
recipeReferenceLoading.value = true;
const { data } = await userApi.shopping.lists.addRecipe(shoppingList.value.id, recipeId);
const { data } = await userApi.shopping.lists.addRecipes(shoppingList.value.id, [{ recipeId }]);
recipeReferenceLoading.value = false;
loadingCounter.value -= 1;

View File

@@ -0,0 +1,146 @@
"""remove instructions index
Revision ID: 7cf3054cbbcc
Revises: b9e516e2d3b3
Create Date: 2025-02-09 15:31:00.772295
"""
import sqlalchemy as sa
from sqlalchemy import orm
from alembic import op
from mealie.db.models._model_utils.guid import GUID
from mealie.core.root_logger import get_logger
# revision identifiers, used by Alembic.
revision = "7cf3054cbbcc"
down_revision: str | None = "b9e516e2d3b3"
branch_labels: str | tuple[str, ...] | None = None
depends_on: str | tuple[str, ...] | None = None
logger = get_logger()
class SqlAlchemyBase(orm.DeclarativeBase):
@classmethod
def normalized_fields(cls) -> list[orm.InstrumentedAttribute]:
return []
class RecipeModel(SqlAlchemyBase):
__tablename__ = "recipes"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
name_normalized: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False, index=True)
description_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
@classmethod
def normalized_fields(cls):
return [cls.name_normalized, cls.description_normalized]
class RecipeIngredientModel(SqlAlchemyBase):
__tablename__ = "recipes_ingredients"
id: orm.Mapped[int] = orm.mapped_column(sa.Integer, primary_key=True)
note_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
original_text_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
@classmethod
def normalized_fields(cls):
return [cls.note_normalized, cls.original_text_normalized]
class IngredientFoodModel(SqlAlchemyBase):
__tablename__ = "ingredient_foods"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
plural_name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
@classmethod
def normalized_fields(cls):
return [cls.name_normalized, cls.plural_name_normalized]
class IngredientFoodAliasModel(SqlAlchemyBase):
__tablename__ = "ingredient_foods_aliases"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
@classmethod
def normalized_fields(cls):
return [cls.name_normalized]
class IngredientUnitModel(SqlAlchemyBase):
__tablename__ = "ingredient_units"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
plural_name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
abbreviation_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
plural_abbreviation_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
@classmethod
def normalized_fields(cls):
return [
cls.name_normalized,
cls.plural_name_normalized,
cls.abbreviation_normalized,
cls.plural_abbreviation_normalized,
]
class IngredientUnitAliasModel(SqlAlchemyBase):
__tablename__ = "ingredient_units_aliases"
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
@classmethod
def normalized_fields(cls):
return [cls.name_normalized]
def truncate_normalized_fields() -> None:
bind = op.get_bind()
session = orm.Session(bind=bind)
models: list[type[SqlAlchemyBase]] = [
RecipeModel,
RecipeIngredientModel,
IngredientFoodModel,
IngredientFoodAliasModel,
IngredientUnitModel,
IngredientUnitAliasModel,
]
for model in models:
for record in session.query(model).all():
for field in model.normalized_fields():
if not (field_value := getattr(record, field.key)):
continue
setattr(record, field.key, field_value[:255])
try:
session.commit()
except Exception:
logger.exception(f"Failed to truncate normalized fields for {model.__name__}")
session.rollback()
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("recipe_instructions", schema=None) as batch_op:
batch_op.drop_index("ix_recipe_instructions_text")
# ### end Alembic commands ###
truncate_normalized_fields()
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("recipe_instructions", schema=None) as batch_op:
batch_op.create_index("ix_recipe_instructions_text", ["text"], unique=False)
# ### end Alembic commands ###

View File

@@ -1,3 +1,12 @@
import re
import warnings
# pyrdfa3 is no longer being updated and has docstrings that emit syntax warnings
warnings.filterwarnings(
"ignore", module=".*pyRdfa", category=SyntaxWarning, message=re.escape("invalid escape sequence '\\-'")
)
# ruff: noqa: E402
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
@@ -115,6 +124,7 @@ register_debug_handler(app)
async def start_scheduler():
SchedulerRegistry.register_daily(
tasks.purge_expired_tokens,
tasks.purge_group_registration,
tasks.purge_password_reset_tokens,
tasks.purge_group_data_exports,

View File

@@ -12,6 +12,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
from mealie.core.settings.themes import Theme
from .db_providers import AbstractDBProvider, db_provider_factory
from .static import PACKAGE_DIR
class ScheduleTime(NamedTuple):
@@ -109,7 +110,7 @@ class AppSettings(AppLoggingSettings):
BASE_URL: str = "http://localhost:8080"
"""trailing slashes are trimmed (ex. `http://localhost:8080/` becomes ``http://localhost:8080`)"""
STATIC_FILES: str = ""
STATIC_FILES: str = str(PACKAGE_DIR / "frontend")
"""path to static files directory (ex. `mealie/dist`)"""
IS_DEMO: bool = False

View File

@@ -5,4 +5,5 @@ from mealie import __version__
APP_VERSION = __version__
CWD = Path(__file__).parent
PACKAGE_DIR = CWD.parent.parent
BASE_DIR = CWD.parent.parent.parent

View File

@@ -69,16 +69,16 @@ def db_is_at_head(alembic_cfg: config.Config) -> bool:
def safe_try(func: Callable):
try:
func()
except Exception as e:
logger.error(f"Error calling '{func.__name__}': {e}")
except Exception:
logger.exception(f"Error calling '{func.__name__}'")
def connect(session: orm.Session) -> bool:
try:
session.execute(text("SELECT 1"))
return True
except Exception as e:
logger.error(f"Error connecting to database: {e}")
except Exception:
logger.exception("Error connecting to database")
return False
@@ -106,23 +106,27 @@ def main():
if not os.path.isfile(alembic_cfg_path):
raise Exception("Provided alembic config path doesn't exist")
run_fixes = False
alembic_cfg = Config(alembic_cfg_path)
if db_is_at_head(alembic_cfg):
logger.debug("Migration not needed.")
else:
logger.info("Migration needed. Performing migration...")
command.upgrade(alembic_cfg, "head")
run_fixes = True
if session.get_bind().name == "postgresql": # needed for fuzzy search and fast GIN text indices
session.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm;"))
db = get_repositories(session, group_id=None, household_id=None)
safe_try(lambda: fix_migration_data(session))
safe_try(lambda: fix_slug_food_names(db))
safe_try(lambda: fix_group_with_no_name(session))
if db.users.get_all():
logger.debug("Database exists")
if run_fixes:
safe_try(lambda: fix_migration_data(session))
safe_try(lambda: fix_slug_food_names(db))
safe_try(lambda: fix_group_with_no_name(session))
else:
logger.info("Database contains no users, initializing...")
init_db(session)

View File

@@ -18,7 +18,9 @@ class SqlAlchemyBase(DeclarativeBase):
@classmethod
def normalize(cls, val: str) -> str:
return unidecode(val).lower().strip()
# We cap the length to 255 to prevent indexes from being too long; see:
# https://www.postgresql.org/docs/current/btree.html
return unidecode(val).lower().strip()[:255]
class BaseMixins:

View File

@@ -23,8 +23,8 @@ class RecipeInstruction(SqlAlchemyBase):
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
position: Mapped[int | None] = mapped_column(Integer, index=True)
type: Mapped[str | None] = mapped_column(String, default="")
title: Mapped[str | None] = mapped_column(String) # This is the section title!!!
text: Mapped[str | None] = mapped_column(String, index=True)
title: Mapped[str | None] = mapped_column(String) # This is the section title
text: Mapped[str | None] = mapped_column(String)
summary: Mapped[str | None] = mapped_column(String)
ingredient_references: Mapped[list[RecipeIngredientRefLink]] = orm.relationship(

View File

@@ -11,11 +11,11 @@
},
"servings-text": {
"makes": "Fait",
"serves": "Pour",
"serves": "Sert",
"serving": "Portion",
"servings": "Portions",
"yield": "Quantité",
"yields": "Produit"
"yield": "Rendement",
"yields": "Rendements"
}
},
"mealplan": {

View File

@@ -1,13 +1,13 @@
{
"generic": {
"server-error": "An unexpected error occurred"
"server-error": "Ocorreu un erro inesperado"
},
"recipe": {
"unique-name-error": "Recipe names must be unique",
"recipe-created": "Recipe Created",
"unique-name-error": "Os nomes de receitas deven ser únicos",
"recipe-created": "Receita creada",
"recipe-defaults": {
"ingredient-note": "1 Cup Flour",
"step-text": "Recipe steps as well as other fields in the recipe page support markdown syntax.\n\n**Add a link**\n\n[My Link](https://demo.mealie.io)\n"
"step-text": "Os pasos da receita, como outros campos na páxina da receita, suportan a sintaxe markdown.\n\n**Adicionar un link**\n\n[Meu link](https://demo.mealie.io)\n"
},
"servings-text": {
"makes": "Makes",
@@ -33,26 +33,26 @@
"exceptions": {
"permission_denied": "You do not have permission to perform this action",
"no-entry-found": "The requested resource was not found",
"integrity-error": "Database integrity error",
"username-conflict-error": "This username is already taken",
"integrity-error": "Erro de integridade da base de datos",
"username-conflict-error": "Este nome de usuario xa está en uso",
"email-conflict-error": "This email is already in use"
},
"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-duplicated": "{name} has been duplicated",
"generic-created": "{name} creado",
"generic-updated": "{name} atualizado",
"generic-created-with-url": "{name} foi creado, {url}",
"generic-updated-with-url": "{name} foi atualizado, {url}",
"generic-duplicated": "{name} foi duplicado",
"generic-deleted": "{name} has been deleted"
},
"datetime": {
"year": "year|years",
"day": "day|days",
"hour": "hour|hours",
"minute": "minute|minutes",
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
"year": "ano|anos",
"day": "dia|dias",
"hour": "hora|horas",
"minute": "minuto|minutos",
"second": "segundo|segundos",
"millisecond": "milisegundo|milisegundos",
"microsecond": "microsegundo|microsegundos"
},
"emails": {
"password": {
@@ -67,14 +67,14 @@
"header_text": "You're Invited!",
"message_top": "You have been invited to join Mealie.",
"message_bottom": "Please click the button above to accept the invitation.",
"button_text": "Accept Invitation"
"button_text": "Aceptar Convite"
},
"test": {
"subject": "Mealie Test Email",
"header_text": "Test Email",
"message_top": "This is a test email.",
"message_bottom": "Please click the button above to test the email.",
"button_text": "Open Mealie"
"button_text": "Abrir o Mealie"
}
}
}

View File

@@ -4,16 +4,16 @@
},
"recipe": {
"unique-name-error": "Назва рецепту повинна бути унікальною",
"recipe-created": "Recipe Created",
"recipe-created": "Рецепт створено",
"recipe-defaults": {
"ingredient-note": "Стакан борошна",
"step-text": "Кроки рецептів, так само як і інші поля сторінки, підтримують синтаксис markdown.\n\n**Додати посилання**\n\n[Mоє посилання](https://demo.mealie.io)\n"
"step-text": "Кроки рецептів, так само як і інші поля сторінки, підтримують синтаксис markdown.\n\n**Додати посилання**\n\n[Моє посилання](https://demo.mealie.io)\n"
},
"servings-text": {
"makes": "Makes",
"serves": "Serves",
"serving": "Serving",
"servings": "Servings",
"serving": "Порція",
"servings": "Порції",
"yield": "Yield",
"yields": "Yields"
}

View File

@@ -6,7 +6,7 @@ from mealie.core.logger.config import log_config
def main():
uvicorn.run(
"app:app",
"mealie.app:app",
host=settings.API_HOST,
port=settings.API_PORT,
log_level=settings.LOG_LEVEL.lower(),

View File

@@ -1,6 +1,6 @@
{
"acorn-squash": {
"name": "courgeron"
"name": "courge poivrée"
},
"alfalfa-sprouts": {
"name": "pousses de luzerne"
@@ -109,7 +109,7 @@
"name": "cannabis"
},
"capsicum": {
"name": "capsicum"
"name": "piment"
},
"caraway": {
"name": "cumin"

View File

@@ -6,20 +6,20 @@
"name": "alfalfa sprouts"
},
"anchovies": {
"name": "anchovies"
"name": "anchoas"
},
"apples": {
"name": "apple",
"plural_name": "apples"
"name": "mazá",
"plural_name": "mazás"
},
"artichoke": {
"name": "artichoke"
"name": "alcachofa"
},
"arugula": {
"name": "arugula"
"name": "rúcula"
},
"asparagus": {
"name": "asparagus"
"name": "espárragos"
},
"avocado": {
"name": "avocado",
@@ -29,7 +29,7 @@
"name": "bacon"
},
"baking-powder": {
"name": "baking powder"
"name": "fermento en po"
},
"baking-soda": {
"name": "baking soda"
@@ -51,7 +51,7 @@
"plural_name": "bell peppers"
},
"blackberries": {
"name": "blackberries"
"name": "amoras"
},
"bok-choy": {
"name": "bok choy"
@@ -60,7 +60,7 @@
"name": "brassicas"
},
"bread": {
"name": "bread"
"name": "pan"
},
"breadfruit": {
"name": "breadfruit"
@@ -84,7 +84,7 @@
"name": "brussels sprouts"
},
"butter": {
"name": "butter"
"name": "manteiga"
},
"butternut-pumpkin": {
"name": "butternut pumpkin"
@@ -93,8 +93,8 @@
"name": "butternut squash"
},
"cabbage": {
"name": "cabbage",
"plural_name": "cabbages"
"name": "repolo",
"plural_name": "repolos"
},
"cactus-edible": {
"name": "cactus, edible"
@@ -115,8 +115,8 @@
"name": "caraway"
},
"carrot": {
"name": "carrot",
"plural_name": "carrots"
"name": "cenoura",
"plural_name": "cenouras"
},
"caster-sugar": {
"name": "caster sugar"
@@ -144,7 +144,7 @@
"name": "cereal grains"
},
"chard": {
"name": "chard"
"name": "acelga"
},
"cheese": {
"name": "queixo"
@@ -176,7 +176,7 @@
},
"coconut": {
"name": "coco",
"plural_name": "coconuts"
"plural_name": "cocos"
},
"coconut-milk": {
"name": "leite de coco"
@@ -194,7 +194,7 @@
"name": "confectioners' sugar"
},
"coriander": {
"name": "coriander"
"name": "coentro"
},
"corn": {
"name": "millo",
@@ -213,7 +213,7 @@
"name": "cream of tartar"
},
"cucumber": {
"name": "pepino",
"name": "cogombro",
"plural_name": "cucumbers"
},
"cumin": {
@@ -243,8 +243,8 @@
"plural_name": "eggplants"
},
"eggs": {
"name": "ovos",
"plural_name": "eggs"
"name": "ovo",
"plural_name": "ovos"
},
"endive": {
"name": "endive",
@@ -291,14 +291,14 @@
"name": "garam masala"
},
"garlic": {
"name": "garlic",
"plural_name": "garlics"
"name": "allo",
"plural_name": "allos"
},
"gem-squash": {
"name": "gem squash"
},
"ghee": {
"name": "ghee"
"name": "manteiga ghee"
},
"giblets": {
"name": "giblets"
@@ -330,7 +330,7 @@
"name": "herbs"
},
"honey": {
"name": "honey"
"name": "mel"
},
"isomalt": {
"name": "isomalt"
@@ -383,8 +383,8 @@
"name": "lettuce"
},
"liver": {
"name": "liver",
"plural_name": "livers"
"name": "fígado",
"plural_name": "figados"
},
"maize": {
"name": "maize"
@@ -393,20 +393,20 @@
"name": "maple syrup"
},
"meat": {
"name": "meat"
"name": "carne"
},
"milk": {
"name": "leite"
},
"mortadella": {
"name": "mortadella"
"name": "mortadela"
},
"mushroom": {
"name": "mushroom",
"plural_name": "mushrooms"
"name": "cogumelo",
"plural_name": "cogumelos"
},
"mussels": {
"name": "mussels"
"name": "mexillón"
},
"nanaimo-bar-mix": {
"name": "nanaimo bar mix"
@@ -424,8 +424,8 @@
"name": "nuts"
},
"octopuses": {
"name": "octopus",
"plural_name": "octopuses"
"name": "polbo",
"plural_name": "polbos"
},
"oils": {
"name": "oils"
@@ -440,7 +440,7 @@
"name": "olive oil"
},
"onion": {
"name": "onion"
"name": "cebola"
},
"onion-family": {
"name": "onion family"
@@ -450,13 +450,13 @@
},
"oranges": {
"name": "laranxas",
"plural_name": "oranges"
"plural_name": "laranxas"
},
"oregano": {
"name": "oregano"
},
"oysters": {
"name": "oysters"
"name": "ostras"
},
"panch-puran": {
"name": "panch puran"
@@ -473,13 +473,13 @@
},
"pear": {
"name": "pera",
"plural_name": "pears"
"plural_name": "peras"
},
"peas": {
"name": "peas"
},
"pepper": {
"name": "pepper",
"name": "",
"plural_name": "peppers"
},
"pineapple": {
@@ -491,11 +491,11 @@
"plural_name": "plantains"
},
"poppy-seeds": {
"name": "poppy seeds"
"name": "sementes de papoula"
},
"potato": {
"name": "potato",
"plural_name": "potatoes"
"name": "pataca",
"plural_name": "patacas"
},
"poultry": {
"name": "poultry"
@@ -524,7 +524,7 @@
"name": "arroz"
},
"rice-flour": {
"name": "rice flour"
"name": "fariña de arroz"
},
"rock-sugar": {
"name": "rock sugar"
@@ -536,7 +536,7 @@
"name": "salmón"
},
"salt": {
"name": "salt"
"name": "sal"
},
"salt-cod": {
"name": "bacallau en salgadura"
@@ -546,10 +546,10 @@
"plural_name": "scallions"
},
"seafood": {
"name": "seafood"
"name": "marisco"
},
"seeds": {
"name": "seeds"
"name": "sementes"
},
"sesame-seeds": {
"name": "sesame seeds"
@@ -633,7 +633,7 @@
},
"tomato": {
"name": "tomate",
"plural_name": "tomatoes"
"plural_name": "tomates"
},
"trout": {
"name": "troita"

View File

@@ -26,7 +26,7 @@
"plural_name": "abacate"
},
"bacon": {
"name": "bacon"
"name": "Carne fumada"
},
"baking-powder": {
"name": "fermento em pó"
@@ -109,7 +109,7 @@
"name": "canábis"
},
"capsicum": {
"name": "capsicum"
"name": "pimentão"
},
"caraway": {
"name": "alcarávia"

View File

@@ -361,7 +361,7 @@
"name": "kolerabe"
},
"kumara": {
"name": "kumara"
"name": "sladek krompir"
},
"leavening-agents": {
"name": "kvas"
@@ -377,7 +377,7 @@
"name": "limonina trava"
},
"lentils": {
"name": "lentils"
"name": "leča"
},
"lettuce": {
"name": "solata"
@@ -434,7 +434,7 @@
"name": "jedilni oslez"
},
"olive": {
"name": "olive"
"name": "oliva"
},
"olive-oil": {
"name": "olivno olje"

View File

@@ -616,7 +616,7 @@
},
"sweetcorn": {
"name": "sockermajs",
"plural_name": "sweetcorns"
"plural_name": "majs"
},
"sweeteners": {
"name": "sötningsmedel"
@@ -680,7 +680,7 @@
},
"yam": {
"name": "jams",
"plural_name": "yams"
"plural_name": "sötpotatisar"
},
"yeast": {
"name": "jäst"

View File

@@ -21,10 +21,10 @@
"name": "Drycker"
},
{
"name": "Bakade varor"
"name": "Bakverk"
},
{
"name": "Konserverade varor"
"name": "Konserver"
},
{
"name": "Smaktillsatser"

View File

@@ -91,8 +91,8 @@
"abbreviation": ""
},
"dash": {
"name": "1/8 de cuillère à café",
"plural_name": "1/8 de cuillères à café",
"name": "goutte",
"plural_name": "gouttes",
"description": "",
"abbreviation": ""
},
@@ -103,8 +103,8 @@
"abbreviation": ""
},
"head": {
"name": "tête",
"plural_name": "têtes",
"name": "personne",
"plural_name": "personnes",
"description": "",
"abbreviation": ""
},

View File

@@ -3,11 +3,11 @@
"name": "teaspoon",
"plural_name": "teaspoons",
"description": "",
"abbreviation": "culleradiña"
"abbreviation": "culleriña"
},
"tablespoon": {
"name": "tablespoon",
"plural_name": "tablespoons",
"name": "culler de sopa",
"plural_name": "culleres de sopa",
"description": "",
"abbreviation": "culleradas sopeiras"
},
@@ -36,33 +36,33 @@
"abbreviation": "qt"
},
"gallon": {
"name": "gallon",
"plural_name": "gallons",
"name": "galón",
"plural_name": "galóns",
"description": "",
"abbreviation": "gal"
},
"milliliter": {
"name": "milliliter",
"plural_name": "milliliters",
"name": "mililitro",
"plural_name": "mililitros",
"description": "",
"abbreviation": "ml"
},
"liter": {
"name": "liter",
"plural_name": "liters",
"name": "litro",
"plural_name": "litros",
"description": "",
"abbreviation": "l"
},
"pound": {
"name": "pound",
"plural_name": "pounds",
"name": "libra",
"plural_name": "libras",
"description": "",
"abbreviation": "lb",
"plural_abbreviation": "lbs"
},
"ounce": {
"name": "ounce",
"plural_name": "ounces",
"name": "onza",
"plural_name": "onzas",
"description": "",
"abbreviation": "oz"
},
@@ -109,14 +109,14 @@
"abbreviation": ""
},
"clove": {
"name": "clove",
"plural_name": "cloves",
"name": "dente",
"plural_name": "dentes",
"description": "",
"abbreviation": ""
},
"can": {
"name": "can",
"plural_name": "cans",
"name": "lata",
"plural_name": "latas",
"description": "",
"abbreviation": ""
},

View File

@@ -31,7 +31,7 @@
},
"quart": {
"name": "quart",
"plural_name": "quarts",
"plural_name": "quart",
"description": "",
"abbreviation": "qt"
},

View File

@@ -86,13 +86,13 @@
},
"splash": {
"name": "трохи",
"plural_name": "splashes",
"plural_name": "трохи",
"description": "",
"abbreviation": ""
},
"dash": {
"name": "трохи",
"plural_name": "dashes",
"plural_name": "трохи",
"description": "",
"abbreviation": ""
},

View File

@@ -9,6 +9,7 @@ from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import HttpRepo
from mealie.schema.household.group_shopping_list import (
ShoppingListAddRecipeParams,
ShoppingListAddRecipeParamsBulk,
ShoppingListCreate,
ShoppingListItemCreate,
ShoppingListItemOut,
@@ -252,17 +253,24 @@ class ShoppingListController(BaseCrudController):
return updated_list
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
def add_recipe_ingredients_to_list(
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListAddRecipeParams | None = None
):
shopping_list, items = self.service.add_recipe_ingredients_to_list(
item_id, recipe_id, data.recipe_increment_quantity if data else 1, data.recipe_ingredients if data else None
)
@router.post("/{item_id}/recipe", response_model=ShoppingListOut)
def add_recipe_ingredients_to_list(self, item_id: UUID4, data: list[ShoppingListAddRecipeParamsBulk]):
shopping_list, items = self.service.add_recipe_ingredients_to_list(item_id, data)
publish_list_item_events(self.publish_event, items)
return shopping_list
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut, deprecated=True)
def add_single_recipe_ingredients_to_list(
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListAddRecipeParams | None = None
):
# Compatibility function for old API
# TODO: remove this function in the future
data = data or ShoppingListAddRecipeParams(recipe_increment_quantity=1)
bulk_data = [data.cast(ShoppingListAddRecipeParamsBulk, recipe_id=recipe_id)]
return self.add_recipe_ingredients_to_list(item_id, bulk_data)
@router.post("/{item_id}/recipe/{recipe_id}/delete", response_model=ShoppingListOut)
def remove_recipe_ingredients_from_list(
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListRemoveRecipeParams | None = None

Some files were not shown because too many files have changed in this diff Show More