mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-23 22:48:20 -05:00
Compare commits
477 Commits
v2.4.0
...
reset-scro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e16a16cd8 | ||
|
|
78a0f74f33 | ||
|
|
8b9e80358b | ||
|
|
2bae6e9d02 | ||
|
|
6b98a7cd74 | ||
|
|
e0238eb3a2 | ||
|
|
5adb7662c4 | ||
|
|
4e6a7a09ff | ||
|
|
719c7c9f6b | ||
|
|
7331007f30 | ||
|
|
ea329a6b71 | ||
|
|
e1a04ba673 | ||
|
|
63a4d4c801 | ||
|
|
5cf3e2565a | ||
|
|
9e1fe618ba | ||
|
|
691300e481 | ||
|
|
939588f54c | ||
|
|
2d8f491666 | ||
|
|
50754ad012 | ||
|
|
04eca1b992 | ||
|
|
aad7dc1abd | ||
|
|
2f19d31d1b | ||
|
|
095b92c29a | ||
|
|
49c704a4b1 | ||
|
|
c15a4f786b | ||
|
|
6e33878e4f | ||
|
|
5ca004802d | ||
|
|
68115cbf2f | ||
|
|
2b4bc8a662 | ||
|
|
fc801c9da4 | ||
|
|
f99b305dc3 | ||
|
|
b0b3d7e5e5 | ||
|
|
eedd2204a6 | ||
|
|
1ccc67774a | ||
|
|
6d98041ec8 | ||
|
|
c24cfb8096 | ||
|
|
ca41bc8d5c | ||
|
|
da3271f33f | ||
|
|
50a986f331 | ||
|
|
f72ebed0dc | ||
|
|
0c534ad9d4 | ||
|
|
9cce0f65aa | ||
|
|
c9e22892a6 | ||
|
|
e794c6b525 | ||
|
|
abc37f258d | ||
|
|
c2fda0d85a | ||
|
|
437a6ae526 | ||
|
|
9f5de0bd5d | ||
|
|
4bf963b14c | ||
|
|
7092d85a53 | ||
|
|
83fd320920 | ||
|
|
28e2666c17 | ||
|
|
62c7e2d2fb | ||
|
|
6540bfacfe | ||
|
|
47eb1ebbb1 | ||
|
|
31f90c79c0 | ||
|
|
3b1edf67fc | ||
|
|
781bbecc7b | ||
|
|
15f06b5378 | ||
|
|
95fa0af28a | ||
|
|
084f99b0de | ||
|
|
2fb5dac966 | ||
|
|
51ec02bdb2 | ||
|
|
1a1fe0a442 | ||
|
|
b0b88d361f | ||
|
|
b4a9c472e5 | ||
|
|
bcc038091a | ||
|
|
9e0db03f8c | ||
|
|
af274bf476 | ||
|
|
ca9d5677b8 | ||
|
|
07483a13ff | ||
|
|
d412271b0b | ||
|
|
cea3ddc883 | ||
|
|
c965d12bf1 | ||
|
|
181aebf424 | ||
|
|
b77ff9c341 | ||
|
|
93cec24f26 | ||
|
|
a2a0ad1af0 | ||
|
|
969a3c9005 | ||
|
|
a09601f051 | ||
|
|
d6110f1a94 | ||
|
|
1562437b98 | ||
|
|
e2eb754cf2 | ||
|
|
3a4222c6c1 | ||
|
|
2673834a9f | ||
|
|
c24d532608 | ||
|
|
89ab7fac25 | ||
|
|
78b55c0b98 | ||
|
|
ac984a2d04 | ||
|
|
079cfe7fe0 | ||
|
|
4a9095fcbb | ||
|
|
384bb7480f | ||
|
|
69488bd6df | ||
|
|
038fbd38ef | ||
|
|
1697d6299e | ||
|
|
b87edc823a | ||
|
|
cacb197aa8 | ||
|
|
5d58c93331 | ||
|
|
104c9b36a5 | ||
|
|
b68c96c348 | ||
|
|
b577cf5520 | ||
|
|
431638c1ed | ||
|
|
a4871b65eb | ||
|
|
582974b265 | ||
|
|
22fdb32f61 | ||
|
|
649013a028 | ||
|
|
14de1410ae | ||
|
|
03bc87d3a8 | ||
|
|
bb7885543e | ||
|
|
404a4cfa9d | ||
|
|
63a5c0076a | ||
|
|
a4ea5ba10d | ||
|
|
fc6b239343 | ||
|
|
9185cd8df1 | ||
|
|
f0a9d5333d | ||
|
|
7bb84d504a | ||
|
|
dad2712fe9 | ||
|
|
8e7e3e21ed | ||
|
|
af3057951d | ||
|
|
2f3ef738c4 | ||
|
|
44ee1440e2 | ||
|
|
c4aaf1a8c3 | ||
|
|
e093a93189 | ||
|
|
51c92a1e35 | ||
|
|
84629c540e | ||
|
|
28b3ba6506 | ||
|
|
a6ce140e60 | ||
|
|
4784672113 | ||
|
|
9db31ca125 | ||
|
|
972b588250 | ||
|
|
57ae31d231 | ||
|
|
7398b2784a | ||
|
|
c13c0868ae | ||
|
|
a652830a26 | ||
|
|
1f34571820 | ||
|
|
4e16273f00 | ||
|
|
d110f21d37 | ||
|
|
6caa74254f | ||
|
|
66bc4c25ec | ||
|
|
89bed4d675 | ||
|
|
25fbdd6523 | ||
|
|
7e64ce2767 | ||
|
|
62dabe2c18 | ||
|
|
3742c4e86c | ||
|
|
98da2cadc6 | ||
|
|
8360829f61 | ||
|
|
aec38e367b | ||
|
|
6ad7009509 | ||
|
|
46505ba8a5 | ||
|
|
4011d6e29b | ||
|
|
7ee7b753d6 | ||
|
|
c77f41d08e | ||
|
|
ab7fa150fe | ||
|
|
22fa5d27e3 | ||
|
|
5f05002c20 | ||
|
|
0cd33de2f6 | ||
|
|
e46d19edfe | ||
|
|
18ff3c3c48 | ||
|
|
da1c9a448e | ||
|
|
58e1f71711 | ||
|
|
918899d346 | ||
|
|
7f57e1d9a2 | ||
|
|
df6dc6c8ac | ||
|
|
840bd32ee3 | ||
|
|
da3d056d81 | ||
|
|
b3ea48333c | ||
|
|
f37b39aad2 | ||
|
|
d4c987e48a | ||
|
|
955e38ea0b | ||
|
|
7d87182b1a | ||
|
|
5e80002297 | ||
|
|
1364cd0d6b | ||
|
|
5d21af0e02 | ||
|
|
64afccb36c | ||
|
|
5b0497e14e | ||
|
|
5010bb5665 | ||
|
|
c7789da1ad | ||
|
|
b853ce221d | ||
|
|
3522f81025 | ||
|
|
a22c0c4787 | ||
|
|
4dfc5ead54 | ||
|
|
c667bda427 | ||
|
|
188b129da4 | ||
|
|
6845b51def | ||
|
|
c8ec19e371 | ||
|
|
c9002d2391 | ||
|
|
0ba4cc4d4c | ||
|
|
5baade58fb | ||
|
|
e667fe8a5e | ||
|
|
dc1ec4e69a | ||
|
|
55af4082e7 | ||
|
|
8b059121d1 | ||
|
|
5bf3ba0cc2 | ||
|
|
d4a1c7f756 | ||
|
|
a06046cf5d | ||
|
|
f8c4112c39 | ||
|
|
e118d24261 | ||
|
|
8f3772ed01 | ||
|
|
007d249c20 | ||
|
|
24be42ee88 | ||
|
|
0d605e20fc | ||
|
|
cbfb649d96 | ||
|
|
b1341b9102 | ||
|
|
6be67a1a98 | ||
|
|
62d2dd1c0d | ||
|
|
5c890f3d0e | ||
|
|
d071215f06 | ||
|
|
e5b2ef49b2 | ||
|
|
766968b97d | ||
|
|
ec0e31f8ec | ||
|
|
c911a3190e | ||
|
|
c4baf50ae3 | ||
|
|
30382b36cb | ||
|
|
6b181c122f | ||
|
|
54bb39af55 | ||
|
|
b994d27b0c | ||
|
|
07cd98c125 | ||
|
|
1c6b35a53c | ||
|
|
7c05d58f26 | ||
|
|
6ecba01eb6 | ||
|
|
a39f8cdb90 | ||
|
|
18ebc3de5f | ||
|
|
ead4d4c95e | ||
|
|
6ae4e67c84 | ||
|
|
e77247441c | ||
|
|
d55e48cbe0 | ||
|
|
94170e3e6c | ||
|
|
e0e619df5a | ||
|
|
8469aae7ab | ||
|
|
94dd6eab81 | ||
|
|
8c25bdb62d | ||
|
|
cbbc07cda9 | ||
|
|
6fc9ece191 | ||
|
|
cd6ccf099b | ||
|
|
4f7ee33f1b | ||
|
|
9bfee56bd5 | ||
|
|
e5da33e38e | ||
|
|
2748db781f | ||
|
|
d9bbf8de30 | ||
|
|
c29f651a36 | ||
|
|
5e217fc269 | ||
|
|
0f58ac5b47 | ||
|
|
2deb9c276c | ||
|
|
f46760755d | ||
|
|
eca2ba36c8 | ||
|
|
cdd8e3aca9 | ||
|
|
d724f408cc | ||
|
|
3b1a6280d6 | ||
|
|
974d848ee2 | ||
|
|
72668e2881 | ||
|
|
9e47ade475 | ||
|
|
ad59e653da | ||
|
|
4ecfd8ec78 | ||
|
|
e254dda368 | ||
|
|
7de47004e9 | ||
|
|
28cc6b8d1e | ||
|
|
21278cd7fe | ||
|
|
9a469fe4fd | ||
|
|
98472ff471 | ||
|
|
e2b5f4d08c | ||
|
|
232ad8410c | ||
|
|
c65bd14d74 | ||
|
|
774b3123a2 | ||
|
|
40818722ab | ||
|
|
9cf40f89ea | ||
|
|
a758406719 | ||
|
|
8b3ff9b099 | ||
|
|
c4b26fef8c | ||
|
|
ba8b94232a | ||
|
|
5f766a8c3f | ||
|
|
46d28bd96b | ||
|
|
d483da6c4c | ||
|
|
a6fd50b1ba | ||
|
|
000ec9475a | ||
|
|
25adfe1a48 | ||
|
|
6381ac4c7f | ||
|
|
c636a4f73e | ||
|
|
38ab8aa48d | ||
|
|
17f64a5cfa | ||
|
|
d11bdaf235 | ||
|
|
cfba2fff7e | ||
|
|
61ae6b3e32 | ||
|
|
9c4afb57b8 | ||
|
|
b12aea8272 | ||
|
|
ec1a9d78ac | ||
|
|
8250e793b8 | ||
|
|
f3310ddba6 | ||
|
|
d573a9ea5d | ||
|
|
d24a518bac | ||
|
|
46b821d832 | ||
|
|
637bb30e13 | ||
|
|
b930ebfb20 | ||
|
|
5e2c40731c | ||
|
|
54ae810acc | ||
|
|
716c85cc3b | ||
|
|
3d1b76bcad | ||
|
|
4843a9a74a | ||
|
|
12aec943dc | ||
|
|
3b0d6050a2 | ||
|
|
3fd3661206 | ||
|
|
df8dd3fe4a | ||
|
|
a2c6b3f69b | ||
|
|
c01593e918 | ||
|
|
48484e5b1a | ||
|
|
173e8792a6 | ||
|
|
28047d9b58 | ||
|
|
82393b0cd1 | ||
|
|
eea9a6ae16 | ||
|
|
ca05c25b61 | ||
|
|
af912ebefb | ||
|
|
e6b46b21d9 | ||
|
|
a41f8b31f1 | ||
|
|
6271b33b1b | ||
|
|
09234e3bf0 | ||
|
|
9f467b702e | ||
|
|
6c156e0e14 | ||
|
|
10818ab0ba | ||
|
|
0778919134 | ||
|
|
fb8746e7b8 | ||
|
|
c82d08c0d9 | ||
|
|
be1dc69be6 | ||
|
|
8ea932ef7c | ||
|
|
70a6bc4769 | ||
|
|
c765401ac5 | ||
|
|
3b12a62fc6 | ||
|
|
c351cf7bd5 | ||
|
|
aea5eb3419 | ||
|
|
cb9008bb5c | ||
|
|
3534e445d8 | ||
|
|
c0ab7673ba | ||
|
|
abf73e08ec | ||
|
|
c4df8f0611 | ||
|
|
b28eefab77 | ||
|
|
6f3a139efd | ||
|
|
790f4a9b9a | ||
|
|
c7c87068bf | ||
|
|
f48dafd855 | ||
|
|
273f628acd | ||
|
|
a8653ea904 | ||
|
|
0093627adb | ||
|
|
2d73c703cb | ||
|
|
10315fe8f7 | ||
|
|
1f7d5a57af | ||
|
|
6d22df7b95 | ||
|
|
f5ddcdf193 | ||
|
|
36f6917308 | ||
|
|
53b5df92ae | ||
|
|
736352af5f | ||
|
|
41940b8003 | ||
|
|
641d24aa1e | ||
|
|
fb08a11ffe | ||
|
|
ba26378abc | ||
|
|
2a4b0d3d8b | ||
|
|
af4b3fa83d | ||
|
|
564012c53d | ||
|
|
2316a3fef9 | ||
|
|
a52fda72d6 | ||
|
|
cb05adeb48 | ||
|
|
f2eadd2908 | ||
|
|
4229061377 | ||
|
|
970ed1da7b | ||
|
|
dd9d9b85c1 | ||
|
|
6001d679e7 | ||
|
|
02cf53340b | ||
|
|
95042f1d40 | ||
|
|
34245687a5 | ||
|
|
7e5c750231 | ||
|
|
93c2df41c3 | ||
|
|
1e69577d12 | ||
|
|
cbafc28fa1 | ||
|
|
8622e19502 | ||
|
|
2fee1778ed | ||
|
|
2284ecb226 | ||
|
|
2716260473 | ||
|
|
0b89ab1a95 | ||
|
|
d2ced50fcc | ||
|
|
bf616f9db5 | ||
|
|
8cd2da0abb | ||
|
|
c74ba0eca1 | ||
|
|
17bed60399 | ||
|
|
7a2f91eb32 | ||
|
|
9b36d08948 | ||
|
|
5105c42abe | ||
|
|
b040778c53 | ||
|
|
0d96ec3858 | ||
|
|
c096605fc9 | ||
|
|
96615b17c9 | ||
|
|
689f31a4da | ||
|
|
f89e038769 | ||
|
|
c979df1372 | ||
|
|
2b124e7951 | ||
|
|
07bbda808f | ||
|
|
04eeaa5acf | ||
|
|
6097ff2a34 | ||
|
|
b5dfaf12f1 | ||
|
|
b8266db218 | ||
|
|
67fe739b52 | ||
|
|
203218a3d5 | ||
|
|
4b992afc67 | ||
|
|
376c14be9a | ||
|
|
5e734548a2 | ||
|
|
aed93ce1f4 | ||
|
|
d7f460be0f | ||
|
|
e9892aba89 | ||
|
|
e565b919df | ||
|
|
2c2de1e95b | ||
|
|
ea0bec2336 | ||
|
|
7f99e2fc36 | ||
|
|
2e13f33eb1 | ||
|
|
dde93e78c9 | ||
|
|
e50299e7ce | ||
|
|
2be4bd1f7c | ||
|
|
398b41ed23 | ||
|
|
25bd742903 | ||
|
|
9918d36a1a | ||
|
|
357e45c264 | ||
|
|
bfa3a3e826 | ||
|
|
b45f54150b | ||
|
|
b5e1debabc | ||
|
|
406ff384c9 | ||
|
|
a12e4fd5bb | ||
|
|
d48d2c24af | ||
|
|
0f7c1d6a14 | ||
|
|
22f306a384 | ||
|
|
688d07a5c8 | ||
|
|
d3c6106566 | ||
|
|
4d21900dff | ||
|
|
795c2cf575 | ||
|
|
eafb7b9ffc | ||
|
|
d0508f7ca4 | ||
|
|
f2e1289ab0 | ||
|
|
92ab103b3a | ||
|
|
dec67f6ef9 | ||
|
|
932473d0ff | ||
|
|
288bb65d67 | ||
|
|
cb1dbe34fa | ||
|
|
4a14403274 | ||
|
|
df04b23c88 | ||
|
|
a6b413b431 | ||
|
|
041d7d81fa | ||
|
|
02197677b7 | ||
|
|
0636816167 | ||
|
|
8b31257617 | ||
|
|
825914a4bd | ||
|
|
fb5f13991a | ||
|
|
72b0f11b72 | ||
|
|
78ab232516 | ||
|
|
716c5c1d87 | ||
|
|
5d33694bc6 | ||
|
|
23a85f51d2 | ||
|
|
43b4e4b028 | ||
|
|
251544b2c5 | ||
|
|
c0b531030f | ||
|
|
f7b78783ec | ||
|
|
bca80dcc53 | ||
|
|
9e25e58378 | ||
|
|
4c4e12a794 | ||
|
|
a3f0506ba3 | ||
|
|
f46e730eb5 | ||
|
|
a2e54aaefc | ||
|
|
2855171fda | ||
|
|
209e932482 | ||
|
|
f31c8f4dac | ||
|
|
999b19d148 | ||
|
|
7d061d428f | ||
|
|
2922782e5e | ||
|
|
47a8383ae5 | ||
|
|
a543b89c38 | ||
|
|
702f8b47ac | ||
|
|
f8e4c7f5cd | ||
|
|
8d325198e8 | ||
|
|
afd304f9e5 | ||
|
|
59f4c51e6e | ||
|
|
613e4ef606 | ||
|
|
aceed2a11e | ||
|
|
02682f8ad4 | ||
|
|
085d0ae877 |
@@ -11,7 +11,7 @@
|
||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
||||
"VARIANT": "3.12-bullseye",
|
||||
// Options
|
||||
"NODE_VERSION": "16"
|
||||
"NODE_VERSION": "20"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
@@ -48,12 +48,13 @@
|
||||
],
|
||||
// Use 'onCreateCommand' to run commands at the end of container creation.
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules && task setup",
|
||||
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules /home/vscode/commandhistory && task setup",
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"dockerDashComposeVersion": "v2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appPort": 3000
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
@@ -25,10 +25,10 @@ venv
|
||||
|
||||
*/node_modules
|
||||
*/dist
|
||||
/dist/
|
||||
*/data/db
|
||||
*/mealie/test
|
||||
*/mealie/.temp
|
||||
|
||||
model.crfmodel
|
||||
/mealie/frontend/
|
||||
|
||||
crowdin.yml
|
||||
|
||||
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -20,20 +20,6 @@
|
||||
|
||||
-->
|
||||
|
||||
## What type of PR is this?
|
||||
|
||||
_(REQUIRED)_
|
||||
|
||||
<!--
|
||||
Delete any of the following that do not apply:
|
||||
-->
|
||||
|
||||
- feature
|
||||
- bug
|
||||
- documentation
|
||||
- cleanup
|
||||
- dev (Internal development)
|
||||
|
||||
## What this PR does / why we need it:
|
||||
|
||||
_(REQUIRED)_
|
||||
|
||||
102
.github/workflows/build-package.yml
vendored
Normal file
102
.github/workflows/build-package.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: Build Package
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-frontend:
|
||||
name: Build frontend
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies 👨🏻💻
|
||||
run: yarn
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run Build 🚚
|
||||
run: yarn generate
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Archive built frontend
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: frontend/dist
|
||||
retention-days: 5
|
||||
|
||||
build-package:
|
||||
name: Build Python package
|
||||
needs: build-frontend
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
plugins: |
|
||||
poetry-plugin-export
|
||||
|
||||
- name: Retrieve built frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-dist
|
||||
path: mealie/frontend
|
||||
|
||||
- name: Override __init__.py
|
||||
run: |
|
||||
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
|
||||
|
||||
- name: Build package and requirements.txt
|
||||
env:
|
||||
SKIP_PACKAGE_DEPS: true
|
||||
run: |
|
||||
task py:package
|
||||
|
||||
- name: Archive built package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-dist
|
||||
path: dist
|
||||
retention-days: 5
|
||||
9
.github/workflows/e2e.yml
vendored
9
.github/workflows/e2e.yml
vendored
@@ -13,16 +13,23 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: ./tests/e2e/yarn.lock
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Retrieve Python package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: backend-dist
|
||||
path: dist
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: ./docker/Dockerfile
|
||||
context: .
|
||||
build-contexts: |
|
||||
packages=dist
|
||||
push: false
|
||||
load: true
|
||||
tags: mealie:e2e
|
||||
|
||||
114
.github/workflows/locale-sync.yml
vendored
Normal file
114
.github/workflows/locale-sync.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Automatic Locale Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Sunday at 2 AM UTC
|
||||
- cron: "0 2 * * 0"
|
||||
workflow_dispatch:
|
||||
# Allow manual triggering from the GitHub UI
|
||||
|
||||
permissions:
|
||||
contents: write # To checkout, commit, and push changes
|
||||
pull-requests: write # To create pull requests
|
||||
|
||||
jobs:
|
||||
sync-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- name: Check venv cache
|
||||
id: cache-validate
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "import fastapi;print('venv good?')" > test.py && poetry run python test.py && echo "cache-hit-success=true" >> $GITHUB_OUTPUT
|
||||
rm test.py
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
||||
poetry install
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Run locale generation
|
||||
run: |
|
||||
cd dev/code-generation
|
||||
poetry run python main.py locales
|
||||
env:
|
||||
CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }}
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit and create PR
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
# Configure git
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
# Use the current branch as the base
|
||||
BASE_BRANCH="${{ github.ref_name }}"
|
||||
echo "Using base branch: $BASE_BRANCH"
|
||||
|
||||
# Create a new branch from the base branch
|
||||
BRANCH_NAME="auto-locale-sync-$(date +%Y%m%d-%H%M%S)"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
# Add and commit changes
|
||||
git add .
|
||||
git commit -m "chore: automatic locale sync"
|
||||
|
||||
# Push the branch
|
||||
git push origin "$BRANCH_NAME"
|
||||
|
||||
sleep 2
|
||||
|
||||
# Create PR using GitHub CLI with explicit repository
|
||||
gh pr create \
|
||||
--repo "${{ github.repository }}" \
|
||||
--title "chore: automatic locale sync" \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$BRANCH_NAME" \
|
||||
--body "## Summary
|
||||
|
||||
Automatically generated locale updates from the weekly sync job.
|
||||
|
||||
## Changes
|
||||
- Updated frontend locale files
|
||||
- Generated from latest translation sources" \
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: No changes detected
|
||||
if: steps.changes.outputs.has_changes == 'false'
|
||||
run: echo "No locale changes detected, skipping PR creation"
|
||||
19
.github/workflows/nightly.yml
vendored
19
.github/workflows/nightly.yml
vendored
@@ -18,13 +18,19 @@ concurrency:
|
||||
jobs:
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
uses: ./.github/workflows/partial-backend.yml
|
||||
uses: ./.github/workflows/test-backend.yml
|
||||
|
||||
frontend-tests:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
name: "Frontend Tests"
|
||||
uses: ./.github/workflows/test-frontend.yml
|
||||
|
||||
build-release:
|
||||
build-package:
|
||||
name: Build Package
|
||||
uses: ./.github/workflows/build-package.yml
|
||||
with:
|
||||
tag: nightly
|
||||
|
||||
publish:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -35,10 +41,11 @@ jobs:
|
||||
id-token: write
|
||||
name: Build Tagged Release
|
||||
if: github.repository == 'mealie-recipes/mealie'
|
||||
uses: ./.github/workflows/partial-builder.yml
|
||||
uses: ./.github/workflows/publish.yml
|
||||
needs:
|
||||
- frontend-tests
|
||||
- backend-tests
|
||||
- build-package
|
||||
with:
|
||||
tag: nightly
|
||||
secrets:
|
||||
@@ -49,7 +56,7 @@ jobs:
|
||||
name: Notify Discord
|
||||
if: github.repository == 'mealie-recipes/mealie'
|
||||
needs:
|
||||
- build-release
|
||||
- publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord notification
|
||||
|
||||
@@ -35,18 +35,22 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Override __init__.py
|
||||
run: |
|
||||
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
|
||||
|
||||
- uses: depot/setup-action@v1
|
||||
|
||||
- name: Retrieve Python package
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: backend-dist
|
||||
path: dist
|
||||
|
||||
- name: Build and push Docker image, via Depot.dev
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: srzjb6mhzm
|
||||
file: ./docker/Dockerfile
|
||||
context: .
|
||||
build-contexts: |
|
||||
packages=dist
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
40
.github/workflows/pull-requests.yml
vendored
40
.github/workflows/pull-requests.yml
vendored
@@ -16,20 +16,16 @@ jobs:
|
||||
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
uses: ./.github/workflows/partial-backend.yml
|
||||
uses: ./.github/workflows/test-backend.yml
|
||||
|
||||
frontend-tests:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
name: "Frontend Tests"
|
||||
uses: ./.github/workflows/test-frontend.yml
|
||||
|
||||
container-scanning:
|
||||
name: "Trivy Container Scanning"
|
||||
uses: ./.github/workflows/partial-trivy-container-scanning.yml
|
||||
|
||||
end-to-end:
|
||||
name: "End-to-End Tests"
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
|
||||
code-ql:
|
||||
name: "CodeQL"
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
@@ -37,3 +33,33 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
build-package:
|
||||
name: "Build Python package"
|
||||
uses: ./.github/workflows/build-package.yml
|
||||
with:
|
||||
tag: e2e
|
||||
|
||||
end-to-end:
|
||||
name: "End-to-End Tests"
|
||||
needs: build-package
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
|
||||
publish-image:
|
||||
name: "Publish PR Image"
|
||||
if: contains(github.event.pull_request.labels.*.name, 'build-image') && github.repository == 'mealie-recipes/mealie'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# The id-token write permission is needed to connect to Depot.dev
|
||||
# as part of the partial-builder.yml action. It needs to be declared
|
||||
# in the parent action, as noted here:
|
||||
# https://github.com/orgs/community/discussions/76409#discussioncomment-8131390
|
||||
id-token: write
|
||||
needs: build-package
|
||||
uses: ./.github/workflows/publish.yml
|
||||
with:
|
||||
tag: pr-${{ github.event.pull_request.number }}
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -7,13 +7,19 @@ on:
|
||||
jobs:
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
uses: ./.github/workflows/partial-backend.yml
|
||||
uses: ./.github/workflows/test-backend.yml
|
||||
|
||||
frontend-tests:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
name: "Frontend Tests"
|
||||
uses: ./.github/workflows/test-frontend.yml
|
||||
|
||||
build-release:
|
||||
build-package:
|
||||
name: Build Package
|
||||
uses: ./.github/workflows/build-package.yml
|
||||
with:
|
||||
tag: release
|
||||
|
||||
publish:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -23,10 +29,11 @@ jobs:
|
||||
# https://github.com/orgs/community/discussions/76409#discussioncomment-8131390
|
||||
id-token: write
|
||||
name: Build Tagged Release
|
||||
uses: ./.github/workflows/partial-builder.yml
|
||||
uses: ./.github/workflows/publish.yml
|
||||
needs:
|
||||
- backend-tests
|
||||
- frontend-tests
|
||||
- build-package
|
||||
with:
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
tags: |
|
||||
@@ -39,7 +46,7 @@ jobs:
|
||||
notify-discord:
|
||||
name: Notify Discord
|
||||
needs:
|
||||
- build-release
|
||||
- publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord notification
|
||||
@@ -52,7 +59,7 @@ jobs:
|
||||
update-image-tags:
|
||||
name: Update image tag in sample docker-compose files
|
||||
needs:
|
||||
- build-release
|
||||
- publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
11
.github/workflows/stale.yml
vendored
11
.github/workflows/stale.yml
vendored
@@ -16,12 +16,13 @@ jobs:
|
||||
with:
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'pinned,security,early-stages,bug: confirmed,feedback,task'
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 5
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has been open 90 days with no activity.'
|
||||
days-before-issue-stale: 90
|
||||
# This stops an issue from ever getting closed automatically.
|
||||
days-before-issue-close: -1
|
||||
stale-pr-label: 'stale'
|
||||
stale-pr-message: 'This PR is stale because it has been open 45 days with no activity.'
|
||||
days-before-pr-stale: 45
|
||||
stale-pr-message: 'This PR has been automatically marked as stale because it has been open 90 days with no activity.'
|
||||
days-before-pr-stale: 90
|
||||
# This stops a PR from ever getting closed automatically.
|
||||
days-before-pr-close: -1
|
||||
# If an issue/PR has a milestone, it's exempt from being marked as stale.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Backend Test/Lint
|
||||
name: Backend Lint and Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Frontend Build/Lin
|
||||
name: Frontend Lint and Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
@@ -34,6 +34,10 @@ jobs:
|
||||
run: yarn
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Prepare nuxt 🚀
|
||||
run: yarn nuxt prepare
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run linter 👀
|
||||
run: yarn lint
|
||||
working-directory: "frontend"
|
||||
@@ -41,37 +45,3 @@ jobs:
|
||||
- name: Run tests 🧪
|
||||
run: yarn test:ci
|
||||
working-directory: "frontend"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies 👨🏻💻
|
||||
run: yarn
|
||||
working-directory: "frontend"
|
||||
|
||||
- name: Run Build 🚚
|
||||
run: yarn build
|
||||
working-directory: "frontend"
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -10,6 +10,9 @@ docs/site/
|
||||
*temp/*
|
||||
.secret
|
||||
frontend/dist/
|
||||
frontend/.output/*
|
||||
frontend/.yarn/*
|
||||
frontend/.yarnrc.yml
|
||||
|
||||
dev/code-generation/generated/*
|
||||
dev/data/mealie.db-journal
|
||||
@@ -52,7 +55,7 @@ pnpm-debug.log*
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
|
||||
/dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -66,6 +69,9 @@ wheels/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# frontend copied into Python module for packaging purposes
|
||||
/mealie/frontend/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
@@ -154,12 +160,12 @@ dev/data/backups/dev_sample_data*.zip
|
||||
dev/data/recipes/*
|
||||
dev/scripts/output/app_routes.py
|
||||
dev/scripts/output/javascriptAPI/*
|
||||
mealie/services/scraper/ingredient_nlp/model.crfmodel
|
||||
dev/code-generation/generated/openapi.json
|
||||
dev/code-generation/generated/test_routes.py
|
||||
mealie/services/parser_services/crfpp/model.crfmodel
|
||||
lcov.info
|
||||
dev/code-generation/openapi.json
|
||||
|
||||
.run/
|
||||
.task/*
|
||||
.dev.env
|
||||
frontend/eslint.config.deprecated.js
|
||||
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.2
|
||||
rev: v0.12.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -18,6 +18,7 @@
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.workingDirectories": [
|
||||
"./frontend"
|
||||
],
|
||||
@@ -60,5 +61,9 @@
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
}
|
||||
}
|
||||
|
||||
103
Taskfile.yml
103
Taskfile.yml
@@ -41,40 +41,36 @@ tasks:
|
||||
setup:ui:
|
||||
desc: setup frontend dependencies
|
||||
dir: frontend
|
||||
run: once
|
||||
cmds:
|
||||
- yarn install
|
||||
sources:
|
||||
- package.json
|
||||
- yarn.lock
|
||||
generates:
|
||||
- node_modules/**
|
||||
|
||||
setup:py:
|
||||
desc: setup python dependencies
|
||||
run: once
|
||||
cmds:
|
||||
- poetry install --with main,dev,postgres
|
||||
- poetry run pre-commit install
|
||||
|
||||
setup:model:
|
||||
desc: setup nlp model
|
||||
vars:
|
||||
MODEL_URL: https://github.com/mealie-recipes/nlp-model/releases/download/v1.0.0/model.crfmodel
|
||||
OUTPUT: ./mealie/services/parser_services/crfpp/model.crfmodel
|
||||
sources:
|
||||
# using pyproject.toml as the dependency since this should only ever need to run once
|
||||
# during setup. There is perhaps a better way to do this.
|
||||
- ./pyproject.toml
|
||||
generates:
|
||||
- ./mealie/services/parser_services/crfpp/model.crfmodel
|
||||
cmds:
|
||||
- curl -L0 {{ .MODEL_URL }} --output {{ .OUTPUT }}
|
||||
- poetry.lock
|
||||
- pyproject.toml
|
||||
- .pre-commit-config.yaml
|
||||
|
||||
setup:
|
||||
desc: setup all dependencies
|
||||
deps:
|
||||
- setup:ui
|
||||
- setup:py
|
||||
- setup:model
|
||||
|
||||
dev:generate:
|
||||
desc: run code generators
|
||||
cmds:
|
||||
- poetry run python dev/code-generation/main.py
|
||||
- poetry run python dev/code-generation/main.py {{ .CLI_ARGS }}
|
||||
- task: py:format
|
||||
|
||||
dev:services:
|
||||
@@ -131,6 +127,63 @@ tasks:
|
||||
- poetry run coverage html
|
||||
- open htmlcov/index.html
|
||||
|
||||
py:package:copy-frontend:
|
||||
desc: copy the frontend files into the Python package
|
||||
internal: true
|
||||
deps:
|
||||
- ui:generate
|
||||
cmds:
|
||||
- rm -rf mealie/frontend
|
||||
- cp -a frontend/dist mealie/frontend
|
||||
sources:
|
||||
- frontend/dist/**
|
||||
generates:
|
||||
- mealie/frontend/**
|
||||
|
||||
py:package:generate-requirements:
|
||||
desc: Generate requirements file to pin all packages, effectively a "pip freeze" before installation begins
|
||||
internal: true
|
||||
cmds:
|
||||
- poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt
|
||||
# Include mealie in the requirements, hashing the package that was just built to ensure it's the one installed
|
||||
- echo "mealie[pgsql]=={{.MEALIE_VERSION}} \\" >> dist/requirements.txt
|
||||
- poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
|
||||
- echo " \\" >> dist/requirements.txt
|
||||
- poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
vars:
|
||||
MEALIE_VERSION:
|
||||
sh: poetry version --short
|
||||
sources:
|
||||
- poetry.lock
|
||||
- pyproject.toml
|
||||
- dist/mealie-*.whl
|
||||
- dist/mealie-*.tar.gz
|
||||
generates:
|
||||
- dist/requirements.txt
|
||||
|
||||
py:package:deps-parallel:
|
||||
desc: Run py:package dependencies in parallel
|
||||
internal: true
|
||||
deps:
|
||||
- setup:py
|
||||
- py:package:copy-frontend
|
||||
|
||||
py:package:deps:
|
||||
desc: Dependencies of py:package, skippable by setting SKIP_PACKAGE_DEPS=true
|
||||
internal: true
|
||||
cmds:
|
||||
- task: py:package:deps-parallel
|
||||
status:
|
||||
- '{{ .SKIP_PACKAGE_DEPS | default "false"}}'
|
||||
|
||||
py:package:
|
||||
desc: builds Python packages (sdist and wheel) in top-level dist directory
|
||||
deps:
|
||||
- py:package:deps
|
||||
cmds:
|
||||
- poetry build -n --output=dist
|
||||
- task: py:package:generate-requirements
|
||||
|
||||
py:
|
||||
desc: runs the backend server
|
||||
cmds:
|
||||
@@ -160,6 +213,14 @@ tasks:
|
||||
cmds:
|
||||
- yarn build
|
||||
|
||||
ui:generate:
|
||||
desc: generates a static version of the frontend in frontend/dist
|
||||
dir: frontend
|
||||
deps:
|
||||
- setup:ui
|
||||
cmds:
|
||||
- yarn generate
|
||||
|
||||
ui:lint:
|
||||
desc: runs the frontend linter
|
||||
dir: frontend
|
||||
@@ -182,7 +243,17 @@ tasks:
|
||||
desc: runs the frontend server
|
||||
dir: frontend
|
||||
cmds:
|
||||
- yarn run dev
|
||||
- yarn run dev --no-fork
|
||||
|
||||
docker:build-from-package:
|
||||
desc: Builds the Docker image from the existing Python package in dist/
|
||||
deps:
|
||||
- py:package
|
||||
cmds:
|
||||
- docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT={{.GIT_COMMIT}} --build-context packages=dist .
|
||||
vars:
|
||||
GIT_COMMIT:
|
||||
sh: git rev-parse HEAD
|
||||
|
||||
docker:prod:
|
||||
desc: builds and runs the production docker image locally
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import pathlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -13,7 +14,7 @@ from mealie.schema._mealie import MealieModel
|
||||
|
||||
BASE = pathlib.Path(__file__).parent.parent.parent
|
||||
|
||||
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY")
|
||||
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") or os.environ.get("CROWDIN_API_KEY", "")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -23,19 +24,22 @@ class LocaleData:
|
||||
|
||||
|
||||
LOCALE_DATA: dict[str, LocaleData] = {
|
||||
"en-US": LocaleData(name="American English"),
|
||||
"en-GB": LocaleData(name="British English"),
|
||||
"af-ZA": LocaleData(name="Afrikaans (Afrikaans)"),
|
||||
"ar-SA": LocaleData(name="العربية (Arabic)", dir="rtl"),
|
||||
"bg-BG": LocaleData(name="Български (Bulgarian)"),
|
||||
"ca-ES": LocaleData(name="Català (Catalan)"),
|
||||
"cs-CZ": LocaleData(name="Čeština (Czech)"),
|
||||
"da-DK": LocaleData(name="Dansk (Danish)"),
|
||||
"de-DE": LocaleData(name="Deutsch (German)"),
|
||||
"el-GR": LocaleData(name="Ελληνικά (Greek)"),
|
||||
"en-GB": LocaleData(name="British English"),
|
||||
"en-US": LocaleData(name="American English"),
|
||||
"es-ES": LocaleData(name="Español (Spanish)"),
|
||||
"et-EE": LocaleData(name="Eesti (Estonian)"),
|
||||
"fi-FI": LocaleData(name="Suomi (Finnish)"),
|
||||
"fr-FR": LocaleData(name="Français (French)"),
|
||||
"fr-BE": LocaleData(name="Belge (Belgian)"),
|
||||
"fr-CA": LocaleData(name="Français canadien (Canadian French)"),
|
||||
"fr-FR": LocaleData(name="Français (French)"),
|
||||
"gl-ES": LocaleData(name="Galego (Galician)"),
|
||||
"he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"),
|
||||
"hr-HR": LocaleData(name="Hrvatski (Croatian)"),
|
||||
@@ -53,6 +57,7 @@ LOCALE_DATA: dict[str, LocaleData] = {
|
||||
"pt-PT": LocaleData(name="Português (Portuguese)"),
|
||||
"ro-RO": LocaleData(name="Română (Romanian)"),
|
||||
"ru-RU": LocaleData(name="Pусский (Russian)"),
|
||||
"sk-SK": LocaleData(name="Slovenčina (Slovak)"),
|
||||
"sl-SI": LocaleData(name="Slovenščina (Slovenian)"),
|
||||
"sr-SP": LocaleData(name="српски (Serbian)"),
|
||||
"sv-SE": LocaleData(name="Svenska (Swedish)"),
|
||||
@@ -71,7 +76,7 @@ export const LOCALES = [{% for locale in locales %}
|
||||
progress: {{ locale.progress }},
|
||||
dir: "{{ locale.dir }}",
|
||||
},{% endfor %}
|
||||
]
|
||||
];
|
||||
|
||||
"""
|
||||
|
||||
@@ -93,8 +98,8 @@ class CrowdinApi:
|
||||
project_id = "451976"
|
||||
api_key = API_KEY
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
api_key = api_key
|
||||
def __init__(self, api_key: str | None):
|
||||
self.api_key = api_key or API_KEY
|
||||
|
||||
@property
|
||||
def headers(self) -> dict:
|
||||
@@ -156,12 +161,13 @@ PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
datetime_dir = PROJECT_DIR / "frontend" / "lang" / "dateTimeFormats"
|
||||
locales_dir = PROJECT_DIR / "frontend" / "lang" / "messages"
|
||||
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.js"
|
||||
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.ts"
|
||||
i18n_config = PROJECT_DIR / "frontend" / "i18n.config.ts"
|
||||
reg_valid = PROJECT_DIR / "mealie" / "schema" / "_mealie" / "validators.py"
|
||||
|
||||
"""
|
||||
This snippet walks the message and dat locales directories and generates the import information
|
||||
for the nuxt.config.js file and automatically injects it into the nuxt.config.js file. Note that
|
||||
for the nuxt.config.ts file and automatically injects it into the nuxt.config.ts file. Note that
|
||||
the code generation ID is hardcoded into the script and required in the nuxt config.
|
||||
"""
|
||||
|
||||
@@ -173,12 +179,12 @@ def inject_nuxt_values():
|
||||
|
||||
all_langs = []
|
||||
for match in locales_dir.glob("*.json"):
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name}" }},'
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}" }},'
|
||||
all_langs.append(lang_string)
|
||||
|
||||
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
||||
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
||||
|
||||
|
||||
def inject_registration_validation_values():
|
||||
@@ -195,7 +201,7 @@ def inject_registration_validation_values():
|
||||
|
||||
|
||||
def generate_locales_ts_file():
|
||||
api = CrowdinApi("")
|
||||
api = CrowdinApi(None)
|
||||
models = api.get_languages()
|
||||
tmpl = Template(LOCALE_TEMPLATE)
|
||||
rendered = tmpl.render(locales=models)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import gen_py_pytest_data_paths
|
||||
@@ -11,15 +12,39 @@ CWD = Path(__file__).parent
|
||||
|
||||
|
||||
def main():
|
||||
items = [
|
||||
(gen_py_schema_exports.main, "schema exports"),
|
||||
(gen_ts_types.main, "frontend types"),
|
||||
(gen_ts_locales.main, "locales"),
|
||||
(gen_py_pytest_data_paths.main, "test data paths"),
|
||||
(gen_py_pytest_routes.main, "pytest routes"),
|
||||
]
|
||||
parser = argparse.ArgumentParser(description="Run code generators")
|
||||
parser.add_argument(
|
||||
"generators",
|
||||
nargs="*",
|
||||
help="Specific generators to run (schema, types, locales, data-paths, routes). If none specified, all will run.", # noqa: E501 - long line
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
for func, name in items:
|
||||
# Define all available generators
|
||||
all_generators = {
|
||||
"schema": (gen_py_schema_exports.main, "schema exports"),
|
||||
"types": (gen_ts_types.main, "frontend types"),
|
||||
"locales": (gen_ts_locales.main, "locales"),
|
||||
"data-paths": (gen_py_pytest_data_paths.main, "test data paths"),
|
||||
"routes": (gen_py_pytest_routes.main, "pytest routes"),
|
||||
}
|
||||
|
||||
# Determine which generators to run
|
||||
if args.generators:
|
||||
# Validate requested generators
|
||||
invalid_generators = [g for g in args.generators if g not in all_generators]
|
||||
if invalid_generators:
|
||||
log.error(f"Invalid generator(s): {', '.join(invalid_generators)}")
|
||||
log.info(f"Available generators: {', '.join(all_generators.keys())}")
|
||||
return
|
||||
|
||||
generators_to_run = [(all_generators[g][0], all_generators[g][1]) for g in args.generators]
|
||||
else:
|
||||
# Run all generators (default behavior)
|
||||
generators_to_run = list(all_generators.values())
|
||||
|
||||
# Run the selected generators
|
||||
for func, name in generators_to_run:
|
||||
log.info(f"Generating {name}...")
|
||||
func()
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -35,7 +34,7 @@ class CodeSlicer:
|
||||
start: int
|
||||
end: int
|
||||
|
||||
indentation: str
|
||||
indentation: str | None
|
||||
text: list[str]
|
||||
|
||||
_next_line = None
|
||||
@@ -47,15 +46,24 @@ class CodeSlicer:
|
||||
|
||||
def push_line(self, string: str) -> None:
|
||||
self._next_line = self._next_line or self.start + 1
|
||||
self.text.insert(self._next_line, self.indentation + string + "\n")
|
||||
self.text.insert(self._next_line, (self.indentation or "") + string + "\n")
|
||||
self._next_line += 1
|
||||
|
||||
|
||||
def get_indentation_of_string(line: str, comment_char: str = "//|#") -> str:
|
||||
return re.sub(rf"{comment_char}.*", "", line).removesuffix("\n")
|
||||
def get_indentation_of_string(line: str) -> str:
|
||||
# Extract everything before the comment
|
||||
if "//" in line:
|
||||
indentation = line.split("//")[0]
|
||||
elif "#" in line:
|
||||
indentation = line.split("#")[0]
|
||||
else:
|
||||
indentation = line
|
||||
|
||||
# Keep only the whitespace, remove any non-whitespace characters
|
||||
return "".join(c for c in indentation if c.isspace())
|
||||
|
||||
|
||||
def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int, str]:
|
||||
def find_start_end(file_text: list[str], gen_id: str) -> tuple[int, int, str | None]:
|
||||
start = None
|
||||
end = None
|
||||
indentation = None
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
|
||||
|
||||

|
||||
|
||||
# {{ recipe.name }}
|
||||
{{ recipe.description }}
|
||||
|
||||
## Ingredients
|
||||
{% for ingredient in recipe.recipeIngredient %}
|
||||
- [ ] {{ ingredient }} {% endfor %}
|
||||
|
||||
## Instructions
|
||||
{% for step in recipe.recipeInstructions %}
|
||||
- [ ] {{ step.text }} {% endfor %}
|
||||
|
||||
{% for note in recipe.notes %}
|
||||
**{{ note.title }}:** {{ note.text }}
|
||||
{% endfor %}
|
||||
|
||||
---
|
||||
|
||||
Tags: {{ recipe.tags }}
|
||||
Categories: {{ recipe.categories }}
|
||||
Original URL: {{ recipe.orgURL }}
|
||||
75
dev/scripts/convert_seed_files_to_new_format.py
Normal file
75
dev/scripts/convert_seed_files_to_new_format.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import glob
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
|
||||
def get_seed_locale_names() -> set[str]:
|
||||
"""Find all locales in the seed/resources/ folder
|
||||
|
||||
Returns:
|
||||
A set of every file name where there's both a seed label and seed food file
|
||||
"""
|
||||
|
||||
LABELS_PATH = "/workspaces/mealie/mealie/repos/seed/resources/labels/locales/"
|
||||
FOODS_PATH = "/workspaces/mealie/mealie/repos/seed/resources/foods/locales/"
|
||||
label_locales = glob.glob("*.json", root_dir=LABELS_PATH)
|
||||
foods_locales = glob.glob("*.json", root_dir=FOODS_PATH)
|
||||
|
||||
# ensure that a locale has both a label and a food seed file
|
||||
return set(label_locales).intersection(foods_locales)
|
||||
|
||||
|
||||
def get_labels_from_file(locale: str) -> list[str]:
|
||||
"""Query a locale to get all of the labels so that they can be added to the new foods seed format
|
||||
|
||||
Returns:
|
||||
All of the labels found within the seed file for a given locale
|
||||
"""
|
||||
|
||||
locale_path = pathlib.Path("/workspaces/mealie/mealie/repos/seed/resources/labels/locales/" + locale)
|
||||
label_names = [label["name"] for label in json.loads(locale_path.read_text(encoding="utf-8"))]
|
||||
return label_names
|
||||
|
||||
|
||||
def transform_foods(locale: str):
|
||||
"""
|
||||
Convert the current food seed file for a locale into a new format which maps each food to a label
|
||||
|
||||
Existing format of foods seed file is a dictionary where each key is a food name and the values are a dictionary
|
||||
of attributes such as name and plural_name
|
||||
|
||||
New format maps each food to a label. The top-level dictionary has each key as a label e.g. "Fruits".
|
||||
Each label key as a value that is a dictionary with an element called "foods"
|
||||
"Foods" is a dictionary of each food for that label, with a key of the english food name e.g. "baking-soda"
|
||||
and a value of attributes, including the translated name of the item e.g. "bicarbonate of soda" for en-GB.
|
||||
"""
|
||||
|
||||
locale_path = pathlib.Path("/workspaces/mealie/mealie/repos/seed/resources/foods/locales/" + locale)
|
||||
|
||||
with open(locale_path, encoding="utf-8") as infile:
|
||||
data = json.load(infile)
|
||||
|
||||
first_value = next(iter(data.values()))
|
||||
if isinstance(first_value, dict) and "foods" in first_value:
|
||||
# Locale is already in the new format, skipping transformation
|
||||
return
|
||||
|
||||
transformed_data = {"": {"foods": dict(data.items())}}
|
||||
|
||||
# Seeding for labels now pulls from the foods file and parses the labels from there (as top-level keys),
|
||||
# thus we need to add all of the existing labels to the new food seed file and give them an empty foods dictionary
|
||||
label_names = get_labels_from_file(locale)
|
||||
for label in label_names:
|
||||
transformed_data[label] = {"foods": {}}
|
||||
|
||||
with open(locale_path, "w", encoding="utf-8") as outfile:
|
||||
json.dump(transformed_data, outfile, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def main():
|
||||
for locale in get_seed_locale_names():
|
||||
transform_foods(locale)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,11 @@
|
||||
FROM node:16 as builder
|
||||
###############################################
|
||||
# Frontend Build
|
||||
###############################################
|
||||
FROM node:20 AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /frontend
|
||||
|
||||
COPY ./frontend .
|
||||
COPY frontend .
|
||||
|
||||
RUN yarn install \
|
||||
--prefer-offline \
|
||||
@@ -17,7 +20,7 @@ RUN yarn generate
|
||||
###############################################
|
||||
# Base Image - Python
|
||||
###############################################
|
||||
FROM python:3.12-slim as python-base
|
||||
FROM python:3.12-slim AS python-base
|
||||
|
||||
ENV MEALIE_HOME="/app"
|
||||
|
||||
@@ -26,14 +29,10 @@ ENV PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=off \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=true \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
PYSETUP_PATH="/opt/pysetup" \
|
||||
VENV_PATH="/opt/pysetup/.venv"
|
||||
VENV_PATH="/opt/mealie"
|
||||
|
||||
# prepend poetry and venv to path
|
||||
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
|
||||
# prepend venv to path
|
||||
ENV PATH="$VENV_PATH/bin:$PATH"
|
||||
|
||||
# create user account
|
||||
RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \
|
||||
@@ -41,43 +40,86 @@ RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \
|
||||
&& mkdir $MEALIE_HOME
|
||||
|
||||
###############################################
|
||||
# Builder Image
|
||||
# Backend Package Build
|
||||
###############################################
|
||||
FROM python-base as builder-base
|
||||
FROM python-base AS backend-builder
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV POETRY_HOME="/opt/poetry" \
|
||||
POETRY_NO_INTERACTION=1
|
||||
|
||||
# prepend poetry to path
|
||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||
|
||||
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
|
||||
ENV POETRY_VERSION=2.0.1
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# install poetry plugins needed to build the package
|
||||
RUN poetry self add "poetry-plugin-export>=1.9"
|
||||
|
||||
WORKDIR /mealie
|
||||
|
||||
# copy project files here to ensure they will be cached.
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
COPY mealie ./mealie
|
||||
|
||||
# Copy frontend to package it into the wheel
|
||||
COPY --from=frontend-builder /frontend/dist ./mealie/frontend
|
||||
|
||||
# Build the source and binary package
|
||||
RUN poetry build --output=dist
|
||||
|
||||
# Create the requirements file, which is used to install the built package and
|
||||
# its pinned dependencies later. mealie is included to ensure the built one is
|
||||
# what's installed.
|
||||
RUN export MEALIE_VERSION=$(poetry version --short) \
|
||||
&& poetry export --only=main --extras=pgsql --output=dist/requirements.txt \
|
||||
&& echo "mealie[pgsql]==$MEALIE_VERSION \\" >> dist/requirements.txt \
|
||||
&& poetry run pip hash dist/mealie-$MEALIE_VERSION-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \
|
||||
&& echo " \\" >> dist/requirements.txt \
|
||||
&& poetry run pip hash dist/mealie-$MEALIE_VERSION.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
|
||||
###############################################
|
||||
# Package Container
|
||||
# Only role is to hold the packages, or be overriden by a --build-context flag.
|
||||
###############################################
|
||||
FROM scratch AS packages
|
||||
COPY --from=backend-builder /mealie/dist /
|
||||
|
||||
###############################################
|
||||
# Python Virtual Environment Build
|
||||
###############################################
|
||||
# Install packages required to build the venv, in parallel to building the wheel
|
||||
FROM python-base AS venv-builder-base
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libwebp-dev \
|
||||
# LDAP Dependencies
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install -U --no-cache-dir pip
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN python3 -m venv --upgrade-deps $VENV_PATH
|
||||
|
||||
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
|
||||
ENV POETRY_VERSION=1.3.1
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
# Install the wheel and all dependencies into the venv
|
||||
FROM venv-builder-base AS venv-builder
|
||||
|
||||
# copy project requirement files here to ensure they will be cached.
|
||||
WORKDIR $PYSETUP_PATH
|
||||
COPY ./poetry.lock ./pyproject.toml ./
|
||||
# Copy built package (wheel) and its dependency requirements
|
||||
COPY --from=packages * /dist/
|
||||
|
||||
# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
|
||||
RUN poetry install -E pgsql --only main
|
||||
|
||||
###############################################
|
||||
# CRFPP Image
|
||||
###############################################
|
||||
FROM hkotel/crfpp as crfpp
|
||||
|
||||
RUN echo "crfpp-container"
|
||||
# Install the wheel with exact versions of dependencies into the venv
|
||||
RUN . $VENV_PATH/bin/activate \
|
||||
&& pip install --require-hashes -r /dist/requirements.txt --find-links /dist
|
||||
|
||||
###############################################
|
||||
# Production Image
|
||||
###############################################
|
||||
FROM python-base as production
|
||||
FROM python-base AS production
|
||||
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
|
||||
ENV PRODUCTION=true
|
||||
ENV TESTING=false
|
||||
@@ -96,39 +138,20 @@ RUN apt-get update \
|
||||
# create directory used for Docker Secrets
|
||||
RUN mkdir -p /run/secrets
|
||||
|
||||
# copying poetry and venv into image
|
||||
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
|
||||
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
||||
# Copy venv into image. It contains a fully-installed mealie backend and frontend.
|
||||
COPY --from=venv-builder $VENV_PATH $VENV_PATH
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||
COPY --from=crfpp /usr/local/lib/ /usr/local/lib
|
||||
COPY --from=crfpp /usr/local/bin/crf_learn /usr/local/bin/crf_learn
|
||||
COPY --from=crfpp /usr/local/bin/crf_test /usr/local/bin/crf_test
|
||||
|
||||
# copy backend
|
||||
COPY ./mealie $MEALIE_HOME/mealie
|
||||
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
|
||||
|
||||
# venv already has runtime deps installed we get a quicker install
|
||||
WORKDIR $MEALIE_HOME
|
||||
RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --only main
|
||||
WORKDIR /
|
||||
|
||||
# Grab CRF++ Model Release
|
||||
RUN python $MEALIE_HOME/mealie/scripts/install_model.py
|
||||
# install nltk data for the ingredient parser
|
||||
ENV NLTK_DATA="/nltk_data/"
|
||||
RUN mkdir -p $NLTK_DATA
|
||||
RUN python -m nltk.downloader -d $NLTK_DATA averaged_perceptron_tagger_eng
|
||||
|
||||
VOLUME [ "$MEALIE_HOME/data/" ]
|
||||
ENV APP_PORT=9000
|
||||
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
HEALTHCHECK CMD python $MEALIE_HOME/mealie/scripts/healthcheck.py || exit 1
|
||||
|
||||
# ----------------------------------
|
||||
# Copy Frontend
|
||||
|
||||
ENV STATIC_FILES=/spa/static
|
||||
COPY --from=builder /app/dist ${STATIC_FILES}
|
||||
HEALTHCHECK CMD python -m mealie.scripts.healthcheck || exit 1
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
|
||||
|
||||
@@ -32,13 +32,51 @@ init() {
|
||||
cd /app
|
||||
|
||||
# Activate our virtual environment here
|
||||
. /opt/pysetup/.venv/bin/activate
|
||||
. /opt/mealie/bin/activate
|
||||
}
|
||||
|
||||
load_secrets() {
|
||||
# Each of these environment variables will support a `_FILE` suffix that allows
|
||||
# for setting the environment variable through the Docker Compose secret
|
||||
# pattern.
|
||||
local -a secret_supported_vars=(
|
||||
"POSTGRES_USER"
|
||||
"POSTGRES_PASSWORD"
|
||||
"POSTGRES_SERVER"
|
||||
"POSTGRES_PORT"
|
||||
"POSTGRES_DB"
|
||||
"POSTGRES_URL_OVERRIDE"
|
||||
|
||||
"SMTP_HOST"
|
||||
"SMTP_PORT"
|
||||
"SMTP_USER"
|
||||
"SMTP_PASSWORD"
|
||||
|
||||
"LDAP_SERVER_URL"
|
||||
"LDAP_QUERY_PASSWORD"
|
||||
|
||||
"OIDC_CONFIGURATION_URL"
|
||||
"OIDC_CLIENT_ID"
|
||||
"OIDC_CLIENT_SECRET"
|
||||
|
||||
"OPENAI_BASE_URL"
|
||||
"OPENAI_API_KEY"
|
||||
)
|
||||
|
||||
# If any secrets are set, prefer them over base environment variables.
|
||||
for var in "${secret_supported_vars[@]}"; do
|
||||
file_var="${var}_FILE"
|
||||
if [ -n "${!file_var}" ]; then
|
||||
export "$var=$(<"${!file_var}")"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
change_user
|
||||
init
|
||||
load_secrets
|
||||
|
||||
# Start API
|
||||
HOST_IP=`/sbin/ip route|awk '/default/ { print $3 }'`
|
||||
|
||||
exec python /app/mealie/main.py
|
||||
exec mealie
|
||||
|
||||
40
docs/docs/contributors/developers-guide/building-packages.md
Normal file
40
docs/docs/contributors/developers-guide/building-packages.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Building Packages
|
||||
|
||||
Released packages are [built and published via GitHub actions](maintainers.md#drafting-releases).
|
||||
|
||||
## Python packages
|
||||
|
||||
To build Python packages locally for testing, use [`task`](starting-dev-server.md#without-dev-containers). After installing `task`, run `task py:package` to perform all the steps needed to build the package and a requirements file. To do it manually, run:
|
||||
```sh
|
||||
pushd frontend
|
||||
yarnpkg install
|
||||
yarnpkg generate
|
||||
popd
|
||||
rm -r mealie/frontend
|
||||
cp -a frontend/dist mealie/frontend
|
||||
poetry build
|
||||
poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt
|
||||
MEALIE_VERSION=$(poetry version --short)
|
||||
echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt
|
||||
poetry run pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt
|
||||
echo " \\" >> dist/requirements.txt
|
||||
poetry run pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt
|
||||
```
|
||||
|
||||
The Python package can be installed with all of its dependencies pinned to the versions tested by the developers with:
|
||||
```sh
|
||||
pip3 install -r dist/requirements.txt --find-links dist
|
||||
```
|
||||
|
||||
To install with the latest but still compatible dependency versions, instead run `pip3 install dist/mealie-$VERSION-py3-none-any.whl` (where `$VERSION` is the version of mealie to install).
|
||||
|
||||
## Docker image
|
||||
One way to build the Docker image is to run the following command in the project root directory:
|
||||
```sh
|
||||
docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT=$(git rev-parse HEAD) .
|
||||
```
|
||||
|
||||
The Docker image can be built from the pre-built Python packages with the task command `task docker:build-from-package`. This is equivalent to:
|
||||
```sh
|
||||
docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT=$(git rev-parse HEAD) --build-context packages=dist .
|
||||
```
|
||||
8
docs/docs/documentation/community-guide/bring-api.md
Normal file
8
docs/docs/documentation/community-guide/bring-api.md
Normal file
@@ -0,0 +1,8 @@
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
Mealie supports adding the ingredients of a recipe to your [Bring](https://www.getbring.com/) shopping list, as you can
|
||||
see [here](https://docs.mealie.io/documentation/getting-started/features/#recipe-actions).
|
||||
However, for this to work, your Mealie instance needs to be exposed to the open Internet so that the Bring servers can access its information. If you don't want your server to be publicly accessible for security reasons, you can use the [Mealie-Bring-API](https://github.com/felixschndr/mealie-bring-api) written by a community member. This integration is entirely local and does not require any service to be exposed to the Internet.
|
||||
|
||||
This is a small web server that runs locally next to your Mealie instance, and instead of Bring pulling the data from you, it pushes the data to Bring. [Check out the project](https://github.com/felixschndr/mealie-bring-api) for more information and installation instructions.
|
||||
27
docs/docs/documentation/community-guide/ios-shortcut.md
Normal file
27
docs/docs/documentation/community-guide/ios-shortcut.md
Normal file
@@ -0,0 +1,27 @@
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
An easy way to add recipes to Mealie from an Apple device is via an Apple Shortcut. This is a short guide to install an configure a shortcut able to add recipes via a link or image(s).
|
||||
|
||||
*Note: if adding via images make sure to enable [Mealie's openai integration](https://docs.mealie.io/documentation/getting-started/installation/open-ai/)*
|
||||
|
||||
## Javascript can only be run via Shortcuts on the Safari browser on MacOS and iOS. If you do not use Safari you may skip this section
|
||||
Some sites have begun blocking AI scraping bots, inadvertently blocking the recipe scraping library Mealie uses as well. To circumvent this, the shortcut uses javascript to capture the raw html loaded in the browser and sends that to mealie when possible.
|
||||
|
||||
**iOS**
|
||||
|
||||
Settings app -> apps -> Shortcuts -> Advanced -> Allow Running Scripts
|
||||
|
||||
**MacOS**
|
||||
|
||||
Shortcuts app -> Settings (CMD ,) -> Advanced -> Allow Running Scripts
|
||||
|
||||
## Initial setup
|
||||
An API key is needed to authenticate with mealie. To create an api key for a user, navigate to http://YOUR_MEALIE_URL/user/profile/api-tokens. Alternatively you can create a key via the mealie home page by clicking the user's profile pic in the top left -> Api Tokens
|
||||
|
||||
The shortcut can be installed via **[This link](https://www.icloud.com/shortcuts/52834724050b42aebe0f2efd8d067360)**. Upon install, replace "MEALIE_API_KEY" with the API key generated previously and "MEALIE_URI" with the full URL used to access your mealie instance e.g. "http://10.0.0.5:9000" or "https://mealie.domain.com".
|
||||
|
||||
## Using the shortcut
|
||||
Once installed, the shortcut will automatically appear as an option when sharing an image or webpage. It can also be useful to add the shortcut to the home screen of your device. If selected from the home screen or shortcuts app, a menu will appear with prompts to import via **taking photo(s)**, **selecting photo(s)**, **scanning a URL**, or **pasting a URL**.
|
||||
|
||||
*Note: despite the mealie API being able to accept multiple recipe images for import it is currently impossible to send multiple files in 1 web request via Shortcuts. Instead, the shortcut combines the images into a singular, vertically-concatenated image to send to mealie. This can result in slightly less-accurate text recognition.*
|
||||
@@ -1,82 +0,0 @@
|
||||
# Using iOS Shortcuts with Mealie
|
||||
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
Don't know what an iOS shortcut is? Neither did I! Experienced iOS users may already be familiar with this utility but for the uninitiated, here is the official Apple explanation:
|
||||
|
||||
> A shortcut is a quick way to get one or more tasks done with your apps. The Shortcuts app lets you create your own shortcuts with multiple steps. For example, build a “Surf Time” shortcut that grabs the surf report, gives an ETA to the beach, and launches your surf music playlist.
|
||||
|
||||
Basically it is a visual scripting language that lets a user build an automation in a guided fashion. The automation can be [shared with anyone](https://www.icloud.com/shortcuts/94aa272af5ff4d2c8fe5e13a946f89a9) but if it is a user creation, you'll have to jump through a few hoops to make an untrusted automation work on your device.
|
||||
|
||||
## Setup Video
|
||||
|
||||
The following YouTube video walks through setting up the shortcut in 3 minutes for those who prefer following along visually.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/XZk6S1MVUrE?si=HGH07RbK-Ip_1qFz" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
## Guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before setting up the shortcut, make sure you have the following information ready and easily accessable on your Apple device.
|
||||
|
||||
1. The URL of your Mealie instance
|
||||
2. An API Key for your user
|
||||
3. A Gemini API Key from [Google AI Studio](https://makersuite.google.com)
|
||||
|
||||
!!! note
|
||||
A Gemini API Key is not required for importing URLs from Safari or your Camera, however you will not be able to take a photo of a recipe and import it without a Gemini key.
|
||||
|
||||
Google AI Studio is currently only available in [certain countries and languages](https://ai.google.dev/available_regions). Most notably it is not currently available in Europe.
|
||||
|
||||
### Setup
|
||||
|
||||
On the Apple device you wish to add the shortcut to, click on [this link](https://www.icloud.com/shortcuts/94aa272af5ff4d2c8fe5e13a946f89a9) to begin the setup of the shortcut.
|
||||
|
||||

|
||||
|
||||
Next, you need to replace `url` and `port` with the information for your Mealie instance.
|
||||
|
||||
If you have a domain that you use (e.g. `https://mealie.example.com`), put that here. If you just run local, then you need to put in your Mealie instance IP and the port you use (e.g. the default is `9925`).
|
||||
|
||||

|
||||
|
||||
Next, you need to replace `MEALIE_API_KEY` with your API token.
|
||||
|
||||

|
||||
|
||||
Finally, replace `GEMINI_API_KEY` with the one you got from [Google AI Studio](https://makersuite.google.com)
|
||||
|
||||

|
||||
|
||||
You may wish to [add the shortcut to your home screen](https://support.apple.com/guide/shortcuts/add-a-shortcut-to-the-home-screen-apd735880972/ios) for easier access.
|
||||
|
||||
## Features
|
||||
|
||||
- Share a website from Safari with Mealie to import via URL.
|
||||
- Share a recipe photo from photos to perform OCR and import a physical recipe.
|
||||
- Trigger the shortcut and take a photo of a physical recipe to import.
|
||||
- Trigger the shortcut to select a photo from your Photos app to import.
|
||||
- Trigger the shortcut to take a picture of a URL (like on the bottom of a printed recipe) to import.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Sometimes Gemini will not be able to parse a recipe, and you will get an error. Users have found success with a combination of the following:
|
||||
|
||||
1. #### Try Again
|
||||
Sometimes Gemini returns the wrong information which causes the import to fail. Often, trying again will be successful.
|
||||
|
||||
2. #### Photo Quality
|
||||
Make sure there is no large glare or shadow over the picture, and you have all the text in frame.
|
||||
|
||||
3. #### Edit the Photo
|
||||
Users have found success by cropping the picture to just the recipe card, adding a "mono" filter, and cranking up the exposure before importing.
|
||||
|
||||
## History
|
||||
|
||||
User [brasilikum](https://github.com/brasilikum) opened an issue on the main repo about how they had created an [iOS shortcut](https://github.com/mealie-recipes/mealie/issues/103) for interested users.
|
||||
|
||||
This original method broke after the transition to version 1.X and an issue was raised on [Github](https://github.com/mealie-recipes/mealie/issues/2092) GitHub user [Zippyy](https://github.com/zippyy) has helped to create a working shortcut for version 1.X.
|
||||
|
||||
When OCR was removed from Mealie, GitHub user [hunterjm](https://github.com/zippyy) created a new shortcut that uses Apple's built-in OCR and Google Gemini to enhance and replace that functionality.
|
||||
@@ -10,7 +10,7 @@
|
||||
Mealie supports 3rd party authentication via [OpenID Connect (OIDC)](https://openid.net/connect/), an identity layer built on top of OAuth2. OIDC is supported by many Identity Providers (IdP), including:
|
||||
|
||||
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
|
||||
- [Authelia](https://www.authelia.com/configuration/identity-providers/open-id-connect/)
|
||||
- [Authelia](https://www.authelia.com/integration/openid-connect/mealie/)
|
||||
- [Keycloak](https://www.keycloak.org/docs/latest/securing_apps/#_oidc)
|
||||
- [Okta](https://www.okta.com/openid-connect/)
|
||||
|
||||
@@ -52,6 +52,8 @@ Before you can start using OIDC Authentication, you must first configure a new c
|
||||
|
||||
Take the client id and your discovery URL and update your environment variables to include the required OIDC variables described in [Installation - Backend Configuration](../installation/backend-config.md#openid-connect-oidc).
|
||||
|
||||
You might also want to set ALLOW_PASSWORD_LOGIN to false, to hide the username+password inputs, if you want to allow logins only via OIDC.
|
||||
|
||||
### Groups
|
||||
|
||||
There are two (optional) [environment variables](../installation/backend-config.md#openid-connect-oidc) that can control which of the users in your IdP can log in to Mealie and what permissions they will have. Keep in mind that these groups **do not necessarily correspond to groups in Mealie**. The groups claim is configurable via the `OIDC_GROUPS_CLAIM` environment variable. The groups should be **defined in your IdP** and be returned in the configured claim value.
|
||||
|
||||
@@ -36,6 +36,10 @@ Before you can start using OIDC Authentication, you must first configure a new c
|
||||
http://localhost:9091/login
|
||||
https://mealie.example.com/login
|
||||
|
||||
If you are hosting Mealie behind a reverse proxy (nginx, Caddy, ...) to terminate TLS, make sure to start Mealie's Gunicorn server
|
||||
with `--forwarded-allow-ips=<ip-of-proxy>`, otherwise the `X-Forwarded-*` headers will be ignored and the generated OIDC redirect
|
||||
URI will use the wrong scheme (http instead of https). This will lead to authentication errors with strict OIDC providers.
|
||||
|
||||
3. Configure origins
|
||||
|
||||
If your identity provider enforces CORS on any endpoints, you will need to specify your Mealie URL as an Allowed Origin.
|
||||
|
||||
@@ -1,164 +1,308 @@
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
## How do I enable "smart" ingredient handling?
|
||||
## Features and Functionality
|
||||
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And, making shopping lists from recipes that have shared ingredients can yield multiple lines of the same ingredient. **But**, Mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
### Set up your Foods and Units
|
||||
Do the following just **once**. Doing this applies to your whole group, so be careful.
|
||||
??? question "How do I enable 'smart' ingredient handling?"
|
||||
|
||||
1. Click on your name in the upper left corner to get to your settings
|
||||
2. In the bottom right, select `Manage Data`
|
||||
3. In the Management page, make sure that a little orange button says `Foods`
|
||||
4. If your Foods database is empty, click `Seed` and choose your language. You should end up with a list of foods. (Wait a bit for seeding to happen, and try not to seed more than once or you will have duplicates)
|
||||
5. Click the little orange `Foods` button and now choose `Units`.
|
||||
6. Click `Seed` and choose your language. You should end up with a list of units (e.g. `tablespoon`)
|
||||
### How do I enable "smart" ingredient handling?
|
||||
|
||||
Initial seeding of Units is pretty complete, but there are many Foods in the world. You'll probably find that you need to add Foods to the database during parsing for the first several recipes. Once you have a well-populated Food database, there are API routes to parse ingredients automatically in bulk. But this is not a good idea without a very complete set of Foods.
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And, making shopping lists from recipes that have shared ingredients can yield multiple lines of the same ingredient. **But**, Mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
|
||||
### Set up Recipes to use Foods and Units
|
||||
Do the following for each recipe you want to intelligently handle ingredients.
|
||||
<p style="font-size: 0.75rem; font-weight: 500;">Set up your Foods and Units</p>
|
||||
Do the following just **once**. Doing this applies to your whole group, so be careful.
|
||||
|
||||
1. Go to a recipe
|
||||
2. Click the Edit button/icon
|
||||
3. Click the Recipe Settings gear and deselect `Disable Ingredient Amounts`
|
||||
4. Save
|
||||
5. The ingredients should now look a little weird (`1 1 cup broth` and so on)
|
||||
6. Click the Edit button/icon again
|
||||
7. Scroll to the ingredients and you should see new fields for Amount, Unit, Food, and Note. The Note in particular will contain the original text of the Recipe.
|
||||
8. Click `Parse` and you will be taken to the ingredient parsing page.
|
||||
9. Choose your parser. The `Natural Language Parser` works very well, but you can also use the `Brute Parser`, or the `OpenAI Parser` if you've [enabled OpenAI support](./installation/backend-config.md#openai).
|
||||
10. Click `Parse All`, and your ingredients should be separated out into Units and Foods based on your seeding in Step 1 above.
|
||||
11. For ingredients where the Unit or Food was not found, you can click a button to accept an automatically suggested Food to add to the database. Or, manually enter the Unit/Food and hit `Enter` (or click `Create`) to add it to the database
|
||||
12. When done, click `Save All` and you will be taken back to the recipe. Now the Unit and Food fields of the recipe should be filled out.
|
||||
1. Click on your name in the upper left corner to get to your settings
|
||||
2. In the bottom right, select `Manage Data`
|
||||
3. In the Management page, make sure that a little orange button says `Foods`
|
||||
4. If your Foods database is empty, click `Seed` and choose your language. You should end up with a list of foods. (Wait a bit for seeding to happen, and try not to seed more than once or you will have duplicates)
|
||||
5. Click the little orange `Foods` button and now choose `Units`.
|
||||
6. Click `Seed` and choose your language. You should end up with a list of units (e.g. `tablespoon`)
|
||||
|
||||
Scaling up this recipe or adding it to a Shopping List will now smartly take care of ingredient amounts and duplicate combinations.
|
||||
Initial seeding of Units is pretty complete, but there are many Foods in the world. You'll probably find that you need to add Foods to the database during parsing for the first several recipes. Once you have a well-populated Food database, there are API routes to parse ingredients automatically in bulk. But this is not a good idea without a very complete set of Foods.
|
||||
|
||||
## Is it safe to upgrade Mealie?
|
||||
<p style="font-size: 0.75rem; font-weight: 500;">Set up Recipes to use Foods and Units</p>
|
||||
|
||||
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Note that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
|
||||
Do the following for each recipe you want to intelligently handle ingredients.
|
||||
|
||||
- [Migration From v0.5.x](./migrating-to-mealie-v1.md)
|
||||
1. Go to a recipe
|
||||
2. Click the Edit button/icon
|
||||
3. Click the Recipe Settings gear and deselect `Disable Ingredient Amounts`
|
||||
4. Save
|
||||
5. The ingredients should now look a little weird (`1 1 cup broth` and so on)
|
||||
6. Click the Edit button/icon again
|
||||
7. Scroll to the ingredients and you should see new fields for Amount, Unit, Food, and Note. The Note in particular will contain the original text of the Recipe.
|
||||
8. Click `Parse` and you will be taken to the ingredient parsing page.
|
||||
9. Choose your parser. The `Natural Language Parser` works very well, but you can also use the `Brute Parser`, or the `OpenAI Parser` if you've [enabled OpenAI support](./installation/backend-config.md#openai).
|
||||
10. Click `Parse All`, and your ingredients should be separated out into Units and Foods based on your seeding in Step 1 above.
|
||||
11. For ingredients where the Unit or Food was not found, you can click a button to accept an automatically suggested Food to add to the database. Or, manually enter the Unit/Food and hit `Enter` (or click `Create`) to add it to the database
|
||||
12. When done, click `Save All` and you will be taken back to the recipe. Now the Unit and Food fields of the recipe should be filled out.
|
||||
|
||||
## How can I change the theme?
|
||||
|
||||
You can change the theme by settings the environment variables.
|
||||
|
||||
- [Backend Config - Themeing](./installation/backend-config.md#themeing)
|
||||
|
||||
## How can I change the login session timeout?
|
||||
|
||||
Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
|
||||
|
||||
- [Backend Config](./installation/backend-config.md)
|
||||
|
||||
## Can I serve Mealie on a subpath?
|
||||
|
||||
No. Due to limitations from the JavaScript Framework, Mealie doesn't support serving Mealie on a subpath.
|
||||
|
||||
## Can I install Mealie without docker?
|
||||
|
||||
Yes, you can install Mealie on your local machine. HOWEVER, it is recommended that you don't. Managing non-system versions of python, node, and npm is a pain. Moreover, updating and upgrading your system with this configuration is unsupported and will likely require manual interventions.
|
||||
|
||||
## What is fuzzy search and how do I use it?
|
||||
Mealie can use fuzzy search, which is robust to minor typos. For example, searching for "brocolli" will still find your recipe for "broccoli soup". But fuzzy search is only functional on a Postgres database backend. To enable fuzzy search you will need to migrate to Postgres:
|
||||
|
||||
1. Backup your database and download the .zip file (same as when [migrating](./migrating-to-mealie-v1.md))
|
||||
2. Set up a [Postgres](./installation/postgres.md) instance of Mealie
|
||||
3. Upload the backup .zip and click to apply it (as as migration)
|
||||
|
||||
## How can I attach an image or video to a Recipe?
|
||||
|
||||
Mealie's Recipe Steps and other fields support markdown syntax and therefore support images and videos. To attach an image to the recipe, you can upload it as an asset and use the provided copy button to generate the html image tag required to render the image. For videos, Mealie provides no way to host videos. You'll need to host your videos with another provider and embed them in your recipe. Generally, the video provider will provide a link to the video and the html tag required to render the video. For example, YouTube provides the following link that works inside a step. You can adjust the width and height attributes as necessary to ensure a fit.
|
||||
|
||||
```html
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/nAUwKeO93bY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
```
|
||||
|
||||
## How can I unlock my account?
|
||||
|
||||
If your account has been locked by bad password attempts, you can use an administrator account to unlock another account. Alternatively, you can unlock all accounts via a script within the container.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /app/mealie/scripts/reset_locked_users.py
|
||||
```
|
||||
|
||||
## How can I change my password?
|
||||
|
||||
You can change your password by going to the user profile page and clicking the "Change Password" button. Alternatively you can use the following script to change your password via the CLI if you are locked out of your account.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /app/mealie/scripts/change_password.py
|
||||
```
|
||||
|
||||
## I can't log in with external auth. How can I change my authentication method?
|
||||
|
||||
Follow the [steps above](#how-can-i-change-my-password) for changing your password. You will be prompted if you would like to switch your authentication method back to local auth so you can log in again.
|
||||
|
||||
## How do private groups, households, and recipes work?
|
||||
|
||||
Managing private groups and recipes can be confusing. The following diagram and notes should help explain how they work to determine if a recipe can be shared publicly.
|
||||
|
||||
- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions
|
||||
- Private groups block all access to recipes, including those that are public, except as noted above.
|
||||
- Private households, similar to private groups, block all access to recipes, except as noted above.
|
||||
- Households with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private recipes block all access to the recipe from public links. This does not affect Private Links.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
r1: Request Access
|
||||
p1: Using Private Link?
|
||||
p2: Is Group Private?
|
||||
p3: Is Household Private?
|
||||
p4: Is Recipe Private?
|
||||
s1: Deny Access
|
||||
n1: Allow Access
|
||||
Scaling up this recipe or adding it to a Shopping List will now smartly take care of ingredient amounts and duplicate combinations.
|
||||
|
||||
|
||||
r1 --> p1
|
||||
p1 --> p2: No
|
||||
p1 --> n1: Yes
|
||||
??? question "How do I enable Nutritional Values?"
|
||||
|
||||
p2 --> s1: Yes
|
||||
p2 --> p3: No
|
||||
### How do I enable Nutritional Values?
|
||||
|
||||
p3 --> s1: Yes
|
||||
p3 --> p4: No
|
||||
Mealie can store Nutritional Information for Recipes. Please note that the values you enter are static for the recipe and no scaling is being done when changing Servings / Yield.
|
||||
|
||||
p4 --> s1: Yes
|
||||
p4 --> n1: No
|
||||
```
|
||||
Do the following to enable Nutritional Values on individual Recipes, or to modify your Household Recipe Preferences
|
||||
|
||||
For more information on public access, check out the [Permissions and Public Access guide](./usage/permissions-and-public-access.md). For more information on groups vs. households, check out the [Groups and Households](./features.md#groups-and-households) section in the Features guide.
|
||||
**Show Nutritional Values on a Single Recipe**
|
||||
|
||||
## Can I use fail2ban with Mealie?
|
||||
Yes, Mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux.
|
||||
1. Go to a recipe
|
||||
2. Click the Edit button/icon
|
||||
3. Click the Recipe Settings gear and select `Show Nutritional Values`
|
||||
4. Scroll down to manually fill out the Nutritional Values
|
||||
5. Save
|
||||
|
||||
Your fail2ban usage should look like the following:
|
||||
```
|
||||
Use datepattern : %d-%b-%y %H:%M:%S : Day-MON-Year2 24hour:Minute:Second
|
||||
Use failregex line : ^ERROR:\s+Incorrect username or password from <HOST>
|
||||
```
|
||||
**Show Nutritional Values by default**
|
||||
|
||||
## Why an API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based on Meal Plan data to remind you to defrost the chicken, marinate the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
1. Click your username in the top left
|
||||
2. Click the 'Household Settings' button
|
||||
3. Under 'Household Recipe Preferences', click to select 'Show nutrition information'
|
||||
4. Click 'Update'
|
||||
|
||||
## Why a database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project, it is a valid concern to be worried about your data. Mealie specifically addresses this concern by providing automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in control of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
|
||||
As to why we need a database?
|
||||
??? question "Why Link Ingredients to a Recipe Step?"
|
||||
|
||||
- **Developer Experience:** Without a database, a lot of the work to maintain your data is taken on by the developer instead of a battle-tested platform for storing data.
|
||||
- **Multi User Support:** With a solid database as backend storage for your data, Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
|
||||
### Why Link Ingredients to a Recipe Step?
|
||||
|
||||
## Why is there no "Keep Screen Alive" button when I access a recipe?
|
||||
You've perhaps visited the Mealie Demo and noticed that it had a "Keep Screen Alive" button, but it doesn't show up in your own Mealie instance.
|
||||
There are typically two possible reasons for this:
|
||||
1. You're accessing your Mealie instance without using HTTPS. The Wake Lock API is only available if HTTPS is used. Read more here: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API
|
||||
2. You're accessing your Mealie instance on a browser which doesn't support the API. You can test this here: https://vueuse.org/core/useWakeLock/#demo
|
||||
Mealie allows you to link ingredients to specific steps in a recipe, ensuring you know exactly when to add each ingredient during the cooking process.
|
||||
|
||||
Solving the above points will most likely resolve your issues. However, if you're still having problems, you are welcome to create an issue. Just remember to add that you've tried the above two options first in your description.
|
||||
**Link Ingredients to Steps in a Recipe**
|
||||
|
||||
1. Go to a recipe
|
||||
2. Click the Edit button/icon
|
||||
3. Scroll down to the step you want to link ingredients to
|
||||
4. Click the ellipsis button next to the step and click 'Link Ingredients'
|
||||
5. Check off the Ingredient(s) that you want to link to that step
|
||||
6. Optionally, click 'Next step' to continue linking remaining ingredients to steps, or click 'Save' to Finish
|
||||
7. Click 'Save' on the Recipe
|
||||
|
||||
You can optionally link the same ingredient to multiple steps, which is useful for prepping an ingredient in one step and using it in another.
|
||||
|
||||
??? question "What is fuzzy search and how do I use it?"
|
||||
|
||||
### What is fuzzy search and how do I use it?
|
||||
|
||||
Mealie can use fuzzy search, which is robust to minor typos. For example, searching for "brocolli" will still find your recipe for "broccoli soup". But fuzzy search is only functional on a Postgres database backend. To enable fuzzy search you will need to migrate to Postgres:
|
||||
|
||||
1. Backup your database and download the .zip file (same as when [migrating](./migrating-to-mealie-v1.md))
|
||||
2. Set up a [Postgres](./installation/postgres.md) instance of Mealie
|
||||
3. Upload the backup .zip and click to apply it (as as migration)
|
||||
|
||||
??? question "How can I attach an image or video to a Recipe?"
|
||||
|
||||
### How can I attach an image or video to a Recipe?
|
||||
|
||||
Mealie's Recipe Steps and other fields support markdown syntax and therefore support images and videos. To attach an image to the recipe, you can upload it as an asset and use the provided copy button to generate the html image tag required to render the image. For videos, Mealie provides no way to host videos. You'll need to host your videos with another provider and embed them in your recipe. Generally, the video provider will provide a link to the video and the html tag required to render the video. For example, YouTube provides the following link that works inside a step. You can adjust the width and height attributes as necessary to ensure a fit.
|
||||
|
||||
```html
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/nAUwKeO93bY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
```
|
||||
|
||||
## Customization and Configuration
|
||||
|
||||
??? question "How can I change the theme?"
|
||||
|
||||
### How can I change the theme?
|
||||
|
||||
You can change the theme by settings the environment variables.
|
||||
|
||||
- [Backend Config - Themeing](./installation/backend-config.md#themeing)
|
||||
|
||||
|
||||
??? question "How can I change the login session timeout?"
|
||||
|
||||
### How can I change the login session timeout?
|
||||
|
||||
Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
|
||||
|
||||
- [Backend Config](./installation/backend-config.md)
|
||||
|
||||
|
||||
??? question "Can I serve Mealie on a subpath?"
|
||||
|
||||
### Can I serve Mealie on a subpath?
|
||||
|
||||
No. Due to limitations from the JavaScript Framework, Mealie doesn't support serving Mealie on a subpath.
|
||||
|
||||
|
||||
??? question "Can I install Mealie without docker?"
|
||||
|
||||
### Can I install Mealie without docker?
|
||||
|
||||
Yes, you can install Mealie on your local machine. HOWEVER, it is recommended that you don't. Managing non-system versions of python, node, and npm is a pain. Moreover, updating and upgrading your system with this configuration is unsupported and will likely require manual interventions.
|
||||
|
||||
|
||||
## Account Management
|
||||
|
||||
??? question "How can I unlock my account?"
|
||||
|
||||
### How can I unlock my account?
|
||||
|
||||
If your account has been locked by bad password attempts, you can use an administrator account to unlock another account. Alternatively, you can unlock all accounts via a script within the container.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/reset_locked_users.py
|
||||
```
|
||||
|
||||
|
||||
??? question "How can I reset admin privileges for my account?"
|
||||
|
||||
### How can I reset admin privileges for my account?
|
||||
|
||||
If you've lost admin privileges and no other admin can restore them, you can use the Command Line Interface (CLI) to grant admin access.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/make_admin.py
|
||||
```
|
||||
|
||||
|
||||
??? question "How can I change my password?"
|
||||
|
||||
### How can I change my password?
|
||||
|
||||
You can change your password by going to the user profile page and clicking the "Change Password" button. Alternatively you can use the following script to change your password via the CLI if you are locked out of your account.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/change_password.py
|
||||
```
|
||||
|
||||
|
||||
??? question "I can't log in with external auth. How can I change my authentication method?"
|
||||
|
||||
### I can't log in with external auth. How can I change my authentication method?
|
||||
|
||||
Follow the [steps above](#how-can-i-change-my-password) for changing your password. You will be prompted if you would like to switch your authentication method back to local auth so you can log in again.
|
||||
|
||||
|
||||
## Collaboration and Privacy
|
||||
|
||||
??? question "How do private groups, households, and recipes work?"
|
||||
|
||||
### How do private groups, households, and recipes work?
|
||||
|
||||
Managing private groups and recipes can be confusing. The following diagram and notes should help explain how they work to determine if a recipe can be shared publicly.
|
||||
|
||||
- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions
|
||||
- Private groups block all access to recipes, including those that are public, except as noted above.
|
||||
- Private households, similar to private groups, block all access to recipes, except as noted above.
|
||||
- Households with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private recipes block all access to the recipe from public links. This does not affect Private Links.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
r1: Request Access
|
||||
p1: Using Private Link?
|
||||
p2: Is Group Private?
|
||||
p3: Is Household Private?
|
||||
p4: Is Recipe Private?
|
||||
s1: Deny Access
|
||||
n1: Allow Access
|
||||
|
||||
|
||||
r1 --> p1
|
||||
p1 --> p2: No
|
||||
p1 --> n1: Yes
|
||||
|
||||
p2 --> s1: Yes
|
||||
p2 --> p3: No
|
||||
|
||||
p3 --> s1: Yes
|
||||
p3 --> p4: No
|
||||
|
||||
p4 --> s1: Yes
|
||||
p4 --> n1: No
|
||||
```
|
||||
|
||||
For more information on public access, check out the [Permissions and Public Access guide](./usage/permissions-and-public-access.md). For more information on groups vs. households, check out the [Groups and Households](./features.md#groups-and-households) section in the Features guide.
|
||||
|
||||
|
||||
## Security and Maintenance
|
||||
|
||||
??? question "How can I use Mealie externally?"
|
||||
|
||||
### How can I use Mealie externally
|
||||
|
||||
Exposing Mealie or any service to the internet can pose significant security risks. Before proceeding, carefully evaluate the potential impacts on your system. Due to the unique nature of each network, we cannot provide specific steps for your setup.
|
||||
|
||||
There is a community guide available for one way to potentially set this up, and you could reach out on Discord for further discussion on what may be best for your network.
|
||||
|
||||
|
||||
??? question "Can I use fail2ban with Mealie?"
|
||||
|
||||
### Can I use fail2ban with Mealie?
|
||||
|
||||
Yes, Mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux.
|
||||
|
||||
Your fail2ban usage should look like the following:
|
||||
```
|
||||
Use datepattern : %d-%b-%y %H:%M:%S : Day-MON-Year2 24hour:Minute:Second
|
||||
Use failregex line : ^ERROR:\s+Incorrect username or password from <HOST>
|
||||
```
|
||||
|
||||
|
||||
??? question "Is it safe to upgrade Mealie?"
|
||||
|
||||
### Is it safe to upgrade Mealie?
|
||||
|
||||
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Note that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
|
||||
|
||||
- [Migration From v0.5.x](./migrating-to-mealie-v1.md)
|
||||
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
|
||||
??? question "Why setup Email?"
|
||||
|
||||
### Why setup Email?
|
||||
|
||||
Mealie uses email to send account invites and password resets. If you don't use these features, you don't need to set up email. There are also other methods to perform these actions that do not require the setup of Email.
|
||||
|
||||
Email settings can be adjusted via environment variables on the backend container:
|
||||
|
||||
- [Backend Config](./installation/backend-config.md)
|
||||
|
||||
Note that many email providers (e.g., Gmail, Outlook) are disabling SMTP Auth and requiring Modern Auth, which Mealie currently does not support. You may need to use an SMTP relay or third-party SMTP provider, such as SMTP2GO.
|
||||
|
||||
??? question "Why an API?"
|
||||
|
||||
### Why an API?
|
||||
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based on Meal Plan data to remind you to defrost the chicken, marinate the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
|
||||
|
||||
??? question "Why a database?"
|
||||
|
||||
### Why a database?
|
||||
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project, it is a valid concern to be worried about your data. Mealie specifically addresses this concern by providing automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in control of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
|
||||
As to why we need a database?
|
||||
|
||||
- **Developer Experience:** Without a database, a lot of the work to maintain your data is taken on by the developer instead of a battle-tested platform for storing data.
|
||||
- **Multi User Support:** With a solid database as backend storage for your data, Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
|
||||
|
||||
|
||||
## Usability
|
||||
|
||||
??? question "Why is there no 'Keep Screen Alive' button when I access a recipe?"
|
||||
|
||||
### Why is there no "Keep Screen Alive" button when I access a recipe?
|
||||
|
||||
You've perhaps visited the Mealie Demo and noticed that it had a "Keep Screen Alive" button, but it doesn't show up in your own Mealie instance.
|
||||
There are typically two possible reasons for this:
|
||||
1. You're accessing your Mealie instance without using HTTPS. The Wake Lock API is only available if HTTPS is used. Read more here: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API
|
||||
2. You're accessing your Mealie instance on a browser which doesn't support the API. You can test this here: https://vueuse.org/core/useWakeLock/#demo
|
||||
|
||||
Solving the above points will most likely resolve your issues. However, if you're still having problems, you are welcome to create an issue. Just remember to add that you've tried the above two options first in your description.
|
||||
|
||||
@@ -35,7 +35,7 @@ Mealie has a robust and flexible recipe organization system with a few different
|
||||
|
||||
#### Categories
|
||||
|
||||
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**.
|
||||
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, **Side**, or **Drinks**.
|
||||
|
||||
[Categories Demo](https://demo.mealie.io/g/home/recipes/categories){ .md-button .md-button--primary }
|
||||
|
||||
@@ -84,7 +84,30 @@ The meal planner has the concept of plan rules. These offer a flexible way to us
|
||||
|
||||
The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list or link a recipe and all of it's ingredients to track meals during the week.
|
||||
|
||||
Managing shopping lists can be done from the Sidebar > Shopping Lists.
|
||||
|
||||
Here you will be able to:
|
||||
- See items already on the Shopping List
|
||||
- See linked recipes with ingredients
|
||||
- Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it.
|
||||
- Check off an item
|
||||
- Add / Change / Remove / Sort Items via the grid icon
|
||||
- Be sure if you are modifying an ingredient to click the 'Save' icon.
|
||||
- Add / Change / Remove / Sort Labels
|
||||
- 'No Label' will always be on the top, others can be Reordered via the 'Reorder Labels' button
|
||||
|
||||
!!! tip
|
||||
If you accidentally checked off an item, you can uncheck it by expanding 'items checked' and unchecking it. This will add it back to the Shopping List.
|
||||
|
||||
!!! tip
|
||||
You can use Labels to categorize your ingredients. You may want to Label by Food Type (Frozen, Fresh, etc), by Store, Tool, Recipe, or more. Play around with this to see what works best for you.
|
||||
|
||||
!!! tip
|
||||
You can toggle 'Food' on items so that if you add multiple of the same food / ingredient, Mealie will automatically combine them together. Do this by editing an item in the Shopping List and clicking the 'Apple' icon. If you then have recipes that contain "1 | cup | cheese" and "2 | cup | cheese" this would be combined to show "3 cups of cheese."
|
||||
|
||||
[See FAQ for more information](../getting-started/faq.md)
|
||||
|
||||
|
||||
[Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
|
||||
|
||||
## Integrations
|
||||
@@ -94,9 +117,9 @@ Mealie is designed to integrate with many different external services. There are
|
||||
### Notifiers
|
||||
|
||||
Notifiers are event-driven notifications sent when specific actions are performed within Mealie. Some actions include:
|
||||
- creating a recipe
|
||||
- adding items to a shopping list
|
||||
- creating a new mealplan
|
||||
- Creating / Updating a recipe
|
||||
- Adding items to a shopping list
|
||||
- Creating a new mealplan
|
||||
|
||||
Notifiers use the [Apprise library](https://github.com/caronc/apprise/wiki), which integrates with a large number of notification services. In addition, certain custom notifiers send basic event data to the consumer (e.g. the `id` of the resource). These include:
|
||||
|
||||
@@ -139,6 +162,9 @@ Below is a list of all valid merge fields:
|
||||
- ${id}
|
||||
- ${slug}
|
||||
- ${url}
|
||||
- ${servings}
|
||||
- ${yieldQuantity}
|
||||
- ${yieldText}
|
||||
|
||||
To add, modify, or delete Recipe Actions, visit the Data Management page (more on that below).
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
| API_DOCS | True | Turns on/off access to the API documentation locally |
|
||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
|
||||
| ALLOW_PASSWORD_LOGIN | true | Whether or not to display the username+password input fields. Keep set to true unless you use OIDC authentication |
|
||||
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
|
||||
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug) |
|
||||
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
|
||||
@@ -31,27 +32,27 @@
|
||||
|
||||
### Database
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------------------- | :------: | ----------------------------------------------------------------------- |
|
||||
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
||||
| POSTGRES_USER | mealie | Postgres database user |
|
||||
| POSTGRES_PASSWORD | mealie | Postgres database password |
|
||||
| POSTGRES_SERVER | postgres | Postgres database server address |
|
||||
| POSTGRES_PORT | 5432 | Postgres database port |
|
||||
| POSTGRES_DB | mealie | Postgres database name |
|
||||
| POSTGRES_URL_OVERRIDE | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables |
|
||||
| Variables | Default | Description |
|
||||
| ------------------------------------------------------- | :------: | ----------------------------------------------------------------------- |
|
||||
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
||||
| POSTGRES_USER<super>[†][secrets]</super> | mealie | Postgres database user |
|
||||
| POSTGRES_PASSWORD<super>[†][secrets]</super> | mealie | Postgres database password |
|
||||
| POSTGRES_SERVER<super>[†][secrets]</super> | postgres | Postgres database server address |
|
||||
| POSTGRES_PORT<super>[†][secrets]</super> | 5432 | Postgres database port |
|
||||
| POSTGRES_DB<super>[†][secrets]</super> | mealie | Postgres database name |
|
||||
| POSTGRES_URL_OVERRIDE<super>[†][secrets]</super> | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables |
|
||||
|
||||
### Email
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ------------------ | :-----: | ------------------------------------------------- |
|
||||
| SMTP_HOST | None | Required For email |
|
||||
| SMTP_PORT | 587 | Required For email |
|
||||
| SMTP_FROM_NAME | Mealie | Required For email |
|
||||
| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' |
|
||||
| SMTP_FROM_EMAIL | None | Required For email |
|
||||
| SMTP_USER | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
| SMTP_PASSWORD | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------------------------- | :-----: | ------------------------------------------------- |
|
||||
| SMTP_HOST<super>[†][secrets]</super> | None | Required For email |
|
||||
| SMTP_PORT<super>[†][secrets]</super> | 587 | Required For email |
|
||||
| SMTP_FROM_NAME | Mealie | Required For email |
|
||||
| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' |
|
||||
| SMTP_FROM_EMAIL | None | Required For email |
|
||||
| SMTP_USER<super>[†][secrets]</super> | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
| SMTP_PASSWORD<super>[†][secrets]</super> | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
|
||||
### Webworker
|
||||
|
||||
@@ -72,21 +73,21 @@ Use this only when mealie is run without a webserver or reverse proxy.
|
||||
|
||||
### LDAP
|
||||
|
||||
| Variables | Default | Description |
|
||||
| -------------------- | :-----: | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
|
||||
| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) |
|
||||
| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP |
|
||||
| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
| LDAP_ENABLE_STARTTLS | False | Optional. Use STARTTLS to connect to the server |
|
||||
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
|
||||
| LDAP_QUERY_BIND | None | Optional bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`). If `None` then anonymous bind will be used |
|
||||
| LDAP_QUERY_PASSWORD | None | Optional password for the bind user used in LDAP_QUERY_BIND |
|
||||
| LDAP_USER_FILTER | None | Optional LDAP filter to narrow down eligible users (e.g. `(memberOf=cn=mealie_user,dc=example,dc=com)`) |
|
||||
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
|
||||
| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id |
|
||||
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
|
||||
| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email |
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------------------------------- | :-----: | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
|
||||
| LDAP_SERVER_URL<super>[†][secrets]</super> | None | LDAP server URL (e.g. ldap://ldap.example.com) |
|
||||
| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP |
|
||||
| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
| LDAP_ENABLE_STARTTLS | False | Optional. Use STARTTLS to connect to the server |
|
||||
| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) |
|
||||
| LDAP_QUERY_BIND | None | Optional bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`). If `None` then anonymous bind will be used |
|
||||
| LDAP_QUERY_PASSWORD<super>[†][secrets]</super> | None | Optional password for the bind user used in LDAP_QUERY_BIND |
|
||||
| LDAP_USER_FILTER | None | Optional LDAP filter to narrow down eligible users (e.g. `(memberOf=cn=mealie_user,dc=example,dc=com)`) |
|
||||
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
|
||||
| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id |
|
||||
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
|
||||
| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email |
|
||||
|
||||
### OpenID Connect (OIDC)
|
||||
|
||||
@@ -94,23 +95,24 @@ Use this only when mealie is run without a webserver or reverse proxy.
|
||||
|
||||
For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
||||
|
||||
| Variables | Default | Description |
|
||||
|---------------------------------------------------|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
|
||||
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
|
||||
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
|
||||
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
|
||||
| OIDC_CLIENT_SECRET <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider |
|
||||
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
||||
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
||||
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed and you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
|
||||
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
|
||||
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
|
||||
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
|
||||
| OIDC_NAME_CLAIM | name | This is the claim which Mealie will use for the users Full Name |
|
||||
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
|
||||
| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. |
|
||||
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------------------------------------------------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
|
||||
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
|
||||
| OIDC_CONFIGURATION_URL<super>[†][secrets]</super> | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
|
||||
| OIDC_CLIENT_ID<super>[†][secrets]</super> | None | The client id of your configured client in your provider |
|
||||
| OIDC_CLIENT_SECRET<super>[†][secrets]</super> <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider |
|
||||
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate, regardless of the `OIDC_ADMIN_GROUP`. For more information see [this page](../authentication/oidc.md#groups) |
|
||||
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be made an admin. For more information see [this page](../authentication/oidc.md#groups) |
|
||||
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed an you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
|
||||
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
|
||||
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
|
||||
| OIDC_SIGNING_ALGORITHM | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
|
||||
| OIDC_NAME_CLAIM | name | This is the claim which Mealie will use for the users Full Name |
|
||||
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
|
||||
| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. |
|
||||
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
|
||||
### OpenAI
|
||||
|
||||
@@ -119,17 +121,17 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
||||
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md).
|
||||
For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`)
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ---------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| OPENAI_BASE_URL | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
| Variables | Default | Description |
|
||||
| ------------------------------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| OPENAI_BASE_URL<super>[†][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY<super>[†][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
|
||||
### Theming
|
||||
|
||||
@@ -154,24 +156,78 @@ Setting the following environmental variables will change the theme of the front
|
||||
|
||||
### Docker Secrets
|
||||
|
||||
Setting a credential can be done using secrets when running in a Docker container.
|
||||
This can be used to avoid leaking passwords through compose files, environment variables, or command-line history.
|
||||
For example, to configure the Postgres database password in Docker compose, create a file on the host that contains only the password, and expose that file to the Mealie service as a secret with the correct name.
|
||||
Note that environment variables take priority over secrets, so any previously defined environment variables should be removed when migrating to secrets.
|
||||
> <super>†</super> Starting in version `2.4.2`, any environment variable in the preceding lists with a dagger
|
||||
> symbol next to them support the Docker Compose secrets pattern, below.
|
||||
[Docker Compose secrets][docker-secrets] can be used to secure sensitive information regarding the Mealie implementation
|
||||
by managing control of each secret independently from the single `.env` file. This is helpful for users that may need
|
||||
different levels of access for various, sensitive environment variables, such as differentiating between hardening
|
||||
operations (e.g., server endpoints and ports) and user access control (e.g., usernames, passwords, and API keys).
|
||||
|
||||
To convert any of these environment variables to a Docker Compose secret, append `_FILE` to the environment variable and
|
||||
connect it with a Docker Compose secret, per the [Docker documentation][docker-secrets].
|
||||
|
||||
If both the base environment variable and the secret pattern of the environment variable are set, the secret will always
|
||||
take precedence.
|
||||
|
||||
For example, a user that wishes to harden their operations by only giving some access to their database URL, but who
|
||||
wish to place additional security around their user access control, may have a Docker Compose configuration similar to:
|
||||
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
...
|
||||
environment:
|
||||
...
|
||||
POSTGRES_USER: postgres
|
||||
secrets:
|
||||
- POSTGRES_PASSWORD
|
||||
# These secrets will be loaded by Docker into the `/run/secrets` folder within the container.
|
||||
- postgres-host
|
||||
- postgres-port
|
||||
- postgres-db-name
|
||||
- postgres-user
|
||||
- postgres-password
|
||||
|
||||
environment:
|
||||
DB_ENGINE: postgres
|
||||
POSTGRES_SERVER: duplicate.entry.tld # This will be ignored, due to the secret defined, below.
|
||||
POSTGRES_SERVER_FILE: /run/secrets/postgres-host
|
||||
POSTGRES_PORT_FILE: /run/secrets/postgres-port
|
||||
POSTGRES_DB_FILE: /run/secrets/postgres-db-name
|
||||
POSTGRES_USER_FILE: /run/secrets/postgres-user
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
|
||||
|
||||
# Each of these secrets are loaded via these local files. Different patterns are available. See the Docker Compose
|
||||
# documentation for more information.
|
||||
secrets:
|
||||
POSTGRES_PASSWORD:
|
||||
file: postgrespassword.txt
|
||||
postgres-host:
|
||||
file: ./secrets/postgres-host.txt
|
||||
postgres-port:
|
||||
file: ./secrets/postgres-port.txt
|
||||
postgres-db-name:
|
||||
file: ./secrets/sensitive/postgres-db-name.txt
|
||||
postgres-user:
|
||||
file: ./secrets/sensitive/postgres-user.txt
|
||||
postgres-password:
|
||||
file: ./secrets/sensitive/postgres-password.txt
|
||||
```
|
||||
In the example above, a directory organization and access pattern may look like the following:
|
||||
```text
|
||||
.
|
||||
├── docker-compose.yml
|
||||
└── secrets # Access restricted to anyone that can manage secrets
|
||||
├── postgres-host.txt
|
||||
├── postgres-port.txt
|
||||
└── sensitive # Access further-restricted to anyone managing service accounts
|
||||
├── postgres-db-name.txt
|
||||
├── postgres-password.txt
|
||||
└── postgres-user.txt
|
||||
```
|
||||
|
||||
How you organize your secrets is ultimately up to you. At minimum, it's highly recommended to use secret patterns for
|
||||
at least these sensitive environment variables when working within shared environments:
|
||||
|
||||
- `POSTGRES_PASSWORD`
|
||||
- `SMTP_PASSWORD`
|
||||
- `LDAP_QUERY_PASSWORD`
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
[docker-secrets]: https://docs.docker.com/compose/use-secrets/
|
||||
[secrets]: #docker-secrets
|
||||
[unicorn_workers]: https://www.uvicorn.org/deployment/#built-in
|
||||
|
||||
@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
|
||||
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
||||
|
||||
1. Take a backup just in case!
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.3.0`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.0.0`
|
||||
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
|
||||
4. Restart the container
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.3.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.0.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
@@ -45,6 +45,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_USER: mealie
|
||||
PGUSER: mealie
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready"]
|
||||
interval: 30s
|
||||
|
||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.3.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.0.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
# Backups and Restoring
|
||||
# Backups and Restores
|
||||
|
||||
Mealie provides an integrated mechanics for doing full installation backups of the database. Navigate to `/admin/backups` to
|
||||
Mealie provides an integrated mechanic for doing full installation backups of the database.
|
||||
|
||||
Navigate to Settings > Backups or manually by adding `/admin/backups` to your instance URL.
|
||||
|
||||
From this page, you will be able to:
|
||||
|
||||
- See a list of available backups
|
||||
- Perform a backups
|
||||
- Restore a backup
|
||||
- Create a backup
|
||||
- Upload a backup
|
||||
- Delete a backup (Confirmation Required)
|
||||
- Download a backup
|
||||
- Perform a restore
|
||||
|
||||
!!! tip
|
||||
If you're using Mealie with SQLite all your data is stored in the /app/data/ folder in the container. You can easily perform entire site backups by stopping the container, and backing up this folder with your chosen tool. This is the **best** way to backup your data.
|
||||
|
||||
## Restoring from a Backup
|
||||
|
||||
To restore from a backup it needs to be uploaded to your instance, this can be done through the web portal. On the lower left hand corner of the backups data table you'll see an upload button. Click this button and select the backup file you want to upload and it will be available to import shortly.
|
||||
To restore from a backup it needs to be uploaded to your instance which can be done through the web portal. On the top left of the page you'll see an upload button. Click this button and select the backup file you want to upload and it will be available to import shortly. You can alternatively use one of the backups you see on the screen, if one exists.
|
||||
|
||||
Before importing it's critical that you understand the following:
|
||||
|
||||
@@ -19,6 +26,9 @@ Before importing it's critical that you understand the following:
|
||||
- This action cannot be undone
|
||||
- If this action is successful you will be logged out and you will need to log back in to complete the restore
|
||||
|
||||
!!! tip
|
||||
If for some reason the restore does not succeed, you can review the logs of what the issue may be, download the backup .ZIP and edit the contents of database.json to potentially resolve the issue. For example, if you receive an error restoring 'shopping-list' you can edit out the contents of that list while allowing other sections to restore. If you would like any assistance on this, reach out over Discord.
|
||||
|
||||
!!! warning
|
||||
Prior to beta-v5 using a mis-matched version of the database backup will result in an error that will prevent you from using the instance of Mealie requiring you to remove all data and reinstall. Post beta-v5 performing a mismatched restore will throw an error and alert the user of the issue.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -48,6 +48,7 @@ markdown_extensions:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.details
|
||||
extra_css:
|
||||
- assets/stylesheets/custom.css
|
||||
extra_javascript:
|
||||
@@ -84,12 +85,13 @@ nav:
|
||||
- OpenID Connect: "documentation/getting-started/authentication/oidc-v2.md"
|
||||
|
||||
- Community Guides:
|
||||
- iOS Shortcuts: "documentation/community-guide/ios.md"
|
||||
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
||||
- Home Assistant: "documentation/community-guide/home-assistant.md"
|
||||
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
|
||||
- Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md"
|
||||
- Bring API without internet exposure: "documentation/community-guide/bring-api.md"
|
||||
- Automate Backups with n8n: "documentation/community-guide/n8n-backup-automation.md"
|
||||
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
|
||||
- Home Assistant: "documentation/community-guide/home-assistant.md"
|
||||
- Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md"
|
||||
- iOS Shortcut: "documentation/community-guide/ios-shortcut.md"
|
||||
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
||||
|
||||
- API Reference: "api/redoc.md"
|
||||
|
||||
@@ -97,6 +99,7 @@ nav:
|
||||
- Non-Code: "contributors/non-coders.md"
|
||||
- Translating: "contributors/translating.md"
|
||||
- Developers Guide:
|
||||
- Building Packages: "contributors/developers-guide/building-packages.md"
|
||||
- Code Contributions: "contributors/developers-guide/code-contributions.md"
|
||||
- Dev Getting Started: "contributors/developers-guide/starting-dev-server.md"
|
||||
- Database Changes: "contributors/developers-guide/database-changes.md"
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
requireConfigFile: false,
|
||||
tsConfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json"],
|
||||
extraFileExtensions: [".vue"],
|
||||
},
|
||||
extends: [
|
||||
"@nuxtjs/eslint-config-typescript",
|
||||
"plugin:nuxt/recommended",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
// "plugin:prettier/recommended",
|
||||
"prettier",
|
||||
],
|
||||
// Re-add once we use nuxt bridge
|
||||
// See https://v3.nuxtjs.org/getting-started/bridge#update-nuxtconfig
|
||||
ignorePatterns: ["nuxt.config.js", "lib/api/types/**/*.ts"],
|
||||
plugins: ["prettier"],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
quotes: ["error", "double"],
|
||||
"vue/component-name-in-template-casing": ["error", "PascalCase"],
|
||||
camelcase: 0,
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
"vue/no-mutating-props": "off",
|
||||
"vue/no-v-text-v-html-on-component": "warn",
|
||||
"vue/no-v-for-template-key-on-child": "off",
|
||||
"vue/valid-v-slot": [
|
||||
"error",
|
||||
{
|
||||
allowModifiers: true,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"error",
|
||||
{
|
||||
"ts-ignore": "allow-with-description",
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{ paths: ["@vue/reactivity", "@vue/runtime-dom", "@vue/composition-api", "vue-demi"] },
|
||||
],
|
||||
|
||||
// TODO Gradually activate all rules
|
||||
// Allow Promise in onMounted
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
"error",
|
||||
{
|
||||
checksVoidReturn: {
|
||||
arguments: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
};
|
||||
Binary file not shown.
@@ -1,378 +0,0 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-100-cyrillic-ext1.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-100-cyrillic2.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-100-greek-ext3.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-100-greek4.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-100-vietnamese5.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-100-latin-ext6.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-100-latin7.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-300-cyrillic-ext8.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-300-cyrillic9.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-300-greek-ext10.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-300-greek11.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-300-vietnamese12.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-300-latin-ext13.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-300-latin14.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-400-cyrillic-ext15.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-400-cyrillic16.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-400-greek-ext17.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-400-greek18.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-400-vietnamese19.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-400-latin-ext20.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-400-latin21.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-500-cyrillic-ext22.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-500-cyrillic23.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-500-greek-ext24.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-500-greek25.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-500-vietnamese26.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-500-latin-ext27.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-500-latin28.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-700-cyrillic-ext29.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-700-cyrillic30.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-700-greek-ext31.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-700-greek32.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-700-vietnamese33.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-700-latin-ext34.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-700-latin35.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-900-cyrillic-ext36.woff2') format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-900-cyrillic37.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-900-greek-ext38.woff2') format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-900-greek39.woff2') format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-900-vietnamese40.woff2') format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-900-latin-ext41.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('~assets/fonts/Roboto-900-latin42.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,11 +17,11 @@
|
||||
}
|
||||
|
||||
.theme--dark.v-application {
|
||||
background-color: var(--v-background-base, #1e1e1e) !important;
|
||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-navigation-drawer {
|
||||
background-color: var(--v-background-base, #1e1e1e) !important;
|
||||
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-card {
|
||||
@@ -29,11 +29,11 @@
|
||||
}
|
||||
|
||||
.left-border {
|
||||
border-left: 5px solid var(--v-primary-base) !important;
|
||||
border-left: 5px solid rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.left-warning-border {
|
||||
border-left: 5px solid var(--v-warning-base) !important;
|
||||
border-left: 5px solid rgb(var(--v-theme-warning)) !important;
|
||||
}
|
||||
|
||||
.handle {
|
||||
@@ -56,3 +56,15 @@
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.fill-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.vue-simple-handler {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
@@ -1,17 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text v-if="cookbook" class="px-1">
|
||||
<v-text-field v-model="cookbook.name" :label="$t('cookbook.cookbook-name')"></v-text-field>
|
||||
<v-textarea v-model="cookbook.description" auto-grow :rows="2" :label="$t('recipe.description')"></v-textarea>
|
||||
<v-card-text
|
||||
v-if="cookbook"
|
||||
class="px-1"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="cookbook.name"
|
||||
:label="$t('cookbook.cookbook-name')"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
/>
|
||||
<v-textarea
|
||||
v-model="cookbook.description"
|
||||
auto-grow
|
||||
:rows="2"
|
||||
:label="$t('recipe.description')"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
/>
|
||||
<QueryFilterBuilder
|
||||
:field-defs="fieldDefs"
|
||||
:initial-query-filter="cookbook.queryFilter"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<v-switch v-model="cookbook.public" hide-details single-line>
|
||||
<v-switch
|
||||
v-model="cookbook.public"
|
||||
hide-details
|
||||
single-line
|
||||
color="primary"
|
||||
>
|
||||
<template #label>
|
||||
{{ $t('cookbook.public-cookbook') }}
|
||||
<HelpIcon small right class="ml-2">
|
||||
<HelpIcon
|
||||
size="small"
|
||||
right
|
||||
class="ml-2"
|
||||
>
|
||||
{{ $t('cookbook.public-cookbook-description') }}
|
||||
</HelpIcon>
|
||||
</template>
|
||||
@@ -21,16 +45,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: { QueryFilterBuilder },
|
||||
props: {
|
||||
cookbook: {
|
||||
modelValue: {
|
||||
type: Object as () => ReadCookBook,
|
||||
required: true,
|
||||
},
|
||||
@@ -39,52 +62,57 @@ export default defineComponent({
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { i18n } = useContext();
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const cookbook = toRef(() => props.modelValue);
|
||||
|
||||
function handleInput(value: string | undefined) {
|
||||
props.cookbook.queryFilterString = value || "";
|
||||
cookbook.value.queryFilterString = value || "";
|
||||
emit("update:modelValue", cookbook.value);
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.tc("category.categories"),
|
||||
label: i18n.t("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.tc("tag.tags"),
|
||||
label: i18n.t("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
{
|
||||
name: "recipe_ingredient.food.id",
|
||||
label: i18n.tc("recipe.ingredients"),
|
||||
label: i18n.t("recipe.ingredients"),
|
||||
type: Organizer.Food,
|
||||
},
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.tc("tool.tools"),
|
||||
label: i18n.t("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.tc("household.households"),
|
||||
label: i18n.t("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.tc("general.date-created"),
|
||||
label: i18n.t("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.tc("general.date-updated"),
|
||||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
cookbook,
|
||||
handleInput,
|
||||
fieldDefs,
|
||||
};
|
||||
|
||||
@@ -7,44 +7,57 @@
|
||||
width="100%"
|
||||
max-width="1100px"
|
||||
:icon="$globals.icons.pages"
|
||||
:title="$tc('general.edit')"
|
||||
:title="$t('general.edit')"
|
||||
:submit-icon="$globals.icons.save"
|
||||
:submit-text="$tc('general.save')"
|
||||
:submit-text="$t('general.save')"
|
||||
:submit-disabled="!editTarget.queryFilterString"
|
||||
can-submit
|
||||
@submit="editCookbook"
|
||||
>
|
||||
<v-card-text>
|
||||
<CookbookEditor :cookbook="editTarget" :actions="actions" />
|
||||
<CookbookEditor
|
||||
v-model="editTarget"
|
||||
:actions="actions"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Page -->
|
||||
<v-container v-if="book" fluid>
|
||||
<v-app-bar color="transparent" flat class="mt-n1">
|
||||
<v-icon large left> {{ $globals.icons.pages }} </v-icon>
|
||||
<v-toolbar-title class="headline"> {{ book.name }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton
|
||||
v-if="canEdit"
|
||||
class="mx-1"
|
||||
:edit="true"
|
||||
@click="handleEditCookbook"
|
||||
/>
|
||||
</v-app-bar>
|
||||
<v-card flat>
|
||||
<v-card-text class="py-0">
|
||||
<v-container
|
||||
v-if="book"
|
||||
class="my-0"
|
||||
>
|
||||
<v-sheet
|
||||
color="transparent"
|
||||
class="d-flex flex-column w-100 pa-0 ma-0"
|
||||
elevation="0"
|
||||
>
|
||||
<div class="d-flex align-center w-100 mb-2">
|
||||
<v-toolbar-title class="headline mb-0">
|
||||
<v-icon size="large" class="mr-3">
|
||||
{{ $globals.icons.pages }}
|
||||
</v-icon>
|
||||
{{ book.name }}
|
||||
</v-toolbar-title>
|
||||
<BaseButton
|
||||
v-if="canEdit"
|
||||
class="mx-1"
|
||||
:edit="true"
|
||||
@click="handleEditCookbook"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="book.description" class="subtitle-1 text-grey-lighten-1 mb-2">
|
||||
{{ book.description }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-sheet>
|
||||
|
||||
<v-container class="pa-0">
|
||||
<RecipeCardSection
|
||||
class="mb-5 mx-1"
|
||||
:recipes="recipes"
|
||||
:query="{ cookbook: slug }"
|
||||
@sortRecipes="assignSorted"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@appendRecipes="appendRecipes"
|
||||
@sort-recipes="assignSorted"
|
||||
@replace-recipes="replaceRecipes"
|
||||
@append-recipes="appendRecipes"
|
||||
@delete="removeRecipe"
|
||||
/>
|
||||
</v-container>
|
||||
@@ -52,89 +65,90 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useRoute, ref, useContext, useMeta, reactive, useRouter } from "@nuxtjs/composition-api";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbook, useCookbooks } from "~/composables/use-group-cookbooks";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
<script lang="ts">
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeCardSection, CookbookEditor },
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardSection, CookbookEditor },
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.value.params.slug;
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { actions } = useCookbooks();
|
||||
const router = useRouter();
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.params.slug as string;
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { actions } = useCookbookStore();
|
||||
const router = useRouter();
|
||||
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
|
||||
const isOwnHousehold = computed(() => {
|
||||
if (!($auth.user && book.value?.householdId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $auth.user.householdId === book.value.householdId;
|
||||
})
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
|
||||
const dialogStates = reactive({
|
||||
edit: false,
|
||||
});
|
||||
|
||||
const editTarget = ref<RecipeCookBook | null>(null);
|
||||
function handleEditCookbook() {
|
||||
dialogStates.edit = true;
|
||||
editTarget.value = book.value;
|
||||
const isOwnHousehold = computed(() => {
|
||||
if (!($auth.user.value && book.value?.householdId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function editCookbook() {
|
||||
if (!editTarget.value) {
|
||||
return;
|
||||
}
|
||||
const response = await actions.updateOne(editTarget.value);
|
||||
return $auth.user.value.householdId === book.value.householdId;
|
||||
});
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
|
||||
const dialogStates = reactive({
|
||||
edit: false,
|
||||
});
|
||||
|
||||
const editTarget = ref<RecipeCookBook | null>(null);
|
||||
function handleEditCookbook() {
|
||||
dialogStates.edit = true;
|
||||
editTarget.value = book.value;
|
||||
}
|
||||
|
||||
async function editCookbook() {
|
||||
if (!editTarget.value) {
|
||||
return;
|
||||
}
|
||||
const response = await actions.updateOne(editTarget.value);
|
||||
|
||||
if (response?.slug && book.value?.slug !== response?.slug) {
|
||||
// if name changed, redirect to new slug
|
||||
if (response?.slug && book.value?.slug !== response?.slug) {
|
||||
router.push(`/g/${route.value.params.groupSlug}/cookbooks/${response?.slug}`);
|
||||
}
|
||||
dialogStates.edit = false;
|
||||
editTarget.value = null;
|
||||
router.push(`/g/${route.params.groupSlug}/cookbooks/${response?.slug}`);
|
||||
}
|
||||
else {
|
||||
// otherwise reload the page, since the recipe criteria changed
|
||||
router.go(0);
|
||||
}
|
||||
dialogStates.edit = false;
|
||||
editTarget.value = null;
|
||||
}
|
||||
|
||||
useMeta(() => {
|
||||
return {
|
||||
title: book?.value?.name || "Cookbook",
|
||||
};
|
||||
});
|
||||
useSeoMeta({
|
||||
title: book?.value?.name || "Cookbook",
|
||||
});
|
||||
|
||||
return {
|
||||
book,
|
||||
slug,
|
||||
tab,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
canEdit,
|
||||
dialogStates,
|
||||
editTarget,
|
||||
handleEditCookbook,
|
||||
editCookbook,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head: {}, // Must include for useMeta
|
||||
});
|
||||
</script>
|
||||
return {
|
||||
book,
|
||||
slug,
|
||||
tab,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
canEdit,
|
||||
dialogStates,
|
||||
editTarget,
|
||||
handleEditCookbook,
|
||||
editCookbook,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,21 +7,24 @@
|
||||
class="elevation-0"
|
||||
@click:row="downloadData"
|
||||
>
|
||||
<template #item.expires="{ item }">
|
||||
<template #[`item.expires`]="{ item }">
|
||||
{{ getTimeToExpire(item.expires) }}
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<BaseButton download small :download-url="`/api/recipes/bulk-actions/export/download?path=${item.path}`">
|
||||
</BaseButton>
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<BaseButton
|
||||
download
|
||||
size="small"
|
||||
:download-url="`/api/recipes/bulk-actions/export/download?path=${item.path}`"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { parseISO, formatDistanceToNow } from "date-fns";
|
||||
import { GroupDataExport } from "~/lib/api/types/group";
|
||||
export default defineComponent({
|
||||
import type { GroupDataExport } from "~/lib/api/types/group";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
exports: {
|
||||
type: Array as () => GroupDataExport[],
|
||||
@@ -29,14 +32,14 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
const headers = [
|
||||
{ text: i18n.t("export.export"), value: "name" },
|
||||
{ text: i18n.t("export.file-name"), value: "filename" },
|
||||
{ text: i18n.t("export.size"), value: "size" },
|
||||
{ text: i18n.t("export.link-expires"), value: "expires" },
|
||||
{ text: "", value: "actions" },
|
||||
{ title: i18n.t("export.export"), value: "name" },
|
||||
{ title: i18n.t("export.file-name"), value: "filename" },
|
||||
{ title: i18n.t("export.size"), value: "size" },
|
||||
{ title: i18n.t("export.link-expires"), value: "expires" },
|
||||
{ title: "", value: "actions" },
|
||||
];
|
||||
|
||||
function getTimeToExpire(timeString: string) {
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
<template>
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle :title="$tc('group.general-preferences')"></BaseCardSectionTitle>
|
||||
<v-checkbox v-model="preferences.privateGroup" class="mt-n4" :label="$t('group.private-group')"></v-checkbox>
|
||||
<BaseCardSectionTitle :title="$t('group.general-preferences')" />
|
||||
<v-checkbox
|
||||
v-model="preferences.privateGroup"
|
||||
class="mt-n4"
|
||||
:label="$t('group.private-group')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,5 +35,4 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -5,31 +5,30 @@
|
||||
:label="label"
|
||||
:hint="description"
|
||||
:persistent-hint="!!description"
|
||||
item-text="name"
|
||||
item-title="name"
|
||||
:multiple="multiselect"
|
||||
:prepend-inner-icon="$globals.icons.household"
|
||||
return-object
|
||||
>
|
||||
<template #selection="data">
|
||||
<template #chip="data">
|
||||
<v-chip
|
||||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
small
|
||||
close
|
||||
:input-value="data.item"
|
||||
size="small"
|
||||
closable
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
@click:close="removeByIndex(data.index)"
|
||||
>
|
||||
{{ data.item.name || data.item }}
|
||||
{{ data.item.raw.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, useContext } from "@nuxtjs/composition-api";
|
||||
import { useHouseholdStore } from "~/composables/store/use-household-store";
|
||||
|
||||
interface HouseholdLike {
|
||||
@@ -37,9 +36,9 @@ interface HouseholdLike {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Array as () => HouseholdLike[],
|
||||
required: true,
|
||||
},
|
||||
@@ -52,11 +51,12 @@ export default defineComponent({
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const selected = computed({
|
||||
get: () => props.value,
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -66,9 +66,9 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const label = computed(
|
||||
() => props.multiselect ? i18n.tc("household.households") : i18n.tc("household.household")
|
||||
() => props.multiselect ? i18n.t("household.households") : i18n.t("household.household"),
|
||||
);
|
||||
|
||||
const { store: households } = useHouseholdStore();
|
||||
|
||||
@@ -8,26 +8,41 @@
|
||||
/>
|
||||
<v-menu
|
||||
offset-y
|
||||
left
|
||||
start
|
||||
:bottom="!menuTop"
|
||||
:nudge-bottom="!menuTop ? '5' : '0'"
|
||||
:top="menuTop"
|
||||
:nudge-top="menuTop ? '5' : '0'"
|
||||
allow-overflow
|
||||
close-delay="125"
|
||||
:open-on-hover="$vuetify.breakpoint.mdAndUp"
|
||||
:open-on-hover="mdAndUp"
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
:class="{ 'rounded-circle': fab }"
|
||||
:size="fab ? 'small' : undefined"
|
||||
:color="color"
|
||||
:icon="!fab"
|
||||
variant="text"
|
||||
dark
|
||||
v-bind="props"
|
||||
@click.prevent
|
||||
>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="item.color"> {{ item.icon }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="index"
|
||||
@click="contextMenuEventHandler(item.event)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="item.color">
|
||||
{{ item.icon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -36,10 +51,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||
import { ShoppingListSummary } from "~/lib/api/types/household";
|
||||
import type { ShoppingListSummary } from "~/lib/api/types/household";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export interface ContextMenuItem {
|
||||
@@ -50,7 +64,7 @@ export interface ContextMenuItem {
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
},
|
||||
@@ -77,7 +91,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { $globals, i18n } = useContext();
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
@@ -85,7 +102,7 @@ export default defineComponent({
|
||||
shoppingListDialog: false,
|
||||
menuItems: [
|
||||
{
|
||||
title: i18n.tc("recipe.add-to-list"),
|
||||
title: i18n.t("recipe.add-to-list"),
|
||||
icon: $globals.icons.cartCheck,
|
||||
color: undefined,
|
||||
event: "shoppingList",
|
||||
@@ -103,16 +120,17 @@ export default defineComponent({
|
||||
scale: 1,
|
||||
...recipe,
|
||||
};
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
async function getShoppingLists() {
|
||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||
if (data) {
|
||||
shoppingLists.value = data.items ?? [];
|
||||
shoppingLists.value = data.items as ShoppingListSummary[] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
shoppingList: () => {
|
||||
getShoppingLists();
|
||||
@@ -139,7 +157,8 @@ export default defineComponent({
|
||||
icon,
|
||||
recipesWithScales,
|
||||
shoppingLists,
|
||||
}
|
||||
mdAndUp,
|
||||
};
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-md-flex" style="gap: 10px">
|
||||
<v-select v-model="inputDay" :items="MEAL_DAY_OPTIONS" :label="$t('meal-plan.rule-day')"></v-select>
|
||||
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" :label="$t('meal-plan.meal-type')"></v-select>
|
||||
<div
|
||||
class="d-md-flex"
|
||||
style="gap: 10px"
|
||||
>
|
||||
<v-select
|
||||
v-model="inputDay"
|
||||
:items="MEAL_DAY_OPTIONS"
|
||||
:label="$t('meal-plan.rule-day')"
|
||||
/>
|
||||
<v-select
|
||||
v-model="inputEntryType"
|
||||
:items="MEAL_TYPE_OPTIONS"
|
||||
:label="$t('meal-plan.meal-type')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
@@ -15,20 +26,19 @@
|
||||
|
||||
<!-- TODO: proper pluralization of inputDay -->
|
||||
{{ $t('meal-plan.this-rule-will-apply', {
|
||||
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
|
||||
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType])
|
||||
}) }}
|
||||
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
|
||||
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType]),
|
||||
}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import { QueryFilterJSON } from "~/lib/api/types/response";
|
||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
QueryFilterBuilder,
|
||||
},
|
||||
@@ -54,26 +64,27 @@ export default defineComponent({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:day", "update:entry-type", "update:query-filter-string"],
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
{ text: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||
{ text: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||
{ text: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||
{ text: i18n.t("meal-plan.side"), value: "side" },
|
||||
{ text: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||
{ title: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||
{ title: i18n.t("meal-plan.side"), value: "side" },
|
||||
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||
];
|
||||
|
||||
const MEAL_DAY_OPTIONS = [
|
||||
{ text: i18n.t("general.monday"), value: "monday" },
|
||||
{ text: i18n.t("general.tuesday"), value: "tuesday" },
|
||||
{ text: i18n.t("general.wednesday"), value: "wednesday" },
|
||||
{ text: i18n.t("general.thursday"), value: "thursday" },
|
||||
{ text: i18n.t("general.friday"), value: "friday" },
|
||||
{ text: i18n.t("general.saturday"), value: "saturday" },
|
||||
{ text: i18n.t("general.sunday"), value: "sunday" },
|
||||
{ text: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||
{ title: i18n.t("general.monday"), value: "monday" },
|
||||
{ title: i18n.t("general.tuesday"), value: "tuesday" },
|
||||
{ title: i18n.t("general.wednesday"), value: "wednesday" },
|
||||
{ title: i18n.t("general.thursday"), value: "thursday" },
|
||||
{ title: i18n.t("general.friday"), value: "friday" },
|
||||
{ title: i18n.t("general.saturday"), value: "saturday" },
|
||||
{ title: i18n.t("general.sunday"), value: "sunday" },
|
||||
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||
];
|
||||
|
||||
const inputDay = computed({
|
||||
@@ -110,42 +121,42 @@ export default defineComponent({
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.tc("category.categories"),
|
||||
label: i18n.t("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.tc("tag.tags"),
|
||||
label: i18n.t("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
{
|
||||
name: "recipe_ingredient.food.id",
|
||||
label: i18n.tc("recipe.ingredients"),
|
||||
label: i18n.t("recipe.ingredients"),
|
||||
type: Organizer.Food,
|
||||
},
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.tc("tool.tools"),
|
||||
label: i18n.t("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.tc("household.households"),
|
||||
label: i18n.t("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "last_made",
|
||||
label: i18n.tc("general.last-made"),
|
||||
label: i18n.t("general.last-made"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.tc("general.date-created"),
|
||||
label: i18n.t("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.tc("general.date-updated"),
|
||||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,27 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-switch v-model="webhookCopy.enabled" :label="$t('general.enabled')"></v-switch>
|
||||
<v-text-field v-model="webhookCopy.name" :label="$t('settings.webhooks.webhook-name')"></v-text-field>
|
||||
<v-text-field v-model="webhookCopy.url" :label="$t('settings.webhooks.webhook-url')"></v-text-field>
|
||||
<v-time-picker v-model="scheduledTime" class="elevation-2" ampm-in-title format="ampm"></v-time-picker>
|
||||
<v-switch
|
||||
v-model="webhookCopy.enabled"
|
||||
color="primary"
|
||||
:label="$t('general.enabled')"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="webhookCopy.name"
|
||||
:label="$t('settings.webhooks.webhook-name')"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="webhookCopy.url"
|
||||
:label="$t('settings.webhooks.webhook-url')"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-time-picker
|
||||
v-model="scheduledTime"
|
||||
class="elevation-2"
|
||||
ampm-in-title
|
||||
format="ampm"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="py-0 justify-end">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $tc('general.delete'),
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.testTube,
|
||||
text: $tc('general.test'),
|
||||
text: $t('general.test'),
|
||||
event: 'test',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
text: $tc('general.save'),
|
||||
text: $t('general.save'),
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
@@ -34,11 +51,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||
import { ReadWebhook } from "~/lib/api/types/household";
|
||||
import type { ReadWebhook } from "~/lib/api/types/household";
|
||||
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
webhook: {
|
||||
type: Object as () => ReadWebhook,
|
||||
@@ -47,6 +63,7 @@ export default defineComponent({
|
||||
},
|
||||
emits: ["delete", "save", "test"],
|
||||
setup(props, { emit }) {
|
||||
const i18n = useI18n();
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
|
||||
@@ -67,6 +84,11 @@ export default defineComponent({
|
||||
emit("save", webhookCopy.value);
|
||||
}
|
||||
|
||||
// Set page title using useSeoMeta
|
||||
useSeoMeta({
|
||||
title: i18n.t("settings.webhooks.webhooks"),
|
||||
});
|
||||
|
||||
return {
|
||||
webhookCopy,
|
||||
scheduledTime,
|
||||
@@ -75,10 +97,5 @@ export default defineComponent({
|
||||
itemLocal,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("settings.webhooks.webhooks") as string,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,157 +1,144 @@
|
||||
<template>
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle class="mt-10" :title="$tc('household.household-preferences')"></BaseCardSectionTitle>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="preferences.privateHousehold"
|
||||
hide-details
|
||||
dense
|
||||
:label="$t('household.private-household')"
|
||||
/>
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.private-household-description") }}
|
||||
</p>
|
||||
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="preferences.lockRecipeEditsFromOtherHouseholds"
|
||||
hide-details
|
||||
dense
|
||||
:label="$t('household.lock-recipe-edits-from-other-households')"
|
||||
/>
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.lock-recipe-edits-from-other-households-description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-select
|
||||
v-model="preferences.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
:items="allDays"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
:label="$t('settings.first-day-of-week')"
|
||||
/>
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle :title="$t('household.household-preferences')" />
|
||||
<div class="mb-6">
|
||||
<v-checkbox v-model="preferences.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.private-household-description") }}
|
||||
</p>
|
||||
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox v-model="preferences.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.lock-recipe-edits-from-other-households-description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-select
|
||||
v-model="preferences.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
:items="allDays"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
:label="$t('settings.first-day-of-week')"
|
||||
variant="underlined"
|
||||
flat
|
||||
/>
|
||||
|
||||
<BaseCardSectionTitle class="mt-5" :title="$tc('household.household-recipe-preferences')"></BaseCardSectionTitle>
|
||||
<div class="preference-container">
|
||||
<div v-for="p in recipePreferences" :key="p.key">
|
||||
<v-checkbox
|
||||
v-model="preferences[p.key]"
|
||||
hide-details
|
||||
dense
|
||||
:label="p.label"
|
||||
/>
|
||||
<p class="ml-8 text-subtitle-2 my-0 py-0">
|
||||
{{ p.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BaseCardSectionTitle class="mt-5" :title="$t('household.household-recipe-preferences')" />
|
||||
<div class="preference-container">
|
||||
<div v-for="p in recipePreferences" :key="p.key">
|
||||
<v-checkbox v-model="preferences[p.key]" hide-details density="compact" :label="p.label" color="primary" />
|
||||
<p class="ml-8 text-subtitle-2 my-0 py-0">
|
||||
{{ p.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
||||
import { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
type Preference = {
|
||||
key: keyof ReadHouseholdPreferences;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
type Preference = {
|
||||
key: keyof ReadHouseholdPreferences;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const recipePreferences: Preference[] = [
|
||||
{
|
||||
key: "recipePublic",
|
||||
label: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||
description: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeShowNutrition",
|
||||
label: i18n.tc("group.show-nutrition-information"),
|
||||
description: i18n.tc("group.show-nutrition-information-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeShowAssets",
|
||||
label: i18n.tc("group.show-recipe-assets"),
|
||||
description: i18n.tc("group.show-recipe-assets-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeLandscapeView",
|
||||
label: i18n.tc("group.default-to-landscape-view"),
|
||||
description: i18n.tc("group.default-to-landscape-view-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeDisableComments",
|
||||
label: i18n.tc("group.disable-users-from-commenting-on-recipes"),
|
||||
description: i18n.tc("group.disable-users-from-commenting-on-recipes-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeDisableAmount",
|
||||
label: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
||||
description: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
|
||||
},
|
||||
];
|
||||
const recipePreferences: Preference[] = [
|
||||
{
|
||||
key: "recipePublic",
|
||||
label: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||
description: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeShowNutrition",
|
||||
label: i18n.t("group.show-nutrition-information"),
|
||||
description: i18n.t("group.show-nutrition-information-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeShowAssets",
|
||||
label: i18n.t("group.show-recipe-assets"),
|
||||
description: i18n.t("group.show-recipe-assets-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeLandscapeView",
|
||||
label: i18n.t("group.default-to-landscape-view"),
|
||||
description: i18n.t("group.default-to-landscape-view-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeDisableComments",
|
||||
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
|
||||
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeDisableAmount",
|
||||
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
||||
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
|
||||
},
|
||||
];
|
||||
|
||||
const allDays = [
|
||||
{
|
||||
name: i18n.t("general.sunday"),
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.monday"),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.tuesday"),
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.wednesday"),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.thursday"),
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.friday"),
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.saturday"),
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
const allDays = [
|
||||
{
|
||||
name: i18n.t("general.sunday"),
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.monday"),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.tuesday"),
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.wednesday"),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.thursday"),
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.friday"),
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.saturday"),
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
allDays,
|
||||
preferences,
|
||||
recipePreferences,
|
||||
};
|
||||
return {
|
||||
allDays,
|
||||
preferences,
|
||||
recipePreferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +1,56 @@
|
||||
<template>
|
||||
<v-toolbar
|
||||
rounded
|
||||
height="0"
|
||||
class="fixed-bar mt-0"
|
||||
color="rgb(255, 0, 0, 0.0)"
|
||||
flat
|
||||
style="z-index: 2; position: sticky"
|
||||
style="z-index: 2; position: sticky; background: transparent; box-shadow: none;"
|
||||
density="compact"
|
||||
elevation="0"
|
||||
>
|
||||
<BaseDialog
|
||||
v-model="deleteDialog"
|
||||
:title="$tc('recipe.delete-recipe')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
@confirm="emitDelete()"
|
||||
>
|
||||
<BaseDialog v-model="deleteDialog" :title="$t('recipe.delete-recipe')" color="error"
|
||||
:icon="$globals.icons.alertCircle" can-confirm @confirm="emitDelete()">
|
||||
<v-card-text>
|
||||
{{ $t("recipe.delete-confirmation") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<div v-if="!open" class="custom-btn-group ma-1">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="ml-1" color="info" button-style :recipe-id="recipe.id" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style class="ml-1" :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" />
|
||||
<div v-if="loggedIn">
|
||||
<v-tooltip v-if="canEdit" bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="ml-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="flat"
|
||||
rounded="circle"
|
||||
size="small"
|
||||
color="info"
|
||||
class="ml-1"
|
||||
v-bind="props"
|
||||
@click="$emit('edit', true)"
|
||||
>
|
||||
<v-icon size="x-large">
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<RecipeTimerMenu
|
||||
fab
|
||||
color="info"
|
||||
class="ml-1"
|
||||
/>
|
||||
|
||||
<RecipeContextMenu
|
||||
show-print
|
||||
:menu-top="false"
|
||||
:name="recipe.name"
|
||||
:slug="recipe.slug"
|
||||
:name="recipe.name!"
|
||||
:slug="recipe.slug!"
|
||||
:menu-icon="$globals.icons.dotsVertical"
|
||||
fab
|
||||
color="info"
|
||||
:card-menu="false"
|
||||
:recipe="recipe"
|
||||
:recipe-id="recipe.id"
|
||||
:recipe-id="recipe.id!"
|
||||
:recipe-scale="recipeScale"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: loggedIn,
|
||||
duplicate: loggedIn,
|
||||
@@ -63,6 +60,7 @@
|
||||
printPreferences: true,
|
||||
share: loggedIn,
|
||||
recipeActions: true,
|
||||
delete: loggedIn,
|
||||
}"
|
||||
class="ml-1"
|
||||
@print="$emit('print')"
|
||||
@@ -72,33 +70,35 @@
|
||||
<v-btn
|
||||
v-for="(btn, index) in editorButtons"
|
||||
:key="index"
|
||||
:fab="$vuetify.breakpoint.xs"
|
||||
:small="$vuetify.breakpoint.xs"
|
||||
:class="{ 'rounded-circle': $vuetify.display.xs }"
|
||||
:size="$vuetify.display.xs ? 'small' : undefined"
|
||||
:color="btn.color"
|
||||
variant="elevated"
|
||||
:icon="$vuetify.display.xs"
|
||||
@click="emitHandler(btn.event)"
|
||||
>
|
||||
<v-icon :left="!$vuetify.breakpoint.xs">{{ btn.icon }}</v-icon>
|
||||
{{ $vuetify.breakpoint.xs ? "" : btn.text }}
|
||||
<v-icon :left="!$vuetify.display.xs">
|
||||
{{ btn.icon }}
|
||||
</v-icon>
|
||||
{{ $vuetify.display.xs ? "" : btn.text }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeTimerMenu from "./RecipeTimerMenu.vue";
|
||||
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
const SAVE_EVENT = "save";
|
||||
const DELETE_EVENT = "delete";
|
||||
const CLOSE_EVENT = "close";
|
||||
const JSON_EVENT = "json";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimerMenu, RecipeTimelineBadge },
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimelineBadge },
|
||||
props: {
|
||||
recipe: {
|
||||
required: true,
|
||||
@@ -133,10 +133,12 @@ export default defineComponent({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["print", "input", "delete", "close", "edit"],
|
||||
setup(_, context) {
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const { i18n, $globals } = useContext();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const editorButtons = [
|
||||
{
|
||||
text: i18n.t("general.delete"),
|
||||
@@ -216,9 +218,13 @@ export default defineComponent({
|
||||
|
||||
.fixed-bar {
|
||||
position: sticky;
|
||||
position: -webkit-sticky; /* for Safari */
|
||||
top: 4.5em;
|
||||
z-index: 2;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
min-height: 0 !important;
|
||||
height: 48px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.fixed-bar-mobile {
|
||||
|
||||
@@ -1,74 +1,110 @@
|
||||
<template>
|
||||
<div v-if="value.length > 0 || edit">
|
||||
<div v-if="model.length > 0 || edit">
|
||||
<v-card class="mt-4">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("asset.assets") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-list v-if="value.length > 0" :flat="!edit">
|
||||
<v-list-item v-for="(item, i) in value" :key="i">
|
||||
<v-list-item-icon class="ma-auto">
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon v-bind="attrs" v-on="on">
|
||||
{{ getIconDefinition(item.icon).icon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ getIconDefinition(item.icon).title }}</span>
|
||||
</v-tooltip>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-2">
|
||||
{{ item.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-divider class="mx-2" />
|
||||
<v-list
|
||||
v-if="model.length > 0"
|
||||
:flat="!edit"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, i) in model"
|
||||
:key="i"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="ma-auto">
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps">
|
||||
{{ getIconDefinition(item.icon).icon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ getIconDefinition(item.icon).title }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<v-list-item-title class="pl-2">
|
||||
{{ item.name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-action>
|
||||
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
|
||||
<v-btn
|
||||
v-if="!edit"
|
||||
color="primary"
|
||||
icon
|
||||
:href="assetURL(item.fileName ?? '')"
|
||||
target="_blank"
|
||||
top
|
||||
>
|
||||
<v-icon> {{ $globals.icons.download }} </v-icon>
|
||||
</v-btn>
|
||||
<div v-else>
|
||||
<v-btn color="error" icon top @click="value.splice(i, 1)">
|
||||
<v-btn
|
||||
color="error"
|
||||
icon
|
||||
top
|
||||
@click="model.splice(i, 1)"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
<AppButtonCopy color="" :copy-text="assetEmbed(item.fileName)" />
|
||||
<AppButtonCopy
|
||||
color=""
|
||||
:copy-text="assetEmbed(item.fileName ?? '')"
|
||||
/>
|
||||
</div>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
<div class="d-flex ml-auto mt-2">
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<BaseDialog
|
||||
v-model="state.newAssetDialog"
|
||||
:title="$tc('asset.new-asset')"
|
||||
:title="$t('asset.new-asset')"
|
||||
:icon="getIconDefinition(state.newAsset.icon).icon"
|
||||
can-submit
|
||||
@submit="addAsset"
|
||||
>
|
||||
<template #activator>
|
||||
<BaseButton v-if="edit" small create @click="state.newAssetDialog = true" />
|
||||
<BaseButton
|
||||
v-if="edit"
|
||||
size="small"
|
||||
create
|
||||
@click="state.newAssetDialog = true"
|
||||
/>
|
||||
</template>
|
||||
<v-card-text class="pt-4">
|
||||
<v-text-field v-model="state.newAsset.name" dense :label="$t('general.name')"></v-text-field>
|
||||
<v-text-field
|
||||
v-model="state.newAsset.name"
|
||||
density="compact"
|
||||
:label="$t('general.name')"
|
||||
/>
|
||||
<div class="d-flex justify-space-between">
|
||||
<v-select
|
||||
v-model="state.newAsset.icon"
|
||||
dense
|
||||
density="compact"
|
||||
:prepend-icon="getIconDefinition(state.newAsset.icon).icon"
|
||||
:items="iconOptions"
|
||||
item-text="title"
|
||||
item-title="title"
|
||||
item-value="name"
|
||||
class="mr-2"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-avatar>
|
||||
<v-avatar>
|
||||
<v-icon class="mr-auto">
|
||||
{{ item.icon }}
|
||||
{{ item.raw.icon }}
|
||||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
</v-avatar>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
</v-select>
|
||||
<AppButtonUpload :post="false" file-name="file" :text-btn="false" @uploaded="setFileObject" />
|
||||
<AppButtonUpload
|
||||
:post="false"
|
||||
file-name="file"
|
||||
:text-btn="false"
|
||||
@uploaded="setFileObject"
|
||||
/>
|
||||
</div>
|
||||
{{ state.fileObject.name }}
|
||||
</v-card-text>
|
||||
@@ -77,124 +113,109 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, useContext } from "@nuxtjs/composition-api";
|
||||
<script setup lang="ts">
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { detectServerBaseUrl } from "~/composables/use-utils";
|
||||
import { RecipeAsset } from "~/lib/api/types/recipe";
|
||||
import type { RecipeAsset } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Array as () => RecipeAsset[],
|
||||
required: true,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = defineProps({
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup(props, context) {
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
newAssetDialog: false,
|
||||
fileObject: {} as File,
|
||||
newAsset: {
|
||||
name: "",
|
||||
icon: "mdi-file",
|
||||
},
|
||||
});
|
||||
|
||||
const { $globals, i18n, req } = useContext();
|
||||
|
||||
const iconOptions = [
|
||||
{
|
||||
name: "mdi-file",
|
||||
title: i18n.t("asset.file"),
|
||||
icon: $globals.icons.file,
|
||||
},
|
||||
{
|
||||
name: "mdi-file-pdf-box",
|
||||
title: i18n.t("asset.pdf"),
|
||||
icon: $globals.icons.filePDF,
|
||||
},
|
||||
{
|
||||
name: "mdi-file-image",
|
||||
title: i18n.t("asset.image"),
|
||||
icon: $globals.icons.fileImage,
|
||||
},
|
||||
{
|
||||
name: "mdi-code-json",
|
||||
title: i18n.t("asset.code"),
|
||||
icon: $globals.icons.codeJson,
|
||||
},
|
||||
{
|
||||
name: "mdi-silverware-fork-knife",
|
||||
title: i18n.t("asset.recipe"),
|
||||
icon: $globals.icons.primary,
|
||||
},
|
||||
];
|
||||
|
||||
const serverBase = detectServerBaseUrl(req);
|
||||
|
||||
function getIconDefinition(icon: string) {
|
||||
return iconOptions.find((item) => item.name === icon) || iconOptions[0];
|
||||
}
|
||||
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
function assetURL(assetName: string) {
|
||||
return recipeAssetPath(props.recipeId, assetName);
|
||||
}
|
||||
|
||||
function assetEmbed(name: string) {
|
||||
return `<img src="${serverBase}${assetURL(name)}" height="100%" width="100%"> </img>`;
|
||||
}
|
||||
|
||||
function setFileObject(fileObject: File) {
|
||||
state.fileObject = fileObject;
|
||||
}
|
||||
|
||||
function validFields() {
|
||||
return state.newAsset.name.length > 0 && state.fileObject.name.length > 0;
|
||||
}
|
||||
|
||||
async function addAsset() {
|
||||
if (!validFields()) {
|
||||
alert.error(i18n.t("asset.error-submitting-form") as string);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.recipes.createAsset(props.slug, {
|
||||
name: state.newAsset.name,
|
||||
icon: state.newAsset.icon,
|
||||
file: state.fileObject,
|
||||
extension: state.fileObject.name.split(".").pop() || "",
|
||||
});
|
||||
|
||||
context.emit("input", [...props.value, data]);
|
||||
state.newAsset = { name: "", icon: "mdi-file" };
|
||||
state.fileObject = {} as File;
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
addAsset,
|
||||
assetURL,
|
||||
assetEmbed,
|
||||
getIconDefinition,
|
||||
iconOptions,
|
||||
setFileObject,
|
||||
};
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const model = defineModel<RecipeAsset[]>({ required: true });
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
newAssetDialog: false,
|
||||
fileObject: {} as File,
|
||||
newAsset: {
|
||||
name: "",
|
||||
icon: "mdi-file",
|
||||
},
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const iconOptions = [
|
||||
{
|
||||
name: "mdi-file",
|
||||
title: i18n.t("asset.file"),
|
||||
icon: $globals.icons.file,
|
||||
},
|
||||
{
|
||||
name: "mdi-file-pdf-box",
|
||||
title: i18n.t("asset.pdf"),
|
||||
icon: $globals.icons.filePDF,
|
||||
},
|
||||
{
|
||||
name: "mdi-file-image",
|
||||
title: i18n.t("asset.image"),
|
||||
icon: $globals.icons.fileImage,
|
||||
},
|
||||
{
|
||||
name: "mdi-code-json",
|
||||
title: i18n.t("asset.code"),
|
||||
icon: $globals.icons.codeJson,
|
||||
},
|
||||
{
|
||||
name: "mdi-silverware-fork-knife",
|
||||
title: i18n.t("asset.recipe"),
|
||||
icon: $globals.icons.primary,
|
||||
},
|
||||
];
|
||||
|
||||
const serverBase = useRequestURL().origin;
|
||||
|
||||
function getIconDefinition(icon: string) {
|
||||
return iconOptions.find(item => item.name === icon) || iconOptions[0];
|
||||
}
|
||||
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
function assetURL(assetName: string) {
|
||||
return recipeAssetPath(props.recipeId, assetName);
|
||||
}
|
||||
|
||||
function assetEmbed(name: string) {
|
||||
return `<img src="${serverBase}${assetURL(name)}" height="100%" width="100%"> </img>`;
|
||||
}
|
||||
|
||||
function setFileObject(fileObject: File) {
|
||||
state.fileObject = fileObject;
|
||||
}
|
||||
|
||||
function validFields() {
|
||||
return state.newAsset.name.length > 0 && state.fileObject.name.length > 0;
|
||||
}
|
||||
|
||||
async function addAsset() {
|
||||
if (!validFields()) {
|
||||
alert.error(i18n.t("asset.error-submitting-form") as string);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.recipes.createAsset(props.slug, {
|
||||
name: state.newAsset.name,
|
||||
icon: state.newAsset.icon,
|
||||
file: state.fileObject,
|
||||
extension: state.fileObject.name.split(".").pop() || "",
|
||||
});
|
||||
if (data) {
|
||||
model.value = [...model.value, data];
|
||||
}
|
||||
state.newAsset = { name: "", icon: "mdi-file" };
|
||||
state.fileObject = {} as File;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,75 +1,108 @@
|
||||
<template>
|
||||
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
||||
<v-lazy>
|
||||
<v-hover v-slot="{ hover }" :open-delay="50">
|
||||
<v-card
|
||||
:class="{ 'on-hover': hover }"
|
||||
:style="{ cursor }"
|
||||
:elevation="hover ? 12 : 2"
|
||||
:to="recipeRoute"
|
||||
:min-height="imageHeight + 75"
|
||||
@click="$emit('click')"
|
||||
<div>
|
||||
<v-hover
|
||||
v-slot="{ isHovering, props }"
|
||||
:open-delay="50"
|
||||
>
|
||||
<RecipeCardImage
|
||||
:icon-size="imageHeight"
|
||||
:height="imageHeight"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
<v-card
|
||||
v-bind="props"
|
||||
:class="{ 'on-hover': isHovering }"
|
||||
:style="{ cursor }"
|
||||
:elevation="isHovering ? 12 : 2"
|
||||
:to="recipeRoute"
|
||||
:min-height="imageHeight + 75"
|
||||
@click.self="$emit('click')"
|
||||
>
|
||||
<v-expand-transition v-if="description">
|
||||
<div v-if="hover" class="d-flex transition-fast-in-fast-out secondary v-card--reveal" style="height: 100%">
|
||||
<v-card-text class="v-card--text-show white--text">
|
||||
<div class="descriptionWrapper">
|
||||
<SafeMarkdown :source="description" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
<RecipeCardImage
|
||||
:icon-size="imageHeight"
|
||||
:height="imageHeight"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
size="small"
|
||||
:image-version="image"
|
||||
>
|
||||
<v-expand-transition v-if="description">
|
||||
<div
|
||||
v-if="isHovering"
|
||||
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
|
||||
style="height: 100%"
|
||||
>
|
||||
<v-card-text class="v-card--text-show white--text">
|
||||
<div class="descriptionWrapper">
|
||||
<SafeMarkdown :source="description" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</RecipeCardImage>
|
||||
<v-card-title class="mb-n3 px-4">
|
||||
<div class="headerClass">
|
||||
{{ name }}
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</RecipeCardImage>
|
||||
<v-card-title class="my-n3 px-2 mb-n6">
|
||||
<div class="headerClass">
|
||||
{{ name }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card-title>
|
||||
|
||||
<slot name="actions">
|
||||
<v-card-actions v-if="showRecipeContent" class="px-1">
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup" class="absolute" :recipe-id="recipeId" show-always />
|
||||
<slot name="actions">
|
||||
<v-card-actions
|
||||
v-if="showRecipeContent"
|
||||
class="px-1"
|
||||
>
|
||||
<RecipeFavoriteBadge
|
||||
v-if="isOwnGroup"
|
||||
class="absolute"
|
||||
:recipe-id="recipeId"
|
||||
show-always
|
||||
/>
|
||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||
|
||||
<RecipeRating class="pb-1" :value="rating" :recipe-id="recipeId" :slug="slug" :small="true" />
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" />
|
||||
<RecipeRating
|
||||
class="ml-n2"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
<v-spacer />
|
||||
<RecipeChips
|
||||
:truncate="true"
|
||||
:items="tags"
|
||||
:title="false"
|
||||
:limit="2"
|
||||
small
|
||||
url-prefix="tags"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup"
|
||||
color="grey darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@delete="$emit('delete', slug)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</slot>
|
||||
<slot></slot>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup"
|
||||
color="grey-darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@delete="$emit('delete', slug)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</slot>
|
||||
<slot />
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</div>
|
||||
</v-lazy>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
@@ -77,7 +110,7 @@ import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import RecipeRating from "./RecipeRating.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
|
||||
props: {
|
||||
name: {
|
||||
@@ -119,12 +152,13 @@ export default defineComponent({
|
||||
default: 200,
|
||||
},
|
||||
},
|
||||
emits: ["click", "delete"],
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
@@ -159,7 +193,7 @@ export default defineComponent({
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.descriptionWrapper{
|
||||
.descriptionWrapper {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 8;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<v-img
|
||||
v-if="!fallBackImage"
|
||||
:height="height"
|
||||
cover
|
||||
min-height="125"
|
||||
max-height="fill-height"
|
||||
:src="getImage(recipeId)"
|
||||
@@ -9,21 +10,28 @@
|
||||
@load="fallBackImage = false"
|
||||
@error="fallBackImage = true"
|
||||
>
|
||||
<slot> </slot>
|
||||
<slot />
|
||||
</v-img>
|
||||
<div v-else class="icon-slot" @click="$emit('click')">
|
||||
<v-icon color="primary" class="icon-position" :size="iconSize">
|
||||
<div
|
||||
v-else
|
||||
class="icon-slot"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<v-icon
|
||||
color="primary"
|
||||
class="icon-position"
|
||||
:size="iconSize"
|
||||
>
|
||||
{{ $globals.icons.primary }}
|
||||
</v-icon>
|
||||
<slot> </slot>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, watch } from "@nuxtjs/composition-api";
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
tiny: {
|
||||
type: Boolean,
|
||||
@@ -55,9 +63,10 @@ export default defineComponent({
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: "fill-height",
|
||||
default: "100%",
|
||||
},
|
||||
},
|
||||
emits: ["click"],
|
||||
setup(props) {
|
||||
const api = useUserApi();
|
||||
|
||||
@@ -75,7 +84,7 @@ export default defineComponent({
|
||||
() => props.recipeId,
|
||||
() => {
|
||||
fallBackImage.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function getImage(recipeId: string) {
|
||||
|
||||
@@ -1,81 +1,121 @@
|
||||
<template>
|
||||
<div :style="`height: ${height}`">
|
||||
<div :style="`height: ${height}px;`">
|
||||
<v-expand-transition>
|
||||
<v-card
|
||||
:ripple="false"
|
||||
:class="isFlat ? 'mx-auto flat' : 'mx-auto'"
|
||||
:style="{ cursor }"
|
||||
hover
|
||||
:to="$listeners.selected ? undefined : recipeRoute"
|
||||
height="100%"
|
||||
:to="$attrs.selected ? undefined : recipeRoute"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<v-img v-if="vertical" class="rounded-sm">
|
||||
<v-img
|
||||
v-if="vertical"
|
||||
class="rounded-sm"
|
||||
cover
|
||||
>
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="height"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
size="small"
|
||||
:image-version="image"
|
||||
:height="height"
|
||||
/>
|
||||
</v-img>
|
||||
<v-list-item three-line :class="vertical ? 'px-2' : 'px-0'">
|
||||
<slot v-if="!vertical" name="avatar">
|
||||
<v-list-item-avatar tile :height="height" width="125" class="v-mobile-img rounded-sm my-0">
|
||||
<v-list-item
|
||||
lines="two"
|
||||
class="py-0"
|
||||
:class="vertical ? 'px-2' : 'px-0'"
|
||||
item-props
|
||||
height="100%"
|
||||
density="compact"
|
||||
>
|
||||
<template #prepend>
|
||||
<slot
|
||||
v-if="!vertical"
|
||||
name="avatar"
|
||||
>
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="height"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
:image-version="image"
|
||||
size="small"
|
||||
width="125"
|
||||
:height="height"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<div class="pl-4 d-flex flex-column justify-space-between align-stretch pr-2">
|
||||
<v-list-item-title class="mt-3 mb-1 text-top text-truncate w-100">
|
||||
{{ name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="ma-0 text-top">
|
||||
<SafeMarkdown v-if="description" :source="description" />
|
||||
<p v-else>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</p>
|
||||
</v-list-item-subtitle>
|
||||
<div
|
||||
class="d-flex flex-nowrap justify-start ma-0 pt-2 pb-0"
|
||||
style="overflow-x: hidden; overflow-y: hidden; white-space: nowrap;"
|
||||
>
|
||||
<RecipeChips
|
||||
:truncate="true"
|
||||
:items="tags"
|
||||
:title="false"
|
||||
:limit="2"
|
||||
small
|
||||
url-prefix="tags"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="actions">
|
||||
<v-card-actions class="w-100 my-0 px-1 py-0">
|
||||
<RecipeFavoriteBadge
|
||||
v-if="isOwnGroup && showRecipeContent"
|
||||
:recipe-id="recipeId"
|
||||
show-always
|
||||
class="ma-0 pa-0"
|
||||
/>
|
||||
<div v-else class="my-0 px-1 py-0" /> <!-- Empty div to keep the layout consistent -->
|
||||
<RecipeRating
|
||||
v-if="showRecipeContent"
|
||||
:class="[{ 'pb-2': !isOwnGroup }, 'ml-n2']"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
</v-list-item-avatar>
|
||||
</slot>
|
||||
<v-list-item-content class="py-0">
|
||||
<v-list-item-title class="mt-1 mb-1 text-top">{{ name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="ma-0 text-top">
|
||||
<SafeMarkdown :source="description" />
|
||||
</v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap justify-start ma-0">
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" />
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-end align-center">
|
||||
<slot name="actions">
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup && showRecipeContent" :recipe-id="recipeId" show-always />
|
||||
<RecipeRating
|
||||
v-if="showRecipeContent"
|
||||
:class="isOwnGroup ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
:small="true"
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<!-- We also add padding to the v-rating above to compensate -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup && showRecipeContent"
|
||||
:slug="slug"
|
||||
:menu-icon="$globals.icons.dotsHorizontal"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@deleted="$emit('delete', slug)"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<!-- We also add padding to the v-rating above to compensate -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup && showRecipeContent"
|
||||
:slug="slug"
|
||||
:menu-icon="$globals.icons.dotsHorizontal"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
class="ml-auto"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@deleted="$emit('delete', slug)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</slot>
|
||||
</v-list-item>
|
||||
<slot />
|
||||
</v-card>
|
||||
@@ -84,7 +124,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
@@ -92,7 +131,7 @@ import RecipeRating from "./RecipeRating.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeFavoriteBadge,
|
||||
RecipeContextMenu,
|
||||
@@ -139,27 +178,23 @@ export default defineComponent({
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
type: [Number],
|
||||
default: 150,
|
||||
},
|
||||
imageHeight: {
|
||||
type: [Number, String],
|
||||
default: "fill-height",
|
||||
},
|
||||
},
|
||||
emits: ["selected", "delete"],
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
});
|
||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
@@ -170,7 +205,10 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
:deep(.v-list-item__prepend) {
|
||||
height: 100%;
|
||||
}
|
||||
.v-mobile-img {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
@@ -198,8 +236,9 @@ export default defineComponent({
|
||||
align-self: start !important;
|
||||
}
|
||||
|
||||
.flat, .theme--dark .flat {
|
||||
box-shadow: none!important;
|
||||
background-color: transparent!important;
|
||||
.flat,
|
||||
.theme--dark .flat {
|
||||
box-shadow: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user