Compare commits

..

127 Commits

Author SHA1 Message Date
mealie-commit-bot[bot]
a7a08b6b11 chore: bump version to v3.15.2 2026-04-16 03:43:59 +00:00
Hayden
bd296c3eaf fix: path traversal vulnerabilities in migration image imports and media routes (#7474) 2026-04-16 03:24:50 +00:00
renovate[bot]
8aa016e57b fix(deps): update dependency python-multipart to v0.0.26 [security] (#7473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 01:39:09 +00:00
renovate[bot]
480574eb3d fix(deps): update dependency python-multipart to v0.0.25 (#7470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 13:30:07 +00:00
mealie-commit-bot[bot]
0573d6fc9c chore: bump version to v3.15.1 2026-04-14 17:37:56 +00:00
Xenov
f8d08c6785 fix: seed labels before foods in setup wizard to prevent race condition (#7429)
Co-authored-by: xenov <redacted>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-14 16:24:32 +00:00
renovate[bot]
e6368174f0 chore(deps): update dependency ruff to v0.15.10 (#7464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 16:22:02 +00:00
renovate[bot]
2252875050 fix(deps): update dependency lxml to v6.0.3 (#7465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 16:21:30 +00:00
DeepReef11
54c62ec491 fix: eliminate white flash on page load for dark theme users (#7358)
Co-authored-by: Docker User <user@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-14 16:18:00 +00:00
Brian Choromanski
af79a751fb fix: Admin settings checkboxes not updating (#7462) 2026-04-14 15:58:07 +00:00
renovate[bot]
6e2c849412 fix(deps): update dependency openai to v2.31.0 (#7460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 22:53:42 +00:00
mealie-commit-bot[bot]
76dbf4df45 chore: bump version to v3.15.0 2026-04-13 17:25:28 +00:00
Michael Genson
4e5a2f9fb5 fix: Search layout fixes (#7459) 2026-04-13 10:56:19 -05:00
DeepReef11
daa0b9728b fix: prevent stale SPA shell after container rebuild (#7344)
Co-authored-by: Docker User <user@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-13 10:46:28 -05:00
renovate[bot]
0986ce2ca1 chore(deps): update dependency types-pyyaml to v6.0.12.20260408 (#7454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 05:44:04 +00:00
renovate[bot]
4972143004 chore(deps): update dependency types-requests to v2.33.0.20260408 (#7455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 05:20:19 +00:00
renovate[bot]
499c42a52a chore(deps): update dependency types-python-dateutil to v2.9.0.20260408 (#7453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 05:07:28 +00:00
renovate[bot]
92cf84f615 chore(deps): update dependency pytest to v9.0.3 (#7452)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 21:58:47 +00:00
renovate[bot]
54511779a2 fix(deps): update dependency rapidfuzz to v3.14.5 (#7450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 13:00:19 +00:00
renovate[bot]
b72ccb8d29 chore(deps): update dependency rich to v15 (#7448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 09:18:36 +00:00
mealie-actions[bot]
9fb3bce792 chore(l10n): Crowdin locale sync (#7447)
Co-authored-by: GitHub Action <action@github.com>
2026-04-12 03:08:27 +00:00
Michael Genson
32141187ba fix: Update frontend refs (#7444) 2026-04-11 11:27:52 -05:00
renovate[bot]
30014f53de fix(deps): update dependency uvicorn to v0.44.0 (#7443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 13:26:30 +00:00
Michael Genson
d2b0681dbb feat: Announcements (#7431)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2026-04-11 08:26:14 -05:00
Brian Choromanski
306f2dcfc6 docs: Updated homepage footer (#7440) 2026-04-11 03:03:23 +00:00
Brian Choromanski
0fb5d31a22 fix: Unchecking took in recipe (#7439)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-11 01:04:30 +00:00
renovate[bot]
1d5b263262 fix(deps): update dependency python-multipart to v0.0.24 (#7438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 22:11:58 +00:00
mealie-actions[bot]
731e3aef37 chore(auto): Update pre-commit hooks (#7364)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-04-10 08:55:50 +00:00
renovate[bot]
fb04602a8e chore(deps): update dependency axios to v1.15.0 [security] (#7436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 00:51:31 +00:00
Kuchenpirat
157b8d2937 chore: upgrade to vuetify v4 (#7432) 2026-04-10 00:39:05 +00:00
Arsène Reymond
6b28bb8eb0 fix: BaseDialog padding (#7428) 2026-04-09 13:53:02 +00:00
renovate[bot]
124d10963e fix(deps): update dependency uvicorn to v0.43.0 (#7430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 19:43:07 +00:00
renovate[bot]
7c2ec93d13 fix(deps): update dependency sqlalchemy to v2.0.49 (#7427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 19:14:31 +00:00
Kuchenpirat
d3e41582ae chore: Nuxt 4 upgrade (#7426) 2026-04-08 15:25:41 +00:00
renovate[bot]
70a251a331 chore(deps): update dependency mypy to v1.20.0 (#7399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:33:49 +00:00
renovate[bot]
4fd224ade7 chore(deps): update dependency types-python-dateutil to v2.9.0.20260402 (#7411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:33:18 +00:00
renovate[bot]
89694f7e54 fix(deps): update dependency requests to v2.33.1 (#7394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:32:12 +00:00
Hayden
7a60ad2227 chore(l10n): New Crowdin updates (#7425) 2026-04-08 06:09:28 +00:00
renovate[bot]
eb71b962bc chore(deps): update node.js to 80fc934 (#7421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 00:42:19 +00:00
Brian Choromanski
fe491bbe56 fix: Support for enter key when creating household (#7419)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-08 00:40:58 +00:00
Michael Genson
27f2dc1bf6 dev: Fix autolabel permission to only use pull_request_target (#7422) 2026-04-07 19:28:28 -05:00
renovate[bot]
b3ea916192 chore(deps): update dependency ruff to v0.15.9 (#7418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 21:52:30 +00:00
renovate[bot]
240d681057 chore(deps): update node.js to df0c595 (#7415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 12:56:02 +00:00
renovate[bot]
6932c9ef2d chore(deps): update node.js to 2ef5213 (#7414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 09:58:35 +00:00
Hayden
1438ba82d5 chore(l10n): New Crowdin updates (#7413) 2026-04-07 05:27:43 +00:00
renovate[bot]
7a5032bf23 chore(deps): update dependency types-requests to v2.33.0.20260402 (#7412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 05:07:26 +00:00
renovate[bot]
c3d1cf4c37 chore(deps): update dependency vite to v7.3.2 [security] (#7410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 21:24:05 +00:00
renovate[bot]
135a9ca684 fix(deps): update dependency pillow to v12.2.0 (#7407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 17:06:36 +00:00
renovate[bot]
ef90515ae8 fix(deps): update dependency fastapi to v0.135.3 (#7406)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 17:06:28 +00:00
Hayden
a853e445ac chore(l10n): New Crowdin updates (#7408) 2026-04-06 17:05:15 +00:00
Hayden
7dad3777d3 chore(l10n): New Crowdin updates (#7400) 2026-04-05 18:36:31 +00:00
renovate[bot]
a6ab0befba fix(deps): update dependency orjson to v3.11.8 (#7398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-05 16:49:03 +00:00
mealie-actions[bot]
2c6997a601 chore(l10n): Crowdin locale sync (#7397)
Co-authored-by: GitHub Action <action@github.com>
2026-04-05 03:07:25 +00:00
Brian Choromanski
9c3b94c019 dev: Bumped gh actions to support node 24 (#7392)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-04 14:13:09 +00:00
Hayden
5ce3099cfa chore(l10n): New Crowdin updates (#7393) 2026-04-04 04:49:26 +00:00
Brian Choromanski
0d1349cc7f fix: Reverted references to categories on the recipe actions data management page (#7391) 2026-04-04 04:05:16 +00:00
Brian Choromanski
7e7d1622dd fix: Display issues with data management pages on mobile (#7389) 2026-04-04 01:08:51 +00:00
renovate[bot]
d24aa7f65a fix(deps): update dependency tzdata to v2026 (#7388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 13:39:15 +00:00
Brian Choromanski
5172571b2e dev: Add linting rules to vscode settings (#7386) 2026-04-03 05:21:07 +00:00
Brian Choromanski
bb278aac35 feat: Added scroll to top on all pages that have recipeCardSection (#7384) 2026-04-03 04:11:16 +00:00
Hayden
4ee97e5348 chore(l10n): New Crowdin updates (#7380) 2026-04-02 09:29:14 +00:00
Hayden
bac00a30a4 chore(l10n): New Crowdin updates (#7379) 2026-04-01 21:23:55 +00:00
Hayden
1123ec848d chore(l10n): New Crowdin updates (#7375) 2026-04-01 08:25:01 +00:00
renovate[bot]
0f767f2e25 chore(deps): update dependency types-requests to v2.33.0.20260327 (#7374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 06:05:21 +00:00
Brian Choromanski
058dbdc9d6 fix: Back button sets view to where you left page (#7370)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-03-31 23:14:32 -05:00
renovate[bot]
94cf825a28 chore(deps): update dependency ruff to v0.15.8 (#7373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 20:44:24 +00:00
Hayden
6ee69b7b3e chore(l10n): New Crowdin updates (#7372) 2026-03-31 20:20:11 +00:00
Arsène Reymond
f36c892bb7 feat: improve BaseDialog on mobile and use it globally (#7076)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-31 12:34:44 +00:00
Hayden
f6305b785e chore(l10n): New Crowdin updates (#7371) 2026-03-31 08:22:12 +00:00
renovate[bot]
1512a9e555 fix(deps): update dependency openai to v2.30.0 (#7369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 01:54:02 +00:00
Hayden
690b6aa57b chore(l10n): New Crowdin updates (#7367) 2026-03-30 19:55:44 +00:00
Hayden
c57af78f8f chore(l10n): New Crowdin updates (#7365) 2026-03-30 07:43:30 +00:00
Hayden
0775156aeb chore(l10n): New Crowdin updates (#7362) 2026-03-29 18:00:49 +00:00
Hayden
3356ebc0b8 chore(l10n): New Crowdin updates (#7360) 2026-03-29 06:05:18 +00:00
renovate[bot]
1b59073dc4 chore(deps): update dependency types-requests to v2.32.4.20260324 (#7359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 05:30:15 +00:00
mealie-actions[bot]
ea3856b620 chore(l10n): Crowdin locale sync (#7357)
Co-authored-by: GitHub Action <action@github.com>
2026-03-29 03:07:40 +00:00
Michael Genson
4f5d1cf1b4 fix: Disable SSL verify when scraping sites for recipe data (#7356) 2026-03-28 20:13:23 -05:00
Hayden
626dee9500 chore(l10n): New Crowdin updates (#7351) 2026-03-28 18:36:42 +00:00
renovate[bot]
1162c700cd fix(deps): update dependency fastapi to v0.135.2 (#7349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 17:19:44 +00:00
Hayden
7b3651d138 chore(l10n): New Crowdin updates (#7346) 2026-03-28 08:25:47 +00:00
renovate[bot]
1a3676c36d chore(deps): update dependency types-python-dateutil to v2.9.0.20260323 (#7345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 05:19:51 +00:00
Brian Choromanski
17d9be3b15 fix: Updated commit hash for opencontainers revision (#7340)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-27 21:51:53 +00:00
renovate[bot]
7a8a511d48 chore(deps): update dependency node-forge to v1.4.0 [security] (#7338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 18:30:03 +00:00
mealie-commit-bot[bot]
085ecbaae3 chore: bump version to v3.14.0 2026-03-27 18:17:49 +00:00
Tom Strange
453d40dab1 feat: Pass user defined units as custom units to parse_ingredient function. (#7334)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-27 16:48:03 +00:00
Michael Genson
fc8b1f3719 fix: Relax URL validation (#7336) 2026-03-27 10:33:33 -05:00
harshitlarl
c029a639fb fix: preserve stored recipe slugs during hydration (#7294)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-03-27 10:08:48 -05:00
Michael Genson
63c549ae5c chore: Resolve startup warnings (#7335) 2026-03-27 09:54:46 -05:00
Michael Genson
b1a846fe62 docs: Add missing OPENAI_AUDIO_MODEL env var to docs (#7333) 2026-03-27 08:55:22 -05:00
Hayden
8545cf0c1c chore(l10n): New Crowdin updates (#7332) 2026-03-27 05:45:58 +00:00
Michael Genson
7c5913b012 fix: HTML/JSON import failing (#7330) 2026-03-26 23:12:25 -05:00
Gabriel Barbosa Soares
4dd8d836e1 fix: unparsed ingredients poorly formatted when fed to NLP parser (#7086)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-03-26 15:19:10 -05:00
Sebastian
449e3baa07 fix: restore recipe notes during JSON import (#7017)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-26 15:04:59 -05:00
francisferrell
e52a887e30 fix: publish all mealplan create, update, and delete events (#7015)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-26 14:52:55 -05:00
renovate[bot]
910ac4c81f fix(deps): update dependency apprise to v1.9.9 (#7327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 19:39:55 +00:00
Michael Genson
52ad02aad8 dev: Update PR template (#7326) 2026-03-26 14:27:25 -05:00
Rohan Shah
93d51a2fdb fix: Removing a recipe ingredient doesn't remove its links to steps (#6896)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-26 14:25:34 -05:00
Arnas Savickas
41c3f1fced feat: Add days in the past selector on meal planner (#6857)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-03-26 14:09:52 -05:00
Michael Genson
9f47f38176 fix: Fix create token API page (#7325) 2026-03-26 10:42:44 -05:00
Hayden
a8142a08a1 chore(l10n): New Crowdin updates (#7323) 2026-03-26 05:31:33 +00:00
Brian Choromanski
1ede524d90 feat: Clarification of site settings (#7321) 2026-03-26 04:44:19 +00:00
renovate[bot]
ab3eb6fec2 fix(deps): update dependency pint to v0.25.3 (#7314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 00:26:00 +00:00
renovate[bot]
d64dcab9bd fix(deps): update dependency requests to v2.33.0 [security] (#7319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 18:25:17 +00:00
renovate[bot]
f9ff29dffc fix(deps): update dependency ingredient-parser-nlp to v2.6.0 (#7318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 18:10:55 +00:00
renovate[bot]
47794089da chore(deps): update node.js to bb20cf7 (#7317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 18:10:19 +00:00
Hayden
b0328ad926 chore(l10n): New Crowdin updates (#7315) 2026-03-25 01:52:29 +00:00
Kuchenpirat
18b3c4beab chore: migrate remaining pages to script setup (#7310) 2026-03-24 15:07:08 +00:00
Hayden
27cb585c80 chore(l10n): New Crowdin updates (#7309) 2026-03-24 14:19:52 +00:00
Hayden
f9ddfa94d4 chore(l10n): New Crowdin updates (#7304) 2026-03-24 01:18:21 +00:00
Kuchenpirat
5ab6e98f9e chore: script setup components (#7299) 2026-03-23 20:18:25 +00:00
mealie-actions[bot]
3ad2d9155d chore(auto): Update pre-commit hooks (#7298)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-03-23 12:48:10 +00:00
Hayden
6278698ce5 chore(l10n): New Crowdin updates (#7297) 2026-03-23 12:47:16 +00:00
Brian Choromanski
3413c23f16 fix: Release Commit (#7274) 2026-03-22 16:35:06 +00:00
mealie-actions[bot]
2ff2f22060 chore(l10n): Crowdin locale sync (#7293)
Co-authored-by: GitHub Action <action@github.com>
2026-03-22 03:06:22 +00:00
Hayden
0f8ccdaade chore(l10n): New Crowdin updates (#7292) 2026-03-21 21:14:49 +00:00
Hayden
825c707035 chore(l10n): New Crowdin updates (#7289) 2026-03-21 09:16:55 +00:00
Hayden
9b9a767b00 chore(l10n): New Crowdin updates (#7282) 2026-03-20 04:16:52 +00:00
Hayden
c8793c474a chore: bump l10n auto-merge line limit from 400 to 6000 (#7279) 2026-03-19 19:22:29 +00:00
renovate[bot]
b64e27b24b chore(deps): update dependency mkdocs-material to v9.7.6 (#7278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-03-19 18:50:03 +00:00
renovate[bot]
94cd6e89cb chore(deps): update dependency ruff to v0.15.7 (#7281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-03-19 18:45:47 +00:00
Hayden
4c02724087 feat: Auto-merge Renovate dependency updates (#7280) 2026-03-19 18:22:59 +00:00
Hayden
33c73feb1c chore(l10n): New Crowdin updates (#7277) 2026-03-19 15:45:17 +00:00
dswd
002a7e3741 fix: Use latest python image as base (#7276) 2026-03-19 13:12:46 +00:00
mealie-commit-bot[bot]
be4f71e5df chore: bump version to v3.13.1 2026-03-18 22:46:46 +00:00
Brian Choromanski
78ff4bb875 fix: Updated workflows to checkout on commit of commit-version-bump (#7217)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-18 17:44:39 -05:00
Sim
26924ab054 fix: #6802 prevent 500 internal server error when patching recipe tags (#6803)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-03-18 13:45:51 -05:00
Hayden
c533da1c21 chore(l10n): New Crowdin updates (#7271) 2026-03-18 14:16:33 +00:00
723 changed files with 39383 additions and 39788 deletions

View File

@@ -2,7 +2,7 @@
## Project Overview
Mealie is a self-hosted recipe manager, meal planner, and shopping list application with a FastAPI backend (Python 3.12) and Nuxt 3 frontend (Vue 3 + TypeScript). It uses SQLAlchemy ORM with support for SQLite and PostgreSQL databases.
Mealie is a self-hosted recipe manager, meal planner, and shopping list application with a FastAPI backend (Python 3.12) and Nuxt 4 frontend (Vue 3 + TypeScript). It uses SQLAlchemy ORM with support for SQLite and PostgreSQL databases.
**Development vs Production:**
- **Development:** Frontend (port 3000) and backend (port 9000) run as separate processes
@@ -28,7 +28,7 @@ Mealie is a self-hosted recipe manager, meal planner, and shopping list applicat
**Schemas & Type Generation:**
- Pydantic schemas in `mealie/schema/` with strict separation: `*In`, `*Out`, `*Create`, `*Update` suffixes
- Auto-exported from submodules via `__init__.py` files (generated by `task dev:generate`)
- TypeScript types auto-generated from Pydantic schemas - **never manually edit** `frontend/lib/api/types/`
- TypeScript types auto-generated from Pydantic schemas - **never manually edit** `frontend/app/lib/api/types/`
**Database & Sessions:**
- Session management via `Depends(generate_session)` in FastAPI routes
@@ -45,13 +45,13 @@ Mealie is a self-hosted recipe manager, meal planner, and shopping list applicat
- **Page Components** (`components/` with page prefix): Last resort for breaking up complex pages
**API Client Pattern:**
- API clients in `frontend/lib/api/` extend `BaseAPI`, `BaseCRUDAPI`, or `BaseCRUDAPIReadOnly`
- Types imported from auto-generated `frontend/lib/api/types/` (DO NOT EDIT MANUALLY)
- Composables in `frontend/composables/` for shared state and API logic (e.g., `use-mealie-auth.ts`)
- API clients in `frontend/app/lib/api/` extend `BaseAPI`, `BaseCRUDAPI`, or `BaseCRUDAPIReadOnly`
- Types imported from auto-generated `frontend/app/lib/api/types/` (DO NOT EDIT MANUALLY)
- Composables in `frontend/app/composables/` for shared state and API logic (e.g., `use-mealie-auth.ts`)
- Use `useAuthBackend()` for authentication state, `useMealieAuth()` for user management
**State Management:**
- Nuxt 3 composables for state (no Vuex)
- Nuxt 4 composables for state (no Vuex)
- Auth state via `use-mealie-auth.ts` composable
- Prefer composables over global state stores
@@ -148,7 +148,7 @@ task docker:prod # Build and run production Docker compose
### Cross-Cutting Concerns
1. **Code generation is source of truth:** After Pydantic schema changes, run `task dev:generate` to update:
- TypeScript types (`frontend/lib/api/types/`)
- TypeScript types (`frontend/app/lib/api/types/`)
- Schema exports (`mealie/schema/*/__init__.py`)
- Test data paths and routes
@@ -189,7 +189,7 @@ task docker:prod # Build and run production Docker compose
- For frontend, does TypeScript code pass strict type checking?
**Generated Files:**
- Verify `frontend/lib/api/types/` files weren't manually edited (they're auto-generated)
- Verify `frontend/app/lib/api/types/` files weren't manually edited (they're auto-generated)
- Check that `mealie/schema/*/__init__.py` exports match actual schema files (auto-generated)
- If schemas changed, confirm generated files were updated via `task dev:generate`
@@ -216,7 +216,7 @@ task docker:prod # Build and run production Docker compose
## Common Gotchas
- **Don't manually edit generated files:** `frontend/lib/api/types/`, schema `__init__.py` files
- **Don't manually edit generated files:** `frontend/app/lib/api/types/`, schema `__init__.py` files
- **Repository context:** Repos are group/household-scoped - passing wrong IDs causes 404s
- **Session handling:** Don't create sessions manually, use dependency injection or `session_context()`
- **Schema changes require codegen:** After changing Pydantic models, run `task dev:generate`
@@ -229,7 +229,7 @@ task docker:prod # Build and run production Docker compose
- `Taskfile.yml` - All development commands and workflows
- `mealie/routes/_base/base_controllers.py` - Controller base classes and patterns
- `mealie/repos/repository_factory.py` - Repository factory and available repos
- `frontend/lib/api/base/base-clients.ts` - API client base classes
- `frontend/app/lib/api/base/base-clients.ts` - API client base classes
- `tests/conftest.py` - Test fixtures and setup
- `dev/code-generation/main.py` - Code generation entry point

View File

@@ -8,11 +8,11 @@
- `chore: `
- `dev:`
If a section of the PR template does not apply to this PR, then delete that section.
If a section of the PR template does not apply to this PR, and is not marked as "required", then delete that section.
PLEASE READ:
-------------------------
Mealie is moving to a regular, automatic release schedule. This means that all PRs should be in a
Mealie uses 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.
@@ -28,8 +28,6 @@ _(REQUIRED)_
What goal is this change working towards?
Provide a bullet pointed summary of how each file was changed.
Briefly explain any decisions you made with respect to the changes.
Include anything here that you didn't include in *Release Notes*
above, such as changes to CI or changes to internal methods.
If there is a UI component to the change, please include before/after images.
-->
@@ -43,6 +41,8 @@ If this PR fixes one of more issues, list them here.
One per line, like so:
Fixes #123
Fixes #39
Be sure to include the word "fixes" otherwise the associated issue will not be closed.
-->
## Special notes for your reviewer:
@@ -67,5 +67,6 @@ _(fill-in or delete this section)_
_(REQUIRED)_
<!--
Describe to which degree an LLM was used in creating this pull request.
Describe to which degree an LLM was used in creating this pull request. Failure to accurately disclose LLM usage may result in
review delays or closure of your PR.
-->

View File

@@ -0,0 +1,99 @@
name: Auto-merge dependency PRs
on:
pull_request:
types: [opened, synchronize, labeled]
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'dependencies')
steps:
- name: Validate PR author
env:
AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
if [[ "$AUTHOR" != "renovate[bot]" ]]; then
echo "::error::PR author must be renovate[bot] for auto-merge (got: $AUTHOR)"
exit 1
fi
echo "Author validated: $AUTHOR"
- name: Reject major updates
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
if echo "$TITLE" | grep -qiE '(major|breaking)'; then
echo "::error::Major/breaking updates require manual review"
exit 1
fi
echo "PR title does not indicate a major update"
- name: Validate file paths
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
FILES=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path')
for file in $FILES; do
if [[ "$file" == "pyproject.toml" ]] || \
[[ "$file" == "uv.lock" ]] || \
[[ "$file" == "frontend/package.json" ]] || \
[[ "$file" == "frontend/yarn.lock" ]] || \
[[ "$file" =~ ^docker/ ]]; then
continue
fi
echo "::error::Unexpected file path: $file"
echo "Only dependency and lock files are allowed for auto-merge"
exit 1
done
echo "All files are in allowed paths"
- name: Approve PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
APPROVED=$(gh pr view "$PR_NUMBER" \
--repo "$REPO" \
--json reviews \
--jq '.reviews[] | select(.state == "APPROVED") | .id' \
| wc -l)
if [ "$APPROVED" -gt 0 ]; then
echo "PR already approved"
exit 0
fi
gh pr review "$PR_NUMBER" \
--repo "$REPO" \
--approve \
--body "Auto-approved: dependency update from Renovate with valid file paths"
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Enable auto-merge
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
gh pr merge "$PR_NUMBER" \
--repo "$REPO" \
--auto \
--squash

View File

@@ -40,8 +40,8 @@ jobs:
echo "PR changes: +$ADDITIONS -$DELETIONS (total: $TOTAL lines)"
if [ "$TOTAL" -gt 400 ]; then
echo "::error::PR exceeds 400 line change limit ($TOTAL lines)"
if [ "$TOTAL" -gt 6000 ]; then
echo "::error::PR exceeds 6000 line change limit ($TOTAL lines)"
exit 1
fi
@@ -55,8 +55,8 @@ jobs:
for file in $FILES; do
# Check if file matches any allowed path
if [[ "$file" == "frontend/composables/use-locales/available-locales.ts" ]] || \
[[ "$file" =~ ^frontend/lang/ ]] || \
if [[ "$file" == "frontend/app/composables/use-locales/available-locales.ts" ]] || \
[[ "$file" =~ ^frontend/app/lang/ ]] || \
[[ "$file" =~ ^mealie/lang/ ]] || \
[[ "$file" =~ ^mealie/repos/seed/resources/[^/]+/locales/ ]]; then
continue
@@ -65,8 +65,8 @@ jobs:
# File doesn't match allowed paths
echo "::error::Invalid file path: $file"
echo "Only the following paths are allowed:"
echo " - frontend/composables/use-locales/available-locales.ts"
echo " - frontend/lang/"
echo " - frontend/app/composables/use-locales/available-locales.ts"
echo " - frontend/app/lang/"
echo " - mealie/lang/"
echo " - mealie/repos/seed/resources/*/locales/"
exit 1

View File

@@ -6,6 +6,9 @@ on:
tag:
required: true
type: string
ref:
required: false
type: string
jobs:
build-frontend:
@@ -14,10 +17,12 @@ jobs:
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.sha }}
- name: Setup node env 🏗
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v6
with:
node-version: 22
check-latest: true
@@ -27,7 +32,7 @@ jobs:
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache node_modules 📦
uses: actions/cache@v4
uses: actions/cache@v5
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 }}
@@ -44,7 +49,7 @@ jobs:
working-directory: "frontend"
- name: Archive built frontend
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: frontend/dist
@@ -63,10 +68,12 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -74,7 +81,7 @@ jobs:
run: pip install uv
- name: Retrieve built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: frontend-dist
path: mealie/frontend
@@ -90,7 +97,7 @@ jobs:
task py:package
- name: Archive built package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: backend-dist
path: dist

View File

@@ -44,11 +44,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -62,7 +62,7 @@ jobs:
# 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@v3
uses: github/codeql-action/autobuild@v4
# 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
@@ -75,6 +75,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -21,7 +21,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v4

View File

@@ -10,21 +10,21 @@ jobs:
run:
working-directory: ./tests/e2e
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: 'yarn'
cache-dependency-path: ./tests/e2e/yarn.lock
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Retrieve Python package
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: backend-dist
path: dist
- name: Build Image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v7
with:
file: ./docker/Dockerfile
context: .

View File

@@ -23,12 +23,12 @@ jobs:
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -37,7 +37,7 @@ jobs:
- name: Load cached venv
id: cached-python-dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: .venv
key: venv-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}

View File

@@ -11,7 +11,7 @@ jobs:
fail-fast: true
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build Dockerfile
run: |
@@ -28,6 +28,6 @@ jobs:
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: "trivy-results.sarif"

View File

@@ -9,6 +9,9 @@ on:
tags:
required: false
type: string
ref:
required: false
type: string
secrets:
DOCKERHUB_USERNAME:
required: true
@@ -20,17 +23,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.sha }}
- name: Log in to the Container registry (ghcr.io)
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (dockerhub)
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -39,7 +44,7 @@ jobs:
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
hkotel/mealie
@@ -47,9 +52,10 @@ jobs:
# Overwrite the image.version label with our tag
labels: |
org.opencontainers.image.version=${{ inputs.tag }}
org.opencontainers.image.revision=${{ inputs.ref || github.sha }}
- name: Retrieve Python package
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: backend-dist
path: dist
@@ -70,4 +76,4 @@ jobs:
${{ inputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
COMMIT=${{ github.sha }}
COMMIT=${{ inputs.ref || github.sha }}

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
# https://github.com/amannn/action-semantic-pull-request
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -5,26 +5,28 @@ on:
push:
branches:
- mealie-next
# pull_request event is required for autolabeler
pull_request:
types: [opened, labeled, unlabeled, reopened, synchronize]
# pull_request_target event is required for autolabeler to support PRs from forks
pull_request_target:
types: [opened, labeled, unlabeled, reopened, synchronize]
workflow_dispatch:
jobs:
update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
name: ✏️ Draft release
draft_release:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: 🚀 Run Release Drafter
uses: release-drafter/release-drafter@v6.0.0
- uses: release-drafter/release-drafter@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
auto_label:
if: github.event_name == 'pull_request_target'
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter/autolabeler@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -21,7 +21,7 @@ jobs:
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout 🛎
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
@@ -60,12 +60,16 @@ jobs:
uses: ./.github/workflows/test-backend.yml
needs:
- commit-version-bump
with:
ref: ${{ needs.commit-version-bump.outputs.commit-sha }}
frontend-tests:
name: "Frontend Tests"
uses: ./.github/workflows/test-frontend.yml
needs:
- commit-version-bump
with:
ref: ${{ needs.commit-version-bump.outputs.commit-sha }}
build-package:
name: Build Package
@@ -74,6 +78,7 @@ jobs:
- commit-version-bump
with:
tag: ${{ github.event.release.tag_name }}
ref: ${{ needs.commit-version-bump.outputs.commit-sha }}
publish:
permissions:
@@ -90,7 +95,9 @@ jobs:
- backend-tests
- frontend-tests
- build-package
- commit-version-bump
with:
ref: ${{ needs.commit-version-bump.outputs.commit-sha }}
tag: ${{ github.event.release.tag_name }}
tags: |
hkotel/mealie:latest
@@ -117,7 +124,7 @@ jobs:
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout 🛎
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0

View File

@@ -13,10 +13,10 @@ jobs:
pull-requests: write
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -25,7 +25,7 @@ jobs:
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/pre-commit

View File

@@ -2,6 +2,10 @@ name: Backend Lint and Test
on:
workflow_call:
inputs:
ref:
required: false
type: string
jobs:
tests:
@@ -42,10 +46,12 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.sha }}
- name: Set up python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -54,7 +60,7 @@ jobs:
- name: Load cached venv
id: cached-python-dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: .venv
key: venv-${{ runner.os }}-${{ hashFiles('**/uv.lock') }}

View File

@@ -2,6 +2,10 @@ name: Frontend Lint and Test
on:
workflow_call:
inputs:
ref:
required: false
type: string
jobs:
lint:
@@ -9,10 +13,12 @@ jobs:
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.sha }}
- name: Setup node env 🏗
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v6
with:
node-version: 22
check-latest: true
@@ -22,7 +28,7 @@ jobs:
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache node_modules 📦
uses: actions/cache@v4
uses: actions/cache@v5
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 }}

View File

@@ -12,7 +12,7 @@ repos:
exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.6
rev: v0.15.8
hooks:
# Linter
- id: ruff-check

View File

@@ -17,6 +17,8 @@
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.formatOnSave": true,
"eslint.useFlatConfig": true,
"eslint.workingDirectories": [
@@ -30,11 +32,12 @@
"**/.svn": true,
"**/CVS": true
},
"files.insertFinalNewline": true,
"i18n-ally.enabledFrameworks": [
"vue"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": "frontend/lang/messages",
"i18n-ally.localesPaths": "frontend/app/lang/messages",
"i18n-ally.sourceLanguage": "en-US",
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.testing.autoTestDiscoverOnSaveEnabled": false,
@@ -67,6 +70,7 @@
},
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.tabSize": 4
}
}

View File

@@ -4,8 +4,8 @@ pull_request_labels: [
"l10n"
]
files:
- source: /frontend/lang/messages/en-US.json
translation: /frontend/lang/messages/%locale%.json
- source: /frontend/app/lang/messages/en-US.json
translation: /frontend/app/lang/messages/%locale%.json
- source: /mealie/lang/messages/en-US.json
translation: /mealie/lang/messages/%locale%.json
- source: /mealie/repos/seed/resources/foods/locales/en-US.json

View File

@@ -84,10 +84,10 @@ class CrowdinApi:
PROJECT_DIR = Path(__file__).parent.parent.parent
datetime_dir = PROJECT_DIR / "frontend" / "lang" / "dateTimeFormats"
locales_dir = PROJECT_DIR / "frontend" / "lang" / "messages"
datetime_dir = PROJECT_DIR / "frontend" / "app" / "lang" / "dateTimeFormats"
locales_dir = PROJECT_DIR / "frontend" / "app" / "lang" / "messages"
nuxt_config = PROJECT_DIR / "frontend" / "nuxt.config.ts"
i18n_config = PROJECT_DIR / "frontend" / "i18n.config.ts"
i18n_config = PROJECT_DIR / "frontend" / "app" / "i18n.config.ts"
reg_valid = PROJECT_DIR / "mealie" / "schema" / "_mealie" / "validators.py"
"""

View File

@@ -33,11 +33,11 @@ PROJECT_DIR = Path(__file__).parent.parent.parent
def generate_global_components_types() -> None:
destination_file = PROJECT_DIR / "frontend" / "types" / "components.d.ts"
destination_file = PROJECT_DIR / "frontend" / "app" / "types" / "components.d.ts"
component_paths = {
"global": PROJECT_DIR / "frontend" / "components" / "global",
"layout": PROJECT_DIR / "frontend" / "components" / "Layout",
"global": PROJECT_DIR / "frontend" / "app" / "components" / "global",
"layout": PROJECT_DIR / "frontend" / "app" / "components" / "Layout",
}
def render_template(template: str, data: dict) -> str | None:
@@ -182,7 +182,7 @@ def generate_typescript_types() -> None: # noqa: C901
return str_path
schema_path = PROJECT_DIR / "mealie" / "schema"
types_dir = PROJECT_DIR / "frontend" / "lib" / "api" / "types"
types_dir = PROJECT_DIR / "frontend" / "app" / "lib" / "api" / "types"
ignore_dirs = ["__pycache__", "static", "_mealie"]

View File

@@ -16,7 +16,7 @@ class CodeTemplates:
class CodeDest:
interface = PARENT / "generated" / "interface.js"
pytest_routes = PARENT / "generated" / "test_routes.py"
use_locales = PROJECT_DIR / "frontend" / "composables" / "use-locales" / "available-locales.ts"
use_locales = PROJECT_DIR / "frontend" / "app" / "composables" / "use-locales" / "available-locales.ts"
class CodeKeys:

View File

@@ -1,7 +1,7 @@
###############################################
# Frontend Build
###############################################
FROM node:24@sha256:5a593d74b632d1c6f816457477b6819760e13624455d587eef0fa418c8d0777b \
FROM node:24@sha256:80fc934952c8f1b2b4d39907af7211f8a9fff1a4c2cf673fb49099292c251cec \
AS frontend-builder
WORKDIR /frontend
@@ -21,7 +21,7 @@ RUN yarn generate
###############################################
# Base Image - Python
###############################################
FROM python:3.12-slim@sha256:2267adc248a477c1f1a852a07a5a224d42abe54c28aafa572efa157dfb001bba \
FROM python:3.12-slim@sha256:7026274c107626d7e940e0e5d6730481a4600ae95d5ca7eb532dd4180313fea9 \
AS python-base
ENV MEALIE_HOME="/app"

View File

@@ -77,7 +77,7 @@ Now you're ready to start the servers. You'll need two shells open, One for the
### Frontend
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).
We use vue-i18n package for internationalization. Translations are stored in json format located in [frontend/app/lang/messages](https://github.com/mealie-recipes/mealie/tree/mealie-next/frontend/app/lang/messages).
### Backend

View File

@@ -122,19 +122,20 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md).
For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`)
| Variables | Default | Description |
|-------------------------------------------------------------------------|:-------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| OPENAI_BASE_URL<super>[&dagger;][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
| OPENAI_API_KEY<super>[&dagger;][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
| OPENAI_CUSTOM_HEADERS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_CUSTOM_PARAMS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_ENABLE_IMAGE_SERVICES <br/> :octicons-tag-24: v1.12.0 | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_ENABLE_TRANSCRIPTION_SERVICES <br/> :octicons-tag-24: v3.13.0 | True | Whether to enable OpenAI transcription services, such as creating recipes via video URL. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
| OPENAI_CUSTOM_PROMPT_DIR <br/> :octicons-tag-24: v3.10.0 | None | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
| Variables | Default | Description |
|-------------------------------------------------------------------------|:-----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| OPENAI_BASE_URL<super>[&dagger;][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
| OPENAI_API_KEY<super>[&dagger;][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
| OPENAI_AUDIO_MODEL <br/> :octicons-tag-24: v3.13.0 | whisper-1 | Which OpenAI model to use for audio transcriptions, if enabled. If you're not sure, leave this empty |
| OPENAI_CUSTOM_HEADERS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_CUSTOM_PARAMS <br/> :octicons-tag-24: v2.0.0 | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_ENABLE_IMAGE_SERVICES <br/> :octicons-tag-24: v1.12.0 | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_ENABLE_TRANSCRIPTION_SERVICES <br/> :octicons-tag-24: v3.13.0 | True | Whether to enable OpenAI transcription services, such as creating recipes via video URL. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
| OPENAI_CUSTOM_PROMPT_DIR <br/> :octicons-tag-24: v3.10.0 | None. | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
### Theming

View File

@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.13.0`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.15.2`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container

View File

@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.13.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.13.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -355,20 +355,20 @@
title="github.com">
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
<path
d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z">
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6.0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6.0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3.0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1.0-6.2-.3-40.4-.3-61.4.0.0-70 15-84.7-29.8.0.0-11.4-29.1-27.8-36.6.0.0-22.9-15.7 1.6-15.4.0.0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5.0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9.0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4.0 33.7-.3 75.4-.3 83.6.0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6.0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9.0-6.2-1.4-2.3-4-3.3-5.6-2z">
</path>
</svg>
</a>
<a class="md-footer-social__link" href="https://twitter.com/kot_hay" rel="noopener" target="_blank"
title="twitter.com">
<svg style="width: 32px; height: 32px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<a class="md-footer-social__link" href="https://bsky.app/profile/haykot.dev" rel="noopener" target="_blank"
title="bsky.app">
<svg style="width: 32px; height: 32px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z">
d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.204-.659-.299-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z">
</path>
</svg>
</a>
<a class="md-footer-social__link" href="https://www.linkedin.com/in/hay-kot" rel="noopener" target="_blank"
title="www.linkedin.com">
title="linkedin.com">
<svg style="width: 32px; height: 32px" viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
<path
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z">

View File

@@ -61,10 +61,6 @@
max-width: 100%;
}
a {
color: rgb(var(--v-theme-primary));
}
.fill-height {
min-height: 100vh;
}
@@ -72,3 +68,8 @@ a {
.vue-simple-handler {
background-color: rgb(var(--v-theme-primary)) !important;
}
p {
margin-top: 0;
margin-bottom: 0;
}

View File

@@ -0,0 +1,116 @@
<template>
<v-container max-width="880" class="end-page-content">
<div class="d-flex flex-column ga-6">
<div>
<v-card-title class="text-h4 justify-center">
{{ $t('admin.setup.setup-complete') }}
</v-card-title>
<v-card-subtitle class="justify-center">
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
</v-card-subtitle>
</div>
<div
v-for="section, idx in sections"
:key="idx"
class="d-flex flex-column ga-3"
>
<v-card-title class="text-h6 pl-0">
{{ section.title }}
</v-card-title>
<div class="sections d-flex flex-column ga-2">
<v-card
v-for="link, linkIdx in section.links"
:key="linkIdx"
clas="link-card"
:href="link.to"
:title="link.text"
:subtitle="link.description"
:append-icon="$globals.icons.chevronRight"
>
<template #prepend>
<v-avatar :icon="link.icon || undefined" variant="tonal" :color="section.color" />
</template>
</v-card>
</div>
</div>
</div>
</v-container>
</template>
<script setup lang="ts">
const i18n = useI18n();
const auth = useMealieAuth();
const groupSlug = computed(() => auth.user.value?.groupSlug);
const { $globals } = useNuxtApp();
const sections = ref([
{
title: i18n.t("profile.data-migrations"),
color: "info",
links: [
{
icon: $globals.icons.backupRestore,
to: "/admin/backups",
text: i18n.t("settings.backup.backup-restore"),
description: i18n.t("admin.setup.restore-from-v1-backup"),
},
{
icon: $globals.icons.import,
to: "/group/migrations",
text: i18n.t("migration.recipe-migration"),
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
},
],
},
{
title: i18n.t("recipe.create-recipes"),
color: "success",
links: [
{
icon: $globals.icons.createAlt,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
text: i18n.t("recipe.create-recipe"),
description: i18n.t("recipe.create-recipe-description"),
},
{
icon: $globals.icons.link,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
text: i18n.t("recipe.import-with-url"),
description: i18n.t("recipe.scrape-recipe-description"),
},
],
},
{
title: i18n.t("user.manage-users"),
color: "primary",
links: [
{
icon: $globals.icons.group,
to: "/admin/manage/users",
text: i18n.t("user.manage-users"),
description: i18n.t("user.manage-users-description"),
},
{
icon: $globals.icons.user,
to: "/user/profile",
text: i18n.t("profile.manage-user-profile"),
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
},
],
},
]);
</script>
<style>
.v-container {
.v-card-title,
.v-card-subtitle {
padding: 0;
white-space: unset;
}
.v-card-item {
gap: 0.5rem;
}
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<BaseDialog
v-if="currentAnnouncement"
v-model="dialog"
:title="$t('announcements.announcements')"
:icon="$globals.icons.bullhornVariant"
:cancel-text="$t('general.done')"
width="100%"
max-width="1200"
>
<div class="d-flex" :style="{ height: useMobile ? '100%' : '60vh', minHeight: '60vh' }">
<!-- Nav list -->
<v-list
v-show="!useMobile || navOpen"
nav
density="compact"
color="primary"
class="overflow-y-auto border-e flex-shrink-0"
style="width: 200px; max-height: 60vh"
>
<v-list-item
v-for="announcement in allAnnouncements.toReversed()"
:key="announcement.key"
:active="currentAnnouncement.key === announcement.key"
rounded
@click="setCurrentAnnouncement(announcement); navOpen = false"
>
<v-list-item-title class="text-body-2">
{{ announcement.meta?.title }}
</v-list-item-title>
<v-list-item-subtitle v-if="announcement.date">
{{ $d(announcement.date) }}
</v-list-item-subtitle>
<template v-if="newAnnouncements.some(a => a.key === announcement.key)" #append>
<v-icon size="x-small" color="info">
{{ $globals.icons.alertCircle }}
</v-icon>
</template>
</v-list-item>
</v-list>
<!-- Main content -->
<div
class="flex-grow-1 overflow-y-auto"
>
<v-btn
v-if="useMobile"
:prepend-icon="navOpen ? $globals.icons.chevronLeft : $globals.icons.chevronRight"
density="compact"
variant="text"
class="mt-2 ms-2"
@click="navOpen = !navOpen"
>
{{ $t("announcements.all-announcements") }}
</v-btn>
<v-card-title>
<v-chip v-if="currentAnnouncement.date" label large class="me-1">
<v-icon class="me-1">
{{ $globals.icons.calendar }}
</v-icon>
{{ $d(currentAnnouncement.date) }}
</v-chip>
{{ currentAnnouncement.meta?.title }}
</v-card-title>
<v-card-text>
<component :is="currentAnnouncement.component" />
</v-card-text>
</div>
</div>
<template #custom-card-action>
<BaseButton
v-if="newAnnouncements.length"
color="success"
:icon="$globals.icons.textBoxCheckOutline"
:text="$t('announcements.mark-all-as-read')"
@click="markAllAsRead"
/>
<BaseButton
:disabled="isLastAnnouncement(currentAnnouncement.key)"
color="info"
:icon="$globals.icons.arrowRightBold"
icon-right
:text="$t('general.next')"
@click="nextAnnouncement"
/>
</template>
</BaseDialog>
</template>
<script setup lang="ts">
import { useAnnouncements } from "~/composables/use-announcements";
import type { Announcement } from "~/composables/use-announcements";
const dialog = defineModel<boolean>({ default: false });
const display = useDisplay();
const useMobile = computed(() => display.smAndDown.value);
const navOpen = ref(false);
const route = useRoute();
watch(() => route.fullPath, () => { dialog.value = false; });
const { newAnnouncements, allAnnouncements, setLastRead, markAllAsRead } = useAnnouncements();
const currentAnnouncement = shallowRef<Announcement | undefined>();
watch(dialog, () => {
if (!dialog.value || currentAnnouncement.value) {
return;
}
// Show first unread on open, or fall back to the newest
const next = newAnnouncements.value.at(0) || allAnnouncements.at(-1)!;
setCurrentAnnouncement(next);
});
function setCurrentAnnouncement(announcement: Announcement) {
currentAnnouncement.value = announcement;
setLastRead(announcement.key);
}
function nextAnnouncement() {
// Find the first unread announcement after the current one (current is already removed from newAnnouncements)
const next = newAnnouncements.value.find(a => a.key > currentAnnouncement.value!.key);
if (next) {
setCurrentAnnouncement(next);
}
}
function isLastAnnouncement(key: string) {
if (!newAnnouncements.value.length) {
return true;
}
else {
return key >= newAnnouncements.value.at(-1)!.key;
}
}
</script>

View File

@@ -0,0 +1,62 @@
<template>
<div>
<p>
Welcome to Mealie! If this is your first time seeing announcements, here's what to expect.
</p>
<div class="mb-2">
Announcements are reserved for things like:
<ul class="ml-6">
<li>Important new features</li>
<li>Major changes</li>
<li>Anything that might require additional user actions (such as migration scripts)</li>
</ul>
</div>
<p>
While we generally keep everything in our <a class="text-primary" href="https://github.com/mealie-recipes/mealie/releases" target="_blank">GitHub release notes</a>,
sometimes certain changes require some extra attention.
</p>
<p>
Announcements are English-only; they're one-off messages from the maintainers, not a replacement for our release notes. Some elements may still be translated.
</p>
<hr class="mt-2 mb-4">
<p>
You can opt out of announcements in your user settings:
<br>
<v-btn class="mt-2" color="primary" to="/user/profile/edit">
{{ $t("profile.user-settings") }}
</v-btn>
</p>
<p v-if="user?.canManageHousehold" class="mt-3">
As {{ user?.admin ? "an admin" : "a household manager" }}, you can disable announcements for your entire household:
<br>
<v-btn class="mt-2" color="primary" to="/household">
{{ $t("profile.household-settings") }}
</v-btn>
</p>
<p v-if="user?.canManage" class="mt-3">
{{ user?.admin ? "You can also" : "As a group manager, you can" }} disable announcements for your entire group:
<br>
<v-btn class="mt-2" color="primary" to="/group">
{{ $t("profile.group-settings") }}
</v-btn>
</p>
</div>
</template>
<script setup lang="ts">
import type { AnnouncementMeta } from "~/composables/use-announcements";
const { user } = useMealieAuth();
</script>
<script lang="ts">
export const meta: AnnouncementMeta = {
title: "Welcome to Mealie 🎉",
};
</script>
<style scoped lang="css">
p {
padding-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,37 @@
import { describe, test, expect } from "vitest";
const announcementFiles = import.meta.glob<{ default: unknown }>(
"~/components/Domain/Announcement/Announcements/*.vue",
);
// Expected format: YYYY-MM-DD_N_slug e.g. 2026-03-27_1_welcome
const FILE_FORMAT = /^\d{4}-\d{2}-\d{2}_\d+_.+$/;
describe("Announcement files", () => {
const filenames = Object.keys(announcementFiles).map(path =>
path.split("/").at(-1)!.replace(".vue", ""),
);
test("directory is not empty", () => {
expect(filenames.length).toBeGreaterThan(0);
});
test("all filenames match YYYY-MM-DD_N_slug format", () => {
for (const name of filenames) {
expect(name, `"${name}" does not match the expected format`).toMatch(FILE_FORMAT);
}
});
test("all date prefixes are valid dates", () => {
for (const name of filenames) {
const datePart = name.split("_", 1)[0]!;
const date = new Date(datePart);
expect(isNaN(date.getTime()), `"${name}" has an invalid date prefix "${datePart}"`).toBe(false);
}
});
test("all filenames are unique", () => {
const unique = new Set(filenames);
expect(unique.size).toBe(filenames.length);
});
});

View File

@@ -0,0 +1,45 @@
<template>
<div v-if="preferences">
<BaseCardSectionTitle :title="$t('group.group-preferences')" />
<div class="mb-6">
<v-checkbox
v-model="local.privateGroup"
hide-details
density="compact"
color="primary"
:label="$t('group.private-group')"
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("group.private-group-description") }}
</p>
<DocLink
class="mt-2"
link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work"
/>
</div>
</div>
<div class="mb-6">
<v-checkbox
v-model="local.showAnnouncements"
hide-details
density="compact"
color="primary"
:label="$t('announcements.show-announcements-from-mealie')"
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("announcements.show-announcements-setting-description") }}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { ReadGroupPreferences } from "~/lib/api/types/user";
const preferences = defineModel<ReadGroupPreferences>({ required: true });
const local = reactive({ ...preferences.value });
watch(local, (newVal) => { preferences.value = { ...newVal }; });
</script>

View File

@@ -2,7 +2,7 @@
<div v-if="preferences">
<BaseCardSectionTitle :title="$t('household.household-preferences')" />
<div class="mb-6">
<v-checkbox v-model="preferences.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
<v-checkbox v-model="local.privateHousehold" hide-details density="compact" :label="$t('household.private-household')" color="primary" />
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("household.private-household-description") }}
@@ -11,15 +11,29 @@
</div>
</div>
<div class="mb-6">
<v-checkbox v-model="preferences.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
<v-checkbox v-model="local.lockRecipeEditsFromOtherHouseholds" hide-details density="compact" :label="$t('household.lock-recipe-edits-from-other-households')" color="primary" />
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("household.lock-recipe-edits-from-other-households-description") }}
</p>
</div>
</div>
<div class="mb-6">
<v-checkbox
v-model="local.showAnnouncements"
hide-details
density="compact"
color="primary"
:label="$t('announcements.show-announcements-from-mealie')"
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("announcements.show-announcements-setting-description") }}
</p>
</div>
</div>
<v-select
v-model="preferences.firstDayOfWeek"
v-model="local.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin"
:items="allDays"
item-title="name"
@@ -34,7 +48,7 @@
</BaseCardSectionTitle>
<div class="preference-container">
<div v-for="p in recipePreferences" :key="p.key">
<v-checkbox v-model="preferences[p.key]" hide-details density="compact" :label="p.label" color="primary" />
<v-checkbox v-model="local[p.key]" hide-details density="compact" :label="p.label" color="primary" />
<p class="ml-8 text-subtitle-2 my-0 py-0">
{{ p.description }}
</p>
@@ -47,6 +61,9 @@
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
const local = reactive({ ...preferences.value });
watch(local, (newVal) => { preferences.value = { ...newVal }; });
const i18n = useI18n();
type Preference = {

View File

@@ -36,10 +36,8 @@
</div>
</v-expand-transition>
</RecipeCardImage>
<v-card-title class="mb-n3 px-4">
<div class="headerClass">
{{ name }}
</div>
<v-card-title class="mb-n3 px-4" style="font-size: 1.25rem;">
{{ name }}
</v-card-title>
<slot name="actions">

View File

@@ -1,24 +1,17 @@
<template>
<div>
<v-app-bar
<v-row
v-if="!disableToolbar"
color="transparent"
:absolute="false"
flat
class="mt-n1 flex-sm-wrap rounded position-relative w-100 left-0 top-0"
class="align-center pb-2"
>
<slot name="title">
<v-icon
v-if="title"
size="large"
start
>
{{ displayTitleIcon }}
</v-icon>
<v-toolbar-title class="headline">
{{ title }}
</v-toolbar-title>
</slot>
<v-icon
v-if="title"
size="large"
start
>
{{ displayTitleIcon }}
</v-icon>
<span class="text-headline-small">{{ title }}</span>
<v-spacer />
<v-btn
:icon="$vuetify.display.xs"
@@ -111,7 +104,7 @@
]"
@toggle-dense-view="toggleMobileCards()"
/>
</v-app-bar>
</v-row>
<div v-if="recipes && ready">
<div class="mt-2">
<v-row v-if="!useMobileCards">
@@ -136,7 +129,7 @@
</v-row>
<v-row
v-else
dense
density="comfortable"
>
<v-col
v-for="recipe in recipes"
@@ -159,14 +152,15 @@
</v-col>
</v-row>
</div>
<v-card v-intersect="infiniteScroll" />
<v-fade-transition>
<AppLoader
v-if="loading"
:loading="loading"
/>
</v-fade-transition>
<v-card v-intersect="infiniteScroll" variant="flat" />
</div>
<v-fade-transition>
<AppLoader
v-if="loading"
:loading="loading"
/>
</v-fade-transition>
<AppScrollToTop />
</div>
</template>
@@ -243,6 +237,7 @@ const ready = ref(false);
const loading = ref(false);
const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const { savePosition, getSavedPage, restorePosition } = useScrollPosition();
const router = useRouter();
const queryFilter = computed(() => {
@@ -283,8 +278,29 @@ async function fetchRecipes(pageCount = 1) {
}
onMounted(async () => {
await initRecipes();
ready.value = true;
loading.value = true;
const savedPage = getSavedPage(route.path);
if (savedPage && savedPage > 2) {
page.value = 1;
hasMore.value = true;
const newRecipes = await fetchRecipes(savedPage);
if (newRecipes.length < perPage * savedPage) {
hasMore.value = false;
}
page.value = savedPage;
emit(REPLACE_RECIPES_EVENT, newRecipes);
ready.value = true;
restorePosition(route.path);
}
else {
await initRecipes();
ready.value = true;
if (savedPage) {
restorePosition(route.path);
}
}
loading.value = false;
});
let lastQuery: string | undefined = JSON.stringify(props.query);
@@ -337,6 +353,8 @@ const infiniteScroll = useThrottleFn(async () => {
emit(APPEND_RECIPES_EVENT, newRecipes);
}
savePosition(route.path, page.value);
loading.value = false;
}, 500);

View File

@@ -1,91 +1,60 @@
<template>
<div class="text-center">
<v-dialog
<BaseButton @click="dialog = true">
{{ $t("new-recipe.bulk-add") }}
</BaseButton>
<BaseDialog
v-model="dialog"
width="800"
:title="$t('new-recipe.bulk-add')"
:icon="$globals.icons.createAlt"
:submit-text="$t('general.add')"
:disable-submit-on-enter="true"
can-submit
@submit="save"
>
<template #activator="{ props: activatorProps }">
<BaseButton
v-bind="activatorProps"
@click="inputText = inputTextProp"
>
{{ $t("new-recipe.bulk-add") }}
</BaseButton>
</template>
<v-card-text>
<v-textarea
v-model="inputText"
variant="outlined"
rows="12"
hide-details
autofocus
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
/>
<v-card>
<v-app-bar
density="compact"
dark
color="primary"
class="mb-2 position-relative left-0 top-0 w-100"
>
<v-icon
size="large"
start
>
{{ $globals.icons.createAlt }}
</v-icon>
<v-toolbar-title class="headline">
{{ $t("new-recipe.bulk-add") }}
</v-toolbar-title>
<v-spacer />
</v-app-bar>
<v-card-text>
<v-textarea
v-model="inputText"
variant="outlined"
rows="12"
hide-details
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
/>
<v-divider />
<v-divider />
<v-list lines="two">
<template
v-for="(util) in utilities"
:key="util.id"
>
<v-list-item
density="compact"
class="py-1"
class="px-0"
>
<v-list-item-title>
<v-list-item-subtitle class="wrap-word">
{{ util.description }}
</v-list-item-subtitle>
<template #prepend>
<v-avatar>
<v-btn
icon
variant="tonal"
base-color="info"
:title="$t('general.run')"
@click="util.action"
>
<v-icon>
{{ $globals.icons.play }}
</v-icon>
</v-btn>
</v-avatar>
</template>
<v-list-item-title class="text-pre-wrap">
{{ util.description }}
</v-list-item-title>
<BaseButton
size="small"
color="info"
@click="util.action"
>
<template #icon>
{{ $globals.icons.robot }}
</template>
{{ $t("general.run") }}
</BaseButton>
</v-list-item>
<v-divider class="mx-2" />
</template>
</v-card-text>
<v-divider />
<v-card-actions>
<BaseButton
cancel
@click="dialog = false"
/>
<v-spacer />
<BaseButton
save
color="success"
@click="save"
/>
</v-card-actions>
</v-card>
</v-dialog>
</v-list>
</v-card-text>
</BaseDialog>
</div>
</template>

View File

@@ -7,66 +7,64 @@
content-class="top-dialog"
:scrollable="false"
>
<v-app-bar
sticky
dark
color="primary-lighten-1 top-0 position-relative left-0"
:rounded="!$vuetify.display.xs"
style="width: 100%;"
>
<v-text-field
id="arrow-search"
v-model="search.query.value"
autofocus
variant="solo"
flat
autocomplete="off"
bg-color="primary-lighten-1"
color="white"
density="compact"
class="mx-2 arrow-search"
hide-details
single-line
:placeholder="$t('search.search')"
:prepend-inner-icon="$globals.icons.search"
/>
<v-btn
v-if="$vuetify.display.xs"
icon
size="x-small"
@click="dialog = false"
>
<v-icon>
{{ $globals.icons.close }}
</v-icon>
</v-btn>
</v-app-bar>
<v-card
class="position-relative mt-1 pa-1 scroll"
max-height="700px"
relative
:rounded="!$vuetify.display.xs"
:loading="loading"
>
<v-toolbar
dark
color="primary-lighten-1"
>
<v-text-field
id="arrow-search"
v-model="search.query.value"
autofocus
variant="solo"
flat
autocomplete="off"
bg-color="primary-lighten-1"
color="white"
density="compact"
class="mx-2 arrow-search"
hide-details
single-line
:placeholder="$t('search.search')"
:prepend-inner-icon="$globals.icons.search"
/>
<v-btn
v-if="$vuetify.display.xs"
icon
size="x-small"
@click="dialog = false"
>
<v-icon>
{{ $globals.icons.close }}
</v-icon>
</v-btn>
</v-toolbar>
<v-card-actions>
<div class="mr-auto">
{{ $t("search.results") }}
</div>
</v-card-actions>
<RecipeCardMobile
v-for="(recipe, index) in search.data.value"
:key="index"
:tabindex="index"
class="ma-1 arrow-nav"
:name="recipe.name ?? ''"
:description="recipe.description ?? ''"
:slug="recipe.slug ?? ''"
:rating="recipe.rating ?? 0"
:image="recipe.image"
:recipe-id="recipe.id ?? ''"
v-bind="$attrs.selected ? { selected: () => handleSelect(recipe) } : {}"
/>
<div class="scroll pa-1" style="max-height: 700px;">
<RecipeCardMobile
v-for="(recipe, index) in search.data.value"
:key="index"
:tabindex="index"
class="ma-1 arrow-nav"
:name="recipe.name ?? ''"
:description="recipe.description ?? ''"
:slug="recipe.slug ?? ''"
:rating="recipe.rating ?? 0"
:image="recipe.image"
:recipe-id="recipe.id ?? ''"
v-bind="$attrs.selected ? { selected: () => handleSelect(recipe) } : {}"
/>
</div>
</v-card>
</v-dialog>
</div>

View File

@@ -0,0 +1,56 @@
<template>
<v-container
fluid
class="px-0"
>
<RecipeExplorerPageSearch
ref="searchComponent"
@ready="onSearchReady"
/>
<v-divider />
<v-container class="mt-6 px-md-6">
<RecipeCardSection
v-if="ready"
class="mt-n5"
:icon="$globals.icons.silverwareForkKnife"
:title="$t('general.recipes')"
:recipes="recipes"
:query="searchQuery"
disable-sort
@item-selected="onItemSelected"
@replace-recipes="replaceRecipes"
@append-recipes="appendRecipes"
/>
</v-container>
</v-container>
</template>
<script setup lang="ts">
import RecipeExplorerPageSearch from "./RecipeExplorerPageParts/RecipeExplorerPageSearch.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useLazyRecipes } from "~/composables/recipes";
const auth = useMealieAuth();
const route = useRoute();
const { isOwnGroup } = useLoggedInState();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const ready = ref(false);
const searchComponent = ref<InstanceType<typeof RecipeExplorerPageSearch>>();
const searchQuery = computed(() => {
return searchComponent.value?.passedQueryWithSeed || {};
});
function onSearchReady() {
ready.value = true;
}
function onItemSelected(item: any, urlPrefix: string) {
searchComponent.value?.filterItems(item, urlPrefix);
}
</script>

View File

@@ -13,7 +13,7 @@
/>
<v-row
:no-gutters="mdAndUp"
dense
density="comfortable"
class="d-flex flex-wrap my-1"
>
<v-col

View File

@@ -1,62 +1,30 @@
<template>
<div>
<v-dialog
<BaseDialog
v-model="dialog"
width="500"
:title="properties.title"
:icon="properties.icon"
can-submit
:submit-disabled="!name"
@submit="select"
>
<v-card>
<v-app-bar
density="compact"
dark
color="primary mb-2 position-relative left-0 top-0 w-100 pl-3"
>
<v-icon
size="large"
start
class="mt-1"
>
{{ itemType === Organizer.Tool ? $globals.icons.potSteam
: itemType === Organizer.Category ? $globals.icons.categories
: $globals.icons.tags }}
</v-icon>
<v-toolbar-title class="headline">
{{ properties.title }}
</v-toolbar-title>
<v-spacer />
</v-app-bar>
<v-card-title />
<v-form @submit.prevent="select">
<v-card-text>
<v-text-field
v-model="name"
density="compact"
:label="properties.label"
:rules="[rules.required]"
autofocus
/>
<v-checkbox
v-if="itemType === Organizer.Tool"
v-model="onHand"
:label="$t('tool.on-hand')"
/>
</v-card-text>
<v-card-actions>
<BaseButton
cancel
@click="dialog = false"
/>
<v-spacer />
<BaseButton
type="submit"
create
:disabled="!name"
/>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
<v-form>
<v-card-text>
<v-text-field
v-model="name"
:label="properties.label"
:rules="[rules.required]"
autofocus
/>
<v-checkbox
v-if="itemType === Organizer.Tool"
v-model="onHand"
:label="$t('tool.on-hand')"
/>
</v-card-text>
</v-form>
</BaseDialog>
</div>
</template>
@@ -65,6 +33,8 @@ import { useUserApi } from "~/composables/api";
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated";
const { $globals } = useNuxtApp();
const CREATED_ITEM_EVENT = "created-item";
interface Props {
@@ -115,18 +85,21 @@ const properties = computed(() => {
return {
title: i18n.t("tag.create-a-tag"),
label: i18n.t("tag.tag-name"),
icon: $globals.icons.tags,
api: userApi.tags,
};
case Organizer.Tool:
return {
title: i18n.t("tool.create-a-tool"),
label: i18n.t("tool.tool-name"),
icon: $globals.icons.potSteam,
api: userApi.tools,
};
default:
return {
title: i18n.t("category.create-a-category"),
label: i18n.t("category.category-name"),
icon: $globals.icons.categories,
api: userApi.categories,
};
}
@@ -139,12 +112,9 @@ const rules = {
async function select() {
if (store) {
// @ts-expect-error the same state is used for different organizer types, which have different requirements
await store.actions.createOne({ name: name.value, onHand: onHand.value });
const newItem = await store.actions.createOne({ name: name.value, onHand: onHand.value });
emit(CREATED_ITEM_EVENT, newItem);
}
const newItem = store.store.value.find(item => item.name === name.value);
emit(CREATED_ITEM_EVENT, newItem);
dialog.value = false;
}
</script>

View File

@@ -26,6 +26,7 @@
v-if="updateTarget"
v-model="dialogs.update"
:title="$t('general.update')"
:icon="$globals.icons.edit"
can-confirm
@confirm="updateOne()"
>
@@ -42,7 +43,7 @@
</v-card-text>
</BaseDialog>
<v-row dense>
<v-row density="comfortable">
<v-col>
<v-text-field
v-model="searchString"
@@ -56,7 +57,7 @@
</v-col>
</v-row>
<v-app-bar
<v-row
color="transparent"
flat
class="mt-n1 rounded align-center position-relative w-100 left-0 top-0"
@@ -75,7 +76,7 @@
create
@click="dialogs.organizer = true"
/>
</v-app-bar>
</v-row>
<section
v-for="(itms, key, idx) in itemsSorted"
:key="'header' + idx"

View File

@@ -27,12 +27,10 @@
color="accent"
variant="flat"
label
:text="item.name"
closable
@click:close="removeByIndex(index)"
>
{{ item.value }}
</v-chip>
/>
</template>
<template
v-if="showAdd"

View File

@@ -21,7 +21,7 @@
@save="saveParsedIngredients"
/>
<v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown }">
<v-card :flat="$vuetify.display.smAndDown" class="d-print-none">
<v-card flat class="d-print-none">
<RecipePageHeader
:recipe="recipe"
:recipe-scale="scale"
@@ -68,17 +68,21 @@
<!--
The left column is conditionally rendered based on cook mode.
-->
<v-col v-if="!isCookMode || isEditForm" cols="12" sm="12" md="4" lg="4">
<RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" />
<RecipePageOrganizers v-if="$vuetify.display.mdAndUp" v-model="recipe" @item-selected="chipClicked" />
<v-col
v-if="!isCookMode || isEditForm"
cols="12"
sm="12"
md="4"
:class="$vuetify.display.mdAndUp ? 'border-e-thin' : null"
>
<RecipePageIngredientToolsView v-if="!isEditForm" :recipe="recipe" :scale="scale" class="pr-2" />
<RecipePageOrganizers v-if="$vuetify.display.mdAndUp" v-model="recipe" class="pr-2" @item-selected="chipClicked" />
</v-col>
<v-divider v-if="$vuetify.display.mdAndUp && !isCookMode" class="my-divider" :vertical="true" />
<!--
the right column is always rendered, but it's layout width is determined by where the left column is
rendered.
-->
<v-col cols="12" sm="12" :md="8 + (isCookMode ? 1 : 0) * 4" :lg="8 + (isCookMode ? 1 : 0) * 4">
<v-col cols="12" sm="12" :md="8 + (isCookMode ? 1 : 0) * 4">
<RecipePageInstructions
v-model="recipe.recipeInstructions"
v-model:assets="recipe.assets"

View File

@@ -82,7 +82,7 @@
</div>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import type { Recipe } from "~/lib/api/types/recipe";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";

View File

@@ -62,17 +62,18 @@ const toolStore = isOwnGroup.value ? useToolStore() : null;
const { user } = usePageUser();
const { isEditMode } = usePageState(props.recipe.slug);
const recipeTools = computed(() => {
const recipeTools = ref<RecipeToolWithOnHand[]>([]);
watch(() => props.recipe.tools, () => {
if (!(user.householdSlug && toolStore)) {
return props.recipe.tools.map(tool => ({ ...tool, onHand: false }) as RecipeToolWithOnHand);
recipeTools.value = props.recipe.tools.map(tool => ({ ...tool, onHand: false }) as RecipeToolWithOnHand);
}
else {
return props.recipe.tools.map((tool) => {
recipeTools.value = props.recipe.tools.map((tool) => {
const onHand = tool.householdsWithTool?.includes(user.householdSlug) || false;
return { ...tool, onHand } as RecipeToolWithOnHand;
});
}
});
}, { immediate: true });
function updateTool(index: number) {
if (user.id && user.householdSlug && toolStore) {

View File

@@ -1,117 +1,101 @@
<template>
<section @keyup.ctrl.z="undoMerge">
<!-- Ingredient Link Editor -->
<v-dialog
v-if="dialog"
<BaseDialog
v-model="dialog"
width="600"
:title="$t('recipe.ingredient-linker')"
:icon="$globals.icons.link"
width="100%"
max-width="600px"
max-height="40%"
>
<v-card :ripple="false">
<v-sheet
color="primary"
class="mt-n1 mb-3 pa-3 d-flex align-center"
style="border-radius: 6px; width: 100%;"
>
<v-icon
size="large"
start
>
{{ $globals.icons.link }}
</v-icon>
<v-toolbar-title class="headline">
{{ $t("recipe.ingredient-linker") }}
</v-toolbar-title>
<v-spacer />
</v-sheet>
<v-card-text class="pt-4">
<p>
{{ activeText }}
</p>
<v-divider class="mb-4" />
<template v-if="Object.keys(groupedUnusedIngredients).length > 0">
<h4 class="py-3 ml-1">
{{ $t("recipe.unlinked") }}
<v-card-text class="pt-4">
<p>
{{ activeText }}
</p>
<v-divider class="my-4" />
<template v-if="Object.keys(groupedUnusedIngredients).length > 0">
<h4 class="ml-1">
{{ $t("recipe.unlinked") }}
</h4>
<template v-for="(ingredients, title) in groupedUnusedIngredients" :key="title">
<h4 v-if="title" class="py-3 ml-1 pl-4">
{{ title }}
</h4>
<template v-for="(ingredients, title) in groupedUnusedIngredients" :key="title">
<h4 v-if="title" class="py-3 ml-1 pl-4">
{{ title }}
</h4>
<v-checkbox-btn
v-for="ing in ingredients"
:key="ing.referenceId"
v-model="activeRefs"
:value="ing.referenceId"
class="ml-4"
>
<template #label>
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
</template>
</v-checkbox-btn>
</template>
<v-checkbox-btn
v-for="ing in ingredients"
:key="ing.referenceId"
v-model="activeRefs"
:value="ing.referenceId"
class="ml-4"
>
<template #label>
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
</template>
</v-checkbox-btn>
</template>
</template>
<template v-if="Object.keys(groupedUsedIngredients).length > 0">
<h4 class="py-3 ml-1">
{{ $t("recipe.linked-to-other-step") }}
<template v-if="Object.keys(groupedUsedIngredients).length > 0">
<h4 class="py-3 ml-1">
{{ $t("recipe.linked-to-other-step") }}
</h4>
<template v-for="(ingredients, title) in groupedUsedIngredients" :key="title">
<h4 v-if="title" class="py-3 ml-1 pl-4">
{{ title }}
</h4>
<template v-for="(ingredients, title) in groupedUsedIngredients" :key="title">
<h4 v-if="title" class="py-3 ml-1 pl-4">
{{ title }}
</h4>
<v-checkbox-btn
v-for="ing in ingredients"
:key="ing.referenceId"
v-model="activeRefs"
:value="ing.referenceId"
class="ml-4"
>
<template #label>
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
</template>
</v-checkbox-btn>
</template>
<v-checkbox-btn
v-for="ing in ingredients"
:key="ing.referenceId"
v-model="activeRefs"
:value="ing.referenceId"
class="ml-4"
>
<template #label>
<RecipeIngredientHtml :ingredient="ing" :scale="scale" />
</template>
</v-checkbox-btn>
</template>
</v-card-text>
</template>
</v-card-text>
<v-divider />
<v-divider />
<v-card-actions>
<template #card-actions>
<BaseButton
cancel
@click="dialog = false"
/>
<v-spacer />
<div class="d-flex flex-wrap justify-end">
<BaseButton
cancel
@click="dialog = false"
class="my-1"
color="info"
@click="autoSetReferences"
>
<template #icon>
{{ $globals.icons.robot }}
</template>
{{ $t("recipe.auto") }}
</BaseButton>
<BaseButton
class="ml-2 my-1"
save
@click="setIngredientIds"
/>
<v-spacer />
<div class="d-flex flex-wrap justify-end">
<BaseButton
class="my-1"
color="info"
@click="autoSetReferences"
>
<template #icon>
{{ $globals.icons.robot }}
</template>
{{ $t("recipe.auto") }}
</BaseButton>
<BaseButton
class="ml-2 my-1"
save
@click="setIngredientIds"
/>
<BaseButton
v-if="availableNextStep"
class="ml-2 my-1"
@click="saveAndOpenNextLinkIngredients"
>
<template #icon>
{{ $globals.icons.forward }}
</template>
{{ $t("recipe.nextStep") }}
</BaseButton>
</div>
</v-card-actions>
</v-card>
</v-dialog>
<BaseButton
v-if="availableNextStep"
class="ml-2 my-1"
@click="saveAndOpenNextLinkIngredients"
>
<template #icon>
{{ $globals.icons.forward }}
</template>
{{ $t("recipe.nextStep") }}
</BaseButton>
</div>
</template>
</BaseDialog>
<div class="d-flex justify-space-between justify-start">
<h2
@@ -851,6 +835,10 @@ function openImageUpload(index: number) {
font-size: 1.5rem;
}
.v-card-text {
font-size: 1rem;
}
.recipe-step-title {
/* Multiline display */
white-space: normal;

View File

@@ -85,7 +85,7 @@
</div>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import { usePageState } from "~/composables/recipe-page/shared-state";
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import type { Recipe } from "~/lib/api/types/recipe";

View File

@@ -208,7 +208,7 @@ const props = defineProps<{
ingredients: NoUndefinedField<RecipeIngredient[]>;
}>();
const { parseIngredientText } = useIngredientTextParser();
const { ingredientToParserString } = useIngredientTextParser();
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
@@ -373,7 +373,7 @@ async function parseIngredients() {
try {
const ingsAsString = props.ingredients
.filter(ing => !ing.referencedRecipe)
.map(ing => parseIngredientText(ing, 1, false) ?? "");
.map(ing => ingredientToParserString(ing));
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
if (error || !data) {
throw new Error("Failed to parse ingredients");

View File

@@ -36,7 +36,7 @@
</div>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import RecipeSettingsSwitches from "./RecipeSettingsSwitches.vue";
const value = defineModel<object>({ required: true });

View File

@@ -15,8 +15,7 @@
</div>
</template>
<script lang="ts" setup>
import { defineModel, defineProps } from "vue";
<script setup lang="ts">
import type { RecipeSettings } from "~/lib/api/types/recipe";
import { useI18n } from "#imports";

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