Compare commits

..

80 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
Hayden
56eb0bca71 release: prep beta 3 (#1349)
* docs: add tag documentation

* bump docker-compose tags

* minor language changes

* beta3 changelog

* auto-gen API docs

* bump version

* bump recipe-scrapers
2022-06-07 11:19:24 -08:00
Hayden
eca8a96509 fix image display - closes #1189 (#1348) 2022-06-05 11:44:00 -08:00
Hayden
7eb80d18d2 feat: attached images by drag and drop for recipe steps (#1341)
* add drag and drop support for recipe steps

* fix recipe assets dialog state

* add attr support for markdown editor

* add persistent hint for recipe text editor
2022-06-05 11:28:38 -08:00
Benjamin Pabst
37a673b34d Update postgres to use most recent version (#1347) 2022-06-05 10:56:01 -08:00
Benjamin Pabst
3e7b8d4b71 Update to use most recent version (#1346) 2022-06-05 10:55:33 -08:00
Jurjen de Jonge
abb114c375 security: delay server response whenever username is non existing (#1338)
* Delay server response whenever username is non existing

* utilize hasher to achieve constant timing

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2022-06-04 10:27:30 -08:00
Hayden
12f480eb75 refactor: unify recipe-organizer components (#1340)
* use generic context menu

* implement organizer stores

* add basic organizer types

* refactor selectors to apply for all organizers

* remove legacy organizer composables
2022-06-03 20:12:32 -08:00
Hayden
bc175d4ca9 New translations en-US.json (Ukrainian) (#1339) 2022-06-03 19:49:58 -08:00
Hayden
f78c5eb359 New Crowdin updates (#1329)
* New translations en-US.json (German)

* New translations en-US.json (German)

* New translations en-US.json (Ukrainian)
2022-06-02 09:12:15 -08:00
Hayden
5a0c034391 fix: shopping list label editor (#1333)
* remove head props

* lazily compute itemsByLabel with watcher on fetch

* remove unused import
2022-06-02 09:12:05 -08:00
Hayden
52fbf6b833 feat: add unit abbreviation support (#1332)
* add 'use-abbreviation' db column

* type generation

* add view and edit elements

* check for use_abbreviation to display

* fix: alembic version check

* test: add use_abbreviation prop tests
2022-06-01 11:59:50 -08:00
dependabot[bot]
592b1de39d fix(deps): bump @vue/composition-api from 1.6.1 to 1.6.2 in /frontend (#1275)
Bumps [@vue/composition-api](https://github.com/vuejs/composition-api) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/vuejs/composition-api/releases)
- [Changelog](https://github.com/vuejs/composition-api/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/composition-api/compare/v1.6.1...v1.6.2)

---
updated-dependencies:
- dependency-name: "@vue/composition-api"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-31 08:48:57 -08:00
dependabot[bot]
f29d5f1dff chore(deps-dev): bump @babel/eslint-parser in /frontend (#1290)
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.15.4 to 7.18.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.2/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-31 08:48:33 -08:00
dependabot[bot]
738ef0aaa7 chore(deps-dev): bump @types/sortablejs in /frontend (#1287)
Bumps [@types/sortablejs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sortablejs) from 1.10.7 to 1.13.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sortablejs)

---
updated-dependencies:
- dependency-name: "@types/sortablejs"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-31 08:48:01 -08:00
Hayden
f1fdec5afe add security.md 2022-05-31 08:29:45 -08:00
Hayden
4c594a48dc wip: pagination-repository (#1316)
* bump mypy

* add pagination + refactor generic repo

* add pagination test

* remove all query object
2022-05-30 10:30:54 -08:00
Hayden
00f144a622 chore: init git-cliff config 2022-05-29 17:34:41 -08:00
Hayden
d2a9f7ca24 fix: consoldate stores to fix mismatched state 2022-05-29 17:34:41 -08:00
Hayden
f831791db2 feat: default unit fractions to True 2022-05-29 17:34:41 -08:00
Hayden
c3bdfe7b3b fix: printer page refs (#1314) 2022-05-29 09:15:38 -08:00
Hayden
3542bb0927 fix: bad import path (#1313)
* fix bad import

* add eslint rule for incorrect imports
2022-05-29 09:09:36 -08:00
Philipp Fischbeck
e898c80f59 fix: update issue links in v1.0.0beta-2 changelog (#1312) 2022-05-29 08:51:11 -08:00
James Addison
27c5cfc56b Fixup: render homepage social media link images at 32x32 size (#1310) 2022-05-29 08:50:44 -08:00
Hayden
369cda0a61 New Crowdin updates (#1308)
* New translations en-US.json (Italian)

* New translations en-US.json (Italian)
2022-05-29 08:50:18 -08:00
290 changed files with 7840 additions and 3460 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

@@ -52,6 +52,7 @@
"package.json": "package-lock.json, yarn.lock, .eslintrc.js, tsconfig.json, .prettierrc, .editorconfig",
"pyproject.toml": "poetry.lock, alembic.ini, .pylintrc, .flake8",
"netlify.toml": "runtime.txt",
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml"
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml",
"README.md": "LICENSE, SECURITY.md"
}
}

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

9
SECURITY.md Normal file
View File

@@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
Since this software is still considered beta/WIP support is always only given for the latest version. Security patches are only available for the latest version and not back-ported to older versions.
## Reporting a Vulnerability
For general security vulnerabilities you're welcome to open a GitHub issues or contribute a fix. If you feel the vulnerability should not be disclosed you can open a generic issue on GitHub and email to the details to [ob92oy0sl@mozmail.com](mailto:ob92oy0sl@mozmail.com) which is monitored by the maintainer.

View File

@@ -0,0 +1,30 @@
"""Add use_abbreviation column to ingredients
Revision ID: ab0bae02578f
Revises: 09dfc897ad62
Create Date: 2022-06-01 11:12:06.748383
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "ab0bae02578f"
down_revision = "09dfc897ad62"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("ingredient_units", sa.Column("use_abbreviation", sa.Boolean(), nullable=True))
op.execute("UPDATE ingredient_units SET use_abbreviation = FALSE WHERE use_abbreviation IS NULL")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("ingredient_units", "use_abbreviation")
# ### end Alembic commands ###

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

64
cliff.toml Normal file
View File

@@ -0,0 +1,64 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/hay-kot/mealie/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

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

@@ -1,29 +1,29 @@
### Bug Fixes
- Bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend ([#1257](https://github.com/orhun/git-cliff/issues/1257))
- Bump @nuxtjs/auth-next in /frontend ([#1265](https://github.com/orhun/git-cliff/issues/1265))
- Bad dev dependency ([#1281](https://github.com/orhun/git-cliff/issues/1281))
- Add touch support for mealplanner delete ([#1298](https://github.com/orhun/git-cliff/issues/1298))
- Bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend ([#1257](https://github.com/hay-kot/mealie/issues/1257))
- Bump @nuxtjs/auth-next in /frontend ([#1265](https://github.com/hay-kot/mealie/issues/1265))
- Bad dev dependency ([#1281](https://github.com/hay-kot/mealie/issues/1281))
- Add touch support for mealplanner delete ([#1298](https://github.com/hay-kot/mealie/issues/1298))
### Documentation
- Add references for VSCode dev containers ([#1299](https://github.com/orhun/git-cliff/issues/1299))
- Docker-compose.dev.yml is currently not functional ([#1300](https://github.com/orhun/git-cliff/issues/1300))
- Add references for VSCode dev containers ([#1299](https://github.com/hay-kot/mealie/issues/1299))
- Docker-compose.dev.yml is currently not functional ([#1300](https://github.com/hay-kot/mealie/issues/1300))
### Features
- Add reports to bulk recipe import (url) ([#1294](https://github.com/orhun/git-cliff/issues/1294))
- Rewrite print implementation to support new ing ([#1305](https://github.com/orhun/git-cliff/issues/1305))
- Add reports to bulk recipe import (url) ([#1294](https://github.com/hay-kot/mealie/issues/1294))
- Rewrite print implementation to support new ing ([#1305](https://github.com/hay-kot/mealie/issues/1305))
### Miscellaneous Tasks
- Github stalebot changes ([#1271](https://github.com/orhun/git-cliff/issues/1271))
- Bump eslint-plugin-nuxt in /frontend ([#1258](https://github.com/orhun/git-cliff/issues/1258))
- Bump @vue/runtime-dom in /frontend ([#1259](https://github.com/orhun/git-cliff/issues/1259))
- Bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend ([#1260](https://github.com/orhun/git-cliff/issues/1260))
- Bump vue2-script-setup-transform in /frontend ([#1263](https://github.com/orhun/git-cliff/issues/1263))
- Update dev dependencies ([#1282](https://github.com/orhun/git-cliff/issues/1282))
- Github stalebot changes ([#1271](https://github.com/hay-kot/mealie/issues/1271))
- Bump eslint-plugin-nuxt in /frontend ([#1258](https://github.com/hay-kot/mealie/issues/1258))
- Bump @vue/runtime-dom in /frontend ([#1259](https://github.com/hay-kot/mealie/issues/1259))
- Bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend ([#1260](https://github.com/hay-kot/mealie/issues/1260))
- Bump vue2-script-setup-transform in /frontend ([#1263](https://github.com/hay-kot/mealie/issues/1263))
- Update dev dependencies ([#1282](https://github.com/hay-kot/mealie/issues/1282))
### Refactor
- Split up recipe create page ([#1283](https://github.com/orhun/git-cliff/issues/1283))
- Split up recipe create page ([#1283](https://github.com/hay-kot/mealie/issues/1283))

View File

@@ -0,0 +1,36 @@
### Bug Fixes
- Update issue links in v1.0.0beta-2 changelog ([#1312](https://github.com/hay-kot/mealie/issues/1312))
- Bad import path ([#1313](https://github.com/hay-kot/mealie/issues/1313))
- Printer page refs ([#1314](https://github.com/hay-kot/mealie/issues/1314))
- Consolidate stores to fix mismatched state
- Bump @vue/composition-api from 1.6.1 to 1.6.2 in /frontend ([#1275](https://github.com/hay-kot/mealie/issues/1275))
- Shopping list label editor ([#1333](https://github.com/hay-kot/mealie/issues/1333))
### Features
- Default unit fractions to True
- Add unit abbreviation support ([#1332](https://github.com/hay-kot/mealie/issues/1332))
- Attached images by drag and drop for recipe steps ([#1341](https://github.com/hay-kot/mealie/issues/1341))
### Docs
- Render homepage social media link images at 32x32 size ([#1310](https://github.com/hay-kot/mealie/issues/1310))
### Miscellaneous Tasks
- Init git-cliff config
- Bump @types/sortablejs in /frontend ([#1287](https://github.com/hay-kot/mealie/issues/1287))
- Bump @babel/eslint-parser in /frontend ([#1290](https://github.com/hay-kot/mealie/issues/1290))
### Refactor
- Unify recipe-organizer components ([#1340](https://github.com/hay-kot/mealie/issues/1340))
### Security
- Delay server response whenever username is non existing ([#1338](https://github.com/hay-kot/mealie/issues/1338))
### Wip
- Pagination-repository ([#1316](https://github.com/hay-kot/mealie/issues/1316))

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

@@ -1,12 +1,20 @@
# Frequently Asked Questions
## Is it Safe to Upgrade Mealie?
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Not that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
**Links**
- [Migration From v0.5.x](./migrating-to-mealie-v1.md)
## How can I change the theme?
You can change the theme by settings the environment variables on the frontend container.
You can change the theme by settings the environment variables on the frontend container.
Links:
- [Frontend Theme](/mealie/documentation/getting-started/installation/frontend-config#themeing)
- [Frontend Theme](./installation/frontend-config#themeing)
## How can I change the language?
@@ -14,29 +22,29 @@ Languages need to be set on the frontend and backend containers as ENV variables
Links
- [Frontend Config](/mealie/documentation/getting-started/installation/frontend-config/)
- [Backend Config](/mealie/documentation/getting-started/installation/backend-config/)
- [Frontend Config](./installation/frontend-config/)
- [Backend Config](./installation/backend-config/)
## How can I change the Login Session Timeout?
Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
- [Backend Config](/mealie/documentation/getting-started/installation/backend-config/)
- [Backend Config](./installation/backend-config/)
## Can I serve Mealie on a subpath?
No. Due to limitations from the Javascript Framework, mealie doesn't support serving Mealie on a subpath.
## Can I install Mealie without docker?
## Can I install Mealie without docker?
Yes, you can install Mealie on your local machine. HOWEVER, it is recommended that you don't. Managing non-system versions of python, node, and npm is a pain. Moreover updating and upgrading your system with this configuration is unsupported and will likely require manual interventions. If you insist on installing Mealie on your local machine, you can use the links below to help guide your path.
- [Advanced Installation](/mealie/documentation/getting-started/installation/advanced/)
- [Advanced Installation](./installation/advanced/)
## How I can attach an Image or Video to a Recipe?
## How I can attach an Image or Video to a Recipe?
Yes. Mealie's Recipe Steps and other fields support the markdown syntax and therefor supports images and videos. To attach an image to the recipe, you can upload it as an asset and use the provided copy button to generate the html image tag required to render the image. For videos, Mealie provides no way to host videos. You'll need to host your videos with another provider and embed them in your recipe. Generally, the video provider will provide a link to the video and the html tag required to render the video. For example, youtube provides the following link that works inside a step. You can adjust the width and height attributes as necessary to ensure a fit.
```html
<iframe width="560" height="315" src="https://www.youtube.com/embed/nAUwKeO93bY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
```
```

View File

@@ -85,6 +85,17 @@ These backups are just plain .zip files that you can download from the UI or acc
## Appendix
### Docker Tags
`mealie:frontend-v1.0.0beta-x` **and** `mealie:api-v1.0.0beta-x`
These are the tags for the latest beta release of the frontend docker-container. These are currently considered the latest and most stable releases and the recommended way of using Mealie.
`mealie:frontend-nightly`**and** `mealie:api-nightly`
The nightly build are the latest and greatest builds that are built directly off of every commit to the `mealie-next` branch and as such may contain bugs. These are great to help the community catch bugs before they hit the stable release or if you like living on the edge.
### Docker Diagram
While the docker-compose file should work without modification, some users want to tailor it to their installation. This diagram shows network and volume architecture for the default setup. You can use this to help you customize your configuration.

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.0-beta-1
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.0-beta-1
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.0-beta-1
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.0-beta-1
image: hkotel/mealie:api-v1.0.0beta-4
container_name: mealie-api
volumes:
- mealie-data:/app/data/

View File

@@ -5,16 +5,12 @@
You should likely find bugs, errors, and unfinished pages within the application. To find the current status of the release you can checkout the [project on github](https://github.com/hay-kot/mealie/projects/7) or reach out on discord.
You should also be aware that Mealie v1 Beta does not have the backup/export feature available. This is the next priority for Mealie v1
and is currently being worked out.
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.
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)
## Key Features
- 🔍 Fuzzy search
- 🏷️ Tag recipes with categories or tags to flexible sorting
@@ -40,15 +36,15 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
## FAQ
### Why An API?
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based of Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based of Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
### Why a Database?
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in controls of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in controls of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
As to why we need a database?
- **Developer Experience:** Without a database a lot of the work to maintain your data is taken on by the developer instead of a battle tested platform for storing data.
- **Multi User Support:** With a solid database as backend storage for your data Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
- **Developer Experience:** Without a database a lot of the work to maintain your data is taken on by the developer instead of a battle tested platform for storing data.
- **Multi User Support:** With a solid database as backend storage for your data Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
## Built With
@@ -68,7 +64,6 @@ As to why we need a database?
Contributions are what make the open source community such an amazing place to learn, develop, and create. Any contributions you make are **greatly appreciated**. See the [Contributors Guide](../../contributors/non-coders.md) for help getting started.
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and help me to know that there is a real demand for project development.
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and help me to know that there is a real demand for project development.
<a href="https://www.buymeacoffee.com/haykot" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

File diff suppressed because one or more lines are too long

View File

@@ -382,7 +382,7 @@
target="_blank"
title="github.com"
>
<svg viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
<path
d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z"
></path>
@@ -395,7 +395,7 @@
target="_blank"
title="twitter.com"
>
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<svg style="width: 32px; height: 32px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
></path>
@@ -408,7 +408,7 @@
target="_blank"
title="www.linkedin.com"
>
<svg viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
<svg style="width: 32px; height: 32px" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
<path
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
></path>

View File

@@ -88,6 +88,8 @@ 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"
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"

View File

@@ -52,6 +52,8 @@ module.exports = {
"ts-ignore": "allow-with-description",
},
],
"no-restricted-imports": ["error", { paths: ["@vue/reactivity", "@vue/runtime-dom", "@vue/composition-api"] }],
// TODO Gradually activate all rules
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",

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

@@ -5,8 +5,8 @@
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" label="Meal Type"></v-select>
</div>
<RecipeCategoryTagSelector v-model="inputCategories" />
<RecipeCategoryTagSelector v-model="inputTags" :tag-selector="true" />
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
{{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }}
{{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }}
@@ -15,7 +15,8 @@
<script lang="ts">
import { defineComponent, computed } from "@nuxtjs/composition-api";
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { RecipeTag, RecipeCategory } from "~/types/api-types/group";
const MEAL_TYPE_OPTIONS = [
{ text: "Breakfast", value: "breakfast" },
@@ -38,7 +39,7 @@ const MEAL_DAY_OPTIONS = [
export default defineComponent({
components: {
RecipeCategoryTagSelector,
RecipeOrganizerSelector,
},
props: {
day: {
@@ -50,11 +51,11 @@ export default defineComponent({
default: "unset",
},
categories: {
type: Array,
type: Array as () => RecipeCategory[],
default: () => [],
},
tags: {
type: Array,
type: Array as () => RecipeTag[],
default: () => [],
},
showHelp: {

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

@@ -45,7 +45,7 @@
@submit="addAsset"
>
<template #activator>
<BaseButton v-if="edit" small create @click="newAssetDialog = true" />
<BaseButton v-if="edit" small create @click="state.newAssetDialog = true" />
</template>
<v-card-text class="pt-4">
<v-text-field v-model="state.newAsset.name" dense :label="$t('general.name')"></v-text-field>

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

@@ -1,111 +0,0 @@
<template>
<div>
<slot>
<v-btn icon class="mt-n1" @click="dialog = true">
<v-icon :color="color">{{ $globals.icons.create }}</v-icon>
</v-btn>
</slot>
<v-dialog v-model="dialog" width="500">
<v-card>
<v-app-bar dense dark color="primary mb-2">
<v-icon large left class="mt-1">
{{ $globals.icons.tags }}
</v-icon>
<v-toolbar-title class="headline">
{{ title }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-title> </v-card-title>
<v-form @submit.prevent="select">
<v-card-text>
<v-text-field
v-model="itemName"
dense
:label="inputLabel"
:rules="[rules.required]"
autofocus
></v-text-field>
</v-card-text>
<v-card-actions>
<BaseButton cancel @click="dialog = false" />
<v-spacer></v-spacer>
<BaseButton type="submit" create :disabled="!itemName" />
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
const CREATED_ITEM_EVENT = "created-item";
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
color: {
type: String,
default: null,
},
tagDialog: {
type: Boolean,
default: true,
},
},
setup(props, context) {
const title = computed(() => props.tagDialog ? "Create a Tag" : "Create a Category");
const inputLabel = computed(() => props.tagDialog ? "Tag Name" : "Category Name");
const rules = {
required: (val: string) => !!val || "A Name is Required",
};
const state = reactive({
dialog: false,
itemName: "",
});
watch(() => state.dialog, (val: boolean) => {
if (!val) state.itemName = "";
});
const api = useUserApi();
async function select() {
const newItem = await (async () => {
if (props.tagDialog) {
const { data } = await api.tags.createOne({ name: state.itemName });
return data;
} else {
const { data } = await api.categories.createOne({ name: state.itemName });
return data;
}
})();
console.log(newItem);
context.emit(CREATED_ITEM_EVENT, newItem);
state.dialog = false;
}
return {
...toRefs(state),
title,
inputLabel,
rules,
select,
};
},
});
</script>
<style></style>

View File

@@ -1,164 +0,0 @@
//TODO: Prevent fetching Categories/Tags multiple time when selector is on page multiple times
<template>
<v-autocomplete
v-model="selected"
:items="activeItems"
:value="value"
:label="inputLabel"
chips
deletable-chips
:dense="dense"
item-text="name"
persistent-hint
multiple
:hide-details="hideDetails"
:hint="hint"
:solo="solo"
:return-object="returnObject"
:prepend-inner-icon="$globals.icons.tags"
v-bind="$attrs"
@input="emitChange"
>
<template #selection="data">
<v-chip
v-if="showSelected"
:key="data.index"
:small="dense"
class="ma-1"
:input-value="data.selected"
close
label
color="accent"
dark
@click:close="removeByIndex(data.index)"
>
{{ data.item.name || data.item }}
</v-chip>
</template>
<template #append-outer>
<RecipeCategoryTagDialog v-if="showAdd" :tag-dialog="tagSelector" @created-item="pushToItem" />
</template>
</v-autocomplete>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
import { useTags, useCategories } from "~/composables/recipes";
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
const MOUNTED_EVENT = "mounted";
export default defineComponent({
components: {
RecipeCategoryTagDialog,
},
props: {
value: {
type: Array as () => (RecipeTag | RecipeCategory | string)[],
required: true,
},
solo: {
type: Boolean,
default: false,
},
dense: {
type: Boolean,
default: true,
},
returnObject: {
type: Boolean,
default: true,
},
tagSelector: {
type: Boolean,
default: false,
},
hint: {
type: String,
default: null,
},
showAdd: {
type: Boolean,
default: false,
},
showLabel: {
type: Boolean,
default: true,
},
showSelected: {
type: Boolean,
default: true,
},
hideDetails: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const { allTags, useAsyncGetAll: getAllTags } = useTags();
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
getAllCategories();
getAllTags();
const state = reactive({
selected: props.value,
});
watch(
() => props.value,
(val) => {
state.selected = val;
}
);
const { i18n } = useContext();
const inputLabel = computed(() => {
if (!props.showLabel) return null;
return props.tagSelector ? i18n.t("tag.tags") : i18n.t("recipe.categories");
});
const activeItems = computed(() => {
let itemObjects: RecipeTag[] | RecipeCategory[] | null;
if (props.tagSelector) itemObjects = allTags.value;
else {
itemObjects = allCategories.value;
}
if (props.returnObject) return itemObjects;
else {
return itemObjects?.map((x: RecipeTag | RecipeCategory) => x.name);
}
});
function emitChange() {
context.emit("input", state.selected);
}
// TODO Is this needed?
onMounted(() => {
context.emit(MOUNTED_EVENT);
});
function removeByIndex(index: number) {
state.selected.splice(index, 1);
}
function pushToItem(createdItem: RecipeTag | RecipeCategory) {
// TODO: Remove excessive get calls
getAllCategories();
getAllTags();
state.selected.push(createdItem);
}
return {
...toRefs(state),
inputLabel,
activeItems,
emitChange,
removeByIndex,
pushToItem,
};
},
});
</script>

View File

@@ -1,207 +0,0 @@
<template>
<div class="text-center">
<BaseDialog
v-model="ItemDeleteDialog"
:title="`Delete ${itemName}`"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="deleteItem()"
>
<v-card-text> Are you sure you want to delete this {{ itemName }}? </v-card-text>
</BaseDialog>
<v-menu
offset-y
left
:bottom="!menuTop"
:nudge-bottom="!menuTop ? '5' : '0'"
:top="menuTop"
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
open-on-hover
content-class="d-print-none"
>
<template #activator="{ on, attrs }">
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
<v-icon>{{ icon }}</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
<v-list-item-icon>
<v-icon :color="item.color">
{{ item.icon }}
</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import colors from "vuetify/lib/util/colors";
import { useUserApi } from "~/composables/api";
export interface ContextMenuIncludes {
delete: boolean;
}
export interface ContextMenuItem {
title: string;
icon: string;
color: string | undefined;
event: string;
}
const ItemTypes = {
tag: "tags",
category: "categories",
tool: "tools",
};
export default defineComponent({
props: {
itemType: {
type: String as () => string,
required: true,
},
useItems: {
type: Object as () => ContextMenuIncludes,
default: () => ({
delete: true,
}),
},
// Append items are added at the end of the useItems list
appendItems: {
type: Array as () => ContextMenuItem[],
default: () => [],
},
// Append items are added at the beginning of the useItems list
leadingItems: {
type: Array as () => ContextMenuItem[],
default: () => [],
},
menuTop: {
type: Boolean,
default: true,
},
fab: {
type: Boolean,
default: false,
},
color: {
type: String,
default: colors.grey.darken2,
},
slug: {
type: String,
required: true,
},
menuIcon: {
type: String,
default: null,
},
name: {
required: true,
type: String,
},
id: {
required: true,
type: String,
},
},
setup(props, context) {
const api = useUserApi();
const state = reactive({
ItemDeleteDialog: false,
loading: false,
menuItems: [] as ContextMenuItem[],
itemName: "tag",
});
const { i18n, $globals } = useContext();
let apiRoute = "tags" as "tags" | "categories" | "tools";
switch (props.itemType) {
case ItemTypes.tag:
state.itemName = "tag";
apiRoute = "tags";
break;
case ItemTypes.category:
state.itemName = "category";
apiRoute = "categories";
break;
case ItemTypes.tool:
state.itemName = "tool";
apiRoute = "tools";
break;
default:
break;
}
// ===========================================================================
// Context Menu Setup
const defaultItems: { [key: string]: ContextMenuItem } = {
delete: {
title: i18n.t("general.delete") as string,
icon: $globals.icons.delete,
color: undefined,
event: "delete",
},
};
// Get Default Menu Items Specified in Props
for (const [key, value] of Object.entries(props.useItems)) {
if (value) {
const item = defaultItems[key];
if (item) {
state.menuItems.push(item);
}
}
}
// Add leading and Apppending Items
state.menuItems = [...props.leadingItems, ...state.menuItems, ...props.appendItems];
const icon = props.menuIcon || $globals.icons.dotsVertical;
async function deleteItem() {
await api[apiRoute].deleteOne(props.id);
context.emit("delete", props.id);
}
// Note: Print is handled as an event in the parent component
const eventHandlers: { [key: string]: () => void } = {
delete: () => {
state.ItemDeleteDialog = true;
},
};
function contextMenuEventHandler(eventKey: string) {
const handler = eventHandlers[eventKey];
if (handler && typeof handler === "function") {
handler();
state.loading = false;
return;
}
context.emit(eventKey);
state.loading = false;
}
return {
...toRefs(state),
contextMenuEventHandler,
deleteItem,
icon,
};
},
});
</script>

View File

@@ -1,123 +0,0 @@
<template>
<div v-if="items">
<v-app-bar color="transparent" flat class="mt-n1 rounded">
<v-icon large left>
{{ icon }}
</v-icon>
<v-toolbar-title class="headline"> {{ headline }} </v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
<v-row>
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
<v-card class="left-border" hover :to="`/recipes/${itemType}/${item.slug}`">
<v-card-actions>
<v-icon>
{{ icon }}
</v-icon>
<v-card-title class="py-1">
{{ item.name }}
</v-card-title>
<v-spacer></v-spacer>
<RecipeCategoryTagToolContextMenu
:id="item.id"
:item-type="itemType"
:slug="item.slug"
:name="item.name"
:use-items="{
delete: true,
}"
@delete="$emit('delete', item.id)"
/>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, useContext, computed, useMeta } from "@nuxtjs/composition-api";
import RecipeCategoryTagToolContextMenu from "./RecipeCategoryTagToolContextMenu.vue";
type ItemType = "tags" | "categories" | "tools";
const ItemTypes = {
tag: "tags",
category: "categories",
tool: "tools",
};
interface GenericItem {
id: string;
name: string;
slug: string;
}
export default defineComponent({
components: { RecipeCategoryTagToolContextMenu },
props: {
itemType: {
type: String as () => ItemType,
required: true,
},
items: {
type: Array as () => GenericItem[],
required: true,
},
},
setup(props) {
const { i18n, $globals } = useContext();
const state = reactive({
headline: "tags",
icon: $globals.icons.tags,
});
switch (props.itemType) {
case ItemTypes.tag:
state.headline = i18n.t("tag.tags") as string;
break;
case ItemTypes.category:
state.headline = i18n.t("category.categories") as string;
break;
case ItemTypes.tool:
state.headline = i18n.t("tool.tools") as string;
state.icon = $globals.icons.potSteam;
break;
default:
break;
}
useMeta(() => ({
title: state.headline,
}));
const itemsSorted = computed(() => {
const byLetter: { [key: string]: Array<GenericItem> } = {};
if (!props.items) return byLetter;
props.items.forEach((item) => {
const letter = item.name[0].toUpperCase();
if (!byLetter[letter]) {
byLetter[letter] = [];
}
byLetter[letter].push(item);
});
return byLetter;
});
return {
...toRefs(state),
itemsSorted,
};
},
// Needed for useMeta
head: {},
});
</script>

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

@@ -118,7 +118,7 @@
<script lang="ts">
import { computed, defineComponent, reactive, ref, toRefs } from "@nuxtjs/composition-api";
import { useFoods, useUnits } from "~/composables/recipes";
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
import { validators } from "~/composables/use-validators";
import { RecipeIngredient } from "~/types/api-types/recipe";
@@ -136,24 +136,28 @@ export default defineComponent({
setup(props) {
// ==================================================
// Foods
const { foods, workingFoodData, actions: foodActions } = useFoods();
const foodStore = useFoodStore();
const foodData = useFoodData();
const foodSearch = ref("");
async function createAssignFood() {
workingFoodData.name = foodSearch.value;
await foodActions.createOne();
props.value.food = foods.value?.find((food) => food.name === foodSearch.value);
foodData.data.name = foodSearch.value;
await foodStore.actions.createOne(foodData.data);
props.value.food = foodStore.foods.value?.find((food) => food.name === foodSearch.value);
foodData.reset();
}
// ==================================================
// Units
const { units, workingUnitData, actions: unitActions } = useUnits();
const unitStore = useUnitStore();
const unitsData = useUnitData();
const unitSearch = ref("");
async function createAssignUnit() {
workingUnitData.name = unitSearch.value;
await unitActions.createOne();
props.value.unit = units.value?.find((unit) => unit.name === unitSearch.value);
unitsData.data.name = unitSearch.value;
await unitStore.actions.createOne(unitsData.data);
props.value.unit = unitStore.units.value?.find((unit) => unit.name === unitSearch.value);
unitsData.reset();
}
const state = reactive({
@@ -226,22 +230,22 @@ export default defineComponent({
}
return {
...toRefs(state),
quantityFilter,
toggleOriginalText,
contextMenuOptions,
handleUnitEnter,
handleFoodEnter,
...toRefs(state),
createAssignFood,
createAssignUnit,
foods,
foods: foodStore.foods,
foodSearch,
toggleTitle,
unitActions,
units,
unitActions: unitStore.actions,
units: unitStore.units,
unitSearch,
validators,
workingUnitData,
workingUnitData: unitsData.data,
};
},
});

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
@@ -168,12 +168,26 @@
</v-icon>
</v-fade-transition>
</v-card-title>
<v-card-text v-if="edit">
<!-- Content -->
<v-card-text
v-if="edit"
:class="{
blur: imageUploadMode,
}"
@drop.stop.prevent="handleImageDrop(index, $event)"
>
<MarkdownEditor
v-model="value[index]['text']"
class="mb-2"
:preview.sync="previewStates[index]"
:display-preview="false"
:textarea="{
hint: 'Attach images by dragging & dropping them into the editor',
persistentHint: true,
}"
/>
<div
v-for="ing in step.ingredientReferences"
:key="ing.referenceId"
@@ -183,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>
@@ -197,12 +219,20 @@
<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 } from "@nuxtjs/composition-api";
import { RecipeStep, IngredientReferences, RecipeIngredient } from "~/types/api-types/recipe";
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 } from "~/composables/use-utils";
import { uuid4, detectServerBaseUrl } from "~/composables/use-utils";
import { useUserApi, useStaticRoutes } from "~/composables/api";
interface MergerHistory {
target: number;
@@ -213,7 +243,6 @@ interface MergerHistory {
export default defineComponent({
components: {
VueMarkdown,
draggable,
},
props: {
@@ -237,9 +266,34 @@ export default defineComponent({
type: Boolean,
default: false,
},
recipeId: {
type: String,
default: "",
},
recipeSlug: {
type: String,
default: "",
},
assets: {
type: Array as () => RecipeAsset[],
required: true,
},
cookMode: {
type: Boolean,
default: false,
},
scale: {
type: Number,
default: 1,
},
},
setup(props, context) {
const { req } = useContext();
const BASE_URL = detectServerBaseUrl(req);
console.log("Base URL", BASE_URL);
const state = reactive({
dialog: false,
disabledSteps: [] as number[],
@@ -281,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 };
});
});
@@ -344,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;
}
@@ -368,7 +437,7 @@ export default defineComponent({
}
function autoSetReferences() {
// Ingore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
// Ignore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
// other common phrases trigger false positives and I'm not sure how else to approach this. In the future I maybe look at looking directly
// at the food variable and seeing if the food is in the instructions, but I still need to support those who don't want to provide the value
// and only use the "notes" feature.
@@ -414,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);
}
// ===============================================================
@@ -493,7 +577,63 @@ export default defineComponent({
const drag = ref(false);
// ===============================================================
// Image Uploader
const api = useUserApi();
const { recipeAssetPath } = useStaticRoutes();
const imageUploadMode = ref(false);
function toggleDragMode() {
console.log("Toggling Drag Mode");
imageUploadMode.value = !imageUploadMode.value;
}
onMounted(() => {
if (props.assets === undefined) {
context.emit("update:assets", []);
}
});
async function handleImageDrop(index: number, e: DragEvent) {
if (!e.dataTransfer) {
return;
}
// Check if the file is an image
const file = e.dataTransfer.files[0];
if (!file || !file.type.startsWith("image/")) {
return;
}
const { data } = await api.recipes.createAsset(props.recipeSlug, {
name: file.name,
icon: "mdi-file-image",
file,
extension: file.name.split(".").pop() || "",
});
if (!data) {
return; // TODO: Handle error
}
context.emit("update:assets", [...props.assets, data]);
const assetUrl = BASE_URL + recipeAssetPath(props.recipeId, data.fileName as string);
const text = `<img src="${assetUrl}" height="100%" width="100%"/>`;
props.value[index].text += text;
}
function toggleCookMode() {
context.emit("cookModeToggle");
}
return {
// Image Uploader
toggleDragMode,
handleImageDrop,
imageUploadMode,
// Rest
drag,
togglePreviewState,
toggleCollapseSection,
@@ -514,6 +654,8 @@ export default defineComponent({
updateIndex,
autoSetReferences,
parseIngredientText,
toggleCookMode,
showCookMode,
};
},
});
@@ -553,4 +695,21 @@ export default defineComponent({
.list-group-item i {
cursor: pointer;
}
.blur {
filter: blur(2px);
}
.upload-overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
</style>

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

@@ -0,0 +1,152 @@
<template>
<div>
<v-dialog v-model="dialog" width="500">
<v-card>
<v-app-bar dense dark color="primary mb-2">
<v-icon large left class="mt-1">
{{ itemType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags }}
</v-icon>
<v-toolbar-title class="headline">
{{ properties.title }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-title> </v-card-title>
<v-form @submit.prevent="select">
<v-card-text>
<v-text-field
v-model="name"
dense
:label="properties.label"
:rules="[rules.required]"
autofocus
></v-text-field>
<v-checkbox v-if="itemType === Organizer.Tool" v-model="onHand" label="On Hand"></v-checkbox>
</v-card-text>
<v-card-actions>
<BaseButton cancel @click="dialog = false" />
<v-spacer></v-spacer>
<BaseButton type="submit" create :disabled="!name" />
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
import { RecipeOrganizer, Organizer } from "~/types/recipe/organizers";
const CREATED_ITEM_EVENT = "created-item";
export default defineComponent({
props: {
value: {
type: Boolean,
default: false,
},
color: {
type: String,
default: null,
},
tagDialog: {
type: Boolean,
default: true,
},
itemType: {
type: String as () => RecipeOrganizer,
default: "category",
},
},
setup(props, context) {
const state = reactive({
name: "",
onHand: false,
});
const dialog = computed({
get() {
return props.value;
},
set(value) {
context.emit("input", value);
},
});
watch(
() => props.value,
(val: boolean) => {
if (!val) state.name = "";
}
);
const userApi = useUserApi();
const store = (() => {
switch (props.itemType) {
case Organizer.Tag:
return useTagStore();
case Organizer.Tool:
return useToolStore();
default:
return useCategoryStore();
}
})();
const properties = computed(() => {
switch (props.itemType) {
case Organizer.Tag:
return {
title: "Create a Tag",
label: "Tag Name",
api: userApi.tags,
};
case Organizer.Tool:
return {
title: "Create a Tool",
label: "Tool Name",
api: userApi.tools,
};
default:
return {
title: "Create a Category",
label: "Category Name",
api: userApi.categories,
};
}
});
const rules = {
required: (val: string) => !!val || "A Name is Required",
};
async function select() {
if (store) {
// @ts-ignore - only property really required is the name
await store.actions.createOne({ name: state.name });
}
const newItem = store.items.value.find((item) => item.name === state.name);
context.emit(CREATED_ITEM_EVENT, newItem);
dialog.value = false;
}
return {
Organizer,
...toRefs(state),
dialog,
properties,
rules,
select,
};
},
});
</script>
<style></style>

View File

@@ -0,0 +1,133 @@
<template>
<div v-if="items">
<RecipeOrganizerDialog v-model="dialog" :item-type="itemType" />
<BaseDialog
v-if="deleteTarget"
v-model="deleteDialog"
:title="`Delete ${deleteTarget.name}`"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="deleteOne()"
>
<v-card-text> Are you sure you want to delete this {{ deleteTarget.name }}? </v-card-text>
</BaseDialog>
<v-app-bar color="transparent" flat class="mt-n1 rounded align-center">
<v-icon large left>
{{ icon }}
</v-icon>
<v-toolbar-title class="headline">
<slot name="title">
{{ headline }}
</slot>
</v-toolbar-title>
<v-spacer></v-spacer>
<BaseButton create @click="dialog = true" />
</v-app-bar>
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
<v-row>
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
<v-card class="left-border" hover :to="`/recipes/${itemType}/${item.slug}`">
<v-card-actions>
<v-icon>
{{ icon }}
</v-icon>
<v-card-title class="py-1">
{{ item.name }}
</v-card-title>
<v-spacer></v-spacer>
<ContextMenu :items="[presets.delete]" @delete="confirmDelete(item)" />
</v-card-actions>
</v-card>
</v-col>
</v-row>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
import { useContextPresets } from "~/composables/use-context-presents";
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
import { RecipeOrganizer } from "~/types/recipe/organizers";
interface GenericItem {
id?: string;
name: string;
slug: string;
}
export default defineComponent({
components: {
RecipeOrganizerDialog,
},
props: {
items: {
type: Array as () => GenericItem[],
required: true,
},
icon: {
type: String,
required: true,
},
itemType: {
type: String as () => RecipeOrganizer,
required: true,
},
},
setup(props, { emit }) {
// =================================================================
// Sorted Items
const itemsSorted = computed(() => {
const byLetter: { [key: string]: Array<GenericItem> } = {};
if (!props.items) return byLetter;
props.items.sort((a, b) => a.name.localeCompare(b.name)).forEach((item) => {
const letter = item.name[0].toUpperCase();
if (!byLetter[letter]) {
byLetter[letter] = [];
}
byLetter[letter].push(item);
});
return byLetter;
});
// =================================================================
// Context Menu
const presets = useContextPresets();
const deleteTarget = ref<GenericItem | null>(null);
const deleteDialog = ref(false);
function confirmDelete(item: GenericItem) {
deleteTarget.value = item;
deleteDialog.value = true;
}
function deleteOne() {
if (!deleteTarget.value) {
return;
}
emit("delete", deleteTarget.value.id);
}
const dialog = ref(false);
return {
dialog,
confirmDelete,
deleteOne,
deleteDialog,
deleteTarget,
presets,
itemsSorted,
};
},
// Needed for useMeta
head: {},
});
</script>

View File

@@ -1,14 +1,14 @@
<template>
<v-autocomplete
v-model="selected"
:items="items"
:items="storeItem"
:value="value"
:label="label"
chips
deletable-chips
item-text="name"
multiple
:prepend-inner-icon="$globals.icons.tags"
:prepend-inner-icon="selectorType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags"
return-object
v-bind="inputAttrs"
>
@@ -17,6 +17,7 @@
:key="data.index"
class="ma-1"
:input-value="data.selected"
small
close
label
color="accent"
@@ -26,41 +27,55 @@
{{ data.item.name || data.item }}
</v-chip>
</template>
<template v-if="showAdd" #append-outer>
<v-btn icon @click="dialog = true">
<v-icon>
{{ $globals.icons.create }}
</v-icon>
</v-btn>
<RecipeOrganizerDialog v-model="dialog" :item-type="selectorType" @created-item="appendCreated" />
</template>
</v-autocomplete>
</template>
<script lang="ts">
import { defineComponent, useContext } from "@nuxtjs/composition-api";
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { computed, onMounted } from "vue-demi";
import RecipeOrganizerDialog from "./RecipeOrganizerDialog.vue";
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
import { RecipeTool } from "~/types/api-types/admin";
type OrganizerType = "tag" | "category" | "tool";
import { useTagStore } from "~/composables/store/use-tag-store";
import { useCategoryStore, useToolStore } from "~/composables/store";
import { Organizer, RecipeOrganizer } from "~/types/recipe/organizers";
export default defineComponent({
components: {
RecipeOrganizerDialog,
},
props: {
value: {
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[] | undefined,
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool | string)[] | undefined,
required: true,
},
/**
* The type of organizer to use.
*/
selectorType: {
type: String as () => OrganizerType,
required: true,
},
/**
* List of items that are available to be chosen from
*/
items: {
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[],
type: String as () => RecipeOrganizer,
required: true,
},
inputAttrs: {
type: Object as () => Record<string, any>,
default: () => ({}),
},
returnObject: {
type: Boolean,
default: true,
},
showAdd: {
type: Boolean,
default: true,
},
},
setup(props, context) {
@@ -81,27 +96,62 @@ export default defineComponent({
const label = computed(() => {
switch (props.selectorType) {
case "tag":
case Organizer.Tag:
return i18n.t("tag.tags");
case "category":
case Organizer.Category:
return i18n.t("category.categories");
case "tool":
return "Tools";
case Organizer.Tool:
return i18n.t("tool.tools");
default:
return "Organizer";
}
});
// ===========================================================================
// Store & Items Setup
const store = (() => {
switch (props.selectorType) {
case Organizer.Tag:
return useTagStore();
case Organizer.Tool:
return useToolStore();
default:
return useCategoryStore();
}
})();
const items = computed(() => {
if (!props.returnObject) {
return store.items.value.map((item) => item.name);
}
return store.items.value;
});
function removeByIndex(index: number) {
if (selected.value === undefined) {
return;
}
const newSelected = selected.value.filter((_, i) => i !== index);
selected.value = [...newSelected];
}
function appendCreated(item: RecipeTag | RecipeCategory | RecipeTool) {
console.log(item);
if (selected.value === undefined) {
return;
}
selected.value = [...selected.value, item];
}
const dialog = ref(false);
return {
Organizer,
appendCreated,
dialog,
storeItem: items,
label,
selected,
removeByIndex,

View File

@@ -11,69 +11,82 @@
</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.value.firstHalf" :key="index">
{{ text }}
</li>
</ul>
</div>
<div class="ingredient-col-2">
<ul>
<li v-for="(text, index) in splitIngredients.value.secondHalf" :key="index">
{{ text }}
</li>
</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>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
// @ts-ignore vue-markdown has no types
import VueMarkdown from "@adapttive/vue-markdown";
import { computed } from "@vue/reactivity";
import { defineComponent, computed } from "@nuxtjs/composition-api";
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: {
@@ -82,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,
};
},
});
@@ -138,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;
@@ -157,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

@@ -122,8 +122,5 @@ export default defineComponent({
listItem,
};
},
head: {
title: "vbase-nuxt",
},
});
</script>
</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

@@ -0,0 +1,56 @@
<template>
<v-menu
offset-y
left
:bottom="!menuTop"
:nudge-bottom="!menuTop ? '5' : '0'"
:top="menuTop"
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
open-on-hover
content-class="d-print-none"
>
<template #activator="{ on, attrs }">
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
<v-icon>{{ $globals.icons.dotsVertical }}</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item v-for="(item, index) in items" :key="index" @click="$emit(item.event)">
<v-list-item-icon>
<v-icon :color="item.color ? item.color : undefined">
{{ item.icon }}
</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import { ContextMenuItem } from "~/composables/use-context-presents";
export default defineComponent({
props: {
items: {
type: Array as () => ContextMenuItem[],
required: true,
},
menuTop: {
type: Boolean,
default: true,
},
fab: {
type: Boolean,
default: false,
},
color: {
type: String,
default: "grey darken-2",
},
},
});
</script>

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

@@ -5,7 +5,7 @@
:buttons="[
{
icon: previewState ? $globals.icons.edit : $globals.icons.eye,
text: previewState ? $t('general.edit') : 'Preview Markdown',
text: previewState ? $tc('general.edit') : 'Preview Markdown',
event: 'toggle',
},
]"
@@ -14,28 +14,23 @@
</div>
<v-textarea
v-if="!previewState"
v-bind="textarea"
v-model="inputVal"
:class="label == '' ? '' : 'mt-5'"
:label="label"
auto-grow
dense
rows="4"
></v-textarea>
<VueMarkdown v-else :source="value"> </VueMarkdown>
/>
<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,
@@ -53,6 +48,10 @@ export default defineComponent({
type: Boolean,
default: true,
},
textarea: {
type: Object,
default: () => ({}),
},
},
setup(props, context) {
const fallbackPreview = ref(false);
@@ -84,5 +83,3 @@ export default defineComponent({
},
});
</script>

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

@@ -11,15 +11,15 @@ export const useStaticRoutes = () => {
// Methods to Generate reference urls for assets/images *
function recipeImage(recipeId: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/original.webp?&rnd=${key}&version=${version}`;
return `${fullBase}/media/recipes/${recipeId}/images/original.webp?rnd=${key}&version=${version}`;
}
function recipeSmallImage(recipeId: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?&rnd=${key}&version=${version}`;
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${version}`;
}
function recipeTinyImage(recipeId: string, version = "", key = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${version}`;
}
function recipeAssetPath(recipeId: string, assetName: string) {

View File

@@ -0,0 +1,99 @@
import { Ref, useAsync } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { BaseCRUDAPI } from "~/api/_base";
type BoundT = {
id?: string | number;
};
interface StoreActions<T extends BoundT> {
getAll(): Ref<T[] | null>;
refresh(): Promise<void>;
createOne(createData: T): Promise<void>;
updateOne(updateData: T): Promise<void>;
deleteOne(id: string | number): Promise<void>;
}
/**
* useStoreActions is a factory function that returns a set of methods
* that can be reused to manage the state of a data store without using
* Vuex. This is primarily used for basic CRUD operations that required
* a lot of refreshing hooks to be called on operations
*/
export function useStoreActions<T extends BoundT>(
api: BaseCRUDAPI<unknown, T, unknown>,
allRef: Ref<T[] | null> | null,
loading: Ref<boolean>
): StoreActions<T> {
function getAll() {
loading.value = true;
const allItems = useAsync(async () => {
const { data } = await api.getAll();
if (data && allRef) {
allRef.value = data.items;
}
if (data) {
return data.items ?? [];
} else {
return [];
}
}, useAsyncKey());
loading.value = false;
return allItems;
}
async function refresh() {
loading.value = true;
const { data } = await api.getAll();
if (data && data.items && allRef) {
allRef.value = data.items;
}
loading.value = false;
}
async function createOne(createData: T) {
loading.value = true;
const { data } = await api.createOne(createData);
if (data && allRef?.value) {
allRef.value.push(data);
} else {
refresh();
}
loading.value = false;
}
async function updateOne(updateData: T) {
if (!updateData.id) {
return;
}
loading.value = true;
const { data } = await api.updateOne(updateData.id, updateData);
if (data && allRef?.value) {
refresh();
}
loading.value = false;
}
async function deleteOne(id: string | number) {
loading.value = true;
const { response } = await api.deleteOne(id);
if (response && allRef?.value) {
refresh();
}
loading.value = false;
}
return {
getAll,
refresh,
createOne,
updateOne,
deleteOne,
};
}

View File

@@ -1,9 +1,6 @@
export { useFraction } from "./use-fraction";
export { useRecipe } from "./use-recipe";
export { useFoods } from "./use-recipe-foods";
export { useUnits } from "./use-recipe-units";
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes";
export { useTags, useCategories, allCategories, allTags } from "./use-tags-categories";
export { parseIngredientText } from "./use-recipe-ingredients";
export { useRecipeSearch } from "./use-recipe-search";
export { useTools } from "./use-recipe-tools";

View File

@@ -1,104 +0,0 @@
import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { useUserApi } from "~/composables/api";
import { VForm } from "~/types/vuetify";
import { IngredientFood } from "~/types/api-types/recipe";
let foodStore: Ref<IngredientFood[] | null> | null = null;
export const useFoods = function () {
const api = useUserApi();
const loading = ref(false);
const deleteTargetId = ref(0);
const validForm = ref(true);
const workingFoodData = reactive<IngredientFood>({
id: "",
name: "",
description: "",
labelId: undefined,
});
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data } = await api.foods.getAll();
return data;
}, useAsyncKey());
loading.value = false;
return units;
},
async refreshAll() {
loading.value = true;
const { data } = await api.foods.getAll();
if (data && foodStore) {
foodStore.value = data;
}
loading.value = false;
},
async createOne(domForm: VForm | null = null) {
if (domForm && !domForm.validate()) {
validForm.value = false;
return;
}
loading.value = true;
const { data } = await api.foods.createOne(workingFoodData);
if (data && foodStore?.value) {
foodStore.value.push(data);
return data;
} else {
this.refreshAll();
}
domForm?.reset();
validForm.value = true;
this.resetWorking();
loading.value = false;
},
async updateOne() {
if (!workingFoodData.id) {
return;
}
loading.value = true;
console.log(workingFoodData);
const { data } = await api.foods.updateOne(workingFoodData.id, workingFoodData);
if (data && foodStore?.value) {
this.refreshAll();
}
loading.value = false;
},
async deleteOne(id: string | number) {
loading.value = true;
const { data } = await api.foods.deleteOne(id);
if (data && foodStore?.value) {
this.refreshAll();
}
},
resetWorking() {
workingFoodData.id = "";
workingFoodData.name = "";
workingFoodData.description = "";
workingFoodData.labelId = undefined;
},
setWorking(item: IngredientFood) {
workingFoodData.id = item.id;
workingFoodData.name = item.name;
workingFoodData.description = item.description || "";
workingFoodData.labelId = item.labelId;
},
flushStore() {
foodStore = null;
},
};
if (!foodStore) {
foodStore = actions.getAll();
}
return { foods: foodStore, workingFoodData, deleteTargetId, actions, validForm };
};

View File

@@ -19,9 +19,10 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
let returnQty = "";
let unitDisplay = unit?.name;
// 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) {
@@ -34,8 +35,12 @@ export function parseIngredientText(ingredient: RecipeIngredient, disableAmount:
} else {
returnQty = (quantity * scale).toString();
}
if (unit?.useAbbreviation && unit.abbreviation) {
unitDisplay = unit.abbreviation;
}
}
const text = `${returnQty} ${unit?.name || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " ");
const text = `${returnQty} ${unitDisplay || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " ");
return sanitizeIngredientHTML(text);
}

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

@@ -1,104 +0,0 @@
import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { useUserApi } from "~/composables/api";
import { VForm } from "~/types/vuetify";
import { IngredientUnit } from "~/types/api-types/recipe";
let unitStore: Ref<IngredientUnit[] | null> | null = null;
export const useUnits = function () {
const api = useUserApi();
const loading = ref(false);
const deleteTargetId = ref(0);
const validForm = ref(true);
const workingUnitData: IngredientUnit = reactive({
id: "",
name: "",
fraction: true,
abbreviation: "",
description: "",
});
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data } = await api.units.getAll();
return data;
}, useAsyncKey());
loading.value = false;
return units;
},
async refreshAll() {
loading.value = true;
const { data } = await api.units.getAll();
if (data && unitStore) {
unitStore.value = data;
}
loading.value = false;
},
async createOne(domForm: VForm | null = null) {
if (domForm && !domForm.validate()) {
validForm.value = false;
return;
}
loading.value = true;
const { data } = await api.units.createOne(workingUnitData);
if (data && unitStore?.value) {
unitStore.value.push(data);
} else {
this.refreshAll();
}
domForm?.reset();
validForm.value = true;
this.resetWorking();
loading.value = false;
},
async updateOne() {
if (!workingUnitData.id) {
return;
}
loading.value = true;
const { data } = await api.units.updateOne(workingUnitData.id, workingUnitData);
if (data && unitStore?.value) {
this.refreshAll();
}
loading.value = false;
},
async deleteOne(id: string | number) {
loading.value = true;
const { data } = await api.units.deleteOne(id);
if (data && unitStore?.value) {
this.refreshAll();
}
},
resetWorking() {
workingUnitData.id = "";
workingUnitData.name = "";
workingUnitData.abbreviation = "";
workingUnitData.description = "";
},
setWorking(item: IngredientUnit) {
workingUnitData.id = item.id;
workingUnitData.name = item.name;
workingUnitData.fraction = item.fraction;
workingUnitData.abbreviation = item.abbreviation;
workingUnitData.description = item.description;
},
flushStore() {
unitStore = null;
},
};
if (!unitStore) {
unitStore = actions.getAll();
}
return { units: unitStore, workingUnitData, deleteTargetId, actions, validForm };
};

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

@@ -1,65 +0,0 @@
import { Ref, ref, useAsync } from "@nuxtjs/composition-api";
import { useUserApi } from "../api";
import { useAsyncKey } from "../use-utils";
import { CategoriesAPI } from "~/api/class-interfaces/organizer-categories";
import { TagsAPI } from "~/api/class-interfaces/organizer-tags";
import { RecipeTag, RecipeCategory } from "~/types/api-types/recipe";
export const allCategories = ref<RecipeCategory[] | null>([]);
export const allTags = ref<RecipeTag[] | null>([]);
function baseTagsCategories(
reference: Ref<RecipeCategory[] | null> | Ref<RecipeTag[] | null>,
api: TagsAPI | CategoriesAPI
) {
function useAsyncGetAll() {
useAsync(async () => {
await refreshItems();
}, useAsyncKey());
}
async function refreshItems() {
const { data } = await api.getAll();
// @ts-ignore hotfix
reference.value = data;
}
async function createOne(payload: { name: string }) {
const { data } = await api.createOne(payload);
if (data) {
refreshItems();
}
}
async function deleteOne(slug: string) {
const { data } = await api.deleteOne(slug);
if (data) {
refreshItems();
}
}
async function updateOne(slug: string, payload: { name: string }) {
// @ts-ignore // TODO: Fix Typescript Issue - Unsure how to fix this while also keeping mixins
const { data } = await api.updateOne(slug, payload);
if (data) {
refreshItems();
}
}
return { useAsyncGetAll, refreshItems, createOne, deleteOne, updateOne };
}
export const useTags = function () {
const api = useUserApi();
return {
allTags,
...baseTagsCategories(allTags, api.tags),
};
};
export const useCategories = function () {
const api = useUserApi();
return {
allCategories,
...baseTagsCategories(allCategories, api.categories),
};
};

View File

@@ -0,0 +1,6 @@
export { useFoodStore, useFoodData } from "./use-food-store";
export { useUnitStore, useUnitData } from "./use-unit-store";
export { useLabelStore, useLabelData } from "./use-label-store";
export { useToolStore, useToolData } from "./use-tool-store";
export { useCategoryStore, useCategoryData } from "./use-category-store";
export { useTagStore, useTagData } from "./use-tag-store";

View File

@@ -0,0 +1,47 @@
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
import { useStoreActions } from "../partials/use-actions-factory";
import { useUserApi } from "~/composables/api";
import { RecipeCategory } from "~/types/api-types/admin";
const categoryStore: Ref<RecipeCategory[]> = ref([]);
export function useCategoryData() {
const data = reactive({
id: "",
name: "",
slug: undefined,
});
function reset() {
data.id = "";
data.name = "";
data.slug = undefined;
}
return {
data,
reset,
};
}
export function useCategoryStore() {
const api = useUserApi();
const loading = ref(false);
const actions = {
...useStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
flushStore() {
categoryStore.value = [];
},
};
if (!categoryStore.value || categoryStore.value?.length === 0) {
actions.getAll();
}
return {
items: categoryStore,
actions,
loading,
};
}

View File

@@ -0,0 +1,50 @@
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useStoreActions } from "../partials/use-actions-factory";
import { useUserApi } from "~/composables/api";
import { IngredientFood } from "~/types/api-types/recipe";
let foodStore: Ref<IngredientFood[] | null> | null = null;
/**
* useFoodData returns a template reactive object
* for managing the creation of units. It also provides a
* function to reset the data back to the initial state.
*/
export const useFoodData = function () {
const data: IngredientFood = reactive({
id: "",
name: "",
description: "",
labelId: undefined,
});
function reset() {
data.id = "";
data.name = "";
data.description = "";
data.labelId = undefined;
}
return {
data,
reset,
};
};
export const useFoodStore = function () {
const api = useUserApi();
const loading = ref(false);
const actions = {
...useStoreActions(api.foods, foodStore, loading),
flushStore() {
foodStore = null;
},
};
if (!foodStore) {
foodStore = actions.getAll();
}
return { foods: foodStore, actions };
};

View File

@@ -0,0 +1,49 @@
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
import { useStoreActions } from "../partials/use-actions-factory";
import { MultiPurposeLabelOut } from "~/types/api-types/labels";
import { useUserApi } from "~/composables/api";
let labelStore: Ref<MultiPurposeLabelOut[] | null> | null = null;
export function useLabelData() {
const data = reactive({
groupId: "",
id: "",
name: "",
color: "",
});
function reset() {
data.groupId = "";
data.id = "";
data.name = "";
data.color = "";
}
return {
data,
reset,
};
}
export function useLabelStore() {
const api = useUserApi();
const loading = ref(false);
const actions = {
...useStoreActions<MultiPurposeLabelOut>(api.multiPurposeLabels, labelStore, loading),
flushStore() {
labelStore = null;
},
};
if (!labelStore) {
labelStore = actions.getAll();
}
return {
labels: labelStore,
actions,
loading,
};
}

View File

@@ -0,0 +1,47 @@
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
import { useStoreActions } from "../partials/use-actions-factory";
import { useUserApi } from "~/composables/api";
import { RecipeTag } from "~/types/api-types/admin";
const items: Ref<RecipeTag[]> = ref([]);
export function useTagData() {
const data = reactive({
id: "",
name: "",
slug: undefined,
});
function reset() {
data.id = "";
data.name = "";
data.slug = undefined;
}
return {
data,
reset,
};
}
export function useTagStore() {
const api = useUserApi();
const loading = ref(false);
const actions = {
...useStoreActions<RecipeTag>(api.tags, items, loading),
flushStore() {
items.value = [];
},
};
if (!items.value || items.value?.length === 0) {
actions.getAll();
}
return {
items,
actions,
loading,
};
}

View File

@@ -0,0 +1,49 @@
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
import { useStoreActions } from "../partials/use-actions-factory";
import { useUserApi } from "~/composables/api";
import { RecipeTool } from "~/types/api-types/recipe";
const toolStore: Ref<RecipeTool[]> = ref([]);
export function useToolData() {
const data = reactive({
id: "",
name: "",
slug: undefined,
onHand: false,
});
function reset() {
data.id = "";
data.name = "";
data.slug = undefined;
data.onHand = false;
}
return {
data,
reset,
};
}
export function useToolStore() {
const api = useUserApi();
const loading = ref(false);
const actions = {
...useStoreActions<RecipeTool>(api.tools, toolStore, loading),
flushStore() {
toolStore.value = [];
},
};
if (!toolStore.value || toolStore.value?.length === 0) {
actions.getAll();
}
return {
items: toolStore,
actions,
loading,
};
}

View File

@@ -0,0 +1,52 @@
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useStoreActions } from "../partials/use-actions-factory";
import { useUserApi } from "~/composables/api";
import { IngredientUnit } from "~/types/api-types/recipe";
let unitStore: Ref<IngredientUnit[] | null> | null = null;
/**
* useUnitData returns a template reactive object
* for managing the creation of units. It also provides a
* function to reset the data back to the initial state.
*/
export const useUnitData = function () {
const data: IngredientUnit = reactive({
id: "",
name: "",
fraction: true,
abbreviation: "",
description: "",
});
function reset() {
data.id = "";
data.name = "";
data.fraction = true;
data.abbreviation = "";
data.description = "";
}
return {
data,
reset,
};
};
export const useUnitStore = function () {
const api = useUserApi();
const loading = ref(false);
const actions = {
...useStoreActions<IngredientUnit>(api.units, unitStore, loading),
flushStore() {
unitStore = null;
},
};
if (!unitStore) {
unitStore = actions.getAll();
}
return { units: unitStore, actions };
};

View File

@@ -0,0 +1,30 @@
import { useContext } from "@nuxtjs/composition-api";
export interface ContextMenuItem {
title: string;
icon: string;
event: string;
color?: string;
}
export function useContextPresets(): { [key: string]: ContextMenuItem } {
const { $globals, i18n } = useContext();
return {
delete: {
title: i18n.tc("general.delete"),
icon: $globals.icons.delete,
event: "delete",
},
edit: {
title: i18n.tc("general.edit"),
icon: $globals.icons.edit,
event: "edit",
},
save: {
title: i18n.tc("general.save"),
icon: $globals.icons.save,
event: "save",
},
};
}

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

@@ -18,4 +18,4 @@
"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",
@@ -521,7 +522,7 @@
"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": "Unione di {food1} in {food2}",
"seed-dialog-text": "Inizializza il database con alimenti in base alla tua lingua locale. Questo creerà oltre 200 alimenti comuni che possono essere utilizzati per organizzare il tuo database. Gli alimenti sono tradotti grazie al contributo della comunità di utenti.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
"seed-dialog-warning": "Hai già alcuni elementi nel tuo database. Questa azione non riconcilierà i duplicati, dovrai gestirli manualmente."
},
"units": {
"seed-dialog-text": "Seed the database with common units based on your local language."
@@ -534,11 +535,11 @@
"user-registration": "Registrazione Utente",
"join-a-group": "Unisciti a un Gruppo",
"create-a-new-group": "Crea un Nuovo Gruppo",
"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": "Fornisci il token di registrazione associato al gruppo a cui desideri partecipare. Dovrai ottenerlo da un membro di gruppo esistente.",
"group-details": "Dettagli del Guppo",
"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.",
"use-seed-data-description": "Mealie viene fornito con una raccolta di alimenti, unità ed etichette che possono essere utilizzate per popolare il tuo gruppo con dati utili per organizzare le tue ricette.",
"account-details": "Dettagli dell'Account"
},
"validation": {

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": {

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