Merge pull request #59 from hay-kot/dev

v0.0.2 - Pre-release Second Patch
This commit is contained in:
Hayden 2021-01-09 13:30:55 -09:00 committed by GitHub
commit 2162216ba7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 3238 additions and 949 deletions

View file

@ -1,2 +1,3 @@
*/node_modules
*/dist
##

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

15
.gitignore vendored
View file

@ -3,14 +3,19 @@
__pycache__/
*.py[cod]
*$py.class
frontend/.env.development
# frontend/.env.development
docs/site/
mealie/temp/*
mealie/temp/api.html
mealie/data/backups/*
mealie/data/debug/*
mealie/data/img/*
!mealie/dist/*
#Exception to keep folders
!mealie/dist/.gitkeep
!mealie/data/backups/.gitkeep
!mealie/data/backups/dev_sample_data*
!mealie/data/debug/.gitkeep
@ -18,12 +23,12 @@ mealie/data/img/*
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
.env.development
# Log files
npm-debug.log*
@ -48,7 +53,7 @@ pnpm-debug.log*
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
@ -143,5 +148,3 @@ ENV/
# Node Modules
node_modules/
/*.env.development*

View file

@ -8,9 +8,12 @@
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": false,
"python.testing.promptToConfigure": false,
"python.discoverTest": true,
"python.testing.pytestEnabled": true,
"cSpell.enableFiletypes": [
"!python"
],
"python.testing.pytestArgs": [
"mealie"
]
}

View file

@ -1,4 +1,4 @@
FROM node:alpine as build-stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY ./frontend/package*.json ./
RUN npm install
@ -18,7 +18,12 @@ WORKDIR /app
RUN pip install -r requirements.txt
COPY ./mealie /app
COPY ./mealie/data/templates/recipes.md /app/data/templates/
COPY ./mealie/data/templates/recipes.md /app/data/templates/recipes.md
COPY --from=build-stage /app/dist /app/dist
RUN rm -rf /app/test /app/temp
ENV ENV prod
VOLUME [ "/app/data" ]
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "9000"]

View file

@ -3,18 +3,17 @@ FROM python:3
RUN apt-get update -y && \
apt-get install -y python-pip python-dev
# We copy just the requirements.txt first to leverage Docker cache
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
RUN pip install pytest
# COPY ./mealie /app
COPY ./mealie /app
ENTRYPOINT [ "python" ]
# TODO Reconfigure Command to start a Gunicorn Server that managed the Uvicorn Server. Also Learn how to do that :-/
CMD [ "app.py" ]

View file

@ -3,7 +3,6 @@
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![MIT License][license-shield]][license-url]
[![LinkedIn][linkedin-shield]][linkedin-url]
[![Docker Pulls][docker-pull]][docker-pull]
<!-- PROJECT LOGO -->
@ -21,14 +20,21 @@
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">Request Feature</a>
</p>
<a href="https://hay-kot.github.io/mealie/api/docs/">API</a>
·
<a href="https://github.com/hay-kot/mealie/issues">
Request Feature
</a>
·
<a href="https://hub.docker.com/repository/docker/hkotel/mealies"> Docker Hub
</a>
</p>
@ -39,7 +45,7 @@
[![Product Name Screen Shot][product-screenshot]](https://example.com)
**Mealie** is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and mealie will automatically import the relavent data or add a family recipe with the UI editor.
**Mealie** is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and mealie will automatically import the relevant data or add a family recipe with the UI editor.
Mealie also provides a secure API for interactions from 3rd party applications. **Why does my recipe manager need an API?** An API allows integration into applications like [Home Assistant]() that can act as notification engines to provide custom notifications based of Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. See the section on [Meal Plan hooks](#hooks) for more information. Additionally, you can access any available API from the backend server. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
@ -56,7 +62,7 @@ Mealie also provides a secure API for interactions from 3rd party applications.
- Add notes to recipes
#### Meal Planner
- Random Meal plan generation based off categories
- Expose notes in the API to allow external applications to access relavent information for meal plans
- Expose notes in the API to allow external applications to access relevant information for meal plans
#### Database Import / Export
- Easily Import / Export your recipes from the UI
- Export recipes in into custom files using Jinja2 templates
@ -73,7 +79,9 @@ Mealie also provides a secure API for interactions from 3rd party applications.
<!-- CONTRIBUTING -->
## Contributing
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Especially test. Literally any tests. See the [Contributors Guide](https://hay-kot.github.io/mealie/contributors/developers-guide/code-contributions/) for help getting started.
If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
<a href="https://www.buymeacoffee.com/haykot" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

View file

@ -17,13 +17,13 @@ Don't forget to [join the Discord](https://discord.gg/R6QDyJgbD2)!
# Todo's
Frontend
- [ ] .Vue file reorganized into something that makes sense
- [x] .Vue file reorganized into something that makes sense
- [ ] Recipe Print Page
- [x] Catch 400 / bad response on create from URL
- [ ] Recipe Editor Data Validation Client Side
- [x] Favicon
- [x] Rename Window
- [ ] Add version indicator and notification for new version available
- [x] Add version indicator and notification for new version available
- [ ] Enhanced Search Functionality
- [ ] Organize Home Page my Category, ideally user selectable.
@ -41,15 +41,21 @@ Backend
# Draft Changelog
## v0.0.2
General
Bug Fixes
- Fixed opacity issues with marked steps - [mtoohey31](https://github.com/mtoohey31)
- Updated Favicon
- Renamed Frontend Window
- Added Debug folder to dump scraper data prior to processing.
- Improved documentation
- Added version tag / relevant links, and new version notifier
- Fixed hot-reloading development environment - [grssmnn](https://github.com/grssmnn)
- Fixed recipe not saving without image
- Fixed parsing error on image property null
Recipes
- Added user feedback on bad URL.
- Better backend data validation for updating recipes, avoid small syntax errors corrupting database entry. [Issue #8](https://github.com/hay-kot/mealie/issues/8)
- Fixed spacing issue while editing new recipes in JSON
General Improvements
- Added Confirmation component to deleting recipes - [zackbcom](https://github.com/zackbcom)
- Updated Theme backend - [zackbcom](https://github.com/zackbcom)
- Added Persistent storage to vuex - [zackbcom](https://github.com/zackbcom)
- General Color/Theme Improvements
- More consistent UI
- More minimalist coloring
- Added API Key Extras to Recipe Data
- Users can now add custom json key/value pairs to all recipes via the editor for access in 3rd part applications. For example users can add a "message" field in the extras that can be accessed on API calls to play a message over google home.
- Improved image rendering (nearly x2 speed)
- Improved documentation + API Documentation
- Improved recipe parsing

View file

@ -1 +1 @@
docker-compose -f docker-compose.dev.yml build && docker-compose -f docker-compose.dev.yml -p dev-mealie up -d
docker-compose -f docker-compose.dev.yml -p dev-mealie up --build

View file

@ -1 +1 @@
docker-compose build && docker-compose -p mealie up -d
docker-compose -p mealie up --build

View file

@ -0,0 +1,18 @@
"""
Helper script to download raw recipe data from a URL and dump it to disk.
The resulting files can be used as test input data.
"""
import sys, json
from scrape_schema_recipe import scrape_url
for url in sys.argv[1:]:
try:
data = scrape_url(url)[0]
slug = list(filter(None, url.split("/")))[-1]
filename = f"{slug}.json"
with open(filename, "w") as f:
json.dump(data, f, indent=4, default=str)
print(f"Saved {filename}")
except Exception as e:
print(f"Error for {url}: {e}")

View file

@ -2,23 +2,26 @@
version: "3.1"
services:
# Vue Frontend
mealie:
mealie-frontend:
image: mealie-frontend:dev
build:
context: ./frontend
dockerfile: frontend.Dockerfile
container_name: mealie_frontend
restart: always
ports:
- 9920:8080
environment:
VUE_APP_API_BASE_URL: "http://mealie-api:9000"
volumes:
- ./frontend:/app
- ./frontend/:/app
- /app/node_modules
# Fast API
mealie-api:
image: mealie-api:dev
build:
context: ./
dockerfile: Dockerfile.dev
container_name: mealie-api
restart: always
ports:
- 9921:9000

View file

@ -16,8 +16,7 @@ services:
db_host: mongo
db_port: 27017
volumes:
- ./mealie/data/img:/app/data/img
- ./mealie/data/backups:/app/data/backups
- ./mealie/data/:/app/data
mongo:
image: mongo
restart: always

View file

@ -1,34 +0,0 @@
# Release Notes
## v0.0.1 - Pre-release Patch
General
- Updated Favicon
- Renamed Frontend Window
- Added Debug folder to dump scraper data prior to processing.
Recipes
- Added user feedback on bad URL
- Better backend data validation for updating recipes, avoid small syntax errors corrupting database entry. [Issue #8](https://github.com/hay-kot/mealie/issues/8)
- Fixed spacing issue while editing new recipes in JSON
## v0.0.0 - Initial Pre-release
The initial pre-release. It should be semi-functional but does not include a lot of user feedback You may notice errors that have no user feedback and have no idea what went wrong.
### Recipes
- Automatic web scrapping for common recipe platforms
- Interactive API Documentation thanks to [FastAPI](https://fastapi.tiangolo.com/) and [Swagger](https://petstore.swagger.io/)
- UI Recipe Editor
- JSON Recipe Editor in browser
- Custom tags and categories
- Rate recipes
- Add notes to recipes
- Migration From Other Platforms
- Chowdown
### Meal Planner
- Random Meal plan generation based off categories
- Expose notes in the API to allow external applications to access relevant information for meal plans
### Database Import / Export
- Easily Import / Export your recipes from the UI
- Export recipes in markdown format for universal access
- Use the default or a custom jinja2 template

View file

@ -0,0 +1,14 @@
# Usage
## Key Components
### Recipe Extras
Recipes extras are a key feature of the Mealie API. They allow you to create custom json key/value pairs within a recipe to reference from 3rd part applications. You can use these keys to contain information to trigger automation or custom messages to relay to your desired device.
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
![api-extras-gif](/gifs/api-extras.gif)
## Examples
Have Ideas? Submit a PR!

File diff suppressed because one or more lines are too long

57
docs/docs/changelog.md Normal file
View file

@ -0,0 +1,57 @@
# Release Notes
## v0.0.2 - Pre-release Second Patch
A quality update with major props to [zackbcom](https://github.com/zackbcom) for working hard on making the theming just that much better!
### Bug Fixes
- Fixed empty backup failure without markdown template
- Fixed opacity issues with marked steps - [mtoohey31](https://github.com/mtoohey31)
- Fixed hot-reloading development environment - [grssmnn](https://github.com/grssmnn)
- Fixed recipe not saving without image
- Fixed parsing error on image property null
### General Improvements
- Added Confirmation component to deleting recipes - [zackbcom](https://github.com/zackbcom)
- Updated Theme backend - [zackbcom](https://github.com/zackbcom)
- Added Persistent storage to vuex - [zackbcom](https://github.com/zackbcom)
- General Color/Theme Improvements
- More consistent UI
- More minimalist coloring
- Added API key extras to Recipe Data - [See Documentation](/api/api-usage/)
- Users can now add custom json key/value pairs to all recipes via the editor for access in 3rd part applications. For example users can add a "message" field in the extras that can be accessed on API calls to play a message over google home.
- Improved image rendering (nearly x2 speed)
- Improved documentation + API Documentation
- Improved recipe parsing
- User feedback on backup importing
## v0.0.1 - Pre-release Patch
### General
- Updated Favicon
- Renamed Frontend Window
- Added Debug folder to dump scraper data prior to processing.
### Recipes
- Added user feedback on bad URL
- Better backend data validation for updating recipes, avoid small syntax errors corrupting database entry. [Issue #8](https://github.com/hay-kot/mealie/issues/8)
- Fixed spacing issue while editing new recipes in JSON
## v0.0.0 - Initial Pre-release
The initial pre-release. It should be semi-functional but does not include a lot of user feedback You may notice errors that have no user feedback and have no idea what went wrong.
### Recipes
- Automatic web scrapping for common recipe platforms
- Interactive API Documentation thanks to [FastAPI](https://fastapi.tiangolo.com/) and [Swagger](https://petstore.swagger.io/)
- UI Recipe Editor
- JSON Recipe Editor in browser
- Custom tags and categories
- Rate recipes
- Add notes to recipes
- Migration From Other Platforms
- Chowdown
### Meal Planner
- Random Meal plan generation based off categories
- Expose notes in the API to allow external applications to access relevant information for meal plans
### Database Import / Export
- Easily Import / Export your recipes from the UI
- Export recipes in markdown format for universal access
- Use the default or a custom jinja2 template

View file

@ -1,13 +1,9 @@
# Contributing to Mealie
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Becoming a maintainer
!!! Warning
It should be known going into this that this is my first open source project, and my first public github repo I'm actively managing. If something does not make sense, or is not best practice. PLEASE feel free to reach out and let me know. I'm all about improving workflow and making it easier for contributors.
[Remember to join the Discord and stay in touch with other developers working on the project](https://discord.gg/R6QDyJgbD2)!
[Please Join the Discord](https://discord.gg/R6QDyJgbD2). We are building a community of developers working on the project.
## We Develop with Github
We use github to host code, to track issues and feature requests, as well as accept pull requests.
@ -15,12 +11,12 @@ We use github to host code, to track issues and feature requests, as well as acc
## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests
Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests:
1. Fork the repo and create your branch from `master`.
1. Fork the repo and create your branch from `dev`.
2. Read the page in in [dev/dev-notes.md](https://github.com/hay-kot/mealie/blob/0.1.0/dev/dev-notes.md) to get an idea on where the project is at.
3. If you've changed APIs, update the documentation.
4. Make sure your code lints.
3. If you're interested on working on major changes please get in touch on discord and coordinate with other developers. No sense in doubling up on work if someones already on it.
4. If you've changed APIs, update the documentation.
5. Issue that pull request!
6. If you make changes to the dev/0.1.0 branch reflect those changes in the dev/dev-notes.md to keep track of changes.
6. If you make changes to the dev branch reflect those changes in the dev/dev-notes.md to keep track of changes. Don't forget to add your name/handle/identifier!
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.

View file

@ -0,0 +1,3 @@
# Guidelines
TODO

View file

@ -0,0 +1,31 @@
# Development: Getting Started
After reading through the [Code Contributions Guide](https://hay-kot.github.io/mealie/contributors/developers-guide/code-contributions/) and forking the repo you can start working. This project is developed with :whale: docker and as such you will be greatly aided by using docker for development. It's not necessary but it is helpful.
## With Docker
`cd` into frontend directory and run `npm install` to install the node modules.
There are 2 scripts to help set up the docker containers in dev/scripts/.
`docker-compose.dev.sh` - Will spin up a docker development server
`docker-compose.sh` - Will spin up a docker production server
There are VSCode tasks created in the .vscode folder. You can use these to quickly execute the scripts above using the command palette.
## Without Docker
?? TODO
## Trouble Shooting
!!! Error "Symptom: Vue Development Server Wont Start"
**Error:** `TypeError: Cannot read property 'upgrade' of undefined`
**Solution:** You may be missing the `/frontend/.env.development.` The contents should be `VUE_APP_API_BASE_URL=http://127.0.0.1:9921`. This is a reference to proxy the the API requests from Vue to 127.0.0.1 at port 9921 where FastAPI should be running.
!!! Error "Symptom: FastAPI Development Server Wont Start"
**Error:** `RuntimeError: Directory '/app/dist' does not exist`
**Solution:** Create an empty /mealie/dist directory. This directory is served as static content by FastAPI. It is provided during the build process and may be missing in development.
Run into another issue? [Ask for help on discord](https://discord.gg/R6QDyJgbD2)

View file

@ -0,0 +1,15 @@
# Non-Code Contributions
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Becoming a maintainer
[Remember to join the Discord and stay in touch with other developers working on the project](https://discord.gg/R6QDyJgbD2)!
Additionally, you can buy me a coffee and support the project. When I get financial support it helps me know that there's real interest in the project and that it's worth the time to keep developing.
<a href="https://www.buymeacoffee.com/haykot" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

View file

@ -1,19 +1,5 @@
# Site Settings Panel
!!! danger
As this is still a **BETA** It is recommended that you backup your data often and store in more than one place. Ad-hear to backup best practices with the [3-2-1 Backup Rule](https://en.wikipedia.org/wiki/Backup)
## Theme Settings
Color themes can be created and set from the UI in the settings page. You can select an existing color theme or create a new one. On creation of a new color theme random colors will first be generated, then you can select and save as you'd like. By default the "default" theme will be loaded for all new users visiting the site. All created color themes are available to all users of the site. Separate color themes can be set for both Light and Dark modes.
![](gifs/theme-demo.gif)
!!! note
Theme data is stored in cookies in the browser. Calling "Save Theme" will refresh the cookie with the selected theme as well save the theme to the database.
## Backup and Export
![](img/admin-backup.png)
# Backup and Export
![](../img/admin-backup.png)
All recipe data can be imported and exported as necessary from the UI. Under the admin page you'll find the section for using Backups and Exports.
@ -21,10 +7,10 @@ To create an export simple add the tag and the markdown template and click Backu
To import a backup it must be in your backups folder. If it is in the backup folder it will automatically show up as an source to restore from. Selected the desired backup and import the backup file.
### Custom Templating
## Custom Templating
On export you can select a template to use to render files using the jinja2 syntax. This can be done to export recipes in other formats besides regular .json.Look at this example for rendering a markdown recipe using the jinja2 syntax.
#### Input
### Input
```jinja2
![Recipe Image](../images/{{ recipe.image }})
@ -52,7 +38,7 @@ Categories: {{ recipe.categories }}
Original URL: {{ recipe.orgURL }}
```
#### Output
### Output
```markdown
![Recipe Image](../images/five-spice-popcorn-chicken.jpg)
@ -91,13 +77,3 @@ Original URL: https://www.bonappetit.com/recipe/five-spice-popcorn-chicken#intci
```
If you decide you don't like mealie. This is a good way to export into a format that can be imported into another.
## Meal Planner Webhooks
Meal planner webhooks are post requests sent from Mealie to an external endpoint. The body of the message is the Recipe JSON of the scheduled meal. If no meal is schedule, no request is sent. The webhook functionality can be enabled or disabled as well as scheduled. Note that you must "Save Webhooks" prior to any changes taking affect server side.
## Migration
### Chowdown
In the Admin page on the in the Migration section you can provide a URL for a repo hosting a Chowdown site and Mealie will pull the images and recipes from the instance and automatically import them into the database. Due to the nature of the yaml format you may have mixed results but you should get an error report of the recipes that had errors and will need to be manually added. Note that you can only import the repo as a whole. You cannot import individual recipes.

View file

@ -1,4 +1,4 @@
# Getting Started
# Installation
To deploy docker on your local network it is highly recommended to use docker to deploy the image straight from dockerhub. Using the docker-compose below you should be able to get a stack up and running easily by changing a few default values and deploying. Currently the only supported database is Mongo. Mealie is looking for contributors to support additional databases.
@ -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 |
@ -38,12 +39,13 @@ services:
db_port: 27017 # The Default port for Mongo DB
TZ: America/Anchorage
volumes:
- ./data/img:/app/data/img
- ./data/backups:/app/data/backups
- ./mealie/data/:/app/data/
mongo:
image: mongo
restart: always
volumes:
- ./mongo:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: root # Change!
MONGO_INITDB_ROOT_PASSWORD: example # Change!
@ -56,6 +58,7 @@ services:
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: example
```
## Ansible Tasks Template

View file

@ -8,4 +8,4 @@ To edit the meal in a meal plan simply select the edit button on the card in the
!!! warning
In coming a future release recipes for meals will be restricted to specific categories.
![](gifs/meal-plan-demo.gif)
![](../gifs/meal-plan-demo.gif)

View file

@ -0,0 +1,12 @@
# Migration
### Chowdown
In the Admin page on the in the Migration section you can provide a URL for a repo hosting a [Chowdown](https://github.com/clarklab/chowdown) repository and Mealie will pull the images and recipes from the instance and automatically import them into the database. Due to the nature of the yaml format you may have mixed results but you should get an error report of the recipes that had errors and will need to be manually added. Note that you can only import the repo as a whole. You cannot import individual recipes.
We'd like to support additional migration paths. [See open issues.](https://github.com/hay-kot/mealie/issues)
**Currently Proposed Are:**
- NextCloud Recipes
- Open Eats

View file

@ -4,7 +4,7 @@
Adding a recipe can be as easy as copying the recipe URL into mealie and letting the web scrapper try to pull down the information. Currently this scraper is implemented with [scrape-schema-recipe package](https://pypi.org/project/scrape-schema-recipe/). You may have mixed results on some websites, especially with blogs or non specific recipe websites. See the bulk import Option below for another a convenient way to add blog style recipes into Mealie.
![](gifs/url-demo.gif)
![](../gifs/url-demo.gif)
## Recipe Editor
@ -12,12 +12,12 @@ Recipes can be edited and created via the UI. This is done with both a form base
You can also add a custom recipe with the UI editor built into the web view. After logging in as a user you'll have access to the editor to make changes to all the content in the recipe.
![](gifs/editor-demo.gif)
![](../gifs/editor-demo.gif)
## Bulk Import
Mealie also supports bulk import of recipe instructions and ingredients. Select "Bulk Add" in the editor and paste in your plain text data to be parsed. Each line is treated as one entry and will be appended to the existing ingredients or instructions if they exist. Empty lines will be stripped from the text.
![](gifs/bulk-add-demo.gif)
![](../gifs/bulk-add-demo.gif)
## Schema
Recipes are stored in the json-like format in mongoDB and then sent and edited in json format on the frontend. Each recipes uses [Recipe Schema](https://schema.org/Recipe) as a general guide with some additional properties specific to Mealie.

View file

@ -0,0 +1,21 @@
# Site Settings Panel
!!! danger
As this is still a **BETA** It is recommended that you backup your data often and store in more than one place. Ad-hear to backup best practices with the [3-2-1 Backup Rule](https://en.wikipedia.org/wiki/Backup)
## Theme Settings
Color themes can be created and set from the UI in the settings page. You can select an existing color theme or create a new one. On creation of a new color theme, the default colors will be used, then you can select and save as you'd like. By default the "default" theme will be loaded for all new users visiting the site. All created color themes are available to all users of the site. Theme Colors will be set for both light and dark modes.
![](../gifs/theme-demo.gif)
!!! note
Theme data is stored in localstorage in the browser. Calling "Save colors and apply theme will refresh the localstorage with the selected theme as well save the theme to the database.
## Meal Planner Webhooks
Meal planner webhooks are post requests sent from Mealie to an external endpoint. The body of the message is the Recipe JSON of the scheduled meal. If no meal is schedule, no request is sent. The webhook functionality can be enabled or disabled as well as scheduled. Note that you must "Save Webhooks" prior to any changes taking affect server side.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

26
docs/docs/html/api.html Normal file

File diff suppressed because one or more lines are too long

View file

@ -4,9 +4,20 @@
<a href="https://github.com/hay-kot/mealie">
</a>
<p align="center">
A Place for All Your Recipes
<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">Request Feature</a>
<a href="https://hay-kot.github.io/mealie/api/docs/">API</a>
·
<a href="https://github.com/hay-kot/mealie/issues">
Request Feature
</a>
·
<a href="https://hub.docker.com/repository/docker/hkotel/mealies"> Docker Hub
</a>
</p>
</p>
@ -62,16 +73,16 @@ Mealie also provides an API for interactions from 3rd party applications. **Why
<!-- ROADMAP -->
## Road Map
[See Roadmap](2.0 - roadmap)
[See Roadmap](roadmap.md)
<!-- CONTRIBUTING -->
## Contributing
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Especially test. Literally any tests.
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Especially test. Literally any tests. See the [Contributors Guide](https://hay-kot.github.io/mealie/contributors/developers-guide/code-contributions/) for help getting started.
If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for the project.
If you are not a coder, you can still contribute financially. financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
<a href="https://www.buymeacoffee.com/haykot" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

View file

@ -28,7 +28,6 @@ Feature placement is not set in stone. This is much more of a guideline than any
* [ ] Basic Form Validation
- [ ] Recipe Viewer
* [ ] Print Page View - Like King Arthur Website
* [ ] Notes Hidden/Not Hidden
* [ ] Total Time Indicator
* [ ] Bake Time

View file

@ -1,19 +1,46 @@
site_name: Mealie Docs
theme:
features:
- navigation.expand
favicon: img/favicon.png
name: material
icon:
logo: material/silverware-variant
features:
- navigation.instant
markdown_extensions:
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- def_list
- pymdownx.highlight
- pymdownx.superfences
- pymdownx.tasklist:
custom_checkbox: true
- admonition
extra_css:
- stylesheets/custom.css
repo_url: https://github.com/hay-kot/mealie
repo_name: hay-kot/mealie
nav:
- About The Project: "index.md"
- Getting Started:
- Installation: "getting-started/install.md"
- Working With Recipes: "getting-started/recipes.md"
- Planning Meals: "getting-started/meal-planner.md"
- Site Settings: "getting-started/site-settings.md"
- Backups and Exports: "getting-started/backups-and-exports.md"
- Recipe Migration: "getting-started/migration-imports.md"
- API Reference:
- API Usage: "api/api-usage.md"
- API Documentation: "api/docs/index.html"
- Contributors Guide:
- Non-Code: "contributors/non-coders.md"
- Developers Guide:
- Code Contributions: "contributors/developers-guide/code-contributions.md"
- Dev Getting Started: "contributors/developers-guide/starting-dev-server.md"
- Guidelines: "contributors/developers-guide/general-guidelines.md"
- Development Road Map: "roadmap.md"
- Change Log: "changelog.md"

View file

@ -0,0 +1 @@
VUE_APP_API_BASE_URL=http://10.10.10.12:9921

5
frontend/.vscode/settings.json vendored Normal file
View file

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

View file

@ -13,7 +13,7 @@ COPY package*.json ./
RUN npm install
# copy project files and folders to the current working directory (i.e. 'app' folder)
# COPY . .
COPY . .
# build app for production with minification
# RUN npm run build

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"
}
}
}
},
@ -2481,9 +2560,9 @@
"dev": true
},
"axios": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
@ -3740,9 +3819,9 @@
}
},
"core-js": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.1.tgz",
"integrity": "sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg=="
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz",
"integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A=="
},
"core-js-compat": {
"version": "3.7.0",
@ -9448,9 +9527,9 @@
"dev": true
},
"sass": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.30.0.tgz",
"integrity": "sha512-26EUhOXRLaUY7+mWuRFqGeGGNmhB1vblpTENO1Z7mAzzIZeVxZr9EZoaY1kyGLFWdSOZxRMAufiN2mkbO6dAlw==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.32.0.tgz",
"integrity": "sha512-fhyqEbMIycQA4blrz/C0pYhv2o4x2y6FYYAH0CshBw3DXh5D5wyERgxw0ptdau1orc/GhNrhF7DFN2etyOCEng==",
"dev": true,
"requires": {
"chokidar": ">=2.0.0 <4.0.0"
@ -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",
@ -11029,9 +11113,9 @@
"integrity": "sha1-9evU+mvShpQD4pqJau1JBEVskSM="
},
"vue-cli-plugin-vuetify": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.8.tgz",
"integrity": "sha512-BHn9wwj/+B9v25mhZq2dV8NafM2LbogymjluPP+CjDnIdcwR3hW38r3nyKsZNPB1jXfWXsvVszipS3b8FqOBCg==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.9.tgz",
"integrity": "sha512-J4fzpz27OmCCAA3CI56ulYsUrZ859dQAh58Z9XZilY03kd/M+svLlPkK45cBIrGGfjSqQ40oyWezA3NiPBEG8g==",
"dev": true,
"requires": {
"null-loader": "^3.0.0",
@ -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",
@ -11249,9 +11242,9 @@
"dev": true
},
"vuetify": {
"version": "2.3.21",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.3.21.tgz",
"integrity": "sha512-c9FOjkpVPDoIim88wbfqSIuCsH3jtgQQBC1iMW+ZFxf/Bj+d73HySL2LhEnZwAQT7XTAUGfad4aLPfcNZzK5YQ=="
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.2.tgz",
"integrity": "sha512-8W1928Fv6GKwLiOThutYf2wtD5C9+vcCavlI8NT0YxNOVvluoL8xrep8mGGwDsCkay+4LzaAX92owKeNi3kpWg=="
},
"vuetify-loader": {
"version": "1.6.0",
@ -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

@ -8,16 +8,15 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.0",
"core-js": "^3.8.1",
"axios": "^0.21.1",
"core-js": "^3.8.2",
"qs": "^6.9.4",
"v-jsoneditor": "^1.4.2",
"vue": "^2.6.11",
"vue-cookies": "^1.7.4",
"vue-html-to-paper": "^1.3.1",
"vue-router": "^3.4.9",
"vuetify": "^2.3.21",
"vuex": "^3.6.0"
"vuetify": "^2.4.2",
"vuex": "^3.6.0",
"vuex-persistedstate": "^4.0.0-beta.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
@ -26,9 +25,9 @@
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.30.0",
"sass": "^1.32.0",
"sass-loader": "^8.0.0",
"vue-cli-plugin-vuetify": "^2.0.8",
"vue-cli-plugin-vuetify": "^2.0.9",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.3.0"
},

View file

@ -2,7 +2,7 @@
<v-app>
<v-app-bar dense app color="primary" dark class="d-print-none">
<v-btn @click="$router.push('/')" icon class="d-flex align-center">
<v-icon size="40" >
<v-icon size="40">
mdi-silverware-variant
</v-icon>
</v-btn>
@ -20,7 +20,7 @@
</v-app-bar>
<v-main>
<v-container>
<AddRecipe />
<AddRecipeFab />
<SnackBar />
<v-expand-transition>
<SearchHeader v-show="search" />
@ -35,41 +35,63 @@
<script>
import Menu from "./components/UI/Menu";
import SearchHeader from "./components/UI/SearchHeader";
import AddRecipe from "./components/AddRecipe";
import AddRecipeFab from "./components/UI/AddRecipeFab";
import SnackBar from "./components/UI/SnackBar";
import Vuetify from "./plugins/vuetify";
export default {
name: "App",
components: {
Menu,
AddRecipe,
AddRecipeFab,
SearchHeader,
SnackBar,
SnackBar
},
watch: {
$route() {
this.search = false;
},
}
},
mounted() {
this.$store.dispatch("initCookies");
this.$store.dispatch("initTheme");
this.$store.dispatch("requestRecentRecipes");
this.darkModeSystemCheck();
this.darkModeAddEventListener();
},
data: () => ({
search: false,
search: false
}),
methods: {
/**
* Checks if 'system' is set for dark mode and then sets the corrisponding value for vuetify
*/
darkModeSystemCheck() {
if (this.$store.getters.getDarkMode === "system")
Vuetify.framework.theme.dark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
},
/**
* This will monitor the OS level darkmode and call to update dark mode.
*/
darkModeAddEventListener() {
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
darkMediaQuery.addEventListener("change", () => {
this.darkModeSystemCheck();
});
},
toggleSearch() {
if (this.search === true) {
this.search = false;
} else {
this.search = true;
}
},
},
}
}
};
</script>

