mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-05 21:08:37 -05:00
Compare commits
559 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4f4ba0c8d | ||
|
|
8cd5835dd8 | ||
|
|
7aa131b326 | ||
|
|
af264bd288 | ||
|
|
72388e8bcf | ||
|
|
c0afef46d6 | ||
|
|
f90665cce9 | ||
|
|
942ac741cd | ||
|
|
1d3a7e8d62 | ||
|
|
5e85fc409e | ||
|
|
2c20e96ede | ||
|
|
608fc39747 | ||
|
|
ed2f40cd6a | ||
|
|
a080cdb432 | ||
|
|
83101e3ed5 | ||
|
|
5d90997ace | ||
|
|
c78c6cf926 | ||
|
|
e26191d116 | ||
|
|
3774f68393 | ||
|
|
c46c412bf5 | ||
|
|
aa9e61a16f | ||
|
|
b2f8d63f33 | ||
|
|
72b47a1103 | ||
|
|
29e150d547 | ||
|
|
e9ae6d86a4 | ||
|
|
f799938373 | ||
|
|
e5fff4ec5c | ||
|
|
192e531c1f | ||
|
|
45e710ee72 | ||
|
|
be579ed664 | ||
|
|
fe953896f8 | ||
|
|
decf7cb307 | ||
|
|
d396a8fdc2 | ||
|
|
a3ef49f559 | ||
|
|
41e8458389 | ||
|
|
18dc2fc6a8 | ||
|
|
6355b3c8db | ||
|
|
3ac8af138f | ||
|
|
2b3803fb2e | ||
|
|
6a80e70486 | ||
|
|
f1dc854770 | ||
|
|
581aa929bd | ||
|
|
461e51bd22 | ||
|
|
1cdf43c599 | ||
|
|
6bfbc7ca0a | ||
|
|
608dbaa4c1 | ||
|
|
89c1e007cb | ||
|
|
fb5db583d2 | ||
|
|
bef3045e65 | ||
|
|
ff958a5015 | ||
|
|
37789c342e | ||
|
|
b6b8bea925 | ||
|
|
60834178ba | ||
|
|
0375a0bd5a | ||
|
|
3361f9a7c3 | ||
|
|
0883ef05ab | ||
|
|
c4eb020a66 | ||
|
|
600f407b4f | ||
|
|
6f92a829d6 | ||
|
|
6b11ff5128 | ||
|
|
29fdad1574 | ||
|
|
54b3df105c | ||
|
|
9a3303b06c | ||
|
|
c17accd82b | ||
|
|
18f7e8d935 | ||
|
|
6d2936cab6 | ||
|
|
cc2e33a254 | ||
|
|
eee6f8113c | ||
|
|
bd10cb8cd8 | ||
|
|
d03081c4e6 | ||
|
|
64d865bf7e | ||
|
|
27efda2772 | ||
|
|
81986e63b8 | ||
|
|
42eef17cfb | ||
|
|
1f724856b1 | ||
|
|
618ea06b7a | ||
|
|
ca2039ae35 | ||
|
|
15ecab86d1 | ||
|
|
aa164424d3 | ||
|
|
99acb349bd | ||
|
|
894162a669 | ||
|
|
347af7d417 | ||
|
|
cac1699aeb | ||
|
|
d577966bfb | ||
|
|
c663efde09 | ||
|
|
9e568a1182 | ||
|
|
fc38ef2ba9 | ||
|
|
323a8100db | ||
|
|
01d3d5d325 | ||
|
|
3f52c66f02 | ||
|
|
566f744220 | ||
|
|
561b50ba45 | ||
|
|
4228c9e753 | ||
|
|
2a5c3f6457 | ||
|
|
389f8b4279 | ||
|
|
f2b71e981e | ||
|
|
ec7e3a5103 | ||
|
|
6f0183cc4b | ||
|
|
12d38c89ea | ||
|
|
492c9a948d | ||
|
|
a808c8a18b | ||
|
|
0c6483aefa | ||
|
|
1568c33c37 | ||
|
|
5d0cf8422b | ||
|
|
d322abc1b4 | ||
|
|
c34180632b | ||
|
|
c74f016f66 | ||
|
|
e11ee47109 | ||
|
|
a044ca2779 | ||
|
|
f584fa33f4 | ||
|
|
4f33c3a44f | ||
|
|
394c271294 | ||
|
|
cb3eb59501 | ||
|
|
c41a4a52ed | ||
|
|
6cbc308d83 | ||
|
|
db765b0693 | ||
|
|
184262df17 | ||
|
|
df10cf8211 | ||
|
|
c91d216fe9 | ||
|
|
1c23d855ae | ||
|
|
fd966e5843 | ||
|
|
f9f1285cb3 | ||
|
|
dbf1202f69 | ||
|
|
b3f12ee7ac | ||
|
|
556dc699c7 | ||
|
|
481ce92d84 | ||
|
|
1df26aeb99 | ||
|
|
5c4694c3d8 | ||
|
|
9b2b8091ac | ||
|
|
ad60b4445b | ||
|
|
040fb48807 | ||
|
|
f8ce5b3afb | ||
|
|
31530a68e1 | ||
|
|
51ca65e3c3 | ||
|
|
19eae21b00 | ||
|
|
34e4480f08 | ||
|
|
ad875c8fd5 | ||
|
|
6ec7baa2f1 | ||
|
|
f39929a905 | ||
|
|
4b69e5b33a | ||
|
|
4c2b559e73 | ||
|
|
178e038c79 | ||
|
|
0b3fe2c8da | ||
|
|
d4e62c5ab6 | ||
|
|
0f591fd273 | ||
|
|
d271252ecc | ||
|
|
4d211e236d | ||
|
|
7926812136 | ||
|
|
f37d8e488f | ||
|
|
56df696546 | ||
|
|
d3436a5ca8 | ||
|
|
9e46c57e78 | ||
|
|
79b6be8550 | ||
|
|
bd9e654de7 | ||
|
|
f149bcc98f | ||
|
|
0095cc7153 | ||
|
|
f276ecd96e | ||
|
|
87bfdb633b | ||
|
|
e47f386643 | ||
|
|
5c9fd22f11 | ||
|
|
30ee276e52 | ||
|
|
50c8e9be79 | ||
|
|
c4fdcec85f | ||
|
|
f7b32debbb | ||
|
|
1f1cc10f21 | ||
|
|
2ae3427a59 | ||
|
|
357f843df5 | ||
|
|
3973fe99a1 | ||
|
|
8d51c14d24 | ||
|
|
f2af8a0bc1 | ||
|
|
c130e8e92d | ||
|
|
ea099a743b | ||
|
|
d21e685219 | ||
|
|
04d0144369 | ||
|
|
8a8580e83c | ||
|
|
6b6c167153 | ||
|
|
e1c4a703a2 | ||
|
|
591c96e52b | ||
|
|
b157c7034f | ||
|
|
245ca5fe3b | ||
|
|
efc0d31724 | ||
|
|
4b7f7b4b8a | ||
|
|
7aee575352 | ||
|
|
2ef5b0d389 | ||
|
|
387a12cf1a | ||
|
|
f26e74f0f2 | ||
|
|
f2b6512eb1 | ||
|
|
93f3762016 | ||
|
|
620465f14c | ||
|
|
a132b83f1b | ||
|
|
5f522b5324 | ||
|
|
bd0aed06ce | ||
|
|
f9f88fb8a4 | ||
|
|
eefe613aaf | ||
|
|
d6d247f1f8 | ||
|
|
3b74ddd9ad | ||
|
|
eec4eeb76a | ||
|
|
73a9e470c3 | ||
|
|
b578d8d4f7 | ||
|
|
7a43546eeb | ||
|
|
e7c310934d | ||
|
|
07d5928f18 | ||
|
|
fa5bc17ed8 | ||
|
|
f8cb80ed7f | ||
|
|
066cd13e13 | ||
|
|
a087760d53 | ||
|
|
675ac9c32b | ||
|
|
d7191983bd | ||
|
|
14b3fd524f | ||
|
|
38ed0d0532 | ||
|
|
3b48f73b91 | ||
|
|
ae7e7942e3 | ||
|
|
ec1eddc06d | ||
|
|
2781771f6b | ||
|
|
9b35a2f904 | ||
|
|
a323350915 | ||
|
|
9b94cfdd24 | ||
|
|
2818ff56ee | ||
|
|
d728df7d40 | ||
|
|
a531eb649e | ||
|
|
fb4aa2b713 | ||
|
|
0df9d4b958 | ||
|
|
99523c70ed | ||
|
|
5c7a4fb861 | ||
|
|
436a24f8b2 | ||
|
|
93534de638 | ||
|
|
f29a11d20e | ||
|
|
880bd91f4a | ||
|
|
8e68782ff6 | ||
|
|
c1e5937ff3 | ||
|
|
6d1e39f871 | ||
|
|
5b92e969dc | ||
|
|
992a74499d | ||
|
|
4744e6371a | ||
|
|
00a03559ff | ||
|
|
a1a45078fb | ||
|
|
88d0b466ce | ||
|
|
e638df37d1 | ||
|
|
44b180f5c0 | ||
|
|
3feb11d138 | ||
|
|
41633be864 | ||
|
|
ce8fa2194b | ||
|
|
7d1c5ad14b | ||
|
|
6274a3dd39 | ||
|
|
a1e8e1aa20 | ||
|
|
b4aff0d8e9 | ||
|
|
e07df8fc43 | ||
|
|
9413e403b4 | ||
|
|
6eab88d44c | ||
|
|
754878eb63 | ||
|
|
c65456c646 | ||
|
|
dd55d23a37 | ||
|
|
aafed68964 | ||
|
|
108ac40b22 | ||
|
|
d2e9c04af1 | ||
|
|
3a1f58037d | ||
|
|
2731fb4a01 | ||
|
|
769db7202d | ||
|
|
644e871ec1 | ||
|
|
8987faa4f6 | ||
|
|
c237b33126 | ||
|
|
7098b67784 | ||
|
|
2d21d00651 | ||
|
|
198d5e4e06 | ||
|
|
98fc333df3 | ||
|
|
c0fb27f979 | ||
|
|
9ec1599427 | ||
|
|
9cfc54b1f5 | ||
|
|
40d2ac9a6b | ||
|
|
44db525049 | ||
|
|
d737cb3e14 | ||
|
|
1034d87a99 | ||
|
|
1243e6804c | ||
|
|
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 |
@@ -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
|
||||
}
|
||||
|
||||
@@ -31,6 +31,4 @@ venv
|
||||
*/mealie/.temp
|
||||
/mealie/frontend/
|
||||
|
||||
model.crfmodel
|
||||
|
||||
crowdin.yml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Package build
|
||||
name: Build Package
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -19,7 +19,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 🛠
|
||||
9
.github/workflows/e2e.yml
vendored
9
.github/workflows/e2e.yml
vendored
@@ -3,15 +3,8 @@ on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build-package:
|
||||
name: "Build Python package"
|
||||
uses: ./.github/workflows/partial-package.yml
|
||||
with:
|
||||
tag: e2e
|
||||
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
needs: build-package
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -20,7 +13,7 @@ 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
|
||||
|
||||
115
.github/workflows/locale-sync.yml
vendored
Normal file
115
.github/workflows/locale-sync.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
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: crowdin 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(l10n): Crowdin locale sync" \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$BRANCH_NAME" \
|
||||
--label "l10n" \
|
||||
--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"
|
||||
17
.github/workflows/nightly.yml
vendored
17
.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 Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
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
|
||||
|
||||
@@ -16,14 +16,7 @@ on:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-package:
|
||||
name: "Build Python package"
|
||||
uses: ./.github/workflows/partial-package.yml
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
|
||||
publish:
|
||||
needs: build-package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
1
.github/workflows/pull-request-lint.yml
vendored
1
.github/workflows/pull-request-lint.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
||||
deps
|
||||
auto
|
||||
l10n
|
||||
config
|
||||
# Configure that a scope must always be provided.
|
||||
requireScope: false
|
||||
# If the PR contains one of these newline-delimited labels, the
|
||||
|
||||
38
.github/workflows/pull-requests.yml
vendored
38
.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 Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
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 }}
|
||||
|
||||
19
.github/workflows/release.yml
vendored
19
.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 Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
uses: ./.github/workflows/test-frontend.yml
|
||||
|
||||
build-release:
|
||||
build-package:
|
||||
name: Build Package
|
||||
uses: ./.github/workflows/build-package.yml
|
||||
with:
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
|
||||
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.
|
||||
|
||||
@@ -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"
|
||||
7
.gitignore
vendored
7
.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
|
||||
@@ -157,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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
exclude: "mkdocs.yml"
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.7
|
||||
rev: v0.12.12
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -18,6 +18,7 @@
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.workingDirectories": [
|
||||
"./frontend"
|
||||
],
|
||||
|
||||
22
Taskfile.yml
22
Taskfile.yml
@@ -61,31 +61,17 @@ tasks:
|
||||
- pyproject.toml
|
||||
- .pre-commit-config.yaml
|
||||
|
||||
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 }}
|
||||
|
||||
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: docs:gen
|
||||
- task: py:format
|
||||
|
||||
dev:services:
|
||||
@@ -102,6 +88,8 @@ tasks:
|
||||
- rm -r ./dev/data/recipes/
|
||||
- rm -r ./dev/data/users/
|
||||
- rm -f ./dev/data/mealie*.db
|
||||
- rm -f ./dev/data/mealie*.db-shm
|
||||
- rm -f ./dev/data/mealie*.db-wal
|
||||
- rm -f ./dev/data/mealie.log
|
||||
- rm -f ./dev/data/.secret
|
||||
|
||||
@@ -258,7 +246,7 @@ 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/
|
||||
|
||||
@@ -35,7 +35,7 @@ conventional_commits = true
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/hay-kot/mealie/issues/${2}))"},
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/mealie-recipes/mealie/issues/${2}))"},
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
|
||||
@@ -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,29 +161,51 @@ 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.
|
||||
"""
|
||||
|
||||
|
||||
def inject_nuxt_values():
|
||||
all_date_locales = [
|
||||
f'"{match.stem}": require("./lang/dateTimeFormats/{match.name}"),' for match in datetime_dir.glob("*.json")
|
||||
]
|
||||
datetime_files = list(datetime_dir.glob("*.json"))
|
||||
datetime_files.sort()
|
||||
|
||||
datetime_imports = []
|
||||
datetime_object_entries = []
|
||||
|
||||
for match in datetime_files:
|
||||
# Convert locale name to camelCase variable name (e.g., "en-US" -> "enUS")
|
||||
var_name = match.stem.replace("-", "")
|
||||
|
||||
# Generate import statement
|
||||
import_line = f'import * as {var_name} from "./lang/dateTimeFormats/{match.name}";'
|
||||
datetime_imports.append(import_line)
|
||||
|
||||
# Generate object entry
|
||||
object_entry = f' "{match.stem}": {var_name},'
|
||||
datetime_object_entries.append(object_entry)
|
||||
|
||||
all_date_locales = datetime_imports + ["", "const datetimeFormats = {"] + datetime_object_entries + ["};"]
|
||||
|
||||
all_langs = []
|
||||
for match in locales_dir.glob("*.json"):
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name}" }},'
|
||||
match_data = LOCALE_DATA.get(match.stem)
|
||||
match_dir = match_data.dir if match_data else "ltr"
|
||||
|
||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}", dir: "{match_dir}" }},'
|
||||
all_langs.append(lang_string)
|
||||
|
||||
all_langs.sort()
|
||||
|
||||
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 +222,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)
|
||||
|
||||
@@ -8,8 +8,8 @@ from utils import log
|
||||
# ============================================================
|
||||
|
||||
template = """// This Code is auto generated by gen_ts_types.py
|
||||
{% for name in global %}import {{ name }} from "@/components/global/{{ name }}.vue";
|
||||
{% endfor %}{% for name in layout %}import {{ name }} from "@/components/layout/{{ name }}.vue";
|
||||
{% for name in global %}import type {{ name }} from "@/components/global/{{ name }}.vue";
|
||||
{% endfor %}{% for name in layout %}import type {{ name }} from "@/components/layout/{{ name }}.vue";
|
||||
{% endfor %}
|
||||
declare module "vue" {
|
||||
export interface GlobalComponents {
|
||||
|
||||
@@ -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 }}
|
||||
@@ -44,7 +44,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 cup unsalted butter, cut into cubes",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26",
|
||||
@@ -54,7 +53,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 cup light brown sugar",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82",
|
||||
@@ -64,7 +62,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1/2 cup granulated white sugar",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "034f481b-c426-4a17-b983-5aea9be4974b",
|
||||
@@ -74,7 +71,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 large eggs",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4",
|
||||
@@ -84,7 +80,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 tsp vanilla extract",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "85561ace-f249-401d-834c-e600a2f6280e",
|
||||
@@ -94,7 +89,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1/2 cup creamy peanut butter",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd",
|
||||
@@ -104,7 +98,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 tsp cornstarch",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0",
|
||||
@@ -114,7 +107,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 tsp baking soda",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "64627441-39f9-4ee3-8494-bafe36451d12",
|
||||
@@ -124,7 +116,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1/2 tsp salt",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384",
|
||||
@@ -134,7 +125,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1 cup cake flour",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "06967994-8548-4952-a8cc-16e8db228ebd",
|
||||
@@ -144,7 +134,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 cups all-purpose flour",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691",
|
||||
@@ -154,7 +143,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "2 cups peanut butter chips",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef",
|
||||
@@ -164,7 +152,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"note": "1½ cups Reese's Pieces candies",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2",
|
||||
@@ -221,7 +208,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"showAssets": False,
|
||||
"landscapeView": False,
|
||||
"disableComments": False,
|
||||
"disableAmount": True,
|
||||
"locked": False,
|
||||
},
|
||||
"assets": [],
|
||||
|
||||
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,7 +1,8 @@
|
||||
###############################################
|
||||
# Frontend Build
|
||||
###############################################
|
||||
FROM node:16 AS frontend-builder
|
||||
FROM node:20@sha256:f3e50c7689a1b6982fab45b1b23ba5adf1fd725e233dc640918fb59f7a57b174 \
|
||||
AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
@@ -20,7 +21,8 @@ RUN yarn generate
|
||||
###############################################
|
||||
# Base Image - Python
|
||||
###############################################
|
||||
FROM python:3.12-slim as python-base
|
||||
FROM python:3.12-slim@sha256:2267adc248a477c1f1a852a07a5a224d42abe54c28aafa572efa157dfb001bba \
|
||||
AS python-base
|
||||
|
||||
ENV MEALIE_HOME="/app"
|
||||
|
||||
@@ -116,17 +118,10 @@ COPY --from=packages * /dist/
|
||||
RUN . $VENV_PATH/bin/activate \
|
||||
&& pip install --require-hashes -r /dist/requirements.txt --find-links /dist
|
||||
|
||||
###############################################
|
||||
# CRFPP Image
|
||||
###############################################
|
||||
FROM hkotel/crfpp as crfpp
|
||||
|
||||
RUN echo "crfpp-container"
|
||||
|
||||
###############################################
|
||||
# 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
|
||||
@@ -139,24 +134,19 @@ RUN apt-get update \
|
||||
gosu \
|
||||
iproute2 \
|
||||
libldap-common \
|
||||
libldap-2.5 \
|
||||
libldap2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# create directory used for Docker Secrets
|
||||
RUN mkdir -p /run/secrets
|
||||
|
||||
# copy CRF++ and add it to the library 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 venv into image. It contains a fully-installed mealie backend and frontend.
|
||||
COPY --from=venv-builder $VENV_PATH $VENV_PATH
|
||||
|
||||
|
||||
# Grab CRF++ Model Release
|
||||
RUN python -m mealie.scripts.install_model
|
||||
# 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
|
||||
|
||||
@@ -45,7 +45,7 @@ Once the prerequisites are installed you can cd into the project base directory
|
||||
=== "Linux / macOS"
|
||||
|
||||
```bash
|
||||
# Naviate To The Root Directory
|
||||
# Navigate To The Root Directory
|
||||
cd /path/to/project
|
||||
|
||||
# Utilize the Taskfile to Install Dependencies
|
||||
|
||||
@@ -13,14 +13,14 @@ Steps:
|
||||
|
||||
#### 1. Get your API Token
|
||||
|
||||
Create an API token from Mealie's User Settings page (https://hay-kot.github.io/mealie/documentation/users-groups/user-settings/#api-key-generation)
|
||||
Create an API token from Mealie's User Settings page (https://docs.mealie.io/documentation/getting-started/api-usage/#getting-a-token)
|
||||
|
||||
#### 2. Create Home Assistant Sensors
|
||||
|
||||
Create REST sensors in home assistant to get the details of today's meal.
|
||||
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal.
|
||||
|
||||
Make sure the url and port (`http://mealie:9000` ) matches your installation's address and _API_ port.
|
||||
Make sure the url and port (`http://mealie:9000`) matches your installation's address and _API_ port.
|
||||
|
||||
```yaml
|
||||
rest:
|
||||
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /app/mealie/scripts/reset_locked_users.py
|
||||
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/reset_locked_users.py
|
||||
```
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /app/mealie/scripts/make_admin.py
|
||||
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/make_admin.py
|
||||
```
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
```shell
|
||||
docker exec -it mealie bash
|
||||
|
||||
python /app/mealie/scripts/change_password.py
|
||||
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/change_password.py
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ The shopping lists feature is a great way to keep track of what you need to buy
|
||||
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.
|
||||
@@ -117,6 +118,7 @@ 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 / Updating a recipe
|
||||
- Adding items to a shopping list
|
||||
- Creating a new mealplan
|
||||
@@ -198,6 +200,7 @@ Mealie lets you fully customize how you organize your users. You can use Groups
|
||||
Groups are fully isolated instances of Mealie. Think of a goup as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
|
||||
|
||||
Common use cases for groups include:
|
||||
|
||||
- Hosting multiple instances of Mealie for others who want to keep their data private and secure
|
||||
- Creating completely isolated recipe pools
|
||||
|
||||
@@ -206,6 +209,7 @@ Common use cases for groups include:
|
||||
Households are subdivisions within a single Group. Households maintain their own users and settings, while sharing their recipes with other households. Households also share organizers (tags, categories, etc.) with the entire group. Meal Plans, Shopping Lists, and Integrations are only accessible within a household.
|
||||
|
||||
Common use cases for households include:
|
||||
|
||||
- Sharing a common recipe pool amongst families
|
||||
- Maintaining separate meal plans and shopping lists from other households
|
||||
- Maintaining separate integrations and customizations from other households
|
||||
|
||||
@@ -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,15 +32,16 @@
|
||||
|
||||
### Database
|
||||
|
||||
| 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 |
|
||||
| Variables | Default | Description |
|
||||
|---------------------------------------------------------|:--------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
||||
| SQLITE_MIGRATE_JOURNAL_WAL | False | If set to true, switches SQLite's journal mode to WAL, which allows for multiple concurrent accesses. This can be useful when you have a decent amount of concurrency or when using certain remote storage systems such as Ceph. |
|
||||
| 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
|
||||
|
||||
@@ -108,7 +110,9 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
||||
| 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
|
||||
@@ -118,13 +122,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<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_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 |
|
||||
| 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 | 300 | 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
|
||||
|
||||
@@ -149,8 +157,6 @@ Setting the following environmental variables will change the theme of the front
|
||||
|
||||
### Docker Secrets
|
||||
|
||||
### Docker 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
|
||||
|
||||
@@ -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.7.0`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.1.2`
|
||||
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
|
||||
|
||||
@@ -60,7 +60,7 @@ The following steps were tested on a Ubuntu 20.04 server, but should work for mo
|
||||
|
||||
## Step 3: Customizing The `docker-compose.yaml` files.
|
||||
|
||||
After you've decided setup the files it's important to set a few ENV variables to ensure that you can use all the features of Mealie. I recommend that you verify and check that:
|
||||
After you've decided how to set up your files, it's important to set a few ENV variables to ensure that you can use all the features of Mealie. Verify that:
|
||||
|
||||
- [x] You've configured the relevant ENV variables for your database selection in the `docker-compose.yaml` files.
|
||||
- [x] You've configured the [SMTP server settings](./backend-config.md#email) (used for invitations, password resets, etc). You can setup a [google app password](https://support.google.com/accounts/answer/185833?hl=en) if you want to send email via gmail.
|
||||
@@ -117,7 +117,7 @@ The latest tag provides the latest released image of Mealie.
|
||||
|
||||
---
|
||||
|
||||
**These tags no are long updated**
|
||||
**These tags are no longer updated**
|
||||
|
||||
`mealie:frontend-v1.0.0beta-x` **and** `mealie:api-v1.0.0beta-x`
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Installing with PostgreSQL
|
||||
|
||||
!!! Warning
|
||||
When upgrading postgresql major versions, manual steps are required [Postgres#37](https://github.com/docker-library/postgres/issues/37).
|
||||
|
||||
PostgreSQL might be considered if you need to support many concurrent users. In addition, some features are only enabled on PostgreSQL, such as fuzzy search.
|
||||
|
||||
**For Environment Variable Configuration, see** [Backend Configuration](./backend-config.md)
|
||||
@@ -7,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.7.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.1.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
@@ -38,7 +41,7 @@ services:
|
||||
|
||||
postgres:
|
||||
container_name: postgres
|
||||
image: postgres:15
|
||||
image: postgres:17
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-pgdata:/var/lib/postgresql/data
|
||||
@@ -46,6 +49,7 @@ services:
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_USER: mealie
|
||||
PGUSER: mealie
|
||||
POSTGRES_DB: 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.7.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.1.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -2,6 +2,3 @@
|
||||
|
||||
## Feature Requests
|
||||
[Please request new features on Github](https://github.com/mealie-recipes/mealie/discussions/new?category=feature-request)
|
||||
|
||||
## Progress
|
||||
See the [Github Projects page](https://github.com/users/hay-kot/projects/2) to see what is currently being worked on
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
You MUST read the release notes prior to upgrading your container. Mealie has a robust backup and restore system for managing your data. Pre-v1.0.0 versions of Mealie use a different database structure, so if you are upgrading from pre-v1.0.0 to v1.0.0, you MUST backup your data and then re-import it. Even if you are already on v1.0.0, it is strongly recommended to backup all data before updating.
|
||||
|
||||
### Before Upgrading
|
||||
- Read The Release Notes
|
||||
- [Read The Release Notes](https://github.com/mealie-recipes/mealie/releases)
|
||||
- Identify Breaking Changes
|
||||
- Create a Backup and Download from the UI
|
||||
- Upgrade
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -351,7 +351,7 @@
|
||||
<!-- Custom narrow footer -->
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-footer-social">
|
||||
<a class="md-footer-social__link" href="https://github.com/hay-kot/mealie" rel="noopener" target="_blank"
|
||||
<a class="md-footer-social__link" href="https://github.com/mealie-recipes/mealie" rel="noopener" target="_blank"
|
||||
title="github.com">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@@ -90,7 +90,7 @@ nav:
|
||||
- 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 Shortcuts: "documentation/community-guide/ios.md"
|
||||
- iOS Shortcut: "documentation/community-guide/ios-shortcut.md"
|
||||
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
||||
|
||||
- API Reference: "api/redoc.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",
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
@@ -20,74 +44,54 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
<script setup lang="ts">
|
||||
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";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
export default defineComponent({
|
||||
components: { QueryFilterBuilder },
|
||||
props: {
|
||||
cookbook: {
|
||||
type: Object as () => ReadCookBook,
|
||||
required: true,
|
||||
},
|
||||
actions: {
|
||||
type: Object as () => any,
|
||||
required: true,
|
||||
},
|
||||
const modelValue = defineModel<ReadCookBook>({ required: true });
|
||||
const i18n = useI18n();
|
||||
const cookbook = toRef(modelValue);
|
||||
function handleInput(value: string | undefined) {
|
||||
cookbook.value.queryFilterString = value || "";
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.t("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
setup(props) {
|
||||
const { i18n } = useContext();
|
||||
|
||||
function handleInput(value: string | undefined) {
|
||||
props.cookbook.queryFilterString = value || "";
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.tc("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.tc("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
{
|
||||
name: "recipe_ingredient.food.id",
|
||||
label: i18n.tc("recipe.ingredients"),
|
||||
type: Organizer.Food,
|
||||
},
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.tc("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.tc("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.tc("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.tc("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
handleInput,
|
||||
fieldDefs,
|
||||
};
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.t("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
});
|
||||
{
|
||||
name: "recipe_ingredient.food.id",
|
||||
label: i18n.t("recipe.ingredients"),
|
||||
type: Organizer.Food,
|
||||
},
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.t("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.t("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.t("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -7,44 +7,56 @@
|
||||
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"
|
||||
/>
|
||||
</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,92 +64,67 @@
|
||||
</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 setup 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 { ReadCookBook } 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();
|
||||
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 book = getOne(slug);
|
||||
|
||||
const isOwnHousehold = computed(() => {
|
||||
if (!($auth.user && book.value?.householdId)) {
|
||||
return false;
|
||||
}
|
||||
const isOwnHousehold = computed(() => {
|
||||
if (!($auth.user.value && book.value?.householdId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $auth.user.householdId === book.value.householdId;
|
||||
})
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
return $auth.user.value.householdId === book.value.householdId;
|
||||
});
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
|
||||
const dialogStates = reactive({
|
||||
edit: false,
|
||||
});
|
||||
const dialogStates = reactive({
|
||||
edit: false,
|
||||
});
|
||||
|
||||
const editTarget = ref<RecipeCookBook | null>(null);
|
||||
function handleEditCookbook() {
|
||||
dialogStates.edit = true;
|
||||
editTarget.value = book.value;
|
||||
}
|
||||
const editTarget = ref<ReadCookBook | 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);
|
||||
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
|
||||
router.push(`/g/${route.value.params.groupSlug}/cookbooks/${response?.slug}`);
|
||||
} else {
|
||||
// otherwise reload the page, since the recipe criteria changed
|
||||
router.go(0);
|
||||
}
|
||||
dialogStates.edit = false;
|
||||
editTarget.value = null;
|
||||
}
|
||||
if (response?.slug && book.value?.slug !== response?.slug) {
|
||||
// if name changed, redirect to new slug
|
||||
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",
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
book,
|
||||
slug,
|
||||
tab,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
canEdit,
|
||||
dialogStates,
|
||||
editTarget,
|
||||
handleEditCookbook,
|
||||
editCookbook,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head: {}, // Must include for useMeta
|
||||
});
|
||||
</script>
|
||||
useSeoMeta({
|
||||
title: book?.value?.name || "Cookbook",
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,55 +7,46 @@
|
||||
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";
|
||||
<script setup lang="ts">
|
||||
import { parseISO, formatDistanceToNow } from "date-fns";
|
||||
import { GroupDataExport } from "~/lib/api/types/group";
|
||||
export default defineComponent({
|
||||
props: {
|
||||
exports: {
|
||||
type: Array as () => GroupDataExport[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { i18n } = useContext();
|
||||
import type { GroupDataExport } from "~/lib/api/types/group";
|
||||
|
||||
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" },
|
||||
];
|
||||
defineProps<{
|
||||
exports: GroupDataExport[];
|
||||
}>();
|
||||
|
||||
function getTimeToExpire(timeString: string) {
|
||||
const expiresAt = parseISO(timeString);
|
||||
const i18n = useI18n();
|
||||
|
||||
return formatDistanceToNow(expiresAt, {
|
||||
addSuffix: false,
|
||||
});
|
||||
}
|
||||
const headers = [
|
||||
{ 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 downloadData(_: any) {
|
||||
console.log("Downloading data...");
|
||||
}
|
||||
function getTimeToExpire(timeString: string) {
|
||||
const expiresAt = parseISO(timeString);
|
||||
|
||||
return {
|
||||
downloadData,
|
||||
headers,
|
||||
getTimeToExpire,
|
||||
};
|
||||
},
|
||||
});
|
||||
return formatDistanceToNow(expiresAt, {
|
||||
addSuffix: false,
|
||||
});
|
||||
}
|
||||
|
||||
function downloadData(_: any) {
|
||||
console.log("Downloading data...");
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
<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";
|
||||
<script setup lang="ts">
|
||||
import type { ReadGroupPreferences } from "~/lib/api/types/user";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
preferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
const preferences = defineModel<ReadGroupPreferences>({ required: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<v-select
|
||||
v-model="selected"
|
||||
:items="households"
|
||||
:label="label"
|
||||
:hint="description"
|
||||
:persistent-hint="!!description"
|
||||
item-text="name"
|
||||
:multiple="multiselect"
|
||||
:prepend-inner-icon="$globals.icons.household"
|
||||
return-object
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
small
|
||||
close
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
@click:close="removeByIndex(data.index)"
|
||||
>
|
||||
{{ data.item.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 {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => HouseholdLike[],
|
||||
required: true,
|
||||
},
|
||||
multiselect: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const selected = computed({
|
||||
get: () => props.value,
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (selected.value === undefined) {
|
||||
selected.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
const { i18n } = useContext();
|
||||
const label = computed(
|
||||
() => props.multiselect ? i18n.tc("household.households") : i18n.tc("household.household")
|
||||
);
|
||||
|
||||
const { store: households } = useHouseholdStore();
|
||||
function removeByIndex(index: number) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
selected.value = [...newSelected];
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
label,
|
||||
households,
|
||||
removeByIndex,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -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: activatorProps }">
|
||||
<v-btn
|
||||
:class="{ 'rounded-circle': fab }"
|
||||
:size="fab ? 'small' : undefined"
|
||||
:color="color"
|
||||
:icon="!fab"
|
||||
variant="text"
|
||||
dark
|
||||
v-bind="activatorProps"
|
||||
@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>
|
||||
@@ -35,11 +50,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
<script setup lang="ts">
|
||||
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,96 +64,84 @@ export interface ContextMenuItem {
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
},
|
||||
props: {
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
interface Props {
|
||||
recipes?: Recipe[];
|
||||
menuTop?: boolean;
|
||||
fab?: boolean;
|
||||
color?: string;
|
||||
menuIcon?: string | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipes: () => [],
|
||||
menuTop: true,
|
||||
fab: false,
|
||||
color: "primary",
|
||||
menuIcon: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
[key: string]: [];
|
||||
}>();
|
||||
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
shoppingListDialog: false,
|
||||
menuItems: [
|
||||
{
|
||||
title: i18n.t("recipe.add-to-list"),
|
||||
icon: $globals.icons.cartCheck,
|
||||
color: undefined,
|
||||
event: "shoppingList",
|
||||
isPublic: false,
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
menuIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { $globals, i18n } = useContext();
|
||||
const api = useUserApi();
|
||||
],
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
shoppingListDialog: false,
|
||||
menuItems: [
|
||||
{
|
||||
title: i18n.tc("recipe.add-to-list"),
|
||||
icon: $globals.icons.cartCheck,
|
||||
color: undefined,
|
||||
event: "shoppingList",
|
||||
isPublic: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
const { shoppingListDialog, menuItems } = toRefs(state);
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipesWithScales = computed(() => {
|
||||
return props.recipes.map((recipe) => {
|
||||
return {
|
||||
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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
shoppingList: () => {
|
||||
getShoppingLists();
|
||||
state.shoppingListDialog = true;
|
||||
},
|
||||
};
|
||||
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
const handler = eventHandlers[eventKey];
|
||||
|
||||
if (handler && typeof handler === "function") {
|
||||
handler();
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
context.emit(eventKey);
|
||||
state.loading = false;
|
||||
}
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipesWithScales = computed(() => {
|
||||
return props.recipes.map((recipe) => {
|
||||
return {
|
||||
...toRefs(state),
|
||||
contextMenuEventHandler,
|
||||
icon,
|
||||
recipesWithScales,
|
||||
shoppingLists,
|
||||
}
|
||||
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 as ShoppingListSummary[] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
shoppingList: () => {
|
||||
getShoppingLists();
|
||||
state.shoppingListDialog = true;
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
const handler = eventHandlers[eventKey];
|
||||
|
||||
if (handler && typeof handler === "function") {
|
||||
handler();
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
emit(eventKey);
|
||||
state.loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,164 +1,122 @@
|
||||
<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="day"
|
||||
:items="MEAL_DAY_OPTIONS"
|
||||
:label="$t('meal-plan.rule-day')"
|
||||
/>
|
||||
<v-select
|
||||
v-model="entryType"
|
||||
:items="MEAL_TYPE_OPTIONS"
|
||||
:label="$t('meal-plan.meal-type')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<QueryFilterBuilder
|
||||
:field-defs="fieldDefs"
|
||||
:initial-query-filter="queryFilter"
|
||||
:initial-query-filter="props.queryFilter"
|
||||
@input="handleQueryFilterInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 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: day === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [day]),
|
||||
mealTypeCriteria: entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [entryType]),
|
||||
}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
||||
<script setup lang="ts">
|
||||
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({
|
||||
components: {
|
||||
QueryFilterBuilder,
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
entryType: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
queryFilterString: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
queryFilter: {
|
||||
type: Object as () => QueryFilterJSON,
|
||||
default: null,
|
||||
},
|
||||
showHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
const inputDay = computed({
|
||||
get: () => {
|
||||
return props.day;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:day", val);
|
||||
},
|
||||
});
|
||||
|
||||
const inputEntryType = computed({
|
||||
get: () => {
|
||||
return props.entryType;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:entry-type", val);
|
||||
},
|
||||
});
|
||||
|
||||
const inputQueryFilterString = computed({
|
||||
get: () => {
|
||||
return props.queryFilterString;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:query-filter-string", val);
|
||||
},
|
||||
});
|
||||
|
||||
function handleQueryFilterInput(value: string | undefined) {
|
||||
inputQueryFilterString.value = value || "";
|
||||
};
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.tc("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.tc("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
{
|
||||
name: "recipe_ingredient.food.id",
|
||||
label: i18n.tc("recipe.ingredients"),
|
||||
type: Organizer.Food,
|
||||
},
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.tc("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.tc("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "last_made",
|
||||
label: i18n.tc("general.last-made"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.tc("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.tc("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
MEAL_TYPE_OPTIONS,
|
||||
MEAL_DAY_OPTIONS,
|
||||
inputDay,
|
||||
inputEntryType,
|
||||
inputQueryFilterString,
|
||||
handleQueryFilterInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
interface Props {
|
||||
queryFilter?: QueryFilterJSON | null;
|
||||
showHelp?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
queryFilter: null,
|
||||
showHelp: false,
|
||||
});
|
||||
|
||||
const day = defineModel<string>("day", { default: "unset" });
|
||||
const entryType = defineModel<string>("entryType", { default: "unset" });
|
||||
const queryFilterString = defineModel<string>("queryFilterString", { default: "" });
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
{ 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 = [
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
function handleQueryFilterInput(value: string | undefined) {
|
||||
console.warn("handleQueryFilterInput called with value:", value);
|
||||
queryFilterString.value = value || "";
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.t("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.t("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
{
|
||||
name: "recipe_ingredient.food.id",
|
||||
label: i18n.t("recipe.ingredients"),
|
||||
type: Organizer.Food,
|
||||
},
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.t("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.t("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "last_made",
|
||||
label: i18n.t("general.last-made"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.t("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -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-text-field
|
||||
v-model="scheduledTime"
|
||||
type="time"
|
||||
clearable
|
||||
variant="underlined"
|
||||
/>
|
||||
</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',
|
||||
},
|
||||
]"
|
||||
@@ -33,52 +50,43 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||
import { ReadWebhook } from "~/lib/api/types/household";
|
||||
<script setup lang="ts">
|
||||
import type { ReadWebhook } from "~/lib/api/types/household";
|
||||
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
webhook: {
|
||||
type: Object as () => ReadWebhook,
|
||||
required: true,
|
||||
},
|
||||
const props = defineProps<{
|
||||
webhook: ReadWebhook;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [id: string];
|
||||
save: [webhook: ReadWebhook];
|
||||
test: [id: string];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
|
||||
const scheduledTime = computed({
|
||||
get() {
|
||||
return itemLocal.value;
|
||||
},
|
||||
emits: ["delete", "save", "test"],
|
||||
setup(props, { emit }) {
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
|
||||
const scheduledTime = computed({
|
||||
get() {
|
||||
return itemLocal.value;
|
||||
},
|
||||
set(v: string) {
|
||||
itemUTC.value = timeLocalToUTC(v);
|
||||
itemLocal.value = v;
|
||||
},
|
||||
});
|
||||
|
||||
const webhookCopy = ref({ ...props.webhook });
|
||||
|
||||
function handleSave() {
|
||||
webhookCopy.value.scheduledTime = itemLocal.value;
|
||||
emit("save", webhookCopy.value);
|
||||
}
|
||||
|
||||
return {
|
||||
webhookCopy,
|
||||
scheduledTime,
|
||||
handleSave,
|
||||
itemUTC,
|
||||
itemLocal,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("settings.webhooks.webhooks") as string,
|
||||
};
|
||||
set(v: string) {
|
||||
itemUTC.value = timeLocalToUTC(v);
|
||||
itemLocal.value = v;
|
||||
},
|
||||
});
|
||||
|
||||
const webhookCopy = ref({ ...props.webhook });
|
||||
|
||||
function handleSave() {
|
||||
webhookCopy.value.scheduledTime = itemLocal.value;
|
||||
emit("save", webhookCopy.value);
|
||||
}
|
||||
|
||||
// Set page title using useSeoMeta
|
||||
useSeoMeta({
|
||||
title: i18n.t("settings.webhooks.webhooks"),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,159 +1,116 @@
|
||||
<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";
|
||||
<script setup lang="ts">
|
||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
|
||||
const i18n = useI18n();
|
||||
|
||||
type Preference = {
|
||||
key: keyof ReadHouseholdPreferences;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
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"),
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
|
||||
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 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);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
allDays,
|
||||
preferences,
|
||||
recipePreferences,
|
||||
};
|
||||
{
|
||||
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"),
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,37 @@
|
||||
<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>
|
||||
<v-tooltip v-if="canEdit" location="bottom" color="info">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="flat"
|
||||
rounded="circle"
|
||||
size="small"
|
||||
color="info"
|
||||
class="ml-1"
|
||||
v-bind="tooltipProps"
|
||||
@click="$emit('edit', true)"
|
||||
>
|
||||
<v-icon size="x-large">
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
@@ -37,14 +41,14 @@
|
||||
<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="{
|
||||
edit: false,
|
||||
@@ -66,125 +70,102 @@
|
||||
<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";
|
||||
<script setup lang="ts">
|
||||
import RecipeContextMenu from "./RecipeContextMenu/RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.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, RecipeTimelineBadge },
|
||||
props: {
|
||||
recipe: {
|
||||
required: true,
|
||||
type: Object as () => Recipe,
|
||||
},
|
||||
slug: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
open: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
loggedIn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(_, context) {
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const { i18n, $globals } = useContext();
|
||||
const editorButtons = [
|
||||
{
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
event: DELETE_EVENT,
|
||||
color: "error",
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.json"),
|
||||
icon: $globals.icons.codeBraces,
|
||||
event: JSON_EVENT,
|
||||
color: "accent",
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.close"),
|
||||
icon: $globals.icons.close,
|
||||
event: CLOSE_EVENT,
|
||||
color: "",
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.save"),
|
||||
icon: $globals.icons.save,
|
||||
event: SAVE_EVENT,
|
||||
color: "success",
|
||||
},
|
||||
];
|
||||
|
||||
function emitHandler(event: string) {
|
||||
switch (event) {
|
||||
case CLOSE_EVENT:
|
||||
context.emit(CLOSE_EVENT);
|
||||
context.emit("input", false);
|
||||
break;
|
||||
case DELETE_EVENT:
|
||||
deleteDialog.value = true;
|
||||
break;
|
||||
default:
|
||||
context.emit(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function emitDelete() {
|
||||
context.emit(DELETE_EVENT);
|
||||
context.emit("input", false);
|
||||
}
|
||||
|
||||
return {
|
||||
deleteDialog,
|
||||
editorButtons,
|
||||
emitHandler,
|
||||
emitDelete,
|
||||
};
|
||||
},
|
||||
interface Props {
|
||||
recipe: Recipe;
|
||||
slug: string;
|
||||
recipeScale?: number;
|
||||
open: boolean;
|
||||
name: string;
|
||||
loggedIn?: boolean;
|
||||
recipeId: string;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipeScale: 1,
|
||||
loggedIn: false,
|
||||
canEdit: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
|
||||
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const editorButtons = [
|
||||
{
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
event: DELETE_EVENT,
|
||||
color: "error",
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.json"),
|
||||
icon: $globals.icons.codeBraces,
|
||||
event: JSON_EVENT,
|
||||
color: "accent",
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.close"),
|
||||
icon: $globals.icons.close,
|
||||
event: CLOSE_EVENT,
|
||||
color: "",
|
||||
},
|
||||
{
|
||||
text: i18n.t("general.save"),
|
||||
icon: $globals.icons.save,
|
||||
event: SAVE_EVENT,
|
||||
color: "success",
|
||||
},
|
||||
];
|
||||
|
||||
function emitHandler(event: string) {
|
||||
switch (event) {
|
||||
case CLOSE_EVENT:
|
||||
emit("close");
|
||||
emit("input", false);
|
||||
break;
|
||||
case DELETE_EVENT:
|
||||
deleteDialog.value = true;
|
||||
break;
|
||||
default:
|
||||
emit(event as any);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function emitDelete() {
|
||||
emit("delete");
|
||||
emit("input", false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -209,9 +190,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 location="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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user