Compare commits

...

105 Commits

Author SHA1 Message Date
renovate[bot]
83bc2f3889 fix(deps): update dependency openai to v2.32.0 (#7507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 22:49:17 +00:00
Hayden
0ffc1e7bf7 chore(l10n): New Crowdin updates (#7506) 2026-04-20 22:25:35 +00:00
renovate[bot]
e166baa33c fix(deps): update dependency pydantic to v2.13.1 (#7505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 17:23:35 +00:00
Hayden
3e25005ea6 chore(l10n): New Crowdin updates (#7502) 2026-04-20 10:16:42 +00:00
Hayden
c92ebf2099 chore(l10n): New Crowdin updates (#7500) 2026-04-19 21:27:03 +00:00
Zdenek Stursa
8e429834af fix: use correct title and icon on Recipe Actions data page (#7498)
Co-authored-by: Zdenek <tvuj-email@example.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 16:04:38 +00:00
mealie-actions[bot]
2ca5694391 chore(l10n): Crowdin locale sync (#7497)
Co-authored-by: GitHub Action <action@github.com>
2026-04-19 03:08:18 +00:00
Michael Genson
8d8987ab05 docs: Update recipe creation docs (#7494) 2026-04-18 17:50:26 -05:00
Zdenek Stursa
372474ea2b fix: prevent delete-image dialog from reopening in a loop inside v-menu (#7469)
Co-authored-by: Zdenek <tvuj-email@example.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 20:39:43 +00:00
renovate[bot]
5b93129368 fix(deps): update dependency pydantic to v2.13.0 (#7492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-18 14:27:40 +00:00
renovate[bot]
ffeb4dceaf chore(deps): update dependency mypy to v1.20.1 (#7490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-18 05:55:14 +00:00
mealie-commit-bot[bot]
5fc4851ef5 chore: bump version to v3.16.0 2026-04-17 20:59:57 +00:00
Michael Genson
d9e933d5ae fix: Misc frontend layout fixes (#7487) 2026-04-17 12:28:13 -05:00
renovate[bot]
0a07835338 fix(deps): update dependency lxml to v6.0.4 (#7485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 16:46:53 +00:00
Michael Genson
7a85ea6ae9 chore: Update yarn deps (#7486) 2026-04-17 11:58:19 -05:00
renovate[bot]
c4c60f1645 fix(deps): update dependency authlib to v1.6.11 [security] (#7481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 16:46:22 +00:00
Zdenek Stursa
9f7ba8dc08 fix: preserve ingredient section titles when parsing recipe ingredients (#7483)
Co-authored-by: Zdenek <tvuj-email@example.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:44:45 +00:00
Michael Genson
c4799ceb9e dev: Enable lockfile maintenance and update deps (#7484) 2026-04-17 11:33:50 -05:00
Michael Genson
828be095a2 fix: Blank query filter builder fields (#7480) 2026-04-16 19:11:05 -05:00
renovate[bot]
18718fb647 chore(deps): update node.js to 33cf7f0 (#7478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 15:57:22 +00:00
Brian Choromanski
fb545962dd feat: Migrate PWA manifest to backend (#7331)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-16 10:59:21 -05:00
Brian Choromanski
781a08ef54 docs: Added copy button to codeblocks (#7343) 2026-04-16 15:34:28 +00:00
mealie-commit-bot[bot]
a7a08b6b11 chore: bump version to v3.15.2 2026-04-16 03:43:59 +00:00
Hayden
bd296c3eaf fix: path traversal vulnerabilities in migration image imports and media routes (#7474) 2026-04-16 03:24:50 +00:00
renovate[bot]
8aa016e57b fix(deps): update dependency python-multipart to v0.0.26 [security] (#7473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 01:39:09 +00:00
renovate[bot]
480574eb3d fix(deps): update dependency python-multipart to v0.0.25 (#7470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 13:30:07 +00:00
mealie-commit-bot[bot]
0573d6fc9c chore: bump version to v3.15.1 2026-04-14 17:37:56 +00:00
Xenov
f8d08c6785 fix: seed labels before foods in setup wizard to prevent race condition (#7429)
Co-authored-by: xenov <redacted>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-14 16:24:32 +00:00
renovate[bot]
e6368174f0 chore(deps): update dependency ruff to v0.15.10 (#7464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 16:22:02 +00:00
renovate[bot]
2252875050 fix(deps): update dependency lxml to v6.0.3 (#7465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 16:21:30 +00:00
DeepReef11
54c62ec491 fix: eliminate white flash on page load for dark theme users (#7358)
Co-authored-by: Docker User <user@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-14 16:18:00 +00:00
Brian Choromanski
af79a751fb fix: Admin settings checkboxes not updating (#7462) 2026-04-14 15:58:07 +00:00
renovate[bot]
6e2c849412 fix(deps): update dependency openai to v2.31.0 (#7460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 22:53:42 +00:00
mealie-commit-bot[bot]
76dbf4df45 chore: bump version to v3.15.0 2026-04-13 17:25:28 +00:00
Michael Genson
4e5a2f9fb5 fix: Search layout fixes (#7459) 2026-04-13 10:56:19 -05:00
DeepReef11
daa0b9728b fix: prevent stale SPA shell after container rebuild (#7344)
Co-authored-by: Docker User <user@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-13 10:46:28 -05:00
renovate[bot]
0986ce2ca1 chore(deps): update dependency types-pyyaml to v6.0.12.20260408 (#7454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 05:44:04 +00:00
renovate[bot]
4972143004 chore(deps): update dependency types-requests to v2.33.0.20260408 (#7455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 05:20:19 +00:00
renovate[bot]
499c42a52a chore(deps): update dependency types-python-dateutil to v2.9.0.20260408 (#7453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 05:07:28 +00:00
renovate[bot]
92cf84f615 chore(deps): update dependency pytest to v9.0.3 (#7452)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 21:58:47 +00:00
renovate[bot]
54511779a2 fix(deps): update dependency rapidfuzz to v3.14.5 (#7450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 13:00:19 +00:00
renovate[bot]
b72ccb8d29 chore(deps): update dependency rich to v15 (#7448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 09:18:36 +00:00
mealie-actions[bot]
9fb3bce792 chore(l10n): Crowdin locale sync (#7447)
Co-authored-by: GitHub Action <action@github.com>
2026-04-12 03:08:27 +00:00
Michael Genson
32141187ba fix: Update frontend refs (#7444) 2026-04-11 11:27:52 -05:00
renovate[bot]
30014f53de fix(deps): update dependency uvicorn to v0.44.0 (#7443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 13:26:30 +00:00
Michael Genson
d2b0681dbb feat: Announcements (#7431)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
2026-04-11 08:26:14 -05:00
Brian Choromanski
306f2dcfc6 docs: Updated homepage footer (#7440) 2026-04-11 03:03:23 +00:00
Brian Choromanski
0fb5d31a22 fix: Unchecking took in recipe (#7439)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-11 01:04:30 +00:00
renovate[bot]
1d5b263262 fix(deps): update dependency python-multipart to v0.0.24 (#7438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 22:11:58 +00:00
mealie-actions[bot]
731e3aef37 chore(auto): Update pre-commit hooks (#7364)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-04-10 08:55:50 +00:00
renovate[bot]
fb04602a8e chore(deps): update dependency axios to v1.15.0 [security] (#7436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 00:51:31 +00:00
Kuchenpirat
157b8d2937 chore: upgrade to vuetify v4 (#7432) 2026-04-10 00:39:05 +00:00
Arsène Reymond
6b28bb8eb0 fix: BaseDialog padding (#7428) 2026-04-09 13:53:02 +00:00
renovate[bot]
124d10963e fix(deps): update dependency uvicorn to v0.43.0 (#7430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 19:43:07 +00:00
renovate[bot]
7c2ec93d13 fix(deps): update dependency sqlalchemy to v2.0.49 (#7427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 19:14:31 +00:00
Kuchenpirat
d3e41582ae chore: Nuxt 4 upgrade (#7426) 2026-04-08 15:25:41 +00:00
renovate[bot]
70a251a331 chore(deps): update dependency mypy to v1.20.0 (#7399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:33:49 +00:00
renovate[bot]
4fd224ade7 chore(deps): update dependency types-python-dateutil to v2.9.0.20260402 (#7411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:33:18 +00:00
renovate[bot]
89694f7e54 fix(deps): update dependency requests to v2.33.1 (#7394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:32:12 +00:00
Hayden
7a60ad2227 chore(l10n): New Crowdin updates (#7425) 2026-04-08 06:09:28 +00:00
renovate[bot]
eb71b962bc chore(deps): update node.js to 80fc934 (#7421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 00:42:19 +00:00
Brian Choromanski
fe491bbe56 fix: Support for enter key when creating household (#7419)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-08 00:40:58 +00:00
Michael Genson
27f2dc1bf6 dev: Fix autolabel permission to only use pull_request_target (#7422) 2026-04-07 19:28:28 -05:00
renovate[bot]
b3ea916192 chore(deps): update dependency ruff to v0.15.9 (#7418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 21:52:30 +00:00
renovate[bot]
240d681057 chore(deps): update node.js to df0c595 (#7415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 12:56:02 +00:00
renovate[bot]
6932c9ef2d chore(deps): update node.js to 2ef5213 (#7414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 09:58:35 +00:00
Hayden
1438ba82d5 chore(l10n): New Crowdin updates (#7413) 2026-04-07 05:27:43 +00:00
renovate[bot]
7a5032bf23 chore(deps): update dependency types-requests to v2.33.0.20260402 (#7412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 05:07:26 +00:00
renovate[bot]
c3d1cf4c37 chore(deps): update dependency vite to v7.3.2 [security] (#7410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 21:24:05 +00:00
renovate[bot]
135a9ca684 fix(deps): update dependency pillow to v12.2.0 (#7407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 17:06:36 +00:00
renovate[bot]
ef90515ae8 fix(deps): update dependency fastapi to v0.135.3 (#7406)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 17:06:28 +00:00
Hayden
a853e445ac chore(l10n): New Crowdin updates (#7408) 2026-04-06 17:05:15 +00:00
Hayden
7dad3777d3 chore(l10n): New Crowdin updates (#7400) 2026-04-05 18:36:31 +00:00
renovate[bot]
a6ab0befba fix(deps): update dependency orjson to v3.11.8 (#7398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-05 16:49:03 +00:00
mealie-actions[bot]
2c6997a601 chore(l10n): Crowdin locale sync (#7397)
Co-authored-by: GitHub Action <action@github.com>
2026-04-05 03:07:25 +00:00
Brian Choromanski
9c3b94c019 dev: Bumped gh actions to support node 24 (#7392)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-04 14:13:09 +00:00
Hayden
5ce3099cfa chore(l10n): New Crowdin updates (#7393) 2026-04-04 04:49:26 +00:00
Brian Choromanski
0d1349cc7f fix: Reverted references to categories on the recipe actions data management page (#7391) 2026-04-04 04:05:16 +00:00
Brian Choromanski
7e7d1622dd fix: Display issues with data management pages on mobile (#7389) 2026-04-04 01:08:51 +00:00
renovate[bot]
d24aa7f65a fix(deps): update dependency tzdata to v2026 (#7388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 13:39:15 +00:00
Brian Choromanski
5172571b2e dev: Add linting rules to vscode settings (#7386) 2026-04-03 05:21:07 +00:00
Brian Choromanski
bb278aac35 feat: Added scroll to top on all pages that have recipeCardSection (#7384) 2026-04-03 04:11:16 +00:00
Hayden
4ee97e5348 chore(l10n): New Crowdin updates (#7380) 2026-04-02 09:29:14 +00:00
Hayden
bac00a30a4 chore(l10n): New Crowdin updates (#7379) 2026-04-01 21:23:55 +00:00
Hayden
1123ec848d chore(l10n): New Crowdin updates (#7375) 2026-04-01 08:25:01 +00:00
renovate[bot]
0f767f2e25 chore(deps): update dependency types-requests to v2.33.0.20260327 (#7374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 06:05:21 +00:00
Brian Choromanski
058dbdc9d6 fix: Back button sets view to where you left page (#7370)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-03-31 23:14:32 -05:00
renovate[bot]
94cf825a28 chore(deps): update dependency ruff to v0.15.8 (#7373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 20:44:24 +00:00
Hayden
6ee69b7b3e chore(l10n): New Crowdin updates (#7372) 2026-03-31 20:20:11 +00:00
Arsène Reymond
f36c892bb7 feat: improve BaseDialog on mobile and use it globally (#7076)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-31 12:34:44 +00:00
Hayden
f6305b785e chore(l10n): New Crowdin updates (#7371) 2026-03-31 08:22:12 +00:00
renovate[bot]
1512a9e555 fix(deps): update dependency openai to v2.30.0 (#7369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 01:54:02 +00:00
Hayden
690b6aa57b chore(l10n): New Crowdin updates (#7367) 2026-03-30 19:55:44 +00:00
Hayden
c57af78f8f chore(l10n): New Crowdin updates (#7365) 2026-03-30 07:43:30 +00:00
Hayden
0775156aeb chore(l10n): New Crowdin updates (#7362) 2026-03-29 18:00:49 +00:00
Hayden
3356ebc0b8 chore(l10n): New Crowdin updates (#7360) 2026-03-29 06:05:18 +00:00
renovate[bot]
1b59073dc4 chore(deps): update dependency types-requests to v2.32.4.20260324 (#7359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 05:30:15 +00:00
mealie-actions[bot]
ea3856b620 chore(l10n): Crowdin locale sync (#7357)
Co-authored-by: GitHub Action <action@github.com>
2026-03-29 03:07:40 +00:00
Michael Genson
4f5d1cf1b4 fix: Disable SSL verify when scraping sites for recipe data (#7356) 2026-03-28 20:13:23 -05:00
Hayden
626dee9500 chore(l10n): New Crowdin updates (#7351) 2026-03-28 18:36:42 +00:00
renovate[bot]
1162c700cd fix(deps): update dependency fastapi to v0.135.2 (#7349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 17:19:44 +00:00
Hayden
7b3651d138 chore(l10n): New Crowdin updates (#7346) 2026-03-28 08:25:47 +00:00
renovate[bot]
1a3676c36d chore(deps): update dependency types-python-dateutil to v2.9.0.20260323 (#7345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 05:19:51 +00:00
Brian Choromanski
17d9be3b15 fix: Updated commit hash for opencontainers revision (#7340)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-03-27 21:51:53 +00:00
renovate[bot]
7a8a511d48 chore(deps): update dependency node-forge to v1.4.0 [security] (#7338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 18:30:03 +00:00
666 changed files with 22543 additions and 22624 deletions

View File

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

View File

@@ -55,8 +55,8 @@ jobs:
for file in $FILES; do
# Check if file matches any allowed path
if [[ "$file" == "frontend/composables/use-locales/available-locales.ts" ]] || \
[[ "$file" =~ ^frontend/lang/ ]] || \
if [[ "$file" == "frontend/app/composables/use-locales/available-locales.ts" ]] || \
[[ "$file" =~ ^frontend/app/lang/ ]] || \
[[ "$file" =~ ^mealie/lang/ ]] || \
[[ "$file" =~ ^mealie/repos/seed/resources/[^/]+/locales/ ]]; then
continue
@@ -65,8 +65,8 @@ jobs:
# File doesn't match allowed paths
echo "::error::Invalid file path: $file"
echo "Only the following paths are allowed:"
echo " - frontend/composables/use-locales/available-locales.ts"
echo " - frontend/lang/"
echo " - frontend/app/composables/use-locales/available-locales.ts"
echo " - frontend/app/lang/"
echo " - mealie/lang/"
echo " - mealie/repos/seed/resources/*/locales/"
exit 1

View File

@@ -17,12 +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
@@ -32,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 }}
@@ -49,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
@@ -68,12 +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"
@@ -81,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
@@ -97,7 +97,7 @@ jobs:
task py:package
- name: Archive built package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: backend-dist
path: dist

View File

@@ -44,11 +44,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -62,7 +62,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -75,6 +75,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ jobs:
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout 🛎
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
@@ -124,7 +124,7 @@ jobs:
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout 🛎
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0

View File

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

View File

@@ -46,12 +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"
@@ -60,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

@@ -13,12 +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
@@ -28,7 +28,7 @@ jobs:
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache node_modules 📦
uses: actions/cache@v4
uses: actions/cache@v5
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
###############################################
# Frontend Build
###############################################
FROM node:24@sha256:bb20cf73b3ad7212834ec48e2174cdcb5775f6550510a5336b842ae32741ce6c \
FROM node:24@sha256:33cf7f057918860b043c307751ef621d74ac96f875b79b6724dcebf2dfd0db6d \
AS frontend-builder
WORKDIR /frontend

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

@@ -6,10 +6,16 @@
### Creating Recipes
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.
- **Recipe HTML or JSON:** Copy/paste structured HTML or JSON and Mealie can import it.
- **Manual Editor:** Create recipes from scratch using the integrated editor.
Mealie's [AI integration](./installation/open-ai.md) greatly expands the ways you can create recipes:
- **Image Import:** Upload an image of a written or typed recipe and Mealie will use OCR and AI to import it.
- **Video URL Import:** Provide a video URL (e.g., YouTube) and Mealie will transcribe the audio and turn it into a recipe.
[Creation Demo](https://demo.mealie.io/g/home/r/create/url){ .md-button .md-button--primary .align-right }
### Importing Recipes

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.14.0`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.16.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,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.14.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.16.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.14.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.16.0 # (3)
container_name: mealie
restart: always
ports:

View File

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

View File

@@ -19,6 +19,7 @@ theme:
custom_dir: docs/overrides
features:
- content.code.annotate
- content.code.copy
- navigation.top
- navigation.instant
- navigation.expand

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,19 +41,14 @@
>
<v-select
v-if="index"
:model-value="field.logicalOperator"
:model-value="field.logicalOperator?.value"
:items="[logOps.AND, logOps.OR]"
item-title="label"
item-value="value"
variant="underlined"
class="text-center"
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- left parenthesis -->
@@ -67,14 +62,9 @@
:model-value="field.leftParenthesis"
:items="['', '(', '((', '(((']"
variant="underlined"
class="text-center"
@update:model-value="setLeftParenthesisValue(field, index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- field name -->
@@ -84,19 +74,14 @@
:class="config.col.class"
>
<v-select
chips
:model-value="field.label"
:items="fieldDefs"
variant="underlined"
item-title="label"
item-value="label"
class="text-center"
@update:model-value="setField(index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- relational operator -->
@@ -107,19 +92,14 @@
>
<v-select
v-if="field.type !== 'boolean'"
:model-value="field.relationalOperatorValue"
:model-value="field.relationalOperatorValue?.value"
:items="field.relationalOperatorChoices"
item-title="label"
item-value="value"
variant="underlined"
class="text-center"
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- field value -->
@@ -275,23 +255,14 @@
:model-value="field.rightParenthesis"
:items="['', ')', '))', ')))']"
variant="underlined"
class="text-center"
@update:model-value="setRightParenthesisValue(field, index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
</v-col>
<!-- field actions -->
<v-col
/>
v-if="!$vuetify.display.smAndDown || index === fields.length - 1"
:cols="config.items.fieldActions.cols(index)"
:sm="config.items.fieldActions.sm(index)"
:class="config.col.class"
>
>
<BaseButtonGroup
:buttons="[
{
@@ -723,9 +694,6 @@ const config = computed(() => {
col: {
class: "d-flex justify-center align-end py-0",
},
select: {
textClass: "d-flex justify-center text-center",
},
items: {
icon: {
cols: (_index: number) => 2,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,17 @@
<template>
<div class="text-center">
<BaseDialog
v-model="dialogDeleteImage"
:title="$t('recipe.delete-image')"
:icon="$globals.icons.alertCircle"
color="error"
can-delete
@delete="deleteImage"
>
<v-card-text>
{{ $t("recipe.delete-image-confirmation") }}
</v-card-text>
</BaseDialog>
<v-menu
v-model="menu"
offset-y
@@ -37,18 +49,6 @@
delete
@click="dialogDeleteImage = true"
/>
<BaseDialog
v-model="dialogDeleteImage"
:title="$t('recipe.delete-image')"
:icon="$globals.icons.alertCircle"
color="error"
can-delete
@delete="deleteImage"
>
<v-card-text>
{{ $t("recipe.delete-image-confirmation") }}
</v-card-text>
</BaseDialog>
</div>
</v-card-title>
<v-card-text class="mt-n5">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -371,14 +371,18 @@ async function parseIngredients() {
}
state.loading.parser = true;
try {
const ingsAsString = props.ingredients
.filter(ing => !ing.referencedRecipe)
.map(ing => ingredientToParserString(ing));
const filteredIngredients = props.ingredients.filter(ing => !ing.referencedRecipe);
const ingsAsString = filteredIngredients.map(ing => ingredientToParserString(ing));
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
if (error || !data) {
throw new Error("Failed to parse ingredients");
}
parsedIngs.value = data;
// Restore section titles from original ingredients the parser doesn't return them
data.forEach((parsed, index) => {
parsed.ingredient.title = filteredIngredients[index]?.title || "";
});
const parsed = data ?? [];
const recipeRefs = props.ingredients.filter(ing => ing.referencedRecipe).map(ing => ({
input: ing.note || "",

View File

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

View File

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

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