View file

@ -19,8 +19,9 @@ export default {
},
async import(fileName) {
apiReq.post(backupURLs.importBackup(fileName));
let response = await apiReq.post(backupURLs.importBackup(fileName));
store.dispatch("requestRecentRecipes");
return response;
},
async delete(fileName) {

View file

@ -12,7 +12,6 @@ export default {
async migrateChowdown(repoURL) {
let postBody = { url: repoURL };
let response = await apiReq.post(migrationURLs.chowdownURL, postBody);
console.log(response);
store.dispatch("requestRecentRecipes");
return response.data;
},

View file

@ -19,7 +19,6 @@ export default {
async requestByName(name) {
let response = await apiReq.get(settingsURLs.specificTheme(name));
console.log(response);
return response.data;
},

View file

@ -1,12 +0,0 @@
<template>
<v-card>
<v-card-title class="card-title mt-1"> SFTP Settings </v-card-title>
</v-card>
</template>
<script>
export default {};
</script>
<style>
</style>

View file

@ -1,159 +0,0 @@
<template>
<v-card>
<v-card-title class="secondary white--text"> Theme Settings </v-card-title>
<v-card-text>
<p>
Select a theme from the dropdown or create a new theme. Note that the
default theme will be served to all users who have not set a theme
preference.
</p>
<v-row dense align="center">
<v-col cols="12" md="2" sm="5">
<v-switch
v-model="darkMode"
inset
label="Dark Mode"
class="my-n3"
@change="toggleDarkMode"
></v-switch>
</v-col>
<v-col cols="12" md="4" sm="3">
<v-form ref="form" lazy-validation>
<v-select
label="Saved Color Schemes"
:items="availableThemes"
item-text="name"
item-value="colors"
return-object
v-model="selectedScheme"
@change="themeSelected"
:rules="[(v) => !!v || 'Theme is required']"
required
>
</v-select>
</v-form>
</v-col>
<v-col cols="12" sm="1">
<NewTheme @new-theme="appendTheme" />
</v-col>
<v-col cols="12" sm="1">
<v-btn text color="error" @click="deleteSelected"> Delete </v-btn>
</v-col>
</v-row>
<v-row dense align-content="center" v-if="activeTheme">
<v-col>
<ColorPicker button-text="Primary" v-model="activeTheme.primary" />
</v-col>
<v-col>
<ColorPicker
button-text="Secondary"
v-model="activeTheme.secondary"
/>
</v-col>
<v-col>
<ColorPicker button-text="Accent" v-model="activeTheme.accent" />
</v-col>
<v-col>
<ColorPicker button-text="Success" v-model="activeTheme.success" />
</v-col>
<v-col>
<ColorPicker button-text="Info" v-model="activeTheme.info" />
</v-col>
<v-col>
<ColorPicker button-text="Warning" v-model="activeTheme.warning" />
</v-col>
<v-col>
<ColorPicker button-text="Error" v-model="activeTheme.error" />
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-row>
<v-col> </v-col>
<v-col></v-col>
<v-col align="end">
<v-btn text color="success" @click="saveThemes"> Save Theme </v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</template>
<script>
import api from "../../api";
import ColorPicker from "./ThemeUI/ColorPicker";
import NewTheme from "./ThemeUI/NewTheme";
export default {
components: {
ColorPicker,
NewTheme,
},
data() {
return {
themes: null,
activeTheme: {},
darkMode: false,
availableThemes: [],
selectedScheme: "",
selectedLight: "",
};
},
async mounted() {
this.availableThemes = await api.themes.requestAll();
this.darkMode = this.$store.getters.getDarkMode;
this.themes = this.$store.getters.getThemes;
this.setThemeEditor();
},
methods: {
async deleteSelected() {
if (this.$refs.form.validate()) {
if (this.selectedScheme === "default") {
// Notify User Can't Delete Default
} else if (this.selectedScheme !== "") {
api.themes.delete(this.selectedScheme.name);
}
this.availableThemes = await api.themes.requestAll();
}
},
async appendTheme(newTheme) {
api.themes.create(newTheme);
this.availableThemes.push(newTheme);
},
themeSelected() {
this.activeTheme = this.selectedScheme.colors;
},
setThemeEditor() {
if (this.darkMode) {
this.activeTheme = this.themes.dark;
} else {
this.activeTheme = this.themes.light;
}
},
toggleDarkMode() {
this.$store.commit("setDarkMode", this.darkMode);
this.selectedScheme = "";
this.setThemeEditor();
},
saveThemes() {
if (this.$refs.form.validate()) {
if (this.darkMode) {
this.themes.dark = this.activeTheme;
} else {
this.themes.light = this.activeTheme;
}
this.$store.commit("setThemes", this.themes);
this.$store.dispatch("initCookies");
api.themes.update(this.selectedScheme.name, this.activeTheme);
} else;
},
},
};
</script>
<style>
</style>

View file

@ -1,12 +0,0 @@
<template>
<v-card>
<v-card-title class="card-title mt-1"> User Settings </v-card-title>
</v-card>
</template>
<script>
export default {};
</script>
<style>
</style>

View file

@ -1,8 +1,7 @@
<template>
<v-card>
<v-card-title class="secondary white--text"> Edit Meal Plan </v-card-title>
<v-card-text> </v-card-text>
<v-card-title class="headline"> Edit Meal Plan </v-card-title>
<v-divider></v-divider>
<v-card-text>
<MealPlanCard v-model="mealPlan.meals" />
<v-row align="center" justify="end">

View file

@ -1,8 +1,7 @@
<template>
<v-card>
<v-card-title class="secondary white--text">
Create a New Meal Plan
</v-card-title>
<v-card-title class="headline"> Create a New Meal Plan </v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row dense>
<v-col cols="12" lg="6" md="6" sm="12">
@ -107,7 +106,7 @@ export default {
this.meals = [];
for (let i = 0; i < this.dateDif; i++) {
this.meals.push({
slug: "",
slug: "empty",
date: this.getDate(i),
dateText: this.getDayText(i),
});

View file

@ -0,0 +1,104 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="700">
<template v-slot:activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on"> API Extras </v-btn>
</template>
<v-card>
<v-card-title> API Extras </v-card-title>
<v-card-text :key="formKey">
<v-row
align="center"
v-for="(value, key, index) in extras"
:key="index"
>
<v-col cols="12" sm="1">
<v-btn
fab
text
x-small
color="white"
elevation="0"
@click="removeExtra(key)"
>
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
</v-col>
<v-col cols="12" md="3" sm="6">
<v-text-field
label="Object Key"
:value="key"
@input="updateKey(index)"
>
</v-text-field>
</v-col>
<v-col cols="12" md="8" sm="6">
<v-text-field label="Object Value" v-model="extras[key]">
</v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-form ref="addKey">
<v-text-field
label="New Key Name"
v-model="newKeyName"
class="pr-4"
:rules="[rules.required, rules.whiteSpace]"
></v-text-field>
</v-form>
<v-btn color="info" text @click="append"> Add Key</v-btn>
<v-spacer></v-spacer>
<v-btn color="success" text @click="save"> Save </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
props: {
extras: Object,
},
data() {
return {
newKeyName: null,
dialog: false,
formKey: 1,
rules: {
required: (v) => !!v || "Key Name Required",
whiteSpace: (v) =>
!v || v.split(" ").length <= 1 || "No White Space Allowed",
},
};
},
methods: {
save() {
this.$emit("save", this.extras);
this.dialog = false;
},
append() {
if (this.$refs.addKey.validate()) {
this.extras[this.newKeyName] = "value";
this.formKey += 1;
}
},
removeExtra(key) {
delete this.extras[key];
this.formKey += 1;
},
},
};
</script>
<style>
</style>

View file

@ -129,6 +129,7 @@
<v-btn class="mt-1" color="secondary" fab dark small @click="addNote">
<v-icon>mdi-plus</v-icon>
</v-btn>
<ExtrasEditor :extras="value.extras" @save="saveExtras" />
</v-col>
<v-divider class="my-divider" :vertical="true"></v-divider>
@ -175,12 +176,14 @@
</template>
<script>
import api from "../../api";
import utils from "../../utils";
import api from "../../../api";
import utils from "../../../utils";
import BulkAdd from "./BulkAdd";
import ExtrasEditor from "./ExtrasEditor";
export default {
components: {
BulkAdd,
ExtrasEditor,
},
props: {
value: Object,
@ -188,13 +191,6 @@ export default {
data() {
return {
fileObject: null,
content: this.value,
disabledSteps: [],
description: String,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
};
},
methods: {
@ -270,6 +266,9 @@ export default {
removeTags(index) {
this.value.tags.splice(index, 1);
},
saveExtras(extras) {
this.value.extras = extras;
},
},
};
</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

@ -50,7 +50,7 @@
<h2 class="mt-4">Categories</h2>
<v-chip
class="ma-1"
color="primary"
color="accent"
dark
v-for="category in categories"
:key="category"
@ -63,7 +63,7 @@
<h2 class="mt-4">Tags</h2>
<v-chip
class="ma-1"
color="primary"
color="accent"
dark
v-for="tag in tags"
:key="tag"

View file

@ -1,128 +0,0 @@
<template>
<v-card-text>
<v-row>
<v-col cols="4">
<h2 class="mb-4">Ingredients</h2>
<div v-for="ingredient in ingredients" :key="ingredient">
<v-row align="center">
<v-checkbox hide-details class="shrink mr-2 mt-0"></v-checkbox>
<v-text-field :value="ingredient"></v-text-field>
</v-row>
</div>
<v-btn
class="ml-n5"
color="primary"
fab`
dark
small
@click="addIngredient"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
<h2 class="mt-6">Categories</h2>
<v-combobox
dense
multiple
chips
item-color="primary"
deletable-chips
:value="categories"
>
<template v-slot:selection="data">
<v-chip :selected="data.selected" close color="primary" dark>
{{ data.item }}
</v-chip>
</template>
</v-combobox>
<h2 class="mt-4">Tags</h2>
<v-combobox dense multiple chips deletable-chips :value="tags">
<template v-slot:selection="data">
<v-chip :selected="data.selected" close color="primary" dark>
{{ data.item }}
</v-chip>
</template>
</v-combobox>
</v-col>
<v-divider :vertical="true"></v-divider>
<v-col>
<h2 class="mb-4">Instructions</h2>
<div v-for="(step, index) in instructions" :key="step.text">
<v-hover v-slot="{ hover }">
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }]"
:elevation="hover ? 12 : 2"
>
<v-card-title>Step: {{ index + 1 }}</v-card-title>
<v-card-text>
<v-textarea dense :value="step.text"></v-textarea>
</v-card-text>
</v-card>
</v-hover>
</div>
<v-btn color="primary" fab dark small @click="addStep">
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-col>
</v-row>
</v-card-text>
</template>
<script>
export default {
props: {
form: Boolean,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
},
data() {
return {
disabledSteps: [],
};
},
methods: {
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
return "disabled-card";
} else {
return;
}
},
saveRecipe() {
this.$emit("save");
},
deleteRecipe() {
this.$emit("delete");
},
addIngredient() {
this.$emit("addingredient");
},
addStep() {
this.$emit("addstep");
},
},
};
</script>
<style>
.disabled-card {
opacity: 0.5;
}
</style>

View file

@ -1,8 +1,7 @@
<template>
<v-card :loading="backupLoading" class="mt-3" min-height="410px">
<v-card-title class="secondary white--text">
Backup and Exports
</v-card-title>
<v-card :loading="backupLoading">
<v-card-title class="headline"> Backup and Exports </v-card-title>
<v-divider></v-divider>
<v-card-text>
<p>
@ -57,15 +56,28 @@
</v-btn>
</v-col>
</v-row>
<SuccessFailureAlert
success-header="Successfully Imported"
:success="successfulImports"
failed-header="Failed Imports"
:failed="failedImports"
/>
</v-card-text>
</v-card>
</template>
<script>
import api from "../../api";
import api from "../../../api";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
export default {
components: {
SuccessFailureAlert,
},
data() {
return {
failedImports: [],
successfulImports: [],
backupLoading: false,
backupTag: null,
selectedBackup: null,
@ -83,11 +95,15 @@ export default {
this.availableBackups = response.imports;
this.availableTemplates = response.templates;
},
importBackup() {
async importBackup() {
if (this.$refs.form.validate()) {
this.backupLoading = true;
api.backups.import(this.selectedBackup);
let response = await api.backups.import(this.selectedBackup);
console.log(response.data);
this.failedImports = response.data.failed;
this.successfulImports = response.data.successful;
this.backupLoading = false;
}
},

View file

@ -1,8 +1,7 @@
<template>
<v-card :loading="loading">
<v-card-title class="secondary white--text mt-1">
Recipe Migration
</v-card-title>
<v-card-title class="headline"> Recipe Migration </v-card-title>
<v-divider></v-divider>
<v-card-text>
<p>
Currently Chowdown via public Repo URL is the only supported type of
@ -40,7 +39,8 @@
</template>
<script>
import api from "../../api";
import api from "../../../api";
// import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
// import TimePicker from "./Webhooks/TimePicker";
export default {
data() {

View file

@ -1,14 +1,22 @@
<template>
<div>
<v-btn text color="success" @click="dialog = true"> New </v-btn>
<v-btn text color="info" @click="dialog = true"> New </v-btn>
<v-dialog v-model="dialog" width="400">
<v-card>
<v-card-title> Add a New Theme </v-card-title>
<v-card-text>
<v-text-field label="Theme Name" v-model="themeName"></v-text-field>
<v-text-field
label="Theme Name"
v-model="themeName"
:rules="[rules.required]"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-btn color="success" text @click="Select"> Create </v-btn>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="dialog = false"> Cancel </v-btn>
<v-btn color="success" text @click="Select" :disabled="!themeName">
Create
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@ -25,6 +33,9 @@ export default {
return {
dialog: false,
themeName: "",
rules: {
required: (val) => !!val || "Required.",
},
};
},
@ -41,13 +52,13 @@ export default {
const newTheme = {
name: this.themeName,
colors: {
primary: this.randomColor(),
accent: this.randomColor(),
secondary: this.randomColor(),
success: this.randomColor(),
info: this.randomColor(),
warning: this.randomColor(),
error: this.randomColor(),
primary: "#E58325",
accent: "#00457A",
secondary: "#973542",
success: "#5AB1BB",
info: "#4990BA",
warning: "#FF4081",
error: "#EF5350",
},
};

View file

@ -0,0 +1,223 @@
<template>
<v-card>
<v-card-title class="headline"> Theme Settings </v-card-title>
<v-divider></v-divider>
<v-card-text>
<h2 class="mt-4 mb-1">Dark Mode</h2>
<p>
Choose how Mealie looks to you. Set your theme preference to follow your
system settings, or choose to use the light or dark theme.
</p>
<v-row dense align="center">
<v-col cols="12">
<v-btn-toggle
v-model="selectedDarkMode"
color="primary "
mandatory
@change="setStoresDarkMode"
>
<v-btn value="system"> Default to system </v-btn>
<v-btn value="light"> Light </v-btn>
<v-btn value="dark"> Dark </v-btn>
</v-btn-toggle>
</v-col>
</v-row></v-card-text
>
<v-divider></v-divider>
<v-card-text>
<h2 class="mt-1 mb-1">Theme</h2>
<p>
Select a theme from the dropdown or create a new theme. Note that the
default theme will be served to all users who have not set a theme
preference.
</p>
<v-form ref="form" lazy-validation>
<v-row dense align="center">
<v-col cols="12" md="4" sm="3">
<v-select
label="Saved Color Theme"
:items="availableThemes"
item-text="name"
return-object
v-model="selectedTheme"
@change="themeSelected"
:rules="[(v) => !!v || 'Theme is required']"
required
>
</v-select>
</v-col>
<v-col cols="12" sm="1">
<NewThemeDialog @new-theme="appendTheme" />
</v-col>
<v-col cols="12" sm="1">
<v-btn text color="error" @click="deleteSelectedThemeValidation">
Delete
</v-btn>
<Confirmation
title="Delete Theme"
message="Are you sure you want to delete this theme?"
color="error"
icon="mdi-alert-circle"
ref="deleteThemeConfirm"
v-on:confirm="deleteSelectedTheme()"
/>
</v-col>
</v-row>
</v-form>
<v-row dense align-content="center" v-if="selectedTheme.colors">
<v-col>
<ColorPickerDialog
button-text="Primary"
v-model="selectedTheme.colors.primary"
/>
</v-col>
<v-col>
<ColorPickerDialog
button-text="Secondary"
v-model="selectedTheme.colors.secondary"
/>
</v-col>
<v-col>
<ColorPickerDialog
button-text="Accent"
v-model="selectedTheme.colors.accent"
/>
</v-col>
<v-col>
<ColorPickerDialog
button-text="Success"
v-model="selectedTheme.colors.success"
/>
</v-col>
<v-col>
<ColorPickerDialog
button-text="Info"
v-model="selectedTheme.colors.info"
/>
</v-col>
<v-col>
<ColorPickerDialog
button-text="Warning"
v-model="selectedTheme.colors.warning"
/>
</v-col>
<v-col>
<ColorPickerDialog
button-text="Error"
v-model="selectedTheme.colors.error"
/>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-row>
<v-col> </v-col>
<v-col></v-col>
<v-col align="end">
<v-btn text color="success" @click="saveThemes">
Save Colors and Apply Theme
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</template>
<script>
import api from "../../../api";
import ColorPickerDialog from "./ColorPickerDialog";
import NewThemeDialog from "./NewThemeDialog";
import Confirmation from "../../UI/Confirmation";
export default {
components: {
ColorPickerDialog,
Confirmation,
NewThemeDialog,
},
data() {
return {
selectedTheme: {},
selectedDarkMode: "system",
availableThemes: [],
};
},
async mounted() {
this.availableThemes = await api.themes.requestAll();
this.selectedTheme = this.$store.getters.getActiveTheme;
this.selectedDarkMode = this.$store.getters.getDarkMode;
},
methods: {
/**
* Open the delete confirmation.
*/
deleteSelectedThemeValidation() {
if (this.$refs.form.validate()) {
if (this.selectedTheme.name === "default") {
// Notify User Can't Delete Default
} else if (this.selectedTheme !== {}) {
this.$refs.deleteThemeConfirm.open();
}
}
},
/**
* Delete the selected Theme
*/
async deleteSelectedTheme() {
//Delete Theme from DB
await api.themes.delete(this.selectedTheme.name);
//Get the new list of available from DB
this.availableThemes = await api.themes.requestAll();
//Change to default if deleting current theme.
if (
!this.availableThemes.some(
(theme) => theme.name === this.selectedTheme.name
)
) {
await this.$store.dispatch("resetTheme");
this.selectedTheme = this.$store.getters.getActiveTheme;
}
},
/**
* Create the new Theme and select it.
*/
async appendTheme(NewThemeDialog) {
await api.themes.create(NewThemeDialog);
this.availableThemes.push(NewThemeDialog);
this.selectedTheme = NewThemeDialog;
},
themeSelected() {
//TODO Revamp Theme selection.
//console.log("this.activeTheme", this.selectedTheme);
},
setStoresDarkMode() {
this.$store.commit("setDarkMode", this.selectedDarkMode);
},
/**
* This will save the current colors and make the selected theme live.
*/
async saveThemes() {
if (this.$refs.form.validate()) {
this.$store.commit("setTheme", this.selectedTheme);
await api.themes.update(
this.selectedTheme.name,
this.selectedTheme.colors
);
}
},
},
};
</script>
<style>
</style>

View file

@ -1,6 +1,6 @@
<template>
<v-card>
<v-card-title class="secondary white--text mt-1">
<v-card-title class="headline">
Meal Planner Webhooks
</v-card-title>
<v-card-text>
@ -20,7 +20,7 @@
></v-switch>
</v-col>
<v-col cols="12" md="3" sm="5">
<TimePicker @save-time="saveTime" />
<TimePickerDialog @save-time="saveTime" />
</v-col>
<v-col cols="12" md="4" sm="5">
<v-btn text color="info" @click="testWebhooks"> Test Webhooks </v-btn>
@ -60,11 +60,11 @@
</template>
<script>
import api from "../../api";
import TimePicker from "./Webhooks/TimePicker";
import api from "../../../api";
import TimePickerDialog from "./TimePickerDialog";
export default {
components: {
TimePicker,
TimePickerDialog,
},
data() {
return {

View file

@ -19,25 +19,29 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="createRecipe"> Submit </v-btn>
<v-btn color="grey" text @click="reset"> Close </v-btn>
<v-btn color="success" text @click="createRecipe"> Submit </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-speed-dial v-model="fab" fixed right bottom open-on-hover>
<template v-slot:activator>
<v-btn v-model="fab" color="accent" dark fab @click="navCreate">
<v-btn v-model="fab" color="accent" dark fab>
<v-icon> mdi-plus </v-icon>
</v-btn>
</template>
<v-btn fab dark small color="primary" @click="addRecipe = true">
<v-icon>mdi-link</v-icon>
</v-btn>
<v-btn fab dark small color="accent" @click="navCreate">
<v-icon>mdi-square-edit-outline</v-icon>
</v-btn>
</v-speed-dial>
</div>
</template>
<script>
import api from "../api";
import api from "../../api";
export default {
data() {
@ -70,10 +74,11 @@ export default {
},
reset() {
(this.fab = false),
(this.addRecipe = false),
(this.recipeURL = ""),
(this.processing = false);
this.fab = false;
this.error = false;
this.addRecipe = false;
this.recipeURL = "";
this.processing = false;
},
},
};

View file

@ -3,9 +3,25 @@
<template v-slot:extension>
<v-col></v-col>
<div v-if="open">
<v-btn class="mr-2" fab dark small color="error" @click="deleteRecipe">
<v-btn
class="mr-2"
fab
dark
small
color="error"
@click="deleteRecipeConfrim"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<Confirmation
title="Delete Recpie"
message="Are you sure you want to delete this recipie?"
color="error"
icon="mdi-alert-circle"
ref="deleteRecipieConfirm"
v-on:confirm="deleteRecipe()"
/>
<v-btn class="mr-2" fab dark small color="success" @click="save">
<v-icon>mdi-content-save</v-icon>
</v-btn>
@ -21,12 +37,20 @@
</template>
<script>
import Confirmation from "./Confirmation";
export default {
props: {
open: {
default: true,
},
type: Boolean,
default: true
}
},
components: {
Confirmation
},
methods: {
editor() {
this.$emit("editor");
@ -34,13 +58,16 @@ export default {
save() {
this.$emit("save");
},
deleteRecipeConfrim() {
this.$refs.deleteRecipieConfirm.open();
},
deleteRecipe() {
this.$emit("delete");
},
json() {
this.$emit("json");
},
},
}
}
};
</script>

View file

@ -0,0 +1,129 @@
<template>
<v-dialog
v-model="dialog"
:max-width="width"
:style="{ zIndex: zIndex }"
@click:outside="cancel"
@keydown.esc="cancel"
>
<v-card>
<v-toolbar v-if="Boolean(title)" :color="color" dense flat dark>
<v-icon v-if="Boolean(icon)" left> {{ icon }}</v-icon>
<v-toolbar-title v-text="title" />
</v-toolbar>
<v-card-text
v-show="!!message"
class="pa-4 text--primary"
v-html="message"
/>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="cancel"> Cancel </v-btn>
<v-btn :color="color" text @click="confirm"> Confirm </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
/**
* Confirmation Component used to add a second validaion step to an action.
* @version 1.0.1
* @author [zackbcom](https://github.com/zackbcom)
* @since Version 1.0.0
*/
export default {
name: "Confirmation",
props: {
/**
* Message to be in body.
*/
message: String,
/**
* Optional Title message to be used in title.
*/
title: String,
/**
* Optional Icon to be used in title.
*/
icon: {
type: String,
default: "mid-alert-circle"
},
/**
* Color theme of the component. Chose one of the defined theme colors.
* @values primary, secondary, accent, success, info, warning, error
*/
color: {
type: String,
default: "error"
},
/**
* Define the max width of the component.
*/
width: {
type: Number,
default: 400
},
/**
* zIndex of the component.
*/
zIndex: {
type: Number,
default: 200
}
},
data: () => ({
/**
* Keep state of open or closed
*/
dialog: false
}),
methods: {
/**
* Sets the modal to be visiable.
*/
open() {
this.dialog = true;
},
/**
* Cancel button handler.
*/
cancel() {
/**
* Cancel event.
*
* @event Cancel
* @property {string} content content of the first prop passed to the event
*/
this.$emit("cancel");
//Hide Modal
this.dialog = false;
},
/**
* confirm button handler.
*/
confirm() {
/**
* confirm event.
*
* @event confirm
* @property {string} content content of the first prop passed to the event
*/
this.$emit("confirm");
//Hide Modal
this.dialog = false;
}
}
};
</script>
<style>
</style>

View file

@ -20,7 +20,7 @@
</template>
<script>
import RecipeCard from "./UI/RecipeCard";
import RecipeCard from "./RecipeCard";
export default {
components: {

View file

@ -0,0 +1,34 @@
<template>
<div>
<v-alert v-if="success[0]" outlined dense type="success">
<h4>{{ successHeader }}</h4>
<v-list dense>
<v-list-item v-for="success in this.success" :key="success">
{{ success }}
</v-list-item>
</v-list>
</v-alert>
<v-alert v-if="failed[0]" outlined dense type="error">
<h4>{{ failedHeader }}</h4>
<v-list dense>
<v-list-item v-for="fail in this.failed" :key="fail">
{{ fail }}
</v-list-item>
</v-list>
</v-alert>
</div>
</template>
<script>
export default {
props: {
successHeader: String,
success: Array,
failedHeader: String,
failed: Array,
},
};
</script>
<style>
</style>

View file

@ -4,11 +4,9 @@ import vuetify from "./plugins/vuetify";
import store from "./store/store";
import VueRouter from "vue-router";
import { routes } from "./routes";
import VueCookies from "vue-cookies";
Vue.config.productionTip = false;
Vue.use(VueRouter);
Vue.use(VueCookies);
const router = new VueRouter({
routes,
@ -23,7 +21,7 @@ new Vue({
}).$mount("#app");
// Truncate
let filter = function(text, length, clamp) {
let filter = function (text, length, clamp) {
clamp = clamp || "...";
let node = document.createElement("div");
node.innerHTML = text;

View file

@ -5,7 +5,7 @@
</template>
<script>
import RecentRecipes from "./RecentRecipes";
import RecentRecipes from "../components/UI/RecentRecipes";
export default {
components: {

View file

@ -8,7 +8,8 @@
<NewMeal v-else @created="requestMeals" />
<v-card class="my-1">
<v-card-title class="secondary white--text"> Meal Plans </v-card-title>
<v-card-title class="headline"> Meal Plans </v-card-title>
<v-divider></v-divider>
<v-timeline align-top :dense="$vuetify.breakpoint.smAndDown">
<v-timeline-item
@ -33,7 +34,7 @@
:key="generateKey(meal.slug, index)"
>
<v-img
class="rounded-lg"
class="rounded-lg info"
:src="getImage(meal.image)"
height="80"
width="80"
@ -69,10 +70,10 @@
</template>
<script>
import api from "../../api";
import utils from "../../utils";
import NewMeal from "./NewMeal";
import EditPlan from "./EditPlan";
import api from "../api";
import utils from "../utils";
import NewMeal from "../components/MealPlan/MealPlanNew";
import EditPlan from "../components/MealPlan/MealPlanEditor";
export default {
components: {
@ -105,7 +106,6 @@ export default {
editPlan(id) {
this.plannedMeals.forEach((element) => {
if (element.uid === id) {
console.log(element);
this.editMealPlan = element;
}
});

View file

@ -1,6 +1,6 @@
<template>
<v-container fill-height>
<v-row justify="center" align="center">
<v-row>
<v-col sm="12">
<v-card
v-for="(meal, index) in mealPlan.meals"
@ -9,21 +9,27 @@
>
<v-row dense no-gutters align="center" justify="center">
<v-col order="1" md="6" sm="12">
<v-card flat>
<v-card-title> {{ meal.name }} </v-card-title>
<v-card
flat
class="align-center justify-center"
align="center"
justify="center"
>
<v-card-title class="justify-center">
{{ meal.name }}
</v-card-title>
<v-card-subtitle> {{ meal.dateText }}</v-card-subtitle>
<v-card-text> {{ meal.description }} </v-card-text>
<v-card-actions>
<v-btn
color="secondary"
text
@click="$router.push(`/recipe/${meal.slug}`)"
>
View Recipe
</v-btn>
</v-card-actions>
<v-btn
align="center"
color="secondary"
text
@click="$router.push(`/recipe/${meal.slug}`)"
>
View Recipe
</v-btn>
</v-card>
</v-col>
<v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12">
@ -39,8 +45,8 @@
</template>
<script>
import api from "../../api";
import utils from "../../utils";
import api from "../api";
import utils from "../utils";
export default {
data() {
return {
@ -49,7 +55,6 @@ export default {
},
async mounted() {
this.mealPlan = await api.mealPlans.thisWeek();
console.log(this.mealPlan);
},
methods: {
getOrder(index) {

View file

@ -29,20 +29,20 @@
/>
</div>
<EditRecipe v-else v-model="recipeDetails" @upload="getImage" />
<RecipeEditor v-else v-model="recipeDetails" @upload="getImage" />
</v-card>
</template>
<script>
import api from "../api";
import EditRecipe from "./RecipeEditor/EditRecipe";
import RecipeEditor from "../components/Recipe/RecipeEditor";
import VJsoneditor from "v-jsoneditor";
import ButtonRow from "./UI/ButtonRow";
import ButtonRow from "../components/UI/ButtonRow";
export default {
components: {
VJsoneditor,
EditRecipe,
RecipeEditor,
ButtonRow,
},
data() {
@ -83,12 +83,19 @@ export default {
onFileChange() {
this.image = URL.createObjectURL(this.fileObject);
},
async createRecipe() {
this.isLoading = true;
this.recipeDetails.image = this.fileObject.name;
if (this.fileObject) {
this.recipeDetails.image = this.fileObject.name;
}
let slug = await api.recipes.create(this.recipeDetails);
await api.recipes.updateImage(slug, this.fileObject);
if (this.fileObject) {
await api.recipes.updateImage(slug, this.fileObject);
}
this.isLoading = false;
this.$router.push(`/recipe/${slug}`);

View file

@ -18,7 +18,7 @@
@delete="deleteRecipe"
/>
<ViewRecipe
<RecipeViewer
v-if="!form"
:name="recipeDetails.name"
:ingredients="recipeDetails.recipeIngredient"
@ -39,7 +39,7 @@
height="1500px"
:options="jsonEditorOptions"
/>
<EditRecipe v-else v-model="recipeDetails" @upload="getImageFile" />
<RecipeEditor v-else v-model="recipeDetails" @upload="getImageFile" />
</v-card>
</template>
@ -47,15 +47,15 @@
import api from "../api";
import utils from "../utils";
import VJsoneditor from "v-jsoneditor";
import ViewRecipe from "./RecipeEditor/ViewRecipe";
import EditRecipe from "./RecipeEditor/EditRecipe";
import ButtonRow from "./UI/ButtonRow";
import RecipeViewer from "../components/Recipe/RecipeViewer";
import RecipeEditor from "../components/Recipe/RecipeEditor";
import ButtonRow from "../components/UI/ButtonRow";
export default {
components: {
VJsoneditor,
ViewRecipe,
EditRecipe,
RecipeViewer,
RecipeEditor,
ButtonRow,
},
data() {
@ -130,7 +130,6 @@ export default {
api.recipes.delete(this.recipeDetails.slug);
},
async saveRecipe() {
console.log(this.recipeDetails);
await api.recipes.update(this.recipeDetails);
if (this.fileObject) {

View file

@ -2,14 +2,18 @@
<v-container>
<v-alert v-if="newVersion" color="green" type="success" outlined>
A New Version of Mealie is Avaiable,
<a href="https://github.com/hay-kot/mealie" class="green--text">
<a
href="https://github.com/hay-kot/mealie/releases/latest"
target="_blank"
class="green--text"
>
Visit the Repo
</a>
</v-alert>
<Theme />
<Backup />
<Webhooks />
<Migration />
<Backup class="mt-2" />
<Webhooks class="mt-2" />
<Migration class="mt-2" />
<p class="text-center my-2">
Version: {{ version }} | Latest: {{ latestVersion }} ·
<a href="https://hay-kot.github.io/mealie/" target="_blank">
@ -27,10 +31,10 @@
</template>
<script>
import Backup from "./Backup";
import Webhooks from "./Webhooks";
import Theme from "./Theme";
import Migration from "./Migration";
import Backup from "../components/Settings/Backup";
import Webhooks from "../components/Settings/Webhook";
import Theme from "../components/Settings/Theme";
import Migration from "../components/Settings/Migration";
import axios from "axios";
export default {
@ -43,7 +47,7 @@ export default {
data() {
return {
latestVersion: null,
version: "v0.0.1",
version: "v0.0.2",
};
},
mounted() {
@ -52,7 +56,6 @@ export default {
computed: {
newVersion() {
if ((this.latestVersion != null) & (this.latestVersion != this.version)) {
console.log("New Version Avaiable");
return true;
} else {
return false;
@ -64,7 +67,6 @@ export default {
let response = await axios.get(
"https://api.github.com/repos/hay-kot/mealie/releases/latest"
);
console.log(response);
this.latestVersion = response.data.tag_name;
},
},

View file

@ -1,23 +1,23 @@
import Home from "./components/Home";
import Page404 from "./components/Page404";
import Recipe from "./components/Recipe";
import NewRecipe from "./components/NewRecipe";
import Admin from "./components/Admin/Admin";
import MealPlanner from "./components/MealPlan/MealPlanner";
import ThisWeek from "./components/MealPlan/ThisWeek";
import HomePage from "./pages/HomePage";
import Page404 from "./pages/404Page";
import RecipePage from "./pages/RecipePage";
import RecipeNewPage from "./pages/RecipeNewPage";
import SettingsPage from "./pages/SettingsPage";
import MeaplPlanPage from "./pages/MealPlanPage";
import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage";
import api from "./api";
export const routes = [
{ path: "/", component: Home },
{ path: "/mealie", component: Home },
{ path: "/recipe/:recipe", component: Recipe },
{ path: "/new/", component: NewRecipe },
{ path: "/settings/site", component: Admin },
{ path: "/meal-plan/planner", component: MealPlanner },
{ path: "/meal-plan/this-week", component: ThisWeek },
{ path: "/", component: HomePage },
{ path: "/mealie", component: HomePage },
{ path: "/recipe/:recipe", component: RecipePage },
{ path: "/new/", component: RecipeNewPage },
{ path: "/settings/site", component: SettingsPage },
{ path: "/meal-plan/planner", component: MeaplPlanPage },
{ path: "/meal-plan/this-week", component: MealPlanThisWeekPage },
{
path: "/meal-plan/today",
beforeEnter: async (_to, _from, next) => {
beforeEnter: async (_to, _from, next) => {
await todaysMealRoute().then((redirect) => {
next(redirect);
});

View file

@ -0,0 +1,70 @@
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",
};
const mutations = {
setTheme(state, payload) {
Vuetify.framework.theme.themes.dark = payload.colors;
Vuetify.framework.theme.themes.light = payload.colors;
state.activeTheme = payload;
},
setDarkMode(state, payload) {
let isDark = inDarkMode(payload);
if (isDark !== null) {
Vuetify.framework.theme.dark = isDark;
state.darkMode = payload;
}
},
};
const actions = {
async resetTheme({ commit }) {
const defaultTheme = await api.themes.requestByName("default");
if (defaultTheme.colors) {
Vuetify.framework.theme.themes.dark = defaultTheme.colors;
Vuetify.framework.theme.themes.light = defaultTheme.colors;
commit("setTheme", defaultTheme);
}
},
async initTheme({ dispatch, getters }) {
//If theme is empty resetTheme
if (Object.keys(getters.getActiveTheme).length === 0) {
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,
};
export default {
state,
mutations,
actions,
getters,
};

View file

@ -1,11 +1,20 @@
import Vue from "vue";
import Vuex from "vuex";
import api from "../api";
import Vuetify from "../plugins/vuetify";
import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings";
Vue.use(Vuex);
const store = new Vuex.Store({
plugins: [
createPersistedState({
paths: ["userSettings"],
}),
],
modules: {
userSettings,
},
state: {
// Snackbar
snackActive: false,
@ -15,29 +24,6 @@ const store = new Vuex.Store({
// All Recipe Data Store
recentRecipes: [],
allRecipes: [],
// Site Settings
darkMode: false,
themes: {
light: {
primary: "#E58325",
accent: "#00457A",
secondary: "#973542",
success: "#43A047",
info: "#FFFD99",
warning: "#FF4081",
error: "#EF5350",
},
dark: {
primary: "#4527A0",
accent: "#FF4081",
secondary: "#26C6DA",
success: "#43A047",
info: "#2196F3",
warning: "#FB8C00",
error: "#FF5252",
},
},
},
mutations: {
@ -53,39 +39,9 @@ const store = new Vuex.Store({
setRecentRecipes(state, payload) {
state.recentRecipes = payload;
},
setDarkMode(state, payload) {
state.darkMode = payload;
Vue.$cookies.set("darkMode", payload);
Vuetify.framework.theme.dark = payload;
},
setThemes(state, payload) {
state.themes = payload;
Vue.$cookies.set("themes", payload);
Vuetify.framework.theme.themes = payload;
},
},
actions: {
async initCookies() {
if (!Vue.$cookies.isKey("themes")) {
const DEFAULT_THEME = await api.themes.requestByName("default");
Vue.$cookies.set("themes", {
light: DEFAULT_THEME.colors,
dark: DEFAULT_THEME.colors,
});
}
this.commit("setThemes", Vue.$cookies.get("themes"));
// Dark Mode
if (!Vue.$cookies.isKey("darkMode")) {
Vue.$cookies.set("darkMode", false);
}
this.commit("setDarkMode", JSON.parse(Vue.$cookies.get("darkMode")));
},
async requestRecentRecipes() {
const keys = [
"name",
@ -108,10 +64,6 @@ const store = new Vuex.Store({
getSnackType: (state) => state.snackType,
getRecentRecipes: (state) => state.recentRecipes,
// Site Settings
getDarkMode: (state) => state.darkMode,
getThemes: (state) => state.themes,
},
});

View file

@ -14,18 +14,26 @@ 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
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
# Mount Vue Frontend only in production
if PRODUCTION:
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
# API Routes
app.include_router(recipe_routes.router)
@ -46,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,125 +1,94 @@
{
"@context": "http://schema.org",
"@context": "https://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",
"keywords": [
"recipes",
"healthyish",
"salad",
"ginger",
"garlic",
"orange",
"oil",
"soy sauce",
"lemon juice",
"sesame oil",
"kosher salt",
"broccoli",
"brown rice",
"egg",
"celery",
"cilantro",
"mint",
"feel good food plan 2021",
"feel good food plan",
"web"
],
"thumbnailUrl": "https://assets.bonappetit.com/photos/5fdbe70a84d333dd1dcc7900/1:1/w_1698,h_1698,c_limit/BA1220feelgoodalt.jpg",
"publisher": {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Bon App\u00e9tit",
"logo": {
"@type": "ImageObject",
"url": "https://www.bonappetit.com/verso/static/bon-appetit/assets/logo-seo.328de564b950e3d5d1fbe3e42f065290ca1d3844.png",
"width": "479px",
"height": "100px"
},
"url": "https://www.bonappetit.com"
"name": "Pressure Cooker Chicken Tortilla Soup",
"description": "",
"author": {
"@type": "Person",
"name": "Kitschen Cat"
},
"isPartOf": {
"@type": [
"CreativeWork",
"Product"
],
"name": "Bon App\u00e9tit"
},
"isAccessibleForFree": true,
"author": [
{
"@type": "Person",
"name": "Devonn Francis",
"sameAs": "https://bon-appetit.com/contributor/devonn-francis/"
}
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4,
"ratingCount": 2
},
"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",
"image": null,
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/",
"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)"
"2 Large Chicken Breasts",
"12 oz your favorite salsa",
"6 Cups Chicken Broth",
"1 onion, chopped",
"1 red bell pepper, diced",
"2 teaspoons cumin",
"1 tablespoon chili powder",
"2 teaspoons salt",
"1/2 teaspoon black pepper",
"1/8 teaspoon cayenne pepper",
"4 ounces tomato paste",
"1 15oz can black beans, drained and rinsed",
"2 cups frozen corn",
"limes, sour cream or greek yogurt, cilantro, green onion, avocado, tortilla chips"
],
"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": "In pressure cooking pot, add chicken, salsa, chicken broth, onion, bell pepper, cumin, chili powder, salt, black pepper, cayenne pepper, and tomato paste. Stir together.",
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-1"
},
{
"@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": "Lock lid and set to high pressure for 10 minutes.",
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-2"
},
{
"@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."
"text": "When time is up, allow pressure to naturally release for 10 minutes and then use a quick release to get all the remaining pressure out.",
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-3"
},
{
"@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."
"text": "Remove lid and shred chicken using two forks.",
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-4"
},
{
"@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."
"text": "Set pressure cooker to &#8220;simmer&#8221; setting and add black beans and corn. Stir until corn is heated through.",
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-5"
},
{
"@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": "Ladle into bowls and top with a squeeze of lime juice, a dollop of sour cream or greek yogurt, a few sprigs of cilantro, chopped green onion, chopped avocado, and crushed tortilla chips.",
"url": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#instruction-step-6"
}
],
"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",
"prepTime": "0:10:00",
"cookTime": "0:10:00",
"totalTime": "0:20:00",
"recipeYield": "8",
"aggregateRating": {
"@type": "AggregateRating",
"reviewCount": "1",
"ratingValue": "5"
},
"review": [
{
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": "5"
},
"author": {
"@type": "Person",
"name": "Alison"
},
"datePublished": "2017-05-08",
"reviewBody": "Simple and delicious, even my kids loved it!"
}
],
"datePublished": "2017-01-18",
"@id": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#recipe",
"isPartOf": {
"@id": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#webpage"
},
"mainEntityOfPage": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/#webpage",
"slug": "pressure-cooker-chicken-tortilla-soup",
"orgURL": "https://www.kitschencat.com/pressure-cooker-chicken-tortilla-soup/",
"categories": [],
"tags": [],
"dateAdded": null,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,002 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -9,7 +9,7 @@ class RecipeDocument(mongoengine.Document):
# id = mongoengine.UUIDField(primary_key=True)
name = mongoengine.StringField(required=True)
description = mongoengine.StringField(required=True)
image = mongoengine.StringField(required=True)
image = mongoengine.StringField(required=False)
recipeYield = mongoengine.StringField(required=True, default="")
recipeIngredient = mongoengine.ListField(required=True, default=[])
recipeInstructions = mongoengine.ListField(requiredd=True, default=[])
@ -23,7 +23,7 @@ class RecipeDocument(mongoengine.Document):
notes = mongoengine.ListField(default=[])
rating = mongoengine.IntField(required=True, default=0)
orgURL = mongoengine.URLField(required=False)
extras = mongoengine.ListField(required=False)
extras = mongoengine.DictField(required=False)
meta = {
"db_alias": "core",

0
mealie/dist/.gitkeep vendored Normal file
View file

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,19 @@
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 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,11 +22,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)
@ -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,38 +1,37 @@
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:
data.process_meals()
data.save_to_db()
except:
raise HTTPException(
status_code=404,
detail=SnackResponse.error("Unable to Create Mealplan See Log"),
)
# raise HTTPException(
# status_code=404,
# detail=SnackResponse.error("Unable to Create Mealplan See Log"),
# )
return SnackResponse.success("Mealplan Created")
@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()
@ -48,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
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,19 +63,16 @@ 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
@ -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

@ -21,5 +21,4 @@ def root():
@router.get("/{full_path:path}", include_in_schema=False)
def root_plus(full_path):
print(full_path)
return FileResponse(BASE_HTML)

View file

@ -28,8 +28,20 @@ def auto_backup_job():
logger.info("Auto Backup Called")
def import_migration(recipe_dict: dict) -> dict:
del recipe_dict["_id"]
del recipe_dict["dateAdded"]
# Migration from list to Object Type Data
if type(recipe_dict["extras"]) == list:
recipe_dict["extras"] = {}
return recipe_dict
def import_from_archive(file_name: str) -> list:
successful_imports = []
failed_imports = []
file_path = BACKUP_DIR.joinpath(file_name)
@ -40,16 +52,15 @@ def import_from_archive(file_name: str) -> list:
for recipe in recipe_dir.glob("*.json"):
with open(recipe, "r") as f:
recipe_dict = json.loads(f.read())
del recipe_dict["_id"]
del recipe_dict["dateAdded"]
recipeDoc = RecipeDocument(**recipe_dict)
try:
recipe_dict = import_migration(recipe_dict)
recipeDoc = RecipeDocument(**recipe_dict)
recipeDoc.save()
successful_imports.append(recipe.stem)
except:
print("Failed Import:", recipe.stem)
logger.info(f"Failed Import: {recipe.stem}")
failed_imports.append(recipe.stem)
image_dir = TEMP_DIR.joinpath("images")
for image in image_dir.iterdir():
@ -57,7 +68,8 @@ def import_from_archive(file_name: str) -> list:
shutil.copy(image, IMG_DIR)
shutil.rmtree(TEMP_DIR)
return successful_imports
return {"successful": successful_imports, "failed": failed_imports}
def export_db(tag=None, templates=None):
@ -82,6 +94,8 @@ def export_db(tag=None, templates=None):
export_recipes(recipe_folder, template)
elif type(templates) == str:
export_recipes(recipe_folder, templates)
else:
export_recipes(recipe_folder)
zip_path = BACKUP_DIR.joinpath(f"{export_tag}")
shutil.make_archive(zip_path, "zip", backup_folder)
@ -92,7 +106,6 @@ def export_db(tag=None, templates=None):
return str(zip_path.absolute()) + ".zip"
def export_images(dest_dir) -> Path:
for file in IMG_DIR.iterdir():
shutil.copy(file, dest_dir.joinpath(file.name))
@ -100,6 +113,7 @@ def export_images(dest_dir) -> Path:
def export_recipes(dest_dir: Path, template=None) -> Path:
all_recipes = RecipeDocument.objects()
logger.info(f"Backing Up Recipes: {all_recipes}")
for recipe in all_recipes:
json_recipe = recipe.to_json(indent=4)

View file

@ -9,13 +9,15 @@ IMG_DIR = CWD.parent.joinpath("data", "img")
def read_image(recipe_slug: str) -> FileResponse:
recipe_slug = recipe_slug.split(".")[0]
for file in IMG_DIR.glob(f"{recipe_slug}*"):
return file
if IMG_DIR.joinpath(recipe_slug).is_file():
return IMG_DIR.joinpath(recipe_slug)
else:
recipe_slug = recipe_slug.split(".")[0]
for file in IMG_DIR.glob(f"{recipe_slug}*"):
return file
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
pass
delete_image(recipe_slug)
image_path = Path(IMG_DIR.joinpath(f"{recipe_slug}.{extension}"))

View file

@ -23,9 +23,9 @@ WEEKDAYS = [
class Meal(BaseModel):
slug: str
slug: Optional[str]
name: Optional[str]
date: Optional[date]
date: date
dateText: str
image: Optional[str]
description: Optional[str]
@ -57,16 +57,23 @@ class MealPlan(BaseModel):
def process_meals(self):
meals = []
for x, meal in enumerate(self.meals):
recipe = Recipe.get_by_slug(meal.slug)
meal_data = {
"slug": recipe.slug,
"name": recipe.name,
"date": self.startDate + timedelta(days=x),
"dateText": meal.dateText,
"image": recipe.image,
"description": recipe.description,
}
try:
recipe = Recipe.get_by_slug(meal.slug)
meal_data = {
"slug": recipe.slug,
"name": recipe.name,
"date": self.startDate + timedelta(days=x),
"dateText": meal.dateText,
"image": recipe.image,
"description": recipe.description,
}
except:
meal_data = {
"date": self.startDate + timedelta(days=x),
"dateText": meal.dateText,
}
meals.append(Meal(**meal_data))

View file

@ -46,7 +46,6 @@ def read_chowdown_file(recipe_file: Path) -> Recipe:
recipe_data: dict = {}
try:
for x, item in enumerate(yaml.load_all(stream, Loader=Loader)):
print(item)
if x == 0:
recipe_data = item
@ -54,7 +53,6 @@ def read_chowdown_file(recipe_file: Path) -> Recipe:
recipe_description = str(item)
except yaml.YAMLError as exc:
print(exc)
return
reformat_data = {
@ -89,7 +87,6 @@ def chowdown_migrate(repo):
failed_recipes = []
for recipe in recipe_dir.glob("*.md"):
print(recipe.name)
try:
new_recipe = read_chowdown_file(recipe)
new_recipe.save_to_db()

View file

@ -42,7 +42,7 @@ class Recipe(BaseModel):
rating: Optional[int]
rating: Optional[int]
orgURL: Optional[str]
extras: Optional[List[str]]
extras: Optional[dict]
class Config:
schema_extra = {
@ -67,6 +67,9 @@ class Recipe(BaseModel):
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"rating": 3,
"extras": {
"message": "Don't forget to defrost the chicken!"
}
}
}
@ -101,8 +104,11 @@ class Recipe(BaseModel):
def save_to_db(self) -> str:
recipe_dict = self.dict()
extension = Path(recipe_dict["image"]).suffix
recipe_dict["image"] = recipe_dict.get("slug") + extension
try:
extension = Path(recipe_dict["image"]).suffix
recipe_dict["image"] = recipe_dict.get("slug") + extension
except:
recipe_dict["image"] = "no image"
try:
total_time = recipe_dict.get("totalTime")

View file

@ -1,3 +1,5 @@
from typing import List
import json
from pathlib import Path
@ -12,10 +14,44 @@ CWD = Path(__file__).parent
TEMP_FILE = CWD.parent.joinpath("data", "debug", "last_recipe.json")
def normalize_data(recipe_data: dict) -> dict:
if type(recipe_data["recipeYield"]) == list:
recipe_data["recipeYield"] = recipe_data["recipeYield"][0]
def normalize_image_url(image) -> str:
if type(image) == list:
return image[0]
elif type(image) == dict:
return image['url']
elif type(image) == str:
return image
else:
raise Exception(f"Unrecognised image URL format: {image}")
def normalize_instructions(instructions) -> List[dict]:
# One long string split by (possibly multiple) new lines
if type(instructions) == str:
return [{"text": line.strip()} for line in filter(None, instructions.splitlines())]
# Plain strings in a list
elif type(instructions) == list and type(instructions[0]) == str:
return [{"text": step.strip()} for step in instructions]
# Dictionaries (let's assume it's a HowToStep) in a list
elif type(instructions) == list and type(instructions[0]) == dict:
return [{"text": step['text'].strip()} for step in instructions if step['@type'] == 'HowToStep']
else:
raise Exception(f"Unrecognised instruction format: {instructions}")
def normalize_yield(yld) -> str:
if type(yld) == list:
return yld[-1]
else:
return yld
def normalize_data(recipe_data: dict) -> dict:
recipe_data["recipeYield"] = normalize_yield(recipe_data.get("recipeYield"))
recipe_data["recipeInstructions"] = normalize_instructions(recipe_data["recipeInstructions"])
return recipe_data
@ -52,7 +88,7 @@ def process_recipe_url(url: str) -> dict:
new_recipe.update(mealie_tags)
try:
img_path = scrape_image(new_recipe.get("image"), slug)
img_path = scrape_image(normalize_image_url(new_recipe.get("image")), slug)
new_recipe["image"] = img_path.name
except:
new_recipe["image"] = None

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