Merge pull request #49 from hay-kot/api-docs

Improved API Documentation, New ENV Varibles and Minor refactoring/renaming
This commit is contained in:
Hayden 2021-01-07 21:48:39 -09:00 committed by GitHub
commit a731b9f6ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 754 additions and 276 deletions

20
.github/workflows/build-docs.yml vendored Normal file
View 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
View file

@ -5,6 +5,9 @@ __pycache__/
*$py.class
# frontend/.env.development
docs/site/
mealie/temp/*
mealie/temp/api.html
mealie/data/backups/*
mealie/data/debug/*

View file

@ -20,12 +20,15 @@
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="/api/docs">API</a>
·
<a href="https://github.com/hay-kot/mealie/issues">
Request Feature
</a>
@ -33,7 +36,6 @@
<a href="https://hub.docker.com/repository/docker/hkotel/mealies"> Docker Hub
</a>
</p>
</p>

View file

@ -0,0 +1,5 @@
# API Examples
TODO
Have Ideas? Submit a PR!

View file

@ -1,3 +0,0 @@
# API Introduction
TODO

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

View file

@ -10,6 +10,8 @@
·
<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
</a>

View file

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

@ -0,0 +1,5 @@
{
"cSpell.enableFiletypes": [
"!javascript"
]
}

View file

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

View file

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

View file

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

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

View file

@ -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,
};

View file

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

View file

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

View file

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

View file

@ -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"],
}
}

View 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",
}
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
import datetime

View file

@ -1 +0,0 @@
// Test Notify