Merge pull request #49 from hay-kot/api-docs
Improved API Documentation, New ENV Varibles and Minor refactoring/renaming
This commit is contained in:
commit
a731b9f6ab
34 changed files with 754 additions and 276 deletions
20
.github/workflows/build-docs.yml
vendored
Normal file
20
.github/workflows/build-docs.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: Publish docs via GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CONFIG_FILE: docs/mkdocs.yml
|
||||
EXTRA_PACKAGES: build-base
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,6 +5,9 @@ __pycache__/
|
|||
*$py.class
|
||||
# frontend/.env.development
|
||||
docs/site/
|
||||
mealie/temp/*
|
||||
mealie/temp/api.html
|
||||
|
||||
|
||||
mealie/data/backups/*
|
||||
mealie/data/debug/*
|
||||
|
|
|
@ -20,11 +20,14 @@
|
|||
A Place for All Your Recipes
|
||||
<br />
|
||||
<a href="https://hay-kot.github.io/mealie/"><strong>Explore the docs »</strong></a>
|
||||
<br />
|
||||
<a href="https://github.com/hay-kot/mealie">
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://github.com/hay-kot/mealie"><s>View Demo</s></a>
|
||||
·
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||
·
|
||||
<a href="/api/docs">API</a>
|
||||
·
|
||||
<a href="https://github.com/hay-kot/mealie/issues">
|
||||
Request Feature
|
||||
|
@ -32,7 +35,6 @@
|
|||
·
|
||||
<a href="https://hub.docker.com/repository/docker/hkotel/mealies"> Docker Hub
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
|
|
5
docs/docs/api/api-examples.md
Normal file
5
docs/docs/api/api-examples.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# API Examples
|
||||
|
||||
TODO
|
||||
|
||||
Have Ideas? Submit a PR!
|
|
@ -1,3 +0,0 @@
|
|||
# API Introduction
|
||||
|
||||
TODO
|
26
docs/docs/api/docs/index.html
Normal file
26
docs/docs/api/docs/index.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -16,6 +16,7 @@ To deploy docker on your local network it is highly recommended to use docker to
|
|||
| db_password | example | The Mongodb password you specified in your mongo container |
|
||||
| db_host | mongo | The host address of MongoDB if you're in docker and using the same network you can use mongo as the host name |
|
||||
| db_port | 27017 | the port to access MongoDB 27017 is the default for mongo |
|
||||
| api_docs | True | Turns on/off access to the API documentation locally. |
|
||||
| TZ | | You should set your time zone accordingly so the date/time features work correctly |
|
||||
|
||||
|
||||
|
|
26
docs/docs/html/api.html
Normal file
26
docs/docs/html/api.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -8,7 +8,9 @@
|
|||
<br />
|
||||
<a href="https://github.com/hay-kot/mealie"><s>View Demo</s></a>
|
||||
·
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||
·
|
||||
<a href="/api/docs">API</a>
|
||||
·
|
||||
<a href="https://github.com/hay-kot/mealie/issues">
|
||||
Request Feature
|
||||
|
|
|
@ -7,7 +7,6 @@ theme:
|
|||
logo: material/silverware-variant
|
||||
features:
|
||||
- navigation.expand
|
||||
- navigation.instant
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.emoji:
|
||||
|
@ -35,7 +34,8 @@ nav:
|
|||
- Backups and Exports: "getting-started/backups-and-exports.md"
|
||||
- Recipe Migration: "getting-started/migration-imports.md"
|
||||
- API Reference:
|
||||
- Swagger/OpenAPI: "api/api-intro.md"
|
||||
- API Documentation: "api/docs/index.html"
|
||||
- Usage Examples: "api/api-examples.md"
|
||||
- Contributors Guide:
|
||||
- Non-Code: "contributors/non-coders.md"
|
||||
- Developers Guide:
|
||||
|
|
5
frontend/.vscode/settings.json
vendored
Normal file
5
frontend/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"cSpell.enableFiletypes": [
|
||||
"!javascript"
|
||||
]
|
||||
}
|
193
frontend/package-lock.json
generated
193
frontend/package-lock.json
generated
|
@ -1738,6 +1738,16 @@
|
|||
"integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/cacache/download/cacache-13.0.1.tgz?cache=0&sync_timestamp=1594428402513&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-13.0.1.tgz",
|
||||
|
@ -1764,6 +1774,34 @@
|
|||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-3.3.1.tgz?cache=0&sync_timestamp=1583735626956&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-cache-dir%2Fdownload%2Ffind-cache-dir-3.3.1.tgz",
|
||||
|
@ -1785,6 +1823,25 @@
|
|||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz?cache=0&sync_timestamp=1597081764621&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flocate-path%2Fdownload%2Flocate-path-5.0.0.tgz",
|
||||
|
@ -1849,6 +1906,16 @@
|
|||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-2.3.8.tgz?cache=0&sync_timestamp=1603882075288&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser-webpack-plugin%2Fdownload%2Fterser-webpack-plugin-2.3.8.tgz",
|
||||
|
@ -1865,6 +1932,18 @@
|
|||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -9082,7 +9161,7 @@
|
|||
},
|
||||
"rechoir": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
||||
"resolved": "https://registry.npm.taobao.org/rechoir/download/rechoir-0.6.2.tgz",
|
||||
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -9736,6 +9815,11 @@
|
|||
"rechoir": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"shvl": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.1.tgz",
|
||||
"integrity": "sha512-VU7R5Uxp38LKHooGuZe0TcX2EPK95nn8DvclAvTPyD9/qHmXvt3dR2pJ4JLZ8uLjxQNQ3zNLFJCreteIj3cvpw=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz?cache=0&sync_timestamp=1585253323149&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsignal-exit%2Fdownload%2Fsignal-exit-3.0.3.tgz",
|
||||
|
@ -11065,11 +11149,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-cookies": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-cookies/-/vue-cookies-1.7.4.tgz",
|
||||
"integrity": "sha512-mOS5Btr8V9zvAtkmQ7/TfqJIropOx7etDAgBywPCmHjvfJl2gFbH2XgoMghleLoyyMTi5eaJss0mPN7arMoslA=="
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npm.taobao.org/vue-eslint-parser/download/vue-eslint-parser-7.1.1.tgz",
|
||||
|
@ -11102,11 +11181,6 @@
|
|||
"integrity": "sha1-UylVzB6yCKPZkLOp+acFdGV+CPI=",
|
||||
"dev": true
|
||||
},
|
||||
"vue-html-to-paper": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-html-to-paper/-/vue-html-to-paper-1.3.1.tgz",
|
||||
"integrity": "sha512-5IdAPUgStfpVHfcG6nXD0FbUB1onWpvwVD+OZ00jJpy3qaRPkaGD7fFIvYgBB9YPkr0VK065LayEvmGmkkfhaQ=="
|
||||
},
|
||||
"vue-loader": {
|
||||
"version": "15.9.5",
|
||||
"resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-15.9.5.tgz?cache=0&sync_timestamp=1605670886675&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-15.9.5.tgz",
|
||||
|
@ -11128,87 +11202,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
||||
|
@ -11268,6 +11261,22 @@
|
|||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz",
|
||||
"integrity": "sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ=="
|
||||
},
|
||||
"vuex-persistedstate": {
|
||||
"version": "4.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-JeiweafcU+9d4+/nRvQwK2PyHS9xCRcGIlL2cn0ny/afTw2RP+5M6SdsjkcYoGNICTGPi5i+K3J46ioWEyVgvg==",
|
||||
"requires": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"shvl": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npm.taobao.org/watchpack/download/watchpack-1.7.5.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwatchpack%2Fdownload%2Fwatchpack-1.7.5.tgz",
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"qs": "^6.9.4",
|
||||
"v-jsoneditor": "^1.4.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-html-to-paper": "^1.3.1",
|
||||
"vue-router": "^3.4.9",
|
||||
"vuetify": "^2.4.1",
|
||||
"vuex": "^3.6.0",
|
||||
|
|
|
@ -16,17 +16,11 @@
|
|||
mandatory
|
||||
@change="setStoresDarkMode"
|
||||
>
|
||||
<v-btn value="system">
|
||||
Default to system
|
||||
</v-btn>
|
||||
<v-btn value="system"> Default to system </v-btn>
|
||||
|
||||
<v-btn value="light">
|
||||
Light
|
||||
</v-btn>
|
||||
<v-btn value="light"> Light </v-btn>
|
||||
|
||||
<v-btn value="dark">
|
||||
Dark
|
||||
</v-btn>
|
||||
<v-btn value="dark"> Dark </v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row></v-card-text
|
||||
|
@ -50,7 +44,7 @@
|
|||
return-object
|
||||
v-model="selectedTheme"
|
||||
@change="themeSelected"
|
||||
:rules="[v => !!v || 'Theme is required']"
|
||||
:rules="[(v) => !!v || 'Theme is required']"
|
||||
required
|
||||
>
|
||||
</v-select>
|
||||
|
@ -140,19 +134,20 @@ export default {
|
|||
components: {
|
||||
ColorPicker,
|
||||
Confirmation,
|
||||
NewTheme
|
||||
NewTheme,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTheme: {},
|
||||
selectedDarkMode: "system",
|
||||
availableThemes: []
|
||||
availableThemes: [],
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.availableThemes = await api.themes.requestAll();
|
||||
this.selectedTheme = this.$store.getters.getActiveTheme;
|
||||
this.selectedDarkMode = this.$store.getters.getDarkMode;
|
||||
console.log(this.selectedDarkMode);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -181,7 +176,7 @@ export default {
|
|||
//Change to default if deleting current theme.
|
||||
if (
|
||||
!this.availableThemes.some(
|
||||
theme => theme.name === this.selectedTheme.name
|
||||
(theme) => theme.name === this.selectedTheme.name
|
||||
)
|
||||
) {
|
||||
await this.$store.dispatch("resetTheme");
|
||||
|
@ -203,6 +198,7 @@ export default {
|
|||
},
|
||||
|
||||
setStoresDarkMode() {
|
||||
console.log(this.selectedDarkMode);
|
||||
this.$store.commit("setDarkMode", this.selectedDarkMode);
|
||||
},
|
||||
/**
|
||||
|
@ -216,8 +212,8 @@ export default {
|
|||
this.selectedTheme.colors
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
198
frontend/src/components/RecipeEditor/PrintRecipe.vue
Normal file
198
frontend/src/components/RecipeEditor/PrintRecipe.vue
Normal file
|
@ -0,0 +1,198 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card flat class="d-print-none">
|
||||
<v-card-text>
|
||||
<v-row align="center" justify="center">
|
||||
<v-btn
|
||||
left
|
||||
color="accent lighten-1 "
|
||||
class="ma-1 image-action"
|
||||
@click="$emit('exit')"
|
||||
>
|
||||
<v-icon> mdi-arrow-left </v-icon>
|
||||
</v-btn>
|
||||
<v-card flat class="text-center" align-center>
|
||||
<v-card-text>Font Size</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab
|
||||
dark
|
||||
x-small
|
||||
color="primary"
|
||||
@click="subtractFontSize"
|
||||
>
|
||||
<v-icon dark> mdi-minus </v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab
|
||||
dark
|
||||
x-small
|
||||
color="primary"
|
||||
@click="addFontSize"
|
||||
>
|
||||
<v-icon dark> mdi-plus </v-icon>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card flat>
|
||||
<v-row dense align="center">
|
||||
<v-col md="10" sm="10">
|
||||
<v-card flat>
|
||||
<v-card-title> {{ recipe.name }} </v-card-title>
|
||||
|
||||
<v-card-text> {{ recipe.description }} </v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col md="1" sm="1" justify-end>
|
||||
<v-img :src="getImage(recipe.image)" max-height="200" max-width="300">
|
||||
</v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card flat align>
|
||||
<v-card-text>
|
||||
<v-row class="mt-n6">
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-rating
|
||||
class="mr-2 align-end static"
|
||||
color="secondary darken-1"
|
||||
background-color="secondary lighten-3"
|
||||
length="5"
|
||||
:value="recipe.rating"
|
||||
></v-rating>
|
||||
</v-row>
|
||||
<h2 class="mt-1">Ingredients</h2>
|
||||
<v-row>
|
||||
<v-list dense class="column-wrapper align-start">
|
||||
<v-list-item
|
||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||
:key="generateKey('ingredient', index)"
|
||||
hide-details
|
||||
class="mb-n3 print-text"
|
||||
:label="ingredient"
|
||||
>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon> mdi-minus </v-icon>
|
||||
</v-list-item-icon>
|
||||
{{ ingredient }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<div v-if="recipe.categories[0]">
|
||||
<h2 class="mt-4">Categories</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="primary"
|
||||
dark
|
||||
v-for="category in recipe.categories"
|
||||
:key="category"
|
||||
>
|
||||
{{ category }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="recipe.tags[0]">
|
||||
<h2 class="mt-4">Tags</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="primary"
|
||||
dark
|
||||
v-for="tag in recipe.tags"
|
||||
:key="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<h2 v-if="recipe.notes[0]" class="my-2">Notes</h2>
|
||||
<v-card
|
||||
flat
|
||||
class="mt-1"
|
||||
v-for="(note, index) in recipe.notes"
|
||||
:key="generateKey('note', index)"
|
||||
>
|
||||
<v-card-title> {{ note.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
{{ note.text }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<h2 class="mb-4">Instructions</h2>
|
||||
|
||||
<v-card
|
||||
v-for="(step, index) in recipe.recipeInstructions"
|
||||
:key="generateKey('step', index)"
|
||||
class="my-n4"
|
||||
flat
|
||||
>
|
||||
<v-card-title class="my-n4">Step: {{ index + 1 }}</v-card-title>
|
||||
<v-card-text class="my-n4">{{ step.text }}</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "../../utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
recipe: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fontSize: 1.0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getImage(image) {
|
||||
if (image) {
|
||||
return utils.getImageURL(image) + "?rnd=" + this.imageKey;
|
||||
}
|
||||
},
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
addFontSize() {
|
||||
this.fontSize += 0.2;
|
||||
},
|
||||
subtractFontSize() {
|
||||
this.fontSize -= 0.2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.column-wrapper {
|
||||
column-count: 2;
|
||||
}
|
||||
</style>
|
|
@ -1,11 +1,22 @@
|
|||
|
||||
import api from "../../api";
|
||||
import Vuetify from "../../plugins/vuetify";
|
||||
|
||||
function inDarkMode(payload) {
|
||||
let isDark;
|
||||
|
||||
if (payload === "system") {
|
||||
//Get System Preference from browser
|
||||
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
isDark = darkMediaQuery.matches;
|
||||
} else if (payload === "dark") isDark = true;
|
||||
else if (payload === "light") isDark = false;
|
||||
|
||||
return isDark;
|
||||
}
|
||||
|
||||
const state = {
|
||||
activeTheme: {},
|
||||
darkMode: 'system'
|
||||
|
||||
darkMode: "system",
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
@ -15,17 +26,7 @@ const mutations = {
|
|||
state.activeTheme = payload;
|
||||
},
|
||||
setDarkMode(state, payload) {
|
||||
let isDark;
|
||||
|
||||
if (payload === 'system') {
|
||||
//Get System Preference from browser
|
||||
const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
isDark = darkMediaQuery.matches;
|
||||
}
|
||||
else if (payload === 'dark')
|
||||
isDark = true;
|
||||
else if (payload === 'light')
|
||||
isDark = false;
|
||||
let isDark = inDarkMode(payload);
|
||||
|
||||
if (isDark !== null) {
|
||||
Vuetify.framework.theme.dark = isDark;
|
||||
|
@ -40,31 +41,30 @@ const actions = {
|
|||
if (defaultTheme.colors) {
|
||||
Vuetify.framework.theme.themes.dark = defaultTheme.colors;
|
||||
Vuetify.framework.theme.themes.light = defaultTheme.colors;
|
||||
commit('setTheme', defaultTheme)
|
||||
commit("setTheme", defaultTheme);
|
||||
}
|
||||
},
|
||||
|
||||
async initTheme({ dispatch, getters }) {
|
||||
//If theme is empty resetTheme
|
||||
if (Object.keys(getters.getActiveTheme).length === 0) {
|
||||
await dispatch('resetTheme')
|
||||
}
|
||||
else {
|
||||
await dispatch("resetTheme");
|
||||
} else {
|
||||
Vuetify.framework.theme.dark = inDarkMode(getters.getDarkMode);
|
||||
Vuetify.framework.theme.themes.dark = getters.getActiveTheme.colors;
|
||||
Vuetify.framework.theme.themes.light = getters.getActiveTheme.colors;
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getActiveTheme: (state) => state.activeTheme,
|
||||
getDarkMode: (state) => state.darkMode
|
||||
}
|
||||
getDarkMode: (state) => state.darkMode,
|
||||
};
|
||||
|
||||
export default {
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
getters
|
||||
}
|
||||
getters,
|
||||
};
|
||||
|
|
|
@ -7,11 +7,13 @@ import userSettings from "./modules/userSettings";
|
|||
Vue.use(Vuex);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
plugins: [createPersistedState({
|
||||
paths: ['userSettings']
|
||||
})],
|
||||
plugins: [
|
||||
createPersistedState({
|
||||
paths: ["userSettings"],
|
||||
}),
|
||||
],
|
||||
modules: {
|
||||
userSettings
|
||||
userSettings,
|
||||
},
|
||||
state: {
|
||||
// Snackbar
|
||||
|
@ -40,7 +42,6 @@ const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
actions: {
|
||||
|
||||
async requestRecentRecipes() {
|
||||
const keys = [
|
||||
"name",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from pathlib import Path
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
|
@ -15,21 +14,27 @@ from routes import (
|
|||
static_routes,
|
||||
user_routes,
|
||||
)
|
||||
from routes.setting_routes import scheduler
|
||||
from settings import PORT
|
||||
from routes.setting_routes import scheduler # ! This has to be imported for scheduling
|
||||
from settings import PORT, PRODUCTION, docs_url, redoc_url
|
||||
from utils.logger import logger
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
WEB_PATH = CWD.joinpath("dist")
|
||||
|
||||
app = FastAPI()
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
description="A place for all your recipes",
|
||||
version="0.0.1",
|
||||
docs_url=docs_url,
|
||||
redoc_url=redoc_url,
|
||||
)
|
||||
|
||||
|
||||
# Mount Vue Frontend only in production
|
||||
env = os.environ.get("ENV")
|
||||
if(env == "prod"):
|
||||
if PRODUCTION:
|
||||
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
||||
|
||||
|
||||
# API Routes
|
||||
app.include_router(recipe_routes.router)
|
||||
app.include_router(meal_routes.router)
|
||||
|
@ -49,6 +54,9 @@ app.include_router(static_routes.router)
|
|||
startup.ensure_dirs()
|
||||
startup.generate_default_theme()
|
||||
|
||||
# Generate API Documentation
|
||||
if not PRODUCTION:
|
||||
startup.generate_api_docs(app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("-----SYSTEM STARTUP-----")
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Recipe",
|
||||
"articleBody": "Leftover rice is ideal for this dish (and a great way to use up any takeout that\u2019s hanging around), since fully chilled rice tends to be drier and will become crispier and browner in the skillet. To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it out like a pancake. Don\u2019t touch until you hear it crackle! Finish with a sunny-side-up egg\u2014or poach it if you don't mind the stovetop fuss. This recipe is part of the 2021\u00a0Feel Good Food Plan, our eight-day dinner plan for starting the year off right.",
|
||||
"alternativeHeadline": "To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it. Don\u2019t touch until you hear it crackle!",
|
||||
"dateModified": "2021-01-03 03:40:32.190000",
|
||||
"datePublished": "2021-01-01 06:00:00",
|
||||
"articleBody": "\u201cAfter a draining day juggling work, homeschooling, and urging children to stop using their masks as slingshots, the ideal food for me isn\u2019t perfectly prepared food that\u2019s been tweezered into position, but a meal that\u2019s simply comforting,\u201d writes the Smitten Kitchen\u2019s Deb Perelman. Right now, it\u2019s this deeply cozy pot of tender chicken thighs, jammy leeks, and broth-soaked rice.",
|
||||
"alternativeHeadline": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
|
||||
"dateModified": "2021-01-06 17:07:07.791000",
|
||||
"datePublished": "2020-08-18 04:00:00",
|
||||
"keywords": [
|
||||
"recipes",
|
||||
"healthyish",
|
||||
"salad",
|
||||
"ginger",
|
||||
"garlic",
|
||||
"orange",
|
||||
"oil",
|
||||
"soy sauce",
|
||||
"lemon juice",
|
||||
"sesame oil",
|
||||
"chicken recipes",
|
||||
"kosher salt",
|
||||
"broccoli",
|
||||
"brown rice",
|
||||
"egg",
|
||||
"celery",
|
||||
"cilantro",
|
||||
"mint",
|
||||
"feel good food plan 2021",
|
||||
"feel good food plan",
|
||||
"black pepper",
|
||||
"butter",
|
||||
"leek",
|
||||
"lemon zest",
|
||||
"rice",
|
||||
"chicken broth",
|
||||
"anchovy",
|
||||
"garlic",
|
||||
"capers",
|
||||
"herb",
|
||||
"olive oil",
|
||||
"healthyish",
|
||||
"web"
|
||||
],
|
||||
"thumbnailUrl": "https://assets.bonappetit.com/photos/5fdbe70a84d333dd1dcc7900/1:1/w_1698,h_1698,c_limit/BA1220feelgoodalt.jpg",
|
||||
"thumbnailUrl": "https://assets.bonappetit.com/photos/5f29796456f43685a49327fb/1:1/w_1125,h_1125,c_limit/Chicken-and-Rice-With-Leeks-Salsa-Verde-01.jpg",
|
||||
"publisher": {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
|
@ -51,75 +47,52 @@
|
|||
"author": [
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "Devonn Francis",
|
||||
"sameAs": "https://bon-appetit.com/contributor/devonn-francis/"
|
||||
"name": "Deb Perelman",
|
||||
"sameAs": "https://bon-appetit.com/contributor/deb-perelman/"
|
||||
}
|
||||
],
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": 4,
|
||||
"ratingCount": 2
|
||||
"ratingCount": 47
|
||||
},
|
||||
"description": "To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it. Don\u2019t touch until you hear it crackle! ",
|
||||
"image": "crispy-rice-with-ginger-citrus-celery-salad.jpg",
|
||||
"name": "Crispy Rice With Ginger-Citrus Celery Salad",
|
||||
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
|
||||
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
|
||||
"headline": "Chicken and Rice With Leeks and Salsa Verde",
|
||||
"name": "Chicken and Rice With Leeks and Salsa Verde",
|
||||
"recipeIngredient": [
|
||||
"1 2\" piece ginger, peeled, finely grated",
|
||||
"1 small garlic clove, finely grated",
|
||||
"Juice of 1 orange",
|
||||
"2 tbsp. vegetable oil",
|
||||
"1Tbsp. coconut aminos or low-sodium soy sauce",
|
||||
"1 Tbsp. fresh lemon juice",
|
||||
"\u00bc tsp. toasted sesame oil",
|
||||
"Kosher salt",
|
||||
"1 medium head of broccoli",
|
||||
"6 Tbsp. (or more) vegetable oil, divided",
|
||||
"Kosher salt",
|
||||
"2 cups chilled cooked brown rice",
|
||||
"4 large eggs",
|
||||
"3 celery stalks, thinly sliced on a steep diagonal",
|
||||
"\u00bd cup cilantro leaves with tender stems",
|
||||
"\u00bd cup mint leaves",
|
||||
"Crushed red pepper flakes (for serving)"
|
||||
"1\u00bd lb. skinless, boneless chicken thighs (4\u20138 depending on size)",
|
||||
"Kosher salt, freshly ground pepper",
|
||||
"3 Tbsp. unsalted butter, divided",
|
||||
"2 large or 3 medium leeks, white and pale green parts only, halved lengthwise, thinly sliced",
|
||||
"Zest and juice of 1 lemon, divided",
|
||||
"1\u00bd cups long-grain white rice, rinsed until water runs clear",
|
||||
"2\u00be cups low-sodium chicken broth",
|
||||
"1 oil-packed anchovy fillet",
|
||||
"2 garlic cloves",
|
||||
"1 Tbsp. drained capers",
|
||||
"Crushed red pepper flakes",
|
||||
"1 cup tender herb leaves (such as parsley, cilantro, and/or mint)",
|
||||
"4\u20135 Tbsp. extra-virgin olive oil"
|
||||
],
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Whisk ginger, garlic, orange juice, vegetable oil, coconut aminos, lemon juice, and sesame oil in a small bowl; season with salt and set aside."
|
||||
"text": "Season chicken with salt and pepper. Melt 2 Tbsp. butter in a large high-sided skillet over medium-high heat. Add leeks and half of lemon zest, season with salt and pepper, and mix to coat leeks in butter. Reduce heat to medium-low, cover, and cook, stirring occasionally, until leeks are somewhat tender, about 5 minutes. Remove lid, increase heat to medium-high, and cook, stirring occasionally, until tender and just starting to take on color, about 3 minutes. Add rice and cook, stirring often, 3 minutes, then add broth, scraping up any browned bits. Tuck short sides of each chicken thigh underneath so they are touching and nestle seam side down into rice mixture. Bring to a simmer. Cover, reduce heat to medium-low, and cook until rice is tender and chicken is cooked through, about 20 minutes. Remove from heat. Cut remaining 1 Tbsp. butter into small pieces and scatter over mixture. Re-cover and let sit 10 minutes."
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Trim about \u00bd\" from woody end of broccoli stem. Peel tough outer layer from stem. Cut florets from stems and thinly slice stems about \u00bd\" thick. Break florets apart with your hands into 1\"\u20131\u00bd\" pieces."
|
||||
"text": "Meanwhile, pulse anchovy, garlic, capers, a few pinches of red pepper flakes, and remaining lemon zest in a food processor until finely chopped. Add herbs; process until a paste forms. With motor running, gradually stream in oil until loosened to a thick sauce. Add half of lemon juice; season salsa verde with salt."
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Heat 2 Tbsp. oil in a large nonstick skillet over medium. Working in 2 batches if needed, arrange broccoli in a single layer and cook, tossing occasionally, until broccoli is bright green and lightly charred around the edges, about\u00a03 minutes. Transfer to a large plate."
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Pour 2 Tbsp. oil into same pan and heat over medium-high. Once you see the first wisp of smoke, add rice and season lightly with salt. Using a spatula or spoon, press rice evenly into pan like a pancake. Rice will begin to crackle, but don\u2019t fuss with it. When the crackling has died down almost completely, about\u00a03 minutes, break rice into large pieces and turn over."
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Add broccoli back to pan and give everything a toss to combine. Cook, tossing occasionally and adding another\u00a01 Tbsp. oil if pan looks dry, until broccoli is tender and rice is warmed through and very crisp, about 5 minutes. Transfer mixture to a platter or divide among plates and set aside."
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Wipe out skillet; heat remaining\u00a02 Tbsp. oil over medium-high. Crack eggs into skillet; season with salt. Oil should bubble around eggs right away. Cook, rotating skillet occasionally, until whites are golden brown and crisp at the edges and set around the yolk (which should be runny), about 2 minutes."
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Toss celery, cilantro, and mint with\u00a03 Tbsp. reserved dressing and a pinch of salt in a medium bowl to combine."
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"text": "Scatter celery salad over fried rice; top with fried eggs and sprinkle with red pepper flakes. Serve extra dressing alongside."
|
||||
"text": "Drizzle remaining lemon juice over chicken and rice. Serve with salsa verde."
|
||||
}
|
||||
],
|
||||
"recipeYield": "4 servings",
|
||||
"url": "https://www.bonappetit.com/recipe/crispy-rice-with-ginger-citrus-celery-salad",
|
||||
"slug": "crispy-rice-with-ginger-citrus-celery-salad",
|
||||
"orgURL": "https://www.bonappetit.com/recipe/crispy-rice-with-ginger-citrus-celery-salad",
|
||||
"recipeYield": "4 Servings",
|
||||
"url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"categories": [],
|
||||
"tags": [],
|
||||
"dateAdded": null,
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1,002 KiB |
Binary file not shown.
Before Width: | Height: | Size: 622 KiB |
|
@ -1,5 +1,4 @@
|
|||
# from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
@ -7,3 +6,24 @@ from pydantic import BaseModel
|
|||
class BackupJob(BaseModel):
|
||||
tag: Optional[str]
|
||||
template: Optional[str]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"tag": "July 23rd 2021",
|
||||
"template": "recipes.md",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Imports(BaseModel):
|
||||
imports: List[str]
|
||||
templates: List[str]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"imports": ["sample_data.zip", "sampe_data2.zip"],
|
||||
"templates": ["recipes.md", "custom_template.md"],
|
||||
}
|
||||
}
|
||||
|
|
12
mealie/models/migration_models.py
Normal file
12
mealie/models/migration_models.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class ChowdownURL(BaseModel):
|
||||
url: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"url": "https://chowdownrepo.com/repo",
|
||||
}
|
||||
}
|
59
mealie/models/recipe_models.py
Normal file
59
mealie/models/recipe_models.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import pydantic
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class RecipeResponse(BaseModel):
|
||||
List
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": [
|
||||
{
|
||||
"slug": "crockpot-buffalo-chicken",
|
||||
"image": "crockpot-buffalo-chicken.jpg",
|
||||
"name": "Crockpot Buffalo Chicken",
|
||||
},
|
||||
{
|
||||
"slug": "downtown-marinade",
|
||||
"image": "downtown-marinade.jpg",
|
||||
"name": "Downtown Marinade",
|
||||
},
|
||||
{
|
||||
"slug": "detroit-style-pepperoni-pizza",
|
||||
"image": "detroit-style-pepperoni-pizza.jpg",
|
||||
"name": "Detroit-Style Pepperoni Pizza",
|
||||
},
|
||||
{
|
||||
"slug": "crispy-carrots",
|
||||
"image": "crispy-carrots.jpg",
|
||||
"name": "Crispy Carrots",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class AllRecipeRequest(BaseModel):
|
||||
properties: List[str]
|
||||
limit: Optional[int]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"properties": ["name", "slug", "image"],
|
||||
"limit": 100,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RecipeURLIn(BaseModel):
|
||||
url: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
|
||||
|
||||
|
||||
class SlugResponse(BaseModel):
|
||||
class Config:
|
||||
schema_extra = {"example": "adult-mac-and-cheese"}
|
|
@ -1,14 +1,20 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from models.backup_models import BackupJob
|
||||
from services.backup_services import (BACKUP_DIR, TEMPLATE_DIR, export_db,
|
||||
import_from_archive)
|
||||
from models.backup_models import BackupJob, Imports
|
||||
from pydantic.main import BaseModel
|
||||
from services.backup_services import (
|
||||
BACKUP_DIR,
|
||||
TEMPLATE_DIR,
|
||||
export_db,
|
||||
import_from_archive,
|
||||
)
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/backups/available/", tags=["Import / Export"])
|
||||
@router.get("/api/backups/available/", tags=["Import / Export"], response_model=Imports)
|
||||
async def available_imports():
|
||||
"""Returns a list of avaiable .zip files for import into Mealie."""
|
||||
imports = []
|
||||
templates = []
|
||||
for archive in BACKUP_DIR.glob("*.zip"):
|
||||
|
@ -17,12 +23,12 @@ async def available_imports():
|
|||
for template in TEMPLATE_DIR.glob("*.md"):
|
||||
templates.append(template.name)
|
||||
|
||||
return {"imports": imports, "templates": templates}
|
||||
return Imports(imports=imports, templates=templates)
|
||||
|
||||
|
||||
@router.post("/api/backups/export/database/", tags=["Import / Export"], status_code=201)
|
||||
async def export_database(data: BackupJob):
|
||||
|
||||
"""Generates a backup of the recipe database in json format."""
|
||||
try:
|
||||
export_path = export_db(data.tag, data.template)
|
||||
except:
|
||||
|
@ -38,6 +44,7 @@ async def export_database(data: BackupJob):
|
|||
"/api/backups/{file_name}/import/", tags=["Import / Export"], status_code=200
|
||||
)
|
||||
async def import_database(file_name: str):
|
||||
""" Import a database backup file generated from Mealie. """
|
||||
imported = import_from_archive(file_name)
|
||||
return imported
|
||||
|
||||
|
@ -48,6 +55,7 @@ async def import_database(file_name: str):
|
|||
status_code=200,
|
||||
)
|
||||
async def delete_backup(backup_name: str):
|
||||
""" Removes a database backup from the file system """
|
||||
|
||||
try:
|
||||
BACKUP_DIR.joinpath(backup_name).unlink()
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
from pprint import pprint
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from models.recipe_models import SlugResponse
|
||||
from services.meal_services import MealPlan
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/meal-plan/all/", tags=["Meal Plan"])
|
||||
@router.get("/api/meal-plan/all/", tags=["Meal Plan"], response_model=List[MealPlan])
|
||||
async def get_all_meals():
|
||||
""" Returns a list of all available meal plans """
|
||||
""" Returns a list of all available Meal Plan """
|
||||
|
||||
return MealPlan.get_all()
|
||||
|
||||
|
||||
@router.post("/api/meal-plan/create/", tags=["Meal Plan"])
|
||||
async def set_meal_plan(data: MealPlan):
|
||||
""" Creates Mealplan from Frontend Data"""
|
||||
""" Creates a meal plan database entry """
|
||||
data.process_meals()
|
||||
data.save_to_db()
|
||||
|
||||
# try:
|
||||
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=404,
|
||||
# detail=SnackResponse.error("Unable to Create Mealplan See Log"),
|
||||
|
@ -33,7 +31,7 @@ async def set_meal_plan(data: MealPlan):
|
|||
|
||||
@router.post("/api/meal-plan/{plan_id}/update/", tags=["Meal Plan"])
|
||||
async def update_meal_plan(plan_id: str, meal_plan: MealPlan):
|
||||
""" Updates a Meal Plan Based off ID """
|
||||
""" Updates a meal plan based off ID """
|
||||
|
||||
try:
|
||||
meal_plan.process_meals()
|
||||
|
@ -49,21 +47,27 @@ async def update_meal_plan(plan_id: str, meal_plan: MealPlan):
|
|||
|
||||
@router.delete("/api/meal-plan/{plan_id}/delete/", tags=["Meal Plan"])
|
||||
async def delete_meal_plan(plan_id):
|
||||
""" Doc Str """
|
||||
""" Removes a meal plan from the database """
|
||||
|
||||
MealPlan.delete(plan_id)
|
||||
|
||||
return SnackResponse.success("Mealplan Deleted")
|
||||
|
||||
|
||||
@router.get("/api/meal-plan/today/", tags=["Meal Plan"])
|
||||
@router.get(
|
||||
"/api/meal-plan/today/",
|
||||
tags=["Meal Plan"],
|
||||
)
|
||||
async def get_today():
|
||||
""" Returns the meal plan data for today """
|
||||
"""
|
||||
Returns the recipe slug for the meal scheduled for today.
|
||||
If no meal is scheduled nothing is returned
|
||||
"""
|
||||
|
||||
return MealPlan.today()
|
||||
|
||||
|
||||
@router.get("/api/meal-plan/this-week/", tags=["Meal Plan"])
|
||||
@router.get("/api/meal-plan/this-week/", tags=["Meal Plan"], response_model=MealPlan)
|
||||
async def get_this_week():
|
||||
""" Returns the meal plan data for this week """
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from models.backup_models import BackupJob
|
||||
from models.migration_models import ChowdownURL
|
||||
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
|
@ -7,10 +8,10 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.post("/api/migration/chowdown/repo/", tags=["Migration"])
|
||||
async def import_chowdown_recipes(repo: dict):
|
||||
async def import_chowdown_recipes(repo: ChowdownURL):
|
||||
""" Import Chowsdown Recipes from Repo URL """
|
||||
try:
|
||||
report = chowdow_migrate(repo.get("url"))
|
||||
report = chowdow_migrate(repo.url)
|
||||
return SnackResponse.success(
|
||||
"Recipes Imported from Git Repo, see report for failures.",
|
||||
additional_data=report,
|
||||
|
|
|
@ -2,6 +2,7 @@ from typing import List, Optional
|
|||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from models.recipe_models import AllRecipeRequest, RecipeURLIn, SlugResponse
|
||||
from services.image_services import read_image, write_image
|
||||
from services.recipe_services import Recipe, read_requested_values
|
||||
from services.scrape_services import create_from_url
|
||||
|
@ -10,17 +11,42 @@ from utils.snackbar import SnackResponse
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/all-recipes/", tags=["Recipes"])
|
||||
@router.get("/api/all-recipes/", tags=["Recipes"], response_model=List[dict])
|
||||
async def get_all_recipes(
|
||||
keys: Optional[List[str]] = Query(...), num: Optional[int] = 100
|
||||
) -> Optional[List[str]]:
|
||||
""" Returns key data for all recipes """
|
||||
):
|
||||
"""
|
||||
Returns key data for all recipes based off the query paramters provided.
|
||||
For example, if slug, image, and name are provided you will recieve a list of
|
||||
recipes containing the slug, image, and name property. By default, responses
|
||||
are limited to 100.
|
||||
|
||||
**Note:** You may experience problems with with query parameters. As an alternative
|
||||
you may also use the post method and provide a body.
|
||||
See the *Post* method for more details.
|
||||
"""
|
||||
|
||||
all_recipes = read_requested_values(keys, num)
|
||||
return all_recipes
|
||||
|
||||
|
||||
@router.get("/api/recipe/{recipe_slug}/", tags=["Recipes"])
|
||||
@router.post("/api/all-recipes/", tags=["Recipes"], response_model=List[dict])
|
||||
async def get_all_recipes_post(body: AllRecipeRequest):
|
||||
"""
|
||||
Returns key data for all recipes based off the body data provided.
|
||||
For example, if slug, image, and name are provided you will recieve a list of
|
||||
recipes containing the slug, image, and name property.
|
||||
|
||||
Refer to the body example for data formats.
|
||||
|
||||
"""
|
||||
|
||||
all_recipes = read_requested_values(body.properties, body.limit)
|
||||
|
||||
return all_recipes
|
||||
|
||||
|
||||
@router.get("/api/recipe/{recipe_slug}/", tags=["Recipes"], response_model=Recipe)
|
||||
async def get_recipe(recipe_slug: str):
|
||||
""" Takes in a recipe slug, returns all data for a recipe """
|
||||
recipe = Recipe.get_by_slug(recipe_slug)
|
||||
|
@ -37,24 +63,21 @@ async def get_recipe_img(recipe_slug: str):
|
|||
|
||||
|
||||
# Recipe Creations
|
||||
@router.post("/api/recipe/create-url/", tags=["Recipes"], status_code=201)
|
||||
async def get_recipe_url(url: dict):
|
||||
""" Takes in a URL and Attempts to scrape data and load it into the database """
|
||||
@router.post(
|
||||
"/api/recipe/create-url/",
|
||||
tags=["Recipes"],
|
||||
status_code=201,
|
||||
response_model=str,
|
||||
)
|
||||
async def parse_recipe_url(url: RecipeURLIn):
|
||||
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||
|
||||
url = url.get("url")
|
||||
slug = create_from_url(url)
|
||||
|
||||
# try:
|
||||
# slug = create_from_url(url)
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Parse URL")
|
||||
# )
|
||||
slug = create_from_url(url.url)
|
||||
|
||||
return slug
|
||||
|
||||
|
||||
@router.post("/api/recipe/create/", tags=["Recipes"])
|
||||
@router.post("/api/recipe/create/", tags=["Recipes"], response_model=SlugResponse)
|
||||
async def create_from_json(data: Recipe) -> str:
|
||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||
created_recipe = data.save_to_db()
|
||||
|
@ -63,7 +86,7 @@ async def create_from_json(data: Recipe) -> str:
|
|||
|
||||
|
||||
@router.post("/api/recipe/{recipe_slug}/update/image/", tags=["Recipes"])
|
||||
def update_image(
|
||||
def update_recipe_image(
|
||||
recipe_slug: str, image: bytes = File(...), extension: str = Form(...)
|
||||
):
|
||||
""" Removes an existing image and replaces it with the incoming file. """
|
||||
|
@ -73,7 +96,7 @@ def update_image(
|
|||
|
||||
|
||||
@router.post("/api/recipe/{recipe_slug}/update/", tags=["Recipes"])
|
||||
async def update(recipe_slug: str, data: Recipe):
|
||||
async def update_recipe(recipe_slug: str, data: Recipe):
|
||||
""" Updates a recipe by existing slug and data. Data should containt """
|
||||
|
||||
data.update(recipe_slug)
|
||||
|
@ -82,7 +105,7 @@ async def update(recipe_slug: str, data: Recipe):
|
|||
|
||||
|
||||
@router.delete("/api/recipe/{recipe_slug}/delete/", tags=["Recipes"])
|
||||
async def delete(recipe_slug: str):
|
||||
async def delete_recipe(recipe_slug: str):
|
||||
""" Deletes a recipe by slug """
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import List
|
||||
|
||||
from db.mongo_setup import global_init
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from services.scheduler_services import Scheduler, post_webhooks
|
||||
|
@ -13,14 +15,14 @@ scheduler.startup_scheduler()
|
|||
|
||||
@router.get("/api/site-settings/", tags=["Settings"])
|
||||
async def get_main_settings():
|
||||
""" Returns basic site Settings """
|
||||
""" Returns basic site settings """
|
||||
|
||||
return SiteSettings.get_site_settings()
|
||||
|
||||
|
||||
@router.post("/api/site-settings/webhooks/test/", tags=["Settings"])
|
||||
async def test_webhooks():
|
||||
""" Test Webhooks """
|
||||
""" Run the function to test your webhooks """
|
||||
|
||||
return post_webhooks()
|
||||
|
||||
|
@ -40,22 +42,26 @@ async def update_settings(data: SiteSettings):
|
|||
return SnackResponse.success("Settings Updated")
|
||||
|
||||
|
||||
@router.get("/api/site-settings/themes/", tags=["Themes"])
|
||||
@router.get(
|
||||
"/api/site-settings/themes/", tags=["Themes"]
|
||||
)
|
||||
async def get_all_themes():
|
||||
""" Returns all site themes """
|
||||
|
||||
return SiteTheme.get_all()
|
||||
|
||||
|
||||
@router.get("/api/site-settings/themes/{theme_name}/", tags=["Themes"])
|
||||
@router.get(
|
||||
"/api/site-settings/themes/{theme_name}/", tags=["Themes"]
|
||||
)
|
||||
async def get_single_theme(theme_name: str):
|
||||
""" Returns basic site Settings """
|
||||
""" Returns a named theme """
|
||||
return SiteTheme.get_by_name(theme_name)
|
||||
|
||||
|
||||
@router.post("/api/site-settings/themes/create/", tags=["Themes"])
|
||||
async def create_theme(data: SiteTheme):
|
||||
""" Creates a Site Color Theme """
|
||||
""" Creates a site color theme database entry """
|
||||
|
||||
try:
|
||||
data.save_to_db()
|
||||
|
@ -69,7 +75,7 @@ async def create_theme(data: SiteTheme):
|
|||
|
||||
@router.post("/api/site-settings/themes/{theme_name}/update/", tags=["Themes"])
|
||||
async def update_theme(theme_name: str, data: SiteTheme):
|
||||
""" Returns basic site Settings """
|
||||
""" Update a theme database entry """
|
||||
try:
|
||||
data.update_document()
|
||||
except:
|
||||
|
@ -82,7 +88,7 @@ async def update_theme(theme_name: str, data: SiteTheme):
|
|||
|
||||
@router.delete("/api/site-settings/themes/{theme_name}/delete/", tags=["Themes"])
|
||||
async def delete_theme(theme_name: str):
|
||||
""" Returns basic site Settings """
|
||||
""" Deletes theme from the database """
|
||||
try:
|
||||
SiteTheme.delete_theme(theme_name)
|
||||
except:
|
||||
|
|
|
@ -24,6 +24,18 @@ class SiteSettings(BaseModel):
|
|||
name: str = "main"
|
||||
webhooks: Webhooks
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "main",
|
||||
"webhooks": {
|
||||
"webhookTime": "00:00",
|
||||
"webhookURLs": ["https://mywebhookurl.com/webhook"],
|
||||
"enable": False,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _unpack_doc(document: SiteSettingsDocument):
|
||||
document = json.loads(document.to_json())
|
||||
|
@ -65,6 +77,22 @@ class SiteTheme(BaseModel):
|
|||
name: str
|
||||
colors: Colors
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "default",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_by_name(theme_name):
|
||||
document = SiteThemeDocument.objects.get(name=theme_name)
|
||||
|
|
|
@ -8,7 +8,16 @@ ENV = CWD.joinpath(".env")
|
|||
dotenv.load_dotenv(ENV)
|
||||
|
||||
# General
|
||||
PRODUCTION = os.environ.get("ENV")
|
||||
PORT = int(os.getenv("mealie_port", 9000))
|
||||
API = os.getenv("api_docs", True)
|
||||
|
||||
if API:
|
||||
docs_url = "/docs"
|
||||
redoc_url = "/redoc"
|
||||
else:
|
||||
docs_url = None
|
||||
redoc_url = None
|
||||
|
||||
# Mongo Database
|
||||
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from services.settings_services import Colors, SiteTheme
|
||||
|
@ -37,5 +38,43 @@ def generate_default_theme():
|
|||
default_theme.save_to_db()
|
||||
|
||||
|
||||
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
||||
|
||||
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<title>My Project - ReDoc</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<style data-styled="" data-styled-version="4.4.1"></style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="redoc-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
|
||||
<script>
|
||||
var spec = %s;
|
||||
Redoc.init(spec, {}, document.getElementById("redoc-container"));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
out_path = CWD.joinpath("temp", "index.html")
|
||||
|
||||
|
||||
def generate_api_docs(app):
|
||||
with open(out_path, "w") as fd:
|
||||
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
import datetime
|
|
@ -1 +0,0 @@
|
|||
// Test Notify
|
Loading…
Reference in a new issue