Compare commits
536 Commits
v1.0.0-RC1
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0800a8d00a | ||
|
|
4d49e307e3 | ||
|
|
20621a1950 | ||
|
|
5a0b8940f5 | ||
|
|
c27d20b5c9 | ||
|
|
5f24e87e84 | ||
|
|
72980b3472 | ||
|
|
cedccf046b | ||
|
|
63514ab41c | ||
|
|
f8e8a40ec1 | ||
|
|
65ce53fb17 | ||
|
|
1352ae81c4 | ||
|
|
5c3b1f2890 | ||
|
|
555e341b65 | ||
|
|
6690ef8cab | ||
|
|
bbd6e7ef92 | ||
|
|
54a954164a | ||
|
|
d20ee21076 | ||
|
|
ce7efd7505 | ||
|
|
04f0d33ca3 | ||
|
|
01649de1e7 | ||
|
|
3a739ba194 | ||
|
|
5e78aa6e29 | ||
|
|
a121fe9b55 | ||
|
|
d1c4a9b422 | ||
|
|
e0c72c5508 | ||
|
|
4df4b7e0b6 | ||
|
|
e8d7c0423f | ||
|
|
583151087e | ||
|
|
c4b493564b | ||
|
|
e1aba3373a | ||
|
|
ee65d7d67d | ||
|
|
9029bccf5b | ||
|
|
4fdf844485 | ||
|
|
d2b1c2c5af | ||
|
|
84bd738ba5 | ||
|
|
e4eb4d3e3b | ||
|
|
422c485832 | ||
|
|
865ab6f843 | ||
|
|
cb948a8289 | ||
|
|
8d38ef1fd7 | ||
|
|
2ae2475b8e | ||
|
|
92c8b196de | ||
|
|
e6ad0aad81 | ||
|
|
c6fbf8bce8 | ||
|
|
8db5f7cce3 | ||
|
|
5f40064e2f | ||
|
|
e0cb6e0624 | ||
|
|
8bc73de815 | ||
|
|
71e2091130 | ||
|
|
8e65a4c65a | ||
|
|
34df20da81 | ||
|
|
254b6ae118 | ||
|
|
a840cb0800 | ||
|
|
098c8194f5 | ||
|
|
1daf41f452 | ||
|
|
72696cac20 | ||
|
|
4d3ea5d231 | ||
|
|
721063d091 | ||
|
|
4ae5c52de9 | ||
|
|
58cb43e999 | ||
|
|
0b0c25d2f3 | ||
|
|
21161dbf2e | ||
|
|
f62feb8da2 | ||
|
|
2cdbe816a6 | ||
|
|
e32fddbc85 | ||
|
|
8c17a81c91 | ||
|
|
d2188508fc | ||
|
|
09dfca4f34 | ||
|
|
61289416a5 | ||
|
|
16d3dbef5d | ||
|
|
702907fc30 | ||
|
|
600c569ae8 | ||
|
|
96995a4168 | ||
|
|
3a9fd11344 | ||
|
|
62dffb622f | ||
|
|
26dfc54d23 | ||
|
|
5d08647196 | ||
|
|
b51cd5d1c2 | ||
|
|
4e66d5fb92 | ||
|
|
f0bbe796ce | ||
|
|
8d8a101580 | ||
|
|
0602d0bf00 | ||
|
|
7e9c8b9e43 | ||
|
|
10ba4d2d7f | ||
|
|
d17e46ee50 | ||
|
|
6acadbc52b | ||
|
|
943d65a2ac | ||
|
|
521f309517 | ||
|
|
f52eab6512 | ||
|
|
6358340df1 | ||
|
|
f60c60be4d | ||
|
|
2a5997a968 | ||
|
|
b4c0a8b509 | ||
|
|
8eae58e0c1 | ||
|
|
8d7659cf6a | ||
|
|
93932cc0c5 | ||
|
|
5436e05d0e | ||
|
|
030588e5bb | ||
|
|
43958527f4 | ||
|
|
4357c37d22 | ||
|
|
631500a574 | ||
|
|
e7eb2a8dc5 | ||
|
|
5418223425 | ||
|
|
940df92222 | ||
|
|
21c4ba54a5 | ||
|
|
58df50f624 | ||
|
|
9c0ce1af27 | ||
|
|
b8dd0fc97c | ||
|
|
0d89c86559 | ||
|
|
ce929f2833 | ||
|
|
1d7fc0a945 | ||
|
|
c64ae94043 | ||
|
|
1aa9bbe8e2 | ||
|
|
9b97a7b3e7 | ||
|
|
60eabf3e1c | ||
|
|
1f195a02a0 | ||
|
|
238ef838ba | ||
|
|
04d2fa2416 | ||
|
|
b79c0ad441 | ||
|
|
ab3c9e8bf0 | ||
|
|
72e78feaad | ||
|
|
da71ee5deb | ||
|
|
e0fca5e4b7 | ||
|
|
2951042e7a | ||
|
|
3cf9d40326 | ||
|
|
bc44cdf16f | ||
|
|
b41509bea0 | ||
|
|
53b790b18c | ||
|
|
00abd43fab | ||
|
|
e37426d5d8 | ||
|
|
aed9f0047c | ||
|
|
9b365f882a | ||
|
|
b51da29e79 | ||
|
|
edb9c517c7 | ||
|
|
d3b3bb63a6 | ||
|
|
0bf09ad910 | ||
|
|
36de815f50 | ||
|
|
d756f3a057 | ||
|
|
40172ed62e | ||
|
|
5ca340a0bd | ||
|
|
8fc7ab8c0a | ||
|
|
52af8d6132 | ||
|
|
fcd00b6f5b | ||
|
|
8c9b513d21 | ||
|
|
c7c96531f9 | ||
|
|
ca7646dde4 | ||
|
|
2bb2106251 | ||
|
|
3cf76a64e7 | ||
|
|
031ba075d8 | ||
|
|
1e4fde88ed | ||
|
|
143f44ad68 | ||
|
|
29c3cb2296 | ||
|
|
8ca5a9454e | ||
|
|
b1fa089236 | ||
|
|
6e8b84be80 | ||
|
|
00db83d218 | ||
|
|
82eda8f630 | ||
|
|
f1559e5512 | ||
|
|
8cf1700d03 | ||
|
|
9571816ac4 | ||
|
|
ccaf7b0279 | ||
|
|
0b05239064 | ||
|
|
4cb07aa09f | ||
|
|
d3c9f928b1 | ||
|
|
d3dac815f6 | ||
|
|
7e8388dec9 | ||
|
|
b80ed443c9 | ||
|
|
089c24cbda | ||
|
|
71b3989eec | ||
|
|
6b3266e470 | ||
|
|
ac9f335e59 | ||
|
|
44ca79e7cd | ||
|
|
7602c67449 | ||
|
|
b3f7f2d89f | ||
|
|
76f7ebd12b | ||
|
|
36ca540033 | ||
|
|
f63c469bf5 | ||
|
|
33eecbbd13 | ||
|
|
a4880c5d95 | ||
|
|
29525ed54e | ||
|
|
2d85c15153 | ||
|
|
c04025b5f2 | ||
|
|
e53c5ca0f0 | ||
|
|
d5252796ac | ||
|
|
f36093c023 | ||
|
|
234ae8732d | ||
|
|
01f4abc0ad | ||
|
|
00f5cc2dd2 | ||
|
|
02d71eb36e | ||
|
|
3ac9d9bce7 | ||
|
|
13180718c7 | ||
|
|
9f18d63b8a | ||
|
|
d4c70d66c5 | ||
|
|
270c70ce87 | ||
|
|
134812e4ce | ||
|
|
7aecaf58b1 | ||
|
|
7e9b429e7d | ||
|
|
07f96ca6a5 | ||
|
|
ae7d3d7e53 | ||
|
|
e11614fba3 | ||
|
|
5837ca4ce7 | ||
|
|
5b778e7a7d | ||
|
|
783aa77b9b | ||
|
|
b6a2dd51ea | ||
|
|
99c9be0778 | ||
|
|
a8dfc1680a | ||
|
|
fe9803e2a5 | ||
|
|
4d2ca15163 | ||
|
|
299dc1a40d | ||
|
|
ae5e557df9 | ||
|
|
1e30462eaf | ||
|
|
10e4c17592 | ||
|
|
0726ce828c | ||
|
|
2dd80bd8dc | ||
|
|
8fa5065814 | ||
|
|
972610d1de | ||
|
|
6a71af98bd | ||
|
|
f6f9a1c532 | ||
|
|
46cb36212a | ||
|
|
ece11a5f39 | ||
|
|
73559bea91 | ||
|
|
9809167188 | ||
|
|
a1b36d9361 | ||
|
|
f89586894c | ||
|
|
408718558b | ||
|
|
342fa6128e | ||
|
|
331bff84c3 | ||
|
|
88aa8b2edb | ||
|
|
8aa9402e1d | ||
|
|
86ba7dcf23 | ||
|
|
7222abe244 | ||
|
|
fae8484f84 | ||
|
|
47d955adca | ||
|
|
e5a19b3a49 | ||
|
|
d31293405c | ||
|
|
faf743ecf3 | ||
|
|
aa44fef36f | ||
|
|
0ccbb415b4 | ||
|
|
b31647b52b | ||
|
|
0aba36e17f | ||
|
|
954ee3dc19 | ||
|
|
1d72178b93 | ||
|
|
36712cdb14 | ||
|
|
2236a93a91 | ||
|
|
46c1a2f0b8 | ||
|
|
bbd8dd6e73 | ||
|
|
ca9f66ee24 | ||
|
|
c48680374d | ||
|
|
c5f2df57f6 | ||
|
|
07037011ae | ||
|
|
861e8ac564 | ||
|
|
92d955172d | ||
|
|
b13d66108d | ||
|
|
ce5b31dce5 | ||
|
|
b1d26283ba | ||
|
|
ef2918c85b | ||
|
|
95bb6d0184 | ||
|
|
8311db7e60 | ||
|
|
c3ec875d59 | ||
|
|
0151527fe3 | ||
|
|
787e2885b3 | ||
|
|
c9c568570f | ||
|
|
c226e71c17 | ||
|
|
c6263a38ab | ||
|
|
5a153b178d | ||
|
|
a6ec488864 | ||
|
|
e5f278fdbb | ||
|
|
275bb9617a | ||
|
|
32616a8c2b | ||
|
|
2ca21d3bbc | ||
|
|
39240a401f | ||
|
|
e7e5c9d62d | ||
|
|
780f0b9ca5 | ||
|
|
36b35caffb | ||
|
|
f38541588a | ||
|
|
0b6549c90a | ||
|
|
2986eb3498 | ||
|
|
62bf39f1a0 | ||
|
|
f7dca11b1c | ||
|
|
c26ffd4f46 | ||
|
|
c46f592d29 | ||
|
|
5a2c2be4e6 | ||
|
|
4c8344d91e | ||
|
|
f343c48450 | ||
|
|
2db654f88b | ||
|
|
8c9d3c1654 | ||
|
|
c62d797c38 | ||
|
|
f14f095d62 | ||
|
|
224bb5bd10 | ||
|
|
6dec3be096 | ||
|
|
c95432dbf1 | ||
|
|
781722ff70 | ||
|
|
f0cc3f7f18 | ||
|
|
8510e27176 | ||
|
|
fd6c01414d | ||
|
|
9908cf294d | ||
|
|
a039f40a08 | ||
|
|
a848d04b38 | ||
|
|
dfb6babf34 | ||
|
|
e168550171 | ||
|
|
b6bb341055 | ||
|
|
d5d2893f72 | ||
|
|
37da49fc05 | ||
|
|
1b68aa1ee9 | ||
|
|
ad0f645447 | ||
|
|
10276a1dea | ||
|
|
3823e4ab0b | ||
|
|
e30614d9c8 | ||
|
|
7448583eb9 | ||
|
|
579d87ed56 | ||
|
|
c0da8b1282 | ||
|
|
59d10ccb13 | ||
|
|
739213f718 | ||
|
|
2aed16746e | ||
|
|
7aac82bff3 | ||
|
|
2cba2d5fd2 | ||
|
|
1c1e50dbda | ||
|
|
f8ad72ec31 | ||
|
|
449bb6f0ce | ||
|
|
415afbfae4 | ||
|
|
b28aa82846 | ||
|
|
310069a7e9 | ||
|
|
1d1d61df77 | ||
|
|
2751e8318a | ||
|
|
b369417690 | ||
|
|
1ac6e651d1 | ||
|
|
0e45b962b2 | ||
|
|
a130bf8847 | ||
|
|
84b2519320 | ||
|
|
f32444b91d | ||
|
|
a2d9387077 | ||
|
|
27c4d92517 | ||
|
|
0135a4e73e | ||
|
|
c1393a6b0c | ||
|
|
5639047167 | ||
|
|
79b4342690 | ||
|
|
a5fecbc48a | ||
|
|
46cae07f2c | ||
|
|
6e2c30aba5 | ||
|
|
9c01b72292 | ||
|
|
5ee077e09f | ||
|
|
a5d7787fd0 | ||
|
|
c9852daf0e | ||
|
|
1f35a23bfa | ||
|
|
cf00325c2b | ||
|
|
714fb6a723 | ||
|
|
340841b37a | ||
|
|
421f718ace | ||
|
|
435b66ce3d | ||
|
|
7742e97bde | ||
|
|
416d7525fc | ||
|
|
14442c61a6 | ||
|
|
1aae6b0f0a | ||
|
|
8536dc7b92 | ||
|
|
b0b7690348 | ||
|
|
781bbe77d7 | ||
|
|
620020abcf | ||
|
|
2ed7bea7eb | ||
|
|
e7d569d4d2 | ||
|
|
ba7e22d045 | ||
|
|
77b4bf67f1 | ||
|
|
5bd5559601 | ||
|
|
97b35735c0 | ||
|
|
c30b47456f | ||
|
|
8268f0bdb9 | ||
|
|
05be1dcefb | ||
|
|
6a00931143 | ||
|
|
627e930d0b | ||
|
|
eac65af05c | ||
|
|
27e9cc0970 | ||
|
|
67e94834dd | ||
|
|
c5f743220b | ||
|
|
38201b0f3c | ||
|
|
5619ba0891 | ||
|
|
caf3922f22 | ||
|
|
3692c4a722 | ||
|
|
0a451cf01b | ||
|
|
297406e453 | ||
|
|
1258de3592 | ||
|
|
0a80b437b6 | ||
|
|
ffa1074298 | ||
|
|
40b5d33cba | ||
|
|
2fa03b71a2 | ||
|
|
ce5555674a | ||
|
|
ccbddf0e7c | ||
|
|
8dee4e942e | ||
|
|
2df8d86e83 | ||
|
|
a4e72a1f63 | ||
|
|
c8722cb44f | ||
|
|
c3c6bd6d33 | ||
|
|
ff115284d9 | ||
|
|
18a405808a | ||
|
|
6cec7f6dcd | ||
|
|
fcd4854cb8 | ||
|
|
215bd7adaa | ||
|
|
0933ad4367 | ||
|
|
1e3418c5f5 | ||
|
|
6c920c2e05 | ||
|
|
3e4a6ac93b | ||
|
|
e175bc8f65 | ||
|
|
2ff0b60d8c | ||
|
|
9fd61308d5 | ||
|
|
01e20acce7 | ||
|
|
1444404154 | ||
|
|
d29f6ffbea | ||
|
|
7fe7a87ff4 | ||
|
|
a0c891bae8 | ||
|
|
fc31ade49e | ||
|
|
3fa8c39f3d | ||
|
|
645421b566 | ||
|
|
be43e5bccd | ||
|
|
b1903aa205 | ||
|
|
66b429c981 | ||
|
|
8f7132ec86 | ||
|
|
5390efc2e7 | ||
|
|
1ee29e9e45 | ||
|
|
8a2d640922 | ||
|
|
b975e9f05b | ||
|
|
e8db1bbe80 | ||
|
|
a7bb02ffbd | ||
|
|
31579d09a3 | ||
|
|
49b4ee2b90 | ||
|
|
0775072ffa | ||
|
|
23e398e0df | ||
|
|
3870819e38 | ||
|
|
984953e2ce | ||
|
|
dad3b37946 | ||
|
|
55defdac8c | ||
|
|
e98eee56be | ||
|
|
63ac21bce2 | ||
|
|
098c94e253 | ||
|
|
08656ec09a | ||
|
|
6c2ac73df7 | ||
|
|
f9742b30d8 | ||
|
|
6f2358106c | ||
|
|
41a9a39cf3 | ||
|
|
05fec0b36e | ||
|
|
b7cc5c0666 | ||
|
|
7ecba52075 | ||
|
|
9b1449194e | ||
|
|
7d0ed267f7 | ||
|
|
ed76d28665 | ||
|
|
74ea7d5e40 | ||
|
|
ac6c8ec35b | ||
|
|
f0ed0d4919 | ||
|
|
1bd31d3dbf | ||
|
|
6dc7613936 | ||
|
|
fbc908585e | ||
|
|
c2fe9dd573 | ||
|
|
24b9b4179e | ||
|
|
4a1b76b2c4 | ||
|
|
d15ec892bc | ||
|
|
04f2022537 | ||
|
|
91ad10dd1a | ||
|
|
c7d76e39fa | ||
|
|
ff8ea8d72a | ||
|
|
99681be075 | ||
|
|
049f8edb86 | ||
|
|
2d05169a8e | ||
|
|
85aac1b97d | ||
|
|
01f1d7538f | ||
|
|
62ca936e91 | ||
|
|
c840407b94 | ||
|
|
a158149273 | ||
|
|
4e22377748 | ||
|
|
ba38434587 | ||
|
|
dfb298e4e3 | ||
|
|
fac0941308 | ||
|
|
600e5c6010 | ||
|
|
94d28295f9 | ||
|
|
43c51d9d4a | ||
|
|
ee1ed670a5 | ||
|
|
3b4d1c4627 | ||
|
|
d787234ac1 | ||
|
|
0d3751e83b | ||
|
|
c3a39d6f43 | ||
|
|
42ecee87e4 | ||
|
|
9fd2a39f43 | ||
|
|
273e584598 | ||
|
|
9634e87f72 | ||
|
|
8880cc09d5 | ||
|
|
f54670e670 | ||
|
|
2589849682 | ||
|
|
bc575ec5ae | ||
|
|
71f95ca3c6 | ||
|
|
8170e66f4f | ||
|
|
b02188a88c | ||
|
|
d440d51ffe | ||
|
|
e50788f685 | ||
|
|
1197aa3f37 | ||
|
|
4b55b838ed | ||
|
|
3a62c9eaa0 | ||
|
|
f61bb87c8e | ||
|
|
80968b02bb | ||
|
|
94cf690e8f | ||
|
|
4fc1ec7663 | ||
|
|
2764d9e2dd | ||
|
|
8ac8459054 | ||
|
|
75e95817a3 | ||
|
|
18b7e3ac9a | ||
|
|
4ec7bd4352 | ||
|
|
83796611a6 | ||
|
|
603a0c0bd9 | ||
|
|
1e92395aab | ||
|
|
97a9fa94a0 | ||
|
|
c918b8f6e4 | ||
|
|
168e226f0e | ||
|
|
f7f8c51661 | ||
|
|
ccb2cd2d6d | ||
|
|
14cc44f948 | ||
|
|
08b6f3b70a | ||
|
|
13f0fcb082 | ||
|
|
fbe8a4e2da | ||
|
|
228679952e | ||
|
|
79796ce4c2 | ||
|
|
1a148a53de | ||
|
|
b7c7c0f947 | ||
|
|
defdc82d74 | ||
|
|
3257ea2980 | ||
|
|
766cfa76b6 | ||
|
|
45141a5c5f | ||
|
|
15f79d1586 | ||
|
|
ebe11da343 | ||
|
|
df3b10b5c2 | ||
|
|
4b04137a04 | ||
|
|
eba9ff00ce | ||
|
|
5f0a9981f3 | ||
|
|
cb1769a352 | ||
|
|
f72fcc3031 | ||
|
|
e8b5d8d66f | ||
|
|
8acd6f906e | ||
|
|
5bc78f81ea | ||
|
|
2ad7cb0ea4 | ||
|
|
3af400ab28 | ||
|
|
3f49e80b9a |
@@ -14,6 +14,7 @@ RUN echo "export PROMPT_COMMAND='history -a'" >> /home/vscode/.bashrc \
|
||||
&& echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/vscode/.bashrc \
|
||||
&& chown vscode:vscode -R /home/vscode/
|
||||
|
||||
RUN npm install -g @go-task/cli
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
@@ -37,4 +38,3 @@ RUN apt-get update \
|
||||
libwebp-dev \
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1
|
||||
# && pip install -U --no-cache-dir pip
|
||||
|
||||
@@ -18,39 +18,35 @@
|
||||
"source=mealie-devcontainer-workspace,target=/workspaces/mealie/frontend/node_modules,type=volume",
|
||||
"source=mealie-bashhistory,target=/home/vscode/commandhistory,type=volume"
|
||||
],
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
},
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"matangover.mypy",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"ms-python.pylint",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"Vue.volar"
|
||||
]
|
||||
}
|
||||
},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"matangover.mypy",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"Vue.volar"
|
||||
],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
9000
|
||||
9000,
|
||||
24678 // used by nuxt when hot-reloading using polling
|
||||
],
|
||||
// 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 && make setup",
|
||||
"onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules && task setup",
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
// "features": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.git
|
||||
.github
|
||||
.dockerignore
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.idea
|
||||
.vscode
|
||||
@@ -12,6 +13,7 @@ __pycache__/
|
||||
htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.pylintrc
|
||||
.pytest_cache/
|
||||
.venv
|
||||
venv
|
||||
@@ -28,3 +30,5 @@ venv
|
||||
*/mealie/.temp
|
||||
|
||||
model.crfmodel
|
||||
|
||||
crowdin.yml
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Bug Report
|
||||
description: "Submit a bug for the latest version of Mealie"
|
||||
title: "[BUG] - YOUR TITLE"
|
||||
title: "[BUG] - YOUR DESCRIPTIVE TITLE GOES HERE"
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
@@ -12,9 +12,9 @@ body:
|
||||
Please confirm and check all the following prior to submission. If you do not do this, your
|
||||
issue may be closed.
|
||||
options:
|
||||
- label: This is not a feature request
|
||||
- label: This is not a feature request.
|
||||
required: true
|
||||
- label: I added a very descriptive title to this issue.
|
||||
- label: I added a very descriptive title to this issue (title field is above this).
|
||||
required: true
|
||||
- label: I used the GitHub search to find a similar issue and didn't find it.
|
||||
required: true
|
||||
@@ -22,7 +22,7 @@ body:
|
||||
required: true
|
||||
- label: I already read the docs and didn't find an answer.
|
||||
required: true
|
||||
- label: This issue can be replicated on the demo site (https://demo.mealie.io/)
|
||||
- label: This issue can be replicated on the demo site (https://demo.mealie.io/).
|
||||
required: false
|
||||
- type: textarea
|
||||
id: description
|
||||
@@ -41,13 +41,15 @@ body:
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Please provide relevent logs
|
||||
label: Please provide relevant logs
|
||||
placeholder: For example from `docker compose logs` or other system logs.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Mealie Version
|
||||
placeholder: Docker image, as well as 'Build' tag from https://yourmealieurl/admin/site-settings, if UI is working
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
@@ -59,7 +61,8 @@ body:
|
||||
- Docker (Windows)
|
||||
- Docker (Synology)
|
||||
- Unraid
|
||||
- Other
|
||||
- TrueNAS
|
||||
- Other (please specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://github.com/hay-kot/mealie/discussions/new?category=feature-request
|
||||
about: Please add any Feature Requests as a Github Discussion using the for in this issue.
|
||||
url: https://github.com/mealie-recipes/mealie/discussions/new?category=feature-request
|
||||
about: Please add any Feature Requests as a Github Discussion using the form in this issue.
|
||||
|
||||
@@ -17,7 +17,7 @@ body:
|
||||
- label: |
|
||||
I have verified that this issue _is not_ related to the underlying library
|
||||
[hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers) by **1)** checking
|
||||
the [debugger](https://demo.mealie.io/recipe/create/debug) and data is returned, **2)**
|
||||
the [debugger](https://demo.mealie.io/g/home/r/create/debug) and data is returned, **2)**
|
||||
verifying that there _are_ errors in the log related to application level code, or
|
||||
**3)** verified that the site provides recipe data, or is otherwise supported by
|
||||
[hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers)
|
||||
|
||||
23
.github/pull_request_template.md
vendored
@@ -2,6 +2,15 @@
|
||||
This template provides some ideas of things to include in your PR description.
|
||||
To start, try providing a short summary of your changes in the Title above.
|
||||
If a section of the PR template does not apply to this PR, then delete that section.
|
||||
|
||||
PLEASE READ:
|
||||
-------------------------
|
||||
Mealie is moving to a regular, automatic release schedule. This means that all PRs should be in a
|
||||
stable state, ready for release. This includes:
|
||||
|
||||
- Ensuring new tests have been added to cover new features, or to prevent regressions.
|
||||
- Work is fully complete and usable
|
||||
|
||||
-->
|
||||
|
||||
## What type of PR is this?
|
||||
@@ -56,17 +65,3 @@ _(fill-in or delete this section)_
|
||||
<!--
|
||||
Describe how you tested this change.
|
||||
-->
|
||||
|
||||
## Release Notes
|
||||
|
||||
_(REQUIRED)_
|
||||
<!--
|
||||
If this PR makes user facing changes, please describe them here. This
|
||||
description will be copied into the release notes/changelog, whenever the
|
||||
next version is released. Keep this section short, and focus on high level
|
||||
changes.
|
||||
Put your text between the block. To omit notes, use NONE within the block.
|
||||
-->
|
||||
|
||||
```release-note
|
||||
```
|
||||
|
||||
21
.github/stale.yml
vendored
@@ -1,21 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- early-stages
|
||||
- "bug: confirmed"
|
||||
- feedback
|
||||
- task
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
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.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
81
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "mealie-next" ]
|
||||
pull_request:
|
||||
branches: [ "mealie-next" ]
|
||||
schedule:
|
||||
- cron: '36 9 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners
|
||||
# Consider using larger runners for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript-typescript', 'python' ]
|
||||
# CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
|
||||
# Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
18
.github/workflows/partial-backend.yml
vendored
@@ -35,8 +35,14 @@ jobs:
|
||||
|
||||
# Steps
|
||||
steps:
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v4
|
||||
@@ -60,14 +66,14 @@ jobs:
|
||||
id: cache-validate
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
echo "import black;print('venv good?')" > test.py && poetry run python test.py && echo ::set-output name=cache-hit-success::true
|
||||
echo "import black;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 tesseract-ocr-all
|
||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
||||
poetry install
|
||||
poetry add "psycopg2-binary==2.8.6"
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' || steps.cache-validate.outputs.cache-hit-success != 'true'
|
||||
@@ -78,11 +84,11 @@ jobs:
|
||||
|
||||
- name: Lint (Ruff)
|
||||
run: |
|
||||
make backend-lint
|
||||
task py:lint
|
||||
|
||||
- name: Mypy Typecheck
|
||||
run: |
|
||||
make backend-typecheck
|
||||
task py:mypy
|
||||
|
||||
- name: Pytest
|
||||
env:
|
||||
@@ -101,4 +107,4 @@ jobs:
|
||||
LDAP_NAME_ATTRIBUTE: cn
|
||||
LDAP_MAIL_ATTRIBUTE: mail
|
||||
run: |
|
||||
make backend-test
|
||||
task py:test
|
||||
|
||||
31
.github/workflows/partial-builder.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
tags:
|
||||
required: false
|
||||
type: string
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: true
|
||||
@@ -17,30 +20,44 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
- name: Log in to the Container registry (ghcr.io)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry (dockerhub)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Override __init__.py
|
||||
run: |
|
||||
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: ./docker/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:${{ inputs.tag }}
|
||||
tags: |
|
||||
hkotel/mealie:${{ inputs.tag }}
|
||||
ghcr.io/${{ github.repository }}:${{ inputs.tag }}
|
||||
${{ inputs.tags }}
|
||||
build-args: |
|
||||
COMMIT=${{ github.sha }}
|
||||
# https://docs.docker.com/build/ci/github-actions/cache/#github-cache
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
16
.github/workflows/partial-frontend.yml
vendored
@@ -9,20 +9,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.3.2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -47,20 +47,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@master
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node env 🏗
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get yarn cache directory path 🛠
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.3.2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build Dockerfile
|
||||
run: |
|
||||
|
||||
22
.github/workflows/release.yml
vendored
@@ -14,6 +14,9 @@ jobs:
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
|
||||
build-release:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
name: Build Tagged Release
|
||||
uses: ./.github/workflows/partial-builder.yml
|
||||
needs:
|
||||
@@ -21,26 +24,13 @@ jobs:
|
||||
- frontend-tests
|
||||
with:
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
tags: |
|
||||
hkotel/mealie:latest
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
publish-docs:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-release
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CONFIG_FILE: docs/mkdocs.yml
|
||||
EXTRA_PACKAGES: build-base
|
||||
|
||||
notify-discord:
|
||||
name: Notify Discord
|
||||
needs:
|
||||
|
||||
41
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# Documentation: https://github.com/marketplace/actions/close-stale-issues
|
||||
name: 'Stale: Flag and close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
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-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
|
||||
# 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.
|
||||
exempt-all-milestones: true
|
||||
# How many API calls will we allow the action to make, essentially.
|
||||
# Doco: https://github.com/actions/stale?tab=readme-ov-file#operations-per-run
|
||||
operations-per-run: 150
|
||||
########################################################################
|
||||
# The below are just default values, but populating here for reference #
|
||||
########################################################################
|
||||
# Automatically remove the stale label when the issues or the pull requests are updated
|
||||
remove-stale-when-updated: true
|
||||
# The reason used when closing issues. Valid values are `completed` and `not_planned`.
|
||||
close-issue-reason: 'not_planned'
|
||||
# If true, PRs currently in draft will not be marked as stale automatically.
|
||||
# We can mark them stale (after `days-before-pr-stale`), though we don't auto-close.
|
||||
exempt-draft-pr: false
|
||||
1
.gitignore
vendored
@@ -162,3 +162,4 @@ lcov.info
|
||||
dev/code-generation/openapi.json
|
||||
|
||||
.run/
|
||||
.task/*
|
||||
|
||||
87
.pylintrc
@@ -60,17 +60,7 @@ confidence=
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
disable=raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
@@ -78,67 +68,10 @@ disable=print-statement,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape
|
||||
missing-function-docstring,
|
||||
missing-class-docstring,
|
||||
missing-module-docstring,
|
||||
too-few-public-methods
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
@@ -292,7 +225,9 @@ good-names=i,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
_,
|
||||
e,
|
||||
db
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
@@ -455,7 +390,7 @@ indent-after-paren=4
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
max-line-length=120
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
@@ -583,5 +518,5 @@ valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
||||
overgeneral-exceptions=builtins.BaseException,
|
||||
builtins.Exception
|
||||
|
||||
15
.vscode/settings.json
vendored
@@ -14,8 +14,8 @@
|
||||
"webp"
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.organizeImports": false
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.workingDirectories": [
|
||||
@@ -36,13 +36,6 @@
|
||||
"i18n-ally.localesPaths": "frontend/lang/messages",
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
|
||||
"python.formatting.provider": "black",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.flake8Enabled": false,
|
||||
"python.linting.pylintEnabled": false,
|
||||
"python.linting.pylintArgs": [
|
||||
"--rcfile=${workspaceFolder}/.pylintrc"
|
||||
],
|
||||
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
@@ -50,7 +43,6 @@
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"python.linting.mypyEnabled": true,
|
||||
"search.mode": "reuseEditor",
|
||||
"python.testing.unittestArgs": [
|
||||
"-v",
|
||||
@@ -69,4 +61,7 @@
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
},
|
||||
}
|
||||
|
||||
23
.vscode/tasks.json
vendored
@@ -1,22 +1,9 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "DEV: Build and Start Docker Compose",
|
||||
"command": "make docker-dev",
|
||||
"type": "shell",
|
||||
"args": [],
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": "test"
|
||||
},
|
||||
{
|
||||
"label": "Production: Build and Start Docker Compose",
|
||||
"command": "make docker-prod",
|
||||
"command": "task docker:prod",
|
||||
"type": "shell",
|
||||
"args": [],
|
||||
"problemMatcher": [
|
||||
@@ -29,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Dev: Start Backend",
|
||||
"command": "make backend",
|
||||
"command": "task py",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
@@ -49,7 +36,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Dev: Start Frontend",
|
||||
"command": "make frontend",
|
||||
"command": "task ui",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
@@ -59,7 +46,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Dev: Start Docs Server",
|
||||
"command": "make docs",
|
||||
"command": "task docs",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
@@ -69,7 +56,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Run python tests",
|
||||
"command": "make test",
|
||||
"command": "task py:test",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
|
||||
3
MAINTAINERS.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Maintainers Guide
|
||||
|
||||
See -> [/docs/docs/contributors/developers-guide/maintainers.md](/docs/docs/contributors/developers-guide/maintainers.md)
|
||||
43
README.md
@@ -1,15 +1,15 @@
|
||||
[![Latest Release][latest-release-shield]][latest-release-url]
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![AGPL License][license-shield]][license-url]
|
||||
[![Docker Pulls][docker-pull]][docker-pull]
|
||||
[](https://www.codefactor.io/repository/github/hay-kot/mealie)
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://github.com/hay-kot/mealie">
|
||||
<a href="https://github.com/mealie-recipes/mealie">
|
||||
<svg style="width:100px;height:100px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z" />
|
||||
</svg>
|
||||
@@ -21,15 +21,14 @@
|
||||
A Place for All Your Recipes
|
||||
<br />
|
||||
<a href="https://nightly.mealie.io"><strong>Explore the docs »</strong></a>
|
||||
<a href="https://github.com/hay-kot/mealie">
|
||||
<a href="https://github.com/mealie-recipes/mealie">
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://demo.mealie.io/">View Demo</a>
|
||||
·
|
||||
<a href="https://github.com/hay-kot/mealie/issues">Report Bug</a>
|
||||
<a href="https://github.com/mealie-recipes/mealie/issues">Report Bug</a>
|
||||
·
|
||||
<a href="https://hub.docker.com/r/hkotel/mealie"> Docker Hub
|
||||
</a>
|
||||
<a href="https://github.com/mealie-recipes/mealie/pkgs/container/mealie">GitHub Container Registry</a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -39,7 +38,7 @@
|
||||
|
||||
# About The Project
|
||||
|
||||
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
|
||||
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the URL and Mealie will automatically import the relevant data, or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
|
||||
|
||||
- [Remember to join the Discord](https://discord.gg/QuStdQGSGK)!
|
||||
- [Documentation](https://nightly.mealie.io)
|
||||
@@ -48,7 +47,7 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
<!-- CONTRIBUTING -->
|
||||
## Contributing
|
||||
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you're going to be working on the code-base, you'll want to use the nightly documentation to ensure you get the latest information.
|
||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. If you're going to be working on the code-base, you'll want to use the nightly documentation to ensure you get the latest information.
|
||||
|
||||
- See the [Contributors Guide](https://nightly.mealie.io/contributors/developers-guide/code-contributions/) for help getting started.
|
||||
- We use [VSCode Dev Containers](https://code.visualstudio.com/docs/remote/containers) to make it easy for contributors to get started!
|
||||
@@ -57,6 +56,12 @@ If you are not a coder, you can still contribute financially. Financial contribu
|
||||
|
||||
<a href="https://www.buymeacoffee.com/haykot" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" style="height: 30px !important;width: 107px !important;" ></a>
|
||||
|
||||
### Translations
|
||||
|
||||
Translations can be a great way for **non-coders** to contribute to project. We use [Crowdin](https://crowdin.com/project/mealie) to allow several contributors to work on translating Mealie. You can simply help by voting for your preferred translations, or even by completely translating Mealie into a new language.
|
||||
|
||||
For more information, check out the translation page on the [contributor's guide](https://nightly.mealie.io/contributors/translating/).
|
||||
|
||||
<!-- LICENSE -->
|
||||
## License
|
||||
Distributed under the AGPL License. See `LICENSE` for more information.
|
||||
@@ -77,17 +82,19 @@ Thanks to Linode for providing Hosting for the Demo, Beta, and Documentation sit
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/hay-kot/mealie.svg?style=flat-square
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/mealie-recipes/mealie.svg?style=flat-square
|
||||
[docker-pull]: https://img.shields.io/docker/pulls/hkotel/mealie
|
||||
[contributors-url]: https://github.com/hay-kot/mealie/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/hay-kot/mealie.svg?style=flat-square
|
||||
[forks-url]: https://github.com/hay-kot/mealie/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/hay-kot/mealie.svg?style=flat-square
|
||||
[stars-url]: https://github.com/hay-kot/mealie/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/hay-kot/mealie.svg?style=flat-square
|
||||
[issues-url]: https://github.com/hay-kot/mealie/issues
|
||||
[license-shield]: https://img.shields.io/github/license/hay-kot/mealie.svg?style=flat-square
|
||||
[license-url]: https://github.com/hay-kot/mealie/blob/mealie-next/LICENSE
|
||||
[contributors-url]: https://github.com/mealie-recipes/mealie/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/mealie-recipes/mealie.svg?style=flat-square
|
||||
[forks-url]: https://github.com/mealie-recipes/mealie/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/mealie-recipes/mealie.svg?style=flat-square
|
||||
[stars-url]: https://github.com/mealie-recipes/mealie/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/mealie-recipes/mealie.svg?style=flat-square
|
||||
[issues-url]: https://github.com/mealie-recipes/mealie/issues
|
||||
[latest-release-shield]: https://img.shields.io/github/v/release/mealie-recipes/mealie.svg?style=flat-square
|
||||
[latest-release-url]: https://img.shields.io/github/v/release/mealie-recipes/mealie
|
||||
[license-shield]: https://img.shields.io/github/license/mealie-recipes/mealie.svg?style=flat-square
|
||||
[license-url]: https://github.com/mealie-recipes/mealie/blob/mealie-next/LICENSE
|
||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
|
||||
[linkedin-url]: https://linkedin.com/in/hay-kot
|
||||
[product-screenshot]: docs/docs/assets/img/home_screenshot.png
|
||||
|
||||
183
Taskfile.yml
Normal file
@@ -0,0 +1,183 @@
|
||||
# https://taskfile.dev
|
||||
|
||||
version: "3"
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
env:
|
||||
DEFAULT_GROUP: Home
|
||||
PRODUCTION: false
|
||||
API_PORT: 9000
|
||||
API_DOCS: True
|
||||
TOKEN_TIME: 256 # hours
|
||||
# mailplit SMTP config
|
||||
# start dev:services to use mailpit
|
||||
SMTP_HOST: localhost
|
||||
SMTP_PORT: 1025
|
||||
SMTP_FROM_NAME: MealieDev
|
||||
SMTP_AUTH_STRATEGY: NONE
|
||||
LANG: en-US
|
||||
|
||||
# loads .env file if it exists
|
||||
dotenv:
|
||||
- .env
|
||||
- .dev.env
|
||||
tasks:
|
||||
docs:gen:
|
||||
desc: runs the API documentation generator
|
||||
cmds:
|
||||
- poetry run python dev/code-generation/gen_docs_api.py
|
||||
|
||||
docs:
|
||||
desc: runs the documentation server
|
||||
dir: docs
|
||||
deps:
|
||||
- docs:gen
|
||||
cmds:
|
||||
- poetry run python -m mkdocs serve
|
||||
|
||||
setup:ui:
|
||||
desc: setup frontend dependencies
|
||||
dir: frontend
|
||||
cmds:
|
||||
- yarn install
|
||||
|
||||
setup:py:
|
||||
desc: setup python dependencies
|
||||
cmds:
|
||||
- poetry install --with main,dev,postgres
|
||||
- poetry run pre-commit install
|
||||
|
||||
setup:model:
|
||||
desc: setup nlp model
|
||||
vars:
|
||||
MODEL_URL: https://github.com/mealie-recipes/nlp-model/releases/download/v1.0.0/model.crfmodel
|
||||
OUTPUT: ./mealie/services/parser_services/crfpp/model.crfmodel
|
||||
sources:
|
||||
# using pyproject.toml as the dependency since this should only ever need to run once
|
||||
# during setup. There is perhaps a better way to do this.
|
||||
- ./pyproject.toml
|
||||
generates:
|
||||
- ./mealie/services/parser_services/crfpp/model.crfmodel
|
||||
cmds:
|
||||
- curl -L0 {{ .MODEL_URL }} --output {{ .OUTPUT }}
|
||||
|
||||
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
|
||||
|
||||
dev:services:
|
||||
desc: starts postgres and mailpit containers
|
||||
dir: docker
|
||||
cmds:
|
||||
- docker compose -f docker-compose.dev.yml up
|
||||
|
||||
dev:clean:
|
||||
desc: cleans up dev environment !! removes all data files !!
|
||||
vars:
|
||||
DEV_DATA: ""
|
||||
cmds:
|
||||
- rm -r ./dev/data/recipes/
|
||||
- rm -r ./dev/data/users/
|
||||
- rm -f ./dev/data/mealie*.db
|
||||
- rm -f ./dev/data/mealie.log
|
||||
- rm -f ./dev/data/.secret
|
||||
|
||||
py:mypy:
|
||||
desc: runs python type checking
|
||||
cmds:
|
||||
- poetry run mypy mealie
|
||||
|
||||
py:test:
|
||||
desc: runs python tests (support args after '--')
|
||||
cmds:
|
||||
- poetry run pytest {{ .CLI_ARGS }}
|
||||
|
||||
py:format:
|
||||
desc: runs python code formatter
|
||||
cmds:
|
||||
- poetry run black mealie
|
||||
|
||||
py:lint:
|
||||
desc: runs python linter
|
||||
cmds:
|
||||
- poetry run ruff mealie
|
||||
|
||||
py:check:
|
||||
desc: runs all linters, type checkers, and formatters
|
||||
deps:
|
||||
- py:format
|
||||
- py:lint
|
||||
- py:mypy
|
||||
- py:test
|
||||
|
||||
py:coverage:
|
||||
desc: runs python coverage and generates html report
|
||||
cmds:
|
||||
- poetry run pytest
|
||||
- poetry run coverage report -m
|
||||
- poetry run coveragepy-lcov
|
||||
- poetry run coverage html
|
||||
- open htmlcov/index.html
|
||||
|
||||
py:
|
||||
desc: runs the backend server
|
||||
cmds:
|
||||
- poetry run python mealie/db/init_db.py
|
||||
- poetry run python mealie/app.py
|
||||
|
||||
py:postgres:
|
||||
desc: runs the backend server configured for containerized postgres
|
||||
env:
|
||||
DB_ENGINE: postgres
|
||||
POSTGRES_USER: mealie
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_SERVER: localhost
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: mealie
|
||||
cmds:
|
||||
- poetry run python mealie/db/init_db.py
|
||||
- poetry run python mealie/app.py
|
||||
|
||||
ui:build:
|
||||
desc: builds the frontend in frontend/dist
|
||||
dir: frontend
|
||||
cmds:
|
||||
- yarn build
|
||||
|
||||
ui:lint:
|
||||
desc: runs the frontend linter
|
||||
dir: frontend
|
||||
cmds:
|
||||
- yarn lint
|
||||
|
||||
ui:test:
|
||||
desc: runs the frontend tests
|
||||
dir: frontend
|
||||
cmds:
|
||||
- yarn test
|
||||
|
||||
ui:check:
|
||||
desc: runs all frontend checks
|
||||
deps:
|
||||
- ui:lint
|
||||
- ui:test
|
||||
|
||||
ui:
|
||||
desc: runs the frontend server
|
||||
dir: frontend
|
||||
cmds:
|
||||
- yarn run dev
|
||||
|
||||
docker:prod:
|
||||
desc: builds and runs the production docker image locally
|
||||
dir: docker
|
||||
cmds:
|
||||
- docker compose -f docker-compose.yml -p mealie up -d --build
|
||||
@@ -7,12 +7,11 @@ Create Date: 2023-02-14 20:45:41.102571
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from text_unidecode import unidecode
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
from mealie.db.models._model_utils import GUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
@@ -52,30 +51,46 @@ def do_data_migration():
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
recipes = session.execute(select(RecipeModel)).scalars().all()
|
||||
ingredients = session.execute(select(RecipeIngredient)).scalars().all()
|
||||
for recipe in recipes:
|
||||
if recipe.name is not None:
|
||||
recipe.name_normalized = unidecode(recipe.name).lower().strip()
|
||||
session.execute(
|
||||
sa.text(
|
||||
f"UPDATE {RecipeModel.__tablename__} SET name_normalized=:name_normalized WHERE id=:id"
|
||||
).bindparams(name_normalized=unidecode(recipe.name).lower().strip(), id=recipe.id)
|
||||
)
|
||||
|
||||
if recipe.description is not None:
|
||||
recipe.description_normalized = unidecode(recipe.description).lower().strip()
|
||||
session.add(recipe)
|
||||
session.execute(
|
||||
sa.text(
|
||||
f"UPDATE {RecipeModel.__tablename__} SET description_normalized=:description_normalized WHERE id=:id"
|
||||
).bindparams(description_normalized=unidecode(recipe.description).lower().strip(), id=recipe.id)
|
||||
)
|
||||
|
||||
ingredients = session.execute(select(RecipeIngredient)).scalars().all()
|
||||
for ingredient in ingredients:
|
||||
if ingredient.note is not None:
|
||||
ingredient.note_normalized = unidecode(ingredient.note).lower().strip()
|
||||
session.execute(
|
||||
sa.text(
|
||||
f"UPDATE {RecipeIngredient.__tablename__} SET note_normalized=:note_normalized WHERE id=:id"
|
||||
).bindparams(note_normalized=unidecode(ingredient.note).lower().strip(), id=ingredient.id)
|
||||
)
|
||||
|
||||
if ingredient.original_text is not None:
|
||||
ingredient.original_text_normalized = unidecode(ingredient.original_text).lower().strip()
|
||||
session.add(ingredient)
|
||||
session.execute(
|
||||
sa.text(
|
||||
f"UPDATE {RecipeIngredient.__tablename__} SET original_text_normalized=:original_text_normalized WHERE id=:id"
|
||||
).bindparams(
|
||||
original_text_normalized=unidecode(ingredient.original_text).lower().strip(), id=ingredient.id
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# Set column to nullable first, since we do not have values here yet
|
||||
op.add_column("recipes", sa.Column("name_normalized", sa.String(), nullable=True))
|
||||
# Set column default first, since we do not have values here yet
|
||||
op.add_column("recipes", sa.Column("name_normalized", sa.String(), nullable=False, server_default=""))
|
||||
op.add_column("recipes", sa.Column("description_normalized", sa.String(), nullable=True))
|
||||
op.drop_index("ix_recipes_description", table_name="recipes")
|
||||
op.drop_index("ix_recipes_name", table_name="recipes")
|
||||
@@ -95,9 +110,9 @@ def upgrade():
|
||||
unique=False,
|
||||
)
|
||||
do_data_migration()
|
||||
# Make recipes.name_normalized not nullable now that column should be filled for all rows
|
||||
# Remove server default now that column should be filled for all rows
|
||||
with op.batch_alter_table("recipes", schema=None) as batch_op:
|
||||
batch_op.alter_column("name_normalized", nullable=False, existing_type=sa.String())
|
||||
batch_op.alter_column("name_normalized", existing_type=sa.String(), server_default=None)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ depends_on = None
|
||||
|
||||
def populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table: sa.Table, session: Session):
|
||||
shopping_lists = session.query(ShoppingList).all()
|
||||
labels = session.query(MultiPurposeLabel).all()
|
||||
|
||||
shopping_lists_labels_data: list[dict] = []
|
||||
for shopping_list in shopping_lists:
|
||||
labels = session.query(MultiPurposeLabel).filter(MultiPurposeLabel.group_id == ShoppingList.group_id).all()
|
||||
for i, label in enumerate(labels):
|
||||
shopping_lists_labels_data.append(
|
||||
{"id": uuid4(), "shopping_list_id": shopping_list.id, "label_id": label.id, "position": i}
|
||||
|
||||
@@ -24,17 +24,22 @@ def populate_group_slugs(session: Session):
|
||||
seen_slugs: set[str] = set()
|
||||
for group in groups:
|
||||
original_name = group.name
|
||||
new_name = original_name
|
||||
attempts = 0
|
||||
while True:
|
||||
slug = slugify(group.name)
|
||||
slug = slugify(new_name)
|
||||
if slug not in seen_slugs:
|
||||
break
|
||||
|
||||
attempts += 1
|
||||
group.name = f"{original_name} ({attempts})"
|
||||
new_name = f"{original_name} ({attempts})"
|
||||
|
||||
seen_slugs.add(slug)
|
||||
group.slug = slug
|
||||
session.execute(
|
||||
sa.text(f"UPDATE {Group.__tablename__} SET name=:name, slug=:slug WHERE id=:id").bindparams(
|
||||
name=new_name, slug=slug, id=group.id
|
||||
)
|
||||
)
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
@@ -22,22 +22,40 @@ def populate_normalized_fields():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
units = session.execute(select(IngredientUnitModel)).scalars().all()
|
||||
units = (
|
||||
session.execute(
|
||||
select(IngredientUnitModel).options(
|
||||
orm.load_only(IngredientUnitModel.name, IngredientUnitModel.abbreviation)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
for unit in units:
|
||||
if unit.name is not None:
|
||||
unit.name_normalized = IngredientUnitModel.normalize(unit.name)
|
||||
session.execute(
|
||||
sa.text(
|
||||
f"UPDATE {IngredientUnitModel.__tablename__} SET name_normalized=:name_normalized WHERE id=:id"
|
||||
).bindparams(name_normalized=IngredientUnitModel.normalize(unit.name), id=unit.id)
|
||||
)
|
||||
|
||||
if unit.abbreviation is not None:
|
||||
unit.abbreviation_normalized = IngredientUnitModel.normalize(unit.abbreviation)
|
||||
session.execute(
|
||||
sa.text(
|
||||
f"UPDATE {IngredientUnitModel.__tablename__} SET abbreviation_normalized=:abbreviation_normalized WHERE id=:id"
|
||||
).bindparams(abbreviation_normalized=IngredientUnitModel.normalize(unit.abbreviation), id=unit.id)
|
||||
)
|
||||
|
||||
session.add(unit)
|
||||
|
||||
foods = session.execute(select(IngredientFoodModel)).scalars().all()
|
||||
foods = (
|
||||
session.execute(select(IngredientFoodModel).options(orm.load_only(IngredientFoodModel.name))).scalars().all()
|
||||
)
|
||||
for food in foods:
|
||||
if food.name is not None:
|
||||
food.name_normalized = IngredientFoodModel.normalize(food.name)
|
||||
|
||||
session.add(food)
|
||||
session.execute(
|
||||
sa.text(
|
||||
f"UPDATE {IngredientFoodModel.__tablename__} SET name_normalized=:name_normalized WHERE id=:id"
|
||||
).bindparams(name_normalized=IngredientFoodModel.normalize(food.name), id=food.id)
|
||||
)
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
@@ -5,15 +5,19 @@ Revises: 0341b154f79a
|
||||
Create Date: 2023-10-04 14:29:26.688065
|
||||
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import UUID4
|
||||
from sqlalchemy.orm import Session, load_only
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models.group.shopping_list import ShoppingListItem
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel, RecipeIngredientModel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "dded3119c1fe"
|
||||
@@ -37,6 +41,91 @@ def _is_postgres():
|
||||
return op.get_context().dialect.name == "postgresql"
|
||||
|
||||
|
||||
def _get_duplicates(session: Session, model: SqlAlchemyBase) -> defaultdict[str, list]:
|
||||
duplicate_map: defaultdict[str, list] = defaultdict(list)
|
||||
|
||||
query = session.execute(sa.text(f"SELECT id, group_id, name FROM {model.__tablename__}"))
|
||||
for row in query.all():
|
||||
id, group_id, name = row
|
||||
key = f"{group_id}$${name}"
|
||||
duplicate_map[key].append(id)
|
||||
|
||||
return duplicate_map
|
||||
|
||||
|
||||
def _resolve_duplicate_food(
|
||||
session: Session,
|
||||
keep_food_id: UUID4,
|
||||
dupe_food_id: UUID4,
|
||||
):
|
||||
for shopping_list_item in session.query(ShoppingListItem).filter_by(food_id=dupe_food_id).all():
|
||||
shopping_list_item.food_id = keep_food_id
|
||||
|
||||
for recipe_ingredient in (
|
||||
session.query(RecipeIngredientModel)
|
||||
.options(load_only(RecipeIngredientModel.id, RecipeIngredientModel.food_id))
|
||||
.filter_by(food_id=dupe_food_id)
|
||||
.all()
|
||||
):
|
||||
recipe_ingredient.food_id = keep_food_id
|
||||
|
||||
session.commit()
|
||||
session.execute(
|
||||
sa.text(f"DELETE FROM {IngredientFoodModel.__tablename__} WHERE id=:id").bindparams(id=dupe_food_id)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
||||
def _resolve_duplicate_unit(
|
||||
session: Session,
|
||||
keep_unit_id: UUID4,
|
||||
dupe_unit_id: UUID4,
|
||||
):
|
||||
for shopping_list_item in session.query(ShoppingListItem).filter_by(unit_id=dupe_unit_id).all():
|
||||
shopping_list_item.unit_id = keep_unit_id
|
||||
|
||||
for recipe_ingredient in session.query(RecipeIngredientModel).filter_by(unit_id=dupe_unit_id).all():
|
||||
recipe_ingredient.unit_id = keep_unit_id
|
||||
|
||||
session.commit()
|
||||
session.execute(
|
||||
sa.text(f"DELETE FROM {IngredientUnitModel.__tablename__} WHERE id=:id").bindparams(id=dupe_unit_id)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
||||
def _resolve_duplicate_label(
|
||||
session: Session,
|
||||
keep_label_id: UUID4,
|
||||
dupe_label_id: UUID4,
|
||||
):
|
||||
for shopping_list_item in session.query(ShoppingListItem).filter_by(label_id=dupe_label_id).all():
|
||||
shopping_list_item.label_id = keep_label_id
|
||||
|
||||
for ingredient_food in session.query(IngredientFoodModel).filter_by(label_id=dupe_label_id).all():
|
||||
ingredient_food.label_id = keep_label_id
|
||||
|
||||
session.commit()
|
||||
session.execute(sa.text(f"DELETE FROM {MultiPurposeLabel.__tablename__} WHERE id=:id").bindparams(id=dupe_label_id))
|
||||
session.commit()
|
||||
|
||||
|
||||
def _resolve_duplicate_foods_units_labels(session: Session):
|
||||
for model, resolve_func in [
|
||||
(IngredientFoodModel, _resolve_duplicate_food),
|
||||
(IngredientUnitModel, _resolve_duplicate_unit),
|
||||
(MultiPurposeLabel, _resolve_duplicate_label),
|
||||
]:
|
||||
duplicate_map = _get_duplicates(session, model)
|
||||
for ids in duplicate_map.values():
|
||||
if len(ids) < 2:
|
||||
continue
|
||||
|
||||
keep_id = ids[0]
|
||||
for dupe_id in ids[1:]:
|
||||
resolve_func(session, keep_id, dupe_id)
|
||||
|
||||
|
||||
def _remove_duplicates_from_m2m_table(session: Session, table_meta: TableMeta):
|
||||
if _is_postgres():
|
||||
default_pk = "CTID"
|
||||
@@ -60,18 +149,19 @@ def _remove_duplicates_from_m2m_table(session: Session, table_meta: TableMeta):
|
||||
session.commit()
|
||||
|
||||
|
||||
def _remove_duplicates_from_m2m_tables(table_metas: list[TableMeta]):
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
|
||||
def _remove_duplicates_from_m2m_tables(session: Session, table_metas: list[TableMeta]):
|
||||
for table_meta in table_metas:
|
||||
_remove_duplicates_from_m2m_table(session, table_meta)
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
|
||||
_resolve_duplicate_foods_units_labels(session)
|
||||
_remove_duplicates_from_m2m_tables(
|
||||
session,
|
||||
[
|
||||
# M2M
|
||||
TableMeta("cookbooks_to_categories", "cookbook_id", "category_id"),
|
||||
TableMeta("cookbooks_to_tags", "cookbook_id", "tag_id"),
|
||||
TableMeta("cookbooks_to_tools", "cookbook_id", "tool_id"),
|
||||
@@ -83,16 +173,13 @@ def upgrade():
|
||||
TableMeta("recipes_to_tools", "recipe_id", "tool_id"),
|
||||
TableMeta("users_to_favorites", "user_id", "recipe_id"),
|
||||
TableMeta("shopping_lists_multi_purpose_labels", "shopping_list_id", "label_id"),
|
||||
# Foods/Units/Labels
|
||||
TableMeta("ingredient_foods", "name", "group_id"),
|
||||
TableMeta("ingredient_units", "name", "group_id"),
|
||||
TableMeta("multi_purpose_labels", "name", "group_id"),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
session.commit()
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# we use batch_alter_table here because otherwise this fails on sqlite
|
||||
|
||||
# M2M
|
||||
with op.batch_alter_table("cookbooks_to_categories") as batch_op:
|
||||
batch_op.create_unique_constraint("cookbook_id_category_id_key", ["cookbook_id", "category_id"])
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""added plural names and alias tables for foods and units
|
||||
|
||||
Revision ID: ba1e4a6cfe99
|
||||
Revises: dded3119c1fe
|
||||
Create Date: 2023-10-19 19:22:55.369319
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "ba1e4a6cfe99"
|
||||
down_revision = "dded3119c1fe"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"ingredient_units_aliases",
|
||||
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("unit_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("name_normalized", sa.String(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["unit_id"],
|
||||
["ingredient_units.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", "unit_id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_units_aliases_created_at"), "ingredient_units_aliases", ["created_at"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_units_aliases_name_normalized"),
|
||||
"ingredient_units_aliases",
|
||||
["name_normalized"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"ingredient_foods_aliases",
|
||||
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("food_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("name_normalized", sa.String(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["food_id"],
|
||||
["ingredient_foods.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", "food_id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_foods_aliases_created_at"), "ingredient_foods_aliases", ["created_at"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_foods_aliases_name_normalized"),
|
||||
"ingredient_foods_aliases",
|
||||
["name_normalized"],
|
||||
unique=False,
|
||||
)
|
||||
op.add_column("ingredient_foods", sa.Column("plural_name", sa.String(), nullable=True))
|
||||
op.add_column("ingredient_foods", sa.Column("plural_name_normalized", sa.String(), nullable=True))
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_foods_plural_name_normalized"), "ingredient_foods", ["plural_name_normalized"], unique=False
|
||||
)
|
||||
op.add_column("ingredient_units", sa.Column("plural_name", sa.String(), nullable=True))
|
||||
op.add_column("ingredient_units", sa.Column("plural_name_normalized", sa.String(), nullable=True))
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_units_plural_name_normalized"), "ingredient_units", ["plural_name_normalized"], unique=False
|
||||
)
|
||||
op.add_column("ingredient_units", sa.Column("plural_abbreviation", sa.String(), nullable=True))
|
||||
op.add_column("ingredient_units", sa.Column("plural_abbreviation_normalized", sa.String(), nullable=True))
|
||||
op.create_index(
|
||||
op.f("ix_ingredient_units_plural_abbreviation_normalized"),
|
||||
"ingredient_units",
|
||||
["plural_abbreviation_normalized"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_ingredient_units_plural_abbreviation_normalized"), table_name="ingredient_units")
|
||||
op.drop_column("ingredient_units", "plural_abbreviation_normalized")
|
||||
op.drop_column("ingredient_units", "plural_abbreviation")
|
||||
op.drop_index(op.f("ix_ingredient_units_plural_name_normalized"), table_name="ingredient_units")
|
||||
op.drop_column("ingredient_units", "plural_name_normalized")
|
||||
op.drop_column("ingredient_units", "plural_name")
|
||||
op.drop_index(op.f("ix_ingredient_foods_plural_name_normalized"), table_name="ingredient_foods")
|
||||
op.drop_column("ingredient_foods", "plural_name_normalized")
|
||||
op.drop_column("ingredient_foods", "plural_name")
|
||||
op.drop_index(op.f("ix_ingredient_foods_aliases_name_normalized"), table_name="ingredient_foods_aliases")
|
||||
op.drop_index(op.f("ix_ingredient_foods_aliases_created_at"), table_name="ingredient_foods_aliases")
|
||||
op.drop_table("ingredient_foods_aliases")
|
||||
op.drop_index(op.f("ix_ingredient_units_aliases_name_normalized"), table_name="ingredient_units_aliases")
|
||||
op.drop_index(op.f("ix_ingredient_units_aliases_created_at"), table_name="ingredient_units_aliases")
|
||||
op.drop_table("ingredient_units_aliases")
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,4 +1,5 @@
|
||||
import pathlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import dotenv
|
||||
@@ -15,38 +16,44 @@ BASE = pathlib.Path(__file__).parent.parent.parent
|
||||
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY")
|
||||
|
||||
|
||||
NAMES = {
|
||||
"en-US": "American English",
|
||||
"en-GB": "British English",
|
||||
"af-ZA": "Afrikaans (Afrikaans)",
|
||||
"ar-SA": "العربية (Arabic)",
|
||||
"ca-ES": "Català (Catalan)",
|
||||
"cs-CZ": "Čeština (Czech)",
|
||||
"da-DK": "Dansk (Danish)",
|
||||
"de-DE": "Deutsch (German)",
|
||||
"el-GR": "Ελληνικά (Greek)",
|
||||
"es-ES": "Español (Spanish)",
|
||||
"fi-FI": "Suomi (Finnish)",
|
||||
"fr-FR": "Français (French)",
|
||||
"he-IL": "עברית (Hebrew)",
|
||||
"hu-HU": "Magyar (Hungarian)",
|
||||
"it-IT": "Italiano (Italian)",
|
||||
"ja-JP": "日本語 (Japanese)",
|
||||
"ko-KR": "한국어 (Korean)",
|
||||
"no-NO": "Norsk (Norwegian)",
|
||||
"nl-NL": "Nederlands (Dutch)",
|
||||
"pl-PL": "Polski (Polish)",
|
||||
"pt-BR": "Português do Brasil (Brazilian Portuguese)",
|
||||
"pt-PT": "Português (Portuguese)",
|
||||
"ro-RO": "Română (Romanian)",
|
||||
"ru-RU": "Pусский (Russian)",
|
||||
"sr-SP": "српски (Serbian)",
|
||||
"sv-SE": "Svenska (Swedish)",
|
||||
"tr-TR": "Türkçe (Turkish)",
|
||||
"uk-UA": "Українська (Ukrainian)",
|
||||
"vi-VN": "Tiếng Việt (Vietnamese)",
|
||||
"zh-CN": "简体中文 (Chinese simplified)",
|
||||
"zh-TW": "繁體中文 (Chinese traditional)",
|
||||
@dataclass
|
||||
class LocaleData:
|
||||
name: str
|
||||
dir: str = "ltr"
|
||||
|
||||
|
||||
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"),
|
||||
"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)"),
|
||||
"es-ES": LocaleData(name="Español (Spanish)"),
|
||||
"fi-FI": LocaleData(name="Suomi (Finnish)"),
|
||||
"fr-FR": LocaleData(name="Français (French)"),
|
||||
"he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"),
|
||||
"hu-HU": LocaleData(name="Magyar (Hungarian)"),
|
||||
"it-IT": LocaleData(name="Italiano (Italian)"),
|
||||
"ja-JP": LocaleData(name="日本語 (Japanese)"),
|
||||
"ko-KR": LocaleData(name="한국어 (Korean)"),
|
||||
"no-NO": LocaleData(name="Norsk (Norwegian)"),
|
||||
"nl-NL": LocaleData(name="Nederlands (Dutch)"),
|
||||
"pl-PL": LocaleData(name="Polski (Polish)"),
|
||||
"pt-BR": LocaleData(name="Português do Brasil (Brazilian Portuguese)"),
|
||||
"pt-PT": LocaleData(name="Português (Portuguese)"),
|
||||
"ro-RO": LocaleData(name="Română (Romanian)"),
|
||||
"ru-RU": LocaleData(name="Pусский (Russian)"),
|
||||
"sr-SP": LocaleData(name="српски (Serbian)"),
|
||||
"sv-SE": LocaleData(name="Svenska (Swedish)"),
|
||||
"tr-TR": LocaleData(name="Türkçe (Turkish)"),
|
||||
"uk-UA": LocaleData(name="Українська (Ukrainian)"),
|
||||
"vi-VN": LocaleData(name="Tiếng Việt (Vietnamese)"),
|
||||
"zh-CN": LocaleData(name="简体中文 (Chinese simplified)"),
|
||||
"zh-TW": LocaleData(name="繁體中文 (Chinese traditional)"),
|
||||
}
|
||||
|
||||
LOCALE_TEMPLATE = """// This Code is auto generated by gen_global_components.py
|
||||
@@ -55,6 +62,7 @@ export const LOCALES = [{% for locale in locales %}
|
||||
name: "{{ locale.name }}",
|
||||
value: "{{ locale.locale }}",
|
||||
progress: {{ locale.progress }},
|
||||
dir: "{{ locale.dir }}",
|
||||
},{% endfor %}
|
||||
]
|
||||
|
||||
@@ -65,12 +73,14 @@ class TargetLanguage(MealieModel):
|
||||
id: str
|
||||
name: str
|
||||
locale: str
|
||||
dir: str = "ltr"
|
||||
threeLettersCode: str
|
||||
twoLettersCode: str
|
||||
progress: float = 0.0
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class CrowdinApi:
|
||||
@@ -103,15 +113,23 @@ class CrowdinApi:
|
||||
models.insert(
|
||||
0,
|
||||
TargetLanguage(
|
||||
id="en-US", name="English", locale="en-US", threeLettersCode="en", twoLettersCode="en", progress=100
|
||||
id="en-US",
|
||||
name="English",
|
||||
locale="en-US",
|
||||
dir="ltr",
|
||||
threeLettersCode="en",
|
||||
twoLettersCode="en",
|
||||
progress=100,
|
||||
),
|
||||
)
|
||||
|
||||
progress: list[dict] = self.get_progress()["data"]
|
||||
|
||||
for model in models:
|
||||
if model.locale in NAMES:
|
||||
model.name = NAMES[model.locale]
|
||||
if model.locale in LOCALE_DATA:
|
||||
locale_data = LOCALE_DATA[model.locale]
|
||||
model.name = locale_data.name
|
||||
model.dir = locale_data.dir
|
||||
|
||||
for p in progress:
|
||||
if p["data"]["languageId"] == model.id:
|
||||
|
||||
74
dev/code-generation/utils/anonymize_backups.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
logger = logging.getLogger("anonymize_backups")
|
||||
|
||||
|
||||
def is_uuid4(value: str):
|
||||
try:
|
||||
UUID(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_iso_datetime(value: str):
|
||||
try:
|
||||
datetime.fromisoformat(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def random_string(length=10):
|
||||
return "".join(random.choice(string.ascii_lowercase) for _ in range(length))
|
||||
|
||||
|
||||
def clean_value(value):
|
||||
try:
|
||||
match value:
|
||||
# preserve non-strings
|
||||
case int(value) | float(value):
|
||||
return value
|
||||
case None:
|
||||
return value
|
||||
# preserve UUIDs and datetimes
|
||||
case str(value) if is_uuid4(value) or is_iso_datetime(value):
|
||||
return value
|
||||
# randomize strings
|
||||
case str(value):
|
||||
return random_string()
|
||||
case _:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
logger.error(f"Failed to anonymize value: {value}")
|
||||
return value
|
||||
|
||||
|
||||
def walk_data_and_anonymize(data):
|
||||
for k, v in data.items():
|
||||
if isinstance(v, list):
|
||||
for item in v:
|
||||
walk_data_and_anonymize(item)
|
||||
else:
|
||||
# preserve alembic version number and enums
|
||||
if k in ["auth_method", "version_num"]:
|
||||
continue
|
||||
|
||||
data[k] = clean_value(v)
|
||||
|
||||
|
||||
def anonymize_database_json(input_filepath: str, output_filepath: str):
|
||||
with open(input_filepath) as f:
|
||||
data = json.load(f)
|
||||
|
||||
walk_data_and_anonymize(data)
|
||||
with open(output_filepath, "w") as f:
|
||||
json.dump(data, f)
|
||||
@@ -50,7 +50,6 @@ RUN apt-get update \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libwebp-dev \
|
||||
tesseract-ocr-all \
|
||||
# LDAP Dependencies
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1 \
|
||||
@@ -89,8 +88,8 @@ RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
gosu \
|
||||
iproute2 \
|
||||
tesseract-ocr-all \
|
||||
libldap-common \
|
||||
libldap-2.5 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# copying poetry and venv into image
|
||||
@@ -139,4 +138,4 @@ EXPOSE ${APP_PORT}
|
||||
COPY ./docker/entry.sh $MEALIE_HOME/run.sh
|
||||
|
||||
RUN chmod +x $MEALIE_HOME/run.sh
|
||||
ENTRYPOINT $MEALIE_HOME/run.sh
|
||||
ENTRYPOINT ["/app/run.sh"]
|
||||
|
||||
21
docker/docker-compose.dev.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
mailpit:
|
||||
image: axllent/mailpit:latest
|
||||
container_name: mealie_dev_mailpit
|
||||
restart: no
|
||||
environment:
|
||||
- "MP_SMTP_AUTH_ACCEPT_ANY=true"
|
||||
- "MP_SMTP_AUTH_ALLOW_INSECURE=true"
|
||||
ports:
|
||||
- "8025:8025"
|
||||
- "1025:1025"
|
||||
postgres:
|
||||
container_name: mealie_dev_postgres
|
||||
image: postgres:15
|
||||
restart: no
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_USER: mealie
|
||||
@@ -1,13 +1,10 @@
|
||||
# Start Backend API
|
||||
#!/bin/bash
|
||||
|
||||
# Strict Mode
|
||||
# set -e
|
||||
# IFS=$'\n\t'
|
||||
# Start Backend API
|
||||
|
||||
# Get PUID/PGID
|
||||
PUID=${PUID:-911}
|
||||
PGID=${PGID:-911}
|
||||
BASH_SOURCE=${BASH_SOURCE:-$0}
|
||||
|
||||
add_user() {
|
||||
groupmod -o -g "$PGID" abc
|
||||
@@ -15,18 +12,18 @@ add_user() {
|
||||
}
|
||||
|
||||
change_user() {
|
||||
# If container is started as root then create a new user and switch to it
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
if [ "$(id -u)" = $PUID ]; then
|
||||
echo "
|
||||
User uid: $PUID
|
||||
User gid: $PGID
|
||||
"
|
||||
elif [ "$(id -u)" = "0" ]; then
|
||||
# If container is started as root then create a new user and switch to it
|
||||
add_user
|
||||
chown -R $PUID:$PGID /app
|
||||
|
||||
echo "Switching to dedicated user"
|
||||
exec gosu $PUID "$BASH_SOURCE" "$@"
|
||||
elif [ "$(id -u)" = $PUID ]; then
|
||||
echo "
|
||||
User uid: $PUID
|
||||
User gid: $PGID
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -41,7 +38,7 @@ init() {
|
||||
poetry run python /app/mealie/db/init_db.py
|
||||
}
|
||||
|
||||
# change_user
|
||||
change_user
|
||||
init
|
||||
GUNICORN_PORT=${API_PORT:-9000}
|
||||
|
||||
@@ -49,7 +46,7 @@ GUNICORN_PORT=${API_PORT:-9000}
|
||||
hostip=`/sbin/ip route|awk '/default/ { print $3 }'`
|
||||
if [ "$WEB_GUNICORN" = 'true' ]; then
|
||||
echo "Starting Gunicorn"
|
||||
gunicorn mealie.app:app -b 0.0.0.0:$GUNICORN_PORT --forwarded-allow-ips=$hostip -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload
|
||||
exec gunicorn mealie.app:app -b 0.0.0.0:$GUNICORN_PORT --forwarded-allow-ips=$hostip -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload
|
||||
else
|
||||
uvicorn mealie.app:app --host 0.0.0.0 --forwarded-allow-ips=$hostip --port $GUNICORN_PORT
|
||||
exec uvicorn mealie.app:app --host 0.0.0.0 --forwarded-allow-ips=$hostip --port $GUNICORN_PORT
|
||||
fi
|
||||
|
||||
BIN
docs/docs/assets/img/ios/api.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/docs/assets/img/ios/gemini.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/docs/assets/img/ios/setup.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
docs/docs/assets/img/ios/url.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/docs/assets/img/pre-v1-backup-location.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 64 KiB |
@@ -55,3 +55,20 @@ th {
|
||||
.md-button {
|
||||
padding: 0.2rem 0.75rem !important;
|
||||
}
|
||||
|
||||
.announce-left {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.announce-left > a {
|
||||
color: #e0e0e0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.announce-left > a:hover {
|
||||
color: var(--md-primary-fg-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ A quality update with major props to [zackbcom](https://github.com/zackbcom) for
|
||||
|
||||
### Recipes
|
||||
- Added user feedback on bad URL
|
||||
- Better backend data validation for updating recipes, avoid small syntax errors corrupting database entry. [Closes #8](https://github.com/hay-kot/mealie/issues/8)
|
||||
- Better backend data validation for updating recipes, avoid small syntax errors corrupting database entry. [Closes #8](https://github.com/mealie-recipes/mealie/issues/8)
|
||||
- Fixed spacing Closes while editing new recipes in JSON
|
||||
|
||||
## v0.0.0 - Initial Pre-release
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
**Database Version: v0.4.0**
|
||||
|
||||
!!! error "Breaking Changes"
|
||||
1. With a recent refactor some users been experiencing issues with an environmental variable not being set correct. If you are experiencing issues, please provide your comments [Here](https://github.com/hay-kot/mealie/issues/281).
|
||||
1. With a recent refactor some users been experiencing issues with an environmental variable not being set correct. If you are experiencing issues, please provide your comments [Here](https://github.com/mealie-recipes/mealie/issues/281).
|
||||
|
||||
2. If you are a developer, you may experience issues with development as a new environmental variable has been introduced. Setting `PRODUCTION=false` will allow you to develop as normal.
|
||||
|
||||
@@ -31,4 +31,4 @@
|
||||
- Unify Logger across the backend
|
||||
- mealie.log and last_recipe.json are now downloadable from the frontend from the /admin/about
|
||||
- New download schema where you request a token and then use that token to hit a single endpoint to download a file. This is a notable change if you are using the API to download backups.
|
||||
- Recipe images can now be added directly from a URL - [See #117 for details](https://github.com/hay-kot/mealie/issues/117)
|
||||
- Recipe images can now be added directly from a URL - [See #117 for details](https://github.com/mealie-recipes/mealie/issues/117)
|
||||
@@ -8,7 +8,7 @@
|
||||
- Fixed #617 - Section behavior when adding a step
|
||||
- Fixed #615 - Recipe Settings are not available when creating new recipe
|
||||
- Fixed #625 - API of today's image returns strange characters
|
||||
- Fixed [#590](https://github.com/hay-kot/mealie/issues/590) - Duplicate Events when using Gunicorn Workers
|
||||
- Fixed [#590](https://github.com/mealie-recipes/mealie/issues/590) - Duplicate Events when using Gunicorn Workers
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
- FastAPI to 0.78.0
|
||||
|
||||
- Recipe Ingredient Editor
|
||||
- [#1140](https://github.com/hay-kot/mealie/issues/1140) - Error in processing the quantity of ingredients #1140 - UI Now prevents entering not-allowed characters in quantity field
|
||||
- [#1140](https://github.com/mealie-recipes/mealie/issues/1140) - Error in processing the quantity of ingredients #1140 - UI Now prevents entering not-allowed characters in quantity field
|
||||
- UI now allows no value to be set in addition to a zero (0) value.
|
||||
- [#1237](https://github.com/hay-kot/mealie/issues/1237) - UI: Saving a 0 quantity ingredient displays 0 until the page is refreshed #1237 - UI Now properly reacts to changes in the quantity field.
|
||||
- [#1237](https://github.com/mealie-recipes/mealie/issues/1237) - UI: Saving a 0 quantity ingredient displays 0 until the page is refreshed #1237 - UI Now properly reacts to changes in the quantity field.
|
||||
|
||||
- Fix Mealie v0.5.x migration issue [#1183](https://github.com/hay-kot/mealie/issues/1183)
|
||||
- Fix Mealie v0.5.x migration issue [#1183](https://github.com/mealie-recipes/mealie/issues/1183)
|
||||
- Consolidated Frontend Types thanks to [@PFischbeck](https://github.com/Fischbeck)
|
||||
- Added support for SSL/No Auth Email [@nkringle](https://github.com/nkringle)
|
||||
- [Implement several notifications for server actions ](https://github.com/hay-kot/mealie/pull/1234)[@miroito](https://github.com/Miroito)
|
||||
- [Implement several notifications for server actions ](https://github.com/mealie-recipes/mealie/pull/1234)[@miroito](https://github.com/Miroito)
|
||||
- Fix display issue for shared recipe rendering on server [@PFischbeck](https://github.com/Fischbeck)
|
||||
|
||||
## v1.0.0b - 2022-05-09
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
- Mealie now stores the original text from parsed ingredients, with the ability to peak at the original text from a recipe. [@miroito](https://github.com/Miroito)
|
||||
- Added some management / utility functions for administrators to manage data and cleanup artifacts from the file system.
|
||||
- Fix clear url action in recipe creation [#1101](https://github.com/hay-kot/mealie/pull/1101) [@miroito](https://github.com/Miroito)
|
||||
- Fix clear url action in recipe creation [#1101](https://github.com/mealie-recipes/mealie/pull/1101) [@miroito](https://github.com/Miroito)
|
||||
- Add group statistics calculations and data storage measurements
|
||||
- No hard limits are currently imposed on groups - though this may be implemented in the future.
|
||||
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
### Bug Fixes
|
||||
|
||||
- Bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend ([#1257](https://github.com/hay-kot/mealie/issues/1257))
|
||||
- Bump @nuxtjs/auth-next in /frontend ([#1265](https://github.com/hay-kot/mealie/issues/1265))
|
||||
- Bad dev dependency ([#1281](https://github.com/hay-kot/mealie/issues/1281))
|
||||
- Add touch support for mealplanner delete ([#1298](https://github.com/hay-kot/mealie/issues/1298))
|
||||
- Bump isomorphic-dompurify from 0.18.0 to 0.19.0 in /frontend ([#1257](https://github.com/mealie-recipes/mealie/issues/1257))
|
||||
- Bump @nuxtjs/auth-next in /frontend ([#1265](https://github.com/mealie-recipes/mealie/issues/1265))
|
||||
- Bad dev dependency ([#1281](https://github.com/mealie-recipes/mealie/issues/1281))
|
||||
- Add touch support for mealplanner delete ([#1298](https://github.com/mealie-recipes/mealie/issues/1298))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add references for VSCode dev containers ([#1299](https://github.com/hay-kot/mealie/issues/1299))
|
||||
- Docker-compose.dev.yml is currently not functional ([#1300](https://github.com/hay-kot/mealie/issues/1300))
|
||||
- Add references for VSCode dev containers ([#1299](https://github.com/mealie-recipes/mealie/issues/1299))
|
||||
- Docker-compose.dev.yml is currently not functional ([#1300](https://github.com/mealie-recipes/mealie/issues/1300))
|
||||
|
||||
### Features
|
||||
|
||||
- Add reports to bulk recipe import (url) ([#1294](https://github.com/hay-kot/mealie/issues/1294))
|
||||
- Rewrite print implementation to support new ing ([#1305](https://github.com/hay-kot/mealie/issues/1305))
|
||||
- Add reports to bulk recipe import (url) ([#1294](https://github.com/mealie-recipes/mealie/issues/1294))
|
||||
- Rewrite print implementation to support new ing ([#1305](https://github.com/mealie-recipes/mealie/issues/1305))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Github stalebot changes ([#1271](https://github.com/hay-kot/mealie/issues/1271))
|
||||
- Bump eslint-plugin-nuxt in /frontend ([#1258](https://github.com/hay-kot/mealie/issues/1258))
|
||||
- Bump @vue/runtime-dom in /frontend ([#1259](https://github.com/hay-kot/mealie/issues/1259))
|
||||
- Bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend ([#1260](https://github.com/hay-kot/mealie/issues/1260))
|
||||
- Bump vue2-script-setup-transform in /frontend ([#1263](https://github.com/hay-kot/mealie/issues/1263))
|
||||
- Update dev dependencies ([#1282](https://github.com/hay-kot/mealie/issues/1282))
|
||||
- Github stalebot changes ([#1271](https://github.com/mealie-recipes/mealie/issues/1271))
|
||||
- Bump eslint-plugin-nuxt in /frontend ([#1258](https://github.com/mealie-recipes/mealie/issues/1258))
|
||||
- Bump @vue/runtime-dom in /frontend ([#1259](https://github.com/mealie-recipes/mealie/issues/1259))
|
||||
- Bump nuxt-vite from 0.1.3 to 0.3.5 in /frontend ([#1260](https://github.com/mealie-recipes/mealie/issues/1260))
|
||||
- Bump vue2-script-setup-transform in /frontend ([#1263](https://github.com/mealie-recipes/mealie/issues/1263))
|
||||
- Update dev dependencies ([#1282](https://github.com/mealie-recipes/mealie/issues/1282))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Split up recipe create page ([#1283](https://github.com/hay-kot/mealie/issues/1283))
|
||||
- Split up recipe create page ([#1283](https://github.com/mealie-recipes/mealie/issues/1283))
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
### Bug Fixes
|
||||
|
||||
- Update issue links in v1.0.0beta-2 changelog ([#1312](https://github.com/hay-kot/mealie/issues/1312))
|
||||
- Bad import path ([#1313](https://github.com/hay-kot/mealie/issues/1313))
|
||||
- Printer page refs ([#1314](https://github.com/hay-kot/mealie/issues/1314))
|
||||
- Update issue links in v1.0.0beta-2 changelog ([#1312](https://github.com/mealie-recipes/mealie/issues/1312))
|
||||
- Bad import path ([#1313](https://github.com/mealie-recipes/mealie/issues/1313))
|
||||
- Printer page refs ([#1314](https://github.com/mealie-recipes/mealie/issues/1314))
|
||||
- Consolidate stores to fix mismatched state
|
||||
- Bump @vue/composition-api from 1.6.1 to 1.6.2 in /frontend ([#1275](https://github.com/hay-kot/mealie/issues/1275))
|
||||
- Shopping list label editor ([#1333](https://github.com/hay-kot/mealie/issues/1333))
|
||||
- Bump @vue/composition-api from 1.6.1 to 1.6.2 in /frontend ([#1275](https://github.com/mealie-recipes/mealie/issues/1275))
|
||||
- Shopping list label editor ([#1333](https://github.com/mealie-recipes/mealie/issues/1333))
|
||||
|
||||
### Features
|
||||
|
||||
- Default unit fractions to True
|
||||
- Add unit abbreviation support ([#1332](https://github.com/hay-kot/mealie/issues/1332))
|
||||
- Attached images by drag and drop for recipe steps ([#1341](https://github.com/hay-kot/mealie/issues/1341))
|
||||
- Add unit abbreviation support ([#1332](https://github.com/mealie-recipes/mealie/issues/1332))
|
||||
- Attached images by drag and drop for recipe steps ([#1341](https://github.com/mealie-recipes/mealie/issues/1341))
|
||||
|
||||
### Docs
|
||||
|
||||
- Render homepage social media link images at 32x32 size ([#1310](https://github.com/hay-kot/mealie/issues/1310))
|
||||
- Render homepage social media link images at 32x32 size ([#1310](https://github.com/mealie-recipes/mealie/issues/1310))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Init git-cliff config
|
||||
- Bump @types/sortablejs in /frontend ([#1287](https://github.com/hay-kot/mealie/issues/1287))
|
||||
- Bump @babel/eslint-parser in /frontend ([#1290](https://github.com/hay-kot/mealie/issues/1290))
|
||||
- Bump @types/sortablejs in /frontend ([#1287](https://github.com/mealie-recipes/mealie/issues/1287))
|
||||
- Bump @babel/eslint-parser in /frontend ([#1290](https://github.com/mealie-recipes/mealie/issues/1290))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Unify recipe-organizer components ([#1340](https://github.com/hay-kot/mealie/issues/1340))
|
||||
- Unify recipe-organizer components ([#1340](https://github.com/mealie-recipes/mealie/issues/1340))
|
||||
|
||||
### Security
|
||||
|
||||
- Delay server response whenever username is non existing ([#1338](https://github.com/hay-kot/mealie/issues/1338))
|
||||
- Delay server response whenever username is non existing ([#1338](https://github.com/mealie-recipes/mealie/issues/1338))
|
||||
|
||||
### Wip
|
||||
|
||||
- Pagination-repository ([#1316](https://github.com/hay-kot/mealie/issues/1316))
|
||||
- Pagination-repository ([#1316](https://github.com/mealie-recipes/mealie/issues/1316))
|
||||
|
||||
@@ -63,57 +63,57 @@ If either of the above actions prevent the user from uploading images, the appli
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- For erroneously-translated datetime config ([#1362](https://github.com/hay-kot/mealie/issues/1362))
|
||||
- Fixed text color on RecipeCard in RecipePrintView and implemented ingredient sections ([#1351](https://github.com/hay-kot/mealie/issues/1351))
|
||||
- Ingredient sections lost after parsing ([#1368](https://github.com/hay-kot/mealie/issues/1368))
|
||||
- Increased float rounding precision for CRF parser ([#1369](https://github.com/hay-kot/mealie/issues/1369))
|
||||
- Infinite scroll bug on all recipes page ([#1393](https://github.com/hay-kot/mealie/issues/1393))
|
||||
- Fast fail of bulk importer ([#1394](https://github.com/hay-kot/mealie/issues/1394))
|
||||
- Bump @mdi/js from 5.9.55 to 6.7.96 in /frontend ([#1279](https://github.com/hay-kot/mealie/issues/1279))
|
||||
- Bump @nuxtjs/i18n from 7.0.3 to 7.2.2 in /frontend ([#1288](https://github.com/hay-kot/mealie/issues/1288))
|
||||
- Bump date-fns from 2.23.0 to 2.28.0 in /frontend ([#1293](https://github.com/hay-kot/mealie/issues/1293))
|
||||
- Bump fuse.js from 6.5.3 to 6.6.2 in /frontend ([#1325](https://github.com/hay-kot/mealie/issues/1325))
|
||||
- Bump core-js from 3.17.2 to 3.23.1 in /frontend ([#1383](https://github.com/hay-kot/mealie/issues/1383))
|
||||
- All-recipes page now sorts alphabetically ([#1405](https://github.com/hay-kot/mealie/issues/1405))
|
||||
- Sort recent recipes by created_at instead of date_added ([#1417](https://github.com/hay-kot/mealie/issues/1417))
|
||||
- Only show scaler when ingredients amounts enabled ([#1426](https://github.com/hay-kot/mealie/issues/1426))
|
||||
- Add missing types for API token deletion ([#1428](https://github.com/hay-kot/mealie/issues/1428))
|
||||
- Entry nutrition checker ([#1448](https://github.com/hay-kot/mealie/issues/1448))
|
||||
- Use == operator instead of is_ for sql queries ([#1453](https://github.com/hay-kot/mealie/issues/1453))
|
||||
- Use `mtime` instead of `ctime` for backup dates ([#1461](https://github.com/hay-kot/mealie/issues/1461))
|
||||
- Mealplan pagination ([#1464](https://github.com/hay-kot/mealie/issues/1464))
|
||||
- Properly use pagination for group event notifies ([#1512](https://github.com/hay-kot/mealie/pull/1512))
|
||||
- For erroneously-translated datetime config ([#1362](https://github.com/mealie-recipes/mealie/issues/1362))
|
||||
- Fixed text color on RecipeCard in RecipePrintView and implemented ingredient sections ([#1351](https://github.com/mealie-recipes/mealie/issues/1351))
|
||||
- Ingredient sections lost after parsing ([#1368](https://github.com/mealie-recipes/mealie/issues/1368))
|
||||
- Increased float rounding precision for CRF parser ([#1369](https://github.com/mealie-recipes/mealie/issues/1369))
|
||||
- Infinite scroll bug on all recipes page ([#1393](https://github.com/mealie-recipes/mealie/issues/1393))
|
||||
- Fast fail of bulk importer ([#1394](https://github.com/mealie-recipes/mealie/issues/1394))
|
||||
- Bump @mdi/js from 5.9.55 to 6.7.96 in /frontend ([#1279](https://github.com/mealie-recipes/mealie/issues/1279))
|
||||
- Bump @nuxtjs/i18n from 7.0.3 to 7.2.2 in /frontend ([#1288](https://github.com/mealie-recipes/mealie/issues/1288))
|
||||
- Bump date-fns from 2.23.0 to 2.28.0 in /frontend ([#1293](https://github.com/mealie-recipes/mealie/issues/1293))
|
||||
- Bump fuse.js from 6.5.3 to 6.6.2 in /frontend ([#1325](https://github.com/mealie-recipes/mealie/issues/1325))
|
||||
- Bump core-js from 3.17.2 to 3.23.1 in /frontend ([#1383](https://github.com/mealie-recipes/mealie/issues/1383))
|
||||
- All-recipes page now sorts alphabetically ([#1405](https://github.com/mealie-recipes/mealie/issues/1405))
|
||||
- Sort recent recipes by created_at instead of date_added ([#1417](https://github.com/mealie-recipes/mealie/issues/1417))
|
||||
- Only show scaler when ingredients amounts enabled ([#1426](https://github.com/mealie-recipes/mealie/issues/1426))
|
||||
- Add missing types for API token deletion ([#1428](https://github.com/mealie-recipes/mealie/issues/1428))
|
||||
- Entry nutrition checker ([#1448](https://github.com/mealie-recipes/mealie/issues/1448))
|
||||
- Use == operator instead of is_ for sql queries ([#1453](https://github.com/mealie-recipes/mealie/issues/1453))
|
||||
- Use `mtime` instead of `ctime` for backup dates ([#1461](https://github.com/mealie-recipes/mealie/issues/1461))
|
||||
- Mealplan pagination ([#1464](https://github.com/mealie-recipes/mealie/issues/1464))
|
||||
- Properly use pagination for group event notifies ([#1512](https://github.com/mealie-recipes/mealie/pull/1512))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add go bulk import example ([#1388](https://github.com/hay-kot/mealie/issues/1388))
|
||||
- Add go bulk import example ([#1388](https://github.com/mealie-recipes/mealie/issues/1388))
|
||||
- Fix old link
|
||||
- Pagination and filtering, and fixed a few broken links ([#1488](https://github.com/hay-kot/mealie/issues/1488))
|
||||
- Pagination and filtering, and fixed a few broken links ([#1488](https://github.com/mealie-recipes/mealie/issues/1488))
|
||||
|
||||
### Features
|
||||
|
||||
- Toggle display of ingredient references in recipe instructions ([#1268](https://github.com/hay-kot/mealie/issues/1268))
|
||||
- Add custom scaling option ([#1345](https://github.com/hay-kot/mealie/issues/1345))
|
||||
- Implemented "order by" API parameters for recipe, food, and unit queries ([#1356](https://github.com/hay-kot/mealie/issues/1356))
|
||||
- Implement user favorites page ([#1376](https://github.com/hay-kot/mealie/issues/1376))
|
||||
- Extend Apprise JSON notification functionality with programmatic data ([#1355](https://github.com/hay-kot/mealie/issues/1355))
|
||||
- Mealplan-webhooks ([#1403](https://github.com/hay-kot/mealie/issues/1403))
|
||||
- Added "last-modified" header to supported record types ([#1379](https://github.com/hay-kot/mealie/issues/1379))
|
||||
- Re-write get all routes to use pagination ([#1424](https://github.com/hay-kot/mealie/issues/1424))
|
||||
- Advanced filtering API ([#1468](https://github.com/hay-kot/mealie/issues/1468))
|
||||
- Restore frontend sorting for all recipes ([#1497](https://github.com/hay-kot/mealie/issues/1497))
|
||||
- Implemented local storage for sorting and dynamic sort icons on the new recipe sort card ([1506](https://github.com/hay-kot/mealie/pull/1506))
|
||||
- create new foods and units from their Data Management pages ([#1511](https://github.com/hay-kot/mealie/pull/1511))
|
||||
- Toggle display of ingredient references in recipe instructions ([#1268](https://github.com/mealie-recipes/mealie/issues/1268))
|
||||
- Add custom scaling option ([#1345](https://github.com/mealie-recipes/mealie/issues/1345))
|
||||
- Implemented "order by" API parameters for recipe, food, and unit queries ([#1356](https://github.com/mealie-recipes/mealie/issues/1356))
|
||||
- Implement user favorites page ([#1376](https://github.com/mealie-recipes/mealie/issues/1376))
|
||||
- Extend Apprise JSON notification functionality with programmatic data ([#1355](https://github.com/mealie-recipes/mealie/issues/1355))
|
||||
- Mealplan-webhooks ([#1403](https://github.com/mealie-recipes/mealie/issues/1403))
|
||||
- Added "last-modified" header to supported record types ([#1379](https://github.com/mealie-recipes/mealie/issues/1379))
|
||||
- Re-write get all routes to use pagination ([#1424](https://github.com/mealie-recipes/mealie/issues/1424))
|
||||
- Advanced filtering API ([#1468](https://github.com/mealie-recipes/mealie/issues/1468))
|
||||
- Restore frontend sorting for all recipes ([#1497](https://github.com/mealie-recipes/mealie/issues/1497))
|
||||
- Implemented local storage for sorting and dynamic sort icons on the new recipe sort card ([1506](https://github.com/mealie-recipes/mealie/pull/1506))
|
||||
- create new foods and units from their Data Management pages ([#1511](https://github.com/mealie-recipes/mealie/pull/1511))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Bump dev deps ([#1418](https://github.com/hay-kot/mealie/issues/1418))
|
||||
- Bump @vue/runtime-dom in /frontend ([#1423](https://github.com/hay-kot/mealie/issues/1423))
|
||||
- Backend page_all route cleanup ([#1483](https://github.com/hay-kot/mealie/issues/1483))
|
||||
- Bump dev deps ([#1418](https://github.com/mealie-recipes/mealie/issues/1418))
|
||||
- Bump @vue/runtime-dom in /frontend ([#1423](https://github.com/mealie-recipes/mealie/issues/1423))
|
||||
- Backend page_all route cleanup ([#1483](https://github.com/mealie-recipes/mealie/issues/1483))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove depreciated repo call ([#1370](https://github.com/hay-kot/mealie/issues/1370))
|
||||
- Remove depreciated repo call ([#1370](https://github.com/mealie-recipes/mealie/issues/1370))
|
||||
|
||||
### Hotfix
|
||||
|
||||
@@ -121,6 +121,6 @@ If either of the above actions prevent the user from uploading images, the appli
|
||||
|
||||
### UI
|
||||
|
||||
- Improve parser ui text display ([#1437](https://github.com/hay-kot/mealie/issues/1437))
|
||||
- Improve parser ui text display ([#1437](https://github.com/mealie-recipes/mealie/issues/1437))
|
||||
|
||||
<!-- generated by git-cliff -->
|
||||
|
||||
@@ -13,15 +13,15 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu
|
||||
3. If you're interested on working on major changes please get in touch on discord and coordinate with other developers. No sense in doubling up on work if someones already on it.
|
||||
4. Once you've got an idea of what changes you want to make, create a draft PR as soon as you can to let us know what you're working on and how we can help!
|
||||
5. If you've changed APIs, update the documentation.
|
||||
6. Run tests, including `make backend-all`. Note that the tests do not clean up after themselves and leave things in the database. So be sure to also run `make clean-data` and/or `make backend-clean` inbetween major testing rounds to be sure that you aren't testing on old data.
|
||||
6. Run tests, including `task py:check`.
|
||||
6. Issue that pull request! First make a draft PR, make sure that the automated github tests all pass, then mark as ready for review.
|
||||
7. Be sure to add release notes to the pull request.
|
||||
|
||||
## Any contributions you make will be under the AGPL Software License
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [AGPL License](https://choosealicense.com/licenses/agpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/hay-kot/mealie/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/hay-kot/mealie/issues/new); it's that easy!
|
||||
## Report bugs using Github's [issues](https://github.com/mealie-recipes/mealie/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/mealie-recipes/mealie/issues/new); it's that easy!
|
||||
|
||||
## Write bug reports with detail, background, and sample code
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
77
docs/docs/contributors/developers-guide/maintainers.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Maintainers Guide
|
||||
|
||||
This is the start of the maintainers guide for Mealie developers. Those who have been invited to the GitHub organization and/or those who wish to play a bigger part in the Mealie developers community may find this helpful.
|
||||
|
||||
## Managing Issues
|
||||
|
||||
If you are working on issues, it can be helpful to understand the workflow for our repository. When an issue comes in it is tagged with the `bug` and `triage` flags. This is to indicate that they need to be reviewed by a maintainer to determine validity.
|
||||
|
||||
After you've reviered an issue it will generally move into one of two states:
|
||||
|
||||
`bug:confirmed`
|
||||
: Your were able to verify the issue and we determined we need to fix it
|
||||
|
||||
|
||||
`needs more info`
|
||||
: The original post does not contain enough information, and if the reporter does not provide additional information, the issue will be automatically closed.
|
||||
|
||||
Once you've reviewed an issue and moved it into another category, you should remove the triage label.
|
||||
|
||||
### While going through issues try to keep the following in mind
|
||||
|
||||
- It is perfectly okay to ignore an issue if it is low quality
|
||||
- You should close any issues that ignore the standard report template and request they reopen the issue using the proper template
|
||||
- You should **not** try to reproduce issues that don't have clear reproduction steps, don't have a version provided, or are generally unclear.
|
||||
- Issues that are not bugs, should likely be converted to discussions.
|
||||
|
||||
## Drafting Releases
|
||||
|
||||
### Tags
|
||||
|
||||
Mealie is published via GitHub actions to the GitHub container registry with the follow tags:
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:nightly`
|
||||
: published with every push to `mealie-next` branch - [Actions File](https://github.com/mealie-recipes/mealie/blob/mealie-next/.github/workflows/nightly.yml)
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:latest`
|
||||
: published when a new GitHub Release is created - [Actions File](https://github.com/mealie-recipes/mealie/blob/mealie-next/.github/workflows/release.yml)
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:{version}`
|
||||
: published when a new GitHub Release is created - [Actions File](https://github.com/mealie-recipes/mealie/blob/mealie-next/.github/workflows/release.yml)
|
||||
|
||||
!!! note
|
||||
Both the latest, and {version} tags will be the same image on the release of a new version
|
||||
|
||||
### Process
|
||||
|
||||
Because we've built all our publishing efforts on GitHub Actions we rely primarily on automations to perform our releases. As such creating a new build of Mealie is as simple as creating a new GitHub release. Here are the general steps we take to create a new release
|
||||
|
||||
1. Navigate to the [Github Release Page](https://github.com/mealie-recipes/mealie/releases) and click the 'Draft a new release' button.
|
||||
2. Choose a tag and increment the version according to the semver specification. i.e, **major** version for breaking changes, **minor** for feature updates, and **patch** for bug fixes.
|
||||
3. Name the Release, usually just the tag is fine, however if there is a special feature you'd like to highlight this would be a great place to do it.
|
||||
4. Click the "Generate release notes" button which will pull in all the Git Commits as a changelog. For bug fix only releases this is sufficient, however if there are major features, or good quality of life improvements it's good to provide those prior to listing the full changelog.
|
||||
|
||||
!!! tip
|
||||
Don't worry about setting the version number in the container or code, it's set during the build process and uses the tag you specified when drafting a new release.
|
||||
|
||||
You can see how this is done in the [Actions File](https://github.com/mealie-recipes/mealie/blob/mealie-next/.github/workflows/partial-builder.yml#L35-L37)
|
||||
|
||||
### Tags and Releases
|
||||
|
||||
Mealie tries to adhere to a strict [Semver](https://semver.org/) policy. This means that we try to keep our releases as stable as possible, and only introduce breaking changes when absolutely necessary. As such we try to keep our releases as follows:
|
||||
|
||||
- **Major** releases are reserved for breaking changes, and are not expected to be frequent. Ideally, we will remain at v1.x.x for the forseeable future.
|
||||
- **Minor** releases are reserved for new features, and are expected to be frequent.
|
||||
- **Patch** releases are reserved for bug fixes, and are expected to be frequent.
|
||||
|
||||
Any maintainer who has privileges on GitHub to create a new release can create a release at any time they feel it is necessary. However, it is recommended that you reach out in the discord to other maintainers and get at least one other maintainer to approve the release.
|
||||
|
||||
An important caveat to this is that we _may_ make breaking changes in a minor release if it is security related. In this case, the releaser should headline the release notes with the notice and impact of the breaking change, however we may not bump the major version depending on user impact.
|
||||
|
||||
### Release Notes
|
||||
|
||||
When drafting a new release, GitHub will automatically pull in all the commits since the last release. This is a great start. After pulling in all of the commits, you should add sections for
|
||||
|
||||
- New Features - Any new features that are being introduced in this release (screenshots are great here)
|
||||
- Bug Fixes - Significant bug fixes that are being introduced in this release, smaller bug fixes can be left out if they are noted in a commit message
|
||||
- Breaking Changes - Any breaking changes that are being introduced in this release (should be rare)
|
||||
@@ -14,12 +14,12 @@ Prerequisites
|
||||
- Visual Studio Code
|
||||
|
||||
### Linux and MacOS
|
||||
|
||||
First ensure that docker is running. Then when you clone the repo and open with VS Code you should see a popup asking you to reopen the project inside a development container. Click yes and it will build the development container and run the setup required to run both the backend API and the frontend webserver. This also pre-configures pre-commit hooks to ensure that the code is up to date before committing.
|
||||
|
||||
### Windows
|
||||
Make sure the VSCode Dev Containers extension is installed, then select "Dev Containers: Clone Repository in Container Volume..." in the command pallete (F1). Select your forked repo and choose the `mealie-next` branch, which contains the latest changes. This mounts your repository directly in WSL2, which [greatly improves the performance of the container](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-a-git-repository-or-github-pr-in-an-isolated-container-volume), and enables hot-reloading for the frontend. Running the container on a mounted volume may not work correctly on Windows due to WSL permission mapping issues.
|
||||
|
||||
[Checkout the makefile reference](#make-file-reference) for all of the available commands.
|
||||
Make sure the VSCode Dev Containers extension is installed, then select "Dev Containers: Clone Repository in Container Volume..." in the command palette (F1). Select your forked repo and choose the `mealie-next` branch, which contains the latest changes. This mounts your repository directly in WSL2, which [greatly improves the performance of the container](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-a-git-repository-or-github-pr-in-an-isolated-container-volume), and enables hot-reloading for the frontend. Running the container on a mounted volume may not work correctly on Windows due to WSL permission mapping issues.
|
||||
|
||||
!!! tip
|
||||
For slow terminal checkout the solution in this [GitHub Issue](https://github.com/microsoft/vscode/issues/133215)
|
||||
@@ -29,16 +29,18 @@ Make sure the VSCode Dev Containers extension is installed, then select "Dev Con
|
||||
```
|
||||
|
||||
## Without Dev Containers
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Python 3.10](https://www.python.org/downloads/)
|
||||
- [Poetry](https://python-poetry.org/docs/#installation)
|
||||
- [Node v16.x](https://nodejs.org/en/)
|
||||
- [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable)
|
||||
- [task](https://taskfile.dev/#/installation)
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
Once the prerequisites are installed you can cd into the project base directory and run `make setup` to install the python and node dependencies.
|
||||
Once the prerequisites are installed you can cd into the project base directory and run `task setup` to install the python and node dependencies, and download the NLP model.
|
||||
|
||||
=== "Linux / macOS"
|
||||
|
||||
@@ -46,29 +48,16 @@ Once the prerequisites are installed you can cd into the project base directory
|
||||
# Naviate To The Root Directory
|
||||
cd /path/to/project
|
||||
|
||||
# Utilize the Makefile to Install Dependencies
|
||||
make setup
|
||||
# Utilize the Taskfile to Install Dependencies
|
||||
task setup
|
||||
```
|
||||
|
||||
=== "Windows"
|
||||
|
||||
``` powershell
|
||||
# Install Python Dependencies
|
||||
Set-Directory -Path "C:\path\to\project"
|
||||
poetry install
|
||||
|
||||
# Install Node Dependencies
|
||||
Set-Directory frontend
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Setting ENV Variables
|
||||
|
||||
Before you start the server you MUST copy the `template.env` and `frontend/template.env` files to their respective locations with the name `.env` and `frontend/.env` respectively. The application will-not run without these files.
|
||||
|
||||
## Postgres
|
||||
- Whether using a container or manual install, you need to set up your own postgres dev server. The database, username, password, etc should match the `POSTGRES_*` options located in the `.env` file.
|
||||
- Install psycog2 with `poetry install -E pgsql` (in the main `mealie` directory, *not* `frontend`)
|
||||
|
||||
The taskfile has two commands that need to be run to run the development environment against a postgres database.
|
||||
|
||||
- `task dev:services` - This will start the postgres database, and a smtp server for email testing.
|
||||
- `task py:postgres` - This will run that backend API configured for the local postgres database.
|
||||
|
||||
## Starting The Server
|
||||
|
||||
@@ -78,57 +67,24 @@ Now you're ready to start the servers. You'll need two shells open, One for the
|
||||
|
||||
```bash
|
||||
# Terminal #1
|
||||
make backend
|
||||
task py
|
||||
|
||||
# Terminal #2
|
||||
make frontend
|
||||
task ui
|
||||
```
|
||||
|
||||
=== "Windows"
|
||||
|
||||
``` powershell
|
||||
# Terminal # 1
|
||||
poetry run python mealie/db/init_db.py # Initialize the database
|
||||
poetry run python mealie/app.py # start application
|
||||
|
||||
# Terminal # 2
|
||||
Set-Directory frontend
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
## Make File Reference
|
||||
|
||||
Run `make help` for reference. If you're on a system that doesn't support makefiles in most cases you can use the commands directly in your terminal by copy/pasting them from the Makefile.
|
||||
|
||||
```
|
||||
docs 📄 Start Mkdocs Development Server
|
||||
code-gen 🤖 Run Code-Gen Scripts
|
||||
setup 🏗 Setup Development Instance
|
||||
setup-model 🤖 Get the latest NLP CRF++ Model
|
||||
clean-data ⚠️ Removes All Developer Data for a fresh server start
|
||||
clean-pyc 🧹 Remove Python file artifacts
|
||||
clean-test 🧹 Remove test and coverage artifacts
|
||||
backend-clean 🧹 Remove all build, test, coverage and Python artifacts
|
||||
backend-test 🧪 Run tests quickly with the default Python
|
||||
backend-format 🧺 Format, Check and Flake8
|
||||
backend-all 🧪 Runs all the backend checks and tests
|
||||
backend-coverage ☂️ Check code coverage quickly with the default Python
|
||||
backend 🎬 Start Mealie Backend Development Server
|
||||
frontend 🎬 Start Mealie Frontend Development Server
|
||||
frontend-build 🏗 Build Frontend in frontend/dist
|
||||
frontend-generate 🏗 Generate Code for Frontend
|
||||
frontend-lint 🧺 Run yarn lint
|
||||
docker-dev 🐳 Build and Start Docker Development Stack (currently not functional, see #756, #1072)
|
||||
docker-prod 🐳 Build and Start Docker Production Stack
|
||||
|
||||
```
|
||||
## Internationalization
|
||||
|
||||
### Frontend
|
||||
We use vue-i18n package for internationalization. Translations are stored in json format located in [frontend/lang/messages](https://github.com/hay-kot/mealie/tree/mealie-next/frontend/lang/messages).
|
||||
|
||||
We use vue-i18n package for internationalization. Translations are stored in json format located in [frontend/lang/messages](https://github.com/mealie-recipes/mealie/tree/mealie-next/frontend/lang/messages).
|
||||
|
||||
### Backend
|
||||
Translations are stored in json format located in [mealie/lang/messages](https://github.com/hay-kot/mealie/tree/mealie-next/mealie/lang/messages).
|
||||
|
||||
Translations are stored in json format located in [mealie/lang/messages](https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/lang/messages).
|
||||
|
||||
### Quick frontend localization with VS Code
|
||||
|
||||
[i18n Ally for VScode](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) is helpful for generating new strings to translate using Code Actions. It also has a nice feature, which shows translations in-place when editing code.
|
||||
|
||||
A few settings must be tweaked to make the most of its features. Some settings are stored on project level, but most of them have to be set manually in your workspace or user settings.\
|
||||
|
||||
@@ -15,6 +15,6 @@ Alternatively, you can register a new parser by fulfilling the `ABCIngredientPar
|
||||
|
||||
|
||||
## Links
|
||||
- [Pretrained Model](https://github.com/hay-kot/mealie-nlp-model)
|
||||
- [Pretrained Model](https://github.com/mealie-recipes/mealie-nlp-model)
|
||||
- [CRF++ (Forked)](https://github.com/hay-kot/crfpp)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||
In a lot of ways, Home Assistant is why this project exists! Since Mealie has a robust API it makes it a great fit for interacting with Home Assistant and pulling information into your dashboard.
|
||||
|
||||
|
||||
@@ -6,13 +6,16 @@ You can use bookmarklets to generate a bookmark that will take your current loca
|
||||
|
||||
You can use a [bookmarklet generator site](https://caiorss.github.io/bookmarklet-maker/) and the code below to generate a bookmark for your site. Just change the `http://localhost:8080` to your sites web address and follow the instructions.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! note
|
||||
There is no trailing `/` at the end of the url!
|
||||
|
||||
```js
|
||||
var url = document.URL;
|
||||
var mealie = "http://localhost:8080";
|
||||
var dest = mealie + "/recipe/create/url?recipe_import_url=" + url;
|
||||
var group_slug = "home" // Change this to your group slug. You can obtain this from your URL after logging-in to Mealie
|
||||
var use_keywords= "&use_keywords=1" // Optional - use keywords from recipe - update to "" if you don't want that
|
||||
var edity = "&edit=1" // Optional - keep in edit mode - update to "" if you don't want that
|
||||
|
||||
if (mealie.slice(-1) === "/") {
|
||||
mealie = mealie.slice(0, -1)
|
||||
}
|
||||
var dest = mealie + "/g/" + group_slug + "/r/create/url?recipe_import_url=" + url + use_keywords + edity;
|
||||
window.open(dest, "_blank");
|
||||
```
|
||||
|
||||
@@ -3,37 +3,80 @@
|
||||
!!! info
|
||||
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
|
||||
|
||||

|
||||
|
||||
User [brasilikum](https://github.com/brasilikum) opened an issue on the main repo about how they had created an [iOS shortcut](https://github.com/hay-kot/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/hay-kot/mealie/issues/2092) GitHub user [Zippyy](https://github.com/zippyy) has helped to create a working shortcut for version 1.X.
|
||||
|
||||
This is a useful utility for iOS users who browse for recipes in their web browser from their devices.
|
||||
|
||||
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/cc568d1615bc4f998789f85d1ef74846) 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.
|
||||
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.
|
||||
|
||||
This guide assumes that you already know how to [generate API tokens](https://hay-kot.github.io/mealie/documentation/users-groups/user-settings/#api-key-generation) for your user that intends to use an iOS shortcut.
|
||||
## Setup Video
|
||||
|
||||
First, click the [link](https://www.icloud.com/shortcuts/cc568d1615bc4f998789f85d1ef74846) and begin the setup of the shortcut.
|
||||
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
|
||||
|
||||
Next, you need to replace `url` and `port` with the information for your mealie instance.
|
||||
### Prerequisites
|
||||
|
||||
If you have a TLD that you use, put that here with no port. If you just run local, Then, you need to put in your mealie instance IP and the port of `9926`.
|
||||
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.
|
||||
|
||||
Finally, you need to replace the word `keyhere` with your API token. Keep the word `Bearer`!!!
|
||||
### 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.
|
||||
|
||||

|
||||

|
||||
|
||||
You should now be able to share a website to the shortcut and have mealie grab all the necessary information!
|
||||
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.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
|
||||
|
||||
To make the setup of a Reverse Proxy much easier, Linuxserver.io developed [SWAG](https://github.com/linuxserver/docker-swag)
|
||||
To make the setup of a Reverse Proxy much easier, Linuxserver.io developed [SWAG](https://github.com/linuxserver/docker-swag)
|
||||
SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt™) sets up an Nginx web server and reverse proxy with PHP support and a built-in certbot client that automates free SSL server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention.
|
||||
|
||||
## Step 1: Get a domain
|
||||
@@ -14,39 +14,38 @@ The first step is to grab a dynamic DNS if you don't have your own subdomain alr
|
||||
|
||||
## Step 2: Set-up SWAG
|
||||
|
||||
Then you will need to set up SWAG, the variables of the docker-compose are explained on the Github page of [SWAG](https://github.com/linuxserver/docker-swag).
|
||||
This is an example of how to set it up using duckdns and docker-compose.
|
||||
Then you will need to set up SWAG, the variables of the docker-compose.yaml file are explained on the Github page of [SWAG](https://github.com/linuxserver/docker-swag).
|
||||
This is an example of how to set it up using duckdns and docker compose.
|
||||
|
||||
!!! example "docker-compose.yml"
|
||||
!!! example "docker-compose.yaml"
|
||||
```yaml
|
||||
version: "3.1"
|
||||
services:
|
||||
swag:
|
||||
image: ghcr.io/linuxserver/swag
|
||||
container_name: swag
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Brussels
|
||||
- URL=<mydomain.duckdns>
|
||||
- SUBDOMAINS=wildcard
|
||||
- VALIDATION=duckdns
|
||||
- CERTPROVIDER= #optional
|
||||
- DNSPLUGIN= #optional
|
||||
- DUCKDNSTOKEN=<duckdnstoken>
|
||||
- EMAIL=<e-mail> #optional
|
||||
- ONLY_SUBDOMAINS=false #optional
|
||||
- EXTRA_DOMAINS=<extradomains> #optional
|
||||
- STAGING=false #optional
|
||||
volumes:
|
||||
- /etc/config/swag:/config
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80 #optional
|
||||
restart: unless-stopped
|
||||
|
||||
swag:
|
||||
image: ghcr.io/linuxserver/swag
|
||||
container_name: swag
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Brussels
|
||||
- URL=<mydomain.duckdns>
|
||||
- SUBDOMAINS=wildcard
|
||||
- VALIDATION=duckdns
|
||||
- CERTPROVIDER= #optional
|
||||
- DNSPLUGIN= #optional
|
||||
- DUCKDNSTOKEN=<duckdnstoken>
|
||||
- EMAIL=<e-mail> #optional
|
||||
- ONLY_SUBDOMAINS=false #optional
|
||||
- EXTRA_DOMAINS=<extradomains> #optional
|
||||
- STAGING=false #optional
|
||||
volumes:
|
||||
- /etc/config/swag:/config
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80 #optional
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Don't forget to change the <code>mydomain.duckns</code> into your personal domain and the <code>duckdnstoken</code> into your token and remove the brackets.
|
||||
@@ -61,26 +60,25 @@ Alternatively, you can create a new file <code>mealie.subdomain.conf</code> in p
|
||||
|
||||
!!! example "mealie.subdomain.conf"
|
||||
```yaml
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
server_name mealie.*;
|
||||
server_name mealie.*;
|
||||
|
||||
include /config/nginx/ssl.conf;
|
||||
include /config/nginx/ssl.conf;
|
||||
|
||||
client_max_body_size 0;
|
||||
client_max_body_size 0;
|
||||
|
||||
location / {
|
||||
include /config/nginx/proxy.conf;
|
||||
include /config/nginx/resolver.conf;
|
||||
set $upstream_app mealie-frontend;
|
||||
set $upstream_port 3000;
|
||||
set $upstream_proto http;
|
||||
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||
}
|
||||
|
||||
}
|
||||
location / {
|
||||
include /config/nginx/proxy.conf;
|
||||
include /config/nginx/resolver.conf;
|
||||
set $upstream_app mealie-frontend;
|
||||
set $upstream_port 3000;
|
||||
set $upstream_proto http;
|
||||
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Port-forward port 443
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## How do I enable "smart" ingredient handling?
|
||||
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And making shopping lists from reciepes that have shared ingredients can yield multiple lines of the same ingredient. **But** mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And making shopping lists from reciepes that have shared ingredients can yield multiple lines of the same ingredient. **But**, mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
### Set up your Foods and Units
|
||||
Do the following just **once**. Doing this applies to your whole group, so be careful.
|
||||
|
||||
@@ -26,9 +26,9 @@ Do the following for each recipe you want to intelligently handle ingredients.
|
||||
6. Click the Edit button/icon again
|
||||
7. Scroll to the ingredients and you should see new fields for Amount, Unit, Food, and Note. The Note in particular will contain the original text of the Recipe.
|
||||
8. Click `Parse` and you will be taken to the ingredient parsing page.
|
||||
9. Choose your parser. the `Natural Language Parser` works very well, but you can also use the `Brute Parser`.
|
||||
10. Click `Parse All` and your ingredients should be separated out into Units and Foods based on your seeding in Step 1 above.
|
||||
11. For ingredients where the Unit or Food were not found, you can click a button to accept an automatically suggested Food to add to the database. Or manually enter the Unit/Food and hit `Enter` (or click `Create`) to add it to the database
|
||||
9. Choose your parser. The `Natural Language Parser` works very well, but you can also use the `Brute Parser`.
|
||||
10. Click `Parse All`, and your ingredients should be separated out into Units and Foods based on your seeding in Step 1 above.
|
||||
11. For ingredients where the Unit or Food was not found, you can click a button to accept an automatically suggested Food to add to the database. Or, manually enter the Unit/Food and hit `Enter` (or click `Create`) to add it to the database
|
||||
12. When done, click `Save All` and you will be taken back to the recipe. Now the Unit and Food fields of the recipe should be filled out.
|
||||
|
||||
Scaling up this recipe or adding it to a Shopping List will now smartly take care of ingredient amounts and duplicate combinations.
|
||||
@@ -41,22 +41,15 @@ Yes. If you are using the v1 branches (including beta), you can upgrade to the l
|
||||
|
||||
## How can I change the theme?
|
||||
|
||||
You can change the theme by settings the environment variables on the frontend container.
|
||||
You can change the theme by settings the environment variables.
|
||||
|
||||
- [Frontend Theme](../installation/frontend-config#themeing)
|
||||
|
||||
## How can I change the language?
|
||||
|
||||
Languages need to be set on the frontend and backend containers as ENV variables.
|
||||
|
||||
- [Frontend Config](../installation/frontend-config/)
|
||||
- [Backend Config](../installation/backend-config/)
|
||||
- [Backend Config - Themeing](./installation/backend-config.md#themeing)
|
||||
|
||||
## How can I change the Login Session Timeout?
|
||||
|
||||
Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
|
||||
|
||||
- [Backend Config](../installation/backend-config/)
|
||||
- [Backend Config](./installation/backend-config.md)
|
||||
|
||||
## Can I serve Mealie on a subpath?
|
||||
|
||||
@@ -64,9 +57,7 @@ No. Due to limitations from the Javascript Framework, mealie doesn't support ser
|
||||
|
||||
## Can I install Mealie without docker?
|
||||
|
||||
Yes, you can install Mealie on your local machine. HOWEVER, it is recommended that you don't. Managing non-system versions of python, node, and npm is a pain. Moreover updating and upgrading your system with this configuration is unsupported and will likely require manual interventions. If you insist on installing Mealie on your local machine, you can use the links below to help guide your path.
|
||||
|
||||
- [Advanced Installation](../installation/advanced/)
|
||||
Yes, you can install Mealie on your local machine. HOWEVER, it is recommended that you don't. Managing non-system versions of python, node, and npm is a pain. Moreover, updating and upgrading your system with this configuration is unsupported and will likely require manual interventions.
|
||||
|
||||
## What is fuzzy search and how do I use it?
|
||||
Mealie can use fuzzy search, which is robust to minor typos. For example, searching for "brocolli" will still find your recipe for "broccoli soup". But fuzzy search is only functional on a Postgres database backend. To enable fuzzy search you will need to migrate to Postgres:
|
||||
@@ -75,9 +66,9 @@ Mealie can use fuzzy search, which is robust to minor typos. For example, search
|
||||
2. Set up a [Postgres](./installation/postgres.md) instance of Mealie
|
||||
3. Upload the backup .zip and click to apply it (as as migration)
|
||||
|
||||
## How i can attach an image or video to a Recipe?
|
||||
## How can I attach an image or video to a Recipe?
|
||||
|
||||
Yes. Mealie's Recipe Steps and other fields support the markdown syntax and therefor supports images and videos. To attach an image to the recipe, you can upload it as an asset and use the provided copy button to generate the html image tag required to render the image. For videos, Mealie provides no way to host videos. You'll need to host your videos with another provider and embed them in your recipe. Generally, the video provider will provide a link to the video and the html tag required to render the video. For example, youtube provides the following link that works inside a step. You can adjust the width and height attributes as necessary to ensure a fit.
|
||||
Mealie's Recipe Steps and other fields support markdown syntax and therefore support images and videos. To attach an image to the recipe, you can upload it as an asset and use the provided copy button to generate the html image tag required to render the image. For videos, Mealie provides no way to host videos. You'll need to host your videos with another provider and embed them in your recipe. Generally, the video provider will provide a link to the video and the html tag required to render the video. For example, YouTube provides the following link that works inside a step. You can adjust the width and height attributes as necessary to ensure a fit.
|
||||
|
||||
```html
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/nAUwKeO93bY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
@@ -85,7 +76,7 @@ Yes. Mealie's Recipe Steps and other fields support the markdown syntax and ther
|
||||
|
||||
## How can I unlock my account?
|
||||
|
||||
If your account has been locked by bad password attempts, you can use an administrator account to unlock another account. Alternatively you can unlock all account via a scripts within the container.
|
||||
If your account has been locked by bad password attempts, you can use an administrator account to unlock another account. Alternatively, you can unlock all accounts via a script within the container.
|
||||
|
||||
```shell
|
||||
docker exec -it mealie-next bash
|
||||
@@ -93,7 +84,7 @@ docker exec -it mealie-next bash
|
||||
python /app/mealie/scripts/reset_locked_users.py
|
||||
```
|
||||
|
||||
## How can I change my password
|
||||
## How can I change my password?
|
||||
|
||||
You can change your password by going to the user profile page and clicking the "Change Password" button. Alternatively you can use the following script to change your password via the CLI if you are locked out of your account.
|
||||
|
||||
@@ -107,8 +98,9 @@ python /app/mealie/scripts/change_password.py
|
||||
|
||||
Managing private groups and recipes can be confusing. The following diagram and notes should help explain how they work to determine if a recipe can be shared publicly.
|
||||
|
||||
- Private links that are generated using th`Share` button bypass all group and recipe permissions.
|
||||
- Private groups block all access to recipes, including those that are public. Expect as noted above.
|
||||
- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions
|
||||
- Private groups block all access to recipes, including those that are public, except as noted above.
|
||||
- Groups with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private recipes block all access to the recipe from public links. This does not affect Private Links.
|
||||
|
||||
```mermaid
|
||||
@@ -132,8 +124,10 @@ stateDiagram-v2
|
||||
p3 --> n1: No
|
||||
```
|
||||
|
||||
For more information, check out the [Permissions and Public Access guide](./usage/permissions-and-public-access.md).
|
||||
|
||||
## Can I use fail2ban with mealie?
|
||||
Yes, mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that, due to restrictions in docker, IP address forwarding only works on linux.
|
||||
Yes, mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux.
|
||||
|
||||
Your fail2ban usage should look like the following:
|
||||
```
|
||||
@@ -142,12 +136,12 @@ Use failregex line : ^ERROR:\s+Incorrect username or password from <HOST>
|
||||
```
|
||||
|
||||
## Why An API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based of Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based on Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
|
||||
## Why a Database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in controls of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project, it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in control of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
|
||||
As to why we need a database?
|
||||
|
||||
- **Developer Experience:** Without a database a lot of the work to maintain your data is taken on by the developer instead of a battle tested platform for storing data.
|
||||
- **Multi User Support:** With a solid database as backend storage for your data Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
|
||||
- **Developer Experience:** Without a database, a lot of the work to maintain your data is taken on by the developer instead of a battle-tested platform for storing data.
|
||||
- **Multi User Support:** With a solid database as backend storage for your data, Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor.
|
||||
|
||||
[Creation Demo](https://demo.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right }
|
||||
[Creation Demo](https://demo.mealie.io/g/home/r/create/url){ .md-button .md-button--primary .align-right }
|
||||
|
||||
### Importing Recipes
|
||||
|
||||
@@ -34,21 +34,21 @@ Mealie has a robust and flexible recipe organization system with a few different
|
||||
|
||||
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**.
|
||||
|
||||
[Categories Demo](https://demo.mealie.io/recipes/categories){ .md-button .md-button--primary }
|
||||
[Categories Demo](https://demo.mealie.io/g/home/recipes/categories){ .md-button .md-button--primary }
|
||||
|
||||
#### Tags
|
||||
|
||||
Tags, are nearly identical to categories in function but play a secondary role in some cases. As such, we recommend that you use tags freely to help you organize your recipes by more specific topics. For example, if a recipe can be frozen or is a great left-over meal, you could assign the tags **frozen** and **left-over** and easily filter for those at a later time.
|
||||
Tags are nearly identical to categories in function but play a secondary role in some cases. As such, we recommend that you use tags freely to help you organize your recipes by more specific topics. For example, if a recipe can be frozen or is a great left-over meal, you could assign the tags **frozen** and **left-over** and easily filter for those at a later time.
|
||||
|
||||
[Tags Demo](https://demo.mealie.io/recipes/tags){ .md-button .md-button--primary }
|
||||
[Tags Demo](https://demo.mealie.io/g/home/recipes/tags){ .md-button .md-button--primary }
|
||||
|
||||
#### Tools
|
||||
|
||||
Tools, are another way that some users like to organize their recipes. If a recipe requires some specific equipment if can be helpful to assign the tools to the recipes. This is particularly useful for things that are less common, like a pressure cooker, or a sous vide.
|
||||
Tools are another way that some users like to organize their recipes. If a recipe requires some specific equipment, it can be helpful to assign tools to the recipes. This is particularly useful for things that are less common, like a pressure cooker or a sous vide.
|
||||
|
||||
Each of the above organizers can be filtered in searches, and have their own pages where you can view all the recipes that are associated with those organizers.
|
||||
Each of the above organizers can be filtered in searches and have their own pages where you can view all the recipes that are associated with those organizers.
|
||||
|
||||
[Tools Demo](https://demo.mealie.io/recipes/tools){ .md-button .md-button--primary }
|
||||
[Tools Demo](https://demo.mealie.io/g/home/recipes/tools){ .md-button .md-button--primary }
|
||||
|
||||
#### Cookbooks
|
||||
|
||||
@@ -60,11 +60,11 @@ Mealie also has the concept of cookbooks. These can be created inside of a group
|
||||
- Pasta Sides: Recipes that have both the **Side** category and the **Pasta** tag
|
||||
- Dessert Breads: Recipes that have both the **Bread** category and the **Dessert** tag
|
||||
|
||||
[Cookbooks Demo](https://demo.mealie.io/group/cookbooks){ .md-button .md-button--primary }
|
||||
[Cookbooks Demo](https://demo.mealie.io/g/home/cookbooks){ .md-button .md-button--primary }
|
||||
|
||||
## Meal Planning
|
||||
|
||||
Mealie uses a calendar like view to help you plan your meals. It shows you the previous day, and the next 6 days by default. You can toggle through the calendar by clicking the arrows on the top of the page. In editor mode, you can use the random recipe buttons, or manually add an entry.
|
||||
Mealie uses a calendar like view to help you plan your meals. It shows you the previous day and the next 6 days by default. You can toggle through the calendar by clicking the arrows on the top of the page. In editor mode, you can use the random recipe buttons or manually add an entry.
|
||||
|
||||
!!! tip
|
||||
You can also add a "Note" type entry to your meal-plan when you want to include something that might not have a specific recipes. This is great for leftovers, or for ordering out.
|
||||
@@ -73,16 +73,16 @@ Mealie uses a calendar like view to help you plan your meals. It shows you the p
|
||||
|
||||
### Planner Rules
|
||||
|
||||
The meal planner has the concept of plan rules. These offer a flexible way to use your organizers to customize how a random recipe is inserted into your meal plan. You can set rules to restrict the pool of recipes based on the Tags and/or Categories of a recipe. Additionally, since meal plans have a Breakfast, Lunch, Dinner, and Snack labels you can specifically set a rule to be active for a **specific meal type** or even a **specific day of the week.**
|
||||
The meal planner has the concept of plan rules. These offer a flexible way to use your organizers to customize how a random recipe is inserted into your meal plan. You can set rules to restrict the pool of recipes based on the Tags and/or Categories of a recipe. Additionally, since meal plans have a Breakfast, Lunch, Dinner, and Snack labels, you can specifically set a rule to be active for a **specific meal type** or even a **specific day of the week.**
|
||||
|
||||
[Planner Settings Demo](https://demo.mealie.io/group/mealplan/settings){ .md-button .md-button--primary }
|
||||
|
||||
## Shopping Lists
|
||||
|
||||
The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list, or link a recipe and all of it's ingredients to track meals during the week.
|
||||
The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list or link a recipe and all of it's ingredients to track meals during the week.
|
||||
|
||||
!!! warning
|
||||
At this time there isn't a tight integration between meal-plans and shopping lists, however it's something we have planned for the future.
|
||||
At this time there isn't a tight integration between meal-plans and shopping lists; however, it's something we have planned for the future.
|
||||
|
||||
|
||||
[Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
|
||||
@@ -90,7 +90,7 @@ The shopping lists feature is a great way to keep track of what you need to buy
|
||||
|
||||
## Data Management
|
||||
|
||||
Managing a robust collection of recipes inevitable requires a lot of data. Mealie has a robust data management system that allows you to easily some of the more important data sets in your collection. Here's some of the features that are available in the `group/data/<type>` pages:
|
||||
Managing a robust collection of recipes inevitable requires a lot of data. Mealie has a robust data management system that allows you to easily export some of the more important data sets in your collection. Here's some of the features that are available in the `group/data/<type>` pages:
|
||||
|
||||
- Recipes
|
||||
- Bulk Actions
|
||||
@@ -113,7 +113,7 @@ Managing a robust collection of recipes inevitable requires a lot of data. Meali
|
||||
|
||||
### Site Settings
|
||||
|
||||
The site settings page contains general information about your installation like the application version, some configuration details, and some utilities to help you confirm your installation is working as expected. For example, you can use the Email Configuration section to validate that your email credentials are setup correctly and that the email service is working as expected. Additionally, there is a docker-volume utility that will confirm your volumes are configured and shared correctly between the front and backend of the application.
|
||||
The site settings page contains general information about your installation like the application version, some configuration details, and some utilities to help you confirm your installation is working as expected. For example, you can use the Email Configuration section to validate that your email credentials are set up correctly and that the email service is working as expected. Additionally, there is a docker-volume utility that will confirm your volumes are configured and shared correctly between the front and backend of the application.
|
||||
|
||||
[Settings Demo](https://demo.mealie.io/admin/site-settings){ .md-button .md-button--primary }
|
||||
|
||||
@@ -125,7 +125,7 @@ There is a small management area for users and groups that allows you to create,
|
||||
|
||||
### Backups
|
||||
|
||||
The backups page provides a full system backup of your installation including all assets and images related to recipes. These are archived into a zip file and stored on the server but can also be downloaded through the UI. Due to some issues in the past Mealie no longer performs automatic backups, **it is advised that during setup you also setup a backup strategy to ensure your data is not lost.**
|
||||
The backups page provides a full system backup of your installation including all assets and images related to recipes. These are archived into a zip file and stored on the server but can also be downloaded through the UI. Due to some issues in the past, Mealie no longer performs automatic backups; **it is advised that during setup you also set up a backup strategy to ensure your data is not lost.**
|
||||
|
||||
|
||||
[Backups Demo](https://demo.mealie.io/admin/backups){ .md-button .md-button--primary }
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# Advanced Installation
|
||||
|
||||
!!! warning "Not Required"
|
||||
|
||||
The items below are completely optional and are not required to manage or install your Mealie instance.
|
||||
|
||||
### Custom Caddy File
|
||||
|
||||
The Docker image provided by Mealie contains both the API and the html bundle in one convenient image. This is done by using a proxy server to serve different parts of the application depending on the URL/URI. Requests sent to `/api/*` or `/docs` will be directed to the API, anything else will be served the static web files. Below is the default Caddyfile that is used to proxy requests. You can override this file by mounting an alternative Caddyfile to `/app/Caddyfile`.
|
||||
|
||||
```
|
||||
{
|
||||
auto_https off
|
||||
admin off
|
||||
}
|
||||
|
||||
:80 {
|
||||
@proxied path /api/* /docs /openapi.json
|
||||
|
||||
root * /app/dist
|
||||
encode gzip
|
||||
uri strip_suffix /
|
||||
|
||||
handle_path /api/recipes/image/* {
|
||||
root * /app/data/img/
|
||||
file_server
|
||||
}
|
||||
|
||||
handle @proxied {
|
||||
reverse_proxy http://127.0.0.1:9000
|
||||
}
|
||||
|
||||
handle {
|
||||
try_files {path}.html {path} /
|
||||
file_server
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deployed without Docker
|
||||
|
||||
!!! error "Unsupported Deployment"
|
||||
|
||||
If you are experiencing a problem with manual deployment, please do not submit a github issue unless it is related to an aspect of the application. For deployment help, the [discord server](https://discord.gg/QuStdQGSGK) is a better place to find support.
|
||||
|
||||
Alternatively, this project is built on Python and SQLite so you may run it as a python application on your server. This is not a supported options for deployment and is only here as a reference for those who would like to do this on their own. To get started you can clone this repository into a directory of your choice and use the instructions below as a reference for how to get started.
|
||||
|
||||
There are three parts to the Mealie application
|
||||
|
||||
- Frontend/Static Files
|
||||
- Backend API
|
||||
- Proxy Server
|
||||
|
||||
### Frontend/ Static Files
|
||||
|
||||
The frontend static files are generated with `npm run build`. This is done during the build process with docker. If you choose to deploy this as a system application you must do this process yourself. In the project directory run `cd frontend` to change directories into the frontend directory and run `npm install` and then `npm run build`. This will generate the static files in a `dist` folder in the frontend directory.
|
||||
|
||||
### Backend API
|
||||
|
||||
The backend API is build with Python, FastAPI, and SQLite and requires Python 3.9, and Poetry. Once the requirements are installed, in the project directory you can run the command `poetry install` to create a python virtual environment and install the python dependencies.
|
||||
|
||||
Once the dependencies are installed you should be ready to run the server. To initialize that database you need to first run `python mealie/db/init_db.py`. Then to start The web server, you run the command `uvicorn mealie.app:app --host 0.0.0.0 --port 9000`
|
||||
|
||||
### Proxy Server
|
||||
|
||||
You must use a proxy server to server up the static files created with `npm run build` and proxy requests to the API. In the docker build this is done with Caddy. You can use the CaddyFile in the section above as a reference. One important thing to keep in mind is that you should drop any trailing `/` in the url. Not doing this may result in failed API requests.
|
||||
|
||||
[workers_per_core]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#workers_per_core
|
||||
[max_workers]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#max_workers
|
||||
[web_concurrency]: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/README.md#web_concurrency
|
||||
@@ -9,13 +9,12 @@
|
||||
| PUID | 911 | UserID permissions between host OS and container |
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| DEFAULT_EMAIL | changeme@example.com | The default username for the superuser |
|
||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
|
||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||
| 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 | true | Allow user sign-up without token (should match frontend env) |
|
||||
| ALLOW_SIGNUP | true | Allow user sign-up without token |
|
||||
|
||||
### Security
|
||||
|
||||
@@ -75,3 +74,24 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
||||
| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id |
|
||||
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
|
||||
| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email |
|
||||
|
||||
### Themeing
|
||||
|
||||
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------------------- | :-----: | --------------------------- |
|
||||
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_ACCENT | #007A99 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_SUCCESS | #43A047 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
|
||||
| THEME_DARK_PRIMARY | #E58325 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ACCENT | #007A99 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SECONDARY | #973542 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SUCCESS | #43A047 | Dark Theme Config Variable |
|
||||
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
|
||||
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
# Installation Checklist
|
||||
|
||||
To install Mealie on your server there are a few steps for proper configuration. Let's go through them.
|
||||
To install Mealie on your server, there are a few steps for proper configuration. Let's go through them.
|
||||
|
||||
!!! tip TLDR
|
||||
Don't need step by step? Checkout the
|
||||
|
||||
Don't need step-by-step? Check out:
|
||||
|
||||
- [SQLite docker-compose](./sqlite.md)
|
||||
- [Postgres docker-compose](./postgres.md)
|
||||
- [Single container docker-compose](./single-container.md)
|
||||
|
||||
## Pre-work
|
||||
|
||||
To deploy mealie on your local network it is highly recommended to use docker to deploy the image straight from dockerhub. Using the docker-compose templates provided, you should be able to get a stack up and running easily by changing a few default values and deploying. You can deploy with either SQLite (default) or Postgres. SQLite is sufficient for most use cases. Additionally, with Mealie's automated backup and restore functionality, you can easily move between SQLite and Postgres as you wish.
|
||||
To deploy mealie on your local network, it is highly recommended to use Docker to deploy the image straight from the GitHub registry. Using the docker-compose templates provided, you should be able to get a stack up and running easily by changing a few default values and deploying. You can deploy with either SQLite (default) or Postgres. SQLite is sufficient for most use cases. Additionally, with Mealie's automated backup and restore functionality, you can easily move between SQLite and Postgres as you wish.
|
||||
|
||||
[Get Docker](https://docs.docker.com/get-docker/)
|
||||
|
||||
[Get Docker Compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
[Mealie on Dockerhub](https://hub.docker.com/r/hkotel/mealie)
|
||||
[Mealie on GitHub registry](https://github.com/mealie-recipes/mealie/pkgs/container/mealie)
|
||||
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
|
||||
!!! warning "32bit Support"
|
||||
|
||||
Due to a build dependency limitation, Mealie is not supported on 32bit ARM systems. If you're running into this limitation on a newer Raspberry Pi, please consider upgrading to a 64bit operating system on the Raspberry Pi.
|
||||
|
||||
## Migrating From over V1 Versions
|
||||
## Migrating From Other V1 Versions
|
||||
|
||||
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:
|
||||
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:nightly`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v1.0.0-RC1.1`
|
||||
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
|
||||
|
||||
For an example of what these changes look like, see the new [SQLite](./sqlite.md) or [PostgreSQL](./postgres.md) docker-compose examples. The container swap should be seemless, at least that's our hope!
|
||||
|
||||
## Step 1: Deployment Type
|
||||
|
||||
SQLite is a popular, open source, self-contained, zero-configuration database that is the ideal choice for Mealie when you have 1-20 Users and your concurrent write operations will be some-what limited.
|
||||
|
||||
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.
|
||||
@@ -49,17 +50,16 @@ You can find the relevant ready to use docker-compose files for supported instal
|
||||
|
||||
## Step 2: Setting up your files.
|
||||
|
||||
The following steps were tested on a Ubuntu 20.04 server, but should work for most other Linux distributions. These steps are not required, but is how I generally will setup services on my server.
|
||||
|
||||
The following steps were tested on a Ubuntu 20.04 server, but should work for most other Linux distributions. These steps are not required, but this is how I generally will setup services on my server.
|
||||
|
||||
1. SSH into your server and navigate to the home directory of the user you want to run Mealie as. If that is your current user, you can use `cd ~` to ensure you're in the right directory.
|
||||
2. Create a directory called `docker` and navigate into it: `mkdir docker && cd docker` (this is optional, if you organizer your docker installs separate from everything else)
|
||||
2. Create a directory called `docker` and navigate into it: `mkdir docker && cd docker` (this is optional, if you organize your docker installs separate from everything else)
|
||||
3. Do the same for mealie: `mkdir mealie && cd mealie`
|
||||
4. Create a docker-compose.yaml file in the mealie directory: `touch docker-compose.yaml`
|
||||
5. Use the text editor or your choice to edit the file and copy the contents of the docker-compose template for the deployment type you want to use: `nano docker-compose.yaml` or `vi docker-compose.yaml`
|
||||
|
||||
5. Use the text editor of your choice to edit the file and copy the contents of the docker-compose template for the deployment type you want to use: `nano docker-compose.yaml` or `vi docker-compose.yaml`
|
||||
|
||||
## Step 2: 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:
|
||||
|
||||
- [x] You've configured the relevant ENV variables for your database selection in the `docker-compose.yaml` files.
|
||||
@@ -68,10 +68,11 @@ After you've decided setup the files it's important to set a few ENV variables t
|
||||
- [x] You've set the `DEFAULT_EMAIL` and `DEFAULT_GROUP` variable.
|
||||
|
||||
## Step 3: Startup
|
||||
After you've configured your database, and updated the `docker-compose.yaml` files, you can start Mealie by running the following command in the directory where you've added your `docker-compose.yaml`.
|
||||
|
||||
After you've configured your database and updated the `docker-compose.yaml` files, you can start Mealie by running the following command in the directory where you've added your `docker-compose.yaml`.
|
||||
|
||||
```bash
|
||||
$ docker-compose up -d
|
||||
$ docker compose up -d
|
||||
```
|
||||
|
||||
You should see the containers start up without error. You should now be able to access the Mealie frontend at [http://localhost:9925](http://localhost:9925).
|
||||
@@ -88,21 +89,32 @@ You should see the containers start up without error. You should now be able to
|
||||
|
||||
## Step 4: Validate Installation
|
||||
|
||||
After the startup is complete you should see a login screen. Use the default credentials above to login and navigate to `/admin/site-settings`. Here you'll find a summary of your configuration details and their respective status. Before proceeding you should validate that the configuration is correct. For any warnings or errors the page will display an error and notify you of what you need to verify.
|
||||
After the startup is complete, you should see a login screen. Use the default credentials above to log in and navigate to `/admin/site-settings`. Here, you'll find a summary of your configuration details and their respective status. Before proceeding, you should validate that the configuration is correct. For any warnings or errors the page will display an error and notify you of what you need to verify.
|
||||
|
||||
## Step 5: Backup
|
||||
|
||||
While v1.0.0 is a great step to data-stability and security, it's not a backup. Mealie provides a full site data backup mechanism through the UI.
|
||||
|
||||
These backups are just plain .zip files that you can download from the UI or access via the mounted volume on your system. For complete data protection you MUST store these backups somewhere safe, and outside of the server where they are deployed.
|
||||
These backups are just plain .zip files that you can download from the UI or access via the mounted volume on your system. For complete data protection you MUST store these backups somewhere safe, outside of the server where they are deployed.
|
||||
|
||||
## Appendix
|
||||
|
||||
### Docker Tags
|
||||
|
||||
See all available tags on [GitHub](https://github.com/mealie-recipes/mealie/pkgs/container/mealie). We do not currently publish new images to Dockerhub.
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:nightly`
|
||||
|
||||
The nightly build are the latest and greatest builds that are built directly off of every commit to the `mealie-next` branch and as such may contain bugs. These are great to help the community catch bugs before they hit the stable release or if you like living on the edge.
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:<version>`
|
||||
|
||||
We also provide versioned containers that allow to pin to a specific release. Each time a new release is built a new tag will be pushed with the version. These are great to pin to a specific version and allows you to have absolute control on when you upgrade your container.
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:latest`
|
||||
|
||||
The latest tag provides the latest released image of Mealie.
|
||||
|
||||
---
|
||||
|
||||
**These tags no are long updated**
|
||||
|
||||
@@ -2,23 +2,21 @@
|
||||
|
||||
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 Environmental Variable Configuration See:**
|
||||
|
||||
- [Configuration](./backend-config.md)
|
||||
**For Environment Variable Configuration, see** [Backend Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:nightly
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.0.0 # (3)
|
||||
container_name: mealie
|
||||
ports:
|
||||
- "9925:9000"
|
||||
- "9925:9000" # (1)
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M # (1)
|
||||
memory: 1000M # (2)
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
@@ -46,7 +44,7 @@ services:
|
||||
image: postgres:15
|
||||
restart: always
|
||||
volumes:
|
||||
- ./mealie-pgdata:/var/lib/postgresql/data
|
||||
- mealie-pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_USER: mealie
|
||||
@@ -62,3 +60,4 @@ volumes:
|
||||
|
||||
1. To access the mealie interface you only need to expose port 9000 on the mealie container. Here we expose port 9925 on the host, but feel free to change this to any port you like.
|
||||
2. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
3. Whilst a 'latest' tag is available, the Mealie team advises specifying a specific version tag and consciously updating to newer versions when you have time to read the release notes and ensure you follow any manual actions required (which should be rare).
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
# Installing with SQLite
|
||||
|
||||
!!! Warning
|
||||
If you're planning on deploying and using Network Attached Storage with Mealie, you should use [Postgres](./postgres.md) instead of SQLite. SQLite is not designed to be used with Network Attached Storage and can cause data corruption, or locked database errors
|
||||
|
||||
|
||||
SQLite is a popular, open source, self-contained, zero-configuration database that is the ideal choice for Mealie when you have 1-20 Users. Below is a ready to use docker-compose.yaml file for deploying Mealie on your server.
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
- [Configuration](./backend-config.md)
|
||||
**For Environment Variable Configuration, see** [Backend Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:nightly
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.0.0 # (3)
|
||||
container_name: mealie
|
||||
ports:
|
||||
- "9925:9000" # (1)
|
||||
@@ -41,3 +43,4 @@ volumes:
|
||||
|
||||
1. To access the mealie interface you only need to expose port 9000 on the container. Here we expose port 9925 on the host, but feel free to change this to any port you like.
|
||||
2. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
3. Whilst a 'latest' tag is available, the Mealie team advises specifying a specific version tag and consciously updating to newer versions when you have time to read the release notes and ensure you follow any manual actions required (which should be rare).
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
# About The Project
|
||||
|
||||
!!! warning "Mealie v1 Beta Release"
|
||||
This documentation is for the Mealie v1 Beta release and is not final. As such, it may contain incomplete or incorrect information. You should understand that installing Mealie v1 Beta is a work in progress and while we've committed to maintaining the database schema and provided migrations, we are still in the process of adding new features, and robust testing to ensure the application works as expected.
|
||||
|
||||
You should likely find bugs, errors, and unfinished pages within the application. To find the current status of the release you can checkout the [project on github](https://github.com/hay-kot/mealie/projects/7) or reach out on discord.
|
||||
|
||||
|
||||
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
|
||||
|
||||
[Remember to join the Discord](https://discord.gg/QuStdQGSGK)
|
||||
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🔍 Smart search, mix & match of "quoted literal searches" and keyword search. Fuzzy search ("is it brocolli or broccoli?") is also available when using a Postgres database.
|
||||
- 🏷️ Tag recipes with categories or tags for flexible sorting
|
||||
- 🕸 Import recipes from around the web by URL
|
||||
@@ -20,32 +14,35 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
- 🛒 Generate shopping lists
|
||||
- 🐳 Easy setup with Docker
|
||||
- 🎨 Customize your interface with color themed layouts
|
||||
- 💾 Export all your data in any format with Jinja2 Templates, with easy data restoration from the user interface.
|
||||
- 🌍 localized in many languages
|
||||
- ➕ Plus tons more!
|
||||
- Flexible API
|
||||
- Custom key/value pairs for recipes
|
||||
- Webhook support
|
||||
- Interactive API Documentation thanks to [FastAPI](https://fastapi.tiangolo.com/) and [Swagger](https://petstore.swagger.io/)
|
||||
- Raw JSON Recipe Editor
|
||||
- Migration from other platforms
|
||||
- Chowdown
|
||||
- Nextcloud Cookbook
|
||||
- Random meal plan generation
|
||||
- Flexible API
|
||||
- Custom key/value pairs for recipes
|
||||
- Webhook support
|
||||
- Interactive API Documentation thanks to [FastAPI](https://fastapi.tiangolo.com/) and [Swagger](https://petstore.swagger.io/)
|
||||
- Raw JSON Recipe Editor
|
||||
- Migration from other platforms
|
||||
- Chowdown
|
||||
- Nextcloud Cookbook
|
||||
- Copy Me That
|
||||
- Paprika
|
||||
- Tandoor Recipes
|
||||
- Random meal plan generation
|
||||
- Advanced rule configuration to fine tune random recipes
|
||||
|
||||
## FAQ
|
||||
See the [Frequently Asked Questions page](./faq.md)
|
||||
|
||||
See the [Frequently Asked Questions page](./faq.md)
|
||||
|
||||
## Built With
|
||||
|
||||
* [Vue.js](https://vuejs.org/)
|
||||
* [Vuetify](https://vuetifyjs.com/en/)
|
||||
* [FastAPI](https://fastapi.tiangolo.com/)
|
||||
* [Docker](https://www.docker.com/)
|
||||
|
||||
- [Vue.js](https://vuejs.org/)
|
||||
- [Vuetify](https://vuetifyjs.com/en/)
|
||||
- [FastAPI](https://fastapi.tiangolo.com/)
|
||||
- [Docker](https://www.docker.com/)
|
||||
|
||||
<!-- CONTRIBUTING -->
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are what make the open source community such an amazing place to learn, develop, and create. Any contributions you make are **greatly appreciated**. See the [Contributors Guide](../../contributors/non-coders.md) for help getting started.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Migrating to Mealie v1 Release
|
||||
|
||||
The version 1 release of Mealie should be seen as an entirely different application. A whole host of changes have been made to improve the application, performance, and developer experience. Most of these improvements required significant breaking changes in the application that made a clean and easy migration impossible. However, if you've used Mealie prior to v1 there is a migration path to get most of your data from the old version to the new v1 version.
|
||||
The version 1 release of Mealie should be seen as an entirely different application. A whole host of changes have been made to improve the application, performance, and developer experience. Most of these improvements required significant breaking changes in the application that made a clean and easy migration impossible. However, if you've used Mealie prior to v1, there is a migration path to get most of your data from the old version to the new v1 version.
|
||||
|
||||
!!! info "Currently Supported Migration Data"
|
||||
Supporting more data is a work in progress, but not a current priority. I'm open to PR's to add support for additional data.
|
||||
Supporting more data is a work in progress, but not a current priority. I'm open to PRs to add support for additional data.
|
||||
|
||||
- [x] Recipes
|
||||
- [x] Categories
|
||||
@@ -16,19 +16,15 @@ The version 1 release of Mealie should be seen as an entirely different applicat
|
||||
|
||||
## Migration Considerations
|
||||
|
||||
Before you migrate to v1.0.0-beta-x please consider the following:
|
||||
Before you migrate to v1.0.0 please consider the following:
|
||||
|
||||
**API Integration Will Break**
|
||||
|
||||
Several of the endpoints in the API have changed. This means that you will need to update your code to use the new endpoints.
|
||||
|
||||
**Meal Plan Notifications Are Not Yet Implemented**
|
||||
**Recipes Are Private By Default**
|
||||
|
||||
If you're using the Meal Plan webhook feature it has yet to be implemented in v1. This feature is being significantly improved in v1 and has yet to be fully fleshed out. If you were a heavy user, you may want to wait until v1 to use this feature.
|
||||
|
||||
**Recipes are Now Private**
|
||||
|
||||
This can be a plus or a minus depending on your use case. If you relied on the old implementation that allowed viewing of recipes without logging in, you will loose that access. We are planning on implementing a public facing interface for groups/tenants to allow unauthenticated users to view public recipes.
|
||||
By default, recipes can only be viewed by logged-in users. You can fine-tune public recipe access, or keep your instance fully private. For more information, check out the [Permissions and Public Access guide](../getting-started/usage/permissions-and-public-access.md).
|
||||
|
||||
|
||||
## Step 1: Setting Up The New Application
|
||||
@@ -37,7 +33,9 @@ Given the nature of the upgrade, it is highly recommended that you stand up a ne
|
||||
|
||||
## Step 2: Exporting Your Data from Pre-v1
|
||||
|
||||
In your instance of Mealie prior to v1, perform an export of your data in the Admin section. Be sure to include the recipes when performing the export. Checking additional items won't impact the migration, but they will be ignored if they are included.
|
||||
In your instance of Mealie prior to v1, perform an export (backup) of your data in the Admin section. Be sure to include the recipes when performing the export. Checking additional items won't impact the migration, but they will be ignored if they are included. The backups section is located on the admin dashboard in the section labeled "Backups":
|
||||
|
||||

|
||||
|
||||
|
||||
## Step 3: Using the Migration Tool
|
||||
@@ -47,11 +45,11 @@ In your new v1 instance, navigate to `/group/migrations` and select "Mealie" fro
|
||||
In most cases, it's faster to manually migrate the recipes that didn't take instead of trying to identify why the recipes failed to import. If you're experiencing issues with the migration tool, please open an issue on GitHub.
|
||||
|
||||
!!! note "Recipe Owners"
|
||||
When perform any migration, it will automatically assign the owner of the recipe to the user that performed the migration. All group members will still be able to access the recipe, however the owner has special permissions to lock the recipe from edits from other users.
|
||||
When perform any migration, it will automatically assign the owner of the recipe to the user that performed the migration. All group members will still be able to access the recipe; however, the owner has special permissions to lock the recipe from edits from other users.
|
||||
|
||||
|
||||
## Step 4: Reviewing New Features
|
||||
|
||||
v1 Comes with a whole host of new features and improvements. Checkout the changelog to get a sense for what's new.
|
||||
v1 Comes with a whole host of new features and improvements. Check out the changelog to get a sense for what's new.
|
||||
|
||||
- [Github releases changelog](https://github.com/hay-kot/mealie/releases)
|
||||
- [Github releases changelog](https://github.com/mealie-recipes/mealie/releases)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Development Road Map
|
||||
|
||||
## Feature Requests
|
||||
[Please request new features on Github](https://github.com/hay-kot/mealie/issues/317)
|
||||
[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
|
||||
|
||||
@@ -19,6 +19,7 @@ If you are upgrading from pre-v1.0.0 to v1.0.0, make sure you read [Migrating to
|
||||
## Docker
|
||||
For all setups using Docker the updating process looks something like this
|
||||
|
||||
- Stop the container using docker-compose down
|
||||
- Pull the latest image using `docker-compose pull`
|
||||
- Start the container again using `docker-compose up -d`
|
||||
- Stop the container using `docker compose down`
|
||||
- If you are not using the latest tag, change the version (image tag) in your docker-compose file
|
||||
- Pull the latest image using `docker compose pull`
|
||||
- Start the container again using `docker compose up -d`
|
||||
|
||||
@@ -34,4 +34,4 @@ ALTER USER mealie WITH SUPERUSER;
|
||||
ALTER USER mealie WITH NOSUPERUSER;
|
||||
```
|
||||
|
||||
For more information see [GitHub Issue #1500](https://github.com/hay-kot/mealie/issues/1500)
|
||||
For more information see [GitHub Issue #1500](https://github.com/mealie-recipes/mealie/issues/1500)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Permissions and Public Access
|
||||
|
||||
Mealie provides various levels of user access and permissions. This includes:
|
||||
- Authentication and registration ([check out the LDAP guide](./ldap.md) for how to configure access using LDAP)
|
||||
- Customizable user permissions
|
||||
- Fine-tuned public access for non-users
|
||||
|
||||
## Customizable User Permissions
|
||||
|
||||
Each user can be configured to have varying levels of access. Some of these permissions include:
|
||||
- Access to Administrator tools
|
||||
- Access to inviting other users
|
||||
- Access to manage their group and group data
|
||||
|
||||
Administrators can navigate to the Settings page and access the User Management page to configure these settings.
|
||||
|
||||
|
||||
[User Management Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary }
|
||||
|
||||
## Public Recipe Access
|
||||
|
||||
By default, groups are set to private, meaning only logged-in users may access the group. In order for a recipe to be viewable by public (not logged-in) users, two criteria must be met:
|
||||
|
||||
1. The group must not be private, *and* the group setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Group Settings page
|
||||
2. The recipe must be set to public. This can be toggled for each recipe individually, or in bulk using the Recipe Data Management page
|
||||
|
||||
Additionally, if the group is not private, public users can view all public group data (public recipes, public cookbooks, etc.) from the home page ([e.g. the demo home page](https://demo.mealie.io/g/home)).
|
||||
|
||||
[Group Settings Demo](https://demo.mealie.io/group){ .md-button .md-button--primary }
|
||||
|
||||
More broadly, here are the rules for how recipe access is determined:
|
||||
|
||||
- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions
|
||||
- Private groups block all access to recipes, including those that are public, except as noted above.
|
||||
- Groups with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private recipes block all access to the recipe from public links. This does not affect Private Links.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
r1: Request Access
|
||||
p1: Using Private Link?
|
||||
p2: Is Group Private?
|
||||
p3: Is Recipe Private?
|
||||
s1: Deny Access
|
||||
n1: Allow Access
|
||||
|
||||
|
||||
r1 --> p1
|
||||
p1 --> p2: No
|
||||
p1 --> n1: Yes
|
||||
|
||||
p2 --> s1: Yes
|
||||
p2 --> p3: No
|
||||
|
||||
p3 --> s1: Yes
|
||||
p3 --> n1: No
|
||||
```
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "base.html" %} {% block analytics %}
|
||||
<script
|
||||
async
|
||||
defer
|
||||
data-domain="hay-kot.github.io/mealie"
|
||||
src="https://plausible.io/js/plausible.js"
|
||||
></script>
|
||||
{% extends "base.html" %}
|
||||
{% block announce %}
|
||||
<div class="announce-left">
|
||||
<a href="https://recipinned.com">
|
||||
Looking for a hosted solution? Explore Recipinned from the creator of Mealie
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,7 +65,6 @@ nav:
|
||||
- FAQ: "documentation/getting-started/faq.md"
|
||||
- API: "documentation/getting-started/api-usage.md"
|
||||
- Road Map: "documentation/getting-started/roadmap.md"
|
||||
- Advanced: "documentation/getting-started/installation/advanced.md"
|
||||
|
||||
- Installation:
|
||||
- Installation Checklist: "documentation/getting-started/installation/installation-checklist.md"
|
||||
@@ -91,6 +90,7 @@ nav:
|
||||
- Developers Guide:
|
||||
- Code Contributions: "contributors/developers-guide/code-contributions.md"
|
||||
- Dev Getting Started: "contributors/developers-guide/starting-dev-server.md"
|
||||
- Maintainers Guide: "contributors/developers-guide/maintainers.md"
|
||||
- Guides:
|
||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
.layout-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.layout-enter,
|
||||
.layout-leave-active {
|
||||
opacity: 0;
|
||||
@@ -16,15 +17,15 @@
|
||||
}
|
||||
|
||||
.theme--dark.v-application {
|
||||
background-color: var(--v-background-base, #121212) !important;
|
||||
background-color: var(--v-background-base, #1e1e1e) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-navigation-drawer {
|
||||
background-color: var(--v-background-base, #121212) !important;
|
||||
background-color: var(--v-background-base, #1e1e1e) !important;
|
||||
}
|
||||
|
||||
.theme--dark.v-card {
|
||||
background-color: #2b2b2b !important;
|
||||
background-color: #1e1e1e !important;
|
||||
}
|
||||
|
||||
.left-border {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
class="mb-5 mx-1"
|
||||
:recipes="recipes"
|
||||
:query="{ cookbook: slug }"
|
||||
:group-slug="groupSlug"
|
||||
@sortRecipes="assignSorted"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@appendRecipes="appendRecipes"
|
||||
@@ -30,24 +29,20 @@
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
props: {
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => $auth.loggedIn);
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.value.params.slug;
|
||||
const { getOne } = useCookbook(loggedIn.value ? null : props.groupSlug);
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
|
||||
145
frontend/components/Domain/Group/GroupMealPlanDayContextMenu.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<RecipeDialogAddToShoppingList
|
||||
v-if="shoppingLists"
|
||||
v-model="shoppingListDialog"
|
||||
:recipes="recipesWithScales"
|
||||
:shopping-lists="shoppingLists"
|
||||
/>
|
||||
<v-menu
|
||||
offset-y
|
||||
left
|
||||
: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"
|
||||
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>
|
||||
<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-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||
import { ShoppingListSummary } from "~/lib/api/types/group";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export interface ContextMenuItem {
|
||||
title: string;
|
||||
icon: string;
|
||||
color: string | undefined;
|
||||
event: string;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
},
|
||||
props: {
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
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 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();
|
||||
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;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
contextMenuEventHandler,
|
||||
icon,
|
||||
recipesWithScales,
|
||||
shoppingLists,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -70,7 +70,6 @@
|
||||
print: true,
|
||||
printPreferences: true,
|
||||
share: loggedIn,
|
||||
publicUrl: recipe.settings && loggedIn ? recipe.settings.public : false,
|
||||
}"
|
||||
@print="$emit('print')"
|
||||
/>
|
||||
@@ -103,7 +102,6 @@ const SAVE_EVENT = "save";
|
||||
const DELETE_EVENT = "delete";
|
||||
const CLOSE_EVENT = "close";
|
||||
const JSON_EVENT = "json";
|
||||
const OCR_EVENT = "ocr";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimerMenu, RecipeTimelineBadge },
|
||||
@@ -140,12 +138,8 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showOcrButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
setup(_, context) {
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const { i18n, $globals } = useContext();
|
||||
@@ -176,15 +170,6 @@ export default defineComponent({
|
||||
},
|
||||
];
|
||||
|
||||
if (props.showOcrButton) {
|
||||
editorButtons.splice(2, 0, {
|
||||
text: i18n.t("ocr-editor.ocr-editor"),
|
||||
icon: $globals.icons.eye,
|
||||
event: OCR_EVENT,
|
||||
color: "accent",
|
||||
});
|
||||
}
|
||||
|
||||
function emitHandler(event: string) {
|
||||
switch (event) {
|
||||
case CLOSE_EVENT:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-if="value.length > 0 || edit">
|
||||
<v-card class="mt-2">
|
||||
<v-card class="mt-4">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("asset.assets") }}
|
||||
</v-card-title>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<slot name="actions">
|
||||
<v-card-actions class="px-1">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="absolute" :slug="slug" show-always />
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup" class="absolute" :slug="slug" show-always />
|
||||
|
||||
<RecipeRating class="pb-1" :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<v-spacer></v-spacer>
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="loggedIn"
|
||||
v-if="isOwnGroup"
|
||||
color="grey darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
@@ -56,7 +56,6 @@
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}"
|
||||
@delete="$emit('delete', slug)"
|
||||
/>
|
||||
@@ -69,12 +68,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import RecipeRating from "./RecipeRating.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
|
||||
@@ -83,10 +83,6 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -124,16 +120,16 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`;
|
||||
return `/g/${groupSlug.value}/r/${props.slug}`;
|
||||
});
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
</v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap justify-end align-center">
|
||||
<slot name="actions">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup" :slug="slug" show-always />
|
||||
<v-rating
|
||||
color="secondary"
|
||||
:class="loggedIn ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
:class="isOwnGroup ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
background-color="secondary lighten-3"
|
||||
dense
|
||||
length="5"
|
||||
@@ -52,7 +52,7 @@
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<!-- We also add padding to the v-rating above to compensate -->
|
||||
<RecipeContextMenu
|
||||
v-if="loggedIn"
|
||||
v-if="isOwnGroup"
|
||||
:slug="slug"
|
||||
:menu-icon="$globals.icons.dotsHorizontal"
|
||||
:name="name"
|
||||
@@ -66,7 +66,6 @@
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}"
|
||||
@deleted="$emit('delete', slug)"
|
||||
/>
|
||||
@@ -80,10 +79,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -96,10 +96,6 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -136,16 +132,16 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`;
|
||||
return `/g/${groupSlug.value}/r/${props.slug}`;
|
||||
});
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:group-slug="groupSlug"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
@@ -100,7 +99,6 @@
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:group-slug="groupSlug"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
@@ -128,12 +126,14 @@ import {
|
||||
toRefs,
|
||||
useAsync,
|
||||
useContext,
|
||||
useRoute,
|
||||
useRouter,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api";
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import RecipeCard from "./RecipeCard.vue";
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
@@ -165,10 +165,6 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
@@ -191,9 +187,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const { $auth, $globals, $vuetify } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const useMobileCards = computed(() => {
|
||||
return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards;
|
||||
});
|
||||
@@ -206,12 +200,15 @@ export default defineComponent({
|
||||
sortLoading: false,
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const router = useRouter();
|
||||
function navigateRandom() {
|
||||
if (props.recipes.length > 0) {
|
||||
const recipe = props.recipes[Math.floor(Math.random() * props.recipes.length)];
|
||||
if (recipe.slug !== undefined) {
|
||||
router.push(loggedIn.value ? `/recipe/${recipe.slug}` : `/explore/recipes/${props.groupSlug}/${recipe.slug}`);
|
||||
router.push(`/g/${groupSlug.value}/r/${recipe.slug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +219,7 @@ export default defineComponent({
|
||||
const ready = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const { fetchMore } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
const { fetchMore } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
|
||||
const queryFilter = computed(() => {
|
||||
const orderBy = props.query?.orderBy || preferences.value.orderBy;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
color="accent"
|
||||
:small="small"
|
||||
dark
|
||||
:to=" loggedIn ? `/?${urlPrefix}=${category.id}` : undefined"
|
||||
:to="isOwnGroup ? `${baseRecipeRoute}?${urlPrefix}=${category.id}` : undefined"
|
||||
>
|
||||
{{ truncateText(category.name) }}
|
||||
</v-chip>
|
||||
@@ -17,7 +17,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/user";
|
||||
|
||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||
@@ -55,9 +56,13 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn
|
||||
})
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
|
||||
const baseRecipeRoute = computed<string>(() => {
|
||||
return `/g/${groupSlug.value}`
|
||||
});
|
||||
|
||||
function truncateText(text: string, length = 20, clamp = "...") {
|
||||
if (!props.truncate) return text;
|
||||
@@ -68,7 +73,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
baseRecipeRoute,
|
||||
isOwnGroup,
|
||||
truncateText,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -69,77 +69,12 @@
|
||||
></v-select>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog v-model="shoppingListDialog" :title="$t('recipe.add-to-list')" :icon="$globals.icons.cartCheck">
|
||||
<v-card-text>
|
||||
<v-card
|
||||
v-for="list in shoppingLists"
|
||||
:key="list.id"
|
||||
hover
|
||||
class="my-2 left-border"
|
||||
@click="openShoppingListIngredientDialog(list)"
|
||||
>
|
||||
<v-card-title class="py-2">
|
||||
{{ list.name }}
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
v-model="shoppingListIngredientDialog"
|
||||
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
||||
:icon="$globals.icons.cartCheck"
|
||||
width="70%"
|
||||
:submit-text="$tc('recipe.add-to-list')"
|
||||
@submit="addRecipeToList()"
|
||||
>
|
||||
<v-card
|
||||
elevation="0"
|
||||
height="fit-content"
|
||||
max-height="60vh"
|
||||
width="100%"
|
||||
:class="$vuetify.breakpoint.smAndDown ? '' : 'ingredient-grid'"
|
||||
:style="$vuetify.breakpoint.smAndDown ? '' : { gridTemplateRows: `repeat(${Math.ceil(recipeIngredients.length / 2)}, min-content)` }"
|
||||
style="overflow-y: auto"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(ingredientData, i) in recipeIngredients"
|
||||
:key="'ingredient' + i"
|
||||
dense
|
||||
@click="recipeIngredients[i].checked = !recipeIngredients[i].checked"
|
||||
>
|
||||
<v-checkbox
|
||||
hide-details
|
||||
:input-value="ingredientData.checked"
|
||||
class="pt-0 my-auto py-auto"
|
||||
color="secondary"
|
||||
/>
|
||||
<v-list-item-content :key="ingredientData.ingredient.quantity">
|
||||
<RecipeIngredientListItem
|
||||
:ingredient="ingredientData.ingredient"
|
||||
:disable-amount="ingredientData.disableAmount"
|
||||
:scale="recipeScale" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
<div class="d-flex justify-end mb-4 mt-2">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.checkboxBlankOutline,
|
||||
text: $tc('shopping-list.uncheck-all-items'),
|
||||
event: 'uncheck',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.checkboxOutline,
|
||||
text: $tc('shopping-list.check-all-items'),
|
||||
event: 'check',
|
||||
},
|
||||
]"
|
||||
@uncheck="bulkCheckIngredients(false)"
|
||||
@check="bulkCheckIngredients(true)"
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<RecipeDialogAddToShoppingList
|
||||
v-if="shoppingLists && recipeRefWithScale"
|
||||
v-model="shoppingListDialog"
|
||||
:recipes="[recipeRefWithScale]"
|
||||
:shopping-lists="shoppingLists"
|
||||
/>
|
||||
<v-menu
|
||||
offset-y
|
||||
left
|
||||
@@ -170,18 +105,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs, useContext, useRouter, ref } from "@nuxtjs/composition-api";
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import { computed, defineComponent, reactive, toRefs, useContext, useRoute, useRouter, ref } from "@nuxtjs/composition-api";
|
||||
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
||||
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { usePlanTypeOptions } from "~/composables/use-group-mealplan";
|
||||
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { ShoppingListSummary } from "~/lib/api/types/group";
|
||||
import { PlanEntryType } from "~/lib/api/types/meal-plan";
|
||||
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
|
||||
import { useCopy } from "~/composables/use-copy";
|
||||
|
||||
export interface ContextMenuIncludes {
|
||||
delete: boolean;
|
||||
@@ -192,7 +127,6 @@ export interface ContextMenuIncludes {
|
||||
print: boolean;
|
||||
printPreferences: boolean;
|
||||
share: boolean;
|
||||
publicUrl: boolean;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
@@ -205,9 +139,9 @@ export interface ContextMenuItem {
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
RecipeDialogPrintPreferences,
|
||||
RecipeDialogShare,
|
||||
RecipeIngredientListItem
|
||||
},
|
||||
props: {
|
||||
useItems: {
|
||||
@@ -222,7 +156,6 @@ export default defineComponent({
|
||||
print: true,
|
||||
printPreferences: true,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}),
|
||||
},
|
||||
// Append items are added at the end of the useItems list
|
||||
@@ -281,7 +214,6 @@ export default defineComponent({
|
||||
recipeDeleteDialog: false,
|
||||
mealplannerDialog: false,
|
||||
shoppingListDialog: false,
|
||||
shoppingListIngredientDialog: false,
|
||||
recipeDuplicateDialog: false,
|
||||
recipeName: props.name,
|
||||
loading: false,
|
||||
@@ -291,10 +223,11 @@ export default defineComponent({
|
||||
pickerMenu: false,
|
||||
});
|
||||
|
||||
const { $auth, i18n, $globals } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { i18n, $auth, $globals } = useContext();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
// ===========================================================================
|
||||
// Context Menu Setup
|
||||
@@ -363,26 +296,19 @@ export default defineComponent({
|
||||
event: "share",
|
||||
isPublic: false,
|
||||
},
|
||||
publicUrl: {
|
||||
title: i18n.tc("recipe.public-link"),
|
||||
icon: $globals.icons.contentCopy,
|
||||
color: undefined,
|
||||
event: "publicUrl",
|
||||
isPublic: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Get Default Menu Items Specified in Props
|
||||
for (const [key, value] of Object.entries(props.useItems)) {
|
||||
if (value) {
|
||||
const item = defaultItems[key];
|
||||
if (item && (item.isPublic || loggedIn.value)) {
|
||||
if (item && (item.isPublic || isOwnGroup.value)) {
|
||||
state.menuItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add leading and Apppending Items
|
||||
// Add leading and Appending Items
|
||||
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
@@ -391,9 +317,8 @@ export default defineComponent({
|
||||
// Context Menu Event Handler
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const selectedShoppingList = ref<ShoppingListSummary>();
|
||||
const recipeRef = ref<Recipe>(props.recipe);
|
||||
const recipeIngredients = ref<{ checked: boolean; ingredient: RecipeIngredient, disableAmount: boolean }[]>([]);
|
||||
const recipeRefWithScale = computed(() => recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined);
|
||||
|
||||
async function getShoppingLists() {
|
||||
const { data } = await api.shopping.lists.getAll();
|
||||
@@ -409,61 +334,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||
selectedShoppingList.value = list;
|
||||
if (!recipeRef.value) {
|
||||
await refreshRecipe();
|
||||
}
|
||||
|
||||
if (recipeRef.value?.recipeIngredient) {
|
||||
recipeIngredients.value = recipeRef.value.recipeIngredient.map((ingredient) => {
|
||||
return {
|
||||
checked: true,
|
||||
ingredient,
|
||||
disableAmount: recipeRef.value.settings?.disableAmount || false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = true;
|
||||
}
|
||||
|
||||
function bulkCheckIngredients(value = true) {
|
||||
recipeIngredients.value.forEach((data) => {
|
||||
data.checked = value;
|
||||
});
|
||||
}
|
||||
|
||||
async function addRecipeToList() {
|
||||
if (!selectedShoppingList.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ingredients: RecipeIngredient[] = [];
|
||||
recipeIngredients.value.forEach((data) => {
|
||||
if (data.checked) {
|
||||
ingredients.push(data.ingredient);
|
||||
}
|
||||
});
|
||||
|
||||
if (!ingredients.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.shopping.lists.addRecipe(
|
||||
selectedShoppingList.value.id,
|
||||
props.recipeId,
|
||||
props.recipeScale,
|
||||
ingredients
|
||||
);
|
||||
if (data) {
|
||||
alert.success(i18n.t("recipe.recipe-added-to-list") as string);
|
||||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function deleteRecipe() {
|
||||
@@ -500,24 +370,7 @@ export default defineComponent({
|
||||
async function duplicateRecipe() {
|
||||
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
|
||||
if (data && data.slug) {
|
||||
router.push(`/recipe/${data.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { copyText } = useCopy();
|
||||
const groupSlug = ref<string>("");
|
||||
|
||||
async function setGroupSlug() {
|
||||
if (groupSlug.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.users.getSelfGroup();
|
||||
if (data) {
|
||||
groupSlug.value = data.slug;
|
||||
} else {
|
||||
// @ts-ignore this will either be a string or undefined
|
||||
groupSlug.value = $auth.user?.groupId
|
||||
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +379,7 @@ export default defineComponent({
|
||||
delete: () => {
|
||||
state.recipeDeleteDialog = true;
|
||||
},
|
||||
edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"),
|
||||
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
||||
download: handleDownloadEvent,
|
||||
duplicate: () => {
|
||||
state.recipeDuplicateDialog = true;
|
||||
@@ -541,22 +394,16 @@ export default defineComponent({
|
||||
state.printPreferencesDialog = true;
|
||||
},
|
||||
shoppingList: () => {
|
||||
getShoppingLists();
|
||||
const promises: Promise<void>[] = [getShoppingLists()];
|
||||
if (!recipeRef.value) {
|
||||
promises.push(refreshRecipe());
|
||||
}
|
||||
|
||||
state.shoppingListDialog = true;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
Promise.allSettled(promises).then(() => { state.shoppingListDialog = true });
|
||||
},
|
||||
share: () => {
|
||||
state.shareDialog = true;
|
||||
},
|
||||
publicUrl: async () => {
|
||||
await setGroupSlug();
|
||||
if (!groupSlug.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
copyText(`${window.location.origin}/explore/recipes/${groupSlug.value}/${props.slug}`);
|
||||
},
|
||||
};
|
||||
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
@@ -577,28 +424,15 @@ export default defineComponent({
|
||||
return {
|
||||
...toRefs(state),
|
||||
recipeRef,
|
||||
recipeRefWithScale,
|
||||
shoppingLists,
|
||||
selectedShoppingList,
|
||||
openShoppingListIngredientDialog,
|
||||
addRecipeToList,
|
||||
bulkCheckIngredients,
|
||||
duplicateRecipe,
|
||||
contextMenuEventHandler,
|
||||
deleteRecipe,
|
||||
addRecipeToPlan,
|
||||
icon,
|
||||
planTypeOptions,
|
||||
recipeIngredients,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.ingredient-grid {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div>
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
:title="$t('data-pages.manage-aliases')"
|
||||
:icon="$globals.icons.edit"
|
||||
:submit-icon="$globals.icons.check"
|
||||
:submit-text="$tc('general.confirm')"
|
||||
@submit="saveAliases"
|
||||
@cancel="$emit('cancel')"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row v-for="alias, i in aliases" :key="i">
|
||||
<v-col cols="10">
|
||||
<v-text-field
|
||||
v-model="alias.name"
|
||||
:label="$t('general.name')"
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $tc('general.delete'),
|
||||
event: 'delete'
|
||||
}
|
||||
]"
|
||||
@delete="deleteAlias(i)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<template #custom-card-action>
|
||||
<BaseButton edit @click="createAlias">{{ $t('data-pages.create-alias') }}
|
||||
<template #icon>
|
||||
{{ $globals.icons.create }}
|
||||
</template>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
||||
export interface GenericAlias {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
data: {
|
||||
type: Object as () => IngredientFood | IngredientUnit,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
// V-Model Support
|
||||
const dialog = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
function createAlias() {
|
||||
aliases.value.push({
|
||||
"name": "",
|
||||
})
|
||||
}
|
||||
|
||||
function deleteAlias(index: number) {
|
||||
aliases.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const aliases = ref<GenericAlias[]>(props.data.aliases || []);
|
||||
function initAliases() {
|
||||
aliases.value = [...props.data.aliases || []];
|
||||
if (!aliases.value.length) {
|
||||
createAlias();
|
||||
}
|
||||
}
|
||||
|
||||
initAliases();
|
||||
whenever(
|
||||
() => props.value,
|
||||
() => {
|
||||
initAliases();
|
||||
},
|
||||
)
|
||||
|
||||
function saveAliases() {
|
||||
const seenAliasNames: string[] = [];
|
||||
const keepAliases: GenericAlias[] = [];
|
||||
aliases.value.forEach((alias) => {
|
||||
if (
|
||||
!alias.name
|
||||
|| alias.name === props.data.name
|
||||
|| alias.name === props.data.pluralName
|
||||
// @ts-ignore only applies to units
|
||||
|| alias.name === props.data.abbreviation
|
||||
// @ts-ignore only applies to units
|
||||
|| alias.name === props.data.pluralAbbreviation
|
||||
|| seenAliasNames.includes(alias.name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
keepAliases.push(alias);
|
||||
seenAliasNames.push(alias.name);
|
||||
})
|
||||
|
||||
aliases.value = keepAliases;
|
||||
context.emit("submit", keepAliases);
|
||||
}
|
||||
|
||||
return {
|
||||
aliases,
|
||||
createAlias,
|
||||
dialog,
|
||||
deleteAlias,
|
||||
saveAliases,
|
||||
validators,
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
</tr>
|
||||
</template>
|
||||
<template #item.name="{ item }">
|
||||
<a :href="`/recipe/${item.slug}`" style="color: inherit; text-decoration: inherit; " @click="$emit('click')">{{ item.name }}</a>
|
||||
<a :href="`/r/${item.slug}`" style="color: inherit; text-decoration: inherit; " @click="$emit('click')">{{ item.name }}</a>
|
||||
</template>
|
||||
<template #item.tags="{ item }">
|
||||
<RecipeChip small :items="item.tags" :is-category="false" url-prefix="tags" />
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<div v-if="dialog">
|
||||
<BaseDialog v-if="shoppingListDialog" v-model="dialog" :title="$t('recipe.add-to-list')" :icon="$globals.icons.cartCheck">
|
||||
<v-card-text>
|
||||
<v-card
|
||||
v-for="list in shoppingLists"
|
||||
:key="list.id"
|
||||
hover
|
||||
class="my-2 left-border"
|
||||
@click="openShoppingListIngredientDialog(list)"
|
||||
>
|
||||
<v-card-title class="py-2">
|
||||
{{ list.name }}
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
v-if="shoppingListIngredientDialog"
|
||||
v-model="dialog"
|
||||
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
||||
:icon="$globals.icons.cartCheck"
|
||||
width="70%"
|
||||
:submit-text="$tc('recipe.add-to-list')"
|
||||
@submit="addRecipesToList()"
|
||||
>
|
||||
<div style="max-height: 70vh; overflow-y: auto">
|
||||
<v-card
|
||||
v-for="(recipeSection, recipeSectionIndex) in recipeIngredientSections" :key="recipeSection.recipeId + recipeSectionIndex"
|
||||
elevation="0"
|
||||
height="fit-content"
|
||||
width="100%"
|
||||
>
|
||||
<v-divider v-if="recipeSectionIndex > 0" class="mt-3" />
|
||||
<v-card-title
|
||||
v-if="recipeIngredientSections.length > 1"
|
||||
class="justify-center text-h5"
|
||||
width="100%"
|
||||
>
|
||||
<v-container style="width: 100%;">
|
||||
<v-row no-gutters class="ma-0 pa-0">
|
||||
<v-col cols="12" align-self="center" class="text-center">
|
||||
{{ recipeSection.recipeName }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="recipeSection.recipeScale > 1" no-gutters class="ma-0 pa-0">
|
||||
<!-- TODO: make this editable in the dialog and visible on single-recipe lists -->
|
||||
<v-col cols="12" align-self="center" class="text-center">
|
||||
({{ $tc("recipe.quantity") }}: {{ recipeSection.recipeScale }})
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-title>
|
||||
<div>
|
||||
<div
|
||||
v-for="(ingredientSection, ingredientSectionIndex) in recipeSection.ingredientSections"
|
||||
:key="recipeSection.recipeId + recipeSectionIndex + ingredientSectionIndex"
|
||||
>
|
||||
<v-card-title v-if="ingredientSection.sectionName" class="ingredient-title mt-2 pb-0 text-h6">
|
||||
{{ ingredientSection.sectionName }}
|
||||
</v-card-title>
|
||||
<div
|
||||
:class="$vuetify.breakpoint.smAndDown ? '' : 'ingredient-grid'"
|
||||
:style="$vuetify.breakpoint.smAndDown ? '' : { gridTemplateRows: `repeat(${Math.ceil(ingredientSection.ingredients.length / 2)}, min-content)` }"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(ingredientData, i) in ingredientSection.ingredients"
|
||||
:key="recipeSection.recipeId + recipeSectionIndex + ingredientSectionIndex + i"
|
||||
dense
|
||||
@click="recipeIngredientSections[recipeSectionIndex]
|
||||
.ingredientSections[ingredientSectionIndex]
|
||||
.ingredients[i].checked = !recipeIngredientSections[recipeSectionIndex]
|
||||
.ingredientSections[ingredientSectionIndex]
|
||||
.ingredients[i]
|
||||
.checked"
|
||||
>
|
||||
<v-checkbox
|
||||
hide-details
|
||||
:input-value="ingredientData.checked"
|
||||
class="pt-0 my-auto py-auto"
|
||||
color="secondary"
|
||||
/>
|
||||
<v-list-item-content :key="ingredientData.ingredient.quantity">
|
||||
<RecipeIngredientListItem
|
||||
:ingredient="ingredientData.ingredient"
|
||||
:disable-amount="ingredientData.disableAmount"
|
||||
:scale="recipeSection.recipeScale" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
<div class="d-flex justify-end mb-4 mt-2">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.checkboxBlankOutline,
|
||||
text: $tc('shopping-list.uncheck-all-items'),
|
||||
event: 'uncheck',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.checkboxOutline,
|
||||
text: $tc('shopping-list.check-all-items'),
|
||||
event: 'check',
|
||||
},
|
||||
]"
|
||||
@uncheck="bulkCheckIngredients(false)"
|
||||
@check="bulkCheckIngredients(true)"
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { toRefs } from "@vueuse/core";
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { ShoppingListSummary } from "~/lib/api/types/group";
|
||||
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
|
||||
export interface RecipeWithScale extends Recipe {
|
||||
scale: number;
|
||||
}
|
||||
|
||||
export interface ShoppingListIngredient {
|
||||
checked: boolean;
|
||||
ingredient: RecipeIngredient;
|
||||
disableAmount: boolean;
|
||||
}
|
||||
|
||||
export interface ShoppingListIngredientSection {
|
||||
sectionName: string;
|
||||
ingredients: ShoppingListIngredient[];
|
||||
}
|
||||
|
||||
export interface ShoppingListRecipeIngredientSection {
|
||||
recipeId: string;
|
||||
recipeName: string;
|
||||
recipeScale: number;
|
||||
ingredientSections: ShoppingListIngredientSection[];
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeIngredientListItem,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => RecipeWithScale[],
|
||||
default: undefined,
|
||||
},
|
||||
shoppingLists: {
|
||||
type: Array as () => ShoppingListSummary[],
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
const api = useUserApi();
|
||||
|
||||
// v-model support
|
||||
const dialog = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
initState();
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
shoppingListDialog: true,
|
||||
shoppingListIngredientDialog: false,
|
||||
});
|
||||
|
||||
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
|
||||
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
||||
|
||||
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
|
||||
for (const recipe of recipes) {
|
||||
if (!recipe.slug) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipeSectionMap.has(recipe.slug)) {
|
||||
// @ts-ignore not undefined, see above
|
||||
recipeSectionMap.get(recipe.slug).recipeScale += recipe.scale;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(recipe.id && recipe.name && recipe.recipeIngredient)) {
|
||||
const { data } = await api.recipes.getOne(recipe.slug);
|
||||
if (!data?.recipeIngredient?.length) {
|
||||
continue;
|
||||
}
|
||||
recipe.id = data.id || "";
|
||||
recipe.name = data.name || "";
|
||||
recipe.recipeIngredient = data.recipeIngredient;
|
||||
} else if (!recipe.recipeIngredient.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const shoppingListIngredients: ShoppingListIngredient[] = recipe.recipeIngredient.map((ing) => {
|
||||
return {
|
||||
checked: true,
|
||||
ingredient: ing,
|
||||
disableAmount: recipe.settings?.disableAmount || false,
|
||||
}
|
||||
});
|
||||
|
||||
const shoppingListIngredientSections = shoppingListIngredients.reduce((sections, ing) => {
|
||||
// if title append new section to the end of the array
|
||||
if (ing.ingredient.title) {
|
||||
sections.push({
|
||||
sectionName: ing.ingredient.title,
|
||||
ingredients: [ing],
|
||||
});
|
||||
return sections;
|
||||
}
|
||||
|
||||
// append new section if first
|
||||
if (sections.length === 0) {
|
||||
sections.push({
|
||||
sectionName: "",
|
||||
ingredients: [ing],
|
||||
});
|
||||
return sections;
|
||||
}
|
||||
|
||||
// otherwise add ingredient to last section in the array
|
||||
sections[sections.length - 1].ingredients.push(ing);
|
||||
return sections;
|
||||
}, [] as ShoppingListIngredientSection[]);
|
||||
|
||||
recipeSectionMap.set(recipe.slug, {
|
||||
recipeId: recipe.id,
|
||||
recipeName: recipe.name,
|
||||
recipeScale: recipe.scale,
|
||||
ingredientSections: shoppingListIngredientSections,
|
||||
})
|
||||
}
|
||||
|
||||
recipeIngredientSections.value = Array.from(recipeSectionMap.values());
|
||||
}
|
||||
|
||||
function initState() {
|
||||
state.shoppingListDialog = true;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
recipeIngredientSections.value = [];
|
||||
selectedShoppingList.value = null;
|
||||
}
|
||||
|
||||
initState();
|
||||
|
||||
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||
if (!props.recipes?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedShoppingList.value = list;
|
||||
await consolidateRecipesIntoSections(props.recipes);
|
||||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = true;
|
||||
}
|
||||
|
||||
function bulkCheckIngredients(value = true) {
|
||||
recipeIngredientSections.value.forEach((recipeSection) => {
|
||||
recipeSection.ingredientSections.forEach((ingSection) => {
|
||||
ingSection.ingredients.forEach((ing) => {
|
||||
ing.checked = value;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function addRecipesToList() {
|
||||
const promises: Promise<any>[] = [];
|
||||
recipeIngredientSections.value.forEach((section) => {
|
||||
if (!selectedShoppingList.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ingredients: RecipeIngredient[] = [];
|
||||
section.ingredientSections.forEach((ingSection) => {
|
||||
ingSection.ingredients.forEach((ing) => {
|
||||
if (ing.checked) {
|
||||
ingredients.push(ing.ingredient);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!ingredients.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(api.shopping.lists.addRecipe(
|
||||
selectedShoppingList.value.id,
|
||||
section.recipeId,
|
||||
section.recipeScale,
|
||||
ingredients,
|
||||
));
|
||||
});
|
||||
|
||||
let success = true;
|
||||
const results = await Promise.allSettled(promises);
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
success = false;
|
||||
}
|
||||
})
|
||||
|
||||
const successMessage = promises.length === 1
|
||||
? i18n.t("recipe.successfully-added-to-list") as string
|
||||
: i18n.t("recipe.failed-to-add-to-list") as string;
|
||||
|
||||
success ? alert.success(successMessage)
|
||||
: alert.error(i18n.t("failed-to-add-recipes-to-list") as string)
|
||||
|
||||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
dialog,
|
||||
...toRefs(state),
|
||||
addRecipesToList,
|
||||
bulkCheckIngredients,
|
||||
openShoppingListIngredientDialog,
|
||||
recipeIngredientSections,
|
||||
selectedShoppingList,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.ingredient-grid {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -31,7 +31,7 @@
|
||||
<div class="mr-auto">
|
||||
{{ $t("search.results") }}
|
||||
</div>
|
||||
<router-link to="/"> {{ $t("search.advanced-search") }} </router-link>
|
||||
<router-link :to="advancedSearchUrl"> {{ $t("search.advanced-search") }} </router-link>
|
||||
</v-card-actions>
|
||||
|
||||
<RecipeCardMobile
|
||||
@@ -54,11 +54,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, reactive, ref, watch, useRoute } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, toRefs, reactive, ref, watch, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
|
||||
import { usePublicExploreApi } from "~/composables/api/api-client";
|
||||
const SELECTED_EVENT = "selected";
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -66,6 +68,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setup(_, context) {
|
||||
const { $auth } = useContext();
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
selectedIndex: -1,
|
||||
@@ -128,7 +131,9 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`)
|
||||
watch(route, close);
|
||||
|
||||
function open() {
|
||||
@@ -140,7 +145,8 @@ export default defineComponent({
|
||||
|
||||
// ===========================================================================
|
||||
// Basic Search
|
||||
const api = useUserApi();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
||||
const search = useRecipeSearch(api);
|
||||
|
||||
// Select Handler
|
||||
@@ -152,6 +158,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
advancedSearchUrl,
|
||||
dialog,
|
||||
open,
|
||||
close,
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, toRefs, reactive, useContext } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, computed, toRefs, reactive, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import { useClipboard, useShare, whenever } from "@vueuse/core";
|
||||
import { RecipeShareToken } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
@@ -105,6 +105,10 @@ export default defineComponent({
|
||||
}
|
||||
);
|
||||
|
||||
const { $auth, i18n } = useContext();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
// ============================================================
|
||||
// Token Actions
|
||||
|
||||
@@ -138,21 +142,30 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
const { i18n } = useContext();
|
||||
const { share, isSupported: shareIsSupported } = useShare();
|
||||
const { copy } = useClipboard();
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
|
||||
function getRecipeText() {
|
||||
return i18n.t("recipe.share-recipe-message", [props.name]);
|
||||
}
|
||||
|
||||
function getTokenLink(token: string) {
|
||||
return `${window.location.origin}/shared/recipes/${token}`;
|
||||
return `${window.location.origin}/g/${groupSlug.value}/shared/r/${token}`;
|
||||
}
|
||||
|
||||
async function copyTokenLink(token: string) {
|
||||
await copy(getTokenLink(token));
|
||||
alert.success(i18n.t("recipe-share.recipe-link-copied-message") as string);
|
||||
if (isSupported.value) {
|
||||
await copy(getTokenLink(token));
|
||||
if (copied.value) {
|
||||
alert.success(i18n.t("recipe-share.recipe-link-copied-message") as string);
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("general.clipboard-copy-failure") as string);
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("general.clipboard-not-supported") as string);
|
||||
}
|
||||
}
|
||||
|
||||
async function shareRecipe(token: string) {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
:items="categories"
|
||||
>
|
||||
<v-icon left>
|
||||
{{ $globals.icons.tags }}
|
||||
{{ $globals.icons.categories }}
|
||||
</v-icon>
|
||||
{{ $t("category.categories") }}
|
||||
</SearchFilter>
|
||||
@@ -40,7 +40,7 @@
|
||||
<!-- Tool Filter -->
|
||||
<SearchFilter v-if="tools" v-model="selectedTools" :require-all.sync="state.requireAllTools" :items="tools">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.tools }}
|
||||
{{ $globals.icons.potSteam }}
|
||||
</v-icon>
|
||||
{{ $t("tool.tools") }}
|
||||
</SearchFilter>
|
||||
@@ -123,7 +123,6 @@
|
||||
class="mt-n5"
|
||||
:icon="$globals.icons.search"
|
||||
:title="$tc('search.results')"
|
||||
:group-slug="groupSlug"
|
||||
:recipes="recipes"
|
||||
:query="passedQuery"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@@ -134,9 +133,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref } from "@nuxtjs/composition-api";
|
||||
import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref, useRoute } from "@nuxtjs/composition-api";
|
||||
import { watchDebounced } from "@vueuse/shared";
|
||||
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||
@@ -150,19 +150,11 @@ import { usePublicToolStore } from "~/composables/store/use-tool-store";
|
||||
|
||||
export default defineComponent({
|
||||
components: { SearchFilter, RecipeCardSection },
|
||||
props: {
|
||||
groupSlug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const state = ref({
|
||||
auto: true,
|
||||
search: "",
|
||||
@@ -176,17 +168,20 @@ export default defineComponent({
|
||||
requireAllFoods: false,
|
||||
});
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
const categories = loggedIn.value ? useCategoryStore() : usePublicCategoryStore(props.groupSlug);
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
|
||||
const selectedCategories = ref<NoUndefinedField<RecipeCategory>[]>([]);
|
||||
|
||||
const foods = loggedIn.value ? useFoodStore() : usePublicFoodStore(props.groupSlug);
|
||||
const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
||||
const selectedFoods = ref<IngredientFood[]>([]);
|
||||
|
||||
const tags = loggedIn.value ? useTagStore() : usePublicTagStore(props.groupSlug);
|
||||
const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
|
||||
const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
|
||||
|
||||
const tools = loggedIn.value ? useToolStore() : usePublicToolStore(props.groupSlug);
|
||||
const tools = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
|
||||
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
|
||||
|
||||
const passedQuery = ref<RecipeSearchQuery | null>(null);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
solo
|
||||
hide-details
|
||||
dense
|
||||
class="mx-1"
|
||||
type="number"
|
||||
:placeholder="$t('recipe.quantity')"
|
||||
@keypress="quantityFilter"
|
||||
@@ -89,7 +88,6 @@
|
||||
hide-details
|
||||
dense
|
||||
solo
|
||||
class="mx-1"
|
||||
:placeholder="$t('recipe.notes')"
|
||||
@click="$emit('clickIngredientField', 'note')"
|
||||
>
|
||||
@@ -100,7 +98,7 @@
|
||||
<BaseButtonGroup
|
||||
hover
|
||||
:large="false"
|
||||
class="my-auto"
|
||||
class="my-auto d-flex"
|
||||
:buttons="btns"
|
||||
@toggle-section="toggleTitle"
|
||||
@toggle-original="toggleOriginalText"
|
||||
|
||||
@@ -52,11 +52,20 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const ingredientCopyText = computed(() => {
|
||||
return props.value
|
||||
.map((ingredient) => {
|
||||
return `${parseIngredientText(ingredient, props.disableAmount, props.scale, false)}`;
|
||||
})
|
||||
.join("\n");
|
||||
const components: string[] = [];
|
||||
props.value.forEach((ingredient) => {
|
||||
if (ingredient.title) {
|
||||
if (components.length) {
|
||||
components.push("");
|
||||
}
|
||||
|
||||
components.push(`[${ingredient.title}]`);
|
||||
}
|
||||
|
||||
components.push(parseIngredientText(ingredient, props.disableAmount, props.scale, false));
|
||||
});
|
||||
|
||||
return components.join("\n");
|
||||
});
|
||||
|
||||
function toggleChecked(index: number) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:class="attrs.class.sheet"
|
||||
:style="tile ? 'max-width: 100%; width: fit-content;' : 'width: 100%;'"
|
||||
>
|
||||
<v-list-item :to="'/recipe/' + recipe.slug" :class="attrs.class.listItem">
|
||||
<v-list-item :to="'/g/' + groupSlug + '/r/' + recipe.slug" :class="attrs.class.listItem">
|
||||
<v-list-item-avatar :class="attrs.class.avatar">
|
||||
<v-icon :class="attrs.class.icon" dark :small="small"> {{ $globals.icons.primary }} </v-icon>
|
||||
</v-list-item-avatar>
|
||||
@@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import DOMPurify from "dompurify";
|
||||
import { useFraction } from "~/composables/recipes/use-fraction";
|
||||
import { ShoppingListItemOut } from "~/lib/api/types/group";
|
||||
@@ -58,7 +58,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const { frac } = useFraction();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const attrs = computed(() => {
|
||||
return props.small ? {
|
||||
@@ -150,6 +153,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
attrs,
|
||||
groupSlug,
|
||||
listItemDescriptions,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="value.length > 0 || edit" class="mt-8">
|
||||
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
|
||||
<div v-for="(note, index) in value" :key="'note' + index" class="mt-1">
|
||||
<div v-for="(note, index) in value" :id="'note' + index" :key="'note' + index" class="mt-1">
|
||||
<v-card v-if="edit">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center">
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
<template>
|
||||
<v-container
|
||||
v-if="recipe && recipe.slug && recipe.settings && recipe.recipeIngredient"
|
||||
:class="{
|
||||
'pa-0': $vuetify.breakpoint.smAndDown,
|
||||
}"
|
||||
>
|
||||
<BannerExperimental />
|
||||
|
||||
<div v-if="loading">
|
||||
<v-spacer />
|
||||
<v-progress-circular indeterminate class="" color="primary"> </v-progress-circular>
|
||||
{{ loadingText }}
|
||||
<v-spacer />
|
||||
</div>
|
||||
<v-row v-if="!loading">
|
||||
<v-col cols="12" sm="7" md="7" lg="7">
|
||||
<RecipeOcrEditorPageCanvas
|
||||
:image="canvasImage"
|
||||
:tsv="tsv"
|
||||
@setText="canvasSetText"
|
||||
@update-recipe="updateRecipe"
|
||||
@close-editor="closeEditor"
|
||||
@text-selected="updateSelectedText"
|
||||
>
|
||||
</RecipeOcrEditorPageCanvas>
|
||||
|
||||
<RecipeOcrEditorPageHelp />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="5" md="5" lg="5">
|
||||
<v-tabs v-model="tab" fixed-tabs>
|
||||
<v-tab key="header">
|
||||
{{ $t("general.recipe") }}
|
||||
</v-tab>
|
||||
<v-tab key="ingredients">
|
||||
{{ $t("recipe.ingredients") }}
|
||||
</v-tab>
|
||||
<v-tab key="instructions">
|
||||
{{ $t("recipe.instructions") }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item key="header">
|
||||
<v-text-field
|
||||
v-model="recipe.name"
|
||||
class="my-3"
|
||||
:label="$t('recipe.recipe-name')"
|
||||
:rules="[validators.required]"
|
||||
@focus="selectedRecipeField = 'name'"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-text-field
|
||||
v-model="recipe.totalTime"
|
||||
class="mx-2"
|
||||
:label="$t('recipe.total-time')"
|
||||
@click="selectedRecipeField = 'totalTime'"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="recipe.prepTime"
|
||||
class="mx-2"
|
||||
:label="$t('recipe.prep-time')"
|
||||
@click="selectedRecipeField = 'prepTime'"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="recipe.performTime"
|
||||
class="mx-2"
|
||||
:label="$t('recipe.perform-time')"
|
||||
@click="selectedRecipeField = 'performTime'"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-textarea
|
||||
v-model="recipe.description"
|
||||
auto-grow
|
||||
min-height="100"
|
||||
:label="$t('recipe.description')"
|
||||
@click="selectedRecipeField = 'description'"
|
||||
>
|
||||
</v-textarea>
|
||||
<v-text-field
|
||||
v-model="recipe.recipeYield"
|
||||
dense
|
||||
:label="$t('recipe.servings')"
|
||||
@click="selectedRecipeField = 'recipeYield'"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-tab-item>
|
||||
<v-tab-item key="ingredients">
|
||||
<div class="d-flex justify-end mt-2">
|
||||
<RecipeDialogBulkAdd class="ml-1 mr-1" :input-text-prop="canvasSelectedText" @bulk-data="addIngredient" />
|
||||
<BaseButton @click="addIngredient"> {{ $t("general.new") }} </BaseButton>
|
||||
</div>
|
||||
<draggable
|
||||
v-if="recipe.recipeIngredient.length > 0"
|
||||
v-model="recipe.recipeIngredient"
|
||||
handle=".handle"
|
||||
v-bind="{
|
||||
animation: 200,
|
||||
group: 'description',
|
||||
disabled: false,
|
||||
ghostClass: 'ghost',
|
||||
}"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
>
|
||||
<TransitionGroup type="transition" :name="!drag ? 'flip-list' : ''">
|
||||
<RecipeIngredientEditor
|
||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||
:key="ingredient.referenceId"
|
||||
v-model="recipe.recipeIngredient[index]"
|
||||
class="list-group-item"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
@delete="recipe.recipeIngredient.splice(index, 1)"
|
||||
@clickIngredientField="setSingleIngredient($event, index)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</draggable>
|
||||
</v-tab-item>
|
||||
<v-tab-item key="instructions">
|
||||
<div class="d-flex justify-end mt-2">
|
||||
<RecipeDialogBulkAdd class="ml-1 mr-1" :input-text-prop="canvasSelectedText" @bulk-data="addStep" />
|
||||
<BaseButton @click="addStep()"> {{ $t("general.new") }}</BaseButton>
|
||||
</div>
|
||||
<RecipePageInstructions
|
||||
v-model="recipe.recipeInstructions"
|
||||
:ingredients="recipe.recipeIngredient"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
:edit="true"
|
||||
:recipe="recipe"
|
||||
:assets.sync="recipe.assets"
|
||||
@click-instruction-field="setSingleStep"
|
||||
/>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, reactive, toRefs, useRouter } from "@nuxtjs/composition-api";
|
||||
import { until } from "@vueuse/core";
|
||||
import { invoke } from "@vueuse/shared";
|
||||
import draggable from "vuedraggable";
|
||||
import RecipePageInstructions from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInstructions.vue";
|
||||
import { useUserApi, useStaticRoutes } from "~/composables/api";
|
||||
import { OcrTsvResponse as NullableOcrTsvResponse } from "~/lib/api/types/ocr";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { Recipe, RecipeIngredient, RecipeStep } from "~/lib/api/types/recipe";
|
||||
import { Paths, Leaves, SelectedRecipeLeaves } from "~/types/ocr-types";
|
||||
import BannerExperimental from "~/components/global/BannerExperimental.vue";
|
||||
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
||||
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
|
||||
import RecipeOcrEditorPageCanvas from "~/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageCanvas.vue";
|
||||
import RecipeOcrEditorPageHelp from "~/components/Domain/Recipe/RecipeOcrEditorPage/RecipeOcrEditorPageParts/RecipeOcrEditorPageHelp.vue";
|
||||
import { uuid4 } from "~/composables/use-utils";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
|
||||
// Temporary Shim until we have a better solution
|
||||
// https://github.com/phillipdupuis/pydantic-to-typescript/issues/28
|
||||
type OcrTsvResponse = NoUndefinedField<NullableOcrTsvResponse>;
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeIngredientEditor,
|
||||
draggable,
|
||||
BannerExperimental,
|
||||
RecipeDialogBulkAdd,
|
||||
RecipePageInstructions,
|
||||
RecipeOcrEditorPageCanvas,
|
||||
RecipeOcrEditorPageHelp,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const router = useRouter();
|
||||
const api = useUserApi();
|
||||
|
||||
const tsv = ref<OcrTsvResponse[]>([]);
|
||||
|
||||
const drag = ref(false);
|
||||
|
||||
const { recipeAssetPath } = useStaticRoutes();
|
||||
|
||||
function assetURL(assetName: string) {
|
||||
return recipeAssetPath(props.recipe.id, assetName);
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
loadingText: "Loading recipe...",
|
||||
tab: null,
|
||||
selectedRecipeField: "" as SelectedRecipeLeaves | "",
|
||||
canvasSelectedText: "",
|
||||
canvasImage: new Image(),
|
||||
});
|
||||
|
||||
const setPropertyValueByPath = function <T extends Recipe>(object: T, path: Paths<T>, value: any) {
|
||||
const a = path.split(".");
|
||||
let nextProperty: any = object;
|
||||
for (let i = 0, n = a.length - 1; i < n; ++i) {
|
||||
const k = a[i];
|
||||
if (k in nextProperty) {
|
||||
nextProperty = nextProperty[k];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
nextProperty[a[a.length - 1]] = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will find the title of a recipe with the assumption that the title
|
||||
* has the biggest ratio of surface area / number of words on the image.
|
||||
* @return Returns the text parts of the block with the highest score.
|
||||
*/
|
||||
function findRecipeTitle() {
|
||||
const filtered = tsv.value.filter((element) => element.level === 2 || element.level === 5);
|
||||
const blocks = [[]] as OcrTsvResponse[][];
|
||||
let blockNum = 1;
|
||||
filtered.forEach((element, index, array) => {
|
||||
if (index !== 0 && array[index - 1].blockNum !== element.blockNum) {
|
||||
blocks.push([]);
|
||||
blockNum = element.blockNum;
|
||||
}
|
||||
blocks[blockNum - 1].push(element);
|
||||
});
|
||||
|
||||
let bestScore = 0;
|
||||
let bestBlock = blocks[0];
|
||||
blocks.forEach((element) => {
|
||||
// element[0] is the block declaration line containing the blocks total dimensions
|
||||
// element.length is the number of words (+ 2) contained in that block
|
||||
const elementScore = (element[0].height * element[0].width) / element.length; // Prettier is adding useless parenthesis for a mysterious reason
|
||||
const elementText = element.map((element) => element.text).join(""); // Identify empty blocks and don't count them
|
||||
if (elementScore > bestScore && elementText !== "") {
|
||||
bestBlock = element;
|
||||
bestScore = elementScore;
|
||||
}
|
||||
});
|
||||
|
||||
return bestBlock
|
||||
.filter((element) => element.level === 5 && element.conf >= 40)
|
||||
.map((element) => {
|
||||
return element.text.trim();
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
invoke(async () => {
|
||||
await until(props.recipe).not.toBeNull();
|
||||
state.loadingText = "Loading OCR data...";
|
||||
|
||||
const assetName = props.recipe.assets[0].fileName;
|
||||
const imagesrc = assetURL(assetName);
|
||||
state.canvasImage.src = imagesrc;
|
||||
|
||||
const res = await api.ocr.assetToTsv(props.recipe.slug, assetName);
|
||||
tsv.value = res.data as OcrTsvResponse[];
|
||||
state.loading = false;
|
||||
|
||||
if (props.recipe.name.match(/New\sOCR\sRecipe(\s\([0-9]+\))?/g)) {
|
||||
props.recipe.name = findRecipeTitle();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function addIngredient(ingredients: Array<string> | null = null) {
|
||||
if (ingredients?.length) {
|
||||
const newIngredients = ingredients.map((x) => {
|
||||
return {
|
||||
referenceId: uuid4(),
|
||||
title: "",
|
||||
note: x,
|
||||
unit: undefined,
|
||||
food: undefined,
|
||||
disableAmount: true,
|
||||
quantity: 1,
|
||||
originalText: "",
|
||||
};
|
||||
});
|
||||
|
||||
if (newIngredients) {
|
||||
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
||||
props.recipe.recipeIngredient.push(...newIngredients);
|
||||
}
|
||||
} else {
|
||||
props.recipe.recipeIngredient.push({
|
||||
referenceId: uuid4(),
|
||||
title: "",
|
||||
note: "",
|
||||
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
||||
unit: undefined,
|
||||
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
||||
food: undefined,
|
||||
disableAmount: true,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addStep(steps: Array<string> | null = null) {
|
||||
if (!props.recipe.recipeInstructions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (steps) {
|
||||
const cleanedSteps = steps.map((step) => {
|
||||
return { id: uuid4(), text: step, title: "", ingredientReferences: [] };
|
||||
});
|
||||
|
||||
props.recipe.recipeInstructions.push(...cleanedSteps);
|
||||
} else {
|
||||
props.recipe.recipeInstructions.push({ id: uuid4(), text: "", title: "", ingredientReferences: [] });
|
||||
}
|
||||
}
|
||||
|
||||
// EVENT HANDLERS
|
||||
|
||||
// Canvas component event handlers
|
||||
async function updateRecipe() {
|
||||
const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe);
|
||||
if (data?.slug) {
|
||||
router.push("/recipe/" + data.slug);
|
||||
}
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
router.push("/recipe/" + props.recipe.slug);
|
||||
}
|
||||
|
||||
const canvasSetText = function () {
|
||||
if (state.selectedRecipeField !== "") {
|
||||
setPropertyValueByPath<Recipe>(props.recipe, state.selectedRecipeField, state.canvasSelectedText);
|
||||
}
|
||||
};
|
||||
|
||||
function updateSelectedText(value: string) {
|
||||
state.canvasSelectedText = value;
|
||||
}
|
||||
|
||||
// Recipe field selection event handlers
|
||||
function setSingleIngredient(f: keyof RecipeIngredient, index: number) {
|
||||
state.selectedRecipeField = `recipeIngredient.${index}.${f}` as SelectedRecipeLeaves;
|
||||
}
|
||||
|
||||
// Leaves<RecipeStep[]> will return some function types making eslint very unhappy
|
||||
type RecipeStepsLeaves = `${number}.${Leaves<RecipeStep>}`;
|
||||
|
||||
function setSingleStep(path: RecipeStepsLeaves) {
|
||||
state.selectedRecipeField = `recipeInstructions.${path}` as SelectedRecipeLeaves;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
addIngredient,
|
||||
addStep,
|
||||
drag,
|
||||
assetURL,
|
||||
updateRecipe,
|
||||
closeEditor,
|
||||
updateSelectedText,
|
||||
tsv,
|
||||
validators,
|
||||
setSingleIngredient,
|
||||
setSingleStep,
|
||||
canvasSetText,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,488 +0,0 @@
|
||||
<template>
|
||||
<v-card flat tile>
|
||||
<v-toolbar v-for="(section, idx) in toolbarIcons" :key="section.sectionTitle" dense style="float: left">
|
||||
<v-toolbar-title bottom>
|
||||
{{ section.sectionTitle }}
|
||||
</v-toolbar-title>
|
||||
<v-tooltip v-for="icon in section.icons" :key="icon.name" bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn icon @click="section.eventHandler(icon.name)">
|
||||
<v-icon :color="section.highlight === icon.name ? 'primary' : 'default'" v-bind="attrs" v-on="on">
|
||||
{{ icon.icon }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ icon.tooltip }}</span>
|
||||
</v-tooltip>
|
||||
<v-divider v-if="idx != toolbarIcons.length - 1" vertical class="mx-2" />
|
||||
</v-toolbar>
|
||||
<v-toolbar dense style="float: right">
|
||||
<BaseButton class="ml-1 mr-1" save @click="updateRecipe()">
|
||||
{{ $t("general.save") }}
|
||||
</BaseButton>
|
||||
<BaseButton cancel @click="closeEditor()">
|
||||
{{ $t("general.close") }}
|
||||
</BaseButton>
|
||||
</v-toolbar>
|
||||
<canvas
|
||||
ref="canvas"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseup="handleMouseUp"
|
||||
@mousemove="handleMouseMove"
|
||||
@wheel="handleMouseScroll"
|
||||
>
|
||||
</canvas>
|
||||
<span style="white-space: pre-wrap">
|
||||
{{ selectedText.trim() }}
|
||||
</span>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, useContext, ref, toRefs, watch, onMounted } from "@nuxtjs/composition-api";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { OcrTsvResponse as NullableOcrTsvResponse } from "~/lib/api/types/ocr";
|
||||
import { CanvasModes, SelectedTextSplitModes, ImagePosition, Mouse, CanvasRect, ToolbarIcons } from "~/types/ocr-types";
|
||||
|
||||
// Temporary Shim until we have a better solution
|
||||
// https://github.com/phillipdupuis/pydantic-to-typescript/issues/28
|
||||
type OcrTsvResponse = NoUndefinedField<NullableOcrTsvResponse>;
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
image: {
|
||||
type: HTMLImageElement,
|
||||
required: true,
|
||||
},
|
||||
tsv: {
|
||||
type: Array as () => OcrTsvResponse[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
canvas: null as HTMLCanvasElement | null,
|
||||
ctx: null as CanvasRenderingContext2D | null,
|
||||
canvasRect: null as DOMRect | null,
|
||||
rect: {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
w: 0,
|
||||
h: 0,
|
||||
},
|
||||
mouse: {
|
||||
current: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
down: false,
|
||||
},
|
||||
selectedText: "",
|
||||
canvasMode: "selection" as CanvasModes,
|
||||
imagePosition: {
|
||||
sx: 0,
|
||||
sy: 0,
|
||||
sWidth: 0,
|
||||
sHeight: 0,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
dWidth: 0,
|
||||
dHeight: 0,
|
||||
scale: 1,
|
||||
panStartPoint: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
} as ImagePosition,
|
||||
isImageSmallerThanCanvas: false,
|
||||
selectedTextSplitMode: "lineNum" as SelectedTextSplitModes,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.selectedText,
|
||||
(value) => {
|
||||
context.emit("text-selected", value);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (state.canvas === null) return; // never happens because the ref "canvas" is in the template
|
||||
state.ctx = state.canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
state.ctx.imageSmoothingEnabled = false;
|
||||
state.canvasRect = state.canvas.getBoundingClientRect();
|
||||
|
||||
state.canvas.width = state.canvasRect.width;
|
||||
if (props.image.width < state.canvas.width) {
|
||||
state.isImageSmallerThanCanvas = true;
|
||||
}
|
||||
state.imagePosition.dWidth = state.canvas.width;
|
||||
|
||||
updateImageScale();
|
||||
state.canvas.height = Math.min(props.image.height * state.imagePosition.scale, 700); // Max height of 700px
|
||||
|
||||
state.imagePosition.sWidth = props.image.width;
|
||||
state.imagePosition.sHeight = props.image.height;
|
||||
state.imagePosition.dWidth = state.canvas.width;
|
||||
drawImage(state.ctx);
|
||||
drawWordBoxesOnCanvas(props.tsv);
|
||||
});
|
||||
|
||||
function handleMouseDown(event: MouseEvent) {
|
||||
if (state.canvasRect === null || state.canvas === null || state.ctx === null) return;
|
||||
state.mouse.down = true;
|
||||
|
||||
updateMousePos(event);
|
||||
|
||||
if (state.canvasMode === "selection") {
|
||||
if (isMouseInRect(state.mouse, state.rect)) {
|
||||
context.emit("setText", state.selectedText);
|
||||
} else {
|
||||
state.ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height);
|
||||
drawImage(state.ctx);
|
||||
state.rect.startX = state.mouse.current.x;
|
||||
state.rect.startY = state.mouse.current.y;
|
||||
resetSelection();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.canvasMode === "panAndZoom") {
|
||||
state.imagePosition.panStartPoint.x = state.mouse.current.x - state.imagePosition.dx;
|
||||
state.imagePosition.panStartPoint.y = state.mouse.current.y - state.imagePosition.dy;
|
||||
resetSelection();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp(_event: MouseEvent) {
|
||||
if (state.canvasRect === null) return;
|
||||
state.mouse.down = false;
|
||||
state.selectedText = getWordsInSelection(props.tsv, state.rect);
|
||||
}
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
if (state.canvasRect === null || state.canvas === null || state.ctx === null) return;
|
||||
|
||||
updateMousePos(event);
|
||||
|
||||
if (state.mouse.down) {
|
||||
if (state.canvasMode === "selection") {
|
||||
state.rect.w = state.mouse.current.x - state.rect.startX;
|
||||
state.rect.h = state.mouse.current.y - state.rect.startY;
|
||||
draw();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.canvasMode === "panAndZoom") {
|
||||
state.canvas.style.cursor = "move";
|
||||
state.imagePosition.dx = state.mouse.current.x - state.imagePosition.panStartPoint.x;
|
||||
state.imagePosition.dy = state.mouse.current.y - state.imagePosition.panStartPoint.y;
|
||||
keepImageInCanvas();
|
||||
state.ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height);
|
||||
drawImage(state.ctx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMouseInRect(state.mouse, state.rect) && state.canvasMode === "selection") {
|
||||
state.canvas.style.cursor = "pointer";
|
||||
} else {
|
||||
state.canvas.style.cursor = "default";
|
||||
}
|
||||
}
|
||||
|
||||
const scrollSensitivity = 0.05;
|
||||
|
||||
function handleMouseScroll(event: WheelEvent) {
|
||||
if (state.isImageSmallerThanCanvas) return;
|
||||
if (state.canvasRect === null || state.canvas === null || state.ctx === null) return;
|
||||
|
||||
if (state.canvasMode === "panAndZoom") {
|
||||
event.preventDefault();
|
||||
|
||||
updateMousePos(event);
|
||||
|
||||
const m = Math.sign(event.deltaY);
|
||||
|
||||
const ndx = state.imagePosition.dx + m * state.imagePosition.dWidth * scrollSensitivity;
|
||||
const ndy = state.imagePosition.dy + m * state.imagePosition.dHeight * scrollSensitivity;
|
||||
const ndw = state.imagePosition.dWidth + -m * state.imagePosition.dWidth * scrollSensitivity * 2;
|
||||
const ndh = state.imagePosition.dHeight + -m * state.imagePosition.dHeight * scrollSensitivity * 2;
|
||||
|
||||
if (ndw < props.image.width) {
|
||||
state.imagePosition.dx = ndx;
|
||||
state.imagePosition.dy = ndy;
|
||||
state.imagePosition.dWidth = ndw;
|
||||
state.imagePosition.dHeight = ndh;
|
||||
}
|
||||
|
||||
keepImageInCanvas();
|
||||
updateImageScale();
|
||||
|
||||
state.ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height);
|
||||
drawImage(state.ctx);
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (state.canvasRect === null || state.canvas === null || state.ctx === null) return;
|
||||
if (state.mouse.down) {
|
||||
state.ctx.imageSmoothingEnabled = false;
|
||||
state.ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height);
|
||||
drawImage(state.ctx);
|
||||
state.ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
|
||||
state.ctx.setLineDash([6]);
|
||||
state.ctx.fillRect(state.rect.startX, state.rect.startY, state.rect.w, state.rect.h);
|
||||
state.ctx.strokeRect(state.rect.startX, state.rect.startY, state.rect.w, state.rect.h);
|
||||
}
|
||||
}
|
||||
|
||||
function drawImage(ctx: CanvasRenderingContext2D) {
|
||||
ctx.drawImage(
|
||||
props.image,
|
||||
state.imagePosition.sx,
|
||||
state.imagePosition.sy,
|
||||
state.imagePosition.sWidth,
|
||||
state.imagePosition.sHeight,
|
||||
state.imagePosition.dx,
|
||||
state.imagePosition.dy,
|
||||
state.imagePosition.dWidth,
|
||||
state.imagePosition.dHeight
|
||||
);
|
||||
}
|
||||
|
||||
function keepImageInCanvas() {
|
||||
if (state.canvasRect === null || state.canvas === null) return;
|
||||
|
||||
// Prevent image from being smaller than the canvas width
|
||||
if (state.imagePosition.dWidth - state.canvas.width < 0) {
|
||||
state.imagePosition.dWidth = state.canvas.width;
|
||||
}
|
||||
|
||||
// Prevent image from being smaller than the canvas height
|
||||
if (state.imagePosition.dHeight - state.canvas.height < 0) {
|
||||
state.imagePosition.dHeight = props.image.height * state.imagePosition.scale;
|
||||
}
|
||||
|
||||
// Prevent to move the image too much to the left
|
||||
if (state.canvas.width - state.imagePosition.dx - state.imagePosition.dWidth > 0) {
|
||||
state.imagePosition.dx = state.canvas.width - state.imagePosition.dWidth;
|
||||
}
|
||||
|
||||
// Prevent to move the image too much to the top
|
||||
if (state.canvas.height - state.imagePosition.dy - state.imagePosition.dHeight > 0) {
|
||||
state.imagePosition.dy = state.canvas.height - state.imagePosition.dHeight;
|
||||
}
|
||||
|
||||
// Prevent to move the image too much to the right
|
||||
if (state.imagePosition.dx > 0) {
|
||||
state.imagePosition.dx = 0;
|
||||
}
|
||||
|
||||
// Prevent to move the image too much to the bottom
|
||||
if (state.imagePosition.dy > 0) {
|
||||
state.imagePosition.dy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function updateImageScale() {
|
||||
state.imagePosition.scale = state.imagePosition.dWidth / props.image.width;
|
||||
|
||||
// force the original ratio to be respected
|
||||
state.imagePosition.dHeight = props.image.height * state.imagePosition.scale;
|
||||
|
||||
// Don't let images bigger than the canvas be zoomed in more than 1:1 scale
|
||||
// Meaning only let images smaller than the canvas to have a scale > 1
|
||||
if (!state.isImageSmallerThanCanvas && state.imagePosition.scale > 1) {
|
||||
state.imagePosition.scale = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
if (state.canvasRect === null) return;
|
||||
state.rect.w = 0;
|
||||
state.rect.h = 0;
|
||||
state.selectedText = "";
|
||||
}
|
||||
|
||||
function updateMousePos<T extends MouseEvent>(event: T) {
|
||||
if (state.canvas === null) return;
|
||||
state.canvasRect = state.canvas.getBoundingClientRect();
|
||||
state.mouse.current = {
|
||||
x: event.clientX - state.canvasRect.left,
|
||||
y: event.clientY - state.canvasRect.top,
|
||||
};
|
||||
}
|
||||
|
||||
function isMouseInRect(mouse: Mouse, rect: CanvasRect) {
|
||||
if (state.canvasRect === null) return;
|
||||
const correctRect = correctRectCoordinates(rect);
|
||||
|
||||
return (
|
||||
mouse.current.x > correctRect.startX &&
|
||||
mouse.current.x < correctRect.startX + correctRect.w &&
|
||||
mouse.current.y > correctRect.startY &&
|
||||
mouse.current.y < correctRect.startY + correctRect.h
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rectangle coordinates with positive dimensions
|
||||
* @param rect A rectangle
|
||||
* @returns An equivalent rectangle with width and height > 0
|
||||
*/
|
||||
function correctRectCoordinates(rect: CanvasRect) {
|
||||
if (rect.w < 0) {
|
||||
rect.startX = rect.startX + rect.w;
|
||||
rect.w = -rect.w;
|
||||
}
|
||||
|
||||
if (rect.h < 0) {
|
||||
rect.startY = rect.startY + rect.h;
|
||||
rect.h = -rect.h;
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
function drawWordBoxesOnCanvas(tsv: OcrTsvResponse[]) {
|
||||
if (state.canvasRect === null || state.canvas === null || state.ctx === null) return;
|
||||
|
||||
state.ctx.fillStyle = "rgb(255, 255, 255, 0.3)";
|
||||
tsv
|
||||
.filter((element) => element.level === 5)
|
||||
.forEach((element) => {
|
||||
if (state.canvasRect === null || state.canvas === null || state.ctx === null) return;
|
||||
state.ctx.fillRect(
|
||||
element.left * state.imagePosition.scale,
|
||||
element.top * state.imagePosition.scale,
|
||||
element.width * state.imagePosition.scale,
|
||||
element.height * state.imagePosition.scale
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Event emitters
|
||||
const updateRecipe = function () {
|
||||
context.emit("update-recipe");
|
||||
};
|
||||
|
||||
const closeEditor = function () {
|
||||
context.emit("close-editor");
|
||||
};
|
||||
|
||||
// TOOLBAR STUFF
|
||||
|
||||
const { $globals, i18n } = useContext();
|
||||
|
||||
const toolbarIcons = ref<ToolbarIcons<CanvasModes | SelectedTextSplitModes>>([
|
||||
{
|
||||
sectionTitle: "Toolbar",
|
||||
eventHandler: switchCanvasMode,
|
||||
highlight: state.canvasMode,
|
||||
icons: [
|
||||
{
|
||||
name: "selection",
|
||||
icon: $globals.icons.selectMode,
|
||||
tooltip: i18n.tc("ocr-editor.selection-mode"),
|
||||
},
|
||||
{
|
||||
name: "panAndZoom",
|
||||
icon: $globals.icons.panAndZoom,
|
||||
tooltip: i18n.tc("ocr-editor.pan-and-zoom-picture"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionTitle: i18n.tc("ocr-editor.split-text"),
|
||||
eventHandler: switchSplitTextMode,
|
||||
highlight: state.selectedTextSplitMode,
|
||||
icons: [
|
||||
{
|
||||
name: "lineNum",
|
||||
icon: $globals.icons.preserveLines,
|
||||
tooltip: i18n.tc("ocr-editor.preserve-line-breaks"),
|
||||
},
|
||||
{
|
||||
name: "blockNum",
|
||||
icon: $globals.icons.preserveBlocks,
|
||||
tooltip: i18n.tc("ocr-editor.split-by-block"),
|
||||
},
|
||||
{
|
||||
name: "flatten",
|
||||
icon: $globals.icons.flatten,
|
||||
tooltip: i18n.tc("ocr-editor.flatten"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function switchCanvasMode(mode: CanvasModes) {
|
||||
if (state.canvasRect === null || state.canvas === null) return;
|
||||
state.canvasMode = mode;
|
||||
toolbarIcons.value[0].highlight = mode;
|
||||
if (mode === "panAndZoom") {
|
||||
state.canvas.style.cursor = "pointer";
|
||||
} else {
|
||||
state.canvas.style.cursor = "default";
|
||||
}
|
||||
}
|
||||
|
||||
function switchSplitTextMode(mode: SelectedTextSplitModes) {
|
||||
if (state.canvasRect === null) return;
|
||||
state.selectedTextSplitMode = mode;
|
||||
toolbarIcons.value[1].highlight = mode;
|
||||
state.selectedText = getWordsInSelection(props.tsv, state.rect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Using rectangle coordinates, filters the tsv to get text elements contained
|
||||
* inside the rectangle
|
||||
* Additionaly adds newlines depending on the current "text split" mode
|
||||
* @param tsv An Object containing tesseracts tsv fields
|
||||
* @param rect Coordinates of a rectangle
|
||||
* @returns Text from tsv contained in the rectangle
|
||||
*/
|
||||
function getWordsInSelection(tsv: OcrTsvResponse[], rect: CanvasRect) {
|
||||
const correctedRect = correctRectCoordinates(rect);
|
||||
|
||||
return tsv
|
||||
.filter(
|
||||
(element) =>
|
||||
element.level === 5 &&
|
||||
correctedRect.startY - state.imagePosition.dy < element.top * state.imagePosition.scale &&
|
||||
correctedRect.startX - state.imagePosition.dx < element.left * state.imagePosition.scale &&
|
||||
correctedRect.startX + correctedRect.w >
|
||||
(element.left + element.width) * state.imagePosition.scale + state.imagePosition.dx &&
|
||||
correctedRect.startY + correctedRect.h >
|
||||
(element.top + element.height) * state.imagePosition.scale + state.imagePosition.dy
|
||||
)
|
||||
.map((element, index, array) => {
|
||||
let separator = " ";
|
||||
if (
|
||||
state.selectedTextSplitMode !== "flatten" &&
|
||||
index !== array.length - 1 &&
|
||||
element[state.selectedTextSplitMode] !== array[index + 1][state.selectedTextSplitMode]
|
||||
) {
|
||||
separator = "\n";
|
||||
}
|
||||
return element.text + separator;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
handleMouseDown,
|
||||
handleMouseUp,
|
||||
handleMouseMove,
|
||||
handleMouseScroll,
|
||||
toolbarIcons,
|
||||
updateRecipe,
|
||||
closeEditor,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-app-bar dense dark color="primary" class="mb-2">
|
||||
<v-icon large left>
|
||||
{{ $globals.icons.help }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> Help </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-text>
|
||||
<h1>Mouse modes</h1>
|
||||
<v-divider class="mb-2 mt-1" />
|
||||
<h2 class="my-2">
|
||||
<v-icon> {{ $globals.icons.selectMode }} </v-icon>{{ $t("ocr-editor.help.selection-mode") }}
|
||||
</h2>
|
||||
<p class="my-1">{{ $t("ocr-editor.help.selection-mode") }}</p>
|
||||
<ol>
|
||||
<li>{{ $t("ocr-editor.help.selection-mode-steps.draw") }}</li>
|
||||
<li>{{ $t("ocr-editor.help.selection-mode-steps.click") }}</li>
|
||||
<li>{{ $t("ocr-editor.help.selection-mode-steps.result") }}</li>
|
||||
</ol>
|
||||
<h2 class="my-2">
|
||||
<v-icon> {{ $globals.icons.panAndZoom }} </v-icon>{{ $t("ocr-editor.help.pan-and-zoom-mode") }}
|
||||
</h2>
|
||||
{{ $t("ocr-editor.help.pan-and-zoom-desc") }}
|
||||
<h1 class="mt-5">{{ $t("ocr-editor.help.split-text-mode") }}</h1>
|
||||
<v-divider class="mb-2 mt-1" />
|
||||
<h2 class="my-2">
|
||||
<v-icon> {{ $globals.icons.preserveLines }} </v-icon>
|
||||
{{ $t("ocr-editor.help.split-modes.line-mode") }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ $t("ocr-editor.help.split-modes.line-mode-desc") }}
|
||||
</p>
|
||||
<h2 class="my-2">
|
||||
<v-icon> {{ $globals.icons.preserveBlocks }} </v-icon>
|
||||
{{ $t("ocr-editor.help.split-modes.block-mode") }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ $t("ocr-editor.help.split-modes.block-mode-desc") }}
|
||||
</p>
|
||||
<h2 class="my-2">
|
||||
<v-icon> {{ $globals.icons.flatten }} </v-icon> {{ $t("ocr-editor.help.split-modes.flat-mode") }}
|
||||
</h2>
|
||||
<p>{{ $t("ocr-editor.help.split-modes.flat-mode-desc") }}</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({});
|
||||
</script>
|
||||
@@ -1,3 +0,0 @@
|
||||
import RecipeOcrEditorPage from "./RecipeOcrEditorPage.vue";
|
||||
|
||||
export default RecipeOcrEditorPage;
|
||||