Compare commits

..

56 Commits

Author SHA1 Message Date
Hayden
13850cda1f security: multiple reported CVE fixes (#1515)
* update out of date license

* update typing / refactor

* fix arbitrarty path injection

* use markdown sanatizer to prevent XSS CWE-79

* fix CWE-918 SSRF by validating url and mime type

* add security docs

* update recipe-scrapers

* resolve DOS from arbitrary url

* update changelog

* bump version

* add ref to #1506

* add #1511 to changelog

* use requests decoder

* actually fix encoding issue
2022-07-31 13:10:20 -08:00
Michael Genson
483f789b8e feat: create new foods and units from their Data Management pages (#1511)
* added create dialogs to food and unit pages

* minor css tweaks

* properly reset create form

* added placeholder name attribute for type checking

* removed unnecessary value assignment

* type fixes

* corrected comment

* add autofocus and use ref<VForm> for form refs

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-07-31 12:31:20 -08:00
Michael Genson
1b83c82997 feat: implement local storage for sorting and dynamic sort icons on the new recipe sort card (#1506)
* added new sort icons

* added dynamic sort icons

* implemented local storage for sorting
and mobile card view

* fixed bug with local storage booleans

* added type hints

* bum vue use to use merge defaults

* use reactive localstorage

* add $vuetify type

* sort returns

* fix type error

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-07-31 11:39:35 -08:00
Philipp Fischbeck
34f52c06a6 fix: properly use pagination for group event notifiers (#1512) 2022-07-31 10:08:48 -08:00
Michael Genson
07fef8af9f feat: restore frontend sorting for all recipes (#1497)
* fixed typo

* merged "all recipes" pagination into recipe card
created custom sort card for all recipes
refactored backend calls for all recipes to sort/paginate

* frontend lint fixes

* restored recipes reference

* replaced "this" with reference

* fix linting errors

* re-order context menu

* add todo

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-07-26 18:08:56 -08:00
Michael Genson
703ee32653 docs: pagination and filtering, and fixed a few broken links (#1488)
* fixed broken links

* added docs for pagination and filtering

* small revision to pagination response example
2022-07-26 17:45:34 -08:00
Michael Genson
3d4e5441dd chore: backend page_all route cleanup (#1483)
* refactored to remove duplicate code

* refactored meal plan slice to use a query filter
2022-07-26 17:43:25 -08:00
Hayden
f00280e32b New Crowdin updates (#1480)
* New translations en-US.json (Dutch)

* New translations en-US.json (Czech)

* New translations en-US.json (German)
2022-07-26 17:41:33 -08:00
Hayden
9e6a720cf1 New Crowdin updates (#1455)
* New translations en-US.json (Swedish)

* New translations en-US.json (Swedish)

* New translations en-US.json (Swedish)

* New translations en-US.json (Lithuanian)

* New translations en-US.json (Lithuanian)

* New translations en-US.json (Lithuanian)

* New translations en-US.json (Lithuanian)

* New translations en-US.json (Lithuanian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Swedish)

* New translations en-US.json (Swedish)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (Slovenian)

* New translations en-US.json (French, Canada)

* New translations en-US.json (French, Canada)

* New translations en-US.json (French, Canada)
2022-07-09 21:17:34 -08:00
Michael Genson
7f50071312 feat: advanced filtering API (#1468)
* created query filter classes

* extended pagination to include query filtering

* added filtering tests

* type improvements

* move type help to dev depedency

* minor type and perf fixes

* breakup test cases

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-07-09 20:57:09 -08:00
Miroito
c64da1fdb7 Feature: Toggle display of ingredient references in recipe instructions (#1268)
* Better cooking mode

* Fix wrong event sent

* feat/cookmode recipe page integration

* implement scaling in cook mode + minor padding

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-07-09 20:28:34 -08:00
Michael Genson
2809cef3b1 fix: mealplan pagination (#1464)
* added pagination to get_slice route

* updated mealplan tests

* renamed vars to match pagination query
2022-07-02 09:44:01 -08:00
Benjamin Pabst
2f7ff6d178 fix: use mtime instead of ctime for backup dates (#1461) 2022-06-27 07:57:09 -08:00
Hayden
c05e048b65 docs: fix old link 2022-06-26 19:39:35 -08:00
Hayden
157bad0e29 fix: use == operator instead of is_ for sql queries (#1453) 2022-06-26 12:42:13 -08:00
Hayden
f96a584a5d New Crowdin updates (#1452)
* New translations en-US.json (French)

* New translations en-US.json (French)

* New translations en-US.json (French)

* New translations en-US.json (French)

* New translations en-US.json (Danish)

* New translations en-US.json (Italian)

* New translations en-US.json (Italian)

* New translations en-US.json (Italian)

* New translations en-US.json (Greek)

* New translations en-US.json (Greek)
2022-06-26 11:21:57 -08:00
Miroito
151e20489a ui: Improve parser ui text display (#1437)
move text display when open to be below the ingredient portion
2022-06-26 11:20:38 -08:00
Hayden
7dbb0858bd New Crowdin updates (#1439)
* New translations en-US.json (Dutch)

* New translations en-US.json (Czech)

* New translations en-US.json (Czech)

* New translations en-US.json (Czech)
2022-06-25 12:20:44 -08:00
Hayden
b921e95163 fix: entry nutrition checker (#1448) 2022-06-25 12:19:04 -08:00
Michael Genson
cb15db2d27 feat: re-write get all routes to use pagination (#1424)
rewrite get_all routes to use a pagination pattern to allow for better implementations of search, filter, and sorting on the frontend or by any client without fetching all the data. Additionally we added a CI check for running the Nuxt built to confirm that no TS errors were present. Finally, I had to remove the header support for the Shopping lists as the browser caching based off last_updated header was not allowing it to read recent updates due to how we're handling the updated_at property in the database with nested fields. This will have to be looked at in the future to reimplement. I'm unsure how many other routes have a similar issue. 

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-25 11:39:38 -08:00
Philipp Fischbeck
c158672d12 fix: add missing types for API token deletion (#1428) 2022-06-21 09:42:03 -08:00
Michael Genson
292bf7068a feat: added "last-modified" header to supported record types (#1379)
* fixed type error

* exposed created/updated timestamps to shopping list schema

* added custom route to mix in "last-modified" header when available in CRUD routes

* mixed in MealieCrudRoute to APIRouters

* added HEAD route for shopping lists/list-items

* replaced default serializer with FastAPI's
2022-06-21 09:41:14 -08:00
Hayden
5db4dedc3f hotfix: tame typescript beast 2022-06-20 16:48:39 -08:00
Hayden
f122c382e9 add recipe.image cache key to bush caches (#1427)
* add recipe.image cache key to bush caches

* hotfix: TS type error
2022-06-19 11:47:16 -08:00
Hayden
c865bc7769 fix: only show scaler when ingredients amounts enabled (#1426) 2022-06-19 10:27:32 -08:00
Michael Genson
efffe26a19 fix: sort recent recipes by created_at instead of date_added (#1417)
* added staticmethod decorators to avoid mypy error

* exposed created and updated timestamps to schema

* changed default sort from date_added to created_at

* explicitely sort recent recipes by created_at

* removed static method and replaced w/ type: ignore
2022-06-19 10:08:26 -08:00
Hayden
8b054fd945 New Crowdin updates (#1406)
* New translations en-US.json (Polish)

* New translations en-US.json (Polish)

* New translations en-US.json (Polish)

* New translations en-US.json (Polish)
2022-06-19 10:03:39 -08:00
Michael Genson
bb1fa52d10 fix: all-recipes page now sorts alphabetically (#1405)
* added sort params to backend call

* hardcoded alphabetical sort param

* removed trivial type annotation

* linters are friends, not food
2022-06-19 10:03:24 -08:00
Hayden
d4b92a8ade revert i18n version 2022-06-17 14:13:18 -08:00
dependabot[bot]
85d514eb1a chore(deps-dev): bump @vue/runtime-dom in /frontend (#1423)
Bumps [@vue/runtime-dom](https://github.com/vuejs/core/tree/HEAD/packages/runtime-dom) from 3.2.36 to 3.2.37.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits/v3.2.37/packages/runtime-dom)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 13:47:12 -08:00
dependabot[bot]
8878f78ab1 fix(deps): bump core-js from 3.17.2 to 3.23.1 in /frontend (#1383)
Bumps [core-js](https://github.com/zloirock/core-js) from 3.17.2 to 3.23.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.17.2...v3.23.1)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 13:45:55 -08:00
dependabot[bot]
d315ad63d2 fix(deps): bump fuse.js from 6.5.3 to 6.6.2 in /frontend (#1325)
Bumps [fuse.js](https://github.com/krisk/Fuse) from 6.5.3 to 6.6.2.
- [Release notes](https://github.com/krisk/Fuse/releases)
- [Changelog](https://github.com/krisk/Fuse/blob/master/CHANGELOG.md)
- [Commits](https://github.com/krisk/Fuse/compare/v6.5.3...v6.6.2)

---
updated-dependencies:
- dependency-name: fuse.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 13:45:24 -08:00
dependabot[bot]
48053b55b9 fix(deps): bump date-fns from 2.23.0 to 2.28.0 in /frontend (#1293)
Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.23.0 to 2.28.0.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v2.23.0...v2.28.0)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 13:36:33 -08:00
dependabot[bot]
78c7399ff7 fix(deps): bump @nuxtjs/i18n from 7.0.3 to 7.2.2 in /frontend (#1288)
Bumps [@nuxtjs/i18n](https://github.com/nuxt-community/i18n-module) from 7.0.3 to 7.2.2.
- [Release notes](https://github.com/nuxt-community/i18n-module/releases)
- [Changelog](https://github.com/nuxt-community/i18n-module/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nuxt-community/i18n-module/compare/v7.0.3...v7.2.2)

---
updated-dependencies:
- dependency-name: "@nuxtjs/i18n"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 13:35:21 -08:00
dependabot[bot]
f70fc18222 fix(deps): bump @mdi/js from 5.9.55 to 6.7.96 in /frontend (#1279)
Bumps [@mdi/js](https://github.com/Templarian/MaterialDesign-JS) from 5.9.55 to 6.7.96.
- [Release notes](https://github.com/Templarian/MaterialDesign-JS/releases)
- [Commits](https://github.com/Templarian/MaterialDesign-JS/compare/v5.9.55...v6.7.96)

---
updated-dependencies:
- dependency-name: "@mdi/js"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 13:35:08 -08:00
Hayden
6f83b0f522 chore: bump dev deps (#1418) 2022-06-17 13:34:22 -08:00
Hayden
5a053cdcd6 feat: mealplan-webhooks (#1403)
* fix type errors on event bus

* webhooks fields required for new implementation

* db migration

* wip: webhook query + tests and stub function

* ignore type checker error

* type and method cleanup

* datetime and time utc validator

* update testing code for utc scheduled time

* fix file cmp function call

* update version_number

* add support for translating "time" objects when restoring backup

* bump recipe-scrapers

* use specific import syntax

* generate frontend types

* utilize names exports

* use utc times

* add task to scheduler

* implement new scheduler functionality

* stub for type annotation

* implement meal-plan data getter

* add experimental banner
2022-06-17 13:25:47 -08:00
Hayden
b1256f4ad2 fix: fast fail of bulk importer (#1394)
* use continue instead of break

* catch additional error case

* spelling is hard
2022-06-15 18:19:52 -08:00
Hayden
525842e9a1 New Crowdin updates (#1392)
* New translations en-US.json (Ukrainian)

* New translations en-US.json (French)

* New translations en-US.json (Italian)

* New translations en-US.json (German)

* New translations en-US.json (Danish)

* New translations en-US.json (French, Canada)

* New translations en-US.json (Ukrainian)
2022-06-15 18:19:36 -08:00
Michael Genson
9e261f5235 fix: infinite scroll bug on all recipes page (#1393) 2022-06-15 12:56:56 -08:00
Jim Eagle
3f808f8f00 docs: add go bulk import example (#1388)
* Fix link
* Add go bulk import
2022-06-15 11:50:19 -08:00
Hayden
394df6c210 New Crowdin updates (#1375)
* New translations en-US.json (French)

* New translations en-US.json (French)
2022-06-15 11:50:01 -08:00
Michael Genson
754e77c9cb feat: extend Apprise JSON notification functionality with programmatic data (#1355)
* Fixed incorrect generic deleted notification text

* Added custom "event_source" header for json notifs

* Added internal reference data to event notifs

* Added event listeners to shopping list items

* Fixed type issues

* moved JSON event source k:v pairs to message body

* added hook for all supported custom endpoints
fixed bug that excluded non-custom notification types

* created event_source class to replace loosely-typed dict

* fixed silent error when dispatching a null task

* moved url updates to static function

* added unit tests for event_source url manipulation

* removed array from event bus (it's unsupported)
2022-06-15 11:49:42 -08:00
Hayden
3030e3e7f4 feat: implement user favorites page (#1376)
* fix geFavorites return

* add support for toggling to dense cards on desktop

* add favorites page link

* implement basic favorites page
2022-06-13 09:33:46 -08:00
Michael Genson
f6c18ec73d fix avoid page breaks in sections when printing recipes and other CSS tweaks (#1372)
* grouped ingredients and instructions into sections

* added missing import

* divided ingredient sections and instruction sections into their own containers

* tweaked css to prevent sections from getting split between pages

* replaced horizontal rule with a text underline

* removed leftover CSS

* implement computer properties as reducers

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-12 16:43:09 -08:00
Hayden
84dc60d7bf New translations en-US.json (Danish) (#1371) 2022-06-11 09:57:05 -08:00
Michael Genson
7541175b75 feat: implemented "order by" API parameters for recipe, food, and unit queries (#1356)
* Added API params to order by different properties

* fix for incorrect var name

* removed invalid default order_by

* implemented fallback for invalid user input
2022-06-11 09:56:55 -08:00
Hayden
932f4a72df refactor: remove depreciated repo call (#1370)
* ingredient parser hot fixes (float equality)

* remove `get` in favor of `get_one` & `multi_query`
2022-06-10 19:01:14 -08:00
Michael Genson
b904b161eb fix: increased float rounding precision for CRF parser (#1369)
* increased float rounding precision for crf parser

* limited fractions to a max denominator of 32 to prevent weirdly specific values

* add test cases for 1/8 and 1/32

* add rounding to avoid more digits than necessary

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-10 18:18:31 -08:00
Michael Genson
504bf41b9c fix: Ingredient sections lost after parsing (#1368)
* fixed bug where ingredient titles were lost after parsing

* added fallback in case of strange behavior during parsing

* removed unnecessary linebreak
2022-06-10 18:17:51 -08:00
Michael Genson
92ccbae657 fix: fixed text color on RecipeCard in RecipePrintView and implemented ingredient sections (#1351)
* Enhanced ingredients in RecipePrintView

* Resolved frontend lint tests

* switched lets to consts and simplified import

* implement with CSS grid

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-10 16:48:07 -08:00
Hayden
c0d59db83d New Crowdin updates (#1365)
* New translations en-US.json (French)

* New translations en-US.json (French)

* New translations en-US.json (French)
2022-06-10 16:38:19 -08:00
Hayden
511ce91630 New Crowdin updates (#1364)
* New translations en-US.json (German)

* New translations en-US.json (Korean)

* New translations en-US.json (English, United Kingdom)

* New translations en-US.json (Portuguese, Brazilian)

* New translations en-US.json (Vietnamese)

* New translations en-US.json (Chinese Traditional)

* New translations en-US.json (Chinese Simplified)

* New translations en-US.json (Turkish)

* New translations en-US.json (Swedish)

* New translations en-US.json (Serbian (Cyrillic))

* New translations en-US.json (Slovak)

* New translations en-US.json (Russian)

* New translations en-US.json (Portuguese)

* New translations en-US.json (Polish)

* New translations en-US.json (Norwegian)

* New translations en-US.json (Dutch)

* New translations en-US.json (Japanese)

* New translations en-US.json (French)

* New translations en-US.json (Hungarian)

* New translations en-US.json (Hebrew)

* New translations en-US.json (Finnish)

* New translations en-US.json (Greek)

* New translations en-US.json (Danish)

* New translations en-US.json (Czech)

* New translations en-US.json (Catalan)

* New translations en-US.json (Bulgarian)

* New translations en-US.json (Arabic)

* New translations en-US.json (Afrikaans)

* New translations en-US.json (Spanish)

* New translations en-US.json (Romanian)

* New translations en-US.json (Ukrainian)

* New translations en-US.json (Italian)

* New translations en-US.json (French, Canada)

* New translations en-US.json (Ukrainian)
2022-06-09 10:16:09 -08:00
Michael Genson
5f5eb2c46d fix: for erroneously-translated datetime config (#1362)
* Fix for erroneously-translated datetime config

* remove datetime formats from crowdin

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-09 08:54:41 -08:00
Michael Genson
4662253d0e Fixed alpha sort in RecipeOrganizerPage (#1354) 2022-06-09 08:50:03 -08:00
Benjamin Pabst
8836a258bd feat: add custom scaling option (#1345)
* Added custom scaling option

* Allow custom scaling with no yield set

* Made edit-scale translated

* fixed merge conflict

* Refactored scale editor to use menu

* replaced vslot with #

* linter issues

* fixed linter issues

* fixed one more linter issue

* format files + minor UI changes

* remove console.log

* move buttons into component and setup v-model

* drop servings text

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-09 08:01:25 -08:00
234 changed files with 6205 additions and 2005 deletions

View File

@@ -9,7 +9,7 @@ on:
- mealie-next
jobs:
ci:
lint:
runs-on: ${{ matrix.os }}
strategy:
@@ -47,3 +47,42 @@ jobs:
- name: Run linter 👀
run: yarn lint
working-directory: "frontend"
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
node: [16]
steps:
- name: Checkout 🛎
uses: actions/checkout@master
- name: Setup node env 🏗
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node }}
check-latest: true
- name: Get yarn cache directory path 🛠
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache node_modules 📦
uses: actions/cache@v2.1.4
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"

View File

@@ -42,7 +42,7 @@
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
- [Remember to join the Discord](https://discord.gg/QuStdQGSGK)!
- [Documentation](https://docs.mealie.io)
- [Documentation](https://nightly.mealie.io)
<!-- CONTRIBUTING -->
@@ -87,7 +87,7 @@ Thanks to Linode for providing Hosting for the Demo, Beta, and Documentation sit
[issues-shield]: https://img.shields.io/github/issues/hay-kot/mealie.svg?style=flat-square
[issues-url]: https://github.com/hay-kot/mealie/issues
[license-shield]: https://img.shields.io/github/license/hay-kot/mealie.svg?style=flat-square
[license-url]: https://github.com/hay-kot/mealie/blob/master/LICENSE.txt
[license-url]: https://github.com/hay-kot/mealie/blob/mealie-next/LICENSE
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
[linkedin-url]: https://linkedin.com/in/hay-kot
[product-screenshot]: docs/docs/assets/img/home_screenshot.png

View File

@@ -0,0 +1,31 @@
"""add new webhook fields
Revision ID: f30cf048c228
Revises: ab0bae02578f
Create Date: 2022-06-15 21:05:34.851857
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "f30cf048c228"
down_revision = "ab0bae02578f"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("webhook_urls", sa.Column("webhook_type", sa.String(), nullable=True))
op.add_column("webhook_urls", sa.Column("scheduled_time", sa.Time(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("webhook_urls", "scheduled_time")
op.drop_column("webhook_urls", "webhook_type")
# ### end Alembic commands ###

View File

@@ -2,8 +2,6 @@ preserve_hierarchy: false
files:
- source: /frontend/lang/messages/en-US.json
translation: /frontend/lang/messages/%locale%.json
- source: /frontend/lang/dateTimeFormats/en-US.json
translation: /frontend/lang/dateTimeFormats/%locale%.json
- source: /mealie/lang/messages/en-US.json
translation: /mealie/lang/messages/%locale%.json
- source: /mealie/repos/seed/resources/foods/locales/en-US.json

View File

@@ -1,30 +0,0 @@
# vx.x.x COOL TITLE GOES HERE
**App Version: vx.x.x**
**Database Version: vx.x.x**
## Breaking Changes
!!! error "Breaking Changes"
#### Database
#### ENV Variables
## Bug Fixes
- Fixed ...
## Features and Improvements
### General
- New Thing 1
### UI Improvements
-
### Behind the Scenes
- Refactoring...

View File

@@ -0,0 +1,126 @@
### Security
#### v1.0.0beta-3 and Under - Recipe Scraper: Server Side Request Forgery Lead To Denial Of Service
!!! error "CWE-918: Server-Side Request Forgery (SSRF)"
In this case if a attacker try to load a huge file then server will try to load the file and eventually server use its all memory which will dos the server
##### Mitigation
HTML is now scraped via a Stream and canceled after a 15 second timeout to prevent arbitrary data from being loaded into the server.
#### v1.0.0beta-3 and Under - Recipe Assets: Remote Code Execution
!!! error "CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine"
As a low privileged user, Create a new recipe and click on the "+" to add a New Asset.
Select a file, then proxy the request that will create the asset.
Since mealie/routes/recipe/recipe_crud_routes.py:306 is calling slugify on the name POST parameter, we use $ which slugify() will remove completely.
Since mealie/routes/recipe/recipe_crud_routes.py:306 is concatenating raw user input from the extension POST parameter into the variable file_name, which ultimately gets used when writing to disk, we can use a directory traversal attack in the extension (e.g. ./../../../tmp/pwn.txt) to write the file to arbitrary location on the server.
As an attacker, now that we have a strong attack primitive, we can start getting creative to get RCE. Since the files were being created by root, we could add an entry to /etc/passwd, create a crontab, etc. but since there was templating functionality in the application that peaked my interest. The PoC in the HTTP request above creates a Jinja2 template at /app/data/template/pwn.html. Since Jinja2 templates execute Python code when rendered, all we have to do now to get code execution is render the malicious template. This was easy enough.
##### Mitigation
We've added proper path sanitization to ensure that the user is not allowed to write to arbitrary locations on the server.
!!! warning "Breaking Change Incoming"
As this has shown a significant area of exposure in the templates that Mealie was provided for exporting recipes, we'll be removing this feature in the next Beta release and will instead rely on the community to provide tooling around transforming recipes using templates. This will significantly limit the possible exposure of users injecting malicious templates into the application. The template functionality will be completely removed in the next beta release v1.0.0beta-5
#### All version Markdown Editor: Cross Site Scripting
!!! error "CWE-79: Cross-site Scripting (XSS) - Stored"
A low privilege user can insert malicious JavaScript code into the Recipe Instructions which will execute in another person's browser that visits the recipe.
`<img src=x onerror=alert(document.domain)>`
##### Mitigation
This issues is present on all pages that allow markdown input. This error has been mitigated by wrapping the 3rd Party Markdown component and using the `domPurify` library to strip out the dangerous HTML.
#### v1.0.0beta-3 and Under - Image Scraper: Server-Side Request Forgery
!!! error "CWE-918: Server-Side Request Forgery (SSRF)"
In the recipe edit page, is possible to upload an image directly or via an URL provided by the user. The function that handles the fetching and saving of the image via the URL doesn't have any URL verification, which allows to fetch internal services.
Furthermore, after the resource is fetch, there is no MIME type validation, which would ensure that the resource is indeed an image. After this, because there is no extension in the provided URL, the application will fallback to jpg, and original for the image name.
Then the result is saved to disk with the original.jpg name, that can be retrieved from the following URL: http://<domain>/api/media/recipes/<recipe-uid>/images/original.jpg. This file will contain the full response of the provided URL.
**Impact**
An attacker can get sensitive information of any internal-only services running. For example, if the application is hosted on Amazon Web Services (AWS) platform, its possible to fetch the AWS API endpoint, https://169.254.169.254, which returns API keys and other sensitive metadata.
##### Mitigation
Two actions were taken to reduce exposure to SSRF in this case.
1. The application will not prevent requests being made to local resources by checking for localhost or 127.0.0.1 domain names.
2. The mime-type of the response is now checked prior to writing to disk.
If either of the above actions prevent the user from uploading images, the application will alert the user of what error occurred.
### Bug Fixes
- For erroneously-translated datetime config ([#1362](https://github.com/hay-kot/mealie/issues/1362))
- Fixed text color on RecipeCard in RecipePrintView and implemented ingredient sections ([#1351](https://github.com/hay-kot/mealie/issues/1351))
- Ingredient sections lost after parsing ([#1368](https://github.com/hay-kot/mealie/issues/1368))
- Increased float rounding precision for CRF parser ([#1369](https://github.com/hay-kot/mealie/issues/1369))
- Infinite scroll bug on all recipes page ([#1393](https://github.com/hay-kot/mealie/issues/1393))
- Fast fail of bulk importer ([#1394](https://github.com/hay-kot/mealie/issues/1394))
- Bump @mdi/js from 5.9.55 to 6.7.96 in /frontend ([#1279](https://github.com/hay-kot/mealie/issues/1279))
- Bump @nuxtjs/i18n from 7.0.3 to 7.2.2 in /frontend ([#1288](https://github.com/hay-kot/mealie/issues/1288))
- Bump date-fns from 2.23.0 to 2.28.0 in /frontend ([#1293](https://github.com/hay-kot/mealie/issues/1293))
- Bump fuse.js from 6.5.3 to 6.6.2 in /frontend ([#1325](https://github.com/hay-kot/mealie/issues/1325))
- Bump core-js from 3.17.2 to 3.23.1 in /frontend ([#1383](https://github.com/hay-kot/mealie/issues/1383))
- All-recipes page now sorts alphabetically ([#1405](https://github.com/hay-kot/mealie/issues/1405))
- Sort recent recipes by created_at instead of date_added ([#1417](https://github.com/hay-kot/mealie/issues/1417))
- Only show scaler when ingredients amounts enabled ([#1426](https://github.com/hay-kot/mealie/issues/1426))
- Add missing types for API token deletion ([#1428](https://github.com/hay-kot/mealie/issues/1428))
- Entry nutrition checker ([#1448](https://github.com/hay-kot/mealie/issues/1448))
- Use == operator instead of is_ for sql queries ([#1453](https://github.com/hay-kot/mealie/issues/1453))
- Use `mtime` instead of `ctime` for backup dates ([#1461](https://github.com/hay-kot/mealie/issues/1461))
- Mealplan pagination ([#1464](https://github.com/hay-kot/mealie/issues/1464))
- Properly use pagination for group event notifies ([#1512](https://github.com/hay-kot/mealie/pull/1512))
### Documentation
- Add go bulk import example ([#1388](https://github.com/hay-kot/mealie/issues/1388))
- Fix old link
- Pagination and filtering, and fixed a few broken links ([#1488](https://github.com/hay-kot/mealie/issues/1488))
### Features
- Toggle display of ingredient references in recipe instructions ([#1268](https://github.com/hay-kot/mealie/issues/1268))
- Add custom scaling option ([#1345](https://github.com/hay-kot/mealie/issues/1345))
- Implemented "order by" API parameters for recipe, food, and unit queries ([#1356](https://github.com/hay-kot/mealie/issues/1356))
- Implement user favorites page ([#1376](https://github.com/hay-kot/mealie/issues/1376))
- Extend Apprise JSON notification functionality with programmatic data ([#1355](https://github.com/hay-kot/mealie/issues/1355))
- Mealplan-webhooks ([#1403](https://github.com/hay-kot/mealie/issues/1403))
- Added "last-modified" header to supported record types ([#1379](https://github.com/hay-kot/mealie/issues/1379))
- Re-write get all routes to use pagination ([#1424](https://github.com/hay-kot/mealie/issues/1424))
- Advanced filtering API ([#1468](https://github.com/hay-kot/mealie/issues/1468))
- Restore frontend sorting for all recipes ([#1497](https://github.com/hay-kot/mealie/issues/1497))
- Implemented local storage for sorting and dynamic sort icons on the new recipe sort card ([1506](https://github.com/hay-kot/mealie/pull/1506))
- create new foods and units from their Data Management pages ([#1511](https://github.com/hay-kot/mealie/pull/1511))
### Miscellaneous Tasks
- Bump dev deps ([#1418](https://github.com/hay-kot/mealie/issues/1418))
- Bump @vue/runtime-dom in /frontend ([#1423](https://github.com/hay-kot/mealie/issues/1423))
- Backend page_all route cleanup ([#1483](https://github.com/hay-kot/mealie/issues/1483))
### Refactor
- Remove depreciated repo call ([#1370](https://github.com/hay-kot/mealie/issues/1370))
### Hotfix
- Tame typescript beast
### UI
- Improve parser ui text display ([#1437](https://github.com/hay-kot/mealie/issues/1437))
<!-- generated by git-cliff -->

View File

@@ -43,6 +43,9 @@ import_from_file $input $token $mealie_url
```
#### Go
See <a href="https://github.com/Jleagle/mealie-importer" target="_blank">Jleagle/mealie-importer</a>.
#### Python
```python
import requests

View File

@@ -11,7 +11,67 @@ Mealie supports long-live api tokens in the user frontend. See [user settings pa
On your local installation you can access interactive API documentation that provides `curl` examples and expected results. This allows you to easily test and interact with your API to identify places to include your own functionality. You can visit the documentation at `http://mealie.yourdomain.com/docs` or see the example at the [Demo Site](https://mealie-demo.hay-kot.dev/docs)
### Recipe Extras
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 part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device.
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 part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device.
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
### Pagination and Filtering
Most document types share a uniform pagination and filtering API (e.g. `GET /api/recipes`). These allow you to filter by an arbitrary combination of criteria and return only a certain number of documents (i.e. a single "page" of documents).
#### Pagination
The pagination API allows you to limit how many documents you return in each call. This is important when serving data to an application, as you don't want to wait for a huge payload every time you load a page. You may also not want to render all documents at once, opting to render only a few at a time.
The `perPage` parameter tells Mealie how many documents to return (this is similar to `LIMIT` in SQL). If you want to keep fetching more data in batches, first determine your batch size (in other words: how many documents you want per-page), then make additional calls by changing the `page` parameter. If your `perPage` size is 30, then page 1 will return the first 30 documents, page 2 will return the next 30 documents, etc.
Many applications will keep track of the query and adjust the page parameter appropriately, but some applications can't do this, or a particular implementation may make this difficult. The response includes pagination guides to help you find the next page and previous page. Here is a sample response:
```json
{
"page": 2,
"per_page": 5,
"total": 23,
"total_pages": 5,
"data": [...],
"next": "/recipes?page=3&per_page=5&order_by=name&order_direction=asc",
"previous": "/recipes?page=1&per_page=5&order_by=name&order_direction=asc"
}
```
Notice that the route does not contain the baseurl (e.g. `https://mymealieapplication.com/api`).
There are a few shorthands available to reduce the number of calls for certain common requests:
- if you want to return _all_ results, effectively disabling pagination, set `perPage = -1` (and fetch the first page)
- if you want to fetch the _last_ page, set `page = -1`
#### Filtering
The `queryFilter` parameter enables fine-grained control over your query. You can filter by any combination of attributes connected by logical operators (`AND`, `OR`). You can also group attributes together using parenthesis. For string, date, or datetime literals, you should surround them in double quotes (e.g. `"Pasta Fagioli"`). If there are no spaces in your literal (such as dates) the API will probably parse it correctly, but it's recommended that you use quotes anyway.
Here are several examples of filters. These filter strings are not surrounded in quotes for ease of reading, but they are _strings_, so they will probably be in quotes in your language.
##### Simple Filters
Here is an example of a filter to find a recipe with the name "Pasta Fagioli": <br>
`name = "Pasta Fagioli"`
This filter will find all recipes created on or after a particular date: <br>
`createdAt >= "2021-02-22"`
> **_NOTE:_** The API uses Python's [dateutil parser](https://dateutil.readthedocs.io/en/stable/parser.html), which parses many different date/datetime formats.
This filter will find all units that have `useAbbreviation` disabled: <br>
`useAbbreviation = false`
##### Compound Filters
You can combine multiple filter statements using logical operators (`AND`, `OR`).
This filter will only return recipes named "Pasta Fagioli" or "Grandma's Brisket": <br>
`name = "Pasta Fagioli" OR name = "Grandma's Brisket"`
This filter will return all recipes created before a particular date, except for the one named "Ultimate Vegan Ramen Recipe With Miso Broth": <br>
`createdAt < "January 2nd, 2014" AND name <> "Ultimate Vegan Ramen Recipe With Miso Broth"`
This filter will return three particular recipes: <br>
`name = "Pasta Fagioli" OR name = "Grandma's Brisket" OR name = "Ultimate Vegan Ramen Recipe With Miso Broth"`
##### Advanced Filters
You can have multiple filter groups combined by logical operators. You can define a filter group with parenthesis.
Here's a filter that will find all recipes updated between two particular times, but exclude the "Pasta Fagioli" recipe: <br>
`(updatedAt > "2022-07-17T15:47:00Z" AND updatedAt < "2022-07-17T15:50:00Z") AND name <> "Pasta Fagioli"`

View File

@@ -2,15 +2,15 @@
**For Environmental Variable Configuration See:**
- [Frontend Configuration](/mealie/documentation/getting-started/installation/frontend-config/)
- [Backend Configuration](/mealie/documentation/getting-started/installation/backend-config/)
- [Frontend Configuration](./frontend-config.md)
- [Backend Configuration](./backend-config.md)
```yaml
---
version: "3.7"
services:
mealie-frontend:
image: hkotel/mealie:frontend-v1.0.0beta-3
image: hkotel/mealie:frontend-v1.0.0beta-4
container_name: mealie-frontend
depends_on:
- mealie-api
@@ -23,7 +23,7 @@ services:
volumes:
- mealie-data:/app/data/ # (3)
mealie-api:
image: hkotel/mealie:api-v1.0.0beta-3
image: hkotel/mealie:api-v1.0.0beta-4
container_name: mealie-api
depends_on:
- postgres

View File

@@ -4,15 +4,15 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
**For Environmental Variable Configuration See:**
- [Frontend Configuration](/mealie/documentation/getting-started/installation/frontend-config/)
- [Backend Configuration](/mealie/documentation/getting-started/installation/backend-config/)
- [Frontend Configuration](./frontend-config.md)
- [Backend Configuration](./backend-config.md)
```yaml
---
version: "3.7"
services:
mealie-frontend:
image: hkotel/mealie:frontend-v1.0.0beta-3
image: hkotel/mealie:frontend-v1.0.0beta-4
container_name: mealie-frontend
environment:
# Set Frontend ENV Variables Here
@@ -23,7 +23,7 @@ services:
volumes:
- mealie-data:/app/data/ # (3)
mealie-api:
image: hkotel/mealie:api-v1.0.0beta-3
image: hkotel/mealie:api-v1.0.0beta-4
container_name: mealie-api
volumes:
- mealie-data:/app/data/

File diff suppressed because one or more lines are too long

View File

@@ -88,6 +88,7 @@ nav:
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
- Change Log:
- v1.0.0beta-4: "changelog/v1.0.0beta-4.md"
- v1.0.0beta-3: "changelog/v1.0.0beta-3.md"
- v1.0.0beta-2: "changelog/v1.0.0beta-2.md"
- v1.0.0 Beta: "changelog/v1.0.0.md"

View File

@@ -1,4 +1,4 @@
import { ApiRequestInstance } from "~/types/api";
import { ApiRequestInstance, PaginationData } from "~/types/api";
export interface CrudAPIInterface {
requests: ApiRequestInstance;
@@ -18,13 +18,13 @@ export abstract class BaseAPI {
}
}
export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType=CreateType> extends BaseAPI implements CrudAPIInterface {
export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType = CreateType> extends BaseAPI implements CrudAPIInterface {
abstract baseRoute: string;
abstract itemRoute(itemId: string | number): string;
async getAll(start = 0, limit = 9999, params = {} as any) {
return await this.requests.get<ReadType[]>(this.baseRoute, {
params: { start, limit, ...params },
async getAll(page = 1, perPage = -1, params = {} as any) {
return await this.requests.get<PaginationData<ReadType>>(this.baseRoute, {
params: { page, perPage, ...params },
});
}

View File

@@ -1,5 +1,15 @@
import { BaseCRUDAPI } from "../_base";
import { ChangePassword, DeleteTokenResponse, LongLiveTokenIn, LongLiveTokenOut, ResetPassword, UserBase, UserIn, UserOut } from "~/types/api-types/user";
import {
ChangePassword,
DeleteTokenResponse,
LongLiveTokenIn,
LongLiveTokenOut,
ResetPassword,
UserBase,
UserFavorites,
UserIn,
UserOut,
} from "~/types/api-types/user";
const prefix = "/api";
@@ -32,7 +42,7 @@ export class UserApi extends BaseCRUDAPI<UserIn, UserOut, UserBase> {
}
async getFavorites(id: string) {
await this.requests.get(routes.usersIdFavorites(id));
return await this.requests.get<UserFavorites>(routes.usersIdFavorites(id));
}
async changePassword(id: string, changePassword: ChangePassword) {

View File

@@ -0,0 +1,84 @@
<template>
<div>
<v-card-text>
<v-switch v-model="webhookCopy.enabled" label="Enabled"></v-switch>
<v-text-field v-model="webhookCopy.name" label="Webhook Name"></v-text-field>
<v-text-field v-model="webhookCopy.url" label="Webhook Url"></v-text-field>
<v-time-picker v-model="scheduledTime" class="elevation-2" ampm-in-title format="ampm"></v-time-picker>
</v-card-text>
<v-card-actions class="py-0 justify-end">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $tc('general.delete'),
event: 'delete',
},
{
icon: $globals.icons.testTube,
text: $tc('general.test'),
event: 'test',
},
{
icon: $globals.icons.save,
text: $tc('general.save'),
event: 'save',
},
]"
@delete="$emit('delete', webhookCopy.id)"
@save="handleSave"
@test="$emit('test', webhookCopy.id)"
/>
</v-card-actions>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
import { ReadWebhook } from "~/types/api-types/group";
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
export default defineComponent({
props: {
webhook: {
type: Object as () => ReadWebhook,
required: true,
},
},
emits: ["delete", "save", "test"],
setup(props, { emit }) {
const itemUTC = ref<string>(props.webhook.scheduledTime);
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
const scheduledTime = computed({
get() {
return itemLocal.value;
},
set(v: string) {
itemUTC.value = timeLocalToUTC(v);
itemLocal.value = v;
},
});
const webhookCopy = ref({ ...props.webhook });
function handleSave() {
webhookCopy.value.scheduledTime = itemLocal.value;
emit("save", webhookCopy.value);
}
return {
webhookCopy,
scheduledTime,
handleSave,
itemUTC,
itemLocal,
};
},
head() {
return {
title: this.$tc("settings.webhooks.webhooks"),
};
},
});
</script>

View File

@@ -24,7 +24,7 @@
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :slug="slug" show-always />
<v-tooltip v-if="!locked" bottom color="info">
<template #activator="{ on, attrs }">
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('input', true)">
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
<v-icon> {{ $globals.icons.edit }} </v-icon>
</v-btn>
</template>

View File

@@ -14,6 +14,7 @@
</v-icon>
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.random") }}
</v-btn>
<v-menu v-if="$listeners.sort" offset-y left>
<template #activator="{ on, attrs }">
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
@@ -23,6 +24,48 @@
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
</v-btn>
</template>
<v-list>
<v-list-item @click="sortRecipesFrontend(EVENTS.az)">
<v-icon left>
{{ $globals.icons.orderAlphabeticalAscending }}
</v-icon>
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.rating)">
<v-icon left>
{{ $globals.icons.star }}
</v-icon>
<v-list-item-title>{{ $t("general.rating") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.created)">
<v-icon left>
{{ $globals.icons.newBox }}
</v-icon>
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.updated)">
<v-icon left>
{{ $globals.icons.update }}
</v-icon>
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipesFrontend(EVENTS.shuffle)">
<v-icon left>
{{ $globals.icons.shuffleVariant }}
</v-icon>
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu v-if="$listeners.sortRecipes" offset-y left>
<template #activator="{ on, attrs }">
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
<v-icon :left="!$vuetify.breakpoint.xsOnly">
{{ preferences.sortIcon }}
</v-icon>
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
</v-btn>
</template>
<v-list>
<v-list-item @click="sortRecipes(EVENTS.az)">
<v-icon left>
@@ -48,17 +91,22 @@
</v-icon>
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipes(EVENTS.shuffle)">
<v-icon left>
{{ $globals.icons.shuffleVariant }}
</v-icon>
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<ContextMenu
v-if="!$vuetify.breakpoint.xsOnly"
:items="[
{
title: 'Toggle View',
icon: $globals.icons.eye,
event: 'toggle-dense-view',
},
]"
@toggle-dense-view="toggleMobileCards()"
/>
</v-app-bar>
<div v-if="recipes" class="mt-2">
<v-row v-if="!viewScale">
<v-row v-if="!useMobileCards">
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
<v-lazy>
<RecipeCard
@@ -99,17 +147,38 @@
</v-col>
</v-row>
</div>
<div v-if="usePagination">
<v-card v-intersect="infiniteScroll"></v-card>
<v-fade-transition>
<AppLoader v-if="loading" :loading="loading" />
</v-fade-transition>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
import {
computed,
defineComponent,
onMounted,
reactive,
ref,
toRefs,
useAsync,
useContext,
useRouter,
} from "@nuxtjs/composition-api";
import { useThrottleFn } from "@vueuse/core";
import RecipeCard from "./RecipeCard.vue";
import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useSorter } from "~/composables/recipes";
import {Recipe} from "~/types/api-types/recipe";
import { useAsyncKey } from "~/composables/use-utils";
import { useLazyRecipes, useSorter } from "~/composables/recipes";
import { Recipe } from "~/types/api-types/recipe";
import { useUserSortPreferences } from "~/composables/use-users/preferences";
const SORT_EVENT = "sort";
const REPLACE_RECIPES_EVENT = "replaceRecipes";
const APPEND_RECIPES_EVENT = "appendRecipes";
export default defineComponent({
components: {
@@ -129,10 +198,6 @@ export default defineComponent({
type: String,
default: null,
},
mobileCards: {
type: Boolean,
default: false,
},
singleColumn: {
type: Boolean,
default: false,
@@ -141,8 +206,14 @@ export default defineComponent({
type: Array as () => Recipe[],
default: () => [],
},
usePagination: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const preferences = useUserSortPreferences();
const utils = useSorter();
const EVENTS = {
@@ -154,8 +225,8 @@ export default defineComponent({
};
const { $globals, $vuetify } = useContext();
const viewScale = computed(() => {
return props.mobileCards || $vuetify.breakpoint.smAndDown;
const useMobileCards = computed(() => {
return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards;
});
const displayTitleIcon = computed(() => {
@@ -164,7 +235,7 @@ export default defineComponent({
const state = reactive({
sortLoading: false,
})
});
const router = useRouter();
function navigateRandom() {
@@ -176,7 +247,114 @@ export default defineComponent({
}
}
const page = ref(1);
const perPage = ref(30);
const hasMore = ref(true);
const ready = ref(false);
const loading = ref(false);
const { fetchMore } = useLazyRecipes();
onMounted(async () => {
if (props.usePagination) {
const newRecipes = await fetchMore(
page.value,
perPage.value,
preferences.value.orderBy,
preferences.value.orderDirection
);
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
ready.value = true;
}
});
const infiniteScroll = useThrottleFn(() => {
useAsync(async () => {
if (!ready.value || !hasMore.value || loading.value) {
return;
}
loading.value = true;
page.value = page.value + 1;
const newRecipes = await fetchMore(
page.value,
perPage.value,
preferences.value.orderBy,
preferences.value.orderDirection
);
if (!newRecipes.length) {
hasMore.value = false;
} else {
context.emit(APPEND_RECIPES_EVENT, newRecipes);
}
loading.value = false;
}, useAsyncKey());
}, 500);
/**
* sortRecipes helps filter using the API. This will eventually replace the sortRecipesFrontend function which pulls all recipes
* (without pagination) and does the sorting in the frontend.
* TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above)
* @param sortType
*/
function sortRecipes(sortType: string) {
if (state.sortLoading || loading.value) {
return;
}
function setter(orderBy: string, ascIcon: string, descIcon: string) {
if (preferences.value.orderBy !== orderBy) {
preferences.value.orderBy = orderBy;
preferences.value.orderDirection = "asc";
} else {
preferences.value.orderDirection = preferences.value.orderDirection === "asc" ? "desc" : "asc";
}
preferences.value.sortIcon = preferences.value.orderDirection === "asc" ? ascIcon : descIcon;
}
switch (sortType) {
case EVENTS.az:
setter("name", $globals.icons.sortAlphabeticalAscending, $globals.icons.sortAlphabeticalDescending);
break;
case EVENTS.rating:
setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending);
break;
case EVENTS.created:
setter("created_at", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending);
break;
case EVENTS.updated:
setter("updated_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending);
break;
default:
console.log("Unknown Event", sortType);
return;
}
useAsync(async () => {
// reset pagination
page.value = 1;
hasMore.value = true;
state.sortLoading = true;
loading.value = true;
// fetch new recipes
const newRecipes = await fetchMore(
page.value,
perPage.value,
preferences.value.orderBy,
preferences.value.orderDirection
);
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
state.sortLoading = false;
loading.value = false;
}, useAsyncKey());
}
function sortRecipesFrontend(sortType: string) {
state.sortLoading = true;
const sortTarget = [...props.recipes];
switch (sortType) {
@@ -203,13 +381,22 @@ export default defineComponent({
state.sortLoading = false;
}
function toggleMobileCards() {
preferences.value.useMobileCards = !preferences.value.useMobileCards;
}
return {
...toRefs(state),
EVENTS,
viewScale,
displayTitleIcon,
EVENTS,
infiniteScroll,
loading,
navigateRandom,
preferences,
sortRecipes,
sortRecipesFrontend,
toggleMobileCards,
useMobileCards,
};
},
});

View File

@@ -261,7 +261,7 @@ export default defineComponent({
async function getShoppingLists() {
const { data } = await api.shopping.lists.getAll();
if (data) {
shoppingLists.value = data;
shoppingLists.value = data.items ?? [];
}
}

View File

@@ -131,10 +131,10 @@ export default defineComponent({
}
async function refreshTokens() {
const { data } = await userApi.recipes.share.getAll(0, 999, { recipe_id: props.recipeId });
const { data } = await userApi.recipes.share.getAll(1, -1, { recipe_id: props.recipeId });
if (data) {
state.tokens = data;
state.tokens = data.items ?? [];
}
}

View File

@@ -11,7 +11,7 @@
<v-list-item dense @click="toggleChecked(index)">
<v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary" />
<v-list-item-content :key="ingredient.quantity">
<VueMarkdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredientDisplay[index]" />
<SafeMarkdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredientDisplay[index]" />
</v-list-item-content>
</v-list-item>
</div>
@@ -22,14 +22,11 @@
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from "@nuxtjs/composition-api";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { parseIngredientText } from "~/composables/recipes";
import { RecipeIngredient } from "~/types/api-types/recipe";
export default defineComponent({
components: {
VueMarkdown,
},
components: {},
props: {
value: {
type: Array as () => RecipeIngredient[],

View File

@@ -58,13 +58,13 @@
</v-card>
</v-dialog>
<div class="d-flex justify-space-between justify-start">
<div v-if="showCookMode" class="d-flex justify-space-between justify-start">
<h2 class="mb-4 mt-1">{{ $t("recipe.instructions") }}</h2>
<BaseButton v-if="!public" minor :to="$router.currentRoute.path + '/cook'" cancel color="primary">
<BaseButton v-if="!public && !edit" minor cancel color="primary" @click="toggleCookMode()">
<template #icon>
{{ $globals.icons.primary }}
</template>
Cook
Cook Mode
</BaseButton>
</div>
<draggable
@@ -197,7 +197,15 @@
<v-expand-transition>
<div v-show="!isChecked(index) && !edit" class="m-0 p-0">
<v-card-text class="markdown">
<VueMarkdown class="markdown" :source="step.text"> </VueMarkdown>
<SafeMarkdown class="markdown" :source="step.text" />
<div v-if="cookMode && step.ingredientReferences && step.ingredientReferences.length > 0">
<v-divider class="mb-2"></v-divider>
<div
v-for="ing in step.ingredientReferences"
:key="ing.referenceId"
v-html="getIngredientByRefId(ing.referenceId)"
/>
</div>
</v-card-text>
</div>
</v-expand-transition>
@@ -211,9 +219,16 @@
<script lang="ts">
import draggable from "vuedraggable";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { ref, toRefs, reactive, defineComponent, watch, onMounted, useContext } from "@nuxtjs/composition-api";
import {
ref,
toRefs,
reactive,
defineComponent,
watch,
onMounted,
useContext,
computed,
} from "@nuxtjs/composition-api";
import { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset } from "~/types/api-types/recipe";
import { parseIngredientText } from "~/composables/recipes";
import { uuid4, detectServerBaseUrl } from "~/composables/use-utils";
@@ -228,7 +243,6 @@ interface MergerHistory {
export default defineComponent({
components: {
VueMarkdown,
draggable,
},
props: {
@@ -264,6 +278,14 @@ export default defineComponent({
type: Array as () => RecipeAsset[],
required: true,
},
cookMode: {
type: Boolean,
default: false,
},
scale: {
type: Number,
default: 1,
},
},
setup(props, context) {
@@ -313,13 +335,20 @@ export default defineComponent({
});
});
const showCookMode = ref(false);
// Eliminate state with an eager call to watcher?
onMounted(() => {
props.value.forEach((element) => {
props.value.forEach((element: RecipeStep) => {
if (element.id !== undefined) {
showTitleEditor.value[element.id] = validateTitle(element.title);
}
// showCookMode.value = false;
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
showCookMode.value = true;
}
showTitleEditor.value = { ...showTitleEditor.value };
});
});
@@ -376,6 +405,14 @@ export default defineComponent({
referenceId: ref,
};
});
// Update the visibility of the cook mode button
showCookMode.value = false;
props.value.forEach((element) => {
if (showCookMode.value === false && element.ingredientReferences && element.ingredientReferences.length > 0) {
showCookMode.value = true;
}
});
state.dialog = false;
}
@@ -446,12 +483,27 @@ export default defineComponent({
});
}
function getIngredientByRefId(refId: string) {
const ing = props.ingredients.find((ing) => ing.referenceId === refId) || "";
const ingredientLookup = computed(() => {
const results: { [key: string]: RecipeIngredient } = {};
return props.ingredients.reduce((prev, ing) => {
if (ing.referenceId === undefined) {
return prev;
}
prev[ing.referenceId] = ing;
return prev;
}, results);
});
function getIngredientByRefId(refId: string | undefined) {
if (refId === undefined) {
return "";
}
const ing = ingredientLookup.value[refId] ?? "";
if (ing === "") {
return "";
}
return parseIngredientText(ing, props.disableAmount);
return parseIngredientText(ing, props.disableAmount, props.scale);
}
// ===============================================================
@@ -571,6 +623,10 @@ export default defineComponent({
props.value[index].text += text;
}
function toggleCookMode() {
context.emit("cookModeToggle");
}
return {
// Image Uploader
toggleDragMode,
@@ -598,6 +654,8 @@ export default defineComponent({
updateIndex,
autoSetReferences,
parseIngredientText,
toggleCookMode,
showCookMode,
};
},
});

View File

@@ -18,7 +18,7 @@
{{ note.title }}
</v-card-title>
<v-card-text>
<VueMarkdown :source="note.text"> </VueMarkdown>
<SafeMarkdown :source="note.text" />
</v-card-text>
</div>
</div>
@@ -30,15 +30,10 @@
</template>
<script lang="ts">
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { defineComponent } from "@nuxtjs/composition-api";
import { RecipeNote } from "~/types/api-types/recipe";
export default defineComponent({
components: {
VueMarkdown,
},
props: {
value: {
type: Array as () => RecipeNote[],

View File

@@ -81,9 +81,12 @@ export default defineComponent({
},
};
const valueNotNull = computed(() => {
Object.values(props.value).forEach((valueProperty) => {
if (valueProperty && valueProperty !== "") return true;
});
let key: keyof Nutrition;
for (key in props.value) {
if (props.value[key] !== null) {
return true;
}
}
return false;
});
@@ -97,8 +100,8 @@ export default defineComponent({
labels,
valueNotNull,
showViewer,
updateValue
}
updateValue,
};
},
});
</script>

View File

@@ -84,7 +84,7 @@ export default defineComponent({
if (!props.items) return byLetter;
props.items.forEach((item) => {
props.items.sort((a, b) => a.name.localeCompare(b.name)).forEach((item) => {
const letter = item.name[0].toUpperCase();
if (!byLetter[letter]) {
byLetter[letter] = [];
@@ -92,12 +92,6 @@ export default defineComponent({
byLetter[letter].push(item);
});
for (const key in byLetter) {
byLetter[key] = byLetter[key].sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
return byLetter;
});

View File

@@ -11,42 +11,57 @@
</section>
<v-card-text class="px-0">
<VueMarkdown :source="recipe.description" />
<SafeMarkdown :source="recipe.description" />
</v-card-text>
<!-- Ingredients -->
<section>
<v-card-title class="headline pl-0"> {{ $t("recipe.ingredients") }} </v-card-title>
<div class="ingredient-grid">
<div class="ingredient-col-1">
<ul>
<li v-for="(text, index) in splitIngredients.firstHalf" :key="index" v-html="text" />
</ul>
</div>
<div class="ingredient-col-2">
<ul>
<li v-for="(text, index) in splitIngredients.secondHalf" :key="index" v-html="text" />
</ul>
<div
v-for="(ingredientSection, sectionIndex) in ingredientSections"
:key="`ingredient-section-${sectionIndex}`"
class="print-section"
>
<div class="ingredient-grid">
<template v-for="(ingredient, ingredientIndex) in ingredientSection.ingredients">
<h4 v-if="ingredient.title" :key="`ingredient-title-${ingredientIndex}`" class="ingredient-title mt-2">
{{ ingredient.title }}
</h4>
<p :key="`ingredient-${ingredientIndex}`" class="ingredient-body" v-html="parseText(ingredient)" />
</template>
</div>
</div>
</section>
<!-- Instructions -->
<section>
<v-card-title class="headline pl-0">{{ $t("recipe.instructions") }}</v-card-title>
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
<h3 v-if="step.title" class="mb-2">{{ step.title }}</h3>
<div class="ml-5">
<h4>{{ $t("recipe.step-index", { step: index + 1 }) }}</h4>
<VueMarkdown :source="step.text" />
<div
v-for="(instructionSection, sectionIndex) in instructionSections"
:key="`instruction-section-${sectionIndex}`"
:class="{ 'print-section': instructionSection.sectionName }"
>
<div v-for="(step, stepIndex) in instructionSection.instructions" :key="`instruction-${stepIndex}`">
<div class="print-section">
<h4 v-if="step.title" :key="`instruction-title-${stepIndex}`" class="instruction-title mb-2">
{{ step.title }}
</h4>
<h5>{{ $t("recipe.step-index", { step: stepIndex + instructionSection.stepOffset + 1 }) }}</h5>
<SafeMarkdown :source="step.text" class="recipe-step-body" />
</div>
</div>
</div>
</section>
<!-- Notes -->
<v-divider v-if="hasNotes" class="grey my-4"></v-divider>
<section>
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
<h4>{{ note.title }}</h4>
<VueMarkdown :source="note.text" />
<div class="print-section">
<h4>{{ note.title }}</h4>
<SafeMarkdown :source="note.text" class="note-body" />
</div>
</div>
</section>
</div>
@@ -54,21 +69,24 @@
<script lang="ts">
import { defineComponent, computed } from "@nuxtjs/composition-api";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import { Recipe } from "~/types/api-types/recipe";
import { Recipe, RecipeIngredient, RecipeStep } from "~/types/api-types/recipe";
import { parseIngredientText } from "~/composables/recipes";
type SplitIngredients = {
firstHalf: string[];
secondHalf: string[];
type IngredientSection = {
sectionName: string;
ingredients: RecipeIngredient[];
};
type InstructionSection = {
sectionName: string;
stepOffset: number;
instructions: RecipeStep[];
};
export default defineComponent({
components: {
RecipeTimeCard,
VueMarkdown,
},
props: {
recipe: {
@@ -77,33 +95,98 @@ export default defineComponent({
},
},
setup(props) {
const splitIngredients = computed<SplitIngredients>(() => {
const firstHalf = props.recipe.recipeIngredient
?.slice(0, Math.ceil(props.recipe.recipeIngredient.length / 2))
.map((ingredient) => {
return parseIngredientText(ingredient, props.recipe?.settings?.disableAmount || false);
});
// Group ingredients by section so we can style them independently
const ingredientSections = computed<IngredientSection[]>(() => {
if (!props.recipe.recipeIngredient) {
return [];
}
const secondHalf = props.recipe.recipeIngredient
?.slice(Math.ceil(props.recipe.recipeIngredient.length / 2))
.map((ingredient) => {
return parseIngredientText(ingredient, props.recipe?.settings?.disableAmount || false);
});
return props.recipe.recipeIngredient.reduce((sections, ingredient) => {
// if title append new section to the end of the array
if (ingredient.title) {
sections.push({
sectionName: ingredient.title,
ingredients: [ingredient],
});
return {
firstHalf: firstHalf || [],
secondHalf: secondHalf || [],
};
return sections;
}
// append new section if first
if (sections.length === 0) {
sections.push({
sectionName: "",
ingredients: [ingredient],
});
return sections;
}
// otherwise add ingredient to last section in the array
sections[sections.length - 1].ingredients.push(ingredient);
return sections;
}, [] as IngredientSection[]);
});
// Group instructions by section so we can style them independently
const instructionSections = computed<InstructionSection[]>(() => {
if (!props.recipe.recipeInstructions) {
return [];
}
return props.recipe.recipeInstructions.reduce((sections, step) => {
const offset = (() => {
if (sections.length === 0) {
return 0;
}
const lastOffset = sections[sections.length - 1].stepOffset;
const lastNumSteps = sections[sections.length - 1].instructions.length;
return lastOffset + lastNumSteps;
})();
// if title append new section to the end of the array
if (step.title) {
sections.push({
sectionName: step.title,
stepOffset: offset,
instructions: [step],
});
return sections;
}
// append if first element
if (sections.length === 0) {
sections.push({
sectionName: "",
stepOffset: offset,
instructions: [step],
});
return sections;
}
// otherwise add step to last section in the array
sections[sections.length - 1].instructions.push(step);
return sections;
}, [] as InstructionSection[]);
});
const hasNotes = computed(() => {
return props.recipe.notes && props.recipe.notes.length > 0;
});
function parseText(ingredient: RecipeIngredient) {
return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false);
}
return {
hasNotes,
splitIngredients,
parseText,
parseIngredientText,
ingredientSections,
instructionSections,
};
},
});
@@ -133,12 +216,23 @@ export default defineComponent({
</style>
<style scoped>
/* Makes all text solid black */
.print-container {
display: none;
background-color: white;
}
.print-container,
.print-container >>> * {
opacity: 1 !important;
color: black !important;
}
/* Prevents sections from being broken up between pages */
.print-section {
page-break-inside: avoid;
}
p {
padding-bottom: 0 !important;
margin-bottom: 0 !important;
@@ -152,7 +246,20 @@ p {
.ingredient-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
grid-gap: 0.5rem;
}
.ingredient-title,
.instruction-title {
grid-column: 1 / span 2;
text-decoration: underline;
text-underline-offset: 4px;
}
.ingredient-body,
.recipe-step-body,
.note-body {
font-size: 14px;
}
ul {

View File

@@ -0,0 +1,103 @@
<template>
<div>
<div class="text-center d-flex align-center">
<div>
<v-menu v-model="menu" :disabled="!editScale" offset-y top nudge-top="6" :close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-card class="pa-1 px-2" dark color="secondary darken-1" small v-bind="attrs" v-on="on">
<span v-if="recipeYield"> {{ scaledYield }} </span>
<span v-if="!recipeYield"> x {{ scale }} </span>
</v-card>
</template>
<v-card min-width="300px">
<v-card-title class="mb-0">
{{ $t("recipe.edit-scale") }}
</v-card-title>
<v-card-text class="mt-n5">
<div class="mt-4 d-flex align-center">
<v-text-field v-model.number="scale" type="number" :min="0" :label="$t('recipe.edit-scale')" />
<v-tooltip right color="secondary darken-1">
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" icon class="mx-1" small v-on="on" @click="scale = 1">
<v-icon>
{{ $globals.icons.undo }}
</v-icon>
</v-btn>
</template>
<span> Reset Scale </span>
</v-tooltip>
</div>
</v-card-text>
</v-card>
</v-menu>
</div>
<BaseButtonGroup
v-if="editScale"
class="pl-2"
:large="false"
:buttons="[
{
icon: $globals.icons.minus,
text: 'Decrease Scale by 1',
event: 'decrement',
},
{
icon: $globals.icons.createAlt,
text: 'Increase Scale by 1',
event: 'increment',
},
]"
@decrement="scale > 1 ? scale-- : null"
@increment="scale++"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed } from "@nuxtjs/composition-api";
export default defineComponent({
props: {
recipeYield: {
type: String,
default: null,
},
basicYield: {
type: String,
default: null,
},
scaledYield: {
type: String,
default: null,
},
editScale: {
type: Boolean,
default: false,
},
value: {
type: Number,
required: true,
},
},
setup(props, { emit }) {
const state = reactive({
tempScale: 1,
menu: false,
});
const scale = computed({
get: () => props.value,
set: (value) => {
const newScaleNumber = parseFloat(`${value}`);
emit("input", isNaN(newScaleNumber) ? 0 : newScaleNumber);
},
});
return {
scale,
...toRefs(state),
};
},
});
</script>

View File

@@ -7,7 +7,9 @@
<v-list-item-content>
<v-list-item-title> {{ $auth.user.fullName }}</v-list-item-title>
<v-list-item-subtitle> {{ $auth.user.admin ? $t("user.admin") : $t("user.user") }}</v-list-item-subtitle>
<v-list-item-subtitle>
<NuxtLink class="favorites-link" :to="`/user/${$auth.user.id}/favorites`"> Favorite Recipes </NuxtLink>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-divider></v-divider>
@@ -200,4 +202,12 @@ export default defineComponent({
display: none;
}
}
.favorites-link {
text-decoration: none;
}
.favorites-link:hover {
text-decoration: underline;
}
</style>

View File

@@ -3,7 +3,7 @@
<v-card-actions>
<v-menu v-if="tableConfig.hideColumns" offset-y bottom nudge-bottom="6" :close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-btn color="accent" class="mr-1" dark v-bind="attrs" v-on="on">
<v-btn color="accent" class="mr-2" dark v-bind="attrs" v-on="on">
<v-icon>
{{ $globals.icons.cog }}
</v-icon>

View File

@@ -22,21 +22,15 @@
dense
rows="4"
/>
<VueMarkdown v-else :source="value" />
<SafeMarkdown v-else :source="value" />
</div>
</template>
<script lang="ts">
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
export default defineComponent({
name: "MarkdownEditor",
components: {
VueMarkdown,
},
props: {
value: {
type: String,

View File

@@ -0,0 +1,42 @@
<template>
<VueMarkdown :source="sanitizeMarkdown(source)"></VueMarkdown>
</template>
<script lang="ts">
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { defineComponent } from "@nuxtjs/composition-api";
import DOMPurify from "isomorphic-dompurify";
export default defineComponent({
components: {
VueMarkdown,
},
props: {
source: {
type: String,
default: "",
},
},
setup() {
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
if (!rawHtml) {
return "";
}
const sanitized = DOMPurify.sanitize(rawHtml, {
USE_PROFILES: { html: true },
// TODO: some more thought could be put into what is allowed and what isn't
ALLOWED_TAGS: ["img", "div", "p"],
ADD_ATTR: ["src", "alt", "height", "width", "class"],
});
return sanitized;
}
return {
sanitizeMarkdown,
};
},
});
</script>

View File

@@ -30,11 +30,15 @@ export function useStoreActions<T extends BoundT>(
const allItems = useAsync(async () => {
const { data } = await api.getAll();
if (allRef) {
allRef.value = data;
if (data && allRef) {
allRef.value = data.items;
}
return data ?? [];
if (data) {
return data.items ?? [];
} else {
return [];
}
}, useAsyncKey());
loading.value = false;
@@ -45,8 +49,8 @@ export function useStoreActions<T extends BoundT>(
loading.value = true;
const { data } = await api.getAll();
if (data && allRef) {
allRef.value = data;
if (data && data.items && allRef) {
allRef.value = data.items;
}
loading.value = false;

View File

@@ -23,7 +23,6 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
// casting to number is required as sometimes quantity is a string
if (quantity && Number(quantity) !== 0) {
console.log("Using Quantity", quantity, typeof quantity);
if (unit?.fraction) {
const fraction = frac(quantity * scale, 10, true);
if (fraction[0] !== undefined && fraction[0] > 0) {

View File

@@ -21,7 +21,12 @@ export const useTools = function (eager = true) {
loading.value = true;
const units = useAsync(async () => {
const { data } = await api.tools.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@@ -33,7 +38,7 @@ export const useTools = function (eager = true) {
const { data } = await api.tools.getAll();
if (data) {
tools.value = data;
tools.value = data.items;
}
loading.value = false;

View File

@@ -18,8 +18,8 @@ function swap(t: Array<unknown>, i: number, j: number) {
export const useSorter = () => {
function sortAToZ(list: Array<Recipe>) {
list.sort((a, b) => {
const textA = a.name?.toUpperCase() ?? "";
const textB = b.name?.toUpperCase() ?? "";
const textA: string = a.name?.toUpperCase() ?? "";
const textB: string = b.name?.toUpperCase() ?? "";
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
}
@@ -61,13 +61,9 @@ export const useLazyRecipes = function () {
const recipes = ref<Recipe[]>([]);
async function fetchMore(start: number, limit: number) {
const { data } = await api.recipes.getAll(start, limit);
if (data) {
data.forEach((recipe) => {
recipes.value?.push(recipe);
});
}
async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") {
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection });
return data ? data.items : [];
}
return {
@@ -80,26 +76,26 @@ export const useRecipes = (all = false, fetchRecipes = true) => {
const api = useUserApi();
// recipes is non-reactive!!
const { recipes, start, end } = (() => {
const { recipes, page, perPage } = (() => {
if (all) {
return {
recipes: allRecipes,
start: 0,
end: 9999,
page: 1,
perPage: -1,
};
} else {
return {
recipes: recentRecipes,
start: 0,
end: 30,
page: 1,
perPage: 30,
};
}
})();
async function refreshRecipes() {
const { data } = await api.recipes.getAll(start, end, { loadFood: true });
const { data } = await api.recipes.getAll(page, perPage, { loadFood: true, orderBy: "created_at" });
if (data) {
recipes.value = data;
recipes.value = data.items;
}
}

View File

@@ -31,7 +31,11 @@ export const useCookbooks = function () {
const units = useAsync(async () => {
const { data } = await api.cookbooks.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@@ -41,8 +45,8 @@ export const useCookbooks = function () {
loading.value = true;
const { data } = await api.cookbooks.getAll();
if (data && cookbookStore) {
cookbookStore.value = data;
if (data && data.items && cookbookStore) {
cookbookStore.value = data.items;
}
loading.value = false;

View File

@@ -26,13 +26,17 @@ export const useMealplans = function (range: Ref<DateRange>) {
loading.value = true;
const units = useAsync(async () => {
const query = {
start: format(range.value.start, "yyyy-MM-dd"),
limit: format(range.value.end, "yyyy-MM-dd"),
start_date: format(range.value.start, "yyyy-MM-dd"),
end_date: format(range.value.end, "yyyy-MM-dd"),
};
// @ts-ignore TODO Modify typing to allow for string start+limit for mealplans
const { data } = await api.mealplans.getAll(query.start, query.limit);
const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date });
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@@ -41,14 +45,14 @@ export const useMealplans = function (range: Ref<DateRange>) {
async refreshAll(this: void) {
loading.value = true;
const query = {
start: format(range.value.start, "yyyy-MM-dd"),
limit: format(range.value.end, "yyyy-MM-dd"),
start_date: format(range.value.start, "yyyy-MM-dd"),
end_date: format(range.value.end, "yyyy-MM-dd"),
};
// @ts-ignore TODO Modify typing to allow for string start+limit for mealplans
const { data } = await api.mealplans.getAll(query.start, query.limit);
const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date });
if (data) {
mealplans.value = data;
if (data && data.items) {
mealplans.value = data.items;
}
loading.value = false;

View File

@@ -14,7 +14,11 @@ export const useGroupWebhooks = function () {
const units = useAsync(async () => {
const { data } = await api.groupWebhooks.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, useAsyncKey());
loading.value = false;
@@ -24,8 +28,8 @@ export const useGroupWebhooks = function () {
loading.value = true;
const { data } = await api.groupWebhooks.getAll();
if (data) {
webhooks.value = data;
if (data && data.items) {
webhooks.value = data.items;
}
loading.value = false;
@@ -37,7 +41,7 @@ export const useGroupWebhooks = function () {
enabled: false,
name: "New Webhook",
url: "",
time: "00:00",
scheduledTime: "00:00",
};
const { data } = await api.groupWebhooks.createOne(payload);
@@ -52,8 +56,23 @@ export const useGroupWebhooks = function () {
return;
}
// Convert to UTC time
const [hours, minutes] = updateData.scheduledTime.split(":");
const newDt = new Date();
newDt.setHours(Number(hours));
newDt.setMinutes(Number(minutes));
updateData.scheduledTime = `${pad(newDt.getUTCHours(), 2)}:${pad(newDt.getUTCMinutes(), 2)}`;
console.log(updateData.scheduledTime);
const payload = {
...updateData,
scheduledTime: updateData.scheduledTime,
};
loading.value = true;
const { data } = await api.groupWebhooks.updateOne(updateData.id, updateData);
const { data } = await api.groupWebhooks.updateOne(updateData.id, payload);
if (data) {
this.refreshAll();
}
@@ -73,3 +92,25 @@ export const useGroupWebhooks = function () {
return { webhooks, actions, validForm };
};
function pad(num: number, size: number) {
let numStr = num.toString();
while (numStr.length < size) numStr = "0" + numStr;
return numStr;
}
export function timeUTCToLocal(time: string): string {
const [hours, minutes] = time.split(":");
const dt = new Date();
dt.setUTCMinutes(Number(minutes));
dt.setUTCHours(Number(hours));
return `${pad(dt.getHours(), 2)}:${pad(dt.getMinutes(), 2)}`;
}
export function timeLocalToUTC(time: string) {
const [hours, minutes] = time.split(":");
const dt = new Date();
dt.setHours(Number(hours));
dt.setMinutes(Number(minutes));
return `${pad(dt.getUTCHours(), 2)}:${pad(dt.getUTCMinutes(), 2)}`;
}

View File

@@ -43,7 +43,12 @@ export const useGroups = function () {
const asyncKey = String(Date.now());
const groups = useAsync(async () => {
const { data } = await api.groups.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, asyncKey);
loading.value = false;
@@ -53,7 +58,13 @@ export const useGroups = function () {
async function refreshAllGroups() {
loading.value = true;
const { data } = await api.groups.getAll();
groups.value = data;
if (data) {
groups.value = data.items;
} else {
groups.value = null;
}
loading.value = false;
}

View File

@@ -17,7 +17,11 @@ export const useAllUsers = function () {
const asyncKey = String(Date.now());
const allUsers = useAsync(async () => {
const { data } = await api.users.getAll();
return data;
if (data) {
return data.items;
} else {
return null;
}
}, asyncKey);
loading.value = false;
@@ -27,7 +31,13 @@ export const useAllUsers = function () {
async function refreshAllUsers() {
loading.value = true;
const { data } = await api.users.getAll();
users.value = data;
if (data) {
users.value = data.items;
} else {
users.value = null;
}
loading.value = false;
}

View File

@@ -0,0 +1,28 @@
import { Ref, useContext } from "@nuxtjs/composition-api";
import { useLocalStorage } from "@vueuse/core";
export interface UserRecipePreferences {
orderBy: string;
orderDirection: string;
sortIcon: string;
useMobileCards: boolean;
}
export function useUserSortPreferences(): Ref<UserRecipePreferences> {
const { $globals } = useContext();
const fromStorage = useLocalStorage(
"recipe-section-preferences",
{
orderBy: "name",
orderDirection: "asc",
sortIcon: $globals.icons.sortAlphabeticalAscending,
useMobileCards: false,
},
{ mergeDefaults: true }
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as Ref<UserRecipePreferences>;
return fromStorage;
}

View File

@@ -1,21 +1,21 @@
{
"short": {
"month": "kort",
"day": "numerisk",
"weekday": "lang"
"month": "short",
"day": "numeric",
"weekday": "long"
},
"medium": {
"month": "lang",
"day": "numerisk",
"weekday": "lang",
"year": "numerisk"
"month": "long",
"day": "numeric",
"weekday": "long",
"year": "numeric"
},
"long": {
"year": "numerisk",
"month": "lang",
"day": "numerisk",
"weekday": "lang",
"hour": "numerisk",
"minute": "numerisk"
"year": "numeric",
"month": "long",
"day": "numeric",
"weekday": "long",
"hour": "numeric",
"minute": "numeric"
}
}
}

View File

@@ -1,21 +1,21 @@
{
"short": {
"month": "kurz",
"day": "numerisch",
"weekday": "lang"
"month": "short",
"day": "numeric",
"weekday": "long"
},
"medium": {
"month": "lang",
"day": "numerisch",
"weekday": "lang",
"year": "numerisch"
"month": "long",
"day": "numeric",
"weekday": "long",
"year": "numeric"
},
"long": {
"year": "numerisch",
"month": "lang",
"day": "numerisch",
"weekday": "lang",
"hour": "numerisch",
"minute": "numerisch"
"year": "numeric",
"month": "long",
"day": "numeric",
"weekday": "long",
"hour": "numeric",
"minute": "numeric"
}
}
}

View File

@@ -1,21 +1,21 @@
{
"short": {
"month": "breve",
"day": "numerico",
"weekday": "lungo"
"month": "short",
"day": "numeric",
"weekday": "long"
},
"medium": {
"month": "lungo",
"day": "numerico",
"weekday": "lungo",
"year": "numerico"
"month": "long",
"day": "numeric",
"weekday": "long",
"year": "numeric"
},
"long": {
"year": "numeric",
"month": "lungo",
"day": "numerico",
"weekday": "lungo",
"hour": "numerico",
"minute": "numerico"
"month": "long",
"day": "numeric",
"weekday": "long",
"hour": "numeric",
"minute": "numeric"
}
}
}

View File

@@ -1,21 +1,21 @@
{
"short": {
"month": "short",
"day": "числовий",
"day": "numeric",
"weekday": "long"
},
"medium": {
"month": "long",
"day": "числовий",
"day": "numeric",
"weekday": "long",
"year": "числовий"
"year": "numeric"
},
"long": {
"year": "числовий",
"year": "numeric",
"month": "long",
"day": "числовий",
"day": "numeric",
"weekday": "long",
"hour": "числовий",
"minute": "числовий"
"hour": "numeric",
"minute": "numeric"
}
}
}

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Descripció",
"disable-amount": "Oculta les quantitats",
"disable-comments": "Oculta els comentaris",
"edit-scale": "Edit Scale",
"fat-content": "Greixos",
"fiber-content": "Fibra",
"grams": "grams",

View File

@@ -1,63 +1,63 @@
{
"about": {
"about": "About",
"about-mealie": "About Mealie",
"api-docs": "API Docs",
"api-port": "API Port",
"application-mode": "Application Mode",
"database-type": "Database Type",
"database-url": "Database URL",
"default-group": "Default Group",
"about": "O aplikaci",
"about-mealie": "O Mealie",
"api-docs": "Dokumentace API",
"api-port": "Dokumentace portu",
"application-mode": "Režim aplikace",
"database-type": "Typ databáze",
"database-url": "URL databáze",
"default-group": "Výchozí skupina",
"demo": "Demo",
"demo-status": "Demo Status",
"development": "Development",
"docs": "Docs",
"download-log": "Download Log",
"download-recipe-json": "Last Scraped JSON",
"demo-status": "Stav dema",
"development": "Vývoj",
"docs": "Dokumentace",
"download-log": "Stáhnout log",
"download-recipe-json": "Poslední scrapovaný JSON",
"github": "Github",
"log-lines": "Log Lines",
"not-demo": "Not Demo",
"log-lines": "Řádky logů",
"not-demo": "Není demo",
"portfolio": "Portfolio",
"production": "Production",
"support": "Support",
"version": "Version"
"production": "Produkce",
"support": "Podpora",
"version": "Verze"
},
"asset": {
"assets": "Assets",
"code": "Code",
"file": "File",
"image": "Image",
"new-asset": "New Asset",
"assets": "Zdroje",
"code": "Kód",
"file": "Soubor",
"image": "Obrázek",
"new-asset": "Nový zdroj",
"pdf": "PDF",
"recipe": "Recipe",
"show-assets": "Show Assets"
"recipe": "Recept",
"show-assets": "Zobrazit zdroje"
},
"category": {
"categories": "Categories",
"category-created": "Category created",
"category-creation-failed": "Category creation failed",
"category-deleted": "Category Deleted",
"category-deletion-failed": "Category deletion failed",
"category-filter": "Category Filter",
"category-update-failed": "Category update failed",
"category-updated": "Category updated",
"uncategorized-count": "Uncategorized {count}"
"categories": "Kategorie",
"category-created": "Kategorie vytvořena",
"category-creation-failed": "Vytvoření kategorie selhalo",
"category-deleted": "Kategorie smazána",
"category-deletion-failed": "Smazání kategorie se nezdařilo",
"category-filter": "Filtr kategorií",
"category-update-failed": "Aktualizace kategorie selhala",
"category-updated": "Kategorie byla aktualizována",
"uncategorized-count": "Nezařazené {count}"
},
"events": {
"apprise-url": "Apprise URL",
"database": "Database",
"delete-event": "Delete Event",
"new-notification-form-description": "Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features.",
"new-version": "New version available!",
"notification": "Notification",
"refresh": "Refresh",
"scheduled": "Scheduled",
"something-went-wrong": "Something Went Wrong!",
"subscribed-events": "Subscribed Events",
"test-message-sent": "Test Message Sent"
"database": "Databáze",
"delete-event": "Smazat Událost",
"new-notification-form-description": "Mealie používá knihovnu Apprise pro generování notifikací. Nabízí spousty služeb pro zasílání oznámení. Podívejte se do jejich wiki pro komplexní návod jak vytvářet URL pro vaši službu. Výběr typu oznámení může obsahovat další extra funkce.",
"new-version": "Je dostupná nová verze!",
"notification": "Oznámení",
"refresh": "Obnovit",
"scheduled": "Naplánováno",
"something-went-wrong": "Něco se nepovedlo!",
"subscribed-events": "Odebírané události",
"test-message-sent": "Testovací zpráva odeslána"
},
"general": {
"cancel": "Cancel",
"cancel": "Zrušit",
"clear": "Clear",
"close": "Close",
"confirm": "Confirm",
@@ -108,62 +108,62 @@
"reset": "Reset",
"saturday": "Saturday",
"save": "Save",
"settings": "Settings",
"share": "Share",
"shuffle": "Shuffle",
"sort": "Sort",
"sort-alphabetically": "Alphabetical",
"status": "Status",
"submit": "Submit",
"success-count": "Success: {count}",
"sunday": "Sunday",
"templates": "Templates:",
"settings": "Nastavení",
"share": "Sdílet",
"shuffle": "Náhodně",
"sort": "Seřadit",
"sort-alphabetically": "Abecedně",
"status": "Stav",
"submit": "Odeslat",
"success-count": "Úspěšné: {count}",
"sunday": "Neděle",
"templates": "Šablony:",
"test": "Test",
"themes": "Themes",
"thursday": "Thursday",
"themes": "Motivy",
"thursday": "Čtvrtek",
"token": "Token",
"tuesday": "Tuesday",
"type": "Type",
"update": "Update",
"updated": "Updated",
"upload": "Upload",
"tuesday": "Úterý",
"type": "Typ",
"update": "Aktualizace",
"updated": "Aktualizováno",
"upload": "Nahrát",
"url": "URL",
"view": "View",
"wednesday": "Wednesday",
"yes": "Yes",
"foods": "Foods",
"units": "Units",
"back": "Back",
"next": "Next"
"view": "Zobrazit",
"wednesday": "Středa",
"yes": "Ano",
"foods": "Potraviny",
"units": "Jednotky",
"back": "Zpět",
"next": "Další"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
"cannot-delete-default-group": "Cannot delete default group",
"cannot-delete-group-with-users": "Cannot delete group with users",
"confirm-group-deletion": "Confirm Group Deletion",
"create-group": "Create Group",
"error-updating-group": "Error updating group",
"group": "Group",
"group-deleted": "Group deleted",
"group-deletion-failed": "Group deletion failed",
"group-id-with-value": "Group ID: {groupID}",
"group-name": "Group Name",
"group-not-found": "Group not found",
"group-with-value": "Group: {groupID}",
"groups": "Groups",
"manage-groups": "Manage Groups",
"user-group": "User Group",
"user-group-created": "User Group Created",
"user-group-creation-failed": "User Group Creation Failed",
"are-you-sure-you-want-to-delete-the-group": "Jste si jisti, že chcete smazat <b>{groupName}<b/>?",
"cannot-delete-default-group": "Nelze smazat výchozí skupinu",
"cannot-delete-group-with-users": "Nelze smazat skupinu obsahující uživatele",
"confirm-group-deletion": "Potvrdit smazání skupiny",
"create-group": "Vytvořit skupinu",
"error-updating-group": "Chyba při aktualizaci skupiny",
"group": "Skupina",
"group-deleted": "Skupina smazána",
"group-deletion-failed": "Smazání skupiny se nezdařilo",
"group-id-with-value": "ID skupiny: {groupID}",
"group-name": "Název skupiny",
"group-not-found": "Skupina nenalezena",
"group-with-value": "Skupina: {groupID}",
"groups": "Skupiny",
"manage-groups": "Spravovat skupiny",
"user-group": "Skupina uživatelů",
"user-group-created": "Uživatelská skupina vytvořena",
"user-group-creation-failed": "Vytvoření uživatelské skupiny se nezdařilo",
"settings": {
"keep-my-recipes-private": "Keep My Recipes Private",
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
"keep-my-recipes-private": "Ponechat mé recepty soukromé",
"keep-my-recipes-private-description": "Nastaví vaši skupinu a všechny recepty jako soukromé. Později to můžete změnit."
}
},
"meal-plan": {
"create-a-new-meal-plan": "Create a New Meal Plan",
"dinner-this-week": "Dinner This Week",
"dinner-today": "Dinner Today",
"create-a-new-meal-plan": "Vytvořit nový jídelníček",
"dinner-this-week": "Večeře na tento týden",
"dinner-today": "Dnešní večeře",
"dinner-tonight": "DINNER TONIGHT",
"edit-meal-plan": "Edit Meal Plan",
"end-date": "End Date",
@@ -224,58 +224,59 @@
"404-page-not-found": "404 Page not found",
"all-recipes": "All Recipes",
"new-page-created": "New page created",
"page": "Page",
"page-creation-failed": "Page creation failed",
"page-deleted": "Page deleted",
"page-deletion-failed": "Page deletion failed",
"page-update-failed": "Page update failed",
"page-updated": "Page updated",
"pages-update-failed": "Pages update failed",
"pages-updated": "Pages updated"
"page": "Stránka",
"page-creation-failed": "Vytvoření stránky se nezdařilo",
"page-deleted": "Stránka smazána",
"page-deletion-failed": "Smazání stránky se nezdařilo",
"page-update-failed": "Aktualizace stránky se nezdařila",
"page-updated": "Stránka aktualizována",
"pages-update-failed": "Aktualizace stránek se nezdařila",
"pages-updated": "Stránky aktualizovány"
},
"recipe": {
"add-key": "Add Key",
"add-to-favorites": "Add to Favorites",
"add-key": "Přidat klíč",
"add-to-favorites": "Přidat do oblíbených",
"api-extras": "API Extras",
"calories": "Calories",
"calories-suffix": "calories",
"carbohydrate-content": "Carbohydrate",
"categories": "Categories",
"comment-action": "Comment",
"comments": "Comments",
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",
"ingredient": "Ingredient",
"ingredients": "Ingredients",
"insert-section": "Insert Section",
"instructions": "Instructions",
"key-name-required": "Key Name Required",
"calories": "Kalorie",
"calories-suffix": "kalorie",
"carbohydrate-content": "Sacharidy",
"categories": "Kategorie",
"comment-action": "Komentář",
"comments": "Komentáře",
"delete-confirmation": "Opravdu chcete smazat tento recept?",
"delete-recipe": "Smazat recept",
"description": "Popis",
"disable-amount": "Nezobrazovat množství ingrediencí",
"disable-comments": "Zakázat komentáře",
"edit-scale": "Upravit měřítko",
"fat-content": "Tuky",
"fiber-content": "Vláknina",
"grams": "gramy",
"ingredient": "Ingredience",
"ingredients": "Ingredience",
"insert-section": "Vložit sekci",
"instructions": "Postup",
"key-name-required": "Je vyžadován název klíče",
"landscape-view-coming-soon": "Landscape View (Coming Soon)",
"milligrams": "milligrams",
"new-key-name": "New Key Name",
"no-white-space-allowed": "No White Space Allowed",
"note": "Note",
"nutrition": "Nutrition",
"object-key": "Object Key",
"object-value": "Object Value",
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein",
"public-recipe": "Public Recipe",
"recipe-created": "Recipe created",
"recipe-creation-failed": "Recipe creation failed",
"recipe-deleted": "Recipe deleted",
"recipe-image": "Recipe Image",
"recipe-image-updated": "Recipe image updated",
"recipe-name": "Recipe Name",
"recipe-settings": "Recipe Settings",
"milligrams": "miligramy",
"new-key-name": "Nový název klíče",
"no-white-space-allowed": "Prázdná místa nejsou povolena",
"note": "Poznámka",
"nutrition": "Výživové hodnoty",
"object-key": "Klíč objektu",
"object-value": "Hodnota objektu",
"original-url": "Původní URL",
"perform-time": "Doba vaření",
"prep-time": "Doba přípravy",
"protein-content": "Bílkoviny",
"public-recipe": "Veřejný recept",
"recipe-created": "Recept vytvořen",
"recipe-creation-failed": "Vytvoření receptu selhalo",
"recipe-deleted": "Recept smazán",
"recipe-image": "Obrázek receptu",
"recipe-image-updated": "Obrázek receptu aktualizován",
"recipe-name": "Název receptu",
"recipe-settings": "Nastavení receptu",
"recipe-update-failed": "Recipe update failed",
"recipe-updated": "Recipe updated",
"remove-from-favorites": "Remove from Favorites",
@@ -334,62 +335,62 @@
"card-per-section": "Card Per Section",
"home-page": "Home Page",
"home-page-sections": "Home Page Sections",
"show-recent": "Show Recent"
"show-recent": "Zobrazit poslední"
},
"language": "Language",
"latest": "Latest",
"local-api": "Local API",
"locale-settings": "Locale settings",
"migrations": "Migrations",
"new-page": "New Page",
"notify": "Notify",
"organize": "Organize",
"page-name": "Page Name",
"pages": "Pages",
"profile": "Profile",
"remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries",
"set-new-time": "Set New Time",
"settings-update-failed": "Settings update failed",
"settings-updated": "Settings updated",
"site-settings": "Site Settings",
"language": "Jazyk",
"latest": "Poslední",
"local-api": "Lokální API",
"locale-settings": "Nastavení locale",
"migrations": "Migrace",
"new-page": "Nová stránka",
"notify": "Upozornit",
"organize": "Organizovat",
"page-name": "Název stránky",
"pages": "Stránky",
"profile": "Profil",
"remove-existing-entries-matching-imported-entries": "Odstranit existující položky odpovídající importovaným položkám",
"set-new-time": "Nastavit nový čas",
"settings-update-failed": "Aktualizace nastavení se nezdařila",
"settings-updated": "Nastavení aktualizováno",
"site-settings": "Nastavení webu",
"theme": {
"accent": "Accent",
"dark": "Dark",
"default-to-system": "Default to system",
"error": "Error",
"error-creating-theme-see-log-file": "Error creating theme. See log file.",
"error-deleting-theme": "Error deleting theme",
"error-updating-theme": "Error updating theme",
"info": "Info",
"light": "Light",
"primary": "Primary",
"secondary": "Secondary",
"success": "Success",
"switch-to-dark-mode": "Switch to dark mode",
"switch-to-light-mode": "Switch to light mode",
"theme-deleted": "Theme deleted",
"theme-name": "Theme Name",
"theme-name-is-required": "Theme Name is required.",
"theme-saved": "Theme Saved",
"theme-updated": "Theme updated",
"warning": "Warning"
"accent": "Odstín",
"dark": "Tmavý",
"default-to-system": "Výchozí nastavení systému",
"error": "Chyba",
"error-creating-theme-see-log-file": "Chyba při vytváření motivu. Viz log soubor.",
"error-deleting-theme": "Chyba při mazání motivu",
"error-updating-theme": "Chyba při aktualizaci motivu",
"info": "Informace",
"light": "Světlý",
"primary": "Primární",
"secondary": "Sekundární",
"success": "Úspěšně dokončeno",
"switch-to-dark-mode": "Přepnout do tmavého režimu",
"switch-to-light-mode": "Přepnout do světlého režimu",
"theme-deleted": "Motiv odstraněn",
"theme-name": "Název motivu",
"theme-name-is-required": "Název motivu je povinný.",
"theme-saved": "Motiv uložen",
"theme-updated": "Motiv aktualizován",
"warning": "Upozornění"
},
"token": {
"active-tokens": "ACTIVE TOKENS",
"active-tokens": "AKTIV TOKENY",
"api-token": "API Token",
"api-tokens": "API Tokens",
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again": "Copy this token for use with an external application. This token will not be viewable again.",
"create-an-api-token": "Create an API Token",
"token-name": "Token Name"
"api-tokens": "API Tokeny",
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again": "Zkopírujte tento token pro použití v externí aplikaci. Tento token nebude znovu zobrazen.",
"create-an-api-token": "Vytvořit nový API token",
"token-name": "Název tokenu"
},
"toolbox": {
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign",
"new-name": "New Name",
"no-unused-items": "No Unused Items",
"recipes-affected": "No Recipes Affected|One Recipe Affected|{count} Recipes Affected",
"remove-unused": "Remove Unused",
"title-case-all": "Title Case All",
"assign-all": "Přiřadit vše",
"bulk-assign": "Hromadné přiřazení",
"new-name": "Nový název",
"no-unused-items": "Žádné nepoužité položky",
"recipes-affected": "Žádné recepty neovlivněny|Jeden recept ovlivněn|{count} receptů ovlivněno",
"remove-unused": "Odstranit nepoužívané",
"title-case-all": "Změnit první písmena slov na kapitálky",
"toolbox": "Toolbox",
"unorganized": "Unorganized"
},
@@ -455,26 +456,26 @@
"are-you-sure-you-want-to-delete-the-link": "Are you sure you want to delete the link <b>{link}<b/>?",
"are-you-sure-you-want-to-delete-the-user": "Are you sure you want to delete the user <b>{activeName} ID: {activeId}<b/>?",
"confirm-link-deletion": "Confirm Link Deletion",
"confirm-password": "Confirm Password",
"confirm-user-deletion": "Confirm User Deletion",
"could-not-validate-credentials": "Could Not Validate Credentials",
"create-link": "Create Link",
"create-user": "Create User",
"current-password": "Current Password",
"e-mail-must-be-valid": "E-mail must be valid",
"edit-user": "Edit User",
"confirm-password": "Potvrdit heslo",
"confirm-user-deletion": "Potvrdit smazání uživatele",
"could-not-validate-credentials": "Nelze ověřit přihlašovací údaje",
"create-link": "Vytvořit odkaz",
"create-user": "Vytvořit uživatele",
"current-password": "Současné heslo",
"e-mail-must-be-valid": "E-mail musí být platný",
"edit-user": "Upravit uživatele",
"email": "Email",
"error-cannot-delete-super-user": "Error! Cannot Delete Super User",
"existing-password-does-not-match": "Existing password does not match",
"full-name": "Full Name",
"invite-only": "Invite Only",
"link-id": "Link ID",
"link-name": "Link Name",
"login": "Login",
"logout": "Logout",
"manage-users": "Manage Users",
"new-password": "New Password",
"new-user": "New User",
"error-cannot-delete-super-user": "Chyba! Nelze odstranit superuživatele",
"existing-password-does-not-match": "Hesla se neshodují",
"full-name": "Jméno a příjmení",
"invite-only": "Jen na pozvání",
"link-id": "ID odkazu",
"link-name": "Název odkazu",
"login": "Přihlášení",
"logout": "Odhlášení",
"manage-users": "Spravovat uživatele",
"new-password": "Nové heslo",
"new-user": "Nový uživatel",
"password-has-been-reset-to-the-default-password": "Password has been reset to the default password",
"password-must-match": "Password must match",
"password-reset-failed": "Password reset failed",

View File

@@ -248,6 +248,7 @@
"description": "Beskrivelse",
"disable-amount": "Slå ingrediensmængder fra",
"disable-comments": "Slå kommentarer fra",
"edit-scale": "Rediger skalering",
"fat-content": "Fedt",
"fiber-content": "Kostfibre",
"grams": "gram",

View File

@@ -248,6 +248,7 @@
"description": "Beschreibung",
"disable-amount": "Zutatenmenge deaktivieren",
"disable-comments": "Kommentare deaktivieren",
"edit-scale": "Edit Scale",
"fat-content": "Fett",
"fiber-content": "Ballaststoffe",
"grams": "g",

View File

@@ -2,16 +2,16 @@
"about": {
"about": "Σχετικά με",
"about-mealie": "Σχετικά με το Mealie",
"api-docs": "API Docs",
"api-port": "API Port",
"api-docs": "Έγγραφα API",
"api-port": "Θύρα API",
"application-mode": "Κατάσταση εφαρμογής",
"database-type": "Τύπος βάσης δεδομένων",
"database-url": "Database URL",
"database-url": "URL Βάσης Δεδομένων",
"default-group": "Προεπιλεγμένη ομάδα",
"demo": "Επίδειξη",
"demo-status": "Κατάσταση επίδειξης",
"development": "Ανάπτυξη",
"docs": "Docs",
"docs": "Έγγραφα",
"download-log": "Λήψη αρχείου καταγραφής",
"download-recipe-json": "Τελευταίο Scraped JSON",
"github": "Github",
@@ -24,7 +24,7 @@
},
"asset": {
"assets": "Στοιχεία",
"code": "Code",
"code": "Κώδικας",
"file": "Αρχείο",
"image": "Εικόνα",
"new-asset": "Νέο Στοιχείο",
@@ -33,7 +33,7 @@
"show-assets": "Εμφάνιση Στοιχείων"
},
"category": {
"categories": "Categories",
"categories": "Κατηγορίες",
"category-created": "Δημιουργήθηκε η κατηγορία",
"category-creation-failed": "Η δημιουργία κατηγορίας απέτυχε",
"category-deleted": "Κατηγορία Διαγράφηκε",
@@ -69,7 +69,7 @@
"dashboard": "Ταμπλό",
"delete": "Διαγραφή",
"disabled": "Ανενεργό",
"download": "Download",
"download": "Λήψη",
"edit": "Επεξεργασία",
"enabled": "Ενεργό",
"exception": "Εξαίρεση",
@@ -131,10 +131,10 @@
"view": "Προβολη",
"wednesday": "Τετάρτη",
"yes": "Ναι",
"foods": "Foods",
"units": "Units",
"back": "Back",
"next": "Next"
"foods": "Φαγητά",
"units": "Μονάδες",
"back": "Πίσω",
"next": "Επόμενο"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό τον ασφαλή σύνδεσμο <b>{groupName}<b/>;",
@@ -156,8 +156,8 @@
"user-group-created": "Η Ομάδα Χρηστών Δημιουργήθηκε",
"user-group-creation-failed": "Αποτυχία Δημιουργίας Ομάδας Χρηστών",
"settings": {
"keep-my-recipes-private": "Keep My Recipes Private",
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
"keep-my-recipes-private": "Κρατήστε Τις Συνταγές Μου Ιδιωτικές",
"keep-my-recipes-private-description": "Ορίζει την ομάδα σας και όλες τις συνταγές ιδιωτικές από προεπιλογή. Μπορείτε πάντα να το αλλάξετε αργότερα."
}
},
"meal-plan": {
@@ -206,7 +206,7 @@
"error-details": "Μόνο ιστοσελίδες που περιέχουν ld+json ή μικροδεδομένα μπορούν να εισαχθούν από την Mealie. Οι πιο σημαντικές ιστοσελίδες συνταγών υποστηρίζουν αυτή τη δομή δεδομένων. Αν το site σας δεν μπορεί να εισαχθεί, αλλά υπάρχουν δεδομένα json στο αρχείο καταγραφής, παρακαλούμε να υποβάλετε ένα github πρόβλημα με το URL και τα δεδομένα.",
"error-title": "Φαίνεται Όπως Δεν Μπορούσαμε Να βρούμε Οτιδήποτε",
"from-url": "Εισαγωγή συνταγής",
"github-issues": "GitHub Issues",
"github-issues": "Σφάλματα GitHub",
"google-ld-json-info": "Google ld+json Info",
"must-be-a-valid-url": "Πρέπει να είναι ένα έγκυρο URL",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Επικόλληση δεδομένων συνταγών σας. Κάθε γραμμή θα αντιμετωπίζεται ως αντικείμενο σε μια λίστα",
@@ -216,9 +216,9 @@
"upload-individual-zip-file": "Ανεβάστε ένα μεμονωμένο αρχείο .zip που εξάγεται από μια άλλη περίπτωση Mealie.",
"url-form-hint": "Αντιγράψτε και επικολλήστε έναν σύνδεσμο από την αγαπημένη σας ιστοσελίδα συνταγών",
"view-scraped-data": "Προβολή Παραγόμενων Δεδομένων",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
"trim-whitespace-description": "Περικοπή αιχμής και διαδρομής κενών καθώς και κενών γραμμών",
"trim-prefix-description": "Περικοπή πρώτου χαρακτήρα από κάθε γραμμή",
"split-by-numbered-line-description": "Προσπάθεια για χωρισμό μιας παραγράφου ταιριάζοντας μοτίβα '1)' ή '1'"
},
"page": {
"404-page-not-found": "404. η σελίδα δεν βρέθηκε",
@@ -248,6 +248,7 @@
"description": "Περιγραφή",
"disable-amount": "Απενεργοποίηση Ποσών Συστατικών",
"disable-comments": "Απενεργοποιηση σχολιων",
"edit-scale": "Επεξεργασία Κλίμακας",
"fat-content": "Λιπαρά",
"fiber-content": "Ίνα",
"grams": "γραμμάρια",
@@ -291,7 +292,7 @@
"title": "Τίτλος",
"total-time": "Συνολικός Χρόνος",
"unable-to-delete-recipe": "Αδυναμία διαγραφής συνταγής",
"no-recipe": "No Recipe"
"no-recipe": "Καμία Συνταγή"
},
"search": {
"advanced-search": "Σύνθετη Αναζήτηση",
@@ -413,9 +414,9 @@
},
"sidebar": {
"all-recipes": "Συνταγές όλες",
"backups": "Backups",
"backups": "Αντίγραφα ασφαλείας",
"categories": "Κατηγορίες",
"cookbooks": "Cookbooks",
"cookbooks": "Μαγειρικά Βιβλία",
"dashboard": "Ταμπλό",
"home-page": "Αρχική Σελίδα",
"manage-users": "Διαχ. χρηστών",
@@ -425,7 +426,7 @@
"site-settings": "Ρυθμ. site",
"tags": "Ετικέτα",
"toolbox": "Εργαλειοθήκη",
"language": "Language"
"language": "Γλώσσα"
},
"signup": {
"error-signing-up": "Σφάλμα Στην Υπογραφή",
@@ -448,7 +449,7 @@
"untagged-count": "Χωρίς ετικέτα {count}"
},
"tool": {
"tools": "Tools"
"tools": "Εργαλεία"
},
"user": {
"admin": "Διαχειριστής",
@@ -467,7 +468,7 @@
"error-cannot-delete-super-user": "Σφάλμα! Αδυναμία Διαγραφής Υπερχρήστη",
"existing-password-does-not-match": "Ο υπάρχων κωδικός πρόσβασης δεν ταιριάζει",
"full-name": "Πλήρες όνομα",
"invite-only": "Invite Only",
"invite-only": "Μόνο με πρόσκληση",
"link-id": "Σύνδεσμος ID",
"link-name": "Όνομα συνδέσμου",
"login": "Σύνδεση",
@@ -480,8 +481,8 @@
"password-reset-failed": "Αποτυχία επαναφοράς κωδικού πρόσβασης",
"password-updated": "Ο κωδικός πρόσβασης ενημερώθηκε",
"password": "Κωδικός",
"password-strength": "Password is {strength}",
"register": "Register",
"password-strength": "Ο κωδικός είναι {strength}",
"register": "Εγγραφή",
"reset-password": "Επαναφορά Κωδικού",
"sign-in": "Είσοδος",
"total-mealplans": "Σύνολο Σχεδίων Γεύματος",
@@ -505,21 +506,21 @@
"webhooks-enabled": "Το webhook είναι ενεργό",
"you-are-not-allowed-to-create-a-user": "Δεν επιτρέπεται να δημιουργήσετε ένα χρήστη",
"you-are-not-allowed-to-delete-this-user": "Δεν επιτρέπεται να διαγράψετε αυτόν τον χρήστη",
"enable-advanced-content": "Enable Advanced Content",
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
"enable-advanced-content": "Ενεργοποίηση Προηγμένου Περιεχομένου",
"enable-advanced-content-description": "Ενεργοποιεί προηγμένες λειτουργίες όπως κλιμάκωση συνταγής, κλειδιά API, Webhooks και διαχείριση δεδομένων. Μην ανησυχείτε, μπορείτε πάντα να το αλλάξετε αργότερα"
},
"language-dialog": {
"translated": "translated",
"choose-language": "Choose Language",
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
"read-the-docs": "Read the docs"
"translated": "μεταφρασμένο",
"choose-language": "Επιλογή γλώσσας",
"select-description": "Επιλέξτε τη γλώσσα για το περιβάλλον εργασίας εργασίας Mealie. Η ρύθμιση ισχύει μόνο για εσάς, όχι για άλλους χρήστες.",
"how-to-contribute-description": "Δεν είναι κάτι μεταφρασμένο ακόμα, λανθασμένο, ή η γλώσσα σας λείπει από τη λίστα? {read-the-docs-link} για το πώς να συνεισφέρει!",
"read-the-docs": "Διαβάστε τα έγγραφα"
},
"data-pages": {
"seed-data": "Seed Data",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"merge-dialog-text": "Ο συνδυασμός των επιλεγμένων τροφίμων θα συγχωνεύσει την πηγή τροφίμων και θα στοχεύσει τα τρόφιμα σε ένα μόνο φαγητό. Η πηγή τροφίμων θα διαγραφεί και όλες οι αναφορές στην πηγή τροφίμων θα ενημερωθούν ώστε να υποδείξουν το τρόφιμο-στόχο.",
"merge-food-example": "Συγχώνευση {food1} στο {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
},

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fibre",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Descripción",
"disable-amount": "Desactivar cantidades de ingredientes",
"disable-comments": "Desactivar comentarios",
"edit-scale": "Edit Scale",
"fat-content": "Grasa",
"fiber-content": "Fibra",
"grams": "gramos",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -216,9 +216,9 @@
"upload-individual-zip-file": "Chargez un fichier .zip exporté depuis une autre instance Mealie.",
"url-form-hint": "Copiez et collez un lien depuis votre site de recettes favori",
"view-scraped-data": "Voir les données récupérées",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
"trim-whitespace-description": "Ajuster les espaces de début et de fin ainsi que les lignes vides",
"trim-prefix-description": "Couper le premier caractère de chaque ligne",
"split-by-numbered-line-description": "Tenter de découper un paragraphe par correspondance de motifs : '1) ou '1.'"
},
"page": {
"404-page-not-found": "404 Page introuvable",
@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Désactiver les quantités d'ingrédients",
"disable-comments": "Désactiver les commentaires",
"edit-scale": "Modifier l'échelle",
"fat-content": "Matières grasses",
"fiber-content": "Fibres",
"grams": "grammes",
@@ -415,7 +416,7 @@
"all-recipes": "Les recettes",
"backups": "Sauvegardes",
"categories": "Catégories",
"cookbooks": "Cookbooks",
"cookbooks": "Livre de recettes",
"dashboard": "Console",
"home-page": "Accueil",
"manage-users": "Utilisateurs",
@@ -467,7 +468,7 @@
"error-cannot-delete-super-user": "Erreur! Impossible de supprimer le super utilisateur",
"existing-password-does-not-match": "Le mot de passe actuel ne correspond pas",
"full-name": "Nom",
"invite-only": "Invite Only",
"invite-only": "Invités uniquement",
"link-id": "ID du lien",
"link-name": "Nom du lien",
"login": "Connexion",
@@ -480,7 +481,7 @@
"password-reset-failed": "Échec de la réinitialisation du mot de passe",
"password-updated": "Mot de passe mis à jour",
"password": "Mot de passe",
"password-strength": "Password is {strength}",
"password-strength": "Robustesse du mot de passe : {strength}",
"register": "S'inscrire",
"reset-password": "Réinitialiser le mot de passe",
"sign-in": "Se connecter",
@@ -505,45 +506,45 @@
"webhooks-enabled": "Webhooks activés",
"you-are-not-allowed-to-create-a-user": "Vous n'avez pas le droit de créer un utilisateur",
"you-are-not-allowed-to-delete-this-user": "Vous n'avez pas le droit de supprimer cet utilisateur",
"enable-advanced-content": "Enable Advanced Content",
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
"enable-advanced-content": "Activer le contenu avancé",
"enable-advanced-content-description": "Active les fonctionnalités avancées comme la mise à l'échelle des recettes, les clés API, les Webhooks, et la gestion des données. Ne vous inquiétez pas, vous pouvez toujours modifier cela plus tard"
},
"language-dialog": {
"translated": "translated",
"choose-language": "Choose Language",
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
"read-the-docs": "Read the docs"
"translated": "traduit",
"choose-language": "Choisir la langue",
"select-description": "Choisissez la langue de l'interface utilisateur de Mealie. Ce paramètre s'applique uniquement à vous, pas aux autres utilisateurs.",
"how-to-contribute-description": "Quelque chose n'est pas encore traduit, mal traduit, ou votre langue est manquante dans la liste ? {read-the-docs-link} sur la façon de contribuer !",
"read-the-docs": "Lire la documentation"
},
"data-pages": {
"seed-data": "Seed Data",
"seed-data": "Génération de données",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
"merge-dialog-text": "La combinaison des aliments sélectionnés fusionnera l'aliment source et l'aliment cible en un seul aliment. L'aliment source sera supprimé et toutes les références à l'aliment source seront mises à jour pour pointer vers l'aliment cible.",
"merge-food-example": "Fusion de {food1} dans {food2}",
"seed-dialog-text": "Ensemencez la base de données avec des aliments basés sur votre langue locale. Cela permettra de créer plus de 200 aliments communs qui pourront être utilisés pour organiser votre base de données. Les aliments sont traduits grâce à un effort communautaire.",
"seed-dialog-warning": "Vous avez déjà des éléments dans votre base de données. Cette action ne réconciliera pas les doublons, vous devrez les gérer manuellement."
},
"units": {
"seed-dialog-text": "Seed the database with common units based on your local language."
"seed-dialog-text": "Introduisez dans la base de données des unités communes basées sur votre langue locale."
},
"labels": {
"seed-dialog-text": "Seed the database with common labels based on your local language."
"seed-dialog-text": "Introduisez dans la base de données des unités communes basées sur votre langue locale."
}
},
"user-registration": {
"user-registration": "User Registration",
"join-a-group": "Join a Group",
"create-a-new-group": "Create a New Group",
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
"group-details": "Group Details",
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
"use-seed-data": "Use Seed Data",
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
"account-details": "Account Details"
"user-registration": "Inscription d'utilisateur",
"join-a-group": "Rejoindre un groupe",
"create-a-new-group": "Créer un nouveau groupe",
"provide-registration-token-description": "Veuillez fournir le jeton d'enregistrement associé au groupe que vous souhaitez rejoindre. Vous devrez l'obtenir auprès d'un membre existant du groupe.",
"group-details": "Détails du groupe",
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d'autres personnes plus tard. Les membres de votre groupe peuvent partager leur planification de repas, leurs listes d'achat, leurs recettes et plus encore !",
"use-seed-data": "Utiliser la génération de données",
"use-seed-data-description": "Mealie est livré avec une collection d'aliments, d'unités et d'étiquettes qui peuvent être utilisés pour alimenter votre groupe avec des données utiles pour organiser vos recettes.",
"account-details": "Détails du compte"
},
"validation": {
"group-name-is-taken": "Group name is taken",
"username-is-taken": "Username is taken",
"email-is-taken": "Email is taken"
"group-name-is-taken": "Le nom du groupe est déjà pris",
"username-is-taken": "Nom d'utilisateur déjà utilisé",
"email-is-taken": "Cet e-mail est déjà pris"
}
}

View File

@@ -18,7 +18,7 @@
"log-lines": "Lignes de log",
"not-demo": "Non",
"portfolio": "Portfolio",
"production": "Production",
"production": "Réalisation",
"support": "Soutenir",
"version": "Version"
},
@@ -216,9 +216,9 @@
"upload-individual-zip-file": "Chargez un fichier .zip exporté depuis une autre instance Mealie.",
"url-form-hint": "Copiez et collez un lien depuis votre site de recettes favori",
"view-scraped-data": "Voir les données récupérées",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
"trim-whitespace-description": "Ajuster les espaces de début et de fin ainsi que les lignes vides",
"trim-prefix-description": "Couper le premier caractère de chaque ligne",
"split-by-numbered-line-description": "Tente de découper un paragraphe par correspondance de motifs : '1) ou '1.'"
},
"page": {
"404-page-not-found": "404 Page introuvable",
@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Désactiver les quantités des ingrédients",
"disable-comments": "Désactiver les commentaires",
"edit-scale": "Modifier l'échelle",
"fat-content": "Matières grasses",
"fiber-content": "Fibres",
"grams": "grammes",
@@ -260,7 +261,7 @@
"milligrams": "milligrammes",
"new-key-name": "Nouveau nom de clé",
"no-white-space-allowed": "Aucun espace blanc autorisé",
"note": "Note",
"note": "Remarque",
"nutrition": "Valeurs nutritionnelles",
"object-key": "Clé d'objet",
"object-value": "Valeur d'objet",
@@ -415,7 +416,7 @@
"all-recipes": "Les recettes",
"backups": "Sauvegardes",
"categories": "Catégories",
"cookbooks": "Cookbooks",
"cookbooks": "Livre de recettes",
"dashboard": "Tableau de bord",
"home-page": "Accueil",
"manage-users": "Utilisateurs",
@@ -451,7 +452,7 @@
"tools": "Outils"
},
"user": {
"admin": "Admin",
"admin": "Administrateur",
"are-you-sure-you-want-to-delete-the-link": "Êtes-vous sûr de vouloir supprimer le lien <b>{link}<b/> ?",
"are-you-sure-you-want-to-delete-the-user": "Êtes-vous sûr de vouloir supprimer l'utilisateur <b>{activeName} ID : {activeId}<b/> ?",
"confirm-link-deletion": "Confirmer la suppression du lien",
@@ -467,7 +468,7 @@
"error-cannot-delete-super-user": "Erreur ! Impossible de supprimer le super utilisateur",
"existing-password-does-not-match": "Le mot de passe actuel ne correspond pas",
"full-name": "Nom",
"invite-only": "Invite Only",
"invite-only": "Invités uniquement",
"link-id": "ID du lien",
"link-name": "Nom du lien",
"login": "Connexion",
@@ -480,7 +481,7 @@
"password-reset-failed": "Échec de la réinitialisation du mot de passe",
"password-updated": "Mot de passe mis à jour",
"password": "Mot de passe",
"password-strength": "Password is {strength}",
"password-strength": "Robustesse du mot de passe : {strength}",
"register": "S'inscrire",
"reset-password": "Réinitialiser le mot de passe",
"sign-in": "Se connecter",
@@ -505,45 +506,45 @@
"webhooks-enabled": "Webhooks activés",
"you-are-not-allowed-to-create-a-user": "Vous n'avez pas le droit de créer un utilisateur",
"you-are-not-allowed-to-delete-this-user": "Vous n'avez pas le droit de supprimer cet utilisateur",
"enable-advanced-content": "Enable Advanced Content",
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
"enable-advanced-content": "Activer le contenu avancé",
"enable-advanced-content-description": "Active les fonctionnalités avancées comme la mise à l'échelle des recettes, les clés API, les Webhooks, et la gestion des données. Ne vous inquiétez pas, vous pouvez toujours modifier cela plus tard"
},
"language-dialog": {
"translated": "traduit",
"choose-language": "Choisir la langue",
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
"read-the-docs": "Read the docs"
"select-description": "Choisissez la langue de l'interface utilisateur de Mealie. Ce paramètre s'applique uniquement à vous, pas aux autres utilisateurs.",
"how-to-contribute-description": "Quelque chose n'est pas encore traduit, mal traduit, ou votre langue est manquante dans la liste ? {read-the-docs-link} sur la façon de contribuer !",
"read-the-docs": "Lire la documentation"
},
"data-pages": {
"seed-data": "Seed Data",
"seed-data": "Génération de données",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
"merge-dialog-text": "La combinaison des aliments sélectionnés fusionnera l'aliment source et l'aliment cible en un seul aliment. L'aliment source sera supprimé et toutes les références à l'aliment source seront mises à jour pour pointer vers l'aliment cible.",
"merge-food-example": "Fusion de {food1} dans {food2}",
"seed-dialog-text": "Ensemencez la base de données avec des aliments basés sur votre langue locale. Cela permettra de créer plus de 200 aliments communs qui pourront être utilisés pour organiser votre base de données. Les aliments sont traduits grâce à un effort communautaire.",
"seed-dialog-warning": "Vous avez déjà des éléments dans votre base de données. Cette action ne réconciliera pas les doublons, vous devrez les gérer manuellement."
},
"units": {
"seed-dialog-text": "Seed the database with common units based on your local language."
"seed-dialog-text": "Introduisez dans la base de données des unités communes basées sur votre langue locale."
},
"labels": {
"seed-dialog-text": "Seed the database with common labels based on your local language."
"seed-dialog-text": "Introduisez dans la base de données des unités communes basées sur votre langue locale."
}
},
"user-registration": {
"user-registration": "User Registration",
"user-registration": "Inscription d'utilisateur",
"join-a-group": "Rejoindre un groupe",
"create-a-new-group": "Créer un nouveau groupe",
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
"provide-registration-token-description": "Veuillez fournir le jeton d'enregistrement associé au groupe que vous souhaitez rejoindre. Vous devrez l'obtenir auprès d'un membre existant du groupe.",
"group-details": "Détails du groupe",
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
"use-seed-data": "Use Seed Data",
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d'autres personnes plus tard. Les membres de votre groupe peuvent partager leur planification de repas, leurs listes d'achat, leurs recettes et plus encore !",
"use-seed-data": "Utiliser la génération de données",
"use-seed-data-description": "Mealie est livré avec une collection d'aliments, d'unités et d'étiquettes qui peuvent être utilisés pour alimenter votre groupe avec des données utiles pour organiser vos recettes.",
"account-details": "Détails du compte"
},
"validation": {
"group-name-is-taken": "Group name is taken",
"group-name-is-taken": "Le nom du groupe est déjà pris",
"username-is-taken": "Nom d'utilisateur déjà utilisé",
"email-is-taken": "Email is taken"
"email-is-taken": "Cet e-mail est déjà pris"
}
}

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Leírás",
"disable-amount": "Hozzávalók mennyiségének letiltása",
"disable-comments": "Megjegyzések letiltása",
"edit-scale": "Edit Scale",
"fat-content": "Zsír",
"fiber-content": "Rostok",
"grams": "gramm",

View File

@@ -248,6 +248,7 @@
"description": "Descrizione",
"disable-amount": "Disabilita Quantità Ingredienti",
"disable-comments": "Disattiva Commenti",
"edit-scale": "Modifica Scala",
"fat-content": "Grassi",
"fiber-content": "Fibre",
"grams": "grammi",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -0,0 +1,550 @@
{
"about": {
"about": "About",
"about-mealie": "About Mealie",
"api-docs": "API Docs",
"api-port": "API Port",
"application-mode": "Application Mode",
"database-type": "Database Type",
"database-url": "Database URL",
"default-group": "Default Group",
"demo": "Demo",
"demo-status": "Demo Status",
"development": "Development",
"docs": "Docs",
"download-log": "Download Log",
"download-recipe-json": "Last Scraped JSON",
"github": "Github",
"log-lines": "Log Lines",
"not-demo": "Not Demo",
"portfolio": "Portfolio",
"production": "Production",
"support": "Support",
"version": "Version"
},
"asset": {
"assets": "Assets",
"code": "Code",
"file": "File",
"image": "Image",
"new-asset": "New Asset",
"pdf": "PDF",
"recipe": "Recipe",
"show-assets": "Show Assets"
},
"category": {
"categories": "Categories",
"category-created": "Category created",
"category-creation-failed": "Category creation failed",
"category-deleted": "Category Deleted",
"category-deletion-failed": "Category deletion failed",
"category-filter": "Category Filter",
"category-update-failed": "Category update failed",
"category-updated": "Category updated",
"uncategorized-count": "Uncategorized {count}"
},
"events": {
"apprise-url": "Apprise URL",
"database": "Database",
"delete-event": "Delete Event",
"new-notification-form-description": "Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features.",
"new-version": "New version available!",
"notification": "Notification",
"refresh": "Refresh",
"scheduled": "Scheduled",
"something-went-wrong": "Something Went Wrong!",
"subscribed-events": "Subscribed Events",
"test-message-sent": "Test Message Sent"
},
"general": {
"cancel": "Cancel",
"clear": "Clear",
"close": "Close",
"confirm": "Confirm",
"confirm-delete-generic": "Are you sure you want to delete this?",
"copied": "Copied",
"create": "Create",
"created": "Created",
"custom": "Custom",
"dashboard": "Dashboard",
"delete": "Delete",
"disabled": "Disabled",
"download": "Download",
"edit": "Edit",
"enabled": "Enabled",
"exception": "Exception",
"failed-count": "Failed: {count}",
"failure-uploading-file": "Failure uploading file",
"favorites": "Favorites",
"field-required": "Field Required",
"file-folder-not-found": "File/folder not found",
"file-uploaded": "File uploaded",
"filter": "Filter",
"friday": "Friday",
"general": "General",
"get": "Get",
"home": "Home",
"image": "Image",
"image-upload-failed": "Image upload failed",
"import": "Import",
"json": "JSON",
"keyword": "Keyword",
"link-copied": "Link Copied",
"loading-recipes": "Loading Recipes",
"monday": "Monday",
"name": "Name",
"new": "New",
"no": "No",
"no-recipe-found": "No Recipe Found",
"ok": "OK",
"options": "Options:",
"print": "Print",
"random": "Random",
"rating": "Rating",
"recent": "Recent",
"recipe": "Recipe",
"recipes": "Recipes",
"rename-object": "Rename {0}",
"reset": "Reset",
"saturday": "Saturday",
"save": "Save",
"settings": "Settings",
"share": "Share",
"shuffle": "Shuffle",
"sort": "Sort",
"sort-alphabetically": "Alphabetical",
"status": "Status",
"submit": "Submit",
"success-count": "Success: {count}",
"sunday": "Sunday",
"templates": "Templates:",
"test": "Test",
"themes": "Themes",
"thursday": "Thursday",
"token": "Token",
"tuesday": "Tuesday",
"type": "Type",
"update": "Update",
"updated": "Updated",
"upload": "Upload",
"url": "URL",
"view": "View",
"wednesday": "Wednesday",
"yes": "Yes",
"foods": "Foods",
"units": "Units",
"back": "Back",
"next": "Next"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
"cannot-delete-default-group": "Cannot delete default group",
"cannot-delete-group-with-users": "Cannot delete group with users",
"confirm-group-deletion": "Confirm Group Deletion",
"create-group": "Create Group",
"error-updating-group": "Error updating group",
"group": "Group",
"group-deleted": "Group deleted",
"group-deletion-failed": "Group deletion failed",
"group-id-with-value": "Group ID: {groupID}",
"group-name": "Group Name",
"group-not-found": "Group not found",
"group-with-value": "Group: {groupID}",
"groups": "Groups",
"manage-groups": "Manage Groups",
"user-group": "User Group",
"user-group-created": "User Group Created",
"user-group-creation-failed": "User Group Creation Failed",
"settings": {
"keep-my-recipes-private": "Keep My Recipes Private",
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
}
},
"meal-plan": {
"create-a-new-meal-plan": "Create a New Meal Plan",
"dinner-this-week": "Dinner This Week",
"dinner-today": "Dinner Today",
"dinner-tonight": "DINNER TONIGHT",
"edit-meal-plan": "Edit Meal Plan",
"end-date": "End Date",
"group": "Group (Beta)",
"main": "Main",
"meal-planner": "Meal Planner",
"meal-plans": "Meal Plans",
"mealplan-categories": "MEALPLAN CATEGORIES",
"mealplan-created": "Mealplan created",
"mealplan-creation-failed": "Mealplan creation failed",
"mealplan-deleted": "Mealplan Deleted",
"mealplan-deletion-failed": "Mealplan deletion failed",
"mealplan-settings": "Mealplan Settings",
"mealplan-update-failed": "Mealplan update failed",
"mealplan-updated": "Mealplan Updated",
"no-meal-plan-defined-yet": "No meal plan defined yet",
"no-meal-planned-for-today": "No meal planned for today",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Only recipes with these categories will be used in Meal Plans",
"planner": "Planner",
"quick-week": "Quick Week",
"side": "Side",
"sides": "Sides",
"start-date": "Start Date"
},
"migration": {
"chowdown": {
"description": "Migrate data from Chowdown",
"title": "Chowdown"
},
"migration-data-removed": "Migration data removed",
"nextcloud": {
"description": "Migrate data from a Nextcloud Cookbook instance",
"title": "Nextcloud Cookbook"
},
"no-migration-data-available": "No Migration Data Available",
"recipe-migration": "Recipe Migration"
},
"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",
"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",
"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",
"view-scraped-data": "View Scraped Data",
"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"
},
"page": {
"404-page-not-found": "404 Page not found",
"all-recipes": "All Recipes",
"new-page-created": "New page created",
"page": "Page",
"page-creation-failed": "Page creation failed",
"page-deleted": "Page deleted",
"page-deletion-failed": "Page deletion failed",
"page-update-failed": "Page update failed",
"page-updated": "Page updated",
"pages-update-failed": "Pages update failed",
"pages-updated": "Pages updated"
},
"recipe": {
"add-key": "Add Key",
"add-to-favorites": "Add to Favorites",
"api-extras": "API Extras",
"calories": "Calories",
"calories-suffix": "calories",
"carbohydrate-content": "Carbohydrate",
"categories": "Categories",
"comment-action": "Comment",
"comments": "Comments",
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",
"ingredient": "Ingredient",
"ingredients": "Ingredients",
"insert-section": "Insert Section",
"instructions": "Instructions",
"key-name-required": "Key Name Required",
"landscape-view-coming-soon": "Landscape View",
"milligrams": "milligrams",
"new-key-name": "New Key Name",
"no-white-space-allowed": "No White Space Allowed",
"note": "Note",
"nutrition": "Nutrition",
"object-key": "Object Key",
"object-value": "Object Value",
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein",
"public-recipe": "Public Recipe",
"recipe-created": "Recipe created",
"recipe-creation-failed": "Recipe creation failed",
"recipe-deleted": "Recipe deleted",
"recipe-image": "Recipe Image",
"recipe-image-updated": "Recipe image updated",
"recipe-name": "Recipe Name",
"recipe-settings": "Recipe Settings",
"recipe-update-failed": "Recipe update failed",
"recipe-updated": "Recipe updated",
"remove-from-favorites": "Remove from Favorites",
"remove-section": "Remove Section",
"save-recipe-before-use": "Save recipe before use",
"section-title": "Section Title",
"servings": "Servings",
"share-recipe-message": "I wanted to share my {0} recipe with you.",
"show-nutrition-values": "Show Nutrition Values",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar",
"title": "Title",
"total-time": "Total Time",
"unable-to-delete-recipe": "Unable to Delete Recipe",
"no-recipe": "No Recipe"
},
"search": {
"advanced-search": "Advanced Search",
"and": "and",
"exclude": "Exclude",
"include": "Include",
"max-results": "Max Results",
"or": "Or",
"results": "Results",
"search": "Search",
"search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter"
},
"settings": {
"add-a-new-theme": "Add a New Theme",
"admin-settings": "Admin Settings",
"backup": {
"backup-created-at-response-export_path": "Backup Created at {path}",
"backup-deleted": "Backup deleted",
"backup-tag": "Backup Tag",
"create-heading": "Create A Backup",
"delete-backup": "Delete Backup",
"error-creating-backup-see-log-file": "Error Creating Backup. See Log File",
"full-backup": "Full Backup",
"import-summary": "Import Summary",
"partial-backup": "Partial Backup",
"unable-to-delete-backup": "Unable to Delete Backup."
},
"backup-and-exports": "Backups",
"change-password": "Change Password",
"current": "Version:",
"custom-pages": "Custom Pages",
"edit-page": "Edit Page",
"events": "Events",
"first-day-of-week": "First day of the week",
"group-settings-updated": "Group Settings Updated",
"homepage": {
"all-categories": "All Categories",
"card-per-section": "Card Per Section",
"home-page": "Home Page",
"home-page-sections": "Home Page Sections",
"show-recent": "Show Recent"
},
"language": "Language",
"latest": "Latest",
"local-api": "Local API",
"locale-settings": "Locale settings",
"migrations": "Migrations",
"new-page": "New Page",
"notify": "Notify",
"organize": "Organize",
"page-name": "Page Name",
"pages": "Pages",
"profile": "Profile",
"remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries",
"set-new-time": "Set New Time",
"settings-update-failed": "Settings update failed",
"settings-updated": "Settings updated",
"site-settings": "Site Settings",
"theme": {
"accent": "Accent",
"dark": "Dark",
"default-to-system": "Default to system",
"error": "Error",
"error-creating-theme-see-log-file": "Error creating theme. See log file.",
"error-deleting-theme": "Error deleting theme",
"error-updating-theme": "Error updating theme",
"info": "Info",
"light": "Light",
"primary": "Primary",
"secondary": "Secondary",
"success": "Success",
"switch-to-dark-mode": "Switch to dark mode",
"switch-to-light-mode": "Switch to light mode",
"theme-deleted": "Theme deleted",
"theme-name": "Theme Name",
"theme-name-is-required": "Theme Name is required.",
"theme-saved": "Theme Saved",
"theme-updated": "Theme updated",
"warning": "Warning"
},
"token": {
"active-tokens": "ACTIVE TOKENS",
"api-token": "API Token",
"api-tokens": "API Tokens",
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again": "Copy this token for use with an external application. This token will not be viewable again.",
"create-an-api-token": "Create an API Token",
"token-name": "Token Name"
},
"toolbox": {
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign",
"new-name": "New Name",
"no-unused-items": "No Unused Items",
"recipes-affected": "No Recipes Affected|One Recipe Affected|{count} Recipes Affected",
"remove-unused": "Remove Unused",
"title-case-all": "Title Case All",
"toolbox": "Toolbox",
"unorganized": "Unorganized"
},
"webhooks": {
"test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL",
"webhooks-caps": "WEBHOOKS",
"webhooks": "Webhooks"
}
},
"shopping-list": {
"all-lists": "All Lists",
"create-shopping-list": "Create Shopping List",
"from-recipe": "From Recipe",
"list-name": "List Name",
"new-list": "New List",
"quantity": "Quantity: {0}",
"shopping-list": "Shopping List",
"shopping-lists": "Shopping Lists"
},
"sidebar": {
"all-recipes": "All Recipes",
"backups": "Backups",
"categories": "Categories",
"cookbooks": "Cookbooks",
"dashboard": "Dashboard",
"home-page": "Home Page",
"manage-users": "Manage Users",
"migrations": "Migrations",
"profile": "Profile",
"search": "Search",
"site-settings": "Site Settings",
"tags": "Tags",
"toolbox": "Toolbox",
"language": "Language"
},
"signup": {
"error-signing-up": "Error Signing Up",
"sign-up": "Sign Up",
"sign-up-link-created": "Sign up link created",
"sign-up-link-creation-failed": "Sign up link creation failed",
"sign-up-links": "Sign Up Links",
"sign-up-token-deleted": "Sign Up Token Deleted",
"sign-up-token-deletion-failed": "Sign up token deletion failed",
"welcome-to-mealie": "Welcome to Mealie! To become a user of this instance you are required to have a valid invitation link. If you haven't recieved an invitation you are unable to sign-up. To recieve a link, contact the sites administrator."
},
"tag": {
"tag-created": "Tag created",
"tag-creation-failed": "Tag creation failed",
"tag-deleted": "Tag deleted",
"tag-deletion-failed": "Tag deletion failed",
"tag-update-failed": "Tag update failed",
"tag-updated": "Tag updated",
"tags": "Tags",
"untagged-count": "Untagged {count}"
},
"tool": {
"tools": "Tools"
},
"user": {
"admin": "Admin",
"are-you-sure-you-want-to-delete-the-link": "Are you sure you want to delete the link <b>{link}<b/>?",
"are-you-sure-you-want-to-delete-the-user": "Are you sure you want to delete the user <b>{activeName} ID: {activeId}<b/>?",
"confirm-link-deletion": "Confirm Link Deletion",
"confirm-password": "Confirm Password",
"confirm-user-deletion": "Confirm User Deletion",
"could-not-validate-credentials": "Could Not Validate Credentials",
"create-link": "Create Link",
"create-user": "Create User",
"current-password": "Current Password",
"e-mail-must-be-valid": "E-mail must be valid",
"edit-user": "Edit User",
"email": "Email",
"error-cannot-delete-super-user": "Error! Cannot Delete Super User",
"existing-password-does-not-match": "Existing password does not match",
"full-name": "Full Name",
"invite-only": "Invite Only",
"link-id": "Link ID",
"link-name": "Link Name",
"login": "Login",
"logout": "Logout",
"manage-users": "Manage Users",
"new-password": "New Password",
"new-user": "New User",
"password-has-been-reset-to-the-default-password": "Password has been reset to the default password",
"password-must-match": "Password must match",
"password-reset-failed": "Password reset failed",
"password-updated": "Password updated",
"password": "Password",
"password-strength": "Password is {strength}",
"register": "Register",
"reset-password": "Reset Password",
"sign-in": "Sign in",
"total-mealplans": "Total MealPlans",
"total-users": "Total Users",
"upload-photo": "Upload Photo",
"use-8-characters-or-more-for-your-password": "Use 8 characters or more for your password",
"user-created": "User created",
"user-creation-failed": "User creation failed",
"user-deleted": "User deleted",
"user-id-with-value": "User ID: {id}",
"user-id": "User ID",
"user-password": "User Password",
"user-successfully-logged-in": "User Successfully Logged In",
"user-update-failed": "User update failed",
"user-updated": "User updated",
"user": "User",
"username": "Username",
"users-header": "USERS",
"users": "Users",
"webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled",
"you-are-not-allowed-to-create-a-user": "You are not allowed to create a user",
"you-are-not-allowed-to-delete-this-user": "You are not allowed to delete this user",
"enable-advanced-content": "Enable Advanced Content",
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
},
"language-dialog": {
"translated": "translated",
"choose-language": "Choose Language",
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
"read-the-docs": "Read the docs"
},
"data-pages": {
"seed-data": "Seed Data",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
},
"units": {
"seed-dialog-text": "Seed the database with common units based on your local language."
},
"labels": {
"seed-dialog-text": "Seed the database with common labels based on your local language."
}
},
"user-registration": {
"user-registration": "User Registration",
"join-a-group": "Join a Group",
"create-a-new-group": "Create a New Group",
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
"group-details": "Group Details",
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
"use-seed-data": "Use Seed Data",
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
"account-details": "Account Details"
},
"validation": {
"group-name-is-taken": "Group name is taken",
"username-is-taken": "Username is taken",
"email-is-taken": "Email is taken"
}
}

View File

@@ -8,7 +8,7 @@
"database-type": "Databasetype",
"database-url": "Database URL",
"default-group": "Standaardgroep",
"demo": "Demo",
"demo": "Voorbeeld",
"demo-status": "Demo status",
"development": "Versies in ontwikkeling",
"docs": "Documentatie",
@@ -33,7 +33,7 @@
"show-assets": "Toon Bijlagen"
},
"category": {
"categories": "Categories",
"categories": "Categorieën",
"category-created": "Categorie aangemaakt",
"category-creation-failed": "Categorie aanmaken mislukt",
"category-deleted": "Categorie Verwijderd",
@@ -69,7 +69,7 @@
"dashboard": "Dashboard",
"delete": "Verwijderen",
"disabled": "Uitgeschakeld",
"download": "Download",
"download": "Downloaden",
"edit": "Bewerken",
"enabled": "Ingeschakeld",
"exception": "Uitzondering",
@@ -83,7 +83,7 @@
"friday": "Vrijdag",
"general": "Algemeen",
"get": "Haal op",
"home": "Home",
"home": "Startpagina",
"image": "Afbeelding",
"image-upload-failed": "Afbeelding uploaden mislukt",
"import": "Importeren",
@@ -131,10 +131,10 @@
"view": "Weergave",
"wednesday": "Woensdag",
"yes": "Ja",
"foods": "Foods",
"units": "Units",
"back": "Back",
"next": "Next"
"foods": "Voedsel",
"units": "Eenheden",
"back": "Terug",
"next": "Volgende"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Weet je zeker dat je <b>{groupName}<b/> wilt verwijderen?",
@@ -156,8 +156,8 @@
"user-group-created": "Gebruikersgroep aangemaakt",
"user-group-creation-failed": "Aanmaken gebruikersgroep is mislukt",
"settings": {
"keep-my-recipes-private": "Keep My Recipes Private",
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
"keep-my-recipes-private": "Houd mijn recepten privé",
"keep-my-recipes-private-description": "Stelt je groep en alle recepten standaard privé in. Je kunt dit later nog wijzigen."
}
},
"meal-plan": {
@@ -217,8 +217,8 @@
"url-form-hint": "Kopieer en plak een link vanuit jouw favoriete receptwebsite",
"view-scraped-data": "Bekijk Opgehaalde Data",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
"trim-prefix-description": "Knip het eerste teken van elke regel bij",
"split-by-numbered-line-description": "Pogingen om een alinea te splitsen door de '1)' of '1.' patronen te gebruiken"
},
"page": {
"404-page-not-found": "404 Pagina niet gevonden",
@@ -248,6 +248,7 @@
"description": "Omschrijving",
"disable-amount": "Ingrediënt Hoeveelheden Uitschakelen",
"disable-comments": "Reacties Uitschakelen",
"edit-scale": "Bewerk Schaal",
"fat-content": "Vet",
"fiber-content": "Vezels",
"grams": "gram",
@@ -291,7 +292,7 @@
"title": "Titel",
"total-time": "Totale Tijd",
"unable-to-delete-recipe": "Kan recept niet verwijderen",
"no-recipe": "No Recipe"
"no-recipe": "Geen Recept"
},
"search": {
"advanced-search": "Geavanceerd Zoeken",
@@ -415,7 +416,7 @@
"all-recipes": "Recepten",
"backups": "Backups",
"categories": "Categorieën",
"cookbooks": "Cookbooks",
"cookbooks": "Koekboeken",
"dashboard": "Dashboard",
"home-page": "Home Pagina",
"manage-users": "Gebruikers",
@@ -424,8 +425,8 @@
"search": "Zoeken",
"site-settings": "Instellingen",
"tags": "Labels",
"toolbox": "Toolbox",
"language": "Language"
"toolbox": "Gereedschapskist",
"language": "Taal"
},
"signup": {
"error-signing-up": "Fout bij registreren",
@@ -448,7 +449,7 @@
"untagged-count": "Niet gelabeld {count}"
},
"tool": {
"tools": "Tools"
"tools": "Hulpmiddelen"
},
"user": {
"admin": "Beheerder",
@@ -467,7 +468,7 @@
"error-cannot-delete-super-user": "Fout! Kan supergebruiker niet verwijderen",
"existing-password-does-not-match": "Bestaande wachtwoord komt niet overeen",
"full-name": "Voor- en achternaam",
"invite-only": "Invite Only",
"invite-only": "Alleen op uitnodiging",
"link-id": "Koppeling ID",
"link-name": "Koppeling Naam",
"login": "Inloggen",
@@ -480,8 +481,8 @@
"password-reset-failed": "Wachtwoord resetten is mislukt",
"password-updated": "Wachtwoord bijgewerkt",
"password": "Wachtwoord",
"password-strength": "Password is {strength}",
"register": "Register",
"password-strength": "Wachtwoord is {strength}",
"register": "Registreren",
"reset-password": "Wachtwoord Herstellen",
"sign-in": "Inloggen",
"total-mealplans": "Totaal maaltijdplannen",
@@ -505,7 +506,7 @@
"webhooks-enabled": "Webhooks ingeschakeld",
"you-are-not-allowed-to-create-a-user": "Je hebt geen toestemming om een gebruiker aan te maken",
"you-are-not-allowed-to-delete-this-user": "Je hebt geen toestemming om deze gebruiker te verwijderen",
"enable-advanced-content": "Enable Advanced Content",
"enable-advanced-content": "Schakel geavanceerde instellingen in",
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
},
"language-dialog": {

View File

@@ -248,6 +248,7 @@
"description": "Beskrivelse",
"disable-amount": "Deaktiver Ingrediens-mengde",
"disable-comments": "Deaktiver kommentarer",
"edit-scale": "Edit Scale",
"fat-content": "Fett",
"fiber-content": "Kostfiber",
"grams": "gram",

View File

@@ -248,6 +248,7 @@
"description": "Opis",
"disable-amount": "Wyłącz ilości składników",
"disable-comments": "Wyłącz komentarze",
"edit-scale": "Edytuj skalę",
"fat-content": "Tłuszcz",
"fiber-content": "Błonnik",
"grams": "gram",
@@ -516,18 +517,18 @@
"read-the-docs": "Przeczytaj dokumentację"
},
"data-pages": {
"seed-data": "Seed Data",
"seed-data": "Dane przykładowe",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-dialog-text": "Połączenie wybranej żywności połączy źródło żywności i żywność docelową w pojedynczą żywność. Źródło żywności zostanie usunięte, a wszystkie odniesienia do źródłowej żywności zostaną zaktualizowane tak, aby wskazywały na docelową żywność.",
"merge-food-example": "Scalanie {food1} do {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
"seed-dialog-text": "Wypełnij bazę daniami na podstawie wybranego lokalnego języka. Akcja ta stworzy ponad 200 zwyczajowych potraw które mogą zostać użyte do organizacji Twojej bazy. Potrawy tłumaczone są przez wysiłek społeczeństwa.",
"seed-dialog-warning": "Posiadasz już wartości w bazie. Rozwiązanie problemu z duplikatami leżeć będzie w gestii użytkownika."
},
"units": {
"seed-dialog-text": "Seed the database with common units based on your local language."
"seed-dialog-text": "Wypełnij bazę zwyczajowymi jednostkami dla wybranego języka."
},
"labels": {
"seed-dialog-text": "Seed the database with common labels based on your local language."
"seed-dialog-text": "Wypełnij bazę zwyczajowymi etytkietami dla wybranego języka."
}
},
"user-registration": {
@@ -537,7 +538,7 @@
"provide-registration-token-description": "Podaj kod rejestracyjny powiązany z grupą do której chcesz dołączyć. Taki kod uzyskać możesz od użytkownika który przynależy już do owej grupy.",
"group-details": "Szczegóły grupy",
"group-details-description": "Zanim utworzysz konto musisz stworzyć grupę. Twoja grupa zawierać będzie tylko Ciebie, ale będziesz istniała możlwiość zaproszenia do niej innych. Użytkownicy Twojej grupy mogą współdzielić plany posiłków, listy zakupów, przepisy i więcej!",
"use-seed-data": "Use Seed Data",
"use-seed-data": "Użyj przykładowych danych",
"use-seed-data-description": "Mealie dostarcza zestaw posiłków, jednostek i opisów które mogą zostać użyte do zapełnienia Twojej grupy przydatnymi danymi do ogranizacji Twoich przepisów.",
"account-details": "Szczegóły konta"
},

View File

@@ -248,6 +248,7 @@
"description": "Descrição",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Desativar Comentários",
"edit-scale": "Edit Scale",
"fat-content": "Gordura",
"fiber-content": "Fibras",
"grams": "gramas",

View File

@@ -248,6 +248,7 @@
"description": "Descrição",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Описание",
"disable-amount": "Не показывать кол-во ингредиентов",
"disable-comments": "Отключить комментарии",
"edit-scale": "Edit Scale",
"fat-content": "Жиры",
"fiber-content": "Клетчатка",
"grams": "гр.",

View File

@@ -248,6 +248,7 @@
"description": "Popis",
"disable-amount": "Vypnúť množstvá surovín",
"disable-comments": "Vypnúť komentáre",
"edit-scale": "Edit Scale",
"fat-content": "Tuky",
"fiber-content": "Vlákniny",
"grams": "gramov",

View File

@@ -0,0 +1,550 @@
{
"about": {
"about": "O programu",
"about-mealie": "O programu Mealie",
"api-docs": "API dokumentacija",
"api-port": "API vrata",
"application-mode": "Način aplikacije",
"database-type": "Tip podatkovne baze",
"database-url": "URL naslov podatkovne baze",
"default-group": "Privzeta skupina",
"demo": "Testno",
"demo-status": "Status testa",
"development": "Razvoj",
"docs": "Dokumentacija",
"download-log": "Prenesi dnevniške zapise",
"download-recipe-json": "Zadnji prebran JSON",
"github": "Github",
"log-lines": "Vrstice",
"not-demo": "Ni testno",
"portfolio": "Portfelj",
"production": "Produkcija",
"support": "Podpora",
"version": "Verzija"
},
"asset": {
"assets": "Viri",
"code": "Koda",
"file": "Datoteka",
"image": "Slika",
"new-asset": "Nov vir",
"pdf": "PDF",
"recipe": "Recept",
"show-assets": "Prikaži vire"
},
"category": {
"categories": "Kategorije",
"category-created": "Kategorija kreirana",
"category-creation-failed": "Napaka pri kreiranju kategorije",
"category-deleted": "Kategorija izbrisana",
"category-deletion-failed": "Napaka pri brisanju kategorije",
"category-filter": "Filter kategorije",
"category-update-failed": "Napaka pri posodobitvi kategorije",
"category-updated": "Kategorija posodobljena",
"uncategorized-count": "Nekategorizirano {count}"
},
"events": {
"apprise-url": "Apprise URL",
"database": "Baza podatkov",
"delete-event": "Zbriši dogodek",
"new-notification-form-description": "Mealie uporablja Apprise knjižnico za kreiranje obvestil. Omogoča več različnih servisov za uporabo obvestil. Preglejte njihovo wiki stran, za bolj natančen vodič, kako izdelati URL za vaš servis. Če je na voljo, so za vaš izbran servis obvestil, na voljo tudi dodane možnosti.",
"new-version": "Na voljo je nova verzija!",
"notification": "Obvestila",
"refresh": "Osveži",
"scheduled": "Načrtovano",
"something-went-wrong": "Nekaj je šlo narobe!",
"subscribed-events": "Naročeni dogodki",
"test-message-sent": "Testno sporočilo je bilo poslano"
},
"general": {
"cancel": "Prekliči",
"clear": "Počisti",
"close": "Zapri",
"confirm": "Potrdi",
"confirm-delete-generic": "Ali ste prepričani, da želite to izbrisati?",
"copied": "Kopirano",
"create": "Ustvari",
"created": "Ustvarjeno",
"custom": "Po meri",
"dashboard": "Nadzorna plošča",
"delete": "Izbriši",
"disabled": "Onemogočeno",
"download": "Prenesi",
"edit": "Uredi",
"enabled": "Omogočeno",
"exception": "Izjema",
"failed-count": "Spodletelo: {count}",
"failure-uploading-file": "Napaka pri nalaganju datoteke",
"favorites": "Priljubljene",
"field-required": "Obvezno polje",
"file-folder-not-found": "Ne najdem datoteke/mape",
"file-uploaded": "Datoteka naložena",
"filter": "Filter",
"friday": "Petek",
"general": "Splošno",
"get": "Pridobi",
"home": "Doma",
"image": "Slika",
"image-upload-failed": "Nalaganje slike ni uspelo",
"import": "Uvozi",
"json": "JSON",
"keyword": "Ključna beseda",
"link-copied": "Povezava kopirana",
"loading-recipes": "Nalagam recepte",
"monday": "Ponedeljek",
"name": "Ime",
"new": "Novo",
"no": "Ne",
"no-recipe-found": "Ne najdem recepta",
"ok": "V redu",
"options": "Možnosti:",
"print": "Natisni",
"random": "Naključno",
"rating": "Ocena",
"recent": "Nedavno",
"recipe": "Recept",
"recipes": "Recepti",
"rename-object": "Preimenuj {0}",
"reset": "Ponastavi",
"saturday": "Sobota",
"save": "Shrani",
"settings": "Nastavitve",
"share": "Deli",
"shuffle": "Naključno",
"sort": "Razvrsti",
"sort-alphabetically": "Po abecedi",
"status": "Stanje",
"submit": "Pošlji",
"success-count": "Uspešno: {count}",
"sunday": "Nedelja",
"templates": "Predloge:",
"test": "Test",
"themes": "Teme",
"thursday": "Četrtek",
"token": "Žeton",
"tuesday": "Torek",
"type": "Tip",
"update": "Posodobi",
"updated": "Posodobljen",
"upload": "Naloži",
"url": "URL",
"view": "Poglej",
"wednesday": "Sreda",
"yes": "Da",
"foods": "Hrana",
"units": "Enote",
"back": "Nazaj",
"next": "Naprej"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Ste prepričani, da želite izbrisati <b>{groupName}<b/>?",
"cannot-delete-default-group": "Privzete skupine ni mogoče izbrisati",
"cannot-delete-group-with-users": "Ne morem izbrisati skupine z uporabniki",
"confirm-group-deletion": "Potrdi brisanje skupine",
"create-group": "Ustvari skupino",
"error-updating-group": "Napaka pri posodobitvi skupine",
"group": "Skupina",
"group-deleted": "Skupina izbrisana",
"group-deletion-failed": "Napaka pri brisanju skupine",
"group-id-with-value": "ID skupine: {groupID}",
"group-name": "Ime skupine",
"group-not-found": "Ne najdem skupine",
"group-with-value": "Skupina: {groupID}",
"groups": "Skupine",
"manage-groups": "Upravljanje skupin",
"user-group": "Uporabniška skupina",
"user-group-created": "Uporabniška skupina je ustvarjena",
"user-group-creation-failed": "Napaka pri ustvarjanju skupine",
"settings": {
"keep-my-recipes-private": "Moji recepti naj bodo privatni",
"keep-my-recipes-private-description": "Nastavi vaše skupine in vse recepte privzeto kot privatni. Kasneje lahko to vedno spremenite."
}
},
"meal-plan": {
"create-a-new-meal-plan": "Izdelaj nov načrt obrokov",
"dinner-this-week": "Večerje tega tedna",
"dinner-today": "Današnja večerja",
"dinner-tonight": "DANAŠNJA VEČERJA",
"edit-meal-plan": "Uredi planer obrokov",
"end-date": "Končni datum",
"group": "Skupina (beta)",
"main": "Glavna jed",
"meal-planner": "Načrtovanje obrokov",
"meal-plans": "Načrti obrokov",
"mealplan-categories": "KATEGORIJE NAČRTA OBROKA",
"mealplan-created": "Načrt obroka je ustvarjen",
"mealplan-creation-failed": "Napaka pri ustvarjanju načrta obroka",
"mealplan-deleted": "Načrt obroka je izbrisan",
"mealplan-deletion-failed": "Napaka pri izbrisu načrta obroka",
"mealplan-settings": "Nastavitve načrta obroka",
"mealplan-update-failed": "Napaka pri posodobitvi načrta obroka",
"mealplan-updated": "Načrt obroka je posodobljen",
"no-meal-plan-defined-yet": "Za danes ni definiranjega načrta obroka",
"no-meal-planned-for-today": "Za danes ni planiranega načrta obroka",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Samo recepti v teh kategorija bodo uporabljeni v načrtu obroka",
"planner": "Načrtovalec",
"quick-week": "Hiter pogled tedna",
"side": "Priloga",
"sides": "Priloge",
"start-date": "Datum začetka"
},
"migration": {
"chowdown": {
"description": "Migriraj podatke iz Chowdown-a",
"title": "Chowdown"
},
"migration-data-removed": "Migrirani podatki so odstranjeni",
"nextcloud": {
"description": "Migriraj podatke iz Nextcloud Cookbook",
"title": "Nextcloud Cookbook"
},
"no-migration-data-available": "Na volji ni migracijskih podatkov",
"recipe-migration": "Migracija recepta"
},
"new-recipe": {
"bulk-add": "Množično dodajanje",
"error-details": "Samo spletne strani, ki vsebujejo Id+json ali mikro podatke, se lahko uvozijo v Mealie. Večina večjih spletnih strani z recepti že podpirajo to podatkovno strukturo. Če se vaša stran ne uvozi, v log datoteki pa so json podatki, prosim odprite težavo na github-u z URL naslovom in podatki.",
"error-title": "Kot kaže nisem ničesar našel",
"from-url": "Uvozi recept",
"github-issues": "GitHub težave",
"google-ld-json-info": "Google Id+json podatki",
"must-be-a-valid-url": "Mora biti veljaven URL",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Prilepi podatke recepta. Vsaka vrsta bo obravnavana kot element v seznamu",
"recipe-markup-specification": "Markup specifikacija recepta",
"recipe-url": "URL recepta",
"upload-a-recipe": "Naloži recept",
"upload-individual-zip-file": "Naloži posamezno .zip datoteko, izvoženo iz druge Mealie namestitve.",
"url-form-hint": "Kopiraj in prilepi povezavo iz vaše priljubljene strani z recepti",
"view-scraped-data": "Poglej postrgane podatke",
"trim-whitespace-description": "Poreži začetne in končne presledke, kot tudi prazne vrstice",
"trim-prefix-description": "Poreži prvi znak v vsaki vrstici",
"split-by-numbered-line-description": "Poskušaj razdeliti odstavek z ujemanjem '1)' ali '1.' vzorcev"
},
"page": {
"404-page-not-found": "404 strani ni mogoče najti",
"all-recipes": "Vsi recepti",
"new-page-created": "Ustvarjena nova stran",
"page": "Stran",
"page-creation-failed": "Ustvarjanje strani ni uspelo",
"page-deleted": "Stran izbirsana",
"page-deletion-failed": "Napaka pri brisanju strani",
"page-update-failed": "Napaka pri posodobitvi strani",
"page-updated": "Stran je posodobljena",
"pages-update-failed": "Napaka pri posodobitvi strani",
"pages-updated": "Strani posodobljene"
},
"recipe": {
"add-key": "Dodajte ključ",
"add-to-favorites": "Dodajte med priljubljene",
"api-extras": "API dodatno",
"calories": "Kalorije",
"calories-suffix": "kalorije",
"carbohydrate-content": "Ogljikovi hidrati",
"categories": "Kategorije",
"comment-action": "Pripomba",
"comments": "Pripombe",
"delete-confirmation": "Ali želite izbrisati ta recept?",
"delete-recipe": "Izbriši recept",
"description": "Opis",
"disable-amount": "Onemogoči prikaz količin sestavin",
"disable-comments": "Onemogoči komentarje",
"edit-scale": "Uredi stran",
"fat-content": "Maščoba",
"fiber-content": "Vlakna",
"grams": "grami",
"ingredient": "Sestavina",
"ingredients": "Sestavine",
"insert-section": "Vstavi odsek",
"instructions": "Navodila",
"key-name-required": "Obvezen vnos imena ključa",
"landscape-view-coming-soon": "Ležeči pogled",
"milligrams": "miligrami",
"new-key-name": "Novo ime ključa",
"no-white-space-allowed": "Presledki niso dovoljeni",
"note": "Zapisek",
"nutrition": "Prehrana",
"object-key": "Ključ objekta",
"object-value": "Vrednost objekta",
"original-url": "Izvorni URL",
"perform-time": "Čas kuhanja",
"prep-time": "Čas priprav",
"protein-content": "Beljakovine",
"public-recipe": "Javen recept",
"recipe-created": "Recept je ustvarjen",
"recipe-creation-failed": "Napaka pri ustvarjanju recepta",
"recipe-deleted": "Recept je izbrisan",
"recipe-image": "Slika recepta",
"recipe-image-updated": "Slika recepta je posodobljena",
"recipe-name": "Ime recepta",
"recipe-settings": "Nastavitve recepta",
"recipe-update-failed": "Napaka pri posodobitvi recepta",
"recipe-updated": "Recept je posodobljen",
"remove-from-favorites": "Odstrani iz priljubljenih",
"remove-section": "Odstrani odsek",
"save-recipe-before-use": "Shrani recept pred uporabo",
"section-title": "Naslov odseka",
"servings": "Porcija",
"share-recipe-message": "Rad bi delil moj {0} recept z vami.",
"show-nutrition-values": "Prikaži vrednosti prehrane",
"sodium-content": "Natrij",
"step-index": "Korak: {step}",
"sugar-content": "Sladkor",
"title": "Naslov",
"total-time": "Skupni čas",
"unable-to-delete-recipe": "Ne morem izbrisati recepta",
"no-recipe": "Ni recepta"
},
"search": {
"advanced-search": "Napredno iskanje",
"and": "in",
"exclude": "Izvzemi",
"include": "Vključi",
"max-results": "Največ rezultatov",
"or": "Ali",
"results": "Rezultati",
"search": "Iskanje",
"search-mealie": "Išči po Mealie (pritisni /)",
"search-placeholder": "Išči...",
"tag-filter": "Filter oznak"
},
"settings": {
"add-a-new-theme": "Dodaj novo temo",
"admin-settings": "Administratorske nastavitve",
"backup": {
"backup-created-at-response-export_path": "Varnostna kopija ustvarjena v {path}",
"backup-deleted": "Varnostna kopija je izbrisana",
"backup-tag": "Oznaka varnostne kopije",
"create-heading": "Izdelaj varnostno kopijo",
"delete-backup": "Izbriši varnostno kopijo",
"error-creating-backup-see-log-file": "Napaka pri izdelovanju varnostni kopije. Preveri strežniške datoteke",
"full-backup": "Popolna varnostna kopija",
"import-summary": "Povzetek uvoza",
"partial-backup": "Delna varnostna kopija",
"unable-to-delete-backup": "Napaka pri izbrisu varnostne kopije."
},
"backup-and-exports": "Varnostne kopije",
"change-password": "Spremeni geslo",
"current": "Verzija:",
"custom-pages": "Strani po meri",
"edit-page": "Uredi stran",
"events": "Dogodki",
"first-day-of-week": "Prvi dan v tednu",
"group-settings-updated": "Nastavitve skupine so bile posodobljene",
"homepage": {
"all-categories": "Vse kategorije",
"card-per-section": "Kartica po odsekih",
"home-page": "Domača stran",
"home-page-sections": "Odseki domače strani",
"show-recent": "Pokaži nedavne"
},
"language": "Jezik",
"latest": "Najnovejše",
"local-api": "Lokalni API",
"locale-settings": "Področne nastavitve",
"migrations": "Migracija",
"new-page": "Nova stran",
"notify": "Obvesti",
"organize": "Organiziraj",
"page-name": "Ime strani",
"pages": "Strani",
"profile": "Profil",
"remove-existing-entries-matching-imported-entries": "Odstrani vrednosti, ki se ujemajo z vnesenimi vrednostmi",
"set-new-time": "Nastavi nov čas",
"settings-update-failed": "Napaka pri posodobitvi nastavitev",
"settings-updated": "Nastavitve so posodobljene",
"site-settings": "Nastavitve strani",
"theme": {
"accent": "Naglas",
"dark": "Temno",
"default-to-system": "Privzeto glede na sistem",
"error": "Napaka",
"error-creating-theme-see-log-file": "Napaka pri izdelavi teme. Preveri dnevniško datoteko.",
"error-deleting-theme": "Napaka pri brisanju teme",
"error-updating-theme": "Napaka pri posodobitvi teme",
"info": "Informacije",
"light": "Svetlo",
"primary": "Primarno",
"secondary": "Sekundarno",
"success": "Uspešno",
"switch-to-dark-mode": "Preklopi na temni način",
"switch-to-light-mode": "Preklopi na svetli način",
"theme-deleted": "Tema je bila izbrisana",
"theme-name": "Ime teme",
"theme-name-is-required": "Ime teme je obvezen podatek.",
"theme-saved": "Tema je shranjena",
"theme-updated": "Tema posodobljena",
"warning": "Opozorilo"
},
"token": {
"active-tokens": "AKTIVNI ŽETONI",
"api-token": "API žeton",
"api-tokens": "API žetoni",
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again": "Kopiraj žeton za uporabo v zunanji aplikaciji. Ta žeton kasneje ne bo več viden.",
"create-an-api-token": "Ustvari nov API žeton",
"token-name": "Ime žetona"
},
"toolbox": {
"assign-all": "Dodeli vse",
"bulk-assign": "Množično dodeljevanje",
"new-name": "Novo ime",
"no-unused-items": "Ni neuporabljeni elementov",
"recipes-affected": "Ne vpliva na recepte|Vpliva na en recept|Vpliva na {count} recepte",
"remove-unused": "Odstrani neuporabljene",
"title-case-all": "Naziv z veliko",
"toolbox": "Orodjarna",
"unorganized": "Neorganizirano"
},
"webhooks": {
"test-webhooks": "Test Webhook-ov",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Spodnji URL naslovi bodo prejeli webhook-e s podatki receptov za načrt obrokov na načrtovani dan. Trenutno se bodo webhooki-i izvedli ob",
"webhook-url": "Webhook URL",
"webhooks-caps": "WEBHOOKS",
"webhooks": "Webbhook-i"
}
},
"shopping-list": {
"all-lists": "Vsi seznami",
"create-shopping-list": "Ustvarite nakupovalni seznam",
"from-recipe": "Iz recepta",
"list-name": "Ime seznama",
"new-list": "Nov seznam",
"quantity": "Količina: {0}",
"shopping-list": "Nakupovalni seznam",
"shopping-lists": "Nakupovalni seznami"
},
"sidebar": {
"all-recipes": "Vsi recepti",
"backups": "Varnostne kopije",
"categories": "Kategorije",
"cookbooks": "Kuharska knjiga",
"dashboard": "Nadzorna plošča",
"home-page": "Domača stran",
"manage-users": "Uporabniki",
"migrations": "Migracija",
"profile": "Profil",
"search": "Iskanje",
"site-settings": "Nastavitve strani",
"tags": "Značke",
"toolbox": "Orodjarna",
"language": "Jezik"
},
"signup": {
"error-signing-up": "Napaka pri vpisu",
"sign-up": "Vpis",
"sign-up-link-created": "Povezava vpisa je ustvarjena",
"sign-up-link-creation-failed": "Napaka pri kreiranju povezave vpisa",
"sign-up-links": "Vpisne povezave",
"sign-up-token-deleted": "Vpisni ključ je izbrisan",
"sign-up-token-deletion-failed": "Napaka pri izbrisu vpisnega ključa",
"welcome-to-mealie": "Dobrodobšli v Meali! Če želite postati uporabnik te namestitve, potrebujete imeti veljavno povezavo povabila. Če še niste prejeli povabila se ne boste mogli vpisati. Če želite prejeti povabilo, kontaktiraje administratorja strani."
},
"tag": {
"tag-created": "Značka kreirana",
"tag-creation-failed": "Napaka pri kreiranju značke",
"tag-deleted": "Značna izbrisana",
"tag-deletion-failed": "Napaka pri izbrisu značke",
"tag-update-failed": "Napaka pri posodobitve značke",
"tag-updated": "Značka posodobljena",
"tags": "Značke",
"untagged-count": "Brez značke {count}"
},
"tool": {
"tools": "Orodja"
},
"user": {
"admin": "Administrator",
"are-you-sure-you-want-to-delete-the-link": "Ste prepričani, da želite izbrisati povezavo <b>{link}<b/>?",
"are-you-sure-you-want-to-delete-the-user": "Ste prepričani, da želite izbrisati uporabnika <b>{activeName}<b/> ID: {activeId}<b/>?",
"confirm-link-deletion": "Potrdite izbris povezave",
"confirm-password": "Potrdite geslo",
"confirm-user-deletion": "Potrdite izbris uporabnika",
"could-not-validate-credentials": "Ne morem potrditi poverilnic",
"create-link": "Ustvarite povezavo",
"create-user": "Ustvarite uporabnika",
"current-password": "Trenutno geslo",
"e-mail-must-be-valid": "E-mail mora biti veljaven",
"edit-user": "Uredi uporabnika",
"email": "E-mail",
"error-cannot-delete-super-user": "Napaka! Ne morem izbrisati super uporabnika",
"existing-password-does-not-match": "Ponovljeno geslo ne ustreza prvemu",
"full-name": "Polno ime",
"invite-only": "Samo na povabilo",
"link-id": "ID povezave",
"link-name": "Ime povezave",
"login": "Prijava",
"logout": "Odjava",
"manage-users": "Upravljanje uporabnikov",
"new-password": "Novo geslo",
"new-user": "Nov uporabnik",
"password-has-been-reset-to-the-default-password": "Geslo je bila nastavljeno na privzeto geslo",
"password-must-match": "Gesli se morata ujemati",
"password-reset-failed": "Napaka pri ponastavitvi gesla",
"password-updated": "Geslo posodobljeno",
"password": "Geslo",
"password-strength": "Moč gesla {strength}",
"register": "Registriraj se",
"reset-password": "Ponastavi geslo",
"sign-in": "Vpis",
"total-mealplans": "Skupaj načrtov obrokov",
"total-users": "Skupaj uporabnikov",
"upload-photo": "Naloži fotografijo",
"use-8-characters-or-more-for-your-password": "Uporabi vsaj 8 znakov ali več za vaše geslo",
"user-created": "Uporabnik ustvarjen",
"user-creation-failed": "Napaka pri ustvarjanju uporabnika",
"user-deleted": "Uporabnik izbrisan",
"user-id-with-value": "ID uporabnika: {id}",
"user-id": "ID uporabnika",
"user-password": "Uporabniško geslo",
"user-successfully-logged-in": "Uporabnik uspešno prijavljen",
"user-update-failed": "Napaka pri posodobitvi uporabnika",
"user-updated": "Uporabnik posodobljen",
"user": "Uporabnik",
"username": "Uporabniško ime",
"users-header": "UPORABNIKI",
"users": "Uporabniki",
"webhook-time": "Webhook čas",
"webhooks-enabled": "Webhook-i omogočeni",
"you-are-not-allowed-to-create-a-user": "Nimate pravic za ustvarjanje uporabnika",
"you-are-not-allowed-to-delete-this-user": "Nimate pravic za izbris tega uporabnika",
"enable-advanced-content": "Omogoči napredne nastavitve",
"enable-advanced-content-description": "Omogoči napredne nastavitve kot so spreminjanje merila receptov, API ključi, Webhook in upravljanje s podatki. Ne skrbite, vse to lahko spremenite kasneje"
},
"language-dialog": {
"translated": "prevedeno",
"choose-language": "Izberite jezik",
"select-description": "Izberite jezik Meali uporabniškega vmesnika. Nastavitve so v uporabi samo za vas in ne za ostale uporabnike.",
"how-to-contribute-description": "Če kaj ni prevedeno, napačno prevedeno ali vaš jezik v celoti manjka iz seznama? Poglejte si {read-the-docs-link} kako lahko pomagate!",
"read-the-docs": "Preberite dokumentacijo"
},
"data-pages": {
"seed-data": "Napolni podatke",
"foods": {
"merge-dialog-text": "Združitev izbranih jedi bo združila izvorno jed in ciljno jed v eno samo jed. Izvorna jed bo izbrisana in vse povezave na izvorno jed, bodo po novem kazale na ciljno jed.",
"merge-food-example": "Združujem {food1} v {food2}",
"seed-dialog-text": "Napolni podatkovno bazo s jedmi, ki izvirajo iz vašega lokalnega jezika. To bo kreiralo 200+ običajnih jedi, ki se lahko uporabijo za organizacijo vaše podatkovne baze. Jedi so prevedene s pomočjo skupnosti.",
"seed-dialog-warning": "Nekatere elemente že imate v podatkovni bazi. To opravilo ne bo upoštevalo dvojnikov in jih boste morali sami ročno upravljati."
},
"units": {
"seed-dialog-text": "Napolni podatkovno bazo z običajnimi enotami, glede na vaš lokalni jezik."
},
"labels": {
"seed-dialog-text": "Napolni podatkovno bazi s običajnimi oznakami, glede na vaš lokalni jezik."
}
},
"user-registration": {
"user-registration": "Registracija uporabnika",
"join-a-group": "Pridruži se skupini",
"create-a-new-group": "Ustvarite novo skupino",
"provide-registration-token-description": "Prosim pridobite registracijski žeton povezan s skupino, ki se ji želite pridružiti. Pridobiti ga boste morali od obstoječega člana skupine.",
"group-details": "Detajli skupine",
"group-details-description": "Preden kreirate račun, morate kreirati skupino. V skupini boste sprva samo vi, vendar imate možnost povabiti še ostale člane. Člani v vaši skupini lahko delijo načrte obrokov, nakupovalne sezname, recepte in še več!",
"use-seed-data": "Uporabi privzete podatke",
"use-seed-data-description": "Meali vključuje zbirko jedi, enot in oznak, ki se lahko uporabno uporabijo v vaši skupini za organizacijo receptov.",
"account-details": "Podatki o računu"
},
"validation": {
"group-name-is-taken": "Ime skupine je že zasedeno",
"username-is-taken": "Uporabniško ime zasedeno",
"email-is-taken": "E-mail je zaseden"
}
}

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -9,7 +9,7 @@
"database-url": "Databas URL",
"default-group": "Standardgrupp",
"demo": "Demo",
"demo-status": "Demo Status",
"demo-status": "Status för demo",
"development": "Utveckling",
"docs": "Dokumentation",
"download-log": "Ladda ner logg",
@@ -33,7 +33,7 @@
"show-assets": "Visa tillgångar"
},
"category": {
"categories": "Categories",
"categories": "Kategorier",
"category-created": "Kategori skapad",
"category-creation-failed": "Kategori gick inte att skapa",
"category-deleted": "Kategori raderad",
@@ -66,7 +66,7 @@
"create": "Skapa",
"created": "Skapad",
"custom": "Anpassad",
"dashboard": "Dashboard",
"dashboard": "Startsida",
"delete": "Ta bort",
"disabled": "Inaktiverad",
"download": "Ladda ner",
@@ -131,10 +131,10 @@
"view": "Visa",
"wednesday": "Onsdag",
"yes": "Ja",
"foods": "Foods",
"units": "Units",
"back": "Back",
"next": "Next"
"foods": "Mat",
"units": "Enheter",
"back": "Tillbaka",
"next": "Nästa"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Är du säker på att du vill radera <b>{groupName}<b/>?",
@@ -156,8 +156,8 @@
"user-group-created": "Användargrupp skapad",
"user-group-creation-failed": "Gruppen gick inte att skapa",
"settings": {
"keep-my-recipes-private": "Keep My Recipes Private",
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
"keep-my-recipes-private": "Behåll mina recept privata",
"keep-my-recipes-private-description": "Sätter din grupp och alla recept till privata som förval. Du kan alltid ändra detta senare."
}
},
"meal-plan": {
@@ -216,9 +216,9 @@
"upload-individual-zip-file": "Ladda upp en individuell .zip-fil som exporteras från en annan Mealie-instans.",
"url-form-hint": "Kopiera och klistra in en länk från din favorit recept webbplats",
"view-scraped-data": "Visa skrotade data",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
"trim-whitespace-description": "Ta bort inledande och avslutande blanksteg samt tomma rader",
"trim-prefix-description": "Ta bort första tecknet från varje rad",
"split-by-numbered-line-description": "Försök att dela ett stycke genom att matcha mönstret '1)' eller '1.'"
},
"page": {
"404-page-not-found": "404 sidan hittades inte",
@@ -236,7 +236,7 @@
"recipe": {
"add-key": "Lägg till nyckel",
"add-to-favorites": "Lägg till i favoriter",
"api-extras": "API Extras",
"api-extras": "API-tillägg",
"calories": "Kalorier",
"calories-suffix": "kalorier",
"carbohydrate-content": "Kolhydrat",
@@ -248,8 +248,9 @@
"description": "Beskrivning",
"disable-amount": "Inaktivera ingredienser mängder",
"disable-comments": "Inaktivera kommentarer",
"edit-scale": "Ändra skala",
"fat-content": "Fett",
"fiber-content": "Fiber",
"fiber-content": "Fibrer",
"grams": "gram",
"ingredient": "Ingrediens",
"ingredients": "Ingredienser",
@@ -291,7 +292,7 @@
"title": "Titel",
"total-time": "Total tid",
"unable-to-delete-recipe": "Gick inte att radera receptet",
"no-recipe": "No Recipe"
"no-recipe": "Inget recept"
},
"search": {
"advanced-search": "Avancerad sökning",
@@ -360,7 +361,7 @@
"error-creating-theme-see-log-file": "Fel vid skapande av tema. Se loggfil.",
"error-deleting-theme": "Fel vid borttagning av tema",
"error-updating-theme": "Gick inte att uppdatera tema",
"info": "Info",
"info": "Information",
"light": "Ljust",
"primary": "Primär",
"secondary": "Sekundär",
@@ -396,7 +397,7 @@
"webhooks": {
"test-webhooks": "Testa Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Följande webbadresser kommer att mottaga webhooks med receptdata för dagens planerade måltid. För närvarande körs webhooks klockan",
"webhook-url": "Webhook URL",
"webhook-url": "Webhook-URL",
"webhooks-caps": "WEBHOOKS",
"webhooks": "Webhooks"
}
@@ -413,9 +414,9 @@
},
"sidebar": {
"all-recipes": "Alla recept",
"backups": "Backups",
"backups": "Säkerhetskopior",
"categories": "Kategorier",
"cookbooks": "Cookbooks",
"cookbooks": "Kokböcker",
"dashboard": "Startsida",
"home-page": "Startsida",
"manage-users": "Användare",
@@ -425,7 +426,7 @@
"site-settings": "Inställningar",
"tags": "Taggar",
"toolbox": "Verktygslåda",
"language": "Language"
"language": "Språk"
},
"signup": {
"error-signing-up": "Fel vid registreringen",
@@ -448,7 +449,7 @@
"untagged-count": "Otaggad {count}"
},
"tool": {
"tools": "Tools"
"tools": "Verktyg"
},
"user": {
"admin": "Administratör",
@@ -467,10 +468,10 @@
"error-cannot-delete-super-user": "Fel! Det går inte att ta bort superanvändare",
"existing-password-does-not-match": "Befintligt lösenord matchar inte",
"full-name": "Fullständigt namn",
"invite-only": "Invite Only",
"invite-only": "Endast inbjudna",
"link-id": "Länk ID",
"link-name": "Länk namn",
"login": "Login",
"login": "Logga in",
"logout": "Logga ut",
"manage-users": "Hantera användare",
"new-password": "Nytt lösenord",
@@ -480,8 +481,8 @@
"password-reset-failed": "Återställningen av lösenordet misslyckades",
"password-updated": "Lösenord uppdaterat",
"password": "Lösenord",
"password-strength": "Password is {strength}",
"register": "Register",
"password-strength": "Lösenordsstyrka {strength}",
"register": "Registrering",
"reset-password": "Ändra lösenord",
"sign-in": "Logga in",
"total-mealplans": "Antal måltidsplaner",
@@ -505,45 +506,45 @@
"webhooks-enabled": "Webhooks aktiverat",
"you-are-not-allowed-to-create-a-user": "Du har inte behörighet att skapa en användare",
"you-are-not-allowed-to-delete-this-user": "Du har inte behörighet att radera denna användare",
"enable-advanced-content": "Enable Advanced Content",
"enable-advanced-content-description": "Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you can always change this later"
"enable-advanced-content": "Aktivera avancerade funktioner",
"enable-advanced-content-description": "Aktiverar avancerade funktioner som receptskalning, API-nycklar, Webhooks och datahantering. Oroa dig inte, du kan alltid ändra detta senare"
},
"language-dialog": {
"translated": "translated",
"choose-language": "Choose Language",
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
"read-the-docs": "Read the docs"
"translated": "översatt",
"choose-language": "Välj språk",
"select-description": "Välj språk för Mealie användargränssnitt. Inställningen gäller endast för dig, inte andra användare.",
"how-to-contribute-description": "Är något inte översatt ännu, felöversatt eller saknas ditt språk i listan? {read-the-docs-link} om hur man bidrar!",
"read-the-docs": "Läs dokumentationen"
},
"data-pages": {
"seed-data": "Seed Data",
"seed-data": "Exempeldata",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
"merge-dialog-text": "Kombinera valda livsmedel kommer att slå samman de valda livsmedlen till ett livsmedel. Ursprungslivsmedlet kommer att raderas och alla hänvisningar till detta kommer att uppdateras för att peka på det kombinerade livsmedlet.",
"merge-food-example": "Slå ihop {food1} till {food2}",
"seed-dialog-text": "Fyll databasen med livsmedel baserade på ditt språk. Detta kommer att skapa 200+ vanliga livsmedel som kan användas för att organisera din databas. Livsmedlen översätts via ett gemenskapsinsats.",
"seed-dialog-warning": "Du har redan några objekt i din databas. Denna åtgärd kommer inte att förena dubbletter, du kommer att behöva hantera dem manuellt."
},
"units": {
"seed-dialog-text": "Seed the database with common units based on your local language."
"seed-dialog-text": "Fyll databasen med vanliga enheter baserade på ditt språk."
},
"labels": {
"seed-dialog-text": "Seed the database with common labels based on your local language."
"seed-dialog-text": "Fyll databasen med vanliga etiketter baserade på ditt språk."
}
},
"user-registration": {
"user-registration": "User Registration",
"join-a-group": "Join a Group",
"create-a-new-group": "Create a New Group",
"provide-registration-token-description": "Please provide the registration token associated with the group that you'd like to join. You'll need to obtain this from an existing group member.",
"group-details": "Group Details",
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
"use-seed-data": "Use Seed Data",
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes.",
"account-details": "Account Details"
"user-registration": "Användarregistrering",
"join-a-group": "Gå med i en grupp",
"create-a-new-group": "Skapa en ny grupp",
"provide-registration-token-description": "Ange registreringstoken som är kopplad till den grupp som du vill gå med. Du måste få detta från en befintlig gruppmedlem.",
"group-details": "Gruppuppgifter",
"group-details-description": "Innan du skapar ett konto måste du skapa en grupp. Din grupp kommer bara att innehålla dig, men du kommer att kunna bjuda in andra senare. Medlemmarna i din grupp kan dela måltidsplaner, inköpslistor, recept och mycket mer!",
"use-seed-data": "Använd exempeldata",
"use-seed-data-description": "Mealie innehåller en samling av livsmedel, enheter och etiketter som kan användas för att fylla din grupp med användbara data för att organisera dina recept.",
"account-details": "Kontouppgifter"
},
"validation": {
"group-name-is-taken": "Group name is taken",
"username-is-taken": "Username is taken",
"email-is-taken": "Email is taken"
"group-name-is-taken": "Gruppnamnet är upptaget",
"username-is-taken": "Användarnamnet är upptaget",
"email-is-taken": "E-postadressen är upptagen"
}
}

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "Опис",
"disable-amount": "Сховати кількість інгредієнтів",
"disable-comments": "Вимкнути коментарі",
"edit-scale": "Maßeinheiten bearbeiten",
"fat-content": "Жири",
"fiber-content": "Волокна",
"grams": "грами",

View File

@@ -248,6 +248,7 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",
"grams": "grams",

View File

@@ -248,6 +248,7 @@
"description": "描述",
"disable-amount": "关闭显示成分数量",
"disable-comments": "禁用评论",
"edit-scale": "Edit Scale",
"fat-content": "脂肪",
"fiber-content": "纤维",
"grams": "克",

View File

@@ -248,6 +248,7 @@
"description": "描述",
"disable-amount": "停用成分數量",
"disable-comments": "關閉留言",
"edit-scale": "Edit Scale",
"fat-content": "脂肪",
"fiber-content": "纖維",
"grams": "克",

View File

@@ -15,17 +15,17 @@
},
"dependencies": {
"@adapttive/vue-markdown": "^4.0.1",
"@mdi/js": "^5.9.55",
"@mdi/js": "^6.7.96",
"@nuxtjs/auth-next": "5.0.0-1624817847.21691f1",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/i18n": "^7.0.3",
"@nuxtjs/i18n": "7.0.3",
"@nuxtjs/proxy": "^2.1.0",
"@nuxtjs/pwa": "^3.3.5",
"@vue/composition-api": "^1.6.2",
"@vueuse/core": "^8.5.0",
"core-js": "^3.15.1",
"date-fns": "^2.23.0",
"fuse.js": "^6.5.3",
"@vueuse/core": "^9.0.2",
"core-js": "^3.23.1",
"date-fns": "^2.28.0",
"fuse.js": "^6.6.2",
"isomorphic-dompurify": "^0.19.0",
"nuxt": "^2.15.8",
"v-jsoneditor": "^1.4.5",
@@ -42,15 +42,15 @@
"@nuxtjs/google-fonts": "^1.3.0",
"@nuxtjs/vuetify": "^1.12.1",
"@types/sortablejs": "^1.13.0",
"@vue/runtime-dom": "^3.2.36",
"@vue/runtime-dom": "^3.2.37",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-nuxt": "^3.2.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.0.1",
"lint-staged": "^12.4.2",
"lint-staged": "^13.0.2",
"nuxt-vite": "0.2.3",
"prettier": "^2.3.2",
"prettier": "^2.7.1",
"vue2-script-setup-transform": "^0.3.5"
}
}

View File

@@ -54,6 +54,35 @@
</v-card-text>
</BaseDialog>
<!-- Create Dialog -->
<BaseDialog
v-model="createDialog"
:icon="$globals.icons.foods"
title="Create Food"
:submit-text="$tc('general.save')"
@submit="createFood"
>
<v-card-text>
<v-form ref="domNewFoodForm">
<v-text-field
v-model="createTarget.name"
autofocus
label="Name"
:rules="[validators.required]"
></v-text-field>
<v-text-field v-model="createTarget.description" label="Description"></v-text-field>
<v-autocomplete
v-model="createTarget.labelId"
clearable
:items="allLabels"
item-value="id"
item-text="name"
label="Food Label"
>
</v-autocomplete>
</v-form> </v-card-text
></BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="editDialog"
@@ -63,7 +92,7 @@
@submit="editSaveFood"
>
<v-card-text v-if="editTarget">
<v-form ref="domCreateFoodForm">
<v-form ref="domNewFoodForm">
<v-text-field v-model="editTarget.name" label="Name" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="editTarget.description" label="Description"></v-text-field>
<v-autocomplete
@@ -100,8 +129,10 @@
:bulk-actions="[]"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@create-one="createEventHandler"
>
<template #button-row>
<BaseButton create @click="createDialog = true" />
<BaseButton @click="mergeDialog = true">
<template #icon> {{ $globals.icons.foods }} </template>
Combine
@@ -128,10 +159,11 @@ import { computed } from "vue-demi";
import type { LocaleObject } from "@nuxtjs/i18n";
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
import { IngredientFood } from "~/types/api-types/recipe";
import { CreateIngredientFood, IngredientFood } from "~/types/api-types/recipe";
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
import { useLocales } from "~/composables/use-locales";
import { useFoodStore, useLabelStore } from "~/composables/store";
import { VForm } from "~/types/vuetify";
export default defineComponent({
components: { MultiPurposeLabel },
@@ -166,6 +198,34 @@ export default defineComponent({
const foodStore = useFoodStore();
// ===============================================================
// Food Creator
const domNewFoodForm = ref<VForm>();
const createDialog = ref(false);
const createTarget = ref<CreateIngredientFood>({
name: "",
});
function createEventHandler() {
createDialog.value = true;
}
async function createFood() {
if (!createTarget.value || !createTarget.value.name) {
return;
}
// @ts-expect-error the createOne function erroneously expects an id because it uses the IngredientFood type
await foodStore.actions.createOne(createTarget.value);
createDialog.value = false;
domNewFoodForm.value?.reset();
createTarget.value = {
name: "",
};
}
// ===============================================================
// Food Editor
@@ -262,6 +322,11 @@ export default defineComponent({
foods: foodStore.foods,
allLabels,
validators,
// Create
createDialog,
createEventHandler,
createFood,
createTarget,
// Edit
editDialog,
editEventHandler,

View File

@@ -65,7 +65,7 @@
<v-card-actions class="mt-n5 mb-1">
<v-menu offset-y bottom nudge-bottom="6" :close-on-content-click="false">
<template #activator="{ on, attrs }">
<v-btn color="accent" class="mr-1" dark v-bind="attrs" v-on="on">
<v-btn color="accent" class="mr-2" dark v-bind="attrs" v-on="on">
<v-icon left>
{{ $globals.icons.cog }}
</v-icon>

View File

@@ -22,6 +22,30 @@
</v-card-text>
</BaseDialog>
<!-- Create Dialog -->
<BaseDialog
v-model="createDialog"
:icon="$globals.icons.units"
title="Create Unit"
:submit-text="$tc('general.save')"
@submit="createUnit"
>
<v-card-text>
<v-form ref="domNewUnitForm">
<v-text-field
v-model="createTarget.name"
autofocus
label="Name"
:rules="[validators.required]"
></v-text-field>
<v-text-field v-model="createTarget.abbreviation" label="Abbreviation"></v-text-field>
<v-text-field v-model="createTarget.description" label="Description"></v-text-field>
<v-checkbox v-model="createTarget.fraction" hide-details label="Display as Fraction"></v-checkbox>
<v-checkbox v-model="createTarget.useAbbreviation" hide-details label="Use Abbreviation"></v-checkbox>
</v-form>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="editDialog"
@@ -100,8 +124,11 @@
:bulk-actions="[]"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@create-one="createEventHandler"
>
<template #button-row>
<BaseButton create @click="createDialog = true" />
<BaseButton @click="mergeDialog = true">
<template #icon> {{ $globals.icons.units }} </template>
Combine
@@ -132,9 +159,10 @@ import { computed, defineComponent, onMounted, ref } from "@nuxtjs/composition-a
import type { LocaleObject } from "@nuxtjs/i18n";
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
import { IngredientUnit } from "~/types/api-types/recipe";
import { CreateIngredientUnit, IngredientUnit } from "~/types/api-types/recipe";
import { useLocales } from "~/composables/use-locales";
import { useUnitStore } from "~/composables/store";
import { VForm } from "~/types/vuetify";
export default defineComponent({
setup() {
@@ -178,6 +206,41 @@ export default defineComponent({
const { units, actions: unitActions } = useUnitStore();
// ============================================================
// Create Units
const createDialog = ref(false);
const domNewUnitForm = ref<VForm>();
// we explicitly set booleans to false since forms don't POST unchecked boxes
const createTarget = ref<CreateIngredientUnit>({
name: "",
fraction: false,
useAbbreviation: false,
});
function createEventHandler() {
createDialog.value = true;
}
async function createUnit() {
if (!createTarget.value || !createTarget.value.name) {
return;
}
// @ts-expect-error the createOne function erroneously expects an id because it uses the IngredientUnit type
await unitActions.createOne(createTarget.value);
createDialog.value = false;
domNewUnitForm.value?.reset();
createTarget.value = {
name: "",
fraction: false,
useAbbreviation: false,
};
}
// ============================================================
// Edit Units
const editDialog = ref(false);
const editTarget = ref<IngredientUnit | null>(null);
@@ -195,6 +258,7 @@ export default defineComponent({
editDialog.value = false;
}
// ============================================================
// Delete Units
const deleteDialog = ref(false);
const deleteTarget = ref<IngredientUnit | null>(null);
@@ -263,6 +327,11 @@ export default defineComponent({
tableHeaders,
units,
validators,
// Create
createDialog,
createEventHandler,
createUnit,
createTarget,
// Edit
editDialog,
editEventHandler,

View File

@@ -126,7 +126,7 @@ export default defineComponent({
const { data } = await api.mealplanRules.getAll();
if (data) {
allRules.value = data;
allRules.value = data.items ?? [];
}
}

View File

@@ -54,7 +54,7 @@
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-text-field v-model="notifiers[index].name" label="Name"></v-text-field>
<v-text-field v-model="notifiers[index].appriseUrl" label="Apprise URL (skipped in blank)"></v-text-field>
<v-text-field v-model="notifiers[index].appriseUrl" label="Apprise URL (skipped if blank)"></v-text-field>
<v-checkbox v-model="notifiers[index].enabled" label="Enable Notifier" dense></v-checkbox>
<v-divider></v-divider>
@@ -130,12 +130,12 @@ export default defineComponent({
const notifiers = useAsync(async () => {
const { data } = await api.groupEventNotifier.getAll();
return data ?? [];
return data?.items;
}, useAsyncKey());
async function refreshNotifiers() {
const { data } = await api.groupEventNotifier.getAll();
notifiers.value = data ?? [];
notifiers.value = data?.items;
}
const createNotifierData: GroupEventNotifierCreate = reactive({

View File

@@ -5,10 +5,16 @@
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-webhooks.svg')"></v-img>
</template>
<template #title> Webhooks </template>
The webhooks defined below will be executed when a meal is defined for the day. At the scheduled time the webhooks
will be sent with the data from the recipe that is scheduled for the day
<v-card-text class="pb-0">
The webhooks defined below will be executed when a meal is defined for the day. At the scheduled time the
webhooks will be sent with the data from the recipe that is scheduled for the day. Note that webhook execution
is not exact. The webhooks are executed on a 5 minutes interval so the webhooks will be executed within 5 +/-
minutes of the scheduled.
</v-card-text>
</BasePageTitle>
<BannerExperimental />
<BaseButton create @click="actions.createOne()" />
<v-expansion-panels class="mt-2">
<v-expansion-panel v-for="(webhook, index) in webhooks" :key="index" class="my-2 left-border rounded">
@@ -17,7 +23,7 @@
<v-icon large left :color="webhook.enabled ? 'info' : null">
{{ $globals.icons.webhook }}
</v-icon>
{{ webhook.name }} - {{ webhook.time }}
{{ webhook.name }} - {{ timeDisplay(timeUTCToLocal(webhook.scheduledTime)) }}
</div>
<template #actions>
<v-btn small icon class="ml-2">
@@ -28,35 +34,12 @@
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-card-text>
<v-switch v-model="webhook.enabled" label="Enabled"></v-switch>
<v-text-field v-model="webhook.name" label="Webhook Name"></v-text-field>
<v-text-field v-model="webhook.url" label="Webhook Url"></v-text-field>
<v-time-picker v-model="webhook.time" class="elevation-2" ampm-in-title format="ampm"></v-time-picker>
</v-card-text>
<v-card-actions class="py-0 justify-end">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
},
{
icon: $globals.icons.testTube,
text: $t('general.test'),
event: 'test',
},
{
icon: $globals.icons.save,
text: $t('general.save'),
event: 'save',
},
]"
@delete="actions.deleteOne(webhook.id)"
@save="actions.updateOne(webhook)"
/>
</v-card-actions>
<GroupWebhookEditor
:key="webhook.id"
:webhook="webhook"
@save="actions.updateOne($event)"
@delete="actions.deleteOne($event)"
/>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
@@ -65,15 +48,28 @@
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import { useGroupWebhooks } from "~/composables/use-group-webhooks";
import { useGroupWebhooks, timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
import GroupWebhookEditor from "~/components/Domain/Group/GroupWebhookEditor.vue";
export default defineComponent({
components: { GroupWebhookEditor },
setup() {
const { actions, webhooks } = useGroupWebhooks();
function timeDisplay(time: string): string {
// returns the time in the format HH:MM AM/PM
const [hours, minutes] = time.split(":");
const ampm = Number(hours) < 12 ? "AM" : "PM";
const hour = Number(hours) % 12 || 12;
const minute = minutes.padStart(2, "0");
return `${hour}:${minute} ${ampm}`;
}
return {
webhooks,
actions,
timeLocalToUTC,
timeUTCToLocal,
timeDisplay,
};
},
head() {

View File

@@ -1,120 +0,0 @@
<template>
<v-container
v-if="recipe"
:class="{
'pa-0': $vuetify.breakpoint.smAndDown,
}"
>
<v-card-title>
<h1 class="headline">{{ recipe.name }}</h1>
</v-card-title>
<v-stepper v-model="activeStep" flat>
<v-toolbar class="ma-1 elevation-2 rounded">
<v-toolbar-title class="headline">
Step {{ activeStep }} of {{ recipe.recipeInstructions.length }}</v-toolbar-title
>
</v-toolbar>
<div class="d-flex mt-3 px-2">
<BaseButton color="primary" @click="$router.go(-1)">
<template #icon> {{ $globals.icons.arrowLeftBold }}</template>
To Recipe
</BaseButton>
<v-btn rounded icon color="primary" class="ml-auto" small @click="scale > 1 ? scale-- : null">
<v-icon>
{{ $globals.icons.minus }}
</v-icon>
</v-btn>
<v-btn rounded color="primary" small> Scale: {{ scale }} </v-btn>
<v-btn rounded icon color="primary" small @click="scale++">
<v-icon>
{{ $globals.icons.createAlt }}
</v-icon>
</v-btn>
</div>
<v-stepper-items>
<template v-for="(step, index) in recipe.recipeInstructions">
<v-stepper-content :key="index + 1 + '-content'" :step="index + 1" class="pa-0 mt-2 elevation-0">
<v-card class="ma-2">
<v-card-text>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<VueMarkdown :source="step.text"> </VueMarkdown>
<template v-if="step.ingredientReferences.length > 0">
<v-divider></v-divider>
<div>
<h2 class="mb-4 mt-4">{{ $t("recipe.ingredients") }}</h2>
<div
v-for="ing in step.ingredientReferences"
:key="ing.referenceId"
v-html="getIngredientByRefId(ing.referenceId)"
></div>
</div>
</template>
</v-card-text>
</v-card>
<v-card-actions class="justify-center">
<BaseButton color="primary" :disabled="index == 0" @click="activeStep = activeStep - 1">
<template #icon> {{ $globals.icons.arrowLeftBold }}</template>
Back
</BaseButton>
<BaseButton
icon-right
:disabled="index + 1 == recipe.recipeInstructions.length"
color="primary"
@click="activeStep = activeStep + 1"
>
<template #icon> {{ $globals.icons.arrowRightBold }}</template>
Next
</BaseButton>
</v-card-actions>
</v-stepper-content>
</template>
</v-stepper-items>
</v-stepper>
</v-container>
</template>
<script lang="ts">
import { defineComponent, useRoute, ref } from "@nuxtjs/composition-api";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { useStaticRoutes } from "~/composables/api";
import { parseIngredientText, useRecipe } from "~/composables/recipes";
export default defineComponent({
components: { VueMarkdown },
setup() {
const route = useRoute();
const slug = route.value.params.slug;
const activeStep = ref(1);
const scale = ref(1);
const { recipe } = useRecipe(slug);
const { recipeImage } = useStaticRoutes();
function getIngredientByRefId(refId: string) {
if (!recipe.value) {
return;
}
const ing = recipe?.value.recipeIngredient?.find((ing) => ing.referenceId === refId) || "";
if (ing === "") {
return "";
}
return parseIngredientText(ing, recipe?.value?.settings?.disableAmount || false, scale.value);
}
return {
scale,
getIngredientByRefId,
activeStep,
slug,
recipe,
recipeImage,
};
},
});
</script>

View File

@@ -17,7 +17,7 @@
<RecipeRating :key="recipe.slug" :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
</v-card-title>
<v-divider class="my-2"></v-divider>
<VueMarkdown :source="recipe.description"> </VueMarkdown>
<SafeMarkdown :source="recipe.description" />
<v-divider></v-divider>
<div class="d-flex justify-center mt-5">
<RecipeTimeCard
@@ -35,7 +35,7 @@
:max-width="enableLandscape ? null : '50%'"
min-height="50"
:height="hideImage ? undefined : imageHeight"
:src="recipeImage(recipe.id, imageKey)"
:src="recipeImage(recipe.id, recipe.image, imageKey)"
class="d-print-none"
@error="hideImage = true"
>
@@ -53,10 +53,7 @@
class="ml-auto mt-n8 pb-4"
@close="closeEditor"
@json="toggleJson"
@edit="
jsonEditor = false;
form = true;
"
@edit="toggleEdit"
@save="updateRecipe(recipe.slug, recipe)"
@delete="deleteRecipe(recipe.slug)"
@print="printRecipe"
@@ -84,7 +81,7 @@
<v-card-title class="px-0 py-2 ma-0 headline">
{{ recipe.name }}
</v-card-title>
<VueMarkdown :source="recipe.description"> </VueMarkdown>
<SafeMarkdown :source="recipe.description" />
<div class="pb-2 d-flex justify-center flex-wrap">
<RecipeTimeCard
@@ -176,40 +173,20 @@
</div>
</div>
<div class="d-flex justify-space-between align-center pt-2 pb-3">
<v-tooltip v-if="!form" small top color="secondary darken-1">
<v-tooltip v-if="!form && recipe.recipeYield" small top color="secondary darken-1">
<template #activator="{ on, attrs }">
<v-btn
v-if="recipe.recipeYield"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
<RecipeScaleEditButton
v-model.number="scale"
v-bind="attrs"
@click="scale = 1"
:recipe-yield="recipe.recipeYield"
:basic-yield="basicYield"
:scaled-yield="scaledYield"
:edit-scale="!recipe.settings.disableAmount && !form"
v-on="on"
>
{{ scaledYield }}
</v-btn>
/>
</template>
<span> Reset Scale </span>
<span> {{ $t("recipe.edit-scale") }} </span>
</v-tooltip>
<template v-if="!recipe.settings.disableAmount && !form">
<v-btn color="secondary darken-1" class="mx-1" small @click="scale > 1 ? scale-- : null">
<v-icon>
{{ $globals.icons.minus }}
</v-icon>
</v-btn>
<v-btn color="secondary darken-1" small @click="scale++">
<v-icon>
{{ $globals.icons.createAlt }}
</v-icon>
</v-btn>
</template>
<v-spacer></v-spacer>
<RecipeRating
@@ -222,7 +199,7 @@
</div>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<v-col v-if="!cookModeToggle || form" cols="12" sm="12" md="4" lg="4">
<RecipeIngredients
v-if="!form"
:value="recipe.recipeIngredient"
@@ -311,17 +288,24 @@
</client-only>
</div>
</v-col>
<v-divider v-if="$vuetify.breakpoint.mdAndUp" class="my-divider" :vertical="true"></v-divider>
<v-divider
v-if="$vuetify.breakpoint.mdAndUp && !cookModeToggle"
class="my-divider"
:vertical="true"
></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<v-col cols="12" sm="12" :md="8 + (cookModeToggle ? 1 : 0) * 4" :lg="8 + (cookModeToggle ? 1 : 0) * 4">
<RecipeInstructions
v-model="recipe.recipeInstructions"
:assets.sync="recipe.assets"
:ingredients="recipe.recipeIngredient"
:disable-amount="recipe.settings.disableAmount"
:edit="form"
:recipe-id="recipe.id"
:recipe-slug="recipe.slug"
:assets.sync="recipe.assets"
:cook-mode="cookModeToggle"
:scale="scale"
@cookModeToggle="cookModeToggle = !cookModeToggle"
/>
<div v-if="form" class="d-flex">
<RecipeDialogBulkAdd class="ml-auto my-2 mr-1" @bulk-data="addStep" />
@@ -377,14 +361,14 @@
</v-card>
<RecipeNutrition
v-if="recipe.settings.showNutrition"
v-if="recipe.settings.showNutrition && !cookModeToggle"
v-model="recipe.nutrition"
class="mt-10"
:edit="form"
/>
<client-only>
<RecipeAssets
v-if="recipe.settings.showAssets"
v-if="recipe.settings.showAssets && !cookModeToggle"
v-model="recipe.assets"
:edit="form"
:slug="recipe.slug"
@@ -393,7 +377,7 @@
</client-only>
</div>
<RecipeNotes v-model="recipe.notes" :edit="form" />
<RecipeNotes v-if="!cookModeToggle" v-model="recipe.notes" :edit="form" />
</v-col>
</v-row>
@@ -405,7 +389,7 @@
:label="$t('recipe.original-url')"
></v-text-field>
<v-btn
v-else-if="recipe.orgURL"
v-else-if="recipe.orgURL && !cookModeToggle"
dense
small
:hover="false"
@@ -458,7 +442,7 @@
</div>
<RecipeComments
v-if="recipe && !recipe.settings.disableComments && !form"
v-if="recipe && !recipe.settings.disableComments && !form && !cookModeToggle"
v-model="recipe.comments"
:slug="recipe.slug"
:recipe-id="recipe.id"
@@ -481,8 +465,6 @@ import {
useRouter,
onMounted,
} from "@nuxtjs/composition-api";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import draggable from "vuedraggable";
import { invoke, until, useWakeLock } from "@vueuse/core";
import { onUnmounted } from "vue-demi";
@@ -500,6 +482,7 @@ import RecipeNutrition from "~/components/Domain/Recipe/RecipeNutrition.vue";
import RecipeInstructions from "~/components/Domain/Recipe/RecipeInstructions.vue";
import RecipeNotes from "~/components/Domain/Recipe/RecipeNotes.vue";
import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue";
import RecipeScaleEditButton from "~/components/Domain/Recipe/RecipeScaleEditButton.vue";
import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue";
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
@@ -509,7 +492,6 @@ import { Recipe } from "~/types/api-types/recipe";
import { uuid4, deepCopy } from "~/composables/use-utils";
import { useRouteQuery } from "~/composables/use-router";
import { useToolStore } from "~/composables/store";
export default defineComponent({
components: {
draggable,
@@ -534,7 +516,7 @@ export default defineComponent({
RecipeSettingsMenu,
RecipeTimeCard,
RecipeTools,
VueMarkdown,
RecipeScaleEditButton,
},
async beforeRouteLeave(_to, _from, next) {
const isSame = JSON.stringify(this.recipe) === JSON.stringify(this.originalRecipe);
@@ -610,6 +592,8 @@ export default defineComponent({
const state = reactive({
form: false,
scale: 1,
scaleTemp: 1,
scaleDialog: false,
hideImage: false,
imageKey: 1,
skeleton: false,
@@ -619,6 +603,7 @@ export default defineComponent({
search: false,
mainMenuBar: false,
},
cookModeToggle: false,
});
const { recipe, loading, fetchRecipe } = useRecipe(slug);
@@ -658,6 +643,12 @@ export default defineComponent({
// ===========================================================================
// Button Click Event Handlers
function toggleEdit() {
state.jsonEditor = false;
state.cookModeToggle = false;
state.form = true;
}
async function updateRecipe(slug: string, recipe: Recipe) {
const { data } = await api.recipes.updateOne(slug, recipe);
state.form = false;
@@ -701,6 +692,19 @@ export default defineComponent({
return recipe.value?.recipeYield;
});
const basicYield = computed(() => {
const regMatchNum = /\d+/;
const yieldString = recipe.value?.recipeYield;
const num = yieldString?.match(regMatchNum);
if (num && num?.length > 0) {
const yieldAsInt = parseInt(num[0]);
return yieldString?.replace(num[0], String(yieldAsInt));
}
return recipe.value?.recipeYield;
});
async function uploadImage(fileObject: File) {
if (!recipe.value || !recipe.value.slug) {
return;
@@ -830,6 +834,13 @@ export default defineComponent({
const drag = ref(false);
// ===============================================================
// Scale
const setScale = (newScale: number) => {
state.scale = newScale;
};
return {
// Wake Lock
drag,
@@ -847,15 +858,18 @@ export default defineComponent({
enableLandscape,
imageHeight,
scaledYield,
basicYield,
toggleJson,
...toRefs(state),
recipe,
api,
loading,
addStep,
setScale,
deleteRecipe,
printRecipe,
closeEditor,
toggleEdit,
updateRecipe,
uploadImage,
validators,

View File

@@ -50,7 +50,11 @@
<v-expansion-panels v-model="panels" multiple>
<v-expansion-panel v-for="(ing, index) in parsedIng" :key="index">
<v-expansion-panel-header class="my-0 py-0" disable-icon-rotate>
{{ ing.input }}
<template #default="{ open }">
<v-fade-transition>
<span v-if="!open" key="0"> {{ ing.input }} </span>
</v-fade-transition>
</template>
<template #actions>
<v-icon left :color="isError(ing) ? 'error' : 'success'">
{{ isError(ing) ? $globals.icons.alert : $globals.icons.check }}
@@ -62,6 +66,7 @@
</v-expansion-panel-header>
<v-expansion-panel-content class="pb-0 mb-0">
<RecipeIngredientEditor v-model="parsedIng[index].ingredient" />
{{ ing.input }}
<v-card-actions>
<v-spacer></v-spacer>
<BaseButton
@@ -140,6 +145,16 @@ export default defineComponent({
const { data } = await api.recipes.parseIngredients(parser.value, raw);
if (data) {
// When we send the recipe ingredient text to be parsed, we lose the reference to the original unparsed ingredient.
// Generally this is fine, but if the unparsed ingredient had a title, we lose it; we add back the title for each ingredient here.
try {
for (let i = 0; i < recipe.value.recipeIngredient.length; i++) {
data[i].ingredient.title = recipe.value.recipeIngredient[i].title;
}
} catch (TypeError) {
console.error("Index Mismatch Error during recipe ingredient parsing; did the number of ingredients change?");
}
parsedIng.value = data;
errors.value = data.map((ing, index: number) => {

View File

@@ -6,7 +6,7 @@
<v-card-text>
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe scraper
and the results will be displayed. If you don't see any data returned, the site you are trying to scrape is
not supported by Mealie or it's scraper library.
not supported by Mealie or its scraper library.
<v-text-field
v-model="recipeUrl"
:label="$t('new-recipe.recipe-url')"

View File

@@ -4,47 +4,35 @@
:icon="$globals.icons.primary"
:title="$t('page.all-recipes')"
:recipes="recipes"
:use-pagination="true"
@sortRecipes="assignSorted"
@replaceRecipes="replaceRecipes"
@appendRecipes="appendRecipes"
@delete="removeRecipe"
></RecipeCardSection>
<v-card v-intersect="infiniteScroll"></v-card>
<v-fade-transition>
<AppLoader v-if="loading" :loading="loading" />
</v-fade-transition>
</v-container>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
import { useThrottleFn } from "@vueuse/core";
import { defineComponent } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useLazyRecipes } from "~/composables/recipes";
import { Recipe } from "~/types/api-types/recipe";
export default defineComponent({
components: { RecipeCardSection },
setup() {
const start = ref(0);
const limit = ref(30);
const increment = ref(30);
const ready = ref(false);
const loading = ref(false);
const { recipes, fetchMore } = useLazyRecipes();
onMounted(async () => {
await fetchMore(start.value, limit.value);
ready.value = true;
});
function appendRecipes(val: Array<Recipe>) {
val.forEach((recipe) => {
recipes.value.push(recipe);
});
}
const infiniteScroll = useThrottleFn(() => {
if (!ready.value) {
return;
}
loading.value = true;
start.value = limit.value + 1;
limit.value = limit.value + increment.value;
fetchMore(start.value, limit.value);
loading.value = false;
}, 500);
function assignSorted(val: Array<Recipe>) {
recipes.value = val;
}
function removeRecipe(slug: string) {
for (let i = 0; i < recipes?.value?.length; i++) {
@@ -55,7 +43,11 @@ export default defineComponent({
}
}
return { recipes, infiniteScroll, loading, removeRecipe };
function replaceRecipes(val: Array<Recipe>) {
recipes.value = val;
}
return { appendRecipes, assignSorted, recipes, removeRecipe, replaceRecipes };
},
head() {
return {
@@ -64,4 +56,3 @@ export default defineComponent({
},
});
</script>

View File

@@ -30,7 +30,7 @@ export default defineComponent({
};
},
head: {
title: "Tags",
title: "Categories",
},
});
</script>

View File

@@ -17,7 +17,7 @@
<RecipeRating :key="recipe.slug" :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
</v-card-title>
<v-divider class="my-2"></v-divider>
<VueMarkdown :source="recipe.description"> </VueMarkdown>
<SafeMarkdown :source="recipe.description"> </SafeMarkdown>
<v-divider></v-divider>
<div class="d-flex justify-center mt-5">
<RecipeTimeCard
@@ -34,7 +34,7 @@
:key="imageKey"
:max-width="enableLandscape ? null : '50%'"
:height="hideImage ? '50' : imageHeight"
:src="recipeImage(recipe.id, imageKey)"
:src="recipeImage(recipe.id, recipe.image, imageKey)"
class="d-print-none"
@error="hideImage = true"
>
@@ -61,7 +61,7 @@
<v-card-title class="pa-0 ma-0 headline">
{{ recipe.name }}
</v-card-title>
<VueMarkdown :source="recipe.description"> </VueMarkdown>
<SafeMarkdown :source="recipe.description"> </SafeMarkdown>
</template>
<template v-else-if="form">
@@ -273,8 +273,6 @@ import {
useMeta,
useRoute,
} from "@nuxtjs/composition-api";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
// import { useRecipeMeta } from "~/composables/recipes";
import { useStaticRoutes, useUserApi } from "~/composables/api";
import RecipeChips from "~/components/Domain/Recipe/RecipeChips.vue";
@@ -296,7 +294,6 @@ export default defineComponent({
RecipePrintView,
RecipeRating,
RecipeTimeCard,
VueMarkdown,
},
layout: "basic",
setup() {

View File

@@ -193,11 +193,11 @@ import { useCopyList } from "~/composables/use-copy";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
import { MultiPurposeLabelOut } from "~/types/api-types/labels";
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/types/api-types/group";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { getDisplayText } from "~/composables/use-display-text";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
type CopyTypes = "plain" | "markdown";
@@ -336,17 +336,9 @@ export default defineComponent({
// Labels, Units, Foods
// TODO: Extract to Composable
const allLabels = ref([] as MultiPurposeLabelOut[]);
const allUnits = useAsync(async () => {
const { data } = await userApi.units.getAll();
return data ?? [];
}, useAsyncKey());
const allFoods = useAsync(async () => {
const { data } = await userApi.foods.getAll();
return data ?? [];
}, useAsyncKey());
const { labels: allLabels } = useLabelStore();
const { units: allUnits } = useUnitStore();
const { foods: allFoods } = useFoodStore();
function sortByLabels() {
byLabel.value = !byLabel.value;
@@ -405,7 +397,10 @@ export default defineComponent({
async function refreshLabels() {
const { data } = await userApi.multiPurposeLabels.getAll();
allLabels.value = data ?? [];
if (data) {
allLabels.value = data.items ?? [];
}
}
refreshLabels();

View File

@@ -60,7 +60,12 @@ export default defineComponent({
async function fetchShoppingLists() {
const { data } = await userApi.shopping.lists.getAll();
return data;
if (!data) {
return [];
}
return data.items;
}
async function refresh() {

View File

@@ -1,13 +1,32 @@
<template>
<div></div>
<v-container>
<RecipeCardSection v-if="user" :icon="$globals.icons.heart" title="User Favorites" :recipes="user.favoriteRecipes">
</RecipeCardSection>
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
<script lang="ts">
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({
components: { RecipeCardSection },
setup() {
return {};
const api = useUserApi();
const route = useRoute();
const userId = route.value.params.id;
const user = useAsync(async () => {
const { data } = await api.users.getFavorites(userId);
return data;
}, useAsyncKey());
return {
user,
};
},
head() {
return {
@@ -16,6 +35,5 @@ export default defineComponent({
},
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -1,4 +1,5 @@
import { Plugin } from "@nuxt/types"
import { Plugin } from "@nuxt/types";
import { Framework } from "vuetify";
import { icons } from "~/utils/icons";
import { Icon } from "~/utils/icons/icon-type";
@@ -15,13 +16,14 @@ declare module "vue/types/vue" {
declare module "@nuxt/types" {
interface Context {
$globals: Globals;
$vuetify: Framework;
}
}
const globalsPlugin: Plugin = (_, inject) => {
inject("globals", {
icons
icons,
});
};
export default globalsPlugin
export default globalsPlugin;

View File

@@ -5,6 +5,7 @@
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/
export type WebhookType = "mealplan";
export type SupportedMigrations = "nextcloud" | "chowdown" | "paprika" | "mealie_alpha";
export interface CreateGroupPreferences {
@@ -25,7 +26,8 @@ export interface CreateWebhook {
enabled?: boolean;
name?: string;
url?: string;
time?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
}
export interface DataMigrationCreate {
sourceType: SupportedMigrations;
@@ -231,7 +233,8 @@ export interface ReadWebhook {
enabled?: boolean;
name?: string;
url?: string;
time?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
groupId: string;
id: string;
}
@@ -304,7 +307,8 @@ export interface SaveWebhook {
enabled?: boolean;
name?: string;
url?: string;
time?: string;
webhookType?: WebhookType & string;
scheduledTime: string;
groupId: string;
}
export interface SeederConfig {

View File

@@ -66,6 +66,8 @@ export interface UserOut {
}
export interface LongLiveTokenOut {
token: string;
name: string;
id: number;
}
export interface ReadGroupPreferences {
privateGroup?: boolean;

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