Compare commits

..

411 Commits

Author SHA1 Message Date
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
mealie-commit-bot[bot]
1969f50ee6 chore: bump version to v3.13.0 2026-03-18 13:58:50 +00:00
renovate[bot]
ff7d23d6d4 fix(deps): update dependency openai to v2.29.0 (#7268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 04:44:25 +00:00
renovate[bot]
5e239be6fa fix(deps): update dependency yt-dlp to v2026.3.17 (#7269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 04:44:19 +00:00
Hayden
e5520b08e5 chore(l10n): New Crowdin updates (#7270) 2026-03-18 04:44:03 +00:00
renovate[bot]
fce0b47e2c chore(deps): update node.js to 5a593d7 (#7267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 16:42:06 +00:00
renovate[bot]
e00d2a6e83 chore(deps): update dependency coverage to v7.13.5 (#7265)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:37:15 +00:00
renovate[bot]
948914df50 chore(deps): update node.js to 6c0cc63 (#7262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:37:08 +00:00
Hayden
0c7d9341bf chore(l10n): New Crowdin updates (#7266) 2026-03-17 14:15:09 +00:00
Michael Genson
b94b24640b feat: Adjust linked recipe unit and seperate when adding to shopping list (#7260) 2026-03-16 16:30:07 -05:00
renovate[bot]
c303198857 fix(deps): update dependency pyjwt to v2.12.1 (#7241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 14:08:10 +00:00
renovate[bot]
9424858985 fix(deps): update dependency uvicorn to v0.42.0 (#7257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 14:06:33 +00:00
mealie-actions[bot]
3711154c44 chore(auto): Update pre-commit hooks (#7256)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-03-16 14:06:08 +00:00
Hayden
cc12d07576 chore(l10n): New Crowdin updates (#7258) 2026-03-16 13:40:00 +00:00
renovate[bot]
3e96bf7f43 fix(deps): update dependency openai to v2.28.0 (#7242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 23:54:37 +00:00
api2062
ee550f4fe3 fix: exclude index.html from workbox precache to prevent stale 401 app shell (#7255)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-15 23:43:54 +00:00
Michael Genson
60d4a62f0a feat: Switch to httpx-curl-cffi for better scraping (#7254) 2026-03-15 18:30:23 -05:00
Hayden
69d740e100 chore(l10n): New Crowdin updates (#7253) 2026-03-15 21:44:20 +00:00
Michael Genson
c4fdab4e05 feat: Recipe import progress (#7252) 2026-03-15 16:44:19 -05:00
MochaFox
04dd514e6a fix: Update user agents in user-agents.txt (#7250) 2026-03-15 18:50:35 +00:00
mealie-actions[bot]
ad5d4b5aba chore(l10n): Crowdin locale sync (#7246)
Co-authored-by: GitHub Action <action@github.com>
2026-03-15 03:07:05 +00:00
Hayden
b948314c6b chore(l10n): New Crowdin updates (#7245) 2026-03-14 21:20:36 +00:00
Hayden
2568015941 chore(l10n): New Crowdin updates (#7240) 2026-03-13 20:38:51 +00:00
renovate[bot]
235a1f1931 fix(deps): update dependency yt-dlp to v2026.3.13 (#7239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 19:02:58 +00:00
renovate[bot]
8c23fd922a chore(deps): update dependency ruff to v0.15.6 (#7237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 02:18:34 +00:00
Hayden
537f5ae1dd chore(l10n): New Crowdin updates (#7236) 2026-03-12 20:21:55 +00:00
renovate[bot]
9372f51000 fix(deps): update dependency pyjwt to v2.12.0 (#7234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 19:30:01 +00:00
Michael Genson
1bdb3ce54c fix: Add ffmpeg to prod image (#7235) 2026-03-12 14:11:15 -05:00
Dean Malan
16e8e8a877 chore: run Ruff with --fix in Pre-commit to auto-fix fixable errors (#7232) 2026-03-12 14:06:06 +00:00
Dean Malan
6a3b38a31e fix: Don't continue parsing recipes with errored HTTP status codes (#7230) 2026-03-12 13:58:40 +00:00
Hayden
8189416495 chore(l10n): New Crowdin updates (#7231) 2026-03-12 08:14:11 +00:00
Hayden
589c6de053 chore(l10n): New Crowdin updates (#7228) 2026-03-11 20:18:17 +00:00
Hayden
d3d339a4aa chore(l10n): New Crowdin updates (#7225) 2026-03-11 05:56:40 +00:00
renovate[bot]
5c2bbea09b chore(deps): update dependency mkdocs-material to v9.7.5 (#7224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 18:11:56 +00:00
Hayden
f6ca4bb29a chore(l10n): New Crowdin updates (#7223) 2026-03-10 17:40:46 +00:00
Hayden
ddc30fc65f chore(l10n): New Crowdin updates (#7220) 2026-03-10 07:30:04 +00:00
Michael Genson
a4d1e0a440 fix: Restore original recipe description string (#7221) 2026-03-09 23:37:54 -05:00
Michael Genson
86b72f4d5b fix: Allow more precise standard quantities in unit data management (#7218) 2026-03-09 18:55:56 -05:00
Aurelien
1344f1674d feat: Add social media video import (YouTube, TikTok, Instagram) (#6764)
Co-authored-by: Maxime Louward <61564950+mlouward@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-09 15:44:27 -05:00
renovate[bot]
5a223aa92d fix(deps): update dependency pint to v0.25.2 (#7214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 18:01:26 +00:00
renovate[bot]
845637c988 fix(deps): pin dependency pint to v0.25 (#7213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 17:37:30 +00:00
Michael Genson
b5c089f58c feat: Unit standardization / conversion (#7121) 2026-03-09 17:13:41 +00:00
ProperTeaMogul
96597915ff fix: migrate RecipeRating component to script setup and defineModel (#7203) 2026-03-09 16:41:09 +00:00
renovate[bot]
98c555fd20 chore(deps): update dependency setuptools to v82.0.1 (#7211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 15:15:35 +00:00
renovate[bot]
118274ad9d fix(deps): update dependency apprise to v1.9.8 (#7207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 15:15:27 +00:00
mealie-actions[bot]
50ea601683 chore(auto): Update pre-commit hooks (#7209)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-03-09 15:03:20 +00:00
Hayden
7cc9010143 chore(l10n): New Crowdin updates (#7208) 2026-03-09 03:33:59 +00:00
Hayden
1f196cf10f chore(l10n): New Crowdin updates (#7205) 2026-03-08 15:35:49 +00:00
mealie-commit-bot[bot]
024e20b4e5 chore: bump version to v3.12.0 2026-03-07 21:59:20 +00:00
Brian Choromanski
5aafebb7a6 fix: Added mealie.log rotations to excluded files for backup (#7138) 2026-03-07 21:36:49 +00:00
Michael Genson
a89460acdf fix: Restore missing description for household's new recipe settings (#7204) 2026-03-07 15:33:56 -06:00
Hayden
89091678d4 chore(l10n): New Crowdin updates (#7202) 2026-03-07 16:03:35 +00:00
Hayden
562eb89ee7 chore(l10n): New Crowdin updates (#7198) 2026-03-07 03:21:10 +00:00
Brian Choromanski
5e40fed623 fix: Logic to unset-rating when currentrating is selected (#7182) 2026-03-06 21:45:02 +00:00
Hayden
0ddfd9caaf chore(l10n): New Crowdin updates (#7195)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-06 20:05:58 +00:00
Michael Genson
56086bdf49 dev: Add backend translations to auto-approval (#7196) 2026-03-06 13:55:03 -06:00
renovate[bot]
a0674dd5d2 fix(deps): update dependency openai to v2.26.0 (#7190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 19:28:08 +00:00
renovate[bot]
45c68d160a chore(deps): update dependency ruff to v0.15.5 (#7193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 19:28:03 +00:00
Hayden
058937e334 chore(l10n): New Crowdin updates (#7194) 2026-03-06 05:25:58 +00:00
Michele Zoncheddu
c91f8e23d7 feat: replace python script with curl (#7192)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-05 23:15:53 +00:00
Michael Genson
77081d0482 feat: include extra ingredients + amounts in NLP parser output (#7191) 2026-03-05 16:28:14 -06:00
Hayden
bf11729a23 chore(l10n): New Crowdin updates (#7189) 2026-03-05 14:25:19 +00:00
renovate[bot]
b5016857c8 chore(deps): update dependency types-python-dateutil to v2.9.0.20260305 (#7188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 05:47:45 +00:00
Hayden
26fdf78709 chore(l10n): New Crowdin updates (#7186) 2026-03-04 12:09:19 +00:00
mealie-actions[bot]
455dbb1441 chore(l10n): Crowdin locale sync (#7183)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-03 21:21:37 -06:00
Hayden
f28de01b2e chore(l10n): New Crowdin updates (#7180) 2026-03-04 02:51:08 +00:00
Brian Choromanski
ccdf7109e2 fix: Updated get_ts_locales to resolve issues with Automatic Locale Sync workflow (#7181) 2026-03-04 02:37:38 +00:00
renovate[bot]
e574896449 chore(deps): update dependency mkdocs-material to v9.7.4 (#7178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 02:34:47 +00:00
Hayden
15d56c42f7 chore(l10n): New Crowdin updates (#7174) 2026-03-02 23:42:12 +00:00
renovate[bot]
c983e8bd59 fix(deps): update dependency sqlalchemy to v2.0.48 (#7173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 20:39:52 +00:00
renovate[bot]
7584c99591 fix(deps): update dependency authlib to v1.6.9 (#7170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 15:28:07 +00:00
renovate[bot]
78718dcf26 fix(deps): update dependency python-dotenv to v1.2.2 (#7164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 15:27:35 +00:00
renovate[bot]
a8ec66f9aa chore(deps): update dependency types-python-dateutil to v2.9.0.20260302 (#7169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 10:12:31 +00:00
mealie-actions[bot]
3899f85735 chore(auto): Update pre-commit hooks (#7168)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-03-02 10:11:41 +00:00
Hayden
7cbe17fe09 chore(l10n): New Crowdin updates (#7166) 2026-03-01 23:50:34 +00:00
renovate[bot]
929349d414 fix(deps): update dependency fastapi to v0.135.1 (#7165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-01 23:50:15 +00:00
Hayden
595a3c66cd chore(l10n): New Crowdin updates (#7160) 2026-02-28 22:53:48 +00:00
renovate[bot]
d4e7dc6e9d fix(deps): update dependency fastapi to v0.134.0 (#7127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 00:43:40 +00:00
Hayden
b719b39c09 chore(l10n): New Crowdin updates (#7157) 2026-02-27 22:05:31 +00:00
renovate[bot]
b208719cb9 fix(deps): update dependency pillow-heif to v1.3.0 (#7155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 15:28:58 +00:00
renovate[bot]
3ee64c930c chore(deps): update dependency ruff to v0.15.4 (#7152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 22:46:53 +00:00
Hayden
189e98fb1f chore(l10n): New Crowdin updates (#7153) 2026-02-26 21:19:36 +00:00
renovate[bot]
7da01f7873 chore(deps): update dependency ruff to v0.15.3 (#7151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 17:08:10 +00:00
Michael Genson
8144799733 feat: Improve auto-form layout (#7150) 2026-02-26 10:50:45 -06:00
Hayden
669df6bbb4 chore(l10n): New Crowdin updates (#7149) 2026-02-26 11:54:33 +00:00
Kuchenpirat
c6171f2cb2 fix: data management food edit / create (#7148) 2026-02-26 11:45:44 +00:00
renovate[bot]
e5d930ecb8 fix(deps): update dependency openai to v2.24.0 (#7137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 22:08:13 +00:00
renovate[bot]
668246d369 fix(deps): update dependency sqlalchemy to v2.0.47 (#7135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 21:57:01 +00:00
Michael Genson
b182b50faa fix(deps): update dependency fastapi to v0.133.1 (#7146) 2026-02-25 15:56:01 -06:00
renovate[bot]
a3e3fa6f56 chore(deps): update node.js to 3a09aa6 (#7140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 21:34:03 +00:00
Hayden
bd7acabcd1 chore(l10n): New Crowdin updates (#7145) 2026-02-25 19:54:30 +00:00
Kuchenpirat
8059cc731f dev: add AI/LLM Assistance section to PR template (#7144)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-02-25 19:39:40 +00:00
Michael Genson
3ae455539c fix: Ensure recipe ingredients have a valid reference_id (#7139) 2026-02-24 19:50:49 -06:00
Hayden
8fd7995681 chore(l10n): New Crowdin updates (#7136) 2026-02-24 19:51:38 +00:00
Kuchenpirat
282eedfe2b chore: refactor data management pages (#7107) 2026-02-24 17:23:33 +00:00
renovate[bot]
03f849f20f chore(deps): update dependency mkdocs-material to v9.7.3 (#7134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 16:09:43 +00:00
renovate[bot]
5db3b6ab72 fix(deps): update dependency openai to v2.23.0 (#7132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 08:56:43 +00:00
Hayden
353c24ca4b chore(l10n): New Crowdin updates (#7131) 2026-02-24 07:02:24 +00:00
Michael Genson
216ae8571c fix: Include unmade recipes when filtering by last made (#7130) 2026-02-23 18:34:16 -06:00
renovate[bot]
02d32c8905 fix(deps): update dependency openai to v2.22.0 (#7128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:15:25 +00:00
Hayden
7e0d083e77 chore(l10n): New Crowdin updates (#7126) 2026-02-23 18:21:38 +00:00
mealie-actions[bot]
b3cea081fe chore(auto): Update pre-commit hooks (#7122)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-02-23 14:07:02 +00:00
renovate[bot]
d79252752b fix(deps): update dependency fastapi to v0.131.0 (#7113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-22 22:44:02 +00:00
Hayden
b3c214d102 chore(l10n): New Crowdin updates (#7119) 2026-02-22 22:43:45 +00:00
Hayden
3a01925e48 chore(l10n): New Crowdin updates (#7116) 2026-02-22 05:31:27 +00:00
Hayden
16e2386f5a chore(l10n): New Crowdin updates (#7112) 2026-02-21 17:27:20 +00:00
renovate[bot]
bbfa105e99 fix(deps): update dependency fastapi to v0.129.1 (#7111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 14:17:34 +00:00
Hayden
c94c9940b2 chore(l10n): New Crowdin updates (#7110)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-02-21 05:44:05 +00:00
Michael Genson
29c6176d89 docs: Remove redoc API generation (#7109) 2026-02-20 20:49:43 +00:00
renovate[bot]
0c0d7d11a5 chore(deps): update dependency pylint to v4.0.5 (#7106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 18:14:48 +00:00
renovate[bot]
e75fc6d391 chore(deps): update dependency ruff to v0.15.2 (#7104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 05:06:50 +00:00
Hayden
f308869154 chore(l10n): New Crowdin updates (#7105) 2026-02-20 04:45:05 +00:00
Michael Genson
af30b8bdfa docs: Add missing release tags to OpenAI docs (#7102) 2026-02-19 14:31:58 -06:00
renovate[bot]
de4f22c3f6 fix(deps): update dependency pydantic-settings to v2.13.1 (#7101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 18:06:46 +00:00
renovate[bot]
4c55b282d6 chore(deps): update dependency rich to v14.3.3 (#7100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 18:06:43 +00:00
Hayden
8d2b2eb581 chore(l10n): New Crowdin updates (#7098)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-02-19 00:24:41 +00:00
Michael Genson
e9daac5fc4 feat: Auto-adjust shopping list item autofocus (#7096) 2026-02-18 16:37:32 -06:00
renovate[bot]
ee1205cfdc chore(deps): update dependency mkdocs-material to v9.7.2 (#7093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 21:07:43 +00:00
renovate[bot]
a165b707af fix(deps): update dependency pillow-heif to v1.2.1 (#7092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 21:07:11 +00:00
Hayden
564385eb83 chore(l10n): New Crowdin updates (#7088) 2026-02-17 23:53:36 +00:00
mealie-commit-bot[bot]
c23aa61f17 chore: bump version to v3.11.0 2026-02-17 04:13:46 +00:00
renovate[bot]
cd39d0c4cb fix(deps): update dependency uvicorn to v0.41.0 (#7083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 01:25:35 +00:00
Michael Genson
20e2d4e1a1 fix: Exclude docs/redoc from (#7082) 2026-02-16 19:24:33 -06:00
mealie-actions[bot]
c09cc5a323 chore(auto): Update pre-commit hooks (#7080)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-02-16 15:36:17 +00:00
Michael Genson
6d7b6bccab fix: Show minimum value for quantity (#7077) 2026-02-15 13:08:07 -06:00
Hayden
91fea086e5 chore(l10n): New Crowdin updates (#7075) 2026-02-15 17:52:15 +00:00
renovate[bot]
e2fbe118a7 fix(deps): update dependency pydantic-settings to v2.13.0 (#7073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 17:30:44 +00:00
Zachary Schaffter
904e6b7d82 fix: #6263 remove reserved prefix (#7033) 2026-02-14 21:05:42 +00:00
renovate[bot]
5aafb56c4f fix(deps): update dependency authlib to v1.6.8 (#7067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 19:01:34 +00:00
renovate[bot]
b4740d291d fix(deps): update dependency openai to v2.21.0 (#7065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 19:01:27 +00:00
Hayden
fc6dc34ace chore(l10n): New Crowdin updates (#7070) 2026-02-14 17:29:38 +00:00
Michael Genson
73d86f6f6b feat: Further improve recipe filter search and shopping list and recipe ingredient editor (#7063) 2026-02-14 00:34:17 -06:00
Hayden
8e225ee796 chore(l10n): New Crowdin updates (#7066) 2026-02-14 05:06:25 +00:00
Hayden
ced233d361 chore(l10n): New Crowdin updates (#7062) 2026-02-13 17:08:08 +00:00
Michael Genson
b173172e6c feat: Improve recipe filter search ordering (#7061) 2026-02-13 10:15:38 -06:00
Michael Genson
a66db96eb5 fix: Search bar width (#7060) 2026-02-13 09:51:55 -06:00
Hayden
dfd5abfb5d chore(l10n): New Crowdin updates (#7059) 2026-02-13 03:52:07 +00:00
renovate[bot]
e2ae5cb5b6 chore(deps): update dependency ruff to v0.15.1 (#7058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 01:08:02 +00:00
Hayden
634aa5cd25 chore(l10n): New Crowdin updates (#7055) 2026-02-13 01:07:47 +00:00
Michael Genson
23c7bd7e3d feat: Customize Ingredient Plural Handling (#7057) 2026-02-12 19:07:23 -06:00
renovate[bot]
9c1ee972c9 fix(deps): update dependency fastapi to v0.129.0 (#7056)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 18:46:30 +00:00
renovate[bot]
1b9023c8c0 chore(deps): update node.js to 00e9195 (#7054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 04:06:13 +00:00
Hayden
3a37cd6959 chore(l10n): New Crowdin updates (#7053) 2026-02-12 03:23:18 +00:00
Michael Genson
8da0d010a5 feat: Add Docker metadata to published images (#7052) 2026-02-11 17:07:40 -06:00
renovate[bot]
37f7f770a8 fix(deps): update dependency fastapi to v0.128.8 (#7049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 16:47:04 +00:00
Hayden
1cebbefd88 chore(l10n): New Crowdin updates (#7048) 2026-02-11 15:19:22 +00:00
Hayden
d55149b904 chore(l10n): New Crowdin updates (#7028) 2026-02-11 13:59:57 +00:00
renovate[bot]
fad7acadfc fix(deps): update dependency openai to v2.20.0 (#7042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 13:59:30 +00:00
renovate[bot]
a539c6cd2e fix(deps): update dependency fastapi to v0.128.7 (#7043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 13:59:13 +00:00
renovate[bot]
7b5502d019 fix(deps): update dependency alembic to v1.18.4 (#7044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 13:56:51 +00:00
renovate[bot]
26d9d8fe24 fix(deps): update dependency pillow to v12.1.1 (#7047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 13:56:08 +00:00
Michael Genson
b64f14aaae feat: Dynamic Placeholders UI (#7034) 2026-02-11 05:43:17 +00:00
renovate[bot]
9b686ecd2b chore(deps): update dependency axios to v1.13.5 [security] (#7041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 21:50:28 +00:00
renovate[bot]
a956a638f4 chore(deps): update dependency coverage to v7.13.4 (#7039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 21:25:33 +00:00
renovate[bot]
c9d9e6822e fix(deps): update dependency fastapi to v0.128.6 (#7040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 21:25:19 +00:00
renovate[bot]
4a563b76ad chore(deps): update dependency setuptools to v82 (#7032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 17:40:17 +00:00
renovate[bot]
73f97c2cca fix(deps): update dependency fastapi to v0.128.5 (#7030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 17:18:42 +00:00
mealie-actions[bot]
75e3c99d72 chore(l10n): Crowdin locale sync (#7029)
Co-authored-by: GitHub Action <action@github.com>
2026-02-08 03:06:17 +00:00
renovate[bot]
217ddd8814 fix(deps): update dependency fastapi to v0.128.4 (#7023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 19:54:18 +00:00
Joey
f2cc8dc922 fix: handle numeric recipeCategory from LLM/site to prevent import failure (#7026) 2026-02-07 19:47:44 +00:00
Hayden
b8329def91 chore(l10n): New Crowdin updates (#7024) 2026-02-07 13:28:44 +00:00
Hayden
2ae7dc3b82 chore(l10n): New Crowdin updates (#7022) 2026-02-07 01:57:29 +00:00
renovate[bot]
510a63a71f chore(deps): update dependency setuptools to v81 (#7021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 22:22:25 +00:00
renovate[bot]
14433819c3 fix(deps): update dependency fastapi to v0.128.3 (#7020)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 21:13:54 +00:00
renovate[bot]
96a9dbccb6 fix(deps): update dependency authlib to v1.6.7 (#7019)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 20:51:27 +00:00
Hayden
cfe20214e5 chore(l10n): New Crowdin updates (#7016) 2026-02-06 12:26:59 +00:00
Hayden
eef54879fe chore(l10n): New Crowdin updates (#7014)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-02-06 00:47:28 +00:00
renovate[bot]
c789ecf0ba fix(deps): update dependency openai to v2.17.0 (#7012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 22:09:12 +00:00
renovate[bot]
008f55e725 fix(deps): update dependency fastapi to v0.128.2 (#7013)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 22:08:58 +00:00
Hayden
bcbe32f503 chore(l10n): New Crowdin updates (#7010) 2026-02-05 00:03:53 +00:00
mealie-commit-bot[bot]
4101797c0e chore: bump version to v3.10.2 2026-02-04 23:32:41 +00:00
Michael Genson
6110200a04 fix: OIDC caching (#7009) 2026-02-04 14:03:40 -06:00
renovate[bot]
49f1e76776 fix(deps): update dependency fastapi to v0.128.1 (#7008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 19:20:25 +00:00
Hayden
24e9417d02 chore(l10n): New Crowdin updates (#7005) 2026-02-04 11:57:13 +00:00
renovate[bot]
69d6985f3b chore(deps): update node.js to 1de022d (#7002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 00:23:07 +00:00
renovate[bot]
84cdeb2398 chore(deps): update dependency coverage to v7.13.3 (#6998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 00:23:00 +00:00
Hayden
6d439de144 chore(l10n): New Crowdin updates (#7004) 2026-02-03 23:33:27 +00:00
Michael Genson
1b586f8c67 chore: Upgrade to ruff 15.0.0 (#7003) 2026-02-03 16:43:42 -06:00
whattheschnell
f82f387146 fix: use BASE_URL config for redirect_url if available (#6995)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-02-03 16:31:20 -06:00
Hayden
d31c07a6c5 chore(l10n): New Crowdin updates (#6997) 2026-02-03 14:15:18 +00:00
renovate[bot]
84372c2f4f chore(deps): update node.js to bdc7252 (#6996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:15:03 +00:00
mealie-commit-bot[bot]
168ac79daa chore: bump version to v3.10.1 2026-02-03 01:04:49 +00:00
Hayden
22296277a8 chore(l10n): New Crowdin updates (#6994) 2026-02-03 00:51:49 +00:00
Michael Genson
6e006458be fix: Button overflow on main page filters (#6992) 2026-02-02 18:44:36 -06:00
Michael Genson
76a2fea076 docs: Typo (#6993) 2026-02-02 16:20:41 -06:00
mealie-commit-bot[bot]
3de4024619 chore: bump version to v3.10.0 2026-02-02 18:33:02 +00:00
renovate[bot]
194771653d chore(deps): pin dependency freezegun to ==1.5.5 (#6991)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 18:18:36 +00:00
renovate[bot]
24aa8f3525 fix(deps): update dependency orjson to v3.11.7 (#6989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:54:33 +00:00
Michael Genson
fb8e318739 fix: Flaky $NOW tests (#6990) 2026-02-02 11:24:13 -06:00
Michael Genson
6255c71609 docs: Misc. cleanup (#6988) 2026-02-02 10:47:23 -06:00
Hayden
f2d1569488 chore(l10n): New Crowdin updates (#6987) 2026-02-02 11:18:56 +00:00
Michael Genson
987c7209fc feat: Query relative dates (#6984) 2026-02-01 21:36:46 -06:00
Hayden
f6dbd1f1f1 chore(l10n): New Crowdin updates (#6983) 2026-02-01 23:23:28 +00:00
Michael Genson
d30118899d fix: Re-enable some style tags (#6982) 2026-02-01 16:24:57 -06:00
Michael Genson
af241dad57 feat: Add range of dates to shopping list from meal planner (#6981) 2026-02-01 15:58:03 -06:00
renovate[bot]
b86de79c6f chore(deps): update dependency rich to v14.3.2 (#6980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 20:28:33 +00:00
Hayden
86e86f8c81 chore(l10n): New Crowdin updates (#6979) 2026-02-01 10:58:51 +00:00
mealie-actions[bot]
d795f91938 chore(l10n): Crowdin locale sync (#6977)
Co-authored-by: GitHub Action <action@github.com>
2026-02-01 04:42:54 +00:00
Michael Genson
a59511cc81 dev: Switch to Mealie bot for auto-merging (#6978) 2026-01-31 22:42:03 -06:00
Michael Genson
a5d4cae6d0 dev: Switch approver for automated i18n merges (#6976) 2026-01-31 21:46:13 -06:00
Michael Genson
2987cf8ba6 dev: Allow locale sync path in auto merge (#6974) 2026-01-31 21:37:31 -06:00
Michael Genson
46b46978ff dev: Increase locale merge limit to 300 and allow PRs from Mealie bot (#6972) 2026-01-31 21:31:52 -06:00
Michael Genson
12857883a9 dev: Fix token vars (#6970) 2026-01-31 21:24:58 -06:00
Michael Genson
60fff3b5b8 dev: Switch to bot token for locale sync (#6969) 2026-01-31 21:21:07 -06:00
Hayden
b42e888929 chore(l10n): New Crowdin updates (#6967) 2026-02-01 00:47:50 +00:00
Michael Genson
570d6f1433 feat: Migrate OpenAI implementation to use structured outputs (#6964) 2026-01-31 11:57:05 -06:00
Morgan
dcf410739e fix: service-worker precache manifest entries not generated correctly (#6815) 2026-01-31 15:51:11 +00:00
Michael Genson
1929d630a1 fix: Remove deprecated warning from shopping list editor (#6963) 2026-01-31 09:44:34 -06:00
Michael Genson
c4c7bf2aed fix: Disable context hover (#6962) 2026-01-31 09:40:31 -06:00
Hayden
47034d18c5 chore(l10n): New Crowdin updates (#6960) 2026-01-31 15:09:56 +00:00
renovate[bot]
7ebe491f74 fix(deps): update dependency ingredient-parser-nlp to v2.5.0 (#6961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 14:52:15 +00:00
Arsène Reymond
719bd89eb1 feat: Improve recipe assets preview (#6602)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-31 14:48:42 +00:00
Hayden
9030c7e6b9 chore(l10n): New Crowdin updates (#6959) 2026-01-30 23:15:01 +00:00
renovate[bot]
0202cc7ef8 fix(deps): update dependency pyjwt to v2.11.0 (#6958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 20:53:19 +00:00
Patrick Lehner (he/him)
381ac9bfde dev: Improve caching in taskfile (#6798) 2026-01-30 20:52:44 +00:00
Patrick Lehner (he/him)
e9fe71c1b7 dev: Add tasks for e2e tests (#6797)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 14:51:40 -06:00
Arsène Reymond
79bbc20cd6 fix: recipe context menu (#6782) 2026-01-30 20:08:49 +00:00
Arsène Reymond
c7be4a452a fix: disable invitations when password login is disabled (#6781) 2026-01-30 20:05:40 +00:00
Imanuel
731ee8ae3d fix: sub-recipes in multi group setup (#6652) (#6663) 2026-01-30 18:50:08 +00:00
Imanuel
c7ae67e7cd feat: Customizable OpenAI prompts (#5146) (#6588)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-01-30 12:00:03 -06:00
Gtt1229
e83891e3ca feat: Added Option to Import Recipe Category During Recipe Import (#6523)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 11:18:15 -06:00
Michael Genson
e3e45c534e dev: Skip Trivy on merge queue (#6957) 2026-01-30 16:47:30 +00:00
Stevie Howard
279cf65673 fix: Seed data - en-US only - correct [some] plural names and add [some] accented characters (#6405)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 16:30:56 +00:00
Fjodor42
cb44ecf394 feat: Add "sprig" as a unit. (#6934)
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-01-30 16:03:41 +00:00
Michael Genson
920eeb26d6 dev: Bunch of GH workflow fixes (#6956) 2026-01-30 15:58:49 +00:00
Hayden
9738d9f363 fix: dispose AlchemyExporter engine after restore completes (#6942) 2026-01-30 15:54:38 +00:00
CodeFaux
37e6123f9e fix: Keep ingredient headers in cook mode (#6946)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 15:36:04 +00:00
Hayden
0a2cabb348 chore(l10n): New Crowdin updates (#6954) 2026-01-30 15:19:02 +00:00
Michael Genson
447a1fb239 dev: Enable CI on merge queues (#6955) 2026-01-30 09:18:52 -06:00
Hayden
b5358896eb fix: use GITHUB_TOKEN for auto-merge to respect CI checks (#6953) 2026-01-30 03:26:10 +00:00
Jérôme
78fbbf0264 fix: correct global scroll strategy to prevent menu fixation (#6577)
Co-authored-by: Jerome <jerome.roth@imt-atlantique.net>
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-01-30 02:27:45 +00:00
Hayden
a33d8204df chore(l10n): New Crowdin updates (#6949)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 02:10:24 +00:00
Hayden
c8046bbdf0 chore: add workflow to auto-merge l10n PRs (#6948)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 02:05:56 +00:00
renovate[bot]
329ad4d8ed fix(deps): update dependency alembic to v1.18.3 (#6945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 00:43:47 +00:00
renovate[bot]
4ccf649aa1 chore(deps): update dependency setuptools to v80.10.2 (#6930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 18:32:44 -06:00
renovate[bot]
5994328a8b fix(deps): update dependency orjson to v3.11.6 (#6952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 13:42:53 -06:00
Hayden
15b5917054 feat: add discard confirmation dialog for recipe editor (#6941)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-28 23:09:32 -06:00
Hayden
e48b150f7c chore(l10n): New Crowdin updates (#6944)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-28 22:57:04 -06:00
renovate[bot]
adbc66316f chore(deps): update dependency coverage to v7.13.2 (#6927)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 22:54:52 -06:00
Hayden
0dc7337972 chore(l10n): New Crowdin updates (#6938) 2026-01-28 19:14:41 +07:00
renovate[bot]
58d4b95a56 fix(deps): update dependency openai to v2.16.0 (#6937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 20:51:07 -06:00
Hayden
0e74bc6cd0 chore(l10n): New Crowdin updates (#6936) 2026-01-27 20:29:06 +00:00
Hayden
4866eec62d chore(l10n): New Crowdin updates (#6935) 2026-01-26 18:34:52 -06:00
github-actions[bot]
c0d659724a chore(auto): Update pre-commit hooks (#6932)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-01-26 16:06:35 +00:00
Hayden
5f0996734a chore(l10n): New Crowdin updates (#6933) 2026-01-26 18:09:28 +07:00
Hayden
8cd0286ca1 chore(l10n): New Crowdin updates (#6929) 2026-01-25 19:49:14 -06:00
renovate[bot]
f214e8843a fix(deps): update dependency python-multipart to v0.0.22 (#6926) 2026-01-25 15:30:40 +00:00
github-actions[bot]
66fea60341 chore(l10n): Crowdin locale sync (#6924) 2026-01-25 15:19:23 +00:00
Hayden
69b4684bce chore(l10n): New Crowdin updates (#6925) 2026-01-25 18:46:24 +07:00
renovate[bot]
b75d6812a3 chore(deps): update dependency rich to v14.3.1 (#6923) 2026-01-24 18:04:27 -06:00
Hayden
ed000c2cc6 chore(l10n): New Crowdin updates (#6922) 2026-01-24 22:45:45 +00:00
Alexandre Eberhardt
d43a2020b3 docs: Add an info callout about client-side cookies. (#6830) 2026-01-24 20:33:19 +00:00
Patrick Lehner (he/him)
ff5e65b323 fix: Make 'auto-search' toggle change color to indicate state (#6809) 2026-01-24 20:21:53 +00:00
Hayden
e1b07a250b chore(l10n): New Crowdin updates (#6912) 2026-01-24 20:07:45 +00:00
renovate[bot]
e68486a0e1 chore(deps): update dependency rich to v14.3.0 (#6919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 13:56:54 -06:00
renovate[bot]
271915ee23 chore(deps): update dependency types-python-dateutil to v2.9.0.20260124 (#6913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 13:43:38 -06:00
renovate[bot]
a3d64c0761 fix(deps): update dependency pillow-heif to v1.2.0 (#6910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 08:50:03 -06:00
Hayden
73c664649d chore(l10n): New Crowdin updates (#6911) 2026-01-23 18:08:07 +07:00
renovate[bot]
d887e68228 chore(deps): update dependency ruff to v0.14.14 (#6909)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 19:59:48 -06:00
Hayden
a8d3ed3310 chore(l10n): New Crowdin updates (#6908) 2026-01-22 15:43:48 -06:00
Hayden
00bd45c8f1 chore(l10n): New Crowdin updates (#6905) 2026-01-22 14:35:50 +00:00
renovate[bot]
05003a5c6f fix(deps): update dependency sqlalchemy to v2.0.46 (#6903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 03:50:27 +00:00
renovate[bot]
e2be09b5d3 chore(deps): update dependency setuptools to v80.10.1 (#6901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 21:39:54 -06:00
github-actions[bot]
b81e0ac03b chore(l10n): Crowdin locale sync (#6891)
Co-authored-by: GitHub Action <action@github.com>
2026-01-22 03:24:04 +00:00
renovate[bot]
c5d822cded chore(deps): update node.js to b2b2184 (#6879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 21:13:45 -06:00
Hayden
0fc66fee9a chore(l10n): New Crowdin updates (#6888) 2026-01-21 03:44:18 +00:00
github-actions[bot]
612c07e6f3 chore(auto): Update pre-commit hooks (#6895) 2026-01-21 02:52:58 +00:00
renovate[bot]
a0ac2923d6 fix(deps): update dependency apprise to v1.9.7 (#6898) 2026-01-20 20:42:15 -06:00
Hayden
7107c08021 chore(l10n): New Crowdin updates (#6886) 2026-01-16 17:13:20 -06:00
renovate[bot]
a0e336edcb fix(deps): update dependency alembic to v1.18.1 (#6878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 13:29:38 -06:00
Hayden
3e306638d0 fix: prevent XSS via javascript: URIs in recipe actions (#6885) 2026-01-16 12:19:27 -06:00
Patrick Lehner (he/him)
a72641b32e feat: Use toggle button for switching any/all mode for search filters (#6833) 2026-01-16 15:52:11 +00:00
renovate[bot]
f4ed9d92bf chore(deps): update dependency ruff to v0.14.13 (#6881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 09:38:32 -06:00
Hayden
5ae35c3500 chore(l10n): New Crowdin updates (#6870) 2026-01-14 23:36:27 +00:00
renovate[bot]
08666e6c21 chore(deps): update node.js to 0ab63ca (#6872) 2026-01-14 17:26:34 -06:00
Hayden
5ae530a637 chore(l10n): New Crowdin updates (#6867) 2026-01-12 14:44:13 -06:00
github-actions[bot]
2b07497486 chore(auto): Update pre-commit hooks (#6866)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-01-12 18:34:46 +00:00
github-actions[bot]
3b65642325 chore(l10n): Crowdin locale sync (#6862)
Co-authored-by: GitHub Action <action@github.com>
2026-01-12 18:20:16 +00:00
Hayden
fdd1057e79 chore(l10n): New Crowdin updates (#6864) 2026-01-12 12:10:00 -06:00
Hayden
f1afebcc04 chore(l10n): New Crowdin updates (#6863) 2026-01-11 17:00:15 +07:00
Hayden
e711be7efa chore(l10n): New Crowdin updates (#6861) 2026-01-10 19:51:06 +00:00
renovate[bot]
ec94b8179c fix(deps): update dependency openai to v2.15.0 (#6859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 22:32:27 -06:00
renovate[bot]
a7c1d6f486 fix(deps): update dependency alembic to v1.18.0 (#6858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-10 03:54:14 +00:00
renovate[bot]
df0b792c52 chore(deps): update dependency ruff to v0.14.11 (#6852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 21:43:04 -06:00
Hayden
1f5054fcbd chore(l10n): New Crowdin updates (#6843) 2026-01-10 03:24:28 +00:00
renovate[bot]
ca483b9cbe chore(deps): update dependency types-requests to v2.32.4.20260107 (#6846) 2026-01-09 21:14:02 -06:00
Mike Nguyen
03dc459162 docs: fix authentik oidc link (#6851) 2026-01-08 20:47:15 +00:00
Michael Genson
cf8f5fe2a2 docs: Fix typo and remove unused tip (#6849) 2026-01-07 14:31:57 -06:00
Hayden
760350ef88 chore(l10n): New Crowdin updates (#6840) 2026-01-05 23:47:44 -06:00
Michael Genson
706d4ee0b5 fix: Coerce null servings into 0 servings (#6839) 2026-01-05 17:49:34 -06:00
Patrick Lehner (he/him)
5fd8545cbe fix: Tags can't be renamed (#6835) 2026-01-05 09:46:48 -06:00
Hayden
3397c06db2 chore(l10n): New Crowdin updates (#6832) 2026-01-04 20:00:39 -06:00
Hayden
22df7a1ec7 chore(l10n): New Crowdin updates (#6827) 2026-01-04 09:58:19 -06:00
renovate[bot]
e87b0c75b6 fix(deps): update dependency pillow to v12.1.0 (#6816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 23:34:36 -06:00
Hayden
b406b7fa16 chore(l10n): New Crowdin updates (#6806)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-04 03:18:35 +00:00
github-actions[bot]
7114ed1122 chore(l10n): Crowdin locale sync (#6825)
Co-authored-by: GitHub Action <action@github.com>
2026-01-03 21:08:10 -06:00
938 changed files with 108324 additions and 103580 deletions

View File

@@ -1,9 +1,10 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT="3.12-bullseye"
FROM mcr.microsoft.com/devcontainers/python:${VARIANT}
# Remove outdated yarn GPG key, if it exists
RUN rm -f /etc/apt/sources.list.d/yarn.list /usr/share/keyrings/yarn-archive-keyring.gpg || true
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
@@ -20,6 +21,7 @@ RUN apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
build-essential \
ffmpeg \
libpq-dev \
libwebp-dev \
libsasl2-dev libldap2-dev libssl-dev \

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:
@@ -61,3 +61,12 @@ _(fill-in or delete this section)_
<!--
Describe how you tested this change.
-->
## AI / LLM Assistance
_(REQUIRED)_
<!--
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

115
.github/workflows/auto-merge-l10n.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
name: Auto-merge l10n 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, 'l10n')
steps:
- name: Validate PR author
env:
AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
if [[
"$AUTHOR" != "hay-kot" &&
"$AUTHOR" != "github-actions[bot]" &&
"$AUTHOR" != "mealie-actions[bot]"
]]; then
echo "::error::PR author must be hay-kot, github-actions[bot], or mealie-actions[bot] for auto-merge (got: $AUTHOR)"
exit 1
fi
echo "Author validated: $AUTHOR"
- name: Validate PR size
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
ADDITIONS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json additions --jq '.additions')
DELETIONS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json deletions --jq '.deletions')
TOTAL=$((ADDITIONS + DELETIONS))
echo "PR changes: +$ADDITIONS -$DELETIONS (total: $TOTAL lines)"
if [ "$TOTAL" -gt 6000 ]; then
echo "::error::PR exceeds 6000 line change limit ($TOTAL lines)"
exit 1
fi
- 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
# Check if file matches any allowed path
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
fi
# File doesn't match allowed paths
echo "::error::Invalid file path: $file"
echo "Only the following paths are allowed:"
echo " - frontend/app/composables/use-locales/available-locales.ts"
echo " - frontend/app/lang/"
echo " - mealie/lang/"
echo " - mealie/repos/seed/resources/*/locales/"
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: l10n PR from trusted author 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

@@ -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

@@ -15,13 +15,20 @@ jobs:
sync-locales:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout repository
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"
@@ -30,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') }}
@@ -105,7 +112,7 @@ jobs:
- Updated frontend locale files
- Generated from latest translation sources" \
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: No changes detected
if: steps.changes.outputs.has_changes == 'false'

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,25 +23,39 @@ 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 }}
- uses: depot/setup-action@v1
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: |
hkotel/mealie
ghcr.io/${{ github.repository }}
# 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
@@ -57,5 +74,6 @@ jobs:
hkotel/mealie:${{ inputs.tag }}
ghcr.io/${{ github.repository }}:${{ inputs.tag }}
${{ 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

@@ -4,14 +4,19 @@ on:
pull_request:
branches:
- mealie-next
merge_group:
types: [checks_requested]
branches:
- mealie-next
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.merge_group.head_ref }}
cancel-in-progress: true
jobs:
pull-request-lint:
name: "Lint PR"
if: github.event_name == 'pull_request'
uses: ./.github/workflows/pull-request-lint.yml
backend-tests:
@@ -24,6 +29,7 @@ jobs:
container-scanning:
name: "Trivy Container Scanning"
if: github.event_name == 'pull_request'
uses: ./.github/workflows/partial-trivy-container-scanning.yml
code-ql:
@@ -47,7 +53,10 @@ jobs:
publish-image:
name: "Publish PR Image"
if: contains(github.event.pull_request.labels.*.name, 'build-image') && github.repository == 'mealie-recipes/mealie'
if: |
github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'build-image') &&
github.repository == 'mealie-recipes/mealie'
permissions:
contents: read
packages: write

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
@@ -40,12 +40,18 @@ jobs:
shell: bash
run: pre-commit autoupdate --color=always
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
# This doesn't currently work for us because it creates the PR but the workflows don't run.
# TODO: Provide a personal access token as a parameter here, that solves that problem.
# https://github.com/peter-evans/create-pull-request
- 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: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@v6
with:
token: ${{ steps.app-token.outputs.token }}
commit-message: "Update pre-commit hooks"
branch: "fix/update-pre-commit-hooks"
labels: |
@@ -54,3 +60,38 @@ jobs:
base: mealie-next
title: "chore(auto): Update pre-commit hooks"
body: "Auto-generated by `.github/workflows/scheduled-checks.yml`"
- name: Approve PR
if: steps.create-pr.outputs.pull-request-number
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.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: Pre-commit hook updates"
- name: Enable auto-merge
if: steps.create-pr.outputs.pull-request-number
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pull-request-number }}
REPO: ${{ github.repository }}
run: |
gh pr merge "$PR_NUMBER" \
--repo "$REPO" \
--auto \
--squash

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,10 @@ repos:
exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.10
rev: v0.15.8
hooks:
- id: ruff
# Linter
- id: ruff-check
args: [ --fix ]
# Formatter
- id: ruff-format

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

@@ -25,16 +25,9 @@ dotenv:
- .env
- .dev.env
tasks:
docs:gen:
desc: runs the API documentation generator
cmds:
- uv run python dev/code-generation/gen_docs_api.py
docs:
desc: runs the documentation server
dir: docs
deps:
- docs:gen
cmds:
- uv run python -m mkdocs serve
@@ -47,8 +40,6 @@ tasks:
sources:
- package.json
- yarn.lock
generates:
- node_modules/**
setup:py:
desc: setup python dependencies
@@ -61,6 +52,18 @@ tasks:
- pyproject.toml
- .pre-commit-config.yaml
setup:e2e:
desc: setup e2e test dependencies
dir: tests/e2e
run: once
cmds:
- yarn install
- yarn playwright install --with-deps
sources:
- package.json
- playwright.config.ts
- yarn.lock
setup:
desc: setup all dependencies
deps:
@@ -71,7 +74,6 @@ tasks:
desc: run code generators
cmds:
- uv run python dev/code-generation/main.py {{ .CLI_ARGS }}
- task: docs:gen
- task: py:format
dev:services:
@@ -179,12 +181,21 @@ tasks:
status:
- '{{ .SKIP_PACKAGE_DEPS | default "false"}}'
py:package:
desc: builds Python packages (sdist and wheel) in top-level dist directory
py:package:build:
internal: true
deps:
- py:package:deps
cmds:
- uv build --out-dir dist
sources:
- uv.lock
- pyproject.toml
- mealie/**
py:package:
desc: builds Python packages (sdist and wheel) in top-level dist directory
cmds:
- task: py:package:build
- task: py:package:generate-requirements
py:
@@ -215,6 +226,12 @@ tasks:
dir: frontend
cmds:
- yarn build
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:generate:
desc: generates a static version of the frontend in frontend/dist
@@ -223,18 +240,36 @@ tasks:
- setup:ui
cmds:
- yarn generate
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:lint:
desc: runs the frontend linter
dir: frontend
cmds:
- yarn lint --max-warnings=0
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:test:
desc: runs the frontend tests
dir: frontend
cmds:
- yarn test
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:check:
desc: runs all frontend checks
@@ -263,3 +298,48 @@ tasks:
dir: docker
cmds:
- docker compose -f docker-compose.yml -p mealie up -d --build
e2e:build-image:
desc: builds the e2e test docker image
deps:
- py:package
cmds:
- docker build --tag mealie:e2e --file docker/Dockerfile --build-context packages=dist .
sources:
- docker/Dockerfile
- dist/**
e2e:start-server:
desc: Builds the image and starts the containers for e2e testing
dir: tests/e2e/docker
deps:
- e2e:build-image
vars:
WAIT_UNTIL_HEALTHY: '{{if .WAIT_UNTIL_HEALTHY}}--wait{{else}}{{end}}'
cmds:
- docker compose up -d {{.WAIT_UNTIL_HEALTHY}}
e2e:stop-server:
desc: Shuts down the e2e testing containers
dir: tests/e2e/docker
cmds:
- docker compose down --volumes
e2e:test:
desc: runs the e2e tests
dir: tests/e2e
deps:
- setup:e2e
vars:
PREVENT_REPORT_OPEN: '{{if .PREVENT_REPORT_OPEN}}PLAYWRIGHT_HTML_OPEN=never{{else}}{{end}}'
cmds:
- '{{.PREVENT_REPORT_OPEN}} yarn playwright test'
e2e:
desc: runs the full e2e test suite
cmds:
- task: e2e:start-server
vars: { WAIT_UNTIL_HEALTHY: true }
- defer: { task: e2e:stop-server }
- task: e2e:test
vars: { PREVENT_REPORT_OPEN: true }

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

@@ -1,80 +0,0 @@
import json
from datetime import UTC, datetime
from typing import Any
from fastapi import FastAPI
from mealie.app import app
from mealie.core.config import determine_data_dir
DATA_DIR = determine_data_dir()
"""Script to export the ReDoc documentation page into a standalone HTML file."""
HTML_TEMPLATE = """<!-- Custom HTML site displayed as the Home chapter -->
{% extends "main.html" %}
{% block tabs %}
{{ super() }}
<style>
body {
margin: 0;
padding: 0;
}
</style>
<div id="redoc-container"></div>
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
<script>
var spec = MY_SPECIFIC_TEXT;
Redoc.init(spec, {}, document.getElementById("redoc-container"));
</script>
{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}
"""
HTML_PATH = DATA_DIR.parent.parent.joinpath("docs/docs/overrides/api.html")
CONSTANT_DT = datetime(2025, 10, 24, 15, 53, 0, 0, tzinfo=UTC)
def normalize_timestamps(s: dict[str, Any]) -> dict[str, Any]:
field_format = s.get("format")
is_timestamp = field_format in ["date-time", "date", "time"]
has_default = s.get("default")
if not is_timestamp:
for k, v in s.items():
if isinstance(v, dict):
s[k] = normalize_timestamps(v)
elif isinstance(v, list):
s[k] = [normalize_timestamps(i) if isinstance(i, dict) else i for i in v]
return s
elif not has_default:
return s
if field_format == "date-time":
s["default"] = CONSTANT_DT.isoformat()
elif field_format == "date":
s["default"] = CONSTANT_DT.date().isoformat()
elif field_format == "time":
s["default"] = CONSTANT_DT.time().isoformat()
return s
def generate_api_docs(my_app: FastAPI):
openapi_schema = my_app.openapi()
openapi_schema = normalize_timestamps(openapi_schema)
with open(HTML_PATH, "w") as fd:
text = HTML_TEMPLATE.replace("MY_SPECIFIC_TEXT", json.dumps(openapi_schema))
fd.write(text)
if __name__ == "__main__":
generate_api_docs(app)

View File

@@ -1,6 +1,7 @@
import json
import os
import pathlib
from dataclasses import dataclass
import re
from pathlib import Path
import dotenv
@@ -10,6 +11,7 @@ from pydantic import ConfigDict
from requests import Response
from utils import CodeDest, CodeKeys, inject_inline, log
from mealie.lang.locale_config import LOCALE_CONFIG, LocalePluralFoodHandling, LocaleTextDirection
from mealie.schema._mealie import MealieModel
BASE = pathlib.Path(__file__).parent.parent.parent
@@ -17,57 +19,6 @@ BASE = pathlib.Path(__file__).parent.parent.parent
API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") or os.environ.get("CROWDIN_API_KEY", "")
@dataclass
class LocaleData:
name: str
dir: str = "ltr"
LOCALE_DATA: dict[str, LocaleData] = {
"af-ZA": LocaleData(name="Afrikaans (Afrikaans)"),
"ar-SA": LocaleData(name="العربية (Arabic)", dir="rtl"),
"bg-BG": LocaleData(name="Български (Bulgarian)"),
"ca-ES": LocaleData(name="Català (Catalan)"),
"cs-CZ": LocaleData(name="Čeština (Czech)"),
"da-DK": LocaleData(name="Dansk (Danish)"),
"de-DE": LocaleData(name="Deutsch (German)"),
"el-GR": LocaleData(name="Ελληνικά (Greek)"),
"en-GB": LocaleData(name="British English"),
"en-US": LocaleData(name="American English"),
"es-ES": LocaleData(name="Español (Spanish)"),
"et-EE": LocaleData(name="Eesti (Estonian)"),
"fi-FI": LocaleData(name="Suomi (Finnish)"),
"fr-BE": LocaleData(name="Belge (Belgian)"),
"fr-CA": LocaleData(name="Français canadien (Canadian French)"),
"fr-FR": LocaleData(name="Français (French)"),
"gl-ES": LocaleData(name="Galego (Galician)"),
"he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"),
"hr-HR": LocaleData(name="Hrvatski (Croatian)"),
"hu-HU": LocaleData(name="Magyar (Hungarian)"),
"is-IS": LocaleData(name="Íslenska (Icelandic)"),
"it-IT": LocaleData(name="Italiano (Italian)"),
"ja-JP": LocaleData(name="日本語 (Japanese)"),
"ko-KR": LocaleData(name="한국어 (Korean)"),
"lt-LT": LocaleData(name="Lietuvių (Lithuanian)"),
"lv-LV": LocaleData(name="Latviešu (Latvian)"),
"nl-NL": LocaleData(name="Nederlands (Dutch)"),
"no-NO": LocaleData(name="Norsk (Norwegian)"),
"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)"),
"sk-SK": LocaleData(name="Slovenčina (Slovak)"),
"sl-SI": LocaleData(name="Slovenščina (Slovenian)"),
"sr-SP": LocaleData(name="српски (Serbian)"),
"sv-SE": LocaleData(name="Svenska (Swedish)"),
"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_ts_locales.py
export const LOCALES = [{% for locale in locales %}
{
@@ -75,6 +26,7 @@ export const LOCALES = [{% for locale in locales %}
value: "{{ locale.locale }}",
progress: {{ locale.progress }},
dir: "{{ locale.dir }}",
pluralFoodHandling: "{{ locale.plural_food_handling }}",
},{% endfor %}
];
@@ -87,10 +39,11 @@ class TargetLanguage(MealieModel):
id: str
name: str
locale: str
dir: str = "ltr"
dir: LocaleTextDirection = LocaleTextDirection.LTR
plural_food_handling: LocalePluralFoodHandling = LocalePluralFoodHandling.ALWAYS
threeLettersCode: str
twoLettersCode: str
progress: float = 0.0
progress: int = 0
class CrowdinApi:
@@ -117,52 +70,24 @@ class CrowdinApi:
def get_languages(self) -> list[TargetLanguage]:
response = self.get_project()
tls = response.json()["data"]["targetLanguages"]
return [TargetLanguage(**t) for t in tls]
models = [TargetLanguage(**t) for t in tls]
models.insert(
0,
TargetLanguage(
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 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:
model.progress = p["data"]["translationProgress"]
models.sort(key=lambda x: x.locale, reverse=True)
return models
def get_progress(self) -> dict:
def get_progress(self) -> dict[str, int]:
response = requests.get(
f"https://api.crowdin.com/api/v2/projects/{self.project_id}/languages/progress?limit=500",
headers=self.headers,
)
return response.json()
data = response.json()["data"]
return {p["data"]["languageId"]: p["data"]["translationProgress"] for p in data}
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"
"""
@@ -195,8 +120,8 @@ def inject_nuxt_values():
all_langs = []
for match in locales_dir.glob("*.json"):
match_data = LOCALE_DATA.get(match.stem)
match_dir = match_data.dir if match_data else "ltr"
match_data = LOCALE_CONFIG.get(match.stem)
match_dir = match_data.dir if match_data else LocaleTextDirection.LTR
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}", dir: "{match_dir}" }},'
all_langs.append(lang_string)
@@ -221,9 +146,82 @@ def inject_registration_validation_values():
inject_inline(reg_valid, CodeKeys.nuxt_local_messages, all_langs)
def _get_local_models() -> list[TargetLanguage]:
return [
TargetLanguage(
id=locale,
name=data.name,
locale=locale,
threeLettersCode=locale.split("-")[-1],
twoLettersCode=locale.split("-")[-1],
)
for locale, data in LOCALE_CONFIG.items()
if locale != "en-US" # Crowdin doesn't include this, so we manually inject it later
]
def _get_local_progress() -> dict[str, int]:
with open(CodeDest.use_locales) as f:
content = f.read()
# Extract the array content between [ and ]
match = re.search(r"export const LOCALES = (\[.*?\]);", content, re.DOTALL)
if not match:
raise ValueError("Could not find LOCALES array in file")
# Convert JS to JSON
array_content = match.group(1)
# Replace unquoted keys with quoted keys for valid JSON
# This converts: { name: "value" } to { "name": "value" }
json_str = re.sub(r"([,\{\s])([a-zA-Z_][a-zA-Z0-9_]*)\s*:", r'\1"\2":', array_content)
# Remove trailing commas before } and ]
json_str = re.sub(r",(\s*[}\]])", r"\1", json_str)
locales = json.loads(json_str)
return {locale["value"]: locale["progress"] for locale in locales}
def get_languages() -> list[TargetLanguage]:
if API_KEY:
api = CrowdinApi(None)
models = api.get_languages()
progress = api.get_progress()
else:
log.warning("CROWDIN_API_KEY is not set, using local lanugages instead")
log.warning("DOUBLE CHECK the output!!! Do not overwrite with bad local locale data!")
models = _get_local_models()
progress = _get_local_progress()
models.insert(
0,
TargetLanguage(
id="en-US",
name="English",
locale="en-US",
dir=LocaleTextDirection.LTR,
plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT,
threeLettersCode="en",
twoLettersCode="en",
progress=100,
),
)
for model in models:
if model.locale in LOCALE_CONFIG:
locale_data = LOCALE_CONFIG[model.locale]
model.name = locale_data.name
model.dir = locale_data.dir
model.plural_food_handling = locale_data.plural_food_handling
model.progress = progress.get(model.id, model.progress)
models.sort(key=lambda x: x.locale, reverse=True)
return models
def generate_locales_ts_file():
api = CrowdinApi(None)
models = api.get_languages()
models = get_languages()
tmpl = Template(LOCALE_TEMPLATE)
rendered = tmpl.render(locales=models)
@@ -233,10 +231,6 @@ def generate_locales_ts_file():
def main():
if API_KEY is None or API_KEY == "":
log.error("CROWDIN_API_KEY is not set")
return
generate_locales_ts_file()
inject_nuxt_values()
inject_registration_validation_values()

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:b52a8d1206132b36d60e51e413d9a81336e8a0206d3b648cabd6d5a49c4c0f54 \
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"
@@ -91,6 +91,7 @@ RUN apt-get update \
build-essential \
libpq-dev \
libwebp-dev \
ffmpeg \
# LDAP Dependencies
libsasl2-dev libldap2-dev libssl-dev \
gnupg gnupg2 gnupg1 \
@@ -111,7 +112,6 @@ RUN . $VENV_PATH/bin/activate \
# Production Image
###############################################
FROM python-base AS production
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
ENV PRODUCTION=true
ENV TESTING=false
@@ -120,6 +120,8 @@ ENV GIT_COMMIT_HASH=$COMMIT
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
ffmpeg \
gosu \
iproute2 \
libldap-common \
@@ -142,7 +144,9 @@ ENV APP_PORT=9000
EXPOSE ${APP_PORT}
HEALTHCHECK CMD python -m mealie.scripts.healthcheck || exit 1
COPY ./docker/healthcheck.sh $MEALIE_HOME/healthcheck.sh
RUN chmod +x $MEALIE_HOME/healthcheck.sh
HEALTHCHECK CMD $MEALIE_HOME/healthcheck.sh
ENV HOST 0.0.0.0

12
docker/healthcheck.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
PORT="${API_PORT:-9000}"
if [ -n "$TLS_CERTIFICATE_PATH" ] && [ -n "$TLS_PRIVATE_KEY_PATH" ]; then
PROTO="https"
else
PROTO="http"
fi
# -k: TLS certificate is likely not issued for 127.0.0.1, so don't verify
curl -fsk "${PROTO}://127.0.0.1:${PORT}/api/app/about" > /dev/null

View File

@@ -1,4 +0,0 @@
---
title: API
template: api.html
---

View File

@@ -6,7 +6,7 @@ While this guide aims to simplify the migration process for developers, it's not
## V1 → V2
The biggest change between V1 and V2 is the introduction of Households. For more information on how households work in relation to groups/users, check out the [Groups and Households](./features.md#groups-and-households) section in the Features guide.
The biggest change between V1 and V2 is the introduction of Households. For more information on how households work in relation to groups/users, check out the [Groups and Households](../../documentation/getting-started/features.md#groups-and-households) section in the Features guide.
### `updateAt` is now `updatedAt`

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

@@ -16,7 +16,7 @@ Recipes extras are a key feature of the Mealie API. They allow you to create cus
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
#### Shopping List and Food Extras
Similarly to recipes, extras are supported on shopping lists, shopping list items, and foods. At this time they are only accessible through the API. Extras for these objects allow for rich integrations between the Mealie shopping list and your favorite list manager, such as Alexa, ToDoist, Trello, or any other list manager with an API.
Similarly to recipes, extras are supported on shopping lists, shopping list items, and foods. At this time they are only accessible through the API. Extras for these objects allow for rich integrations between the Mealie shopping list and your favorite list manager, such as Todoist, Trello, or any other list manager with an API.
To keep shopping lists in sync, for instance, you can store your Trello list id on your Mealie shopping list: <br />
`{"trello_list_id": "5abbe4b7ddc1b351ef961414"}`
@@ -52,6 +52,7 @@ Many applications will keep track of the query and adjust the page parameter app
Notice that the route does not contain the baseurl (e.g. `https://mymealieapplication.com/api`).
There are a few shorthands available to reduce the number of calls for certain common requests:
- if you want to return _all_ results, effectively disabling pagination, set `perPage = -1` (and fetch the first page)
- if you want to fetch the _last_ page, set `page = -1`
@@ -78,8 +79,8 @@ This filter will find all foods that are not named "carrot": <br>
##### Keyword Filters
The API supports many SQL keywords, such as `IS NULL` and `IN`, as well as their negations (e.g. `IS NOT NULL` and `NOT IN`).
Here is an example of a filter that returns all recipes where the "last made" value is not null: <br>
`lastMade IS NOT NULL`
Here is an example of a filter that returns all shopping list items without a food: <br>
`foodId IS NULL`
This filter will find all recipes that don't start with the word "Test": <br>
`name NOT LIKE "Test%"`
@@ -89,6 +90,28 @@ This filter will find all recipes that don't start with the word "Test": <br>
This filter will find all recipes that have particular slugs: <br>
`slug IN ["pasta-fagioli", "delicious-ramen"]`
##### Placeholder Keywords
You can use placeholders to insert dynamic values as opposed to static values. Currently the only supported placeholder keyword is `$NOW`, to insert the current date/time.
`$NOW` can optionally be paired with basic offsets. Here is an example of a filter which gives you recipes not made within the past 30 days: <br>
`lastMade <= "$NOW-30d"`
Supported offset operations include:
- `-` for subtracting a time (i.e. in the past)
- `+` for adding a time (i.e. in the future)
Supported offset intervals include:
- `y` for years
- `m` for months
- `d` for days
- `H` for hours
- `M` for minutes
- `S` for seconds
Note that intervals are _case sensitive_ (e.g. `s` is an invalid interval).
##### Nested Property filters
When querying tables with relationships, you can filter properties on related tables. For instance, if you want to query all recipes owned by a particular user: <br>
`user.username = "SousChef20220320"`
@@ -96,7 +119,7 @@ When querying tables with relationships, you can filter properties on related ta
This timeline event filter will return all timeline events for recipes that were created after a particular date: <br>
`recipe.createdAt >= "2023-02-25"`
This recipe filter will return all recipes that contains a particular set of tags: <br>
This recipe filter will return all recipes that contain a particular set of tags: <br>
`tags.name CONTAINS ALL ["Easy", "Cajun"]`
##### Compound Filters

View File

@@ -9,7 +9,7 @@
Mealie supports 3rd party authentication via [OpenID Connect (OIDC)](https://openid.net/connect/), an identity layer built on top of OAuth2. OIDC is supported by many Identity Providers (IdP), including:
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
- [Authentik](https://integrations.goauthentik.io/documentation/mealie/)
- [Authelia](https://www.authelia.com/integration/openid-connect/mealie/)
- [Keycloak](https://www.keycloak.org/docs/latest/securing_apps/#_oidc)
- [Okta](https://www.okta.com/openid-connect/)
@@ -68,7 +68,6 @@ Example configurations for several Identity Providers have been provided by the
If you don't see your provider and have successfully set it up, please consider [creating your own example](https://github.com/mealie-recipes/mealie/discussions/new?category=oauth-provider-example) so that others can have a smoother setup.
## Migration from Mealie v1.x
**High level changes**

View File

@@ -5,9 +5,11 @@
## Recipes
### Creating Recipes
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.
Mealie offers several ways to create recipes:
- **Recipe Scraper:** Create recipes from hundreds of websites by simply providing a URL.
- **Image Import:** Upload an image of a written or typed recipe and Mealie will use OCR to import it.
- **Video URL Import:** Provide a video URL (e.g., YouTube) and Mealie will transcribe the audio and parse the recipe.
- **Manual Editor:** Create recipes from scratch using the integrated editor.
[Creation Demo](https://demo.mealie.io/g/home/r/create/url){ .md-button .md-button--primary .align-right }
### Importing Recipes
@@ -85,13 +87,13 @@ The meal planner has the concept of plan rules. These offer a flexible way to us
The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list or link a recipe and all of it's ingredients to track meals during the week.
Managing shopping lists can be done from the Sidebar > Shopping Lists.
Managing shopping lists can be done from the Sidebar > Shopping Lists.
Here you will be able to:
- See items already on the Shopping List
- See linked recipes with ingredients
- Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it.
- Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it.
- Check off an item
- Add / Change / Remove / Sort Items via the grid icon
- Be sure if you are modifying an ingredient to click the 'Save' icon.
@@ -103,13 +105,10 @@ Here you will be able to:
!!! tip
You can use Labels to categorize your ingredients. You may want to Label by Food Type (Frozen, Fresh, etc), by Store, Tool, Recipe, or more. Play around with this to see what works best for you.
!!! tip
You can toggle 'Food' on items so that if you add multiple of the same food / ingredient, Mealie will automatically combine them together. Do this by editing an item in the Shopping List and clicking the 'Apple' icon. If you then have recipes that contain "1 | cup | cheese" and "2 | cup | cheese" this would be combined to show "3 cups of cheese."
[See FAQ for more information](../getting-started/faq.md)
[Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
## Integrations
@@ -198,7 +197,7 @@ Mealie lets you fully customize how you organize your users. You can use Groups
### Groups
Groups are fully isolated instances of Mealie. Think of a goup as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
Groups are fully isolated instances of Mealie. Think of a group as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
Common use cases for groups include:

View File

@@ -122,17 +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 | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
| 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
@@ -235,6 +238,10 @@ The examples below provide copy-ready Docker Compose environment configurations
THEME_DARK_ERROR: '#E57373'
```
!!! info
Browser cookies may cause the client to keep outdated settings.
Clearing the cookies can be required for the change to take effect.
### Docker Secrets
> <super>&dagger;</super> Starting in version `2.4.2`, any environment variable in the preceding lists with a dagger

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.9.2`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.15.0`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container

View File

@@ -10,9 +10,15 @@ For most users, supplying the OpenAI API key is all you need to do; you will use
Alternatively, if you have another service you'd like to use in-place of OpenAI, you can configure Mealie to use that instead, as long as it has an OpenAI-compatible API. For instance, a common self-hosted alternative to OpenAI is [Ollama](https://ollama.com/). To use Ollama with Mealie, change your `OPENAI_BASE_URL` to `http://localhost:11434/v1` (where `http://localhost:11434` is wherever you're hosting Ollama, and `/v1` enables the OpenAI-compatible endpoints). Note that you *must* provide an API key, even though it is ultimately ignored by Ollama.
If you wish to disable image recognition features (to save costs, or because your custom model doesn't support them) you can set `OPENAI_ENABLE_IMAGE_SERVICES` to `False`. For more information on what configuration options are available, check out the [backend configuration](./backend-config.md#openai).
If you wish to disable image recognition features (to save costs, or because your custom model doesn't support them) you can set `OPENAI_ENABLE_IMAGE_SERVICES` to `False`.
If you wish to disable transcription features (to save costs, or because your custom model doesn't support them) you can set `OPENAI_ENABLE_TRANSCRIPTION_SERVICES` to `False`.
For more information on what configuration options are available, check out the [backend configuration](./backend-config.md#openai).
## OpenAI Features
- The OpenAI Ingredient Parser can be used as an alternative to the NLP and Brute Force parsers. Simply choose the OpenAI parser while parsing ingredients (:octicons-tag-24: v1.7.0)
- When importing a recipe via URL, if the default recipe scraper is unable to read the recipe data from a webpage, the webpage contents will be parsed by OpenAI (:octicons-tag-24: v1.9.0)
- You can import an image of a written recipe, which is sent to OpenAI and imported into Mealie. The recipe can be hand-written or typed, as long as the text is in the picture. You can also optionally have OpenAI translate the recipe into your own language (:octicons-tag-24: v1.12.0)
- You can import a recipe via a video URL (e.g., a YouTube link). The video is transcribed using OpenAI's Whisper model, and the transcription is parsed into a recipe (:octicons-tag-24: v3.13.0)

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.9.2 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.15.0 # (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.9.2 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.15.0 # (3)
container_name: mealie
restart: always
ports:

File diff suppressed because one or more lines are too long

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

@@ -93,7 +93,7 @@ nav:
- iOS Shortcut: "documentation/community-guide/ios-shortcut.md"
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
- API Reference: "api/redoc.md"
- API Reference: "https://demo.mealie.io/docs"
- Contributors Guide:
- Non-Code: "contributors/non-coders.md"

View File

@@ -16,6 +16,10 @@
max-width: 950px !important;
}
.lg-container {
max-width: 1100px !important;
}
.theme--dark.v-application {
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
}
@@ -57,10 +61,6 @@
max-width: 100%;
}
a {
color: rgb(var(--v-theme-primary));
}
.fill-height {
min-height: 100vh;
}
@@ -68,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

@@ -73,11 +73,11 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
import type { ReadCookBook } from "~/lib/api/types/cookbook";
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
const $auth = useMealieAuth();
const auth = useMealieAuth();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const slug = route.params.slug as string;
@@ -88,11 +88,11 @@ const router = useRouter();
const book = getOne(slug);
const isOwnHousehold = computed(() => {
if (!($auth.user.value && book.value?.householdId)) {
if (!(auth.user.value && book.value?.householdId)) {
return false;
}
return $auth.user.value.householdId === book.value.householdId;
return auth.user.value.householdId === book.value.householdId;
});
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);

View File

@@ -0,0 +1,230 @@
<template>
<!-- Create Dialog -->
<BaseDialog
v-model="createDialog"
:title="createTitle || $t('general.create')"
:icon="icon"
color="primary"
max-width="600px"
width="100%"
:submit-disabled="!createFormValid"
can-confirm
@confirm="emit('create-one', createForm.data)"
>
<div class="mx-2 mt-2">
<slot name="create-dialog-top" />
<AutoForm
v-model="createForm.data"
v-model:is-valid="createFormValid"
:items="createForm.items"
class="py-2"
/>
</div>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="editDialog"
:title="editTitle || $t('general.edit')"
:icon="icon"
color="primary"
max-width="600px"
width="100%"
:submit-disabled="!editFormValid"
can-confirm
@confirm="emit('edit-one', editForm.data)"
>
<div class="mx-2 mt-2">
<slot name="edit-dialog-top" />
<AutoForm
v-model="editForm.data"
v-model:is-valid="editFormValid"
:items="editForm.items"
class="py-2"
/>
</div>
<template #custom-card-action>
<slot name="edit-dialog-custom-action" />
</template>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="$emit('deleteOne', deleteTarget.id)"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p v-if="deleteTarget" class="mt-4 ml-4">
{{ deleteTarget.name || deleteTarget.title || deleteTarget.id }}
</p>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="$emit('bulk-action', 'delete-selected', bulkDeleteTarget)"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll height="400" item-height="25" :items="bulkDeleteTarget">
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name || item.title || item.id }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<BaseCardSectionTitle
:icon="icon"
section
:title="title"
/>
<CrudTable
:headers="tableHeaders"
:table-config="tableConfig"
:data="data || []"
:bulk-actions="bulkActions"
:initial-sort="initialSort"
@edit-one="editEventHandler"
@delete-one="deleteEventHandler"
@bulk-action="handleBulkAction"
>
<template
v-for="slotName in itemSlotNames"
#[slotName]="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
/>
</template>
<template #button-row>
<BaseButton
create
@click="createDialog = true"
>
{{ $t("general.create") }}
</BaseButton>
<slot name="table-button-row" />
</template>
<template #button-bottom>
<slot name="table-button-bottom" />
</template>
</CrudTable>
</template>
<script setup lang="ts">
import type { TableHeaders, TableConfig, BulkAction } from "~/components/global/CrudTable.vue";
import type { AutoFormItems } from "~/types/auto-forms";
const slots = useSlots();
const emit = defineEmits<{
(e: "deleteOne", id: string): void;
(e: "deleteMany", ids: string[]): void;
(e: "create-one" | "edit-one", data: any): void;
(e: "bulk-action", event: string, items: any[]): void;
}>();
const tableHeaders = defineModel<TableHeaders[]>("tableHeaders", { required: true });
const createForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("createForm", { required: true });
const createDialog = defineModel("createDialog", { type: Boolean, default: false });
const editForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("editForm", { required: true });
const editDialog = defineModel("editDialog", { type: Boolean, default: false });
defineProps({
icon: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
createTitle: {
type: String,
},
editTitle: {
type: String,
},
tableConfig: {
type: Object as PropType<TableConfig>,
default: () => ({
hideColumns: false,
canExport: true,
}),
},
data: {
type: Array as PropType<Array<any>>,
required: true,
},
bulkActions: {
type: Array as PropType<BulkAction[]>,
required: true,
},
initialSort: {
type: String,
default: "name",
},
});
// ============================================================
// Bulk Action Handler
function handleBulkAction(event: string, items: any[]) {
if (event === "delete-selected") {
bulkDeleteEventHandler(items);
return;
}
emit("bulk-action", event, items);
}
// ============================================================
// Create & Edit
const createFormValid = ref(false);
const editFormValid = ref(false);
const itemSlotNames = computed(() => Object.keys(slots).filter(slotName => slotName.startsWith("item.")));
const editEventHandler = (item: any) => {
editForm.value.data = { ...item };
editDialog.value = true;
};
// ============================================================
// Delete Logic
const deleteTarget = ref<any>(null);
const deleteDialog = ref(false);
function deleteEventHandler(item: any) {
deleteTarget.value = item;
deleteDialog.value = true;
}
// ============================================================
// Bulk Delete Logic
const bulkDeleteTarget = ref<Array<any>>([]);
const bulkDeleteDialog = ref(false);
function bulkDeleteEventHandler(items: Array<any>) {
bulkDeleteTarget.value = items;
bulkDeleteDialog.value = true;
console.log("Bulk Delete Event Handler", items);
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div v-if="preferences">
<BaseCardSectionTitle :title="$t('group.group-preferences')" />
<div class="mb-6">
<v-checkbox
v-model="preferences.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="preferences.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 });
</script>

View File

@@ -15,7 +15,6 @@
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
:open-on-hover="mdAndUp"
content-class="d-print-none"
>
<template #activator="{ props: activatorProps }">
@@ -83,8 +82,6 @@ const emit = defineEmits<{
[key: string]: [];
}>();
const { mdAndUp } = useDisplay();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const api = useUserApi();
@@ -94,7 +91,7 @@ const state = reactive({
shoppingListDialog: false,
menuItems: [
{
title: i18n.t("recipe.add-to-list"),
title: i18n.t("meal-plan.add-day-to-list"),
icon: $globals.icons.cartCheck,
color: undefined,
event: "shoppingList",
@@ -126,8 +123,8 @@ async function getShoppingLists() {
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
shoppingList: () => {
getShoppingLists();
shoppingList: async () => {
await getShoppingLists();
state.shoppingListDialog = true;
},
};

View File

@@ -36,7 +36,7 @@
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
import { Organizer } from "~/lib/api/types/non-generated";
import type { QueryFilterJSON } from "~/lib/api/types/response";
import type { QueryFilterJSON } from "~/lib/api/types/non-generated";
interface Props {
queryFilter?: QueryFilterJSON | null;
@@ -76,7 +76,6 @@ const MEAL_DAY_OPTIONS = [
];
function handleQueryFilterInput(value: string | undefined) {
console.warn("handleQueryFilterInput called with value:", value);
queryFilterString.value = value || "";
}
@@ -114,7 +113,7 @@ const fieldDefs: FieldDefinition[] = [
{
name: "last_made",
label: i18n.t("general.last-made"),
type: "date",
type: "relativeDate",
},
{
name: "created_at",

View File

@@ -18,6 +18,20 @@
</p>
</div>
</div>
<div class="mb-6">
<v-checkbox
v-model="preferences.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"
:prepend-icon="$globals.icons.calendarWeekBegin"
@@ -29,7 +43,9 @@
flat
/>
<BaseCardSectionTitle class="mt-5" :title="$t('household.household-recipe-preferences')" />
<BaseCardSectionTitle class="mt-5" :title="$t('household.household-recipe-preferences')">
{{ $t("household.default-recipe-preferences-description") }}
</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" />

View File

@@ -108,7 +108,7 @@
<v-select
v-if="field.type !== 'boolean'"
:model-value="field.relationalOperatorValue"
:items="field.relationalOperatorOptions"
:items="field.relationalOperatorChoices"
item-title="label"
item-value="value"
variant="underlined"
@@ -129,9 +129,9 @@
:class="config.col.class"
>
<v-select
v-if="field.fieldOptions"
v-if="field.fieldChoices"
:model-value="field.values"
:items="field.fieldOptions"
:items="field.fieldChoices"
item-title="label"
item-value="value"
multiple
@@ -169,23 +169,39 @@
>
<template #activator="{ props: activatorProps }">
<v-text-field
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
persistent-hint
:prepend-icon="$globals.icons.calendar"
:model-value="$d(safeNewDate(field.value + 'T00:00:00'))"
variant="underlined"
color="primary"
class="date-input"
v-bind="activatorProps"
readonly
/>
</template>
<v-date-picker
:model-value="field.value ? new Date(field.value + 'T00:00:00') : null"
:model-value="safeNewDate(field.value + 'T00:00:00')"
hide-header
:first-day-of-week="firstDayOfWeek"
:local="$i18n.locale"
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
/>
</v-menu>
<!--
Relative dates are assumed to be negative intervals with a unit of days.
The input is a *positive*, interpreted internally as a *negative* offset.
-->
<v-number-input
v-else-if="field.type === 'relativeDate'"
:model-value="parseRelativeDateOffset(field.value)"
:suffix="$t('query-filter.dates.days-ago', parseRelativeDateOffset(field.value))"
variant="underlined"
control-variant="stacked"
density="compact"
inset
:min="0"
:precision="0"
class="date-input"
@update:model-value="setFieldValue(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Category"
v-model="field.organizers"
@@ -319,7 +335,13 @@ import { useDebounceFn } from "@vueuse/core";
import { useHouseholdSelf } from "~/composables/use-households";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { Organizer } from "~/lib/api/types/non-generated";
import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
import type {
LogicalOperator,
QueryFilterJSON,
QueryFilterJSONPart,
RelationalKeyword,
RelationalOperator,
} from "~/lib/api/types/non-generated";
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
import { useUserStore } from "~/composables/store/use-user-store";
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
@@ -341,7 +363,14 @@ const emit = defineEmits<{
}>();
const { household } = useHouseholdSelf();
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
const {
logOps,
placeholderKeywords,
getRelOps,
buildQueryFilterString,
getFieldFromFieldDef,
isOrganizerType,
} = useQueryFilterBuilder();
const firstDayOfWeek = computed(() => {
return household.value?.preferences?.firstDayOfWeek || 0;
@@ -396,16 +425,29 @@ function setField(index: number, fieldLabel: string) {
return;
}
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldOptions !== fields.value[index].fieldOptions);
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldChoices !== fields.value[index].fieldChoices);
const updatedField = { ...fields.value[index], ...fieldDef };
// we have to set this explicitly since it might be undefined
updatedField.fieldOptions = fieldDef.fieldOptions;
updatedField.fieldChoices = fieldDef.fieldChoices;
fields.value[index] = {
...getFieldFromFieldDef(updatedField, resetValue),
id: fields.value[index].id, // keep the id
};
// Defaults
switch (fields.value[index].type) {
case "date":
fields.value[index].value = safeNewDate("");
break;
case "relativeDate":
fields.value[index].value = "$NOW-30d";
break;
default:
break;
}
}
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
@@ -425,12 +467,21 @@ function setLogicalOperatorValue(field: FieldWithId, index: number, value: Logic
}
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
const relOps = getRelOps(field.type);
fields.value[index].relationalOperatorValue = relOps.value[value];
}
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
state.datePickers[index] = false;
fields.value[index].value = value;
if (field.type === "relativeDate") {
// Value is set to an int representing the offset from $NOW
// Values are assumed to be negative offsets ('-') with a unit of days ('d')
fields.value[index].value = `$NOW-${Math.abs(value)}d`;
}
else {
fields.value[index].value = value;
}
}
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
@@ -448,12 +499,7 @@ function removeField(index: number) {
state.datePickers.splice(index, 1);
}
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
/* newFields.forEach((field, index) => {
const updatedField = getFieldFromFieldDef(field);
fields.value[index] = updatedField; // recursive!!!
}); */
const fieldsUpdater = useDebounceFn(() => {
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
if (qf) {
console.debug(`Set query filter: ${qf}`);
@@ -519,6 +565,9 @@ async function initializeFields() {
...getFieldFromFieldDef(fieldDef),
id: useUid(),
};
const relOps = getRelOps(field.type);
field.leftParenthesis = part.leftParenthesis || field.leftParenthesis;
field.rightParenthesis = part.rightParenthesis || field.rightParenthesis;
field.logicalOperator = part.logicalOperator
@@ -527,12 +576,15 @@ async function initializeFields() {
field.relationalOperatorValue = part.relationalOperator
? relOps.value[part.relationalOperator]
: field.relationalOperatorValue;
field.relationalOperatorValue = part.relationalOperator
? relOps.value[part.relationalOperator]
: field.relationalOperatorValue;
if (field.leftParenthesis || field.rightParenthesis) {
state.showAdvanced = true;
}
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
if (typeof part.value === "string") {
field.values = part.value ? [part.value] : [];
}
@@ -601,7 +653,7 @@ function buildQueryFilterJSON(): QueryFilterJSON {
relationalOperator: field.relationalOperatorValue?.value,
};
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
if (field.fieldChoices?.length || isOrganizerType(field.type)) {
part.value = field.values.map(value => value.toString());
}
else if (field.type === "boolean") {
@@ -619,6 +671,50 @@ function buildQueryFilterJSON(): QueryFilterJSON {
return qfJSON;
}
function safeNewDate(input: string): Date {
const date = new Date(input);
if (isNaN(date.getTime())) {
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
}
return date;
}
/**
* Parse a relative date string offset (e.g. $NOW-30d --> 30)
*
* Currently only values with a negative offset ('-') and a unit of days ('d') are supported
*/
function parseRelativeDateOffset(value: string): number {
const defaultVal = 30;
if (!value) {
return defaultVal;
}
try {
if (!value.startsWith(placeholderKeywords.value["$NOW"].value)) {
return defaultVal;
}
const remainder = value.slice(placeholderKeywords.value["$NOW"].value.length);
if (!remainder.startsWith("-")) {
throw new Error("Invalid operator (not '-')");
}
if (remainder.slice(-1) !== "d") {
throw new Error("Invalid unit (not 'd')");
}
// Slice off sign and unit
return parseInt(remainder.slice(1, -1));
}
catch (error) {
console.warn(`Unable to parse relative date offset from '${value}': ${error}`);
return defaultVal;
}
}
const config = computed(() => {
const multiple = fields.value.length > 1;
const adv = state.showAdvanced;
@@ -689,4 +785,13 @@ const config = computed(() => {
.bg-light {
background-color: rgba(255, 255, 255, var(--bg-opactity));
}
:deep(.date-input input) {
text-align: end;
padding-right: 6px;
}
:deep(.date-input .v-field__field) {
align-items: center;
}
</style>

View File

@@ -79,7 +79,7 @@
@print="$emit('print')"
/>
</div>
<div v-if="open" class="custom-btn-group gapped">
<div v-if="open" class="custom-btn-group gapped ma-1">
<v-btn
v-for="(btn, index) in editorButtons"
:key="index"

View File

@@ -1,60 +1,97 @@
<template>
<div v-if="model.length > 0 || edit">
<v-card class="mt-4">
<v-card-title class="py-2">
{{ $t("asset.assets") }}
</v-card-title>
<v-list-item class="pr-2 pl-0">
<v-card-title>
{{ $t("asset.assets") }}
</v-card-title>
<template #append>
<v-btn
v-if="edit"
variant="plain"
:icon="$globals.icons.create"
@click="state.newAssetDialog = true"
/>
</template>
</v-list-item>
<v-divider class="mx-2" />
<v-list
v-if="model.length > 0"
lines="two"
:flat="!edit"
>
<v-list-item
v-for="(item, i) in model"
:key="i"
:href="!edit ? assetURL(item.fileName ?? '') : ''"
target="_blank"
class="pr-2"
>
<template #prepend>
<div class="ma-auto">
<v-tooltip location="bottom">
<template #activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps">
{{ getIconDefinition(item.icon).icon }}
</v-icon>
</template>
<span>{{ getIconDefinition(item.icon).title }}</span>
</v-tooltip>
</div>
<v-avatar size="48" rounded="lg" class="elevation-1">
<v-img
v-if="isImage(item.fileName)"
:src="assetURL(item.fileName ?? '')"
:alt="item.name"
loading="lazy"
cover
/>
<v-icon v-else size="large">
{{ getIconDefinition(item.icon).icon }}
</v-icon>
</v-avatar>
</template>
<v-list-item-title class="pl-2">
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
<template #append>
<v-menu v-if="edit" location="bottom end">
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
icon
variant="plain"
>
<v-icon :icon="$globals.icons.dotsVertical" />
</v-btn>
</template>
<v-list density="compact" min-width="220">
<v-list-item
:href="assetURL(item.fileName ?? '')"
:prepend-icon="$globals.icons.eye"
:title="$t('general.view')"
target="_blank"
/>
<v-list-item
:href="assetURL(item.fileName ?? '')"
:prepend-icon="$globals.icons.download"
:title="$t('general.download')"
download
/>
<v-list-item
v-if="edit"
:prepend-icon="$globals.icons.contentCopy"
:title="$t('general.copy')"
@click="copyText(assetEmbed(item.fileName ?? ''))"
/>
<v-list-item
v-if="edit"
:prepend-icon="$globals.icons.delete"
:title="$t('general.delete')"
@click="model.splice(i, 1)"
/>
</v-list>
</v-menu>
<v-btn
v-if="!edit"
color="primary"
icon
size="small"
variant="plain"
:href="assetURL(item.fileName ?? '')"
target="_blank"
top
download
>
<v-icon> {{ $globals.icons.download }} </v-icon>
</v-btn>
<div v-else>
<v-btn
color="error"
icon
size="small"
top
@click="model.splice(i, 1)"
>
<v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn>
<AppButtonCopy
color=""
:copy-text="assetEmbed(item.fileName ?? '')"
/>
</div>
</template>
</v-list-item>
</v-list>
@@ -68,18 +105,9 @@
can-submit
@submit="addAsset"
>
<template #activator>
<BaseButton
v-if="edit"
size="small"
create
@click="state.newAssetDialog = true"
/>
</template>
<v-card-text class="pt-4">
<v-text-field
v-model="state.newAsset.name"
density="compact"
:label="$t('general.name')"
/>
<div class="d-flex justify-space-between">
@@ -92,10 +120,14 @@
item-value="name"
class="mr-2"
>
<template #item="{ item, props: itemProps }">
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template #prepend>
<v-icon>{{ item.raw.icon }}</v-icon>
<v-avatar>
<v-icon>
{{ item.raw.icon }}
</v-icon>
</v-avatar>
</template>
</v-list-item>
</template>
@@ -107,7 +139,6 @@
@uploaded="setFileObject"
/>
</div>
{{ state.fileObject.name }}
</v-card-text>
</BaseDialog>
</div>
@@ -118,6 +149,7 @@
import { useStaticRoutes, useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import type { RecipeAsset } from "~/lib/api/types/recipe";
import { useCopy } from "~/composables/use-copy";
const props = defineProps({
slug: {
@@ -149,6 +181,7 @@ const state = reactive({
const i18n = useI18n();
const { $globals } = useNuxtApp();
const { copyText } = useCopy();
const iconOptions = [
{
@@ -184,21 +217,31 @@ function getIconDefinition(icon: string) {
return iconOptions.find(item => item.name === icon) || iconOptions[0];
}
function isImage(fileName?: string | null) {
if (!fileName) return false;
return /\.(png|jpe?g|gif|webp|bmp|avif)$/i.test(fileName);
}
const { recipeAssetPath } = useStaticRoutes();
function assetURL(assetName: string) {
return recipeAssetPath(props.recipeId, assetName);
}
function assetEmbed(name: string) {
return `<img src="${serverBase}${assetURL(name)}" height="100%" width="100%"> </img>`;
return `<img src="${serverBase}${assetURL(name)}" height="100%" width="100%" />`;
}
function setFileObject(fileObject: File) {
state.fileObject = fileObject;
// If the user didn't provide a name, default to the file base name
if (!state.newAsset.name?.trim()) {
state.newAsset.name = fileObject.name.substring(0, fileObject.name.lastIndexOf("."));
}
}
function validFields() {
return state.newAsset.name.length > 0 && state.fileObject.name.length > 0;
// Only require a file; name will fall back to the file name if empty
return Boolean(state.fileObject?.name);
}
async function addAsset() {
@@ -207,8 +250,10 @@ async function addAsset() {
return;
}
const nameToUse = state.newAsset.name?.trim() || state.fileObject.name;
const { data } = await api.recipes.createAsset(props.slug, {
name: state.newAsset.name,
name: nameToUse,
icon: state.newAsset.icon,
file: state.fileObject,
extension: state.fileObject.name.split(".").pop() || "",

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">
@@ -130,11 +128,11 @@ defineEmits<{
delete: [slug: string];
}>();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
const showRecipeContent = computed(() => props.recipeId && props.slug);
const recipeRoute = computed<string>(() => {
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";

View File

@@ -160,11 +160,11 @@ defineEmits<{
delete: [slug: string];
}>();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
const showRecipeContent = computed(() => props.recipeId && props.slug);
const recipeRoute = computed<string>(() => {
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";

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>
@@ -219,7 +213,7 @@ const EVENTS = {
shuffle: "shuffle",
};
const $auth = useMealieAuth();
const auth = useMealieAuth();
const { $globals } = useNuxtApp();
const { isOwnGroup } = useLoggedInState();
const useMobileCards = computed(() => {
@@ -234,7 +228,7 @@ const sortLoading = ref(false);
const randomSeed = ref(Date.now().toString());
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const page = ref(1);
const perPage = 32;
@@ -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

@@ -10,7 +10,6 @@
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
:open-on-hover="$vuetify.display.mdAndUp"
content-class="d-print-none"
@update:model-value="onMenuToggle"
>
@@ -24,7 +23,6 @@
:fab="fab"
v-bind="activatorProps"
@click.prevent
@mouseenter="onHover"
>
<v-icon
:size="!fab ? undefined : 'x-large'"
@@ -127,12 +125,6 @@ const contentProps = computed(() => {
return rest;
});
function onHover() {
if (!isMenuContentLoaded.value) {
isMenuContentLoaded.value = true;
}
}
function onMenuToggle(isOpen: boolean) {
if (isOpen && !isMenuContentLoaded.value) {
isMenuContentLoaded.value = true;

View File

@@ -176,6 +176,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
[key: string]: any;
deleted: [slug: string];
print: [];
}>();
const api = useUserApi();
@@ -201,13 +202,13 @@ const newMealdateString = computed(() => {
});
const i18n = useI18n();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const { $globals } = useNuxtApp();
const { household } = useHouseholdSelf();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
const firstDayOfWeek = computed(() => {
return household.value?.preferences?.firstDayOfWeek || 0;
@@ -295,12 +296,12 @@ const recipeRefWithScale = computed(() =>
);
const isAdminAndNotOwner = computed(() => {
return (
$auth.user.value?.admin
&& $auth.user.value?.id !== recipeRef.value?.userId
auth.user.value?.admin
&& auth.user.value?.id !== recipeRef.value?.userId
);
});
const canDelete = computed(() => {
const user = $auth.user.value;
const user = auth.user.value;
const recipe = recipeRef.value;
return user && recipe && (user.admin || user.id === recipe.userId);
});

View File

@@ -110,8 +110,8 @@ defineEmits<{
const selected = defineModel<Recipe[]>({ default: () => [] });
const i18n = useI18n();
const $auth = useMealieAuth();
const groupSlug = $auth.user.value?.groupSlug;
const auth = useMealieAuth();
const groupSlug = auth.user.value?.groupSlug;
const router = useRouter();
// Initialize sort state with default sorting by dateAdded descending

View File

@@ -86,6 +86,19 @@
class="text-center"
>
{{ recipeSection.recipeName }}
<v-tooltip v-if="recipeSection.parentRecipe?.name" location="top">
<template #activator="{ props: tooltipProps }">
<v-icon
v-bind="tooltipProps"
size="tiny"
class="mb-2 ml-2"
style="cursor: pointer"
>
{{ $globals.icons.potSteam }}
</v-icon>
</template>
<span>{{ $t("shopping-list.ingredient-of-recipe", { recipe: recipeSection.parentRecipe.name }) }}</span>
</v-tooltip>
</v-col>
</v-row>
<v-row
@@ -203,6 +216,7 @@ export interface ShoppingListRecipeIngredientSection {
recipeName: string;
recipeScale: number;
ingredientSections: ShoppingListIngredientSection[];
parentRecipe?: Recipe;
}
interface Props {
@@ -217,7 +231,7 @@ const props = withDefaults(defineProps<Props>(), {
const dialog = defineModel<boolean>({ default: false });
const i18n = useI18n();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const api = useUserApi();
const preferences = useShoppingListPreferences();
const ready = ref(false);
@@ -227,7 +241,7 @@ const currentHouseholdSlug = ref("");
const filteredShoppingLists = ref<ShoppingListSummary[]>([]);
const state = reactive({
shoppingListDialog: true,
shoppingListDialog: false,
shoppingListIngredientDialog: false,
shoppingListShowAllToggled: false,
});
@@ -237,11 +251,11 @@ const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllTog
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
watch(dialog, (newVal, oldVal) => {
if (newVal && !oldVal) {
currentHouseholdSlug.value = $auth.user.value?.householdSlug || "";
watch([dialog, () => preferences.value.viewAllLists], () => {
if (dialog.value) {
currentHouseholdSlug.value = auth.user.value?.householdSlug || "";
filteredShoppingLists.value = props.shoppingLists.filter(
list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id,
list => preferences.value.viewAllLists || list.userId === auth.user.value?.id,
);
if (filteredShoppingLists.value.length === 1 && !state.shoppingListShowAllToggled) {
@@ -249,16 +263,80 @@ watch(dialog, (newVal, oldVal) => {
openShoppingListIngredientDialog(selectedShoppingList.value);
}
else {
state.shoppingListDialog = true;
ready.value = true;
}
}
else if (!newVal) {
else if (!dialog.value) {
initState();
}
});
function buildIngredientSections(ingredients: ShoppingListIngredient[]): ShoppingListIngredientSection[] {
let currentTitle = "";
const onHandIngs: ShoppingListIngredient[] = [];
const sections = ingredients.reduce((acc, ing) => {
if (ing.ingredient.title) {
currentTitle = ing.ingredient.title;
}
if (!acc.length || currentTitle !== acc[acc.length - 1].sectionName) {
if (acc.length) {
acc[acc.length - 1].ingredients.push(...onHandIngs);
onHandIngs.length = 0;
}
acc.push({ sectionName: currentTitle, ingredients: [] });
}
const householdsWithFood = ing.ingredient?.food?.householdsWithIngredientFood || [];
if (householdsWithFood.includes(currentHouseholdSlug.value)) {
onHandIngs.push(ing);
return acc;
}
acc[acc.length - 1].ingredients.push(ing);
return acc;
}, [] as ShoppingListIngredientSection[]);
if (sections.length) {
sections[sections.length - 1].ingredients.push(...onHandIngs);
}
return sections;
}
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
function addSubRecipeToMap(ing: RecipeIngredient, parentQuantity: number, parentScale: number, parentRecipe: Recipe) {
const ref = ing.referencedRecipe!;
const key = ref.id || ref.slug || "";
const ownIngs: ShoppingListIngredient[] = [];
const subRefIngs: RecipeIngredient[] = [];
for (const subIng of ref.recipeIngredient ?? []) {
if (subIng.referencedRecipe) {
subRefIngs.push(subIng);
}
else {
const householdsWithFood = subIng.food?.householdsWithIngredientFood || [];
ownIngs.push({
checked: !householdsWithFood.includes(currentHouseholdSlug.value),
ingredient: { ...subIng, quantity: (ing.quantity || 1) * (subIng.quantity || 1) },
});
}
}
recipeSectionMap.set(key, {
recipeId: ref.id || "",
recipeName: ref.name || "",
recipeScale: parentQuantity * parentScale,
ingredientSections: buildIngredientSections(ownIngs),
parentRecipe,
});
subRefIngs.forEach(subIng => addSubRecipeToMap(subIng, (ing.quantity || 1) * (subIng.quantity || 1), parentScale, ref));
}
for (const recipe of recipes) {
if (!recipe.slug) {
continue;
@@ -290,88 +368,36 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
continue;
}
const shoppingListIngredients: ShoppingListIngredient[] = [];
function flattenRecipeIngredients(ing: RecipeIngredient, parentTitle = ""): ShoppingListIngredient[] {
const ownIngs: ShoppingListIngredient[] = [];
const subRefIngs: RecipeIngredient[] = [];
recipeData.recipeIngredient.forEach((ing) => {
if (ing.referencedRecipe) {
// Recursively flatten all ingredients in the referenced recipe
return (ing.referencedRecipe.recipeIngredient ?? []).flatMap((subIng) => {
const calculatedQty = (ing.quantity || 1) * (subIng.quantity || 1);
// Pass the referenced recipe name as the section title
return flattenRecipeIngredients(
{ ...subIng, quantity: calculatedQty },
"",
);
});
subRefIngs.push(ing);
}
else {
// Regular ingredient
const householdsWithFood = ing.food?.householdsWithIngredientFood || [];
return [{
ownIngs.push({
checked: !householdsWithFood.includes(currentHouseholdSlug.value),
ingredient: {
...ing,
title: ing.title || parentTitle,
},
}];
}
}
recipeData.recipeIngredient.forEach((ing) => {
const flattened = flattenRecipeIngredients(ing, "");
shoppingListIngredients.push(...flattened);
});
let currentTitle = "";
const onHandIngs: ShoppingListIngredient[] = [];
const shoppingListIngredientSections = shoppingListIngredients.reduce((sections, ing) => {
if (ing.ingredient.title) {
currentTitle = ing.ingredient.title;
}
else if (ing.ingredient.referencedRecipe?.name) {
currentTitle = ing.ingredient.referencedRecipe.name;
}
// If this is the first item in the section, create a new section
if (sections.length === 0 || currentTitle !== sections[sections.length - 1].sectionName) {
if (sections.length) {
// Add the on-hand ingredients to the previous section
sections[sections.length - 1].ingredients.push(...onHandIngs);
onHandIngs.length = 0;
}
sections.push({
sectionName: currentTitle,
ingredients: [],
ingredient: ing,
});
}
// Store the on-hand ingredients for later
const householdsWithFood = (ing.ingredient?.food?.householdsWithIngredientFood || []);
if (householdsWithFood.includes(currentHouseholdSlug.value)) {
onHandIngs.push(ing);
return sections;
}
// Add the ingredient to previous section
sections[sections.length - 1].ingredients.push(ing);
return sections;
}, [] as ShoppingListIngredientSection[]);
// Add remaining on-hand ingredients to the previous section
shoppingListIngredientSections[shoppingListIngredientSections.length - 1].ingredients.push(...onHandIngs);
});
recipeSectionMap.set(recipe.slug, {
recipeId: recipeData.id,
recipeName: recipeData.name,
recipeScale: recipeData.scale,
ingredientSections: shoppingListIngredientSections,
ingredientSections: buildIngredientSections(ownIngs),
});
subRefIngs.forEach(ing => addSubRecipeToMap(ing, ing.quantity || 1, recipeData.scale, recipeData));
}
recipeIngredientSections.value = Array.from(recipeSectionMap.values());
}
function initState() {
state.shoppingListDialog = true;
state.shoppingListDialog = false;
state.shoppingListIngredientDialog = false;
state.shoppingListShowAllToggled = false;
recipeIngredientSections.value = [];

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"
>
<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"
size="x-small"
class="rounded-circle"
light
@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>
@@ -87,7 +85,7 @@ const emit = defineEmits<{
selected: [recipe: RecipeSummary];
}>();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const loading = ref(false);
const selectedIndex = ref(-1);
@@ -153,7 +151,7 @@ watch(dialog, (val) => {
});
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
watch(route, close);
function open() {

View File

@@ -119,10 +119,10 @@ whenever(
);
const i18n = useI18n();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const { household } = useHouseholdSelf();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const firstDayOfWeek = computed(() => {
return household.value?.preferences?.firstDayOfWeek || 0;

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

@@ -100,6 +100,7 @@
v-model="state.auto"
:label="$t('search.auto-search')"
single-line
color="primary"
/>
<v-btn
block
@@ -140,13 +141,13 @@ const emit = defineEmits<{
ready: [];
}>();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const route = useRoute();
const { $globals } = useNuxtApp();
const i18n = useI18n();
const showRandomLoading = ref(false);
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const {
state,

View File

@@ -81,11 +81,11 @@ import {
usePublicToolStore,
} from "~/composables/store";
const $auth = useMealieAuth();
const auth = useMealieAuth();
const route = useRoute();
const { isOwnGroup } = useLoggedInState();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const {
state,

View File

@@ -52,14 +52,14 @@ const isFavorite = computed(() => {
async function toggleFavorite() {
const api = useUserApi();
const $auth = useMealieAuth();
const auth = useMealieAuth();
if (!$auth.user.value) return;
if (!auth.user.value) return;
if (!isFavorite.value) {
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
await api.users.addFavorite(auth.user.value?.id, props.recipeId);
}
else {
await api.users.removeFavorite($auth.user.value?.id, props.recipeId);
await api.users.removeFavorite(auth.user.value?.id, props.recipeId);
}
await refreshUserRatings();
}

View File

@@ -13,7 +13,7 @@
/>
<v-row
:no-gutters="mdAndUp"
dense
density="comfortable"
class="d-flex flex-wrap my-1"
>
<v-col
@@ -44,9 +44,8 @@
</v-number-input>
</v-col>
<v-col
v-if="!state.isRecipe"
sm="12"
md="3"
md="2"
cols="12"
>
<v-autocomplete
@@ -58,8 +57,8 @@
density="compact"
variant="solo"
return-object
:items="units || []"
:custom-filter="normalizeFilter"
:items="filteredUnits"
:custom-filter="() => true"
item-title="name"
class="mx-1"
:placeholder="$t('recipe.choose-unit')"
@@ -104,7 +103,7 @@
<v-col
v-if="!state.isRecipe"
m="12"
md="3"
md="4"
cols="12"
class=""
>
@@ -117,8 +116,8 @@
density="compact"
variant="solo"
return-object
:items="foods || []"
:custom-filter="normalizeFilter"
:items="filteredFoods"
:custom-filter="() => true"
item-title="name"
class="mx-1 py-0"
:placeholder="$t('recipe.choose-food')"
@@ -162,7 +161,7 @@
<v-col
v-if="state.isRecipe"
m="12"
md="6"
md="4"
cols="12"
class=""
>
@@ -176,7 +175,6 @@
variant="solo"
return-object
:items="search.data.value || []"
:custom-filter="normalizeFilter"
item-title="name"
class="mx-1 py-0"
:placeholder="$t('search.type-to-search')"
@@ -227,11 +225,11 @@
</template>
<script setup lang="ts">
import { ref, computed, reactive, toRefs } from "vue";
import { ref, computed, reactive, toRefs, watch } from "vue";
import { useDisplay } from "vuetify";
import { useI18n } from "vue-i18n";
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
import { normalizeFilter } from "~/composables/use-utils";
import { useSearch } from "~/composables/use-search";
import { useNuxtApp } from "#app";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
import { usePublicExploreApi, useUserApi } from "~/composables/api";
@@ -343,8 +341,8 @@ const btns = computed(() => {
// Foods
const foodStore = useFoodStore();
const foodData = useFoodData();
const foodSearch = ref("");
const foodAutocomplete = ref<HTMLInputElement>();
const { search: foodSearch, filtered: filteredFoods } = useSearch(foodStore.store);
async function createAssignFood() {
foodData.data.name = foodSearch.value;
@@ -355,8 +353,8 @@ async function createAssignFood() {
// Recipes
const route = useRoute();
const $auth = useMealieAuth();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
const auth = useMealieAuth();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const { isOwnGroup } = useLoggedInState();
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
@@ -375,8 +373,8 @@ watch(loading, (val) => {
// Units
const unitStore = useUnitStore();
const unitsData = useUnitData();
const unitSearch = ref("");
const unitAutocomplete = ref<HTMLInputElement>();
const { search: unitSearch, filtered: filteredUnits } = useSearch(unitStore.store);
async function createAssignUnit() {
unitsData.data.name = unitSearch.value;
@@ -430,9 +428,6 @@ function quantityFilter(e: KeyboardEvent) {
}
const { showTitle } = toRefs(state);
const foods = foodStore.store;
const units = unitStore.store;
</script>
<style>

View File

@@ -12,7 +12,7 @@
<script setup lang="ts">
import { computed } from "vue";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
import { useParsedIngredientText } from "~/composables/recipes";
import { useIngredientTextParser } from "~/composables/recipes";
interface Props {
ingredient?: RecipeIngredient;
@@ -20,6 +20,7 @@ interface Props {
}
const { ingredient, scale = 1 } = defineProps<Props>();
const { useParsedIngredientText } = useIngredientTextParser();
const baseText = computed(() => {
if (!ingredient) return "";

View File

@@ -34,7 +34,7 @@
<script setup lang="ts">
import type { RecipeIngredient } from "~/lib/api/types/household";
import { useParsedIngredientText } from "~/composables/recipes";
import { useIngredientTextParser } from "~/composables/recipes";
interface Props {
ingredient: RecipeIngredient;
@@ -44,8 +44,9 @@ const props = withDefaults(defineProps<Props>(), {
scale: 1,
});
const route = useRoute();
const $auth = useMealieAuth();
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
const auth = useMealieAuth();
const groupSlug = computed(() => route.params.groupSlug || auth.user?.value?.groupSlug || "");
const { useParsedIngredientText } = useIngredientTextParser();
const parsedIng = computed(() => {
return useParsedIngredientText(props.ingredient, props.scale, true, groupSlug.value.toString());

View File

@@ -17,15 +17,13 @@
v-for="(ingredient, index) in value"
:key="'ingredient' + index"
>
<template v-if="!isCookMode">
<h3
v-if="showTitleEditor[index]"
class="mt-2"
>
{{ ingredient.title }}
</h3>
<v-divider v-if="showTitleEditor[index]" />
</template>
<h3
v-if="showTitleEditor[index]"
class="mt-2"
>
{{ ingredient.title }}
</h3>
<v-divider v-if="showTitleEditor[index]" />
<v-list-item
density="compact"
class="pa-0"
@@ -54,7 +52,7 @@
<script setup lang="ts">
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import { parseIngredientText } from "~/composables/recipes";
import { useIngredientTextParser } from "~/composables/recipes";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
interface Props {
@@ -68,6 +66,8 @@ const props = withDefaults(defineProps<Props>(), {
isCookMode: false,
});
const { parseIngredientText } = useIngredientTextParser();
function validateTitle(title?: string | null) {
return !(title === undefined || title === "" || title === null);
}

View File

@@ -159,7 +159,7 @@ const madeThisDialog = ref(false);
const userApi = useUserApi();
const { household } = useHouseholdSelf();
const i18n = useI18n();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const domMadeThisForm = ref<VForm>();
const newTimelineEvent = ref<RecipeTimelineEventIn>({
subject: "",
@@ -179,7 +179,7 @@ const newTimelineEventTimestampString = computed(() => {
const lastMade = ref(props.recipe.lastMade);
const lastMadeReady = ref(false);
onMounted(async () => {
if (!$auth.user?.value?.householdSlug) {
if (!auth.user?.value?.householdSlug) {
lastMade.value = props.recipe.lastMade;
}
else {
@@ -255,8 +255,8 @@ async function createTimelineEvent() {
madeThisFormLoading.value = true;
newTimelineEvent.value.recipeId = props.recipe.id;
// Note: $auth.user is now a ref
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName });
// Note: auth.user is now a ref
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: auth.user.value?.fullName });
// the user only selects the date, so we set the time to end of day local time
// we choose the end of day so it always comes after "new recipe" events

View File

@@ -73,10 +73,10 @@ const props = withDefaults(defineProps<Props>(), {
disabled: false,
});
const $auth = useMealieAuth();
const auth = useMealieAuth();
const { frac } = useFraction();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug || auth.user?.value?.groupSlug || "");
const attrs = computed(() => {
return props.small

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"
@@ -162,9 +163,9 @@ const state = reactive({
},
});
const $auth = useMealieAuth();
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user?.value?.groupSlug || "");
const groupSlug = computed(() => route.params.groupSlug as string || auth.user?.value?.groupSlug || "");
// =================================================================
// Context Menu

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

@@ -1,5 +1,18 @@
<template>
<div>
<BaseDialog
v-model="discardDialog"
:title="$t('general.discard-changes')"
color="warning"
:icon="$globals.icons.alertCircle"
can-confirm
@confirm="confirmDiscard"
@cancel="cancelDiscard"
>
<v-card-text>
{{ $t("general.discard-changes-description") }}
</v-card-text>
</BaseDialog>
<RecipePageParseDialog
:model-value="isParsing"
:ingredients="recipe.recipeIngredient"
@@ -8,13 +21,14 @@
@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"
:landscape="landscape"
@save="saveRecipe"
@delete="deleteRecipe"
@close="closeEditor"
/>
<RecipeJsonEditor
v-if="isEditJSON"
@@ -54,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"
@@ -174,6 +192,7 @@
<script setup lang="ts">
import { invoke, until } from "@vueuse/core";
import type { RouteLocationNormalized } from "vue-router";
import RecipeIngredients from "../RecipeIngredients.vue";
import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue";
import RecipePageFooter from "./RecipePageParts/RecipePageFooter.vue";
@@ -205,12 +224,11 @@ import { useNavigationWarning } from "~/composables/use-navigation-warning";
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
const display = useDisplay();
const i18n = useI18n();
const $auth = useMealieAuth();
const auth = useMealieAuth();
const route = useRoute();
const { isOwnGroup } = useLoggedInState();
const groupSlug = computed(() => (route.params.groupSlug as string) || $auth.user?.value?.groupSlug || "");
const groupSlug = computed(() => (route.params.groupSlug as string) || auth.user?.value?.groupSlug || "");
const router = useRouter();
const api = useUserApi();
@@ -231,26 +249,68 @@ const notLinkedIngredients = computed(() => {
* and prompts the user to save if they have unsaved changes.
*/
const originalRecipe = ref<Recipe | null>(null);
const discardDialog = ref(false);
const pendingRoute = ref<RouteLocationNormalized | null>(null);
invoke(async () => {
await until(recipe.value).not.toBeNull();
originalRecipe.value = deepCopy(recipe.value);
});
onUnmounted(async () => {
const isSame = JSON.stringify(recipe.value) === JSON.stringify(originalRecipe.value);
if (isEditMode.value && !isSame && recipe.value?.slug !== undefined) {
const save = window.confirm(i18n.t("general.unsaved-changes"));
if (save) {
await api.recipes.updateOne(recipe.value.slug, recipe.value);
}
function hasUnsavedChanges(): boolean {
if (originalRecipe.value === null) {
return false;
}
return JSON.stringify(recipe.value) !== JSON.stringify(originalRecipe.value);
}
function restoreOriginalRecipe() {
if (originalRecipe.value) {
recipe.value = deepCopy(originalRecipe.value) as NoUndefinedField<Recipe>;
}
}
function closeEditor() {
if (hasUnsavedChanges()) {
pendingRoute.value = null;
discardDialog.value = true;
}
else {
setMode(PageMode.VIEW);
}
}
function confirmDiscard() {
restoreOriginalRecipe();
discardDialog.value = false;
if (pendingRoute.value) {
const destination = pendingRoute.value;
pendingRoute.value = null;
router.push(destination);
}
else {
setMode(PageMode.VIEW);
}
}
function cancelDiscard() {
discardDialog.value = false;
pendingRoute.value = null;
}
onBeforeRouteLeave((to) => {
if (isEditMode.value && hasUnsavedChanges()) {
pendingRoute.value = to;
discardDialog.value = true;
return false;
}
});
onUnmounted(() => {
deactivateNavigationWarning();
toggleCookMode();
clearPageState(recipe.value.slug || "");
console.debug("reset RecipePage state during unmount");
});
const hasLinkedIngredients = computed(() => {
return recipe.value.recipeInstructions.some(
@@ -300,6 +360,8 @@ async function saveRecipe() {
if (data?.slug) {
router.push(`/g/${groupSlug.value}/r/` + data.slug);
recipe.value = data as NoUndefinedField<Recipe>;
// Update the snapshot after successful save
originalRecipe.value = deepCopy(recipe.value);
}
}

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

@@ -16,7 +16,7 @@
:open="isEditMode"
:recipe-id="recipe.id"
class="ml-auto mt-n7 pb-4"
@close="setMode(PageMode.VIEW)"
@close="$emit('close')"
@json="toggleEditMode()"
@edit="setMode(PageMode.EDIT)"
@save="$emit('save')"
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
landscape: false,
});
defineEmits(["save", "delete", "print"]);
defineEmits(["save", "delete", "print", "close"]);
const { recipeImage } = useStaticRoutes();
const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);

View File

@@ -17,7 +17,7 @@
</v-card-title>
<RecipeRating
:key="recipe.slug"
:value="recipe.rating"
:model-value="recipe.rating"
:recipe-id="recipe.id"
:slug="recipe.slug"
/>

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