Merge pull request #59 from hay-kot/dev
v0.0.2 - Pre-release Second Patch
|
@ -1,2 +1,3 @@
|
|||
*/node_modules
|
||||
*/dist
|
||||
*/dist
|
||||
##
|
20
.github/workflows/build-docs.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: Publish docs via GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CONFIG_FILE: docs/mkdocs.yml
|
||||
EXTRA_PACKAGES: build-base
|
15
.gitignore
vendored
|
@ -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*
|
7
.vscode/settings.json
vendored
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"]
|
|
@ -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" ]
|
||||
CMD [ "app.py" ]
|
24
README.md
|
@ -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">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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1 +1 @@
|
|||
docker-compose build && docker-compose -p mealie up -d
|
||||
docker-compose -p mealie up --build
|
18
dev/scripts/scrape_recipe.py
Normal 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}")
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
14
docs/docs/api/api-usage.md
Normal 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!
|
26
docs/docs/api/docs/index.html
Normal file
57
docs/docs/changelog.md
Normal 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
|
|
@ -1,26 +1,22 @@
|
|||
# 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.
|
||||
|
||||
## 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:
|
||||
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.
|
|
@ -0,0 +1,3 @@
|
|||
# Guidelines
|
||||
|
||||
TODO
|
|
@ -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)
|
15
docs/docs/contributors/non-coders.md
Normal 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>
|
|
@ -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)
|
||||
|
||||
|
@ -90,14 +76,4 @@ Categories: []
|
|||
Original URL: https://www.bonappetit.com/recipe/five-spice-popcorn-chicken#intcid=_bon-appetit-recipe-bottom-recirc_3cad5ce9-734a-46f8-b503-78c33d2e7279_similar2-3
|
||||
```
|
||||
|
||||
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.
|
||||
If you decide you don't like mealie. This is a good way to export into a format that can be imported into another.
|
|
@ -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
|
|
@ -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)
|
12
docs/docs/getting-started/migration-imports.md
Normal 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
|
|
@ -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.
|
21
docs/docs/getting-started/site-settings.md
Normal 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.
|
||||
|
||||
|
BIN
docs/docs/gifs/api-extras.gif
Normal file
After Width: | Height: | Size: 14 MiB |
26
docs/docs/html/api.html
Normal file
|
@ -4,9 +4,20 @@
|
|||
<a href="https://github.com/hay-kot/mealie">
|
||||
</a>
|
||||
<p align="center">
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||
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">Request Feature</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
1
frontend/.env.development
Normal file
|
@ -0,0 +1 @@
|
|||
VUE_APP_API_BASE_URL=http://10.10.10.12:9921
|
5
frontend/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"cSpell.enableFiletypes": [
|
||||
"!javascript"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
221
frontend/package-lock.json
generated
|
@ -1738,6 +1738,16 @@
|
|||
"integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/cacache/download/cacache-13.0.1.tgz?cache=0&sync_timestamp=1594428402513&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-13.0.1.tgz",
|
||||
|
@ -1764,6 +1774,34 @@
|
|||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-3.3.1.tgz?cache=0&sync_timestamp=1583735626956&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-cache-dir%2Fdownload%2Ffind-cache-dir-3.3.1.tgz",
|
||||
|
@ -1785,6 +1823,25 @@
|
|||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz?cache=0&sync_timestamp=1597081764621&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flocate-path%2Fdownload%2Flocate-path-5.0.0.tgz",
|
||||
|
@ -1849,6 +1906,16 @@
|
|||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-2.3.8.tgz?cache=0&sync_timestamp=1603882075288&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser-webpack-plugin%2Fdownload%2Fterser-webpack-plugin-2.3.8.tgz",
|
||||
|
@ -1865,6 +1932,18 @@
|
|||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -19,7 +19,6 @@ export default {
|
|||
|
||||
async requestByName(name) {
|
||||
let response = await apiReq.get(settingsURLs.specificTheme(name));
|
||||
console.log(response);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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">
|
|
@ -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),
|
||||
});
|
104
frontend/src/components/Recipe/RecipeEditor/ExtrasEditor.vue
Normal 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>
|
|
@ -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>
|
198
frontend/src/components/Recipe/RecipePrint.vue
Normal file
|
@ -0,0 +1,198 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card flat class="d-print-none">
|
||||
<v-card-text>
|
||||
<v-row align="center" justify="center">
|
||||
<v-btn
|
||||
left
|
||||
color="accent lighten-1 "
|
||||
class="ma-1 image-action"
|
||||
@click="$emit('exit')"
|
||||
>
|
||||
<v-icon> mdi-arrow-left </v-icon>
|
||||
</v-btn>
|
||||
<v-card flat class="text-center" align-center>
|
||||
<v-card-text>Font Size</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab
|
||||
dark
|
||||
x-small
|
||||
color="primary"
|
||||
@click="subtractFontSize"
|
||||
>
|
||||
<v-icon dark> mdi-minus </v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
fab
|
||||
dark
|
||||
x-small
|
||||
color="primary"
|
||||
@click="addFontSize"
|
||||
>
|
||||
<v-icon dark> mdi-plus </v-icon>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card flat>
|
||||
<v-row dense align="center">
|
||||
<v-col md="10" sm="10">
|
||||
<v-card flat>
|
||||
<v-card-title> {{ recipe.name }} </v-card-title>
|
||||
|
||||
<v-card-text> {{ recipe.description }} </v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col md="1" sm="1" justify-end>
|
||||
<v-img :src="getImage(recipe.image)" max-height="200" max-width="300">
|
||||
</v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card flat align>
|
||||
<v-card-text>
|
||||
<v-row class="mt-n6">
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-rating
|
||||
class="mr-2 align-end static"
|
||||
color="secondary darken-1"
|
||||
background-color="secondary lighten-3"
|
||||
length="5"
|
||||
:value="recipe.rating"
|
||||
></v-rating>
|
||||
</v-row>
|
||||
<h2 class="mt-1">Ingredients</h2>
|
||||
<v-row>
|
||||
<v-list dense class="column-wrapper align-start">
|
||||
<v-list-item
|
||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||
:key="generateKey('ingredient', index)"
|
||||
hide-details
|
||||
class="mb-n3 print-text"
|
||||
:label="ingredient"
|
||||
>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon> mdi-minus </v-icon>
|
||||
</v-list-item-icon>
|
||||
{{ ingredient }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<div v-if="recipe.categories[0]">
|
||||
<h2 class="mt-4">Categories</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="primary"
|
||||
dark
|
||||
v-for="category in recipe.categories"
|
||||
:key="category"
|
||||
>
|
||||
{{ category }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="recipe.tags[0]">
|
||||
<h2 class="mt-4">Tags</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="primary"
|
||||
dark
|
||||
v-for="tag in recipe.tags"
|
||||
:key="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<h2 v-if="recipe.notes[0]" class="my-2">Notes</h2>
|
||||
<v-card
|
||||
flat
|
||||
class="mt-1"
|
||||
v-for="(note, index) in recipe.notes"
|
||||
:key="generateKey('note', index)"
|
||||
>
|
||||
<v-card-title> {{ note.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
{{ note.text }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<h2 class="mb-4">Instructions</h2>
|
||||
|
||||
<v-card
|
||||
v-for="(step, index) in recipe.recipeInstructions"
|
||||
:key="generateKey('step', index)"
|
||||
class="my-n4"
|
||||
flat
|
||||
>
|
||||
<v-card-title class="my-n4">Step: {{ index + 1 }}</v-card-title>
|
||||
<v-card-text class="my-n4">{{ step.text }}</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "../../utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
recipe: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fontSize: 1.0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getImage(image) {
|
||||
if (image) {
|
||||
return utils.getImageURL(image) + "?rnd=" + this.imageKey;
|
||||
}
|
||||
},
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
addFontSize() {
|
||||
this.fontSize += 0.2;
|
||||
},
|
||||
subtractFontSize() {
|
||||
this.fontSize -= 0.2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.column-wrapper {
|
||||
column-count: 2;
|
||||
}
|
||||
</style>
|
|
@ -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"
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
},
|
|
@ -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() {
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
223
frontend/src/components/Settings/Theme/index.vue
Normal 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>
|
||||
|
|
@ -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 {
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
|
||||
|
|
129
frontend/src/components/UI/Confirmation.vue
Normal 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>
|
|
@ -20,7 +20,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeCard from "./UI/RecipeCard";
|
||||
import RecipeCard from "./RecipeCard";
|
||||
|
||||
export default {
|
||||
components: {
|
34
frontend/src/components/UI/SuccessFailureAlert.vue
Normal 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>
|
|
@ -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;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import RecentRecipes from "./RecentRecipes";
|
||||
import RecentRecipes from "../components/UI/RecentRecipes";
|
||||
|
||||
export default {
|
||||
components: {
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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) {
|
|
@ -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}`);
|
|
@ -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) {
|
|
@ -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;
|
||||
},
|
||||
},
|
|
@ -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);
|
||||
});
|
||||
|
|
70
frontend/src/store/modules/userSettings.js
Normal 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,
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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-----")
|
||||
|
|
|
@ -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 “simmer” 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,
|
||||
|
|
Before Width: | Height: | Size: 2 MiB |
Before Width: | Height: | Size: 1,002 KiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 622 KiB |
Before Width: | Height: | Size: 2.2 MiB |
Before Width: | Height: | Size: 1.1 MiB |
|
@ -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
|
@ -1,5 +1,4 @@
|
|||
# from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
@ -7,3 +6,24 @@ from pydantic import BaseModel
|
|||
class BackupJob(BaseModel):
|
||||
tag: Optional[str]
|
||||
template: Optional[str]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"tag": "July 23rd 2021",
|
||||
"template": "recipes.md",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Imports(BaseModel):
|
||||
imports: List[str]
|
||||
templates: List[str]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"imports": ["sample_data.zip", "sampe_data2.zip"],
|
||||
"templates": ["recipes.md", "custom_template.md"],
|
||||
}
|
||||
}
|
||||
|
|
12
mealie/models/migration_models.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class ChowdownURL(BaseModel):
|
||||
url: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"url": "https://chowdownrepo.com/repo",
|
||||
}
|
||||
}
|
59
mealie/models/recipe_models.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import pydantic
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class RecipeResponse(BaseModel):
|
||||
List
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": [
|
||||
{
|
||||
"slug": "crockpot-buffalo-chicken",
|
||||
"image": "crockpot-buffalo-chicken.jpg",
|
||||
"name": "Crockpot Buffalo Chicken",
|
||||
},
|
||||
{
|
||||
"slug": "downtown-marinade",
|
||||
"image": "downtown-marinade.jpg",
|
||||
"name": "Downtown Marinade",
|
||||
},
|
||||
{
|
||||
"slug": "detroit-style-pepperoni-pizza",
|
||||
"image": "detroit-style-pepperoni-pizza.jpg",
|
||||
"name": "Detroit-Style Pepperoni Pizza",
|
||||
},
|
||||
{
|
||||
"slug": "crispy-carrots",
|
||||
"image": "crispy-carrots.jpg",
|
||||
"name": "Crispy Carrots",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class AllRecipeRequest(BaseModel):
|
||||
properties: List[str]
|
||||
limit: Optional[int]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"properties": ["name", "slug", "image"],
|
||||
"limit": 100,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RecipeURLIn(BaseModel):
|
||||
url: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
|
||||
|
||||
|
||||
class SlugResponse(BaseModel):
|
||||
class Config:
|
||||
schema_extra = {"example": "adult-mac-and-cheese"}
|
|
@ -1,14 +1,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()
|
||||
|
|
|
@ -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 """
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from models.backup_models import BackupJob
|
||||
from models.migration_models import ChowdownURL
|
||||
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
|
@ -7,10 +8,10 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.post("/api/migration/chowdown/repo/", tags=["Migration"])
|
||||
async def import_chowdown_recipes(repo: dict):
|
||||
async def import_chowdown_recipes(repo: ChowdownURL):
|
||||
""" Import Chowsdown Recipes from Repo URL """
|
||||
try:
|
||||
report = chowdow_migrate(repo.get("url"))
|
||||
report = chowdow_migrate(repo.url)
|
||||
return SnackResponse.success(
|
||||
"Recipes Imported from Git Repo, see report for failures.",
|
||||
additional_data=report,
|
||||
|
|
|
@ -2,6 +2,7 @@ from typing import List, Optional
|
|||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from models.recipe_models import AllRecipeRequest, RecipeURLIn
|
||||
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}"))
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|