Compare commits

..

75 Commits

Author SHA1 Message Date
Hayden
8fe0cdc8a4 fix list test 2021-02-06 14:04:01 -09:00
Hayden
8b07f4e9e0 cleanup 2021-02-06 13:56:36 -09:00
Hayden
445fed853f Merge branch 'master' of https://github.com/hay-kot/mealie into feature/recipe-categories 2021-02-06 13:55:01 -09:00
Hayden
3cc3825bbe docs/new gifs & general updates 2021-02-06 13:42:44 -09:00
Hayden
23026a4cc4 debug routes 2021-02-06 13:07:21 -09:00
Hayden
b558a50a7d cleanup todos 2021-02-06 12:17:19 -09:00
Hayden
8866256a21 added category scroll 2021-02-06 12:17:10 -09:00
Hayden
7bcbe0464a refactor/create global css 2021-02-06 12:16:51 -09:00
Hayden
f471e38d88 fixed navigate on enter in search 2021-02-06 11:06:40 -09:00
hayden
561f586dd1 fixed backwards sort 2021-02-03 20:07:41 -09:00
hayden
38e4afcc3c api refactoring + random cleanup 2021-02-03 19:07:19 -09:00
hayden
c44edf97ae changelog 2021-02-03 19:07:01 -09:00
hayden
d0cc0a089a route refactoring 2021-02-03 19:06:51 -09:00
hayden
7188e58f4c general cleanup 2021-02-02 20:44:44 -09:00
hayden
40b78e427d drag and drop ingredients 2021-02-02 20:44:27 -09:00
hayden
c8e239bd81 refactor/ remove old code 2021-02-02 20:43:36 -09:00
hayden
cc35a4be19 refactor/ router endpoint 2021-02-02 20:42:58 -09:00
hayden
b6111afe69 fix categories database errors 2021-01-31 19:10:21 -09:00
hayden
88baa46a33 remove console.log + refactor categories 2021-01-31 19:10:09 -09:00
hayden
732a2cbc51 bug/normalize recipe steps html 2021-01-30 21:28:27 -09:00
hayden
34a10f375f update branch todos 2021-01-30 21:00:28 -09:00
hayden
f000dffde2 frontend category management 2021-01-30 20:58:16 -09:00
hayden
abcf40899f Merge branch 'dev' of https://github.com/hay-kot/mealie into feature/recipe-categories 2021-01-30 17:30:29 -09:00
hayden
d6794cba7d category/tag database relationship and endpoints 2021-01-30 17:25:05 -09:00
hayden
016108d35f refactor/recipe routers 2021-01-30 17:24:19 -09:00
wengtad
9b41990ea9 Fix missing translations key (#133)
* translation: add simplified & traditional chinese

* Fix missing translations

* fix chinese translations
2021-01-30 11:32:40 -08:00
hayden
3cadc3d04b merge kentors changes 2021-01-29 18:43:07 -09:00
Hayden
874bea7fa4 v0.2.0 Updates (#130)
* migration redesign init

* new color picker

* changelog

* added UI language selection

* fix layout issue on recipe editor

* remove git as dependency

* added UI editor for original URL

* CI/CD Tests

* test: fixed migration routes

* test todos

* bug/added docker volume

* chowdow test data

* partial image recipe image testing

* added card section card

* settings form

* homepage cetegory ui

* frontend category placeholder

* fixed broken scheduler

* remove old files

* removed temp test

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-29 19:31:24 -08:00
dekvall
ce48ae61c7 scraper: unescape html in instructions (#129)
Some urls erroneously deliver escaped html their instructions,
sometimes they are even escaped on multiple levels like here:

https://www.ica.se/recept/kladdig-kladdkaka-722982/

```
>>> normalize_instruction("S&amp;auml;tt ugnen p&amp;aring; 200&amp;deg;C.")
'Sätt ugnen på 200°C.'
```
2021-01-29 16:01:37 -08:00
dekvall
c746f7f4f8 translation: add swedish (#128)
* language: da is Danish

* translations: add swedish
2021-01-29 15:49:02 -08:00
Nick CJ
7f67f844bc Fix link to dev-notes.md (#110) 2021-01-24 12:15:12 -09:00
Hayden
079ebd8ee1 Migration redesign (#119)
* migration redesign init

* new color picker

* changelog

* added UI language selection

* fix layout issue on recipe editor

* remove git as dependency

* added UI editor for original URL

* CI/CD Tests

* test: fixed migration routes

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-23 19:53:39 -09:00
hayden
138093d062 fixed poetry install on docker.dev build 2021-01-22 22:53:17 -09:00
Hayden
99cf606a45 fixed menu links 2021-01-22 15:33:47 -09:00
Hayden
41e079d423 Upload component (#113)
* unified upload button + download backups

* javascript toolings

* fix vuetur config

* fixed type check error

* refactor: clean up bag javascript

* UI updates + name validation

* docs: changelog + sp

* fixed route links

* changelog

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-22 09:23:25 -09:00
Hayden
f35e9c20d6 Upload component (#108)
* unified upload button + download backups

* javascript toolings

* fix vuetur config

* fixed type check error

* refactor: clean up bag javascript

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-20 20:56:47 -09:00
Hayden
51893e89cd Meal planner improvements (#107)
* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

* docker-file shrink

* rebuild

* refactor: moving directories around

* adding funding

* mealplan redesign

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-20 17:01:43 -09:00
Hayden
76830802cc Refactor + New Docker File (#105)
* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

* docker-file shrink

* rebuild

* refactor: moving directories around

* adding funding

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-20 17:00:56 -09:00
Hayden
29db7f8a67 Settings, Themes and Migration Route Tests (#100)
* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-19 15:51:36 -09:00
sephrat
22a517b9c0 Fix typos (#96) 2021-01-19 10:06:48 -09:00
Hayden
2ffaecb7b7 Mealplan CRUD Tests (#95)
* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-18 19:54:46 -09:00
Hayden
bfc28fc506 New tests (#94)
* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-18 15:20:15 -09:00
Bastien
6ee4fc2ea6 Add French Translation (#93) 2021-01-18 13:09:42 -09:00
Hayden
567bff8b42 Dropping Mongo From Dev Branch (#89)
* Fix link to Docker Hub

Found an extra s. DESTROYED it.

* initial pass

* second pass cleanup

* backup card framework

* backup card functionality

* translation

* upload button vile creation

* Release v0.1.0 Candidate (#85)

* Changed uvicorn port to 80

* Changed port in docker-compose to match dockerfile

* Readded environment variables in docker-compose

* production image rework

* Use opengraph metadata to make basic recipe cards when full recipe metadata is not available

* fixed instrucitons on parse

* add last_recipe

* automated testing

* roadmap update

* Sqlite (#75)

* file structure

* auto-test

* take 2

* refactor ap scheduler and startup process

* fixed scraper error

* database abstraction

* database abstraction

* port recipes over to new schema

* meal migration

* start settings migration

* finale mongo port

* backup improvements

* migration imports to new DB structure

* unused import cleanup

* docs strings

* settings and theme import logic

* cleanup

* fixed tinydb error

* requirements

* fuzzy search

* remove scratch file

* sqlalchemy models

* improved search ui

* recipe models almost done

* sql modal population

* del scratch

* rewrite database model mixins

* mostly grabage

* recipe updates

* working sqllite

* remove old files and reorganize

* final cleanup

Co-authored-by: Hayden <hay-kot@pm.me>

* Backup card (#78)

* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>

* added mkdocs to docker-compose

* Translations (#72)

* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>

* fail to start bug fixes

* feature: prep/cook/total time slots (#80)

Co-authored-by: Hayden <hay-kot@pm.me>

* missing bind attributes

* Bug fixes (#81)

* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>

* dockerfile hotfix

* dockerfile hotfix

* Version Release Final Touches (#84)

* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>

* db init hotfix

* bug: fix crash in mongo

* fix mongo bug

* fixed version notifier

* finale changelog

Co-authored-by: kentora <=>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>

* build container

* webscraper hotfix

* dev bug: change data location to prevent reloads

* api docs

* api docs bug

* workflow update

Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>
2021-01-18 09:48:58 -09:00
Hayden
3a5f99919b finale changelog 2021-01-17 21:24:40 -09:00
Hayden
d0f9917c6f fixed version notifier 2021-01-17 21:22:39 -09:00
Hayden
d1fe1f44b8 fix mongo bug 2021-01-17 15:06:43 -09:00
Hayden
b00b0c8af4 bug: fix crash in mongo 2021-01-17 13:52:43 -09:00
Hayden
c6403a7998 db init hotfix 2021-01-17 12:10:13 -09:00
Hayden
a76f472aa4 Version Release Final Touches (#84)
* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-17 10:53:42 -09:00
Hayden
6bc7c4ceb0 dockerfile hotfix 2021-01-16 16:50:37 -09:00
Hayden
cf45fa0015 dockerfile hotfix 2021-01-16 16:49:13 -09:00
Hayden
5b15ffdf5f Bug fixes (#81)
* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-16 15:48:19 -09:00
Hayden
53f15b17ea missing bind attributes 2021-01-16 12:11:19 -09:00
Hayden
cbcbc3a339 feature: prep/cook/total time slots (#80)
Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-16 12:06:02 -09:00
Hayden
9a616910f3 fail to start bug fixes 2021-01-16 12:05:37 -09:00
kentora
0167f2f1ca Translations (#72)
* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2021-01-16 12:00:35 -09:00
Hayden
b689c4715b added mkdocs to docker-compose 2021-01-16 09:33:37 -09:00
Hayden
d0f89956f4 Backup card (#78)
* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-16 09:32:55 -09:00
Hayden
25988836c0 Sqlite (#75)
* file structure

* auto-test

* take 2

* refactor ap scheduler and startup process

* fixed scraper error

* database abstraction

* database abstraction

* port recipes over to new schema

* meal migration

* start settings migration

* finale mongo port

* backup improvements

* migration imports to new DB structure

* unused import cleanup

* docs strings

* settings and theme import logic

* cleanup

* fixed tinydb error

* requirements

* fuzzy search

* remove scratch file

* sqlalchemy models

* improved search ui

* recipe models almost done

* sql modal population

* del scratch

* rewrite database model mixins

* mostly grabage

* recipe updates

* working sqllite

* remove old files and reorganize

* final cleanup

Co-authored-by: Hayden <hay-kot@pm.me>
2021-01-15 21:46:35 -09:00
Hayden
5f25b2492e roadmap update 2021-01-10 12:21:53 -09:00
Hayden
4562a8ca10 automated testing 2021-01-10 11:21:06 -09:00
Hayden
80468d0c47 add last_recipe 2021-01-10 11:08:19 -09:00
Hayden
9efd9399d9 fixed instrucitons on parse 2021-01-10 11:04:52 -09:00
Hayden
8d0604da3a Merge pull request #65 from richardmitic/opengraph
Use opengraph metadata to make basic recipe cards
2021-01-10 10:58:48 -09:00
Richard Mitic
9818d567b9 Use opengraph metadata to make basic recipe cards when full recipe metadata is not available 2021-01-10 20:15:43 +01:00
Hayden
a4a33af1c3 Merge pull request #64 from hay-kot/dockerfile-minification
Dockerfile minification
2021-01-09 23:20:55 -09:00
Hayden
965fa81d9b Merge branch 'dev' into dockerfile-minification 2021-01-09 23:20:40 -09:00
Hayden
0eb1e952b2 production image rework 2021-01-09 22:57:19 -09:00
Hayden
021a9025da Merge pull request #57 from kentora/issue-55
Changed uvicorn port to 80
2021-01-09 22:55:54 -09:00
kentora
fe69114b0b Merge branch 'dev' of https://github.com/hay-kot/mealie into issue-55 2021-01-10 08:34:25 +01:00
kentora
296e24b119 Readded environment variables in docker-compose 2021-01-10 08:30:06 +01:00
Hayden
4b0e9c0d76 Merge pull request #61 from hay-kot/master
Fix PR Error
2021-01-09 19:53:38 -09:00
kentora
9c1d0d9ec9 Changed port in docker-compose to match dockerfile 2021-01-09 22:25:01 +01:00
kentora
e7cb7c6cfd Changed uvicorn port to 80 2021-01-09 22:18:21 +01:00
102 changed files with 1307 additions and 1400 deletions

View File

@@ -1,4 +1,4 @@
name: Docker Build Production
name: Docker Build Dev
on:
push:

View File

@@ -1,6 +1,6 @@
{
"python.formatting.provider": "black",
"python.pythonPath": ".venv/bin/python3.9",
"python.pythonPath": ".venv/bin/python3.8",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],

45
.vscode/tasks.json vendored
View File

@@ -13,42 +13,15 @@
"group": "test"
},
{
"label": "Production: Build and Start Docker Compose",
"command": "./dev/scripts/docker-compose.sh",
"type": "shell",
"args": [],
"problemMatcher": ["$tsc"],
"presentation": {
"reveal": "always"
},
"group": "test"
},
{
"label": "Dev: Start local Backend",
"command": "../${config:python.pythonPath}",
"args": ["app.py"],
"options": {
"cwd": "${workspaceFolder}/mealie/"
},
"type": "shell",
"problemMatcher": [],
"presentation": {
"reveal": "always",
"group": "groupA"
"label": "Production: Build and Start Docker Compose",
"command": "./dev/scripts/docker-compose.sh",
"type": "shell",
"args": [],
"problemMatcher": ["$tsc"],
"presentation": {
"reveal": "always"
},
"group": "test"
}
},
{
"label": "Dev: Start local Frontend",
"command": "npm run serve",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}/frontend/"
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"group": "groupA"
}
}
]
}

View File

@@ -5,7 +5,7 @@ RUN npm install
COPY ./frontend/ .
RUN npm run build
FROM python:3.9-alpine
FROM python:3.8-alpine
RUN apk add --no-cache libxml2-dev libxslt-dev libxml2
ENV ENV prod
@@ -17,6 +17,7 @@ COPY ./pyproject.toml /app/
RUN apk add --update --no-cache --virtual .build-deps \
curl \
g++ \
py-lxml \
python3-dev \
musl-dev \
gcc \
@@ -34,5 +35,5 @@ COPY --from=build-stage /app/dist /app/dist
RUN rm -rf /app/test /app/.temp
VOLUME [ "/app/data/" ]
VOLUME [ "/app_data/" ]
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]

33
Dockerfile.arm Normal file
View File

@@ -0,0 +1,33 @@
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY ./frontend/package*.json ./
RUN npm install
COPY ./frontend/ .
RUN npm run build
FROM mrnr91/uvicorn-gunicorn-fastapi:python3.8
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN apt-get update -y && \
apt-get install -y python-pip python-dev git curl --no-install-recommends
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
cd /usr/local/bin && \
ln -s /opt/poetry/bin/poetry && \
poetry config virtualenvs.create false
COPY ./pyproject.toml ./app/poetry.lock* /app/
COPY ./mealie /app
RUN poetry install --no-root --no-dev
COPY --from=build-stage /app/dist /app/dist
RUN rm -rf /app/test /app/.temp
ENV ENV prod
ENV APP_MODULE "app:app"
VOLUME [ "/app/data" ]

1
dev/scripts/buildx.sh Normal file
View File

@@ -0,0 +1 @@
docker buildx build .

0
dev/scripts/scrape_recipe.py Executable file → Normal file
View File

View File

@@ -0,0 +1,17 @@
$CWD = Get-Location
$pyFolder = Join-Path -Path $CWD -ChildPath "mealie"
$pyVenv = Join-Path -Path $CWD -ChildPath "/venv/Scripts/python.exe"
$pyScript = Join-Path -Path $CWD -ChildPath "/mealie/app.py"
$pythonCommand = "powershell.exe -NoExit -Command $pyVenv $pyScript"
$vuePath = Join-Path -Path $CWD -ChildPath "/frontend"
$npmCommand = "powershell.exe -NoExit -Command npm run serve"
wt -d $pyFolder "powershell.exe" $pythonCommand `; split-pane -d $vuePath "powershell.exe" $npmCommand
Start-Process chrome "http://127.0.0.1:8000/docs"
Start-Process chrome "http://127.0.0.1:8080
"

16
docker-compose.arm.yml Normal file
View File

@@ -0,0 +1,16 @@
# Use root/example as user/password credentials
# Frontend/Backend Served via the same Uvicorn Server
version: "3.1"
services:
mealie:
build:
context: ./
dockerfile: Dockerfile.arm
container_name: mealie
restart: always
ports:
- 9090:80
environment:
db_type: sql
volumes:
- ./mealie/data/:/app/data

View File

@@ -10,3 +10,6 @@ services:
- 9090:80
environment:
db_type: sqlite
# volumes:
# - ./mealie/data/:/app/data

View File

@@ -1,20 +1,5 @@
# Release Notes
## v0.3.0 - Draft!
### Features and Improvements
- Open search with `/` hotkey!
- Unified and improved snackbar notifications
- Recipe Viewer
- Categories, Tags, and Notes will not be displayed below the steps on smaller screens
- Recipe Editor
- Text areas now auto grow to fit content
- Description, Steps, and Notes support Markdown! This includes inline html in Markdown.
### Development / Misc
- Added async file response for images, downloading files.
- Breakup recipe view component
## v0.2.0 - Now with Test!
This is, what I think, is a big release! Tons of new features and some great quality of life improvements with some additional features. You may find that I made promises to include some fixes/features in v0.2.0. The short of is I greatly underestimated the work needed to refactor the database to a usable state and integrate categories in a way that is useful for users. This shouldn't be taken as a sign that I'm dropping those feature requests or ignoring them. I felt it was better to push a release in the current state rather than drag on development to try and fulfil all of the promises I made.
@@ -71,6 +56,7 @@ This is, what I think, is a big release! Tons of new features and some great qua
!!! error "Breaking Changes"
- API endpoints have been refactored to adhear to a more consistent standard. This is a WIP and more changes are likely to occur.
- Officially Dropped MongoDB Support
- Mounting volume moved to different internal location due to development issues. New volume should be mounted as `mealie/data:/app_data/`. Volume mounts need to be changed.
- Database Breaks! We have not yet implemented a database migration service. As such, upgrades cannot be done by simply pulling the image. You must first export your recipes, update your deployment, and then import your recipes. This pattern is likely to be how upgrades take place prior to v1.0. After v1.0 migrations will be done automatically.
## v0.1.0 - Initial Beta

View File

@@ -14,16 +14,7 @@ There are VSCode tasks created in the .vscode folder. You can use these to quick
## Without Docker
Prerequisites
- Python 3.8+
- Poetry
- Nodejs
- npm
change directories into the mealie directory and run poetry install. cd into the frontend directory and run npm install. After installing dependencies, you can use vscode tasks to run the front and backend server. Use the command pallette to access the tasks.
Alternatively you can run `npm run serve` in the frontend directory and `python app.py` in the mealie directory to get everything up and running for development.
?? TODO
## Trouble Shooting

View File

@@ -1,5 +1,5 @@
# 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 only SQLite is supported. Postrgres support is planned, however for most loads you may find SQLite performant enough.
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 MongoDB and SQLite are supported. MongoDB support will be dropped in v0.2.0 so it is recommended to go with SQLite for new deployments. Postrgres support is planned, however for most loads you may find SQLite performant enough for most use cases.
[Get Docker](https://docs.docker.com/get-docker/)
@@ -14,7 +14,7 @@ Deployment with the Docker CLI can be done with `docker run` and specify the dat
docker run \
-e db_type='sqlite' \
-p 9000:80 \
-v `pwd`:'/app/data/' \
-v `pwd`:'/app_data/' \
hkotel/mealie:latest
```
@@ -35,7 +35,7 @@ services:
db_type: sqlite
TZ: America/Anchorage
volumes:
- ./mealie/data/:/app/data
- ./mealie/data/:/app_data
```
@@ -46,7 +46,7 @@ services:
| db_type | sqlite | The database type to be used. Current Options 'sqlite' |
| mealie_port | 9000 | The port exposed by mealie. **do not change this if you're running in docker** If you'd like to use another port, map 9000 to another port of the host. |
| api_docs | True | Turns on/off access to the API documentation locally. |
| TZ | UTC | You should set your time zone accordingly so the date/time features work correctly |
| TZ | | You should set your time zone accordingly so the date/time features work correctly |
## Deployed as a Python Application

View File

@@ -1 +1 @@
VUE_APP_API_BASE_URL=http://localhost:9000
VUE_APP_API_BASE_URL=http://10.10.10.12:9921

View File

@@ -4,37 +4,6 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@adapttive/vue-markdown": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@adapttive/vue-markdown/-/vue-markdown-3.0.3.tgz",
"integrity": "sha512-nopu1itoXJ5CwXe70dACSAq4n17nybBRZqXDu2ZyHC+vjVNjPiSp4kuvdBZHKMVHBEC7YakW4b9Mfkr2Wvybfw==",
"requires": {
"highlight.js": "^10.4.0",
"markdown-it": "^12.0.3",
"markdown-it-abbr": "^1.0.4",
"markdown-it-deflist": "^2.1.0",
"markdown-it-emoji": "^2.0.0",
"markdown-it-external-preview": "^1.0.4",
"markdown-it-footnote": "^3.0.2",
"markdown-it-ins": "^3.0.0",
"markdown-it-katex": "npm:@iktakahiro/markdown-it-katex@^4.0.1",
"markdown-it-mark": "^3.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"markdown-it-task-lists": "^2.1.1",
"markdown-it-toc-and-anchor": "^4.2.0"
},
"dependencies": {
"markdown-it-katex": {
"version": "npm:@iktakahiro/markdown-it-katex@4.0.1",
"resolved": "https://registry.npmjs.org/@iktakahiro/markdown-it-katex/-/markdown-it-katex-4.0.1.tgz",
"integrity": "sha512-kGFooO7fIOgY34PSG8ZNVsUlKhhNoqhzW2kq94TNGa8COzh73PO4KsEoPOsQVG1mEAe8tg7GqG0FoVao0aMHaw==",
"requires": {
"katex": "^0.12.0"
}
}
}
},
"@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npm.taobao.org/@babel/code-frame/download/@babel/code-frame-7.10.4.tgz?cache=0&sync_timestamp=1593522948158&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fcode-frame%2Fdownload%2F%40babel%2Fcode-frame-7.10.4.tgz",
@@ -1385,11 +1354,6 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true
},
"@smartweb/vue-flash-message": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/@smartweb/vue-flash-message/-/vue-flash-message-0.6.10.tgz",
"integrity": "sha512-ceDUUzXI6FDscev36kZQvc2BO+MayOt6uJ2HSh9zoOkfa0PVIhmaoB56InlTTsK7MmlSIvPJpRB+Habdx3MtNw=="
},
"@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -1481,14 +1445,6 @@
"@types/node": "*"
}
},
"@types/concat-stream": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz",
"integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=",
"requires": {
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.34",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
@@ -1531,14 +1487,6 @@
"@types/range-parser": "*"
}
},
"@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
"integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=",
"requires": {
"@types/node": "*"
}
},
"@types/glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@@ -1596,7 +1544,8 @@
"@types/node": {
"version": "14.14.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==",
"dev": true
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -1613,7 +1562,8 @@
"@types/qs": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.3",
@@ -2042,6 +1992,51 @@
"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
},
"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
},
"ssri": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz",
"integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1",
"minipass": "^3.1.1"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -2058,6 +2053,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.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
@@ -2074,6 +2079,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"
}
}
}
},
@@ -2555,11 +2572,6 @@
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
"dev": true
},
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
},
"asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@@ -2658,7 +2670,8 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"atob": {
"version": "2.1.2",
@@ -2800,7 +2813,8 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
},
"batch": {
"version": "0.6.1",
@@ -2841,16 +2855,6 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -2977,11 +2981,6 @@
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
},
"browser-or-node": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-1.3.0.tgz",
"integrity": "sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg=="
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -3101,7 +3100,8 @@
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"buffer-indexof": {
"version": "1.1.1",
@@ -3270,7 +3270,8 @@
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true
},
"chalk": {
"version": "2.4.2",
@@ -3553,17 +3554,6 @@
"integrity": "sha1-ovSEN6LKqaIkNueUvwceyeYc7fY=",
"dev": true
},
"clipboard": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
"integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
"optional": true,
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"clipboardy": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
@@ -3724,6 +3714,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@@ -3731,7 +3722,8 @@
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1605992628233&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz",
"integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM="
"integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=",
"dev": true
},
"commondir": {
"version": "1.0.1",
@@ -3802,6 +3794,7 @@
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
@@ -4036,7 +4029,8 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
"cosmiconfig": {
"version": "5.2.1",
@@ -4637,13 +4631,8 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"optional": true
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"depd": {
"version": "1.1.2",
@@ -4952,7 +4941,8 @@
"entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
"dev": true
},
"errno": {
"version": "0.1.8",
@@ -5633,13 +5623,6 @@
"schema-utils": "^2.5.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"filesize": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
@@ -5786,6 +5769,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
@@ -5908,11 +5892,6 @@
"has-symbols": "^1.0.1"
}
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
"integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw="
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -5988,15 +5967,6 @@
"slash": "^2.0.0"
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"optional": true,
"requires": {
"delegate": "^3.1.2"
}
},
"graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
@@ -6149,7 +6119,8 @@
"highlight.js": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz",
"integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw=="
"integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==",
"dev": true
},
"hmac-drbg": {
"version": "1.0.1",
@@ -6329,17 +6300,6 @@
}
}
},
"http-basic": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
"integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
"requires": {
"caseless": "^0.12.0",
"concat-stream": "^1.6.2",
"http-response-object": "^3.0.1",
"parse-cache-control": "^1.0.1"
}
},
"http-deceiver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
@@ -6396,21 +6356,6 @@
"micromatch": "^3.1.10"
}
},
"http-response-object": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
"requires": {
"@types/node": "^10.0.3"
},
"dependencies": {
"@types/node": {
"version": "10.17.51",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.51.tgz",
"integrity": "sha512-KANw+MkL626tq90l++hGelbl67irOJzGhUJk6a1Bt8QHOeh9tztJx+L0AqttraWKinmZn7Qi5lJZJzx45Gq0dg=="
}
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -6455,7 +6400,8 @@
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
},
"iferr": {
"version": "0.1.5",
@@ -6555,7 +6501,8 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Finherits%2Fdownload%2Finherits-2.0.4.tgz",
"integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w="
"integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=",
"dev": true
},
"inquirer": {
"version": "7.3.3",
@@ -7024,7 +6971,8 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"isexe": {
"version": "2.0.0",
@@ -7222,14 +7170,6 @@
"verror": "1.10.0"
}
},
"katex": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.12.0.tgz",
"integrity": "sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==",
"requires": {
"commander": "^2.19.0"
}
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -7277,14 +7217,6 @@
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
"linkify-it": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"loader-fs-cache": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz",
@@ -7478,108 +7410,6 @@
"object-visit": "^1.0.0"
}
},
"markdown-it": {
"version": "12.0.4",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz",
"integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==",
"requires": {
"argparse": "^2.0.1",
"entities": "~2.1.0",
"linkify-it": "^3.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}
}
},
"markdown-it-abbr": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz",
"integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g="
},
"markdown-it-deflist": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
"integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg=="
},
"markdown-it-emoji": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz",
"integrity": "sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ=="
},
"markdown-it-external-preview": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/markdown-it-external-preview/-/markdown-it-external-preview-1.0.4.tgz",
"integrity": "sha512-kuhuUeL1vmunHdzzUKTSec3Xl30vBbeDu/fgnlPvvQIRcOnWlCK+6pT2ov9R5igaJ0oXS6GMiyDlE7QiL7bd8w==",
"requires": {
"browser-or-node": "^1.3.0",
"buffer": "^6.0.3",
"prismjs": "^1.22.0",
"sync-request": "^6.1.0"
},
"dependencies": {
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
}
}
},
"markdown-it-footnote": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz",
"integrity": "sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A=="
},
"markdown-it-ins": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.1.tgz",
"integrity": "sha512-32SSfZqSzqyAmmQ4SHvhxbFqSzPDqsZgMHDwxqPzp+v+t8RsmqsBZRG+RfRQskJko9PfKC2/oxyOs4Yg/CfiRw=="
},
"markdown-it-mark": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz",
"integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A=="
},
"markdown-it-sub": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz",
"integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g="
},
"markdown-it-sup": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz",
"integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M="
},
"markdown-it-task-lists": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
},
"markdown-it-toc-and-anchor": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/markdown-it-toc-and-anchor/-/markdown-it-toc-and-anchor-4.2.0.tgz",
"integrity": "sha512-DusSbKtg8CwZ92ztN7bOojDpP4h0+w7BVOPuA3PHDIaabMsERYpwsazLYSP/UlKedoQjOz21mwlai36TQ04EpA==",
"requires": {
"clone": "^2.1.0",
"uslug": "^1.0.4"
},
"dependencies": {
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
}
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -7597,11 +7427,6 @@
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"dev": true
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -7707,12 +7532,14 @@
"mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"dev": true
},
"mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"dev": true,
"requires": {
"mime-db": "1.45.0"
}
@@ -7937,13 +7764,6 @@
"thenify-all": "^1.0.0"
}
},
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"dev": true,
"optional": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -8486,11 +8306,6 @@
"safe-buffer": "^5.1.1"
}
},
"parse-cache-control": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
"integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104="
},
"parse-json": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
@@ -9350,14 +9165,6 @@
"renderkid": "^2.0.4"
}
},
"prismjs": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
"integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==",
"requires": {
"clipboard": "^2.0.0"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -9367,7 +9174,8 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"progress": {
"version": "2.0.3",
@@ -9375,14 +9183,6 @@
"integrity": "sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=",
"dev": true
},
"promise": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz",
"integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==",
"requires": {
"asap": "~2.0.6"
}
},
"promise-inflight": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -9569,6 +9369,7 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -9909,7 +9710,8 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"safe-regex": {
"version": "1.1.0",
@@ -9973,12 +9775,6 @@
"ajv-keywords": "^3.5.2"
}
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
"optional": true
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -10732,6 +10528,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -10837,24 +10634,6 @@
"util.promisify": "~1.0.0"
}
},
"sync-request": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
"integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
"requires": {
"http-response-object": "^3.0.1",
"sync-rpc": "^1.2.1",
"then-request": "^6.0.0"
}
},
"sync-rpc": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
"integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
"requires": {
"get-port": "^3.1.0"
}
},
"table": {
"version": "5.4.6",
"resolved": "https://registry.npm.taobao.org/table/download/table-5.4.6.tgz?cache=0&sync_timestamp=1605825218994&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftable%2Fdownload%2Ftable-5.4.6.tgz",
@@ -10998,31 +10777,6 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"then-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
"integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
"requires": {
"@types/concat-stream": "^1.6.0",
"@types/form-data": "0.0.33",
"@types/node": "^8.0.0",
"@types/qs": "^6.2.31",
"caseless": "~0.12.0",
"concat-stream": "^1.6.0",
"form-data": "^2.2.0",
"http-basic": "^8.1.1",
"http-response-object": "^3.0.1",
"promise": "^8.0.0",
"qs": "^6.4.0"
},
"dependencies": {
"@types/node": {
"version": "8.10.66",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
"integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
}
}
},
"thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -11089,12 +10843,6 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"optional": true
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz",
@@ -11247,12 +10995,8 @@
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"uglify-js": {
"version": "3.4.10",
@@ -11354,11 +11098,6 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
"unorm": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
"integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -11482,14 +11221,6 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
"uslug": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz",
"integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=",
"requires": {
"unorm": ">= 1.0.0"
}
},
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@@ -11510,7 +11241,8 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"util.promisify": {
"version": "1.0.1",
@@ -12103,11 +11835,7 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
"optional": true
},
"glob-parent": {
"version": "3.1.0",
@@ -12409,11 +12137,7 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
"optional": true
},
"glob-parent": {
"version": "3.1.0",

View File

@@ -9,8 +9,6 @@
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
},
"dependencies": {
"@adapttive/vue-markdown": "^3.0.3",
"@smartweb/vue-flash-message": "^0.6.10",
"axios": "^0.21.1",
"core-js": "^3.8.2",
"fuse.js": "^6.4.6",

View File

@@ -1,22 +1,16 @@
<template>
<v-app>
<v-app-bar clipped-left dense app color="primary" dark class="d-print-none">
<router-link to="/">
<v-btn icon>
<v-icon size="40"> mdi-silverware-variant </v-icon>
</v-btn>
</router-link>
<v-btn @click="$router.push('/')" icon>
<v-icon size="40"> mdi-silverware-variant </v-icon>
</v-btn>
<div btn class="pl-2">
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
>Mealie
</v-toolbar-title>
<v-toolbar-title @click="$router.push('/')">Mealie</v-toolbar-title>
</div>
<v-spacer></v-spacer>
<v-expand-x-transition>
<SearchBar
ref="mainSearchBar"
class="mt-7"
v-if="search"
:show-results="true"
@@ -35,7 +29,6 @@
<SnackBar />
<router-view></router-view>
</v-container>
<FlashMessage :position="'right bottom'"></FlashMessage>
</v-main>
</v-app>
</template>
@@ -61,13 +54,6 @@ export default {
this.search = false;
},
},
created() {
window.addEventListener("keyup", e => {
if (e.key == "/") {
this.search = !this.search;
}
});
},
mounted() {
this.$store.dispatch("initTheme");
@@ -108,34 +94,5 @@ export default {
</script>
<style>
.notify-info-color {
border: 1px, solid, var(--v-info-base) !important;
border-left: 3px, solid, var(--v-info-base) !important;
background-color: var(--v-info-base) !important;
}
.notify-warning-color {
border: 1px, solid, var(--v-warning-base) !important;
border-left: 3px, solid, var(--v-warning-base) !important;
background-color: var(--v-warning-base) !important;
}
.notify-error-color {
border: 1px, solid, var(--v-error-base) !important;
border-left: 3px, solid, var(--v-error-base) !important;
background-color: var(--v-error-base) !important;
}
.notify-success-color {
border: 1px, solid, var(--v-success-base) !important;
border-left: 3px, solid, var(--v-success-base) !important;
background-color: var(--v-success-base) !important;
}
.notify-base {
color: white !important;
margin-right: 60px;
margin-bottom: -5px;
opacity: 0.9 !important;
}
</style>

View File

@@ -6,9 +6,8 @@ import themes from "./api/themes";
import migration from "./api/migration";
import myUtils from "./api/upload";
import category from "./api/category";
import meta from "./api/meta";
// import api from "@/api";
// import api from "../api";
export default {
recipes: recipe,
@@ -19,5 +18,4 @@ export default {
migrations: migration,
utils: myUtils,
categories: category,
meta: meta,
};

View File

@@ -1,20 +1,23 @@
const baseURL = "/api/";
import axios from "axios";
import utils from "@/utils";
import store from "../store/store";
// look for data.snackbar in response
function processResponse(response) {
try {
utils.notify.show(response.data.snackbar.text, response.data.snackbar.type);
store.commit("setSnackBar", {
text: response.data.snackbar.text,
type: response.data.snackbar.type,
});
} catch (err) {
return;
}
return;
}
const apiReq = {
post: async function(url, data) {
let response = await axios.post(url, data).catch(function(error) {
post: async function (url, data) {
let response = await axios.post(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return error.response;
@@ -24,19 +27,8 @@ const apiReq = {
return response;
},
put: async function(url, data) {
let response = await axios.put(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
return response;
},
get: async function(url, data) {
let response = await axios.get(url, data).catch(function(error) {
put: async function (url, data) {
let response = await axios.put(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return response;
@@ -46,8 +38,19 @@ const apiReq = {
return response;
},
delete: async function(url, data) {
let response = await axios.delete(url, data).catch(function(error) {
get: async function (url, data) {
let response = await axios.get(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
// processResponse(response);
return response;
},
delete: async function (url, data) {
let response = await axios.delete(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return response;

View File

@@ -1,15 +0,0 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
const prefix = baseURL + "debug";
const debugURLs = {
version: `${prefix}/version`,
};
export default {
async get_version() {
let response = await apiReq.get(debugURLs.version);
return response.data;
},
};

View File

@@ -1,14 +1,14 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
const prefix = baseURL + "themes";
const prefix = baseURL + "themes/";
const settingsURLs = {
allThemes: `${baseURL}themes`,
specificTheme: themeName => `${prefix}/${themeName}`,
createTheme: `${prefix}/create`,
updateTheme: themeName => `${prefix}/${themeName}`,
deleteTheme: themeName => `${prefix}/${themeName}`,
specificTheme: (themeName) => `${prefix}themes/${themeName}`,
createTheme: `${prefix}themes/create`,
updateTheme: (themeName) => `${prefix}themes/${themeName}`,
deleteTheme: (themeName) => `${prefix}themes/${themeName}`,
};
export default {
@@ -33,7 +33,6 @@ export default {
colors: colors,
};
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
console.log(response.data);
return response.data;
},

View File

@@ -1,7 +1,7 @@
import { apiReq } from "./api-utils";
export default {
// import api from "@/api";
// import api from "../api";
async uploadFile(url, fileObject) {
let response = await apiReq.post(url, fileObject, {
headers: {

View File

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

View File

@@ -20,8 +20,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../../api";
import utils from "../../utils";
import MealPlanCard from "./MealPlanCard";
export default {
components: {

View File

@@ -85,8 +85,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../../api";
import utils from "../../utils";
import MealPlanCard from "./MealPlanCard";
export default {
components: {

View File

@@ -12,8 +12,28 @@
></v-file-input>
</v-col>
<v-col cols="3"></v-col>
<v-row>
<v-col>
<v-text-field
label="Total Time"
v-model="value.totalTime"
></v-text-field>
</v-col>
<v-col
><v-text-field
label="Prep Time"
v-model="value.prepTime"
></v-text-field
></v-col>
<v-col
><v-text-field
label="Cook Time / Perform Time"
v-model="value.performTime"
></v-text-field
></v-col>
</v-row>
</v-row>
<v-row dense>
<v-row>
<v-col>
<v-text-field
:label="$t('recipe.total-time')"
@@ -41,23 +61,21 @@
>
</v-text-field>
<v-textarea
auto-grow
min-height="100"
height="100"
:label="$t('recipe.description')"
v-model="value.description"
>
</v-textarea>
<div class="my-2"></div>
<v-row dense disabled>
<v-col sm="4">
<v-col sm="5">
<v-text-field
:label="$t('recipe.servings')"
v-model="value.recipeYield"
class="rounded-sm"
>
</v-text-field>
</v-col>
<v-spacer></v-spacer>
<v-col></v-col>
<v-rating
class="mr-2 align-end"
color="secondary darken-1"
@@ -188,7 +206,6 @@
</v-row>
<v-textarea
auto-grow
:label="$t('recipe.note')"
v-model="value.notes[index]['text']"
>
@@ -221,18 +238,17 @@
elevation="0"
@click="removeStep(index)"
>
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
{{ $t("recipe.step-index", { step: index + 1 }) }}
</v-card-title>
<v-icon color="error">mdi-delete</v-icon> </v-btn
>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title
>
<v-card-text>
<v-textarea
auto-grow
dense
v-model="value.recipeInstructions[index]['text']"
:key="generateKey('instructions', index)"
>
</v-textarea>
></v-textarea>
</v-card-text>
</v-card>
</v-hover>
@@ -254,8 +270,8 @@
<script>
import draggable from "vuedraggable";
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 {
@@ -272,8 +288,8 @@ export default {
drag: false,
fileObject: null,
rules: {
required: v => !!v || this.$i18n.t("recipe.key-name-required"),
whiteSpace: v =>
required: (v) => !!v || this.$i18n.t("recipe.key-name-required"),
whiteSpace: (v) =>
!v ||
v.split(" ").length <= 1 ||
this.$i18n.t("recipe.no-white-space-allowed"),
@@ -290,7 +306,7 @@ export default {
methods: {
async getCategories() {
let response = await api.categories.get_all();
this.categories = response.map(cat => cat.name);
this.categories = response.map((cat) => cat.name);
},
uploadImage() {
this.$emit("upload", this.fileObject);
@@ -337,7 +353,7 @@ export default {
appendSteps(steps) {
let processSteps = [];
steps.forEach(element => {
steps.forEach((element) => {
processSteps.push({ text: element });
});

View File

@@ -161,7 +161,7 @@
</template>
<script>
import utils from "@/utils";
import utils from "../../utils";
export default {
props: {

View File

@@ -0,0 +1,183 @@
<template>
<div>
<v-card-title class="headline">
{{ name }}
</v-card-title>
<v-card-text>
{{ description }}
<div class="my-2"></div>
<v-row dense disabled>
<v-col>
<v-btn
v-if="yields"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ yields }}
</v-btn>
</v-col>
<v-rating
class="mr-2 align-end static"
color="secondary darken-1"
background-color="secondary lighten-3"
length="5"
:value="rating"
></v-rating>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<h2 class="mb-4">{{$t('recipe.ingredients')}}</h2>
<div
v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)"
>
<v-checkbox
hide-details
class="ingredients"
:label="ingredient"
color="secondary"
>
</v-checkbox>
</div>
<div v-if="categories[0]">
<h2 class="mt-4">{{$t('recipe.categories')}}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="category in categories"
:key="category"
>
{{ category }}
</v-chip>
</div>
<div v-if="tags[0]">
<h2 class="mt-4">{{$t('recipe.tags')}}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="tag in tags"
:key="tag"
>
{{ tag }}
</v-chip>
</div>
<h2 v-if="notes[0]" class="my-4">{{$t('recipe.notes')}}</h2>
<v-card
class="mt-1"
v-for="(note, index) in 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-divider class="my-divider" :vertical="true"></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<h2 class="mb-4">{{$t('recipe.instructions')}}</h2>
<v-hover
v-for="(step, index) in instructions"
:key="generateKey('step', index)"
v-slot="{ hover }"
>
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
@click="toggleDisabled(index)"
>
<v-card-title>{{ $t('recipe.step-index', {step: index + 1}) }}</v-card-title>
<v-card-text>{{ step.text }}</v-card-text>
</v-card>
</v-hover>
</v-col>
</v-row>
<v-row>
<v-col></v-col>
<v-btn
v-if="orgURL"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
:href="orgURL"
color="secondary darken-1"
target="_blank"
class="rounded-sm mr-4"
>
{{$t('recipe.original-url')}}
</v-btn>
</v-row>
</v-card-text>
</div>
</template>
<script>
import utils from "../../utils";
export default {
props: {
name: String,
description: String,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
notes: Array,
rating: Number,
yields: String,
orgURL: String,
},
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;
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
.static {
pointer-events: none;
}
.my-divider {
margin: 0 -1px;
}
</style>

View File

@@ -1,34 +0,0 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div
v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)"
>
<v-checkbox
hide-details
class="ingredients"
:label="ingredient"
color="secondary"
>
</v-checkbox>
</div>
</div>
</template>
<script>
import utils from "@/utils";
export default {
props: {
ingredients: Array,
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View File

@@ -1,36 +0,0 @@
<template>
<div>
<h2 v-if="notes[0]" class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in notes"
:key="generateKey('note', index)"
>
<v-card-title> {{ note.title }}</v-card-title>
<v-card-text>
<vue-markdown :source="note.text"> </vue-markdown>
</v-card-text>
</v-card>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
notes: Array,
},
components: {
VueMarkdown,
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View File

@@ -1,26 +0,0 @@
<template>
<div v-if="items[0]">
<h2 class="mt-4">{{ title }}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="category in items"
:key="category"
>
{{ category }}
</v-chip>
</div>
</template>
<script>
export default {
props: {
items: Array,
title: String,
},
};
</script>
<style>
</style>

View File

@@ -1,67 +0,0 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<v-hover
v-for="(step, index) in steps"
:key="generateKey('step', index)"
v-slot="{ hover }"
>
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
@click="toggleDisabled(index)"
>
<v-card-title>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title>
<v-card-text>
<vue-markdown :source="step.text"> </vue-markdown>
</v-card-text>
</v-card>
</v-hover>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
steps: Array,
},
components: {
VueMarkdown,
},
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;
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View File

@@ -1,130 +0,0 @@
<template>
<div>
<v-card-title class="headline">
{{ name }}
</v-card-title>
<v-card-text>
<vue-markdown :source="description"> </vue-markdown>
<v-row dense disabled>
<v-col>
<v-btn
v-if="yields"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ yields }}
</v-btn>
</v-col>
<v-rating
class="mr-2 align-end static"
color="secondary darken-1"
background-color="secondary lighten-3"
length="5"
:value="rating"
></v-rating>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<Ingredients :ingredients="ingredients" />
<div v-if="medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
</div>
</v-col>
<v-divider
v-if="medium"
class="my-divider"
:vertical="true"
></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<Steps :steps="instructions" />
</v-col>
</v-row>
<div v-if="!medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
</div>
<v-row class="mt-2 mb-1">
<v-col></v-col>
<v-btn
v-if="orgURL"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
:href="orgURL"
color="secondary darken-1"
target="_blank"
class="rounded-sm mr-4"
>
{{ $t("recipe.original-url") }}
</v-btn>
</v-row>
</v-card-text>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
import RecipeChips from "./RecipeChips";
import Steps from "./Steps";
import Notes from "./Notes";
import Ingredients from "./Ingredients";
export default {
components: {
VueMarkdown,
RecipeChips,
Steps,
Notes,
Ingredients,
},
props: {
name: String,
description: String,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
notes: Array,
rating: Number,
yields: String,
orgURL: String,
},
data() {
return {
disabledSteps: [],
};
},
computed: {
medium() {
return this.$vuetify.breakpoint.mdAndUp;
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
.static {
pointer-events: none;
}
.my-divider {
margin: 0 -1px;
}
</style>

View File

@@ -38,8 +38,8 @@
<script>
import ImportDialog from "./ImportDialog";
import api from "@/api";
import utils from "@/utils";
import api from "../../../api";
import utils from "../../../utils";
export default {
props: {
backups: Array,

View File

@@ -38,8 +38,8 @@
<script>
import ImportDialog from "./ImportDialog";
import api from "@/api";
import utils from "@/utils";
import api from "../../../api";
import utils from "../../../utils";
export default {
props: {
backups: Array,

View File

@@ -46,7 +46,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
export default {
data() {
return {
@@ -84,7 +84,7 @@ export default {
methods: {
async getAvailableBackups() {
let response = await api.backups.requestAvailable();
response.templates.forEach(element => {
response.templates.forEach((element) => {
this.availableTemplates.push(element);
});
},
@@ -101,6 +101,7 @@ export default {
templates: this.selectedTemplates,
};
await api.backups.create(data);
this.loading = false;

View File

@@ -46,7 +46,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import UploadBtn from "../../UI/UploadBtn";
import AvailableBackupCard from "./AvailableBackupCard";

View File

@@ -126,7 +126,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import draggable from "vuedraggable";
export default {

View File

@@ -56,8 +56,8 @@
<script>
import UploadBtn from "../../UI/UploadBtn";
import utils from "@/utils";
import api from "@/api";
import utils from "../../../utils";
import api from "../../../api";
export default {
props: {
folder: String,

View File

@@ -41,7 +41,7 @@
<script>
import MigrationCard from "./MigrationCard";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import api from "@/api";
import api from "../../../api";
export default {
components: {
MigrationCard,
@@ -78,7 +78,7 @@ export default {
},
async getAvailableMigrations() {
let response = await api.migrations.getMigrations();
response.forEach(element => {
response.forEach((element) => {
if (element.type === "nextcloud") {
this.migrations.nextcloud.availableImports = element.files;
} else if (element.type === "chowdown") {

View File

@@ -53,7 +53,7 @@
return-object
v-model="selectedTheme"
@change="themeSelected"
:rules="[v => !!v || $t('settings.theme.theme-is-required')]"
:rules="[(v) => !!v || $t('settings.theme.theme-is-required')]"
required
>
</v-select>
@@ -136,7 +136,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import ColorPickerDialog from "./ColorPickerDialog";
import NewThemeDialog from "./NewThemeDialog";
import Confirmation from "../../UI/Confirmation";
@@ -186,7 +186,7 @@ export default {
//Change to default if deleting current theme.
if (
!this.availableThemes.some(
theme => theme.name === this.selectedTheme.name
(theme) => theme.name === this.selectedTheme.name
)
) {
await this.$store.dispatch("resetTheme");

View File

@@ -56,7 +56,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import TimePickerDialog from "./TimePickerDialog";
export default {
components: {

View File

@@ -49,7 +49,7 @@
</template>
<script>
import api from "@/api";
import api from "../../api";
export default {
data() {

View File

@@ -44,14 +44,14 @@
color="primary"
block="block"
type="submit"
>{{ $t("login.sign-in") }}</v-btn
>{{$t('login.sign-in')}}</v-btn
>
<v-btn
v-else
block="block"
type="submit"
@click.prevent="options.isLoggingIn = true"
>{{ $t("login.sign-up") }}</v-btn
>{{$t('login.sign-up')}}</v-btn
>
</v-form>
</v-card-text>
@@ -72,7 +72,7 @@
</template>
<script>
import api from "@/api";
import api from "../../api";
export default {
props: {},
data() {

View File

@@ -40,7 +40,7 @@
</template>
<script>
import utils from "@/utils";
import utils from "../../utils";
export default {
props: {
name: String,

View File

@@ -12,8 +12,6 @@
hide-no-data
cache-items
solo
autofocus
auto-select-first
>
<template
v-if="showResults"
@@ -45,7 +43,7 @@
<script>
import Fuse from "fuse.js";
import utils from "@/utils";
import utils from "../../utils";
export default {
props: {

View File

@@ -9,7 +9,7 @@
</template>
<script>
import api from "@/api";
import api from "../../api";
export default {
props: {
url: String,

View File

@@ -5,9 +5,7 @@ import store from "./store/store";
import VueRouter from "vue-router";
import { routes } from "./routes";
import i18n from "./i18n";
import FlashMessage from "@smartweb/vue-flash-message";
Vue.use(FlashMessage);
Vue.config.productionTip = false;
Vue.use(VueRouter);
@@ -16,16 +14,16 @@ const router = new VueRouter({
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
});
const vueApp = new Vue({
new Vue({
vuetify,
store,
router,
i18n,
render: h => h(App),
render: (h) => h(App),
}).$mount("#app");
// Truncate
let truncate = function(text, length, clamp) {
let truncate = function (text, length, clamp) {
clamp = clamp || "...";
let node = document.createElement("div");
node.innerHTML = text;
@@ -33,12 +31,11 @@ let truncate = function(text, length, clamp) {
return content.length > length ? content.slice(0, length) + clamp : content;
};
let titleCase = function(value) {
return value.replace(/(?:^|\s|-)\S/g, x => x.toUpperCase());
let titleCase = function (value) {
return value.replace(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
};
Vue.filter("truncate", truncate);
Vue.filter("titleCase", titleCase);
export { vueApp };
export { router };

View File

@@ -13,7 +13,7 @@
</template>
<script>
import api from "@/api";
import api from "../api";
import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar";
export default {

View File

@@ -21,7 +21,7 @@
</template>
<script>
import api from "@/api";
import api from "../api";
import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar";
export default {
@@ -55,7 +55,7 @@ export default {
},
methods: {
async buildPage() {
this.homeCategories.forEach(async element => {
this.homeCategories.forEach(async (element) => {
let recipes = await this.getRecipeByCategory(element.slug);
recipes.position = element.position;
this.recipeByCategory.push(recipes);

View File

@@ -74,8 +74,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../api";
import utils from "../utils";
import NewMeal from "../components/MealPlan/MealPlanNew";
import EditPlan from "../components/MealPlan/MealPlanEditor";

View File

@@ -49,8 +49,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../api";
import utils from "../utils";
export default {
data() {
return {

View File

@@ -39,7 +39,7 @@
</template>
<script>
import api from "@/api";
import api from "../api";
import RecipeEditor from "../components/Recipe/RecipeEditor";
import VJsoneditor from "v-jsoneditor";

View File

@@ -56,8 +56,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../api";
import utils from "../utils";
import VJsoneditor from "v-jsoneditor";
import RecipeViewer from "../components/Recipe/RecipeViewer";
import RecipeEditor from "../components/Recipe/RecipeEditor";
@@ -107,7 +107,7 @@ export default {
},
watch: {
$route: function() {
$route: function () {
this.getRecipeDetails();
},
},

View File

@@ -44,7 +44,6 @@ import General from "../components/Settings/General";
import Webhooks from "../components/Settings/Webhook";
import Theme from "../components/Settings/Theme";
import Migration from "../components/Settings/Migration";
import api from "@/api";
import axios from "axios";
export default {
@@ -58,13 +57,11 @@ export default {
data() {
return {
latestVersion: null,
version: null,
version: "v0.1.0",
};
},
async mounted() {
mounted() {
this.getVersion();
let versionData = await api.meta.get_version();
this.version = versionData.version;
},
computed: {
newVersion() {

View File

@@ -6,8 +6,6 @@ Vue.use(Vuetify);
const vuetify = new Vuetify({
theme: {
dark: false,
options: { customProperties: true },
themes: {
light: {
primary: "#E58325",

View File

@@ -8,7 +8,7 @@ import AllRecipesPage from "./pages/AllRecipesPage";
import CategoryPage from "./pages/CategoryPage";
import MeaplPlanPage from "./pages/MealPlanPage";
import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage";
import api from "@/api";
import api from "./api";
export const routes = [
{ path: "/", component: HomePage },
@@ -24,7 +24,7 @@ export const routes = [
{
path: "/meal-plan/today",
beforeEnter: async (_to, _from, next) => {
await todaysMealRoute().then(redirect => {
await todaysMealRoute().then((redirect) => {
next(redirect);
});
},

View File

@@ -1,4 +1,4 @@
import api from "@/api";
import api from "../../api";
const state = {
showRecent: true,
@@ -30,10 +30,10 @@ const actions = {
};
const getters = {
getShowRecent: state => state.showRecent,
getShowLimit: state => state.showLimit,
getCategories: state => state.categories,
getHomeCategories: state => state.homeCategories,
getShowRecent: (state) => state.showRecent,
getShowLimit: (state) => state.showLimit,
getCategories: (state) => state.categories,
getHomeCategories: (state) => state.homeCategories,
};
export default {

View File

@@ -1,4 +1,4 @@
import api from "@/api";
import api from "../../api";
import Vuetify from "../../plugins/vuetify";
function inDarkMode(payload) {
@@ -60,9 +60,9 @@ const actions = {
};
const getters = {
getActiveTheme: state => state.activeTheme,
getDarkMode: state => state.darkMode,
getIsDark: state => state.isDark,
getActiveTheme: (state) => state.activeTheme,
getDarkMode: (state) => state.darkMode,
getIsDark: (state) => state.isDark,
};
export default {

View File

@@ -1,6 +1,6 @@
import Vue from "vue";
import Vuex from "vuex";
import api from "@/api";
import api from "../api";
import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings";
import language from "./modules/language";
@@ -64,11 +64,11 @@ const store = new Vuex.Store({
getters: {
//
getSnackText: state => state.snackText,
getSnackActive: state => state.snackActive,
getSnackType: state => state.snackType,
getSnackText: (state) => state.snackText,
getSnackActive: (state) => state.snackActive,
getSnackType: (state) => state.snackType,
getRecentRecipes: state => state.recentRecipes,
getRecentRecipes: (state) => state.recentRecipes,
},
});

View File

@@ -1,15 +1,6 @@
// import utils from "@/utils";
// import utils from "../../utils";
// import Vue from "vue";
// import Vuetify from "./plugins/vuetify";
import { vueApp } from "./main";
const notifyHelpers = {
baseCSS: "notify-base",
error: "notify-error-color",
warning: "notify-warning-color",
success: "notify-success-color",
info: "notify-info-color",
};
const days = [
"Sunday",
@@ -81,28 +72,4 @@ export default {
return `${year}-${month}-${day}`;
},
notify: {
show: function(text, type = "info", title = null) {
vueApp.flashMessage.show({
status: type,
title: title,
message: text,
time: 3000,
blockClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
contentClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
});
},
info: function(text, title = null) {
this.show(text, "info", title);
},
success: function(text, title = null) {
this.show(text, "success", title);
},
error: function(text, title = null) {
this.show(text, "error", title);
},
warning: function(text, title = null) {
this.show(text, "warning", title);
},
},
};

View File

@@ -3,7 +3,7 @@ from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
# import utils.startup as startup
from app_config import APP_VERSION, PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
from app_config import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
from routes import (
backup_routes,
debug_routes,
@@ -12,6 +12,7 @@ from routes import (
setting_routes,
static_routes,
theme_routes,
user_routes,
)
from routes.recipe import (
all_recipe_routes,
@@ -19,13 +20,25 @@ from routes.recipe import (
recipe_crud_routes,
tag_routes,
)
from services.settings_services import default_settings_init
from utils.api_docs import generate_api_docs
from utils.logger import logger
"""
TODO:
- [x] Fix Duplicate Category
- [x] Fix category overflow
- [ ] Enable Database Name Versioning
- [ ] Finish Frontend Category Management
- [x] Delete Category
- [ ] Sort Sidebar A-Z
- [ ] Refactor Test Endpoints - Abstract to fixture?
"""
app = FastAPI(
title="Mealie",
description="A place for all your recipes",
version=APP_VERSION,
version="0.0.1",
docs_url=docs_url,
redoc_url=redoc_url,
)
@@ -39,11 +52,6 @@ def start_scheduler():
import services.scheduler.scheduled_jobs
def init_settings():
default_settings_init()
import services.theme_services
def api_routers():
# Recipes
app.include_router(all_recipe_routes.router)
@@ -57,6 +65,8 @@ def api_routers():
app.include_router(theme_routes.router)
# Backups/Imports Routes
app.include_router(backup_routes.router)
# User Routes
app.include_router(user_routes.router)
# Migration Routes
app.include_router(migration_routes.router)
app.include_router(debug_routes.router)
@@ -81,7 +91,6 @@ app.include_router(static_routes.router)
# generate_api_docs(app)
start_scheduler()
init_settings()
if __name__ == "__main__":
logger.info("-----SYSTEM STARTUP-----")

View File

@@ -15,32 +15,14 @@ def ensure_dirs():
ENV = CWD.joinpath(".env")
dotenv.load_dotenv(ENV)
# General
APP_VERSION = "v0.2.1"
DB_VERSION = "v0.2.0"
PRODUCTION = os.environ.get("ENV")
PORT = int(os.getenv("mealie_port", 9000))
API = os.getenv("api_docs", True)
if API:
docs_url = "/docs"
redoc_url = "/redoc"
else:
docs_url = None
redoc_url = None
# Helpful Globals
BASE_DIR = CWD
DATA_DIR = CWD.parent.joinpath("app_data")
if PRODUCTION:
DATA_DIR = Path("/app/data")
WEB_PATH = CWD.joinpath("dist")
IMG_DIR = DATA_DIR.joinpath("img")
BACKUP_DIR = DATA_DIR.joinpath("backups")
DEBUG_DIR = DATA_DIR.joinpath("debug")
MIGRATION_DIR = DATA_DIR.joinpath("migration")
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
SQLITE_DIR = DATA_DIR.joinpath("db")
TEMP_DIR = DATA_DIR.joinpath(".temp")
@@ -53,23 +35,31 @@ REQUIRED_DIRS = [
MIGRATION_DIR,
TEMPLATE_DIR,
SQLITE_DIR,
NEXTCLOUD_DIR,
CHOWDOWN_DIR,
]
ensure_dirs()
APP_VERSION = "v0.2.0"
# General
PRODUCTION = os.environ.get("ENV")
PORT = int(os.getenv("mealie_port", 9000))
API = os.getenv("api_docs", True)
if API:
docs_url = "/docs"
redoc_url = "/redoc"
else:
docs_url = None
redoc_url = None
# DATABASE ENV
SQLITE_FILE = None
DATABASE_TYPE = os.getenv("db_type", "sqlite")
# DATABASE ENV
DATABASE_TYPE = os.getenv("db_type", "sqlite") # mongo, sqlite
if DATABASE_TYPE == "sqlite":
USE_SQL = True
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{APP_VERSION}.sqlite")
else:
raise Exception(
"Unable to determine database type. Acceptible options are 'sqlite' "
"Unable to determine database type. Acceptible options are 'mongo' or 'tinydb' "
)
# Mongo Database
@@ -82,3 +72,6 @@ DB_PORT = os.getenv("db_port", 27017)
# SFTP Email Stuff - For use Later down the line!
SFTP_USERNAME = os.getenv("sftp_username", None)
SFTP_PASSWORD = os.getenv("sftp_password", None)
ensure_dirs()

View File

@@ -9,6 +9,7 @@ from db.sql.theme_models import SiteThemeModel
"""
# TODO
- [ ] Abstract Classes to use save_new, and update from base models
- [x] Create Category and Tags Table with Many to Many relationship
"""
@@ -17,7 +18,7 @@ class _Recipes(BaseDocument):
self.primary_key = "slug"
self.sql_model = RecipeModel
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
def update_image(self, session: Session, slug: str, extension: str) -> str:
entry: RecipeModel = self._query_one(session, match_value=slug)
entry.image = f"{slug}.{extension}"
session.commit()
@@ -48,14 +49,13 @@ class _Settings(BaseDocument):
self.primary_key = "name"
self.sql_model = SiteSettingsModel
def create(self, session: Session, main: dict, webhooks: dict) -> str:
def save_new(self, session: Session, main: dict, webhooks: dict) -> str:
new_settings = self.sql_model(main.get("name"), webhooks)
session.add(new_settings)
return_data = new_settings.dict()
session.commit()
return return_data
return new_settings.dict()
class _Themes(BaseDocument):

View File

@@ -106,7 +106,7 @@ class BaseDocument:
return db_entry
def create(self, session: Session, document: dict) -> dict:
def save_new(self, session: Session, document: dict) -> dict:
"""Creates a new database entry for the given SQL Alchemy Model.
Args: \n

63
mealie/db/db_mealplan.py Normal file
View File

@@ -0,0 +1,63 @@
from typing import List
from app_config import USE_MONGO, USE_SQL
from db.db_base import BaseDocument
from db.db_setup import USE_MONGO, USE_SQL
from db.mongo.meal_models import MealDocument, MealPlanDocument
from db.sql.db_session import create_session
from db.sql.meal_models import MealPlanModel
class _Meals(BaseDocument):
def __init__(self) -> None:
self.primary_key = "uid"
if USE_SQL:
self.sql_model = MealPlanModel
self.create_session = create_session
self.document = MealPlanDocument
@staticmethod
def _process_meals(meals: List[dict]) -> List[MealDocument]:
"""Turns a list of Meals in dictionary form into a list of
MealDocuments that can be attached to a MealPlanDocument
Args: \n
meals (List[dict]): From a Pydantic Class in meal_services.py \n
Returns:
a List of MealDocuments
"""
meal_docs = []
for meal in meals:
meal_doc = MealDocument(**meal)
meal_docs.append(meal_doc)
return meal_docs
def save_new_mongo(self, plan_data: dict) -> None:
"""Saves a new meal plan into the database
Args: \n
plan_data (dict): From a Pydantic Class in meal_services.py \n
"""
if USE_MONGO:
plan_data["meals"] = _Meals._process_meals(plan_data["meals"])
document = self.document(**plan_data)
document.save()
elif USE_SQL:
pass
def update_mongo(self, uid: str, plan_data: dict) -> dict:
if USE_MONGO:
document = self.document.objects.get(uid=uid)
if document:
new_meals = _Meals._process_meals(plan_data["meals"])
document.update(set__meals=new_meals)
document.save()
elif USE_SQL:
pass

68
mealie/db/db_recipes.py Normal file
View File

@@ -0,0 +1,68 @@
from app_config import USE_MONGO, USE_SQL
from db.db_base import BaseDocument
from db.mongo.recipe_models import RecipeDocument
from db.sql.db_session import create_session
from db.sql.recipe_models import RecipeModel
class _Recipes(BaseDocument):
def __init__(self) -> None:
self.primary_key = "slug"
if USE_SQL:
self.sql_model = RecipeModel
self.create_session = create_session
else:
self.document = RecipeDocument
def save_new_sql(self, recipe_data: dict):
session = self.create_session()
new_recipe = self.sql_model(**recipe_data)
session.add(new_recipe)
session.commit()
return recipe_data
def update_mongo(self, slug: str, new_data: dict) -> None:
if USE_MONGO:
document = self.document.objects.get(slug=slug)
if document:
document.update(set__name=new_data.get("name"))
document.update(set__description=new_data.get("description"))
document.update(set__image=new_data.get("image"))
document.update(set__recipeYield=new_data.get("recipeYield"))
document.update(set__recipeIngredient=new_data.get("recipeIngredient"))
document.update(
set__recipeInstructions=new_data.get("recipeInstructions")
)
document.update(set__totalTime=new_data.get("totalTime"))
document.update(set__slug=new_data.get("slug"))
document.update(set__categories=new_data.get("categories"))
document.update(set__tags=new_data.get("tags"))
document.update(set__notes=new_data.get("notes"))
document.update(set__orgURL=new_data.get("orgURL"))
document.update(set__rating=new_data.get("rating"))
document.update(set__extras=new_data.get("extras"))
document.save()
return new_data
# elif USE_SQL:
# session, recipe = self._query_one(match_value=slug)
# recipe.update(session=session, **new_data)
# recipe_dict = recipe.dict()
# session.commit()
# session.close()
# return recipe_dict
def update_image(self, slug: str, extension: str) -> None:
if USE_MONGO:
document = self.document.objects.get(slug=slug)
if document:
document.update(set__image=f"{slug}.{extension}")
elif USE_SQL:
pass

44
mealie/db/db_settings.py Normal file
View File

@@ -0,0 +1,44 @@
from app_config import USE_MONGO, USE_SQL
from db.db_base import BaseDocument
from db.db_setup import USE_MONGO, USE_SQL
from db.mongo.settings_models import SiteSettingsDocument, WebhooksDocument
from db.sql.db_session import create_session
from db.sql.settings_models import SiteSettingsModel
class _Settings(BaseDocument):
def __init__(self) -> None:
self.primary_key = "name"
if USE_SQL:
self.sql_model = SiteSettingsModel
self.create_session = create_session
self.document = SiteSettingsDocument
def save_new(self, main: dict, webhooks: dict) -> str:
if USE_MONGO:
main["webhooks"] = WebhooksDocument(**webhooks)
new_doc = self.document(**main)
return new_doc.save()
elif USE_SQL:
session = create_session()
new_settings = self.sql_model(main.get("name"), webhooks)
session.add(new_settings)
session.commit()
return new_settings.dict()
def update_mongo(self, name: str, new_data: dict) -> dict:
if USE_MONGO:
document = self.document.objects.get(name=name)
if document:
document.update(set__webhooks=WebhooksDocument(**new_data["webhooks"]))
document.save()
elif USE_SQL:
return

56
mealie/db/db_themes.py Normal file
View File

@@ -0,0 +1,56 @@
from app_config import USE_MONGO, USE_SQL
from db.db_base import BaseDocument
from db.db_setup import USE_MONGO, USE_SQL
from db.mongo.settings_models import SiteThemeDocument, ThemeColorsDocument
from db.sql.db_session import create_session
from db.sql.theme_models import SiteThemeModel
class _Themes(BaseDocument):
def __init__(self) -> None:
self.primary_key = "name"
if USE_SQL:
self.sql_model = SiteThemeModel
self.create_session = create_session
else:
self.document = SiteThemeDocument
def save_new(self, theme_data: dict) -> None:
if USE_MONGO:
theme_data["colors"] = ThemeColorsDocument(**theme_data["colors"])
document = self.document(**theme_data)
document.save()
elif USE_SQL:
session = self.create_session()
new_theme = self.sql_model(**theme_data)
session.add(new_theme)
session.commit()
return_data = new_theme.dict()
session.close()
return return_data
def update(self, data: dict) -> dict:
if USE_MONGO:
colors = ThemeColorsDocument(**data["colors"])
theme_document = self.document.objects.get(name=data.get("name"))
if theme_document:
theme_document.update(set__colors=colors)
theme_document.save()
else:
raise Exception("No database entry was found to update")
elif USE_SQL:
session, theme_model = self._query_one(
match_value=data["name"], match_key="name"
)
theme_model.update(**data)
session.commit()
session.close()

View File

@@ -7,7 +7,6 @@ import sqlalchemy.orm as orm
from db.sql.model_base import BaseMixins, SqlAlchemyBase
from slugify import slugify
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import validates
from utils.logger import logger
@@ -44,26 +43,20 @@ recipes2tags = sa.Table(
class Category(SqlAlchemyBase):
__tablename__ = "categories"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
name = sa.Column(sa.String, index=True)
slug = sa.Column(sa.String, index=True, unique=True)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2categories, back_populates="categories"
)
@validates("name")
def validate_name(self, key, name):
assert not name == ""
return name
def __init__(self, name) -> None:
self.name = name.strip()
self.slug = slugify(name)
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Category).filter(Category.slug == test_slug).one()
result = session.query(Category).filter(Category.name == name.strip()).one()
if result:
logger.info("Category exists, associating recipe")
return result
@@ -89,17 +82,12 @@ class Category(SqlAlchemyBase):
class Tag(SqlAlchemyBase):
__tablename__ = "tags"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
name = sa.Column(sa.String, index=True)
slug = sa.Column(sa.String, index=True, unique=True)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2tags, back_populates="tags"
)
@validates("name")
def validate_name(self, key, name):
assert not name == ""
return name
def to_str(self):
return self.name
@@ -117,9 +105,8 @@ class Tag(SqlAlchemyBase):
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Tag).filter(Tag.slug == test_slug).first()
result = session.query(Tag).filter(Tag.name == name.strip()).first()
if result:
logger.info("Tag exists, associating recipe")
@@ -182,7 +169,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
id = sa.Column(sa.Integer, primary_key=True)
# General Recipe Properties
name = sa.Column(sa.String, nullable=False)
name = sa.Column(sa.String)
description = sa.Column(sa.String)
image = sa.Column(sa.String)
recipeYield = sa.Column(sa.String)
@@ -218,11 +205,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
orgURL = sa.Column(sa.String)
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete")
@validates("name")
def validate_name(self, key, name):
assert not name == ""
return name
def __init__(
self,
session,

View File

@@ -1,38 +0,0 @@
from datetime import date
from typing import List, Optional
from pydantic import BaseModel
class Meal(BaseModel):
slug: Optional[str]
name: Optional[str]
date: date
dateText: str
image: Optional[str]
description: Optional[str]
class MealData(BaseModel):
name: Optional[str]
slug: str
dateText: str
class MealPlan(BaseModel):
uid: Optional[str]
startDate: date
endDate: date
meals: List[Meal]
class Config:
schema_extra = {
"example": {
"startDate": date.today(),
"endDate": date.today(),
"meals": [
{"slug": "Packed Mac and Cheese", "date": date.today()},
{"slug": "Eggs and Toast", "date": date.today()},
],
}
}

View File

@@ -1,80 +1,38 @@
import datetime
from typing import Any, List, Optional
from typing import List, Optional
from pydantic import BaseModel, validator
from slugify import slugify
import pydantic
from pydantic.main import BaseModel
class RecipeNote(BaseModel):
title: str
text: str
class RecipeStep(BaseModel):
text: str
class Recipe(BaseModel):
# Standard Schema
name: str
description: Optional[str]
image: Optional[Any]
recipeYield: Optional[str]
recipeIngredient: Optional[list]
recipeInstructions: Optional[list]
totalTime: Optional[str] = None
prepTime: Optional[str] = None
performTime: Optional[str] = None
# Mealie Specific
slug: Optional[str] = ""
categories: Optional[List[str]] = []
tags: Optional[List[str]] = []
dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = []
rating: Optional[int]
orgURL: Optional[str]
extras: Optional[dict] = {}
class AllRecipeResponse(BaseModel):
class Config:
schema_extra = {
"example": {
"name": "Chicken and Rice With Leeks and Salsa Verde",
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
"recipeYield": "4 Servings",
"recipeIngredient": [
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
"Kosher salt, freshly ground pepper",
"3 Tbsp. unsalted butter, divided",
],
"recipeInstructions": [
{
"text": "Season chicken with salt and pepper.",
},
],
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"tags": ["favorite", "yummy!"],
"categories": ["Dinner", "Pasta"],
"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!"},
}
"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",
},
]
}
@validator("slug", always=True, pre=True)
def validate_slug(slug: str, values):
name: str = values["name"]
calc_slug: str = slugify(name)
if slug == calc_slug:
return slug
else:
slug = calc_slug
return slug
class AllRecipeRequest(BaseModel):
properties: List[str]

View File

@@ -1,26 +0,0 @@
from typing import List, Optional
from pydantic import BaseModel
class Webhooks(BaseModel):
webhookTime: str = "00:00"
webhookURLs: Optional[List[str]] = []
enabled: bool = False
class SiteSettings(BaseModel):
name: str = "main"
webhooks: Webhooks
class Config:
schema_extra = {
"example": {
"name": "main",
"webhooks": {
"webhookTime": "00:00",
"webhookURLs": ["https://mywebhookurl.com/webhook"],
"enable": False,
},
}
}

View File

@@ -1,31 +0,0 @@
from pydantic import BaseModel
class Colors(BaseModel):
primary: str
accent: str
secondary: str
success: str
info: str
warning: str
error: str
class SiteTheme(BaseModel):
name: str
colors: Colors
class Config:
schema_extra = {
"example": {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
}

View File

@@ -0,0 +1,10 @@
from typing import Optional
from pydantic import BaseModel
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None

View File

@@ -32,10 +32,10 @@ def available_imports():
@router.post("/export/database", status_code=201)
def export_database(data: BackupJob, session: Session = Depends(generate_session)):
def export_database(data: BackupJob, db: Session = Depends(generate_session)):
"""Generates a backup of the recipe database in json format."""
export_path = backup_all(
session=session,
session=db,
tag=data.tag,
templates=data.templates,
export_recipes=data.options.recipes,
@@ -66,7 +66,7 @@ def upload_backup_zipfile(archive: UploadFile = File(...)):
@router.get("/{file_name}/download")
async def upload_nextcloud_zipfile(file_name: str):
def upload_nextcloud_zipfile(file_name: str):
""" Upload a .zip File to later be imported into Mealie """
file = BACKUP_DIR.joinpath(file_name)
@@ -80,12 +80,12 @@ async def upload_nextcloud_zipfile(file_name: str):
@router.post("/{file_name}/import", status_code=200)
def import_database(
file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)
file_name: str, import_data: ImportJob, db: Session = Depends(generate_session)
):
""" Import a database backup file generated from Mealie. """
import_db = ImportDatabase(
session=session,
session=db,
zip_archive=import_data.name,
import_recipes=import_data.recipes,
force_import=import_data.force,
@@ -98,7 +98,7 @@ def import_database(
return imported
@router.delete("/{file_name}/delete", status_code=200)
@router.delete("/{file_name}/delete", tags=["Import / Export"], status_code=200)
def delete_backup(file_name: str):
""" Removes a database backup from the file system """
@@ -110,4 +110,4 @@ def delete_backup(file_name: str):
detail=SnackResponse.error("Unable to Delete Backup. See Log File"),
)
return SnackResponse.error(f"{file_name} Deleted")
return SnackResponse.success(f"{file_name} Deleted")

View File

@@ -1,6 +1,7 @@
import json
import os
from app_config import APP_VERSION, DEBUG_DIR
from app_config import DEBUG_DIR
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
from utils.logger import LOGGER_FILE
@@ -8,12 +9,6 @@ from utils.logger import LOGGER_FILE
router = APIRouter(prefix="/api/debug", tags=["Debug"])
@router.get("/version")
async def get_mealie_version():
""" Returns the current version of mealie"""
return {"version": APP_VERSION}
@router.get("/last-recipe-json")
async def get_last_recipe_json():
""" Doc Str """
@@ -27,7 +22,18 @@ async def get_log(num: int):
""" Doc Str """
with open(LOGGER_FILE, "rb") as f:
log_text = tail(f, num)
HTML_RESPONSE = log_text
HTML_RESPONSE = f"""
<html>
<head>
<title>Mealie Log</title>
</head>
<body style="white-space: pre-line">
<p>
{log_text}
</p>
</body>
</html>
"""
return HTML_RESPONSE

View File

@@ -10,53 +10,66 @@ router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/all", response_model=List[MealPlan])
def get_all_meals(session: Session = Depends(generate_session)):
def get_all_meals(db: Session = Depends(generate_session)):
""" Returns a list of all available Meal Plan """
return MealPlan.get_all(session)
return MealPlan.get_all(db)
@router.post("/create")
def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)):
def set_meal_plan(data: MealPlan, db: Session = Depends(generate_session)):
""" Creates a meal plan database entry """
data.process_meals(session)
data.save_to_db(session)
data.process_meals(db)
data.save_to_db(db)
# raise HTTPException(
# status_code=404,
# detail=SnackResponse.error("Unable to Create Mealplan See Log"),
# )
return SnackResponse.success("Mealplan Created")
@router.get("/this-week", response_model=MealPlan)
def get_this_week(session: Session = Depends(generate_session)):
def get_this_week(db: Session = Depends(generate_session)):
""" Returns the meal plan data for this week """
return MealPlan.this_week(session)
return MealPlan.this_week(db)
@router.put("/{plan_id}")
def update_meal_plan(
plan_id: str, meal_plan: MealPlan, session: Session = Depends(generate_session)
plan_id: str, meal_plan: MealPlan, db: Session = Depends(generate_session)
):
""" Updates a meal plan based off ID """
meal_plan.process_meals(session)
meal_plan.update(session, plan_id)
meal_plan.process_meals(db)
meal_plan.update(db, plan_id)
# try:
# meal_plan.process_meals()
# meal_plan.update(plan_id)
# except:
# raise HTTPException(
# status_code=404,
# detail=SnackResponse.error("Unable to Update Mealplan"),
# )
return SnackResponse.info("Mealplan Updated")
return SnackResponse.success("Mealplan Updated")
@router.delete("/{plan_id}")
def delete_meal_plan(plan_id, session: Session = Depends(generate_session)):
def delete_meal_plan(plan_id, db: Session = Depends(generate_session)):
""" Removes a meal plan from the database """
MealPlan.delete(session, plan_id)
MealPlan.delete(db, plan_id)
return SnackResponse.error("Mealplan Deleted")
return SnackResponse.success("Mealplan Deleted")
@router.get("/today", tags=["Meal Plan"])
def get_today(session: Session = Depends(generate_session)):
def get_today(db: Session = Depends(generate_session)):
"""
Returns the recipe slug for the meal scheduled for today.
If no meal is scheduled nothing is returned
"""
return MealPlan.today(session)
return MealPlan.today(db)

View File

@@ -37,14 +37,14 @@ def get_avaiable_nextcloud_imports():
@router.post("/{type}/{file_name}/import")
def import_nextcloud_directory(
type: str, file_name: str, session: Session = Depends(generate_session)
type: str, file_name: str, db: Session = Depends(generate_session)
):
""" Imports all the recipes in a given directory """
file_path = MIGRATION_DIR.joinpath(type, file_name)
if type == "nextcloud":
return nextcloud_migrate(session, file_path)
return nextcloud_migrate(db, file_path)
elif type == "chowdown":
return chowdow_migrate(session, file_path)
return chowdow_migrate(db, file_path)
else:
return SnackResponse.error("Incorrect Migration Type Selected")
@@ -62,7 +62,7 @@ def delete_migration_data(type: str, file_name: str):
else:
SnackResponse.error("File/Folder not found.")
return SnackResponse.error(f"Migration Data Remove: {remove_path.absolute()}")
return SnackResponse.info(f"Migration Data Remove: {remove_path.absolute()}")
@router.post("/{type}/upload")

View File

@@ -4,8 +4,6 @@ from fastapi import APIRouter, Depends
from models.category_models import RecipeCategoryResponse
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter(
prefix="/api/categories",
tags=["Recipe Categories"],
@@ -35,5 +33,3 @@ async def delete_recipe_category(
from any recipes that contain it """
db.categories.delete(session, category)
return SnackResponse(f"Category Deleted: {category}")

View File

@@ -62,11 +62,11 @@ def delete_recipe(recipe_slug: str, db: Session = Depends(generate_session)):
status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")
)
return SnackResponse.error(f"Recipe {recipe_slug} Deleted")
return SnackResponse.success("Recipe Deleted")
@router.get("/{recipe_slug}/image")
async def get_recipe_img(recipe_slug: str):
def get_recipe_img(recipe_slug: str):
""" Takes in a recipe slug, returns the static image """
recipe_image = read_image(recipe_slug)
@@ -75,13 +75,10 @@ async def get_recipe_img(recipe_slug: str):
@router.put("/{recipe_slug}/image")
def update_recipe_image(
recipe_slug: str,
image: bytes = File(...),
extension: str = Form(...),
session: Session = Depends(generate_session),
recipe_slug: str, image: bytes = File(...), extension: str = Form(...)
):
""" Removes an existing image and replaces it with the incoming file. """
response = write_image(recipe_slug, image, extension)
Recipe.update_image(session, recipe_slug, extension)
Recipe.update_image(recipe_slug, extension)
return response

View File

@@ -3,8 +3,6 @@ from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter(tags=["Recipes"])
router = APIRouter(
@@ -32,5 +30,3 @@ async def delete_recipe_tag(tag: str, session: Session = Depends(generate_sessio
from any recipes that contain it"""
db.tags.delete(session, tag)
return SnackResponse.error(f"Tag Deleted: {tag}")

View File

@@ -1,8 +1,6 @@
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from models.settings_models import SiteSettings
from services.settings_services import default_settings_init
from services.settings_services import SiteSettings
from sqlalchemy.orm.session import Session
from utils.post_webhooks import post_webhooks
from utils.snackbar import SnackResponse
@@ -11,24 +9,10 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
@router.get("")
def get_main_settings(session: Session = Depends(generate_session)):
def get_main_settings(db: Session = Depends(generate_session)):
""" Returns basic site settings """
try:
data = db.settings.get(session, "main")
except:
default_settings_init(session)
data = db.settings.get(session, "main")
return data
@router.put("")
def update_settings(data: SiteSettings, session: Session = Depends(generate_session)):
""" Returns Site Settings """
db.settings.update(session, "main", data.dict())
return SnackResponse.success("Settings Updated")
return SiteSettings.get_site_settings(db)
@router.post("/webhooks/test")
@@ -36,3 +20,20 @@ def test_webhooks():
""" Run the function to test your webhooks """
return post_webhooks()
@router.put("")
def update_settings(data: SiteSettings, db: Session = Depends(generate_session)):
""" Returns Site Settings """
data.update(db)
# try:
# data.update()
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
# )
return SnackResponse.success("Settings Updated")

View File

@@ -15,10 +15,10 @@ def facivon():
@router.get("/")
async def root():
def root():
return FileResponse(BASE_HTML)
@router.get("/{full_path:path}")
async def root_plus(full_path):
def root_plus(full_path):
return FileResponse(BASE_HTML)

View File

@@ -1,47 +1,64 @@
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from models.theme_models import SiteTheme
from services.settings_services import SiteTheme
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
from db.database import db
router = APIRouter(prefix="/api", tags=["Themes"])
@router.get("/themes")
def get_all_themes(session: Session = Depends(generate_session)):
def get_all_themes(db: Session = Depends(generate_session)):
""" Returns all site themes """
return db.themes.get_all(session)
return SiteTheme.get_all(db)
@router.post("/themes/create")
def create_theme(data: SiteTheme, session: Session = Depends(generate_session)):
def create_theme(data: SiteTheme, db: Session = Depends(generate_session)):
""" Creates a site color theme database entry """
db.themes.create(session, data.dict())
data.save_to_db(db)
# try:
# data.save_to_db()
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Save Theme")
# )
return SnackResponse.success("Theme Saved")
@router.get("/themes/{theme_name}")
def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
def get_single_theme(theme_name: str, db: Session = Depends(generate_session)):
""" Returns a named theme """
return db.themes.get(session, theme_name)
return SiteTheme.get_by_name(db, theme_name)
@router.put("/themes/{theme_name}")
def update_theme(
theme_name: str, data: SiteTheme, session: Session = Depends(generate_session)
theme_name: str, data: SiteTheme, db: Session = Depends(generate_session)
):
""" Update a theme database entry """
db.themes.update(session, theme_name, data.dict())
data.update_document(db)
return SnackResponse.info(f"Theme Updated: {theme_name}")
# try:
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Update Theme")
# )
return SnackResponse.success("Theme Updated")
@router.delete("/themes/{theme_name}")
def delete_theme(theme_name: str, session: Session = Depends(generate_session)):
def delete_theme(theme_name: str, db: Session = Depends(generate_session)):
""" Deletes theme from the database """
db.themes.delete(session, theme_name)
SiteTheme.delete_theme(db, theme_name)
# try:
# SiteTheme.delete_theme(theme_name)
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Delete Theme")
# )
return SnackResponse.error(f"Theme Deleted: {theme_name}")
return SnackResponse.success("Theme Deleted")

View File

@@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm
# from fastapi_login import LoginManager
# from fastapi_login.exceptions import InvalidCredentialsException
router = APIRouter()
# SECRET = "876cfb59db03d9c17cefec967b00255d3f7d93a823e5dc2a"
# manager = LoginManager(SECRET, tokenUrl="/api/auth/token")
# fake_db = {"johndoe@e.mail": {"password": "hunter2"}}
# @manager.user_loader
# def load_user(email: str): # could also be an asynchronous function
# user = fake_db.get(email)
# return user
# @router.post("/api/auth/token", tags=["User Gen"])
# def login(data: OAuth2PasswordRequestForm = Depends()):
# email = data.username
# password = data.password
# user = load_user(email) # we are using the same function to retrieve the user
# if not user:
# raise InvalidCredentialsException # you can also use your own HTTPException
# elif password != user["password"]:
# raise InvalidCredentialsException
# access_token = manager.create_access_token(data=dict(sub=email))
# return {"access_token": access_token, "token_type": "bearer"}

5
mealie/run.sh Normal file
View File

@@ -0,0 +1,5 @@
## Run Migration
## Start Application
uvicorn app:app --host 0.0.0.0 --port 80

View File

@@ -4,11 +4,11 @@ from datetime import datetime
from pathlib import Path
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from db.database import db
from db.db_setup import create_session
from jinja2 import Template
from services.meal_services import MealPlan
from services.recipe_services import Recipe
from services.settings_services import SiteSettings, SiteTheme
from utils.logger import logger
@@ -88,18 +88,20 @@ class ExportDatabase:
shutil.copy(file, self.img_dir.joinpath(file.name))
def export_settings(self):
all_settings = db.settings.get(self.session, "main")
all_settings = SiteSettings.get_site_settings(self.session)
out_file = self.settings_dir.joinpath("settings.json")
ExportDatabase._write_json_file(all_settings, out_file)
ExportDatabase._write_json_file(all_settings.dict(), out_file)
def export_themes(self):
all_themes = db.themes.get_all(self.session)
all_themes = SiteTheme.get_all(self.session)
if all_themes:
all_themes = [x.dict() for x in all_themes]
out_file = self.themes_dir.joinpath("themes.json")
ExportDatabase._write_json_file(all_themes, out_file)
def export_meals(self):
#! Problem Parseing Datetime Objects... May come back to this
def export_meals(
self,
): #! Problem Parseing Datetime Objects... May come back to this
meal_plans = MealPlan.get_all(self.session)
if meal_plans:
meal_plans = [x.dict() for x in meal_plans]
@@ -108,7 +110,7 @@ class ExportDatabase:
ExportDatabase._write_json_file(meal_plans, out_file)
@staticmethod
def _write_json_file(data: dict, out_file: Path):
def _write_json_file(data, out_file: Path):
json_data = json.dumps(data, indent=4, default=str)
with open(out_file, "w") as f:

View File

@@ -1,15 +1,12 @@
import json
import shutil
import zipfile
from logging import error
from pathlib import Path
from typing import List
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from db.database import db
from models.theme_models import SiteTheme
from services.recipe_services import Recipe
from services.settings_services import SiteSettings
from services.settings_services import SiteSettings, SiteTheme
from sqlalchemy.orm.session import Session
from utils.logger import logger
@@ -57,7 +54,6 @@ class ImportDatabase:
raise Exception("Import file does not exist")
def run(self):
report = {}
if self.imp_recipes:
report = self.import_recipes()
if self.imp_settings:
@@ -99,24 +95,9 @@ class ImportDatabase:
del recipe_dict["_id"]
del recipe_dict["dateAdded"]
except:
pass
logger.info("Detected new backup Schema, skipping migration...")
return recipe_dict
# Migration from list to Object Type Data
try:
if "" in recipe_dict["tags"]:
recipe_dict["tags"] = [
tag for tag in recipe_dict["tags"] if not tag == ""
]
except:
pass
try:
if "" in recipe_dict["categories"]:
recipe_dict["categories"] = [
cat for cat in recipe_dict["categories"] if not cat == ""
]
except:
pass
if type(recipe_dict["extras"]) == list:
recipe_dict["extras"] = {}
@@ -132,13 +113,11 @@ class ImportDatabase:
themes_file = self.import_dir.joinpath("themes", "themes.json")
with open(themes_file, "r") as f:
themes: list[dict] = json.loads(f.read())
themes: list = json.loads(f.read())
for theme in themes:
if theme.get("name") == "default":
continue
new_theme = SiteTheme(**theme)
try:
db.themes.create(self.session, new_theme.dict())
new_theme.save_to_db(self.session)
except:
logger.info(f"Unable Import Theme {new_theme.name}")
@@ -148,7 +127,9 @@ class ImportDatabase:
with open(settings_file, "r") as f:
settings: dict = json.loads(f.read())
db.settings.update(self.session, settings.get("name"), settings)
settings = SiteSettings(**settings)
settings.update(self.session)
def clean_up(self):
shutil.rmtree(TEMP_DIR)

View File

@@ -8,6 +8,19 @@ from sqlalchemy.orm.session import Session
from services.recipe_services import Recipe
CWD = Path(__file__).parent
THIS_WEEK = CWD.parent.joinpath("data", "meal_plan", "this_week.json")
NEXT_WEEK = CWD.parent.joinpath("data", "meal_plan", "next_week.json")
WEEKDAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
class Meal(BaseModel):
slug: Optional[str]
@@ -68,7 +81,7 @@ class MealPlan(BaseModel):
self.meals = meals
def save_to_db(self, session: Session):
db.meals.create(session, self.dict())
db.meals.save_new(session, self.dict())
@staticmethod
def get_all(session: Session) -> List:

View File

@@ -1,4 +1,5 @@
import datetime
import json
from pathlib import Path
from typing import Any, List, Optional
@@ -97,7 +98,13 @@ class Recipe(BaseModel):
except:
recipe_dict["image"] = "no image"
recipe_doc = db.recipes.create(session, recipe_dict)
# try:
# total_time = recipe_dict.get("totalTime")
# recipe_dict["totalTime"] = str(total_time)
# except:
# pass
recipe_doc = db.recipes.save_new(session, recipe_dict)
recipe = Recipe(**recipe_doc)
return recipe.slug
@@ -115,7 +122,7 @@ class Recipe(BaseModel):
return updated_slug.get("slug")
@staticmethod
def update_image(session: Session, slug: str, extension: str = None) -> str:
def update_image(slug: str, extension: str) -> str:
"""A helper function to pass the new image name and extension
into the database.
@@ -123,8 +130,11 @@ class Recipe(BaseModel):
slug (str): The current recipe slug
extension (str): the file extension of the new image
"""
return db.recipes.update_image(session, slug, extension)
return db.recipes.update_image(slug, extension)
@staticmethod
def get_all(session: Session):
return db.recipes.get_all(session)

View File

@@ -3,9 +3,8 @@ from db.db_setup import create_session
from services.backups.exports import auto_backup_job
from services.scheduler.global_scheduler import scheduler
from services.scheduler.scheduler_utils import Cron, cron_parser
from services.settings_services import SiteSettings
from utils.logger import logger
from models.settings_models import SiteSettings
from db.database import db
from utils.post_webhooks import post_webhooks
@@ -16,8 +15,7 @@ def update_webhook_schedule():
poll the database for changes and reschedule the webhook time
"""
session = create_session()
settings = db.settings.get(session, "main")
settings = SiteSettings(**settings)
settings = SiteSettings.get_site_settings(session=session)
time = cron_parser(settings.webhooks.webhookTime)
job = JOB_STORE.get("webhooks")

View File

@@ -14,7 +14,7 @@ from w3lib.html import get_base_url
from services.image_services import scrape_image
from services.recipe_services import Recipe
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
TEMP_FILE = DEBUG_DIR.joinpath("last_recipe.json")
def cleanhtml(raw_html):
@@ -121,7 +121,6 @@ def process_recipe_data(new_recipe: dict, url=None) -> dict:
def extract_recipe_from_html(html: str, url: str) -> dict:
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html, python_objects=True)
dump_last_json(scraped_recipes)
if not scraped_recipes:
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(
@@ -165,11 +164,7 @@ def og_fields(properties: List[Tuple[str, str]], field_name: str) -> List[str]:
def basic_recipe_from_opengraph(html: str, url: str) -> dict:
base_url = get_base_url(html, url)
data = extruct.extract(html, base_url=base_url)
try:
properties = data["opengraph"][0]["properties"]
except:
return
properties = data["opengraph"][0]["properties"]
return {
"name": og_field(properties, "og:title"),
"description": og_field(properties, "og:description"),
@@ -189,13 +184,6 @@ def basic_recipe_from_opengraph(html: str, url: str) -> dict:
}
def dump_last_json(recipe_data: dict):
with open(LAST_JSON, "w") as f:
f.write(json.dumps(recipe_data, indent=4, default=str))
return
def process_recipe_url(url: str) -> dict:
r = requests.get(url)
new_recipe = extract_recipe_from_html(r.text, url)
@@ -206,6 +194,9 @@ def process_recipe_url(url: str) -> dict:
def create_from_url(url: str) -> Recipe:
recipe_data = process_recipe_url(url)
with open(TEMP_FILE, "w") as f:
f.write(json.dumps(recipe_data, indent=4, default=str))
recipe = Recipe(**recipe_data)
return recipe

View File

@@ -1,16 +1,149 @@
from typing import List, Optional
from db.database import db
from db.db_setup import create_session, sql_exists
from models.settings_models import SiteSettings, Webhooks
from pydantic import BaseModel
from sqlalchemy.orm.session import Session
from utils.logger import logger
def default_settings_init(session: Session = None):
if session == None:
session = create_session()
class Webhooks(BaseModel):
webhookTime: str = "00:00"
webhookURLs: Optional[List[str]] = []
enabled: bool = False
class SiteSettings(BaseModel):
name: str = "main"
webhooks: Webhooks
class Config:
schema_extra = {
"example": {
"name": "main",
"webhooks": {
"webhookTime": "00:00",
"webhookURLs": ["https://mywebhookurl.com/webhook"],
"enable": False,
},
}
}
@staticmethod
def get_all(session: Session):
db.settings.get_all(session)
@classmethod
def get_site_settings(cls, session: Session):
try:
document = db.settings.get(session=session, match_value="main")
except:
webhooks = Webhooks()
default_entry = SiteSettings(name="main", webhooks=webhooks)
document = db.settings.save_new(
session, default_entry.dict(), webhooks.dict()
)
return cls(**document)
def update(self, session: Session):
db.settings.update(session, "main", new_data=self.dict())
class Colors(BaseModel):
primary: str
accent: str
secondary: str
success: str
info: str
warning: str
error: str
class SiteTheme(BaseModel):
name: str
colors: Colors
class Config:
schema_extra = {
"example": {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
}
@classmethod
def get_by_name(cls, session: Session, theme_name):
db_entry = db.themes.get(session, theme_name)
name = db_entry.get("name")
colors = Colors(**db_entry.get("colors"))
return cls(name=name, colors=colors)
@staticmethod
def get_all(session: Session):
all_themes = db.themes.get_all(session)
for index, theme in enumerate(all_themes):
name = theme.get("name")
colors = Colors(**theme.get("colors"))
all_themes[index] = SiteTheme(name=name, colors=colors)
return all_themes
def save_to_db(self, session: Session):
db.themes.save_new(session, self.dict())
def update_document(self, session: Session):
db.themes.update(session, self.name, self.dict())
@staticmethod
def delete_theme(session: Session, theme_name: str) -> str:
""" Removes the theme by name """
db.themes.delete(session, theme_name)
def default_theme_init():
default_colors = {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
}
session = create_session()
try:
SiteTheme.get_by_name(session, "default")
logger.info("Default theme exists... skipping generation")
except:
logger.info("Generating Default Theme")
colors = Colors(**default_colors)
default_theme = SiteTheme(name="default", colors=colors)
default_theme.save_to_db(session)
def default_settings_init():
session = create_session()
try:
document = db.settings.get(session, "main")
except:
webhooks = Webhooks()
default_entry = SiteSettings(name="main", webhooks=webhooks)
document = db.settings.create(session, default_entry.dict(), webhooks.dict())
except:
pass
document = db.settings.save_new(session, default_entry.dict(), webhooks.dict())
session.close()
if not sql_exists:
default_settings_init()
default_theme_init()

View File

@@ -1,28 +0,0 @@
from db.database import db
from db.db_setup import create_session, sql_exists
from utils.logger import logger
def default_theme_init():
default_theme = {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
session = create_session()
try:
db.themes.create(session, default_theme)
logger.info("Generating default theme...")
except:
logger.info("Default Theme Exists.. skipping generation")
if not sql_exists:
default_theme_init()

View File

@@ -5,8 +5,6 @@ from app_config import SQLITE_DIR
from db.db_setup import generate_session, sql_global_init
from fastapi.testclient import TestClient
from pytest import fixture
from services.settings_services import default_settings_init
from services.theme_services import default_theme_init
from tests.test_config import TEST_DATA
@@ -20,13 +18,13 @@ TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
def override_get_db():
try:
db = TestSessionLocal()
default_theme_init()
default_settings_init()
yield db
finally:
db.close()
@fixture(scope="session")
def api_client():

View File

View File

@@ -0,0 +1,99 @@
import json
import re
from pathlib import Path
import pytest
from services.scrape_services import (
extract_recipe_from_html,
normalize_data,
normalize_instructions,
)
CWD = Path(__file__).parent
RAW_RECIPE_DIR = CWD.parent.joinpath("data", "recipes-raw")
RAW_HTML_DIR = CWD.parent.joinpath("data", "html-raw")
# https://github.com/django/django/blob/stable/1.3.x/django/core/validators.py#L45
url_validation_regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
r"localhost|" # localhost...
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
@pytest.mark.parametrize(
"json_file,num_steps",
[
("best-homemade-salsa-recipe.json", 2),
(
"blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2.json",
3,
),
("bon_appetit.json", 8),
("chunky-apple-cake.json", 4),
("dairy-free-impossible-pumpkin-pie.json", 7),
("how-to-make-instant-pot-spaghetti.json", 8),
("instant-pot-chicken-and-potatoes.json", 4),
("instant-pot-kerala-vegetable-stew.json", 13),
("jalapeno-popper-dip.json", 4),
("microwave_sweet_potatoes_04783.json", 4),
("moroccan-skirt-steak-with-roasted-pepper-couscous.json", 4),
("Pizza-Knoblauch-Champignon-Paprika-vegan.html.json", 3),
],
)
def test_normalize_data(json_file, num_steps):
recipe_data = normalize_data(json.load(open(RAW_RECIPE_DIR.joinpath(json_file))))
assert len(recipe_data["recipeInstructions"]) == num_steps
@pytest.mark.parametrize(
"instructions",
[
"A\n\nB\n\nC\n\n",
"A\nB\nC\n",
"A\r\n\r\nB\r\n\r\nC\r\n\r\n",
"A\r\nB\r\nC\r\n",
["A", "B", "C"],
[{"@type": "HowToStep", "text": x} for x in ["A", "B", "C"]],
],
)
def test_normalize_instructions(instructions):
assert normalize_instructions(instructions) == [
{"text": "A"},
{"text": "B"},
{"text": "C"},
]
def test_html_no_recipe_data():
path = RAW_HTML_DIR.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
assert recipe_data["recipeInstructions"] == [
{"text": "Could not detect instructions"}
]
def test_html_with_recipe_data():
path = RAW_HTML_DIR.joinpath("healthy_pasta_bake_60759.html")
url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759"
recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert len(recipe_data["recipeIngredient"]) == 13
assert len(recipe_data["recipeInstructions"]) == 4

View File

@@ -32,7 +32,6 @@ def default_theme(api_client):
"error": "#EF5350",
},
}
api_client.post(THEMES_CREATE, json=default_theme)
return default_theme

View File

@@ -65,20 +65,20 @@ def test_normalize_instructions(instructions):
]
# def test_html_no_recipe_data(): #! Unsure why it's failing, code didn't change?
# path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
# url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
# recipe_data = extract_recipe_from_html(open(path).read(), url)
def test_html_no_recipe_data():
path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
recipe_data = extract_recipe_from_html(open(path).read(), url)
# assert len(recipe_data["name"]) > 10
# assert len(recipe_data["slug"]) > 10
# assert recipe_data["orgURL"] == url
# assert len(recipe_data["description"]) > 100
# assert url_validation_regex.match(recipe_data["image"])
# assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
# assert recipe_data["recipeInstructions"] == [
# {"text": "Could not detect instructions"}
# ]
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
assert recipe_data["recipeInstructions"] == [
{"text": "Could not detect instructions"}
]
def test_html_with_recipe_data():

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