mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-15 05:20:15 -04:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db2c14093d | ||
|
|
9a0525c3a0 | ||
|
|
a2e5826da0 | ||
|
|
d4f4ba0c8d | ||
|
|
8cd5835dd8 | ||
|
|
7aa131b326 | ||
|
|
af264bd288 | ||
|
|
72388e8bcf | ||
|
|
c0afef46d6 | ||
|
|
f90665cce9 | ||
|
|
942ac741cd | ||
|
|
1d3a7e8d62 | ||
|
|
5e85fc409e | ||
|
|
2c20e96ede | ||
|
|
608fc39747 | ||
|
|
ed2f40cd6a | ||
|
|
a080cdb432 | ||
|
|
83101e3ed5 | ||
|
|
5d90997ace | ||
|
|
c78c6cf926 | ||
|
|
e26191d116 | ||
|
|
3774f68393 | ||
|
|
c46c412bf5 | ||
|
|
aa9e61a16f | ||
|
|
b2f8d63f33 | ||
|
|
72b47a1103 | ||
|
|
29e150d547 | ||
|
|
e9ae6d86a4 | ||
|
|
f799938373 | ||
|
|
e5fff4ec5c | ||
|
|
192e531c1f | ||
|
|
45e710ee72 | ||
|
|
be579ed664 | ||
|
|
fe953896f8 | ||
|
|
decf7cb307 | ||
|
|
d396a8fdc2 | ||
|
|
a3ef49f559 | ||
|
|
41e8458389 | ||
|
|
18dc2fc6a8 | ||
|
|
6355b3c8db | ||
|
|
3ac8af138f | ||
|
|
2b3803fb2e | ||
|
|
6a80e70486 | ||
|
|
f1dc854770 | ||
|
|
581aa929bd | ||
|
|
461e51bd22 | ||
|
|
1cdf43c599 | ||
|
|
6bfbc7ca0a | ||
|
|
608dbaa4c1 | ||
|
|
89c1e007cb | ||
|
|
fb5db583d2 | ||
|
|
bef3045e65 | ||
|
|
ff958a5015 | ||
|
|
37789c342e | ||
|
|
b6b8bea925 | ||
|
|
60834178ba | ||
|
|
0375a0bd5a | ||
|
|
3361f9a7c3 | ||
|
|
0883ef05ab | ||
|
|
c4eb020a66 | ||
|
|
600f407b4f | ||
|
|
6f92a829d6 | ||
|
|
6b11ff5128 | ||
|
|
29fdad1574 | ||
|
|
54b3df105c | ||
|
|
9a3303b06c | ||
|
|
c17accd82b | ||
|
|
18f7e8d935 | ||
|
|
6d2936cab6 | ||
|
|
cc2e33a254 | ||
|
|
eee6f8113c | ||
|
|
bd10cb8cd8 | ||
|
|
d03081c4e6 | ||
|
|
64d865bf7e | ||
|
|
27efda2772 | ||
|
|
81986e63b8 | ||
|
|
42eef17cfb | ||
|
|
1f724856b1 | ||
|
|
618ea06b7a | ||
|
|
ca2039ae35 | ||
|
|
15ecab86d1 | ||
|
|
aa164424d3 | ||
|
|
99acb349bd |
5
.github/workflows/locale-sync.yml
vendored
5
.github/workflows/locale-sync.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
# Add and commit changes
|
# Add and commit changes
|
||||||
git add .
|
git add .
|
||||||
git commit -m "chore: automatic locale sync"
|
git commit -m "chore: crowdin locale sync"
|
||||||
|
|
||||||
# Push the branch
|
# Push the branch
|
||||||
git push origin "$BRANCH_NAME"
|
git push origin "$BRANCH_NAME"
|
||||||
@@ -96,9 +96,10 @@ jobs:
|
|||||||
# Create PR using GitHub CLI with explicit repository
|
# Create PR using GitHub CLI with explicit repository
|
||||||
gh pr create \
|
gh pr create \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--title "chore: automatic locale sync" \
|
--title "chore(l10n): Crowdin locale sync" \
|
||||||
--base "$BASE_BRANCH" \
|
--base "$BASE_BRANCH" \
|
||||||
--head "$BRANCH_NAME" \
|
--head "$BRANCH_NAME" \
|
||||||
|
--label "l10n" \
|
||||||
--body "## Summary
|
--body "## Summary
|
||||||
|
|
||||||
Automatically generated locale updates from the weekly sync job.
|
Automatically generated locale updates from the weekly sync job.
|
||||||
|
|||||||
1
.github/workflows/pull-request-lint.yml
vendored
1
.github/workflows/pull-request-lint.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
|||||||
deps
|
deps
|
||||||
auto
|
auto
|
||||||
l10n
|
l10n
|
||||||
|
config
|
||||||
# Configure that a scope must always be provided.
|
# Configure that a scope must always be provided.
|
||||||
requireScope: false
|
requireScope: false
|
||||||
# If the PR contains one of these newline-delimited labels, the
|
# If the PR contains one of these newline-delimited labels, the
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ repos:
|
|||||||
exclude: ^tests/data/
|
exclude: ^tests/data/
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.12.10
|
rev: v0.12.12
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ tasks:
|
|||||||
- rm -r ./dev/data/recipes/
|
- rm -r ./dev/data/recipes/
|
||||||
- rm -r ./dev/data/users/
|
- rm -r ./dev/data/users/
|
||||||
- rm -f ./dev/data/mealie*.db
|
- rm -f ./dev/data/mealie*.db
|
||||||
|
- rm -f ./dev/data/mealie*.db-shm
|
||||||
|
- rm -f ./dev/data/mealie*.db-wal
|
||||||
- rm -f ./dev/data/mealie.log
|
- rm -f ./dev/data/mealie.log
|
||||||
- rm -f ./dev/data/.secret
|
- rm -f ./dev/data/.secret
|
||||||
|
|
||||||
|
|||||||
@@ -173,9 +173,25 @@ the code generation ID is hardcoded into the script and required in the nuxt con
|
|||||||
|
|
||||||
|
|
||||||
def inject_nuxt_values():
|
def inject_nuxt_values():
|
||||||
all_date_locales = [
|
datetime_files = list(datetime_dir.glob("*.json"))
|
||||||
f'"{match.stem}": require("./lang/dateTimeFormats/{match.name}"),' for match in datetime_dir.glob("*.json")
|
datetime_files.sort()
|
||||||
]
|
|
||||||
|
datetime_imports = []
|
||||||
|
datetime_object_entries = []
|
||||||
|
|
||||||
|
for match in datetime_files:
|
||||||
|
# Convert locale name to camelCase variable name (e.g., "en-US" -> "enUS")
|
||||||
|
var_name = match.stem.replace("-", "")
|
||||||
|
|
||||||
|
# Generate import statement
|
||||||
|
import_line = f'import * as {var_name} from "./lang/dateTimeFormats/{match.name}";'
|
||||||
|
datetime_imports.append(import_line)
|
||||||
|
|
||||||
|
# Generate object entry
|
||||||
|
object_entry = f' "{match.stem}": {var_name},'
|
||||||
|
datetime_object_entries.append(object_entry)
|
||||||
|
|
||||||
|
all_date_locales = datetime_imports + ["", "const datetimeFormats = {"] + datetime_object_entries + ["};"]
|
||||||
|
|
||||||
all_langs = []
|
all_langs = []
|
||||||
for match in locales_dir.glob("*.json"):
|
for match in locales_dir.glob("*.json"):
|
||||||
@@ -186,7 +202,6 @@ def inject_nuxt_values():
|
|||||||
all_langs.append(lang_string)
|
all_langs.append(lang_string)
|
||||||
|
|
||||||
all_langs.sort()
|
all_langs.sort()
|
||||||
all_date_locales.sort()
|
|
||||||
|
|
||||||
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
||||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
###############################################
|
###############################################
|
||||||
# Frontend Build
|
# Frontend Build
|
||||||
###############################################
|
###############################################
|
||||||
FROM node:20@sha256:572a90df10a58ebb7d3f223d661d964a6c2383a9c2b5763162b4f631c53dc56a \
|
FROM node:20@sha256:f3e50c7689a1b6982fab45b1b23ba5adf1fd725e233dc640918fb59f7a57b174 \
|
||||||
AS frontend-builder
|
AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Once the prerequisites are installed you can cd into the project base directory
|
|||||||
=== "Linux / macOS"
|
=== "Linux / macOS"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Naviate To The Root Directory
|
# Navigate To The Root Directory
|
||||||
cd /path/to/project
|
cd /path/to/project
|
||||||
|
|
||||||
# Utilize the Taskfile to Install Dependencies
|
# Utilize the Taskfile to Install Dependencies
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ The shopping lists feature is a great way to keep track of what you need to buy
|
|||||||
Managing shopping lists can be done from the Sidebar > Shopping Lists.
|
Managing shopping lists can be done from the Sidebar > Shopping Lists.
|
||||||
|
|
||||||
Here you will be able to:
|
Here you will be able to:
|
||||||
|
|
||||||
- See items already on the Shopping List
|
- See items already on the Shopping List
|
||||||
- See linked recipes with ingredients
|
- 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.
|
||||||
@@ -117,6 +118,7 @@ Mealie is designed to integrate with many different external services. There are
|
|||||||
### Notifiers
|
### Notifiers
|
||||||
|
|
||||||
Notifiers are event-driven notifications sent when specific actions are performed within Mealie. Some actions include:
|
Notifiers are event-driven notifications sent when specific actions are performed within Mealie. Some actions include:
|
||||||
|
|
||||||
- Creating / Updating a recipe
|
- Creating / Updating a recipe
|
||||||
- Adding items to a shopping list
|
- Adding items to a shopping list
|
||||||
- Creating a new mealplan
|
- Creating a new mealplan
|
||||||
@@ -198,6 +200,7 @@ Mealie lets you fully customize how you organize your users. You can use Groups
|
|||||||
Groups are fully isolated instances of Mealie. Think of a goup as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
|
Groups are fully isolated instances of Mealie. Think of a goup as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
|
||||||
|
|
||||||
Common use cases for groups include:
|
Common use cases for groups include:
|
||||||
|
|
||||||
- Hosting multiple instances of Mealie for others who want to keep their data private and secure
|
- Hosting multiple instances of Mealie for others who want to keep their data private and secure
|
||||||
- Creating completely isolated recipe pools
|
- Creating completely isolated recipe pools
|
||||||
|
|
||||||
@@ -206,6 +209,7 @@ Common use cases for groups include:
|
|||||||
Households are subdivisions within a single Group. Households maintain their own users and settings, while sharing their recipes with other households. Households also share organizers (tags, categories, etc.) with the entire group. Meal Plans, Shopping Lists, and Integrations are only accessible within a household.
|
Households are subdivisions within a single Group. Households maintain their own users and settings, while sharing their recipes with other households. Households also share organizers (tags, categories, etc.) with the entire group. Meal Plans, Shopping Lists, and Integrations are only accessible within a household.
|
||||||
|
|
||||||
Common use cases for households include:
|
Common use cases for households include:
|
||||||
|
|
||||||
- Sharing a common recipe pool amongst families
|
- Sharing a common recipe pool amongst families
|
||||||
- Maintaining separate meal plans and shopping lists from other households
|
- Maintaining separate meal plans and shopping lists from other households
|
||||||
- Maintaining separate integrations and customizations from other households
|
- Maintaining separate integrations and customizations from other households
|
||||||
|
|||||||
@@ -32,15 +32,16 @@
|
|||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
| ------------------------------------------------------- | :------: | ----------------------------------------------------------------------- |
|
|---------------------------------------------------------|:--------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
|
||||||
| POSTGRES_USER<super>[†][secrets]</super> | mealie | Postgres database user |
|
| SQLITE_MIGRATE_JOURNAL_WAL | False | If set to true, switches SQLite's journal mode to WAL, which allows for multiple concurrent accesses. This can be useful when you have a decent amount of concurrency or when using certain remote storage systems such as Ceph. |
|
||||||
| POSTGRES_PASSWORD<super>[†][secrets]</super> | mealie | Postgres database password |
|
| POSTGRES_USER<super>[†][secrets]</super> | mealie | Postgres database user |
|
||||||
| POSTGRES_SERVER<super>[†][secrets]</super> | postgres | Postgres database server address |
|
| POSTGRES_PASSWORD<super>[†][secrets]</super> | mealie | Postgres database password |
|
||||||
| POSTGRES_PORT<super>[†][secrets]</super> | 5432 | Postgres database port |
|
| POSTGRES_SERVER<super>[†][secrets]</super> | postgres | Postgres database server address |
|
||||||
| POSTGRES_DB<super>[†][secrets]</super> | mealie | Postgres database name |
|
| POSTGRES_PORT<super>[†][secrets]</super> | 5432 | Postgres database port |
|
||||||
| POSTGRES_URL_OVERRIDE<super>[†][secrets]</super> | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables |
|
| POSTGRES_DB<super>[†][secrets]</super> | mealie | Postgres database name |
|
||||||
|
| POSTGRES_URL_OVERRIDE<super>[†][secrets]</super> | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables |
|
||||||
|
|
||||||
### Email
|
### Email
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
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!
|
1. Take a backup just in case!
|
||||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.1.1`
|
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.2.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.
|
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
|
4. Restart the container
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ The following steps were tested on a Ubuntu 20.04 server, but should work for mo
|
|||||||
|
|
||||||
## Step 3: Customizing The `docker-compose.yaml` files.
|
## Step 3: Customizing The `docker-compose.yaml` files.
|
||||||
|
|
||||||
After you've decided setup the files it's important to set a few ENV variables to ensure that you can use all the features of Mealie. I recommend that you verify and check that:
|
After you've decided how to set up your files, it's important to set a few ENV variables to ensure that you can use all the features of Mealie. Verify that:
|
||||||
|
|
||||||
- [x] You've configured the relevant ENV variables for your database selection in the `docker-compose.yaml` files.
|
- [x] You've configured the relevant ENV variables for your database selection in the `docker-compose.yaml` files.
|
||||||
- [x] You've configured the [SMTP server settings](./backend-config.md#email) (used for invitations, password resets, etc). You can setup a [google app password](https://support.google.com/accounts/answer/185833?hl=en) if you want to send email via gmail.
|
- [x] You've configured the [SMTP server settings](./backend-config.md#email) (used for invitations, password resets, etc). You can setup a [google app password](https://support.google.com/accounts/answer/185833?hl=en) if you want to send email via gmail.
|
||||||
@@ -117,7 +117,7 @@ The latest tag provides the latest released image of Mealie.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**These tags no are long updated**
|
**These tags are no longer updated**
|
||||||
|
|
||||||
`mealie:frontend-v1.0.0beta-x` **and** `mealie:api-v1.0.0beta-x`
|
`mealie:frontend-v1.0.0beta-x` **and** `mealie:api-v1.0.0beta-x`
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v3.1.1 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.2.0 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v3.1.1 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.2.0 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
You MUST read the release notes prior to upgrading your container. Mealie has a robust backup and restore system for managing your data. Pre-v1.0.0 versions of Mealie use a different database structure, so if you are upgrading from pre-v1.0.0 to v1.0.0, you MUST backup your data and then re-import it. Even if you are already on v1.0.0, it is strongly recommended to backup all data before updating.
|
You MUST read the release notes prior to upgrading your container. Mealie has a robust backup and restore system for managing your data. Pre-v1.0.0 versions of Mealie use a different database structure, so if you are upgrading from pre-v1.0.0 to v1.0.0, you MUST backup your data and then re-import it. Even if you are already on v1.0.0, it is strongly recommended to backup all data before updating.
|
||||||
|
|
||||||
### Before Upgrading
|
### Before Upgrading
|
||||||
- Read The Release Notes
|
- [Read The Release Notes](https://github.com/mealie-recipes/mealie/releases)
|
||||||
- Identify Breaking Changes
|
- Identify Breaking Changes
|
||||||
- Create a Backup and Download from the UI
|
- Create a Backup and Download from the UI
|
||||||
- Upgrade
|
- Upgrade
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
import RecipeContextMenu from "./RecipeContextMenu/RecipeContextMenu.vue";
|
||||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||||
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
|
|||||||
@@ -55,12 +55,9 @@
|
|||||||
/>
|
/>
|
||||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||||
|
|
||||||
<RecipeRating
|
<RecipeCardRating
|
||||||
class="ml-n2"
|
|
||||||
:model-value="rating"
|
:model-value="rating"
|
||||||
:recipe-id="recipeId"
|
:recipe-id="recipeId"
|
||||||
:slug="slug"
|
|
||||||
small
|
|
||||||
/>
|
/>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<RecipeChips
|
<RecipeChips
|
||||||
@@ -75,9 +72,10 @@
|
|||||||
|
|
||||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||||
<RecipeContextMenu
|
<RecipeContextMenu
|
||||||
v-if="isOwnGroup"
|
v-if="isOwnGroup && showRecipeContent"
|
||||||
color="grey-darken-2"
|
color="grey-darken-2"
|
||||||
:slug="slug"
|
:slug="slug"
|
||||||
|
:menu-icon="$globals.icons.dotsVertical"
|
||||||
:name="name"
|
:name="name"
|
||||||
:recipe-id="recipeId"
|
:recipe-id="recipeId"
|
||||||
:use-items="{
|
:use-items="{
|
||||||
@@ -90,7 +88,7 @@
|
|||||||
printPreferences: false,
|
printPreferences: false,
|
||||||
share: true,
|
share: true,
|
||||||
}"
|
}"
|
||||||
@delete="$emit('delete', slug)"
|
@deleted="$emit('delete', slug)"
|
||||||
/>
|
/>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</slot>
|
</slot>
|
||||||
@@ -103,9 +101,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||||
import RecipeChips from "./RecipeChips.vue";
|
import RecipeChips from "./RecipeChips.vue";
|
||||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
import RecipeContextMenu from "./RecipeContextMenu/RecipeContextMenu.vue";
|
||||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||||
import RecipeRating from "./RecipeRating.vue";
|
import RecipeCardRating from "./RecipeCardRating.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -87,13 +87,11 @@
|
|||||||
class="ma-0 pa-0"
|
class="ma-0 pa-0"
|
||||||
/>
|
/>
|
||||||
<div v-else class="my-0 px-1 py-0" /> <!-- Empty div to keep the layout consistent -->
|
<div v-else class="my-0 px-1 py-0" /> <!-- Empty div to keep the layout consistent -->
|
||||||
<RecipeRating
|
<RecipeCardRating
|
||||||
v-if="showRecipeContent"
|
v-if="showRecipeContent"
|
||||||
:class="[{ 'pb-2': !isOwnGroup }, 'ml-n2']"
|
:class="[{ 'pb-2': !isOwnGroup }, 'ml-n2']"
|
||||||
:value="rating"
|
:model-value="rating"
|
||||||
:recipe-id="recipeId"
|
:recipe-id="recipeId"
|
||||||
:slug="slug"
|
|
||||||
small
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||||
@@ -128,9 +126,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
import RecipeContextMenu from "./RecipeContextMenu/RecipeContextMenu.vue";
|
||||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||||
import RecipeRating from "./RecipeRating.vue";
|
import RecipeCardRating from "./RecipeCardRating.vue";
|
||||||
import RecipeChips from "./RecipeChips.vue";
|
import RecipeChips from "./RecipeChips.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
|
|
||||||
|
|||||||
101
frontend/components/Domain/Recipe/RecipeCardRating.vue
Normal file
101
frontend/components/Domain/Recipe/RecipeCardRating.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rating-display">
|
||||||
|
<span
|
||||||
|
v-for="(star, index) in ratingDisplay"
|
||||||
|
:key="index"
|
||||||
|
class="star"
|
||||||
|
:class="{
|
||||||
|
'star-half': star === 'half',
|
||||||
|
'text-secondary': !useGroupStyle,
|
||||||
|
'text-grey-darken-1': useGroupStyle,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- We render both the full and empty stars for "half" stars because they're layered over each other -->
|
||||||
|
<span
|
||||||
|
v-if="star === 'empty' || star === 'half'"
|
||||||
|
class="star-empty"
|
||||||
|
>
|
||||||
|
☆
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="star === 'full' || star === 'half'"
|
||||||
|
class="star-full"
|
||||||
|
>
|
||||||
|
★
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
|
import { useUserSelfRatings } from "~/composables/use-users";
|
||||||
|
|
||||||
|
type Star = "full" | "half" | "empty";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
recipeId: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
const { userRatings } = useUserSelfRatings();
|
||||||
|
|
||||||
|
const userRating = computed(() => {
|
||||||
|
return userRatings.value.find(r => r.recipeId === props.recipeId)?.rating ?? undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ratingValue = computed(() => userRating.value || props.modelValue || 0);
|
||||||
|
const useGroupStyle = computed(() => isOwnGroup.value && !userRating.value && props.modelValue);
|
||||||
|
const ratingDisplay = computed<Star[]>(
|
||||||
|
() => {
|
||||||
|
const stars: Star[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const diff = ratingValue.value - i;
|
||||||
|
if (diff >= 1) {
|
||||||
|
stars.push("full");
|
||||||
|
}
|
||||||
|
else if (diff >= 0.25) { // round to half star if rating is at least 0.25 but not quite a full star
|
||||||
|
stars.push("half");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stars.push("empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stars;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.rating-display {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
|
.star {
|
||||||
|
font-size: 18px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
&.star-half {
|
||||||
|
.star-full {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -199,7 +199,7 @@ const emit = defineEmits<{
|
|||||||
appendRecipes: [recipes: Recipe[]];
|
appendRecipes: [recipes: Recipe[]];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { $vuetify } = useNuxtApp();
|
const display = useDisplay();
|
||||||
const preferences = useUserSortPreferences();
|
const preferences = useUserSortPreferences();
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
@@ -215,7 +215,7 @@ const $auth = useMealieAuth();
|
|||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const useMobileCards = computed(() => {
|
const useMobileCards = computed(() => {
|
||||||
return $vuetify.display.smAndDown.value || preferences.value.useMobileCards;
|
return display.smAndDown.value || preferences.value.useMobileCards;
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayTitleIcon = computed(() => {
|
const displayTitleIcon = computed(() => {
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-menu
|
||||||
|
offset-y
|
||||||
|
start
|
||||||
|
:eager="isMenuContentLoaded"
|
||||||
|
:bottom="!menuTop"
|
||||||
|
:nudge-bottom="!menuTop ? '5' : '0'"
|
||||||
|
:top="menuTop"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
|
<template #activator="{ props: activatorProps }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
:variant="fab ? 'flat' : undefined"
|
||||||
|
:rounded="fab ? 'circle' : undefined"
|
||||||
|
:size="fab ? 'small' : undefined"
|
||||||
|
:color="fab ? 'info' : 'secondary'"
|
||||||
|
:fab="fab"
|
||||||
|
v-bind="activatorProps"
|
||||||
|
@click.prevent
|
||||||
|
@mouseenter="onHover"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
:size="!fab ? undefined : 'x-large'"
|
||||||
|
:color="fab ? 'white' : 'secondary'"
|
||||||
|
>
|
||||||
|
{{ icon }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<RecipeContextMenuContent
|
||||||
|
v-if="isMenuContentLoaded"
|
||||||
|
v-bind="contentProps"
|
||||||
|
@deleted="$emit('deleted', $event)"
|
||||||
|
/>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
|
interface ContextMenuIncludes {
|
||||||
|
delete?: boolean;
|
||||||
|
edit?: boolean;
|
||||||
|
download?: boolean;
|
||||||
|
duplicate?: boolean;
|
||||||
|
mealplanner?: boolean;
|
||||||
|
shoppingList?: boolean;
|
||||||
|
print?: boolean;
|
||||||
|
printPreferences?: boolean;
|
||||||
|
share?: boolean;
|
||||||
|
recipeActions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextMenuItem {
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
color?: string;
|
||||||
|
event: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
useItems?: ContextMenuIncludes;
|
||||||
|
appendItems?: ContextMenuItem[];
|
||||||
|
leadingItems?: ContextMenuItem[];
|
||||||
|
menuTop?: boolean;
|
||||||
|
fab?: boolean;
|
||||||
|
color?: string;
|
||||||
|
slug: string;
|
||||||
|
menuIcon?: string | null;
|
||||||
|
name: string;
|
||||||
|
recipe?: Recipe;
|
||||||
|
recipeId: string;
|
||||||
|
recipeScale?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
useItems: () => ({
|
||||||
|
delete: true,
|
||||||
|
edit: true,
|
||||||
|
download: true,
|
||||||
|
duplicate: false,
|
||||||
|
mealplanner: true,
|
||||||
|
shoppingList: true,
|
||||||
|
print: true,
|
||||||
|
printPreferences: true,
|
||||||
|
share: true,
|
||||||
|
recipeActions: true,
|
||||||
|
}),
|
||||||
|
appendItems: () => [],
|
||||||
|
leadingItems: () => [],
|
||||||
|
menuTop: true,
|
||||||
|
fab: false,
|
||||||
|
color: "primary",
|
||||||
|
menuIcon: null,
|
||||||
|
recipe: undefined,
|
||||||
|
recipeScale: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
[key: string]: any;
|
||||||
|
deleted: [slug: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
|
const isMenuContentLoaded = ref(false);
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
return props.menuIcon || $globals.icons.dotsVertical;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Props to pass to the content component (excluding internal wrapper props)
|
||||||
|
const contentProps = computed(() => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onHover() {
|
||||||
|
if (!isMenuContentLoaded.value) {
|
||||||
|
isMenuContentLoaded.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMenuToggle(isOpen: boolean) {
|
||||||
|
if (isOpen && !isMenuContentLoaded.value) {
|
||||||
|
isMenuContentLoaded.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecipeContextMenuContent = defineAsyncComponent(
|
||||||
|
() => import("./RecipeContextMenuContent.vue"),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@@ -1,159 +1,125 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-center">
|
<RecipeDialogShare v-model="shareDialog" :recipe-id="recipeId" :name="name" />
|
||||||
<!-- Recipe Share Dialog -->
|
<RecipeDialogPrintPreferences v-model="printPreferencesDialog" :recipe="recipeRef" />
|
||||||
<RecipeDialogShare v-model="shareDialog" :recipe-id="recipeId" :name="name" />
|
<BaseDialog
|
||||||
<RecipeDialogPrintPreferences v-model="printPreferencesDialog" :recipe="recipeRef" />
|
v-model="recipeDeleteDialog"
|
||||||
<BaseDialog
|
:title="$t('recipe.delete-recipe')"
|
||||||
v-model="recipeDeleteDialog"
|
color="error"
|
||||||
:title="$t('recipe.delete-recipe')"
|
:icon="$globals.icons.alertCircle"
|
||||||
color="error"
|
can-confirm
|
||||||
:icon="$globals.icons.alertCircle"
|
@confirm="deleteRecipe()"
|
||||||
can-confirm
|
>
|
||||||
@confirm="deleteRecipe()"
|
<v-card-text>
|
||||||
>
|
<template v-if="isAdminAndNotOwner">
|
||||||
<v-card-text>
|
{{ $t("recipe.admin-delete-confirmation") }}
|
||||||
<template v-if="isAdminAndNotOwner">
|
|
||||||
{{ $t("recipe.admin-delete-confirmation") }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ $t("recipe.delete-confirmation") }}
|
|
||||||
</template>
|
|
||||||
</v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
<BaseDialog
|
|
||||||
v-model="recipeDuplicateDialog"
|
|
||||||
:title="$t('recipe.duplicate')"
|
|
||||||
color="primary"
|
|
||||||
:icon="$globals.icons.duplicate"
|
|
||||||
can-confirm
|
|
||||||
@confirm="duplicateRecipe()"
|
|
||||||
>
|
|
||||||
<v-card-text>
|
|
||||||
<v-text-field
|
|
||||||
v-model="recipeName"
|
|
||||||
density="compact"
|
|
||||||
:label="$t('recipe.recipe-name')"
|
|
||||||
autofocus
|
|
||||||
@keyup.enter="duplicateRecipe()"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
<BaseDialog
|
|
||||||
v-model="mealplannerDialog"
|
|
||||||
:title="$t('recipe.add-recipe-to-mealplan')"
|
|
||||||
color="primary"
|
|
||||||
:icon="$globals.icons.calendar"
|
|
||||||
can-confirm
|
|
||||||
@confirm="addRecipeToPlan()"
|
|
||||||
>
|
|
||||||
<v-card-text>
|
|
||||||
<v-menu
|
|
||||||
v-model="pickerMenu"
|
|
||||||
:close-on-content-click="false"
|
|
||||||
transition="scale-transition"
|
|
||||||
offset-y
|
|
||||||
max-width="290px"
|
|
||||||
min-width="auto"
|
|
||||||
>
|
|
||||||
<template #activator="{ props: activatorProps }">
|
|
||||||
<v-text-field
|
|
||||||
v-model="newMealdateString"
|
|
||||||
:label="$t('general.date')"
|
|
||||||
:prepend-icon="$globals.icons.calendar"
|
|
||||||
v-bind="activatorProps"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<v-date-picker
|
|
||||||
v-model="newMealdate"
|
|
||||||
hide-header
|
|
||||||
:first-day-of-week="firstDayOfWeek"
|
|
||||||
:local="$i18n.locale"
|
|
||||||
@update:model-value="pickerMenu = false"
|
|
||||||
/>
|
|
||||||
</v-menu>
|
|
||||||
<v-select
|
|
||||||
v-model="newMealType"
|
|
||||||
:return-object="false"
|
|
||||||
:items="planTypeOptions"
|
|
||||||
:label="$t('recipe.entry-type')"
|
|
||||||
item-title="text"
|
|
||||||
item-value="value"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
<RecipeDialogAddToShoppingList
|
|
||||||
v-if="shoppingLists && recipeRefWithScale"
|
|
||||||
v-model="shoppingListDialog"
|
|
||||||
:recipes="[recipeRefWithScale]"
|
|
||||||
:shopping-lists="shoppingLists"
|
|
||||||
/>
|
|
||||||
<v-menu
|
|
||||||
offset-y
|
|
||||||
start
|
|
||||||
:bottom="!menuTop"
|
|
||||||
:nudge-bottom="!menuTop ? '5' : '0'"
|
|
||||||
:top="menuTop"
|
|
||||||
:nudge-top="menuTop ? '5' : '0'"
|
|
||||||
allow-overflow
|
|
||||||
close-delay="125"
|
|
||||||
:open-on-hover="$vuetify.display.mdAndUp"
|
|
||||||
content-class="d-print-none"
|
|
||||||
>
|
|
||||||
<template #activator="{ props: activatorProps }">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
:variant="fab ? 'flat' : undefined"
|
|
||||||
:rounded="fab ? 'circle' : undefined"
|
|
||||||
:size="fab ? 'small' : undefined"
|
|
||||||
:color="fab ? 'info' : 'secondary'"
|
|
||||||
:fab="fab"
|
|
||||||
v-bind="activatorProps"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
<v-icon
|
|
||||||
:size="!fab ? undefined : 'x-large'"
|
|
||||||
:color="fab ? 'white' : 'secondary'"
|
|
||||||
>
|
|
||||||
{{ icon }}
|
|
||||||
</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
</template>
|
||||||
<v-list density="compact">
|
<template v-else>
|
||||||
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
{{ $t("recipe.delete-confirmation") }}
|
||||||
<template #prepend>
|
</template>
|
||||||
<v-icon :color="item.color">
|
</v-card-text>
|
||||||
{{ item.icon }}
|
</BaseDialog>
|
||||||
</v-icon>
|
<BaseDialog
|
||||||
</template>
|
v-model="recipeDuplicateDialog"
|
||||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
:title="$t('recipe.duplicate')"
|
||||||
</v-list-item>
|
color="primary"
|
||||||
<div v-if="useItems.recipeActions && recipeActions && recipeActions.length">
|
:icon="$globals.icons.duplicate"
|
||||||
<v-divider />
|
can-confirm
|
||||||
<v-list-item
|
@confirm="duplicateRecipe()"
|
||||||
v-for="(action, index) in recipeActions"
|
>
|
||||||
:key="index"
|
<v-card-text>
|
||||||
@click="executeRecipeAction(action)"
|
<v-text-field
|
||||||
>
|
v-model="recipeName"
|
||||||
<template #prepend>
|
density="compact"
|
||||||
<v-icon color="undefined">
|
:label="$t('recipe.recipe-name')"
|
||||||
{{ $globals.icons.linkVariantPlus }}
|
autofocus
|
||||||
</v-icon>
|
@keyup.enter="duplicateRecipe()"
|
||||||
</template>
|
/>
|
||||||
<v-list-item-title>
|
</v-card-text>
|
||||||
{{ action.title }}
|
</BaseDialog>
|
||||||
</v-list-item-title>
|
<BaseDialog
|
||||||
</v-list-item>
|
v-model="mealplannerDialog"
|
||||||
</div>
|
:title="$t('recipe.add-recipe-to-mealplan')"
|
||||||
</v-list>
|
color="primary"
|
||||||
</v-menu>
|
:icon="$globals.icons.calendar"
|
||||||
</div>
|
can-confirm
|
||||||
|
@confirm="addRecipeToPlan()"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<v-menu
|
||||||
|
v-model="pickerMenu"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
transition="scale-transition"
|
||||||
|
offset-y
|
||||||
|
max-width="290px"
|
||||||
|
min-width="auto"
|
||||||
|
>
|
||||||
|
<template #activator="{ props: activatorProps }">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newMealdateString"
|
||||||
|
:label="$t('general.date')"
|
||||||
|
:prepend-icon="$globals.icons.calendar"
|
||||||
|
v-bind="activatorProps"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<v-date-picker
|
||||||
|
v-model="newMealdate"
|
||||||
|
hide-header
|
||||||
|
:first-day-of-week="firstDayOfWeek"
|
||||||
|
:local="$i18n.locale"
|
||||||
|
@update:model-value="pickerMenu = false"
|
||||||
|
/>
|
||||||
|
</v-menu>
|
||||||
|
<v-select
|
||||||
|
v-model="newMealType"
|
||||||
|
:return-object="false"
|
||||||
|
:items="planTypeOptions"
|
||||||
|
:label="$t('recipe.entry-type')"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
<RecipeDialogAddToShoppingList
|
||||||
|
v-if="shoppingLists && recipeRefWithScale"
|
||||||
|
v-model="shoppingListDialog"
|
||||||
|
:recipes="[recipeRefWithScale]"
|
||||||
|
:shopping-lists="shoppingLists"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon :color="item.color">
|
||||||
|
{{ item.icon }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<div v-if="useItems.recipeActions && recipeActions && recipeActions.length">
|
||||||
|
<v-divider />
|
||||||
|
<v-list-item
|
||||||
|
v-for="(action, index) in recipeActions"
|
||||||
|
:key="index"
|
||||||
|
@click="executeRecipeAction(action)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon color="undefined">
|
||||||
|
{{ $globals.icons.linkVariantPlus }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ action.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</div>
|
||||||
|
</v-list>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||||
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
import RecipeDialogPrintPreferences from "~/components/Domain/Recipe/RecipeDialogPrintPreferences.vue";
|
||||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
import RecipeDialogShare from "~/components/Domain/Recipe/RecipeDialogShare.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useGroupRecipeActions } from "~/composables/use-group-recipe-actions";
|
import { useGroupRecipeActions } from "~/composables/use-group-recipe-actions";
|
||||||
@@ -225,7 +191,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
delete: [slug: string];
|
deleted: [slug: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
@@ -336,8 +302,6 @@ const defaultItems: { [key: string]: ContextMenuItem } = {
|
|||||||
// Add leading and Appending Items
|
// Add leading and Appending Items
|
||||||
menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
|
menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
|
||||||
|
|
||||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Context Menu Event Handler
|
// Context Menu Event Handler
|
||||||
|
|
||||||
@@ -407,7 +371,7 @@ async function deleteRecipe() {
|
|||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
router.push(`/g/${groupSlug.value}`);
|
router.push(`/g/${groupSlug.value}`);
|
||||||
}
|
}
|
||||||
emit("delete", props.slug);
|
emit("deleted", props.slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = useDownloader();
|
const download = useDownloader();
|
||||||
@@ -1,715 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container
|
|
||||||
fluid
|
|
||||||
class="px-0"
|
|
||||||
>
|
|
||||||
<div class="search-container pb-8">
|
|
||||||
<form
|
|
||||||
class="search-box pa-2"
|
|
||||||
@submit.prevent="search"
|
|
||||||
>
|
|
||||||
<div class="d-flex justify-center mb-2">
|
|
||||||
<v-text-field
|
|
||||||
ref="input"
|
|
||||||
v-model="state.search"
|
|
||||||
variant="outlined"
|
|
||||||
hide-details
|
|
||||||
clearable
|
|
||||||
color="primary"
|
|
||||||
:placeholder="$t('search.search-placeholder')"
|
|
||||||
:prepend-inner-icon="$globals.icons.search"
|
|
||||||
@keyup.enter="hideKeyboard"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="search-row">
|
|
||||||
<!-- Category Filter -->
|
|
||||||
<SearchFilter
|
|
||||||
v-if="categories"
|
|
||||||
v-model="selectedCategories"
|
|
||||||
v-model:require-all="state.requireAllCategories"
|
|
||||||
:items="categories"
|
|
||||||
>
|
|
||||||
<v-icon start>
|
|
||||||
{{ $globals.icons.categories }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("category.categories") }}
|
|
||||||
</SearchFilter>
|
|
||||||
|
|
||||||
<!-- Tag Filter -->
|
|
||||||
<SearchFilter
|
|
||||||
v-if="tags"
|
|
||||||
v-model="selectedTags"
|
|
||||||
v-model:require-all="state.requireAllTags"
|
|
||||||
:items="tags"
|
|
||||||
>
|
|
||||||
<v-icon start>
|
|
||||||
{{ $globals.icons.tags }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("tag.tags") }}
|
|
||||||
</SearchFilter>
|
|
||||||
|
|
||||||
<!-- Tool Filter -->
|
|
||||||
<SearchFilter
|
|
||||||
v-if="tools"
|
|
||||||
v-model="selectedTools"
|
|
||||||
v-model:require-all="state.requireAllTools"
|
|
||||||
:items="tools"
|
|
||||||
>
|
|
||||||
<v-icon start>
|
|
||||||
{{ $globals.icons.potSteam }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("tool.tools") }}
|
|
||||||
</SearchFilter>
|
|
||||||
|
|
||||||
<!-- Food Filter -->
|
|
||||||
<SearchFilter
|
|
||||||
v-if="foods"
|
|
||||||
v-model="selectedFoods"
|
|
||||||
v-model:require-all="state.requireAllFoods"
|
|
||||||
:items="foods"
|
|
||||||
>
|
|
||||||
<v-icon start>
|
|
||||||
{{ $globals.icons.foods }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("general.foods") }}
|
|
||||||
</SearchFilter>
|
|
||||||
|
|
||||||
<!-- Household Filter -->
|
|
||||||
<SearchFilter
|
|
||||||
v-if="households.length > 1"
|
|
||||||
v-model="selectedHouseholds"
|
|
||||||
:items="households"
|
|
||||||
radio
|
|
||||||
>
|
|
||||||
<v-icon start>
|
|
||||||
{{ $globals.icons.household }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("household.households") }}
|
|
||||||
</SearchFilter>
|
|
||||||
|
|
||||||
<!-- Sort Options -->
|
|
||||||
<v-menu
|
|
||||||
offset-y
|
|
||||||
nudge-bottom="3"
|
|
||||||
>
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<v-btn
|
|
||||||
class="ml-auto"
|
|
||||||
size="small"
|
|
||||||
color="accent"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<v-icon :start="!$vuetify.display.xs">
|
|
||||||
{{ state.orderDirection === "asc" ? $globals.icons.sortAscending : $globals.icons.sortDescending }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $vuetify.display.xs ? null : sortText }}
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-card>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
slim
|
|
||||||
density="comfortable"
|
|
||||||
:prepend-icon="state.orderDirection === 'asc' ? $globals.icons.sortDescending : $globals.icons.sortAscending"
|
|
||||||
:title="state.orderDirection === 'asc' ? $t('general.sort-descending') : $t('general.sort-ascending')"
|
|
||||||
@click="toggleOrderDirection()"
|
|
||||||
/>
|
|
||||||
<v-divider />
|
|
||||||
<v-list-item
|
|
||||||
v-for="v in sortable"
|
|
||||||
:key="v.name"
|
|
||||||
:active="state.orderBy === v.value"
|
|
||||||
slim
|
|
||||||
density="comfortable"
|
|
||||||
:prepend-icon="v.icon"
|
|
||||||
:title="v.name"
|
|
||||||
@click="setOrderBy(v.value)"
|
|
||||||
/>
|
|
||||||
</v-list>
|
|
||||||
</v-card>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
<!-- Settings -->
|
|
||||||
<v-menu
|
|
||||||
offset-y
|
|
||||||
bottom
|
|
||||||
start
|
|
||||||
nudge-bottom="3"
|
|
||||||
:close-on-content-click="false"
|
|
||||||
>
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<v-btn
|
|
||||||
size="small"
|
|
||||||
color="accent"
|
|
||||||
dark
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<v-icon size="small">
|
|
||||||
{{ $globals.icons.cog }}
|
|
||||||
</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-card>
|
|
||||||
<v-card-text>
|
|
||||||
<v-switch
|
|
||||||
v-model="state.auto"
|
|
||||||
:label="$t('search.auto-search')"
|
|
||||||
single-line
|
|
||||||
/>
|
|
||||||
<v-btn
|
|
||||||
block
|
|
||||||
color="primary"
|
|
||||||
@click="reset"
|
|
||||||
>
|
|
||||||
{{ $t("general.reset") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!state.auto"
|
|
||||||
class="search-button-container"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
size="x-large"
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
block
|
|
||||||
>
|
|
||||||
<v-icon start>
|
|
||||||
{{ $globals.icons.search }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("search.search") }}
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<v-divider />
|
|
||||||
<v-container class="mt-6 px-md-6">
|
|
||||||
<RecipeCardSection
|
|
||||||
v-if="state.ready"
|
|
||||||
class="mt-n5"
|
|
||||||
:icon="$globals.icons.silverwareForkKnife"
|
|
||||||
:title="$t('general.recipes')"
|
|
||||||
:recipes="recipes"
|
|
||||||
:query="passedQueryWithSeed"
|
|
||||||
disable-sort
|
|
||||||
@item-selected="filterItems"
|
|
||||||
@replace-recipes="replaceRecipes"
|
|
||||||
@append-recipes="appendRecipes"
|
|
||||||
/>
|
|
||||||
</v-container>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { watchDebounced } from "@vueuse/shared";
|
|
||||||
import SearchFilter from "~/components/Domain/SearchFilter.vue";
|
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
|
||||||
import {
|
|
||||||
useCategoryStore,
|
|
||||||
usePublicCategoryStore,
|
|
||||||
useFoodStore,
|
|
||||||
usePublicFoodStore,
|
|
||||||
useHouseholdStore,
|
|
||||||
usePublicHouseholdStore,
|
|
||||||
useTagStore,
|
|
||||||
usePublicTagStore,
|
|
||||||
useToolStore,
|
|
||||||
usePublicToolStore,
|
|
||||||
} from "~/composables/store";
|
|
||||||
import { useUserSearchQuerySession, useUserSortPreferences } from "~/composables/use-users/preferences";
|
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
|
||||||
import type { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
|
||||||
import { useLazyRecipes } from "~/composables/recipes";
|
|
||||||
import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
|
||||||
components: { SearchFilter, RecipeCardSection },
|
|
||||||
setup() {
|
|
||||||
const router = useRouter();
|
|
||||||
const i18n = useI18n();
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { $globals } = useNuxtApp();
|
|
||||||
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
const state = ref({
|
|
||||||
auto: true,
|
|
||||||
ready: false,
|
|
||||||
search: "",
|
|
||||||
orderBy: "created_at",
|
|
||||||
orderDirection: "desc" as "asc" | "desc",
|
|
||||||
|
|
||||||
// and/or
|
|
||||||
requireAllCategories: false,
|
|
||||||
requireAllTags: false,
|
|
||||||
requireAllTools: false,
|
|
||||||
requireAllFoods: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
|
||||||
const searchQuerySession = useUserSearchQuerySession();
|
|
||||||
const sortPreferences = useUserSortPreferences();
|
|
||||||
|
|
||||||
watch(() => state.value.orderBy, (newValue) => {
|
|
||||||
sortPreferences.value.orderBy = newValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => state.value.orderDirection, (newValue) => {
|
|
||||||
sortPreferences.value.orderDirection = newValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
|
||||||
const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
|
|
||||||
const selectedCategories = ref<NoUndefinedField<RecipeCategory>[]>([]);
|
|
||||||
|
|
||||||
const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
|
||||||
const selectedFoods = ref<IngredientFood[]>([]);
|
|
||||||
|
|
||||||
const households = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
|
|
||||||
const selectedHouseholds = ref([] as NoUndefinedField<HouseholdSummary>[]);
|
|
||||||
|
|
||||||
const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
|
|
||||||
const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
|
|
||||||
|
|
||||||
const tools = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
|
|
||||||
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
|
|
||||||
|
|
||||||
function calcPassedQuery(): RecipeSearchQuery {
|
|
||||||
return {
|
|
||||||
// the search clear button sets search to null, which still renders the query param for a moment,
|
|
||||||
// whereas an empty string is not rendered
|
|
||||||
search: state.value.search ? state.value.search : "",
|
|
||||||
categories: toIDArray(selectedCategories.value),
|
|
||||||
foods: toIDArray(selectedFoods.value),
|
|
||||||
households: toIDArray(selectedHouseholds.value),
|
|
||||||
tags: toIDArray(selectedTags.value),
|
|
||||||
tools: toIDArray(selectedTools.value),
|
|
||||||
requireAllCategories: state.value.requireAllCategories,
|
|
||||||
requireAllTags: state.value.requireAllTags,
|
|
||||||
requireAllTools: state.value.requireAllTools,
|
|
||||||
requireAllFoods: state.value.requireAllFoods,
|
|
||||||
orderBy: state.value.orderBy,
|
|
||||||
orderDirection: state.value.orderDirection,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const passedQuery = ref<RecipeSearchQuery>(calcPassedQuery());
|
|
||||||
|
|
||||||
// we calculate this separately because otherwise we can't check for query changes
|
|
||||||
const passedQueryWithSeed = computed(() => {
|
|
||||||
return {
|
|
||||||
...passedQuery.value,
|
|
||||||
_searchSeed: Date.now().toString(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryDefaults = {
|
|
||||||
search: "",
|
|
||||||
orderBy: "created_at",
|
|
||||||
orderDirection: "desc" as "asc" | "desc",
|
|
||||||
requireAllCategories: false,
|
|
||||||
requireAllTags: false,
|
|
||||||
requireAllTools: false,
|
|
||||||
requireAllFoods: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
state.value.search = queryDefaults.search;
|
|
||||||
state.value.orderBy = queryDefaults.orderBy;
|
|
||||||
state.value.orderDirection = queryDefaults.orderDirection;
|
|
||||||
sortPreferences.value.orderBy = queryDefaults.orderBy;
|
|
||||||
sortPreferences.value.orderDirection = queryDefaults.orderDirection;
|
|
||||||
state.value.requireAllCategories = queryDefaults.requireAllCategories;
|
|
||||||
state.value.requireAllTags = queryDefaults.requireAllTags;
|
|
||||||
state.value.requireAllTools = queryDefaults.requireAllTools;
|
|
||||||
state.value.requireAllFoods = queryDefaults.requireAllFoods;
|
|
||||||
selectedCategories.value = [];
|
|
||||||
selectedFoods.value = [];
|
|
||||||
selectedHouseholds.value = [];
|
|
||||||
selectedTags.value = [];
|
|
||||||
selectedTools.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleOrderDirection() {
|
|
||||||
state.value.orderDirection = state.value.orderDirection === "asc" ? "desc" : "asc";
|
|
||||||
sortPreferences.value.orderDirection = state.value.orderDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOrderBy(value: string) {
|
|
||||||
state.value.orderBy = value;
|
|
||||||
sortPreferences.value.orderBy = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toIDArray(array: { id: string }[]) {
|
|
||||||
// we sort the array to make sure the query is always the same
|
|
||||||
return array.map(item => item.id).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideKeyboard() {
|
|
||||||
input.value.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
const input: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
async function search() {
|
|
||||||
const oldQueryValueString = JSON.stringify(passedQuery.value);
|
|
||||||
const newQueryValue = calcPassedQuery();
|
|
||||||
const newQueryValueString = JSON.stringify(newQueryValue);
|
|
||||||
if (oldQueryValueString === newQueryValueString) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
passedQuery.value = newQueryValue;
|
|
||||||
const query = {
|
|
||||||
categories: passedQuery.value.categories,
|
|
||||||
foods: passedQuery.value.foods,
|
|
||||||
tags: passedQuery.value.tags,
|
|
||||||
tools: passedQuery.value.tools,
|
|
||||||
// Only add the query param if it's not the default value
|
|
||||||
...{
|
|
||||||
auto: state.value.auto ? undefined : "false",
|
|
||||||
search: passedQuery.value.search === queryDefaults.search ? undefined : passedQuery.value.search,
|
|
||||||
households: !passedQuery.value.households?.length || passedQuery.value.households?.length === households.store.value.length ? undefined : passedQuery.value.households,
|
|
||||||
requireAllCategories: passedQuery.value.requireAllCategories ? "true" : undefined,
|
|
||||||
requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined,
|
|
||||||
requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined,
|
|
||||||
requireAllFoods: passedQuery.value.requireAllFoods ? "true" : undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await router.push({ query });
|
|
||||||
searchQuerySession.value.recipe = JSON.stringify(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitUntilAndExecute(
|
|
||||||
condition: () => boolean,
|
|
||||||
callback: () => void,
|
|
||||||
opts = { timeout: 2000, interval: 500 },
|
|
||||||
): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const state = {
|
|
||||||
timeout: undefined as number | undefined,
|
|
||||||
interval: undefined as number | undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const check = () => {
|
|
||||||
if (condition()) {
|
|
||||||
clearInterval(state.interval);
|
|
||||||
clearTimeout(state.timeout);
|
|
||||||
callback();
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// For some reason these were returning NodeJS.Timeout
|
|
||||||
state.interval = setInterval(check, opts.interval) as unknown as number;
|
|
||||||
state.timeout = setTimeout(() => {
|
|
||||||
clearInterval(state.interval);
|
|
||||||
reject(new Error("Timeout"));
|
|
||||||
}, opts.timeout) as unknown as number;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortText = computed(() => {
|
|
||||||
const sort = sortable.find(s => s.value === state.value.orderBy);
|
|
||||||
if (!sort) return "";
|
|
||||||
return `${sort.name}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortable = [
|
|
||||||
{
|
|
||||||
icon: $globals.icons.orderAlphabeticalAscending,
|
|
||||||
name: i18n.t("general.sort-alphabetically"),
|
|
||||||
value: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: $globals.icons.newBox,
|
|
||||||
name: i18n.t("general.created"),
|
|
||||||
value: "created_at",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: $globals.icons.chefHat,
|
|
||||||
name: i18n.t("general.last-made"),
|
|
||||||
value: "last_made",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: $globals.icons.star,
|
|
||||||
name: i18n.t("general.rating"),
|
|
||||||
value: "rating",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: $globals.icons.update,
|
|
||||||
name: i18n.t("general.updated"),
|
|
||||||
value: "updated_at",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: $globals.icons.diceMultiple,
|
|
||||||
name: i18n.t("general.random"),
|
|
||||||
value: "random",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.query,
|
|
||||||
() => {
|
|
||||||
if (!Object.keys(route.query).length) {
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function filterItems(item: RecipeCategory | RecipeTag | RecipeTool, urlPrefix: string) {
|
|
||||||
if (urlPrefix === "categories") {
|
|
||||||
const result = categories.store.value.filter(category => (category.id as string).includes(item.id as string));
|
|
||||||
selectedCategories.value = result as NoUndefinedField<RecipeTag>[];
|
|
||||||
}
|
|
||||||
else if (urlPrefix === "tags") {
|
|
||||||
const result = tags.store.value.filter(tag => (tag.id as string).includes(item.id as string));
|
|
||||||
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
|
||||||
}
|
|
||||||
else if (urlPrefix === "tools") {
|
|
||||||
const result = tools.store.value.filter(tool => (tool.id).includes(item.id || ""));
|
|
||||||
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hydrateSearch() {
|
|
||||||
const query = router.currentRoute.value.query;
|
|
||||||
if (query.auto?.length) {
|
|
||||||
state.value.auto = query.auto === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.search?.length) {
|
|
||||||
state.value.search = query.search as string;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.value.search = queryDefaults.search;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.value.orderBy = sortPreferences.value.orderBy;
|
|
||||||
state.value.orderDirection = sortPreferences.value.orderDirection as "asc" | "desc";
|
|
||||||
|
|
||||||
if (query.requireAllCategories?.length) {
|
|
||||||
state.value.requireAllCategories = query.requireAllCategories === "true";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.value.requireAllCategories = queryDefaults.requireAllCategories;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.requireAllTags?.length) {
|
|
||||||
state.value.requireAllTags = query.requireAllTags === "true";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.value.requireAllTags = queryDefaults.requireAllTags;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.requireAllTools?.length) {
|
|
||||||
state.value.requireAllTools = query.requireAllTools === "true";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.value.requireAllTools = queryDefaults.requireAllTools;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.requireAllFoods?.length) {
|
|
||||||
state.value.requireAllFoods = query.requireAllFoods === "true";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.value.requireAllFoods = queryDefaults.requireAllFoods;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
if (query.categories?.length) {
|
|
||||||
promises.push(
|
|
||||||
waitUntilAndExecute(
|
|
||||||
() => categories.store.value.length > 0,
|
|
||||||
() => {
|
|
||||||
const result = categories.store.value.filter(item =>
|
|
||||||
(query.categories as string[]).includes(item.id as string),
|
|
||||||
);
|
|
||||||
|
|
||||||
selectedCategories.value = result as NoUndefinedField<RecipeCategory>[];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedCategories.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.tags?.length) {
|
|
||||||
promises.push(
|
|
||||||
waitUntilAndExecute(
|
|
||||||
() => tags.store.value.length > 0,
|
|
||||||
() => {
|
|
||||||
const result = tags.store.value.filter(item => (query.tags as string[]).includes(item.id as string));
|
|
||||||
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedTags.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.tools?.length) {
|
|
||||||
promises.push(
|
|
||||||
waitUntilAndExecute(
|
|
||||||
() => tools.store.value.length > 0,
|
|
||||||
() => {
|
|
||||||
const result = tools.store.value.filter(item => (query.tools as string[]).includes(item.id));
|
|
||||||
selectedTools.value = result as NoUndefinedField<RecipeTool>[];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedTools.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.foods?.length) {
|
|
||||||
promises.push(
|
|
||||||
waitUntilAndExecute(
|
|
||||||
() => {
|
|
||||||
if (foods.store.value) {
|
|
||||||
return foods.store.value.length > 0;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
const result = foods.store.value?.filter(item => (query.foods as string[]).includes(item.id));
|
|
||||||
selectedFoods.value = result ?? [];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedFoods.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.households?.length) {
|
|
||||||
promises.push(
|
|
||||||
waitUntilAndExecute(
|
|
||||||
() => {
|
|
||||||
if (households.store.value) {
|
|
||||||
return households.store.value.length > 0;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
const result = households.store.value?.filter(item => (query.households as string[]).includes(item.id));
|
|
||||||
selectedHouseholds.value = result as NoUndefinedField<HouseholdSummary>[] ?? [];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedHouseholds.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
// restore the user's last search query
|
|
||||||
if (searchQuerySession.value.recipe && !(Object.keys(route.query).length > 0)) {
|
|
||||||
try {
|
|
||||||
const query = JSON.parse(searchQuerySession.value.recipe);
|
|
||||||
await router.replace({ query });
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
searchQuerySession.value.recipe = "";
|
|
||||||
router.replace({ query: {} });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await hydrateSearch();
|
|
||||||
await search();
|
|
||||||
state.value.ready = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
watchDebounced(
|
|
||||||
[
|
|
||||||
() => state.value.search,
|
|
||||||
() => state.value.requireAllCategories,
|
|
||||||
() => state.value.requireAllTags,
|
|
||||||
() => state.value.requireAllTools,
|
|
||||||
() => state.value.requireAllFoods,
|
|
||||||
() => state.value.orderBy,
|
|
||||||
() => state.value.orderDirection,
|
|
||||||
selectedCategories,
|
|
||||||
selectedFoods,
|
|
||||||
selectedHouseholds,
|
|
||||||
selectedTags,
|
|
||||||
selectedTools,
|
|
||||||
],
|
|
||||||
async () => {
|
|
||||||
if (state.value.ready && state.value.auto) {
|
|
||||||
await search();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
debounce: 500,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sortText,
|
|
||||||
search,
|
|
||||||
reset,
|
|
||||||
state,
|
|
||||||
categories: categories.store as unknown as NoUndefinedField<RecipeCategory>[],
|
|
||||||
tags: tags.store as unknown as NoUndefinedField<RecipeTag>[],
|
|
||||||
foods: foods.store,
|
|
||||||
tools: tools.store as unknown as NoUndefinedField<RecipeTool>[],
|
|
||||||
households: households.store as unknown as NoUndefinedField<HouseholdSummary>[],
|
|
||||||
|
|
||||||
sortable,
|
|
||||||
toggleOrderDirection,
|
|
||||||
setOrderBy,
|
|
||||||
hideKeyboard,
|
|
||||||
input,
|
|
||||||
|
|
||||||
selectedCategories,
|
|
||||||
selectedFoods,
|
|
||||||
selectedHouseholds,
|
|
||||||
selectedTags,
|
|
||||||
selectedTools,
|
|
||||||
appendRecipes,
|
|
||||||
assignSorted,
|
|
||||||
recipes,
|
|
||||||
removeRecipe,
|
|
||||||
replaceRecipes,
|
|
||||||
passedQueryWithSeed,
|
|
||||||
|
|
||||||
filterItems,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="css">
|
|
||||||
.search-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.65rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
width: 950px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-button-container {
|
|
||||||
margin: 3rem auto 0 auto;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<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 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";
|
||||||
|
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
components: { RecipeCardSection, RecipeExplorerPageSearch },
|
||||||
|
setup() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready,
|
||||||
|
searchComponent,
|
||||||
|
searchQuery,
|
||||||
|
recipes,
|
||||||
|
appendRecipes,
|
||||||
|
replaceRecipes,
|
||||||
|
onSearchReady,
|
||||||
|
onItemSelected,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search-container pb-8">
|
||||||
|
<form
|
||||||
|
class="search-box pa-2"
|
||||||
|
@submit.prevent="search"
|
||||||
|
>
|
||||||
|
<div class="d-flex justify-center mb-2">
|
||||||
|
<v-text-field
|
||||||
|
ref="input"
|
||||||
|
v-model="state.search"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
|
color="primary"
|
||||||
|
:placeholder="$t('search.search-placeholder')"
|
||||||
|
:prepend-inner-icon="$globals.icons.search"
|
||||||
|
@keyup.enter="hideKeyboard"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="search-row">
|
||||||
|
<RecipeExplorerPageSearchFilters />
|
||||||
|
<!-- Sort Options -->
|
||||||
|
<v-menu
|
||||||
|
offset-y
|
||||||
|
nudge-bottom="3"
|
||||||
|
>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
class="ml-auto"
|
||||||
|
size="small"
|
||||||
|
color="accent"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<v-icon :start="!$vuetify.display.xs">
|
||||||
|
{{ state.orderDirection === "asc" ? $globals.icons.sortAscending : $globals.icons.sortDescending }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $vuetify.display.xs ? null : sortText }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
slim
|
||||||
|
density="comfortable"
|
||||||
|
:prepend-icon="state.orderDirection === 'asc' ? $globals.icons.sortDescending : $globals.icons.sortAscending"
|
||||||
|
:title="state.orderDirection === 'asc' ? $t('general.sort-descending') : $t('general.sort-ascending')"
|
||||||
|
@click="toggleOrderDirection"
|
||||||
|
/>
|
||||||
|
<v-divider />
|
||||||
|
<v-list-item
|
||||||
|
v-for="v in sortable"
|
||||||
|
:key="v.name"
|
||||||
|
:active="state.orderBy === v.value"
|
||||||
|
slim
|
||||||
|
density="comfortable"
|
||||||
|
:prepend-icon="v.icon"
|
||||||
|
:title="v.name"
|
||||||
|
@click="setOrderBy(v.value)"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<v-menu
|
||||||
|
offset-y
|
||||||
|
bottom
|
||||||
|
start
|
||||||
|
nudge-bottom="3"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
color="accent"
|
||||||
|
dark
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<v-icon size="small">
|
||||||
|
{{ $globals.icons.cog }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<v-switch
|
||||||
|
v-model="state.auto"
|
||||||
|
:label="$t('search.auto-search')"
|
||||||
|
single-line
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
@click="reset"
|
||||||
|
>
|
||||||
|
{{ $t("general.reset") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!state.auto"
|
||||||
|
class="search-button-container"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
size="x-large"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<v-icon start>
|
||||||
|
{{ $globals.icons.search }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("search.search") }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import RecipeExplorerPageSearchFilters from "./RecipeExplorerPageSearchFilters.vue";
|
||||||
|
import { useRecipeExplorerSearch, clearRecipeExplorerSearchState } from "~/composables/use-recipe-explorer-search";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
ready: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const route = useRoute();
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
passedQueryWithSeed,
|
||||||
|
search,
|
||||||
|
reset,
|
||||||
|
toggleOrderDirection,
|
||||||
|
setOrderBy,
|
||||||
|
filterItems,
|
||||||
|
initialize,
|
||||||
|
} = useRecipeExplorerSearch(groupSlug);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
passedQueryWithSeed,
|
||||||
|
filterItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initialize();
|
||||||
|
emit("ready");
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Clear the cache when component unmounts to ensure fresh state on remount
|
||||||
|
clearRecipeExplorerSearchState(groupSlug.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortText = computed(() => {
|
||||||
|
const sort = sortable.value.find(s => s.value === state.value.orderBy);
|
||||||
|
if (!sort) return "";
|
||||||
|
return `${sort.name}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortable = computed(() => [
|
||||||
|
{
|
||||||
|
icon: $globals.icons.orderAlphabeticalAscending,
|
||||||
|
name: i18n.t("general.sort-alphabetically"),
|
||||||
|
value: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.newBox,
|
||||||
|
name: i18n.t("general.created"),
|
||||||
|
value: "created_at",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.chefHat,
|
||||||
|
name: i18n.t("general.last-made"),
|
||||||
|
value: "last_made",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.star,
|
||||||
|
name: i18n.t("general.rating"),
|
||||||
|
value: "rating",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.update,
|
||||||
|
name: i18n.t("general.updated"),
|
||||||
|
value: "updated_at",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.diceMultiple,
|
||||||
|
name: i18n.t("general.random"),
|
||||||
|
value: "random",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const input: Ref<any> = ref(null);
|
||||||
|
|
||||||
|
function hideKeyboard() {
|
||||||
|
input.value?.blur();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.65rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 950px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button-container {
|
||||||
|
margin: 3rem auto 0 auto;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Category Filter -->
|
||||||
|
<SearchFilter
|
||||||
|
v-if="categories"
|
||||||
|
v-model="selectedCategories"
|
||||||
|
v-model:require-all="state.requireAllCategories"
|
||||||
|
:items="categories"
|
||||||
|
>
|
||||||
|
<v-icon start>
|
||||||
|
{{ $globals.icons.categories }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("category.categories") }}
|
||||||
|
</SearchFilter>
|
||||||
|
|
||||||
|
<!-- Tag Filter -->
|
||||||
|
<SearchFilter
|
||||||
|
v-if="tags"
|
||||||
|
v-model="selectedTags"
|
||||||
|
v-model:require-all="state.requireAllTags"
|
||||||
|
:items="tags"
|
||||||
|
>
|
||||||
|
<v-icon start>
|
||||||
|
{{ $globals.icons.tags }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("tag.tags") }}
|
||||||
|
</SearchFilter>
|
||||||
|
|
||||||
|
<!-- Tool Filter -->
|
||||||
|
<SearchFilter
|
||||||
|
v-if="tools"
|
||||||
|
v-model="selectedTools"
|
||||||
|
v-model:require-all="state.requireAllTools"
|
||||||
|
:items="tools"
|
||||||
|
>
|
||||||
|
<v-icon start>
|
||||||
|
{{ $globals.icons.potSteam }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("tool.tools") }}
|
||||||
|
</SearchFilter>
|
||||||
|
|
||||||
|
<!-- Food Filter -->
|
||||||
|
<SearchFilter
|
||||||
|
v-if="foods"
|
||||||
|
v-model="selectedFoods"
|
||||||
|
v-model:require-all="state.requireAllFoods"
|
||||||
|
:items="foods"
|
||||||
|
>
|
||||||
|
<v-icon start>
|
||||||
|
{{ $globals.icons.foods }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("general.foods") }}
|
||||||
|
</SearchFilter>
|
||||||
|
|
||||||
|
<!-- Household Filter -->
|
||||||
|
<SearchFilter
|
||||||
|
v-if="households.length > 1"
|
||||||
|
v-model="selectedHouseholds"
|
||||||
|
:items="households"
|
||||||
|
radio
|
||||||
|
>
|
||||||
|
<v-icon start>
|
||||||
|
{{ $globals.icons.household }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("household.households") }}
|
||||||
|
</SearchFilter>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
|
import { useRecipeExplorerSearch } from "~/composables/use-recipe-explorer-search";
|
||||||
|
import {
|
||||||
|
useCategoryStore,
|
||||||
|
usePublicCategoryStore,
|
||||||
|
useFoodStore,
|
||||||
|
usePublicFoodStore,
|
||||||
|
useHouseholdStore,
|
||||||
|
usePublicHouseholdStore,
|
||||||
|
useTagStore,
|
||||||
|
usePublicTagStore,
|
||||||
|
useToolStore,
|
||||||
|
usePublicToolStore,
|
||||||
|
} from "~/composables/store";
|
||||||
|
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
selectedCategories,
|
||||||
|
selectedFoods,
|
||||||
|
selectedHouseholds,
|
||||||
|
selectedTags,
|
||||||
|
selectedTools,
|
||||||
|
} = useRecipeExplorerSearch(groupSlug);
|
||||||
|
|
||||||
|
const { store: categories } = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
|
||||||
|
const { store: tags } = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
|
||||||
|
const { store: tools } = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
|
||||||
|
const { store: foods } = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
||||||
|
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
|
||||||
|
</script>
|
||||||
@@ -43,8 +43,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
buttonStyle: false,
|
buttonStyle: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const api = useUserApi();
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
||||||
|
|
||||||
const isFavorite = computed(() => {
|
const isFavorite = computed(() => {
|
||||||
@@ -53,6 +51,9 @@ const isFavorite = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function toggleFavorite() {
|
async function toggleFavorite() {
|
||||||
|
const api = useUserApi();
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
|
||||||
if (!$auth.user.value) return;
|
if (!$auth.user.value) return;
|
||||||
if (!isFavorite.value) {
|
if (!isFavorite.value) {
|
||||||
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
||||||
|
|||||||
@@ -119,6 +119,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { whenever } from "@vueuse/core";
|
import { whenever } from "@vueuse/core";
|
||||||
|
import { formatISO } from "date-fns";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { useHouseholdSelf } from "~/composables/use-households";
|
import { useHouseholdSelf } from "~/composables/use-households";
|
||||||
@@ -148,7 +149,7 @@ const newTimelineEventImageName = ref<string>("");
|
|||||||
const newTimelineEventImagePreviewUrl = ref<string>();
|
const newTimelineEventImagePreviewUrl = ref<string>();
|
||||||
const newTimelineEventTimestamp = ref<Date>(new Date());
|
const newTimelineEventTimestamp = ref<Date>(new Date());
|
||||||
const newTimelineEventTimestampString = computed(() => {
|
const newTimelineEventTimestampString = computed(() => {
|
||||||
return newTimelineEventTimestamp.value.toISOString().substring(0, 10);
|
return formatISO(newTimelineEventTimestamp.value, { representation: "date" });
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastMade = ref(props.recipe.lastMade);
|
const lastMade = ref(props.recipe.lastMade);
|
||||||
@@ -169,7 +170,7 @@ whenever(
|
|||||||
() => madeThisDialog.value,
|
() => madeThisDialog.value,
|
||||||
() => {
|
() => {
|
||||||
// Set timestamp to now
|
// Set timestamp to now
|
||||||
newTimelineEventTimestamp.value = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
|
newTimelineEventTimestamp.value = new Date();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown.value }">
|
<v-container v-show="!isCookMode" key="recipe-page" class="px-0" :class="{ 'pa-0': $vuetify.display.smAndDown }">
|
||||||
<v-card :flat="$vuetify.display.smAndDown.value" class="d-print-none">
|
<v-card :flat="$vuetify.display.smAndDown" class="d-print-none">
|
||||||
<RecipePageHeader
|
<RecipePageHeader
|
||||||
:recipe="recipe"
|
:recipe="recipe"
|
||||||
:recipe-scale="scale"
|
:recipe-scale="scale"
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
<v-divider />
|
<v-divider />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="overflow-y-auto"
|
<v-col class="overflow-y-auto"
|
||||||
:class="$vuetify.display.smAndDown.value ? 'py-2': 'py-6'"
|
:class="$vuetify.display.smAndDown ? 'py-2': 'py-6'"
|
||||||
style="height: 100%" cols="12" sm="7">
|
style="height: 100%" cols="12" sm="7">
|
||||||
<h2 class="text-h5 px-4 font-weight-medium opacity-80">
|
<h2 class="text-h5 px-4 font-weight-medium opacity-80">
|
||||||
{{ $t('recipe.instructions') }}
|
{{ $t('recipe.instructions') }}
|
||||||
@@ -188,7 +188,7 @@ import { useNavigationWarning } from "~/composables/use-navigation-warning";
|
|||||||
|
|
||||||
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
|
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
|
||||||
|
|
||||||
const { $vuetify } = useNuxtApp();
|
const display = useDisplay();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -278,7 +278,7 @@ async function deleteRecipe() {
|
|||||||
*/
|
*/
|
||||||
const landscape = computed(() => {
|
const landscape = computed(() => {
|
||||||
const preferLandscape = recipe.value.settings?.landscapeView;
|
const preferLandscape = recipe.value.settings?.landscapeView;
|
||||||
const smallScreen = !$vuetify.display.smAndUp.value;
|
const smallScreen = !display.smAndUp.value;
|
||||||
|
|
||||||
if (preferLandscape) {
|
if (preferLandscape) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { $vuetify } = useNuxtApp();
|
const display = useDisplay();
|
||||||
const { recipeImage } = useStaticRoutes();
|
const { recipeImage } = useStaticRoutes();
|
||||||
const { imageKey } = usePageState(props.recipe.slug);
|
const { imageKey } = usePageState(props.recipe.slug);
|
||||||
const { user } = usePageUser();
|
const { user } = usePageUser();
|
||||||
@@ -42,7 +42,7 @@ if (user) {
|
|||||||
|
|
||||||
const hideImage = ref(false);
|
const hideImage = ref(false);
|
||||||
const imageHeight = computed(() => {
|
const imageHeight = computed(() => {
|
||||||
return $vuetify.display.xs.value ? "200" : "400";
|
return display.xs.value ? "200" : "400";
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeImageUrl = computed(() => {
|
const recipeImageUrl = computed(() => {
|
||||||
|
|||||||
@@ -29,32 +29,49 @@
|
|||||||
{{ activeText }}
|
{{ activeText }}
|
||||||
</p>
|
</p>
|
||||||
<v-divider class="mb-4" />
|
<v-divider class="mb-4" />
|
||||||
<v-checkbox-btn
|
<template v-if="Object.keys(groupedUnusedIngredients).length > 0">
|
||||||
v-for="ing in unusedIngredients"
|
|
||||||
:key="ing.referenceId"
|
|
||||||
v-model="activeRefs"
|
|
||||||
:value="ing.referenceId"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
|
||||||
</template>
|
|
||||||
</v-checkbox-btn>
|
|
||||||
|
|
||||||
<template v-if="usedIngredients.length > 0">
|
|
||||||
<h4 class="py-3 ml-1">
|
<h4 class="py-3 ml-1">
|
||||||
{{ $t("recipe.linked-to-other-step") }}
|
{{ $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>
|
</h4>
|
||||||
<v-checkbox-btn
|
<v-checkbox-btn
|
||||||
v-for="ing in usedIngredients"
|
v-for="ing in ingredients"
|
||||||
:key="ing.referenceId"
|
:key="ing.referenceId"
|
||||||
v-model="activeRefs"
|
v-model="activeRefs"
|
||||||
:value="ing.referenceId"
|
:value="ing.referenceId"
|
||||||
|
class="ml-4"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox-btn>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<v-checkbox-btn
|
||||||
|
v-for="ing in ingredients"
|
||||||
|
:key="ing.referenceId"
|
||||||
|
v-model="activeRefs"
|
||||||
|
:value="ing.referenceId"
|
||||||
|
class="ml-4"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
||||||
|
</template>
|
||||||
|
</v-checkbox-btn>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
@@ -563,6 +580,71 @@ const ingredientLookup = computed(() => {
|
|||||||
}, results);
|
}, results);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Map each ingredient's referenceId to its section title
|
||||||
|
const ingredientSectionTitles = computed(() => {
|
||||||
|
const titleMap: { [key: string]: string } = {};
|
||||||
|
let currentTitle = "";
|
||||||
|
|
||||||
|
// Go through all ingredients in order
|
||||||
|
props.recipe.recipeIngredient.forEach((ingredient) => {
|
||||||
|
if (ingredient.referenceId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this ingredient has a title, update the current title
|
||||||
|
if (ingredient.title) {
|
||||||
|
currentTitle = ingredient.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign the current title to this ingredient
|
||||||
|
titleMap[ingredient.referenceId] = currentTitle;
|
||||||
|
});
|
||||||
|
|
||||||
|
return titleMap;
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedUnusedIngredients = computed(() => {
|
||||||
|
const groups: { [key: string]: RecipeIngredient[] } = {};
|
||||||
|
|
||||||
|
// Group ingredients by section title
|
||||||
|
unusedIngredients.value.forEach((ingredient) => {
|
||||||
|
if (ingredient.referenceId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the section title from the mapping, or fallback to the ingredient's own title
|
||||||
|
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
|
||||||
|
|
||||||
|
if (!groups[title]) {
|
||||||
|
groups[title] = [];
|
||||||
|
}
|
||||||
|
groups[title].push(ingredient);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedUsedIngredients = computed(() => {
|
||||||
|
const groups: { [key: string]: RecipeIngredient[] } = {};
|
||||||
|
|
||||||
|
// Group ingredients by section title
|
||||||
|
usedIngredients.value.forEach((ingredient) => {
|
||||||
|
if (ingredient.referenceId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the section title from the mapping, or fallback to the ingredient's own title
|
||||||
|
const title = ingredientSectionTitles.value[ingredient.referenceId] || ingredient.title || "";
|
||||||
|
|
||||||
|
if (!groups[title]) {
|
||||||
|
groups[title] = [];
|
||||||
|
}
|
||||||
|
groups[title].push(ingredient);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
});
|
||||||
|
|
||||||
function getIngredientByRefId(refId: string | undefined) {
|
function getIngredientByRefId(refId: string | undefined) {
|
||||||
if (refId === undefined) {
|
if (refId === undefined) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
:recipe="recipes.get(event.recipeId)"
|
:recipe="recipes.get(event.recipeId)"
|
||||||
:show-recipe-cards="showRecipeCards"
|
:show-recipe-cards="showRecipeCards"
|
||||||
:width="$vuetify.display.smAndDown ? '100%' : undefined"
|
:width="$vuetify.display.smAndDown ? '100%' : undefined"
|
||||||
@update="updateTimelineEvent(index)"
|
@update="updateTimelineEvent(index, $event)"
|
||||||
@delete="deleteTimelineEvent(index)"
|
@delete="deleteTimelineEvent(index)"
|
||||||
/>
|
/>
|
||||||
</v-timeline>
|
</v-timeline>
|
||||||
@@ -186,20 +186,17 @@ function toggleEventTypeOption(option: TimelineEventType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timeline Actions
|
// Timeline Actions
|
||||||
async function updateTimelineEvent(index: number) {
|
async function updateTimelineEvent(index: number, event: RecipeTimelineEventUpdate) {
|
||||||
const event = timelineEvents.value[index];
|
const eventId = timelineEvents.value[index].id;
|
||||||
const payload: RecipeTimelineEventUpdate = {
|
const { response } = await api.recipes.updateTimelineEvent(eventId, event);
|
||||||
subject: event.subject,
|
|
||||||
eventMessage: event.eventMessage,
|
|
||||||
image: event.image,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { response } = await api.recipes.updateTimelineEvent(event.id, payload);
|
|
||||||
if (response?.status !== 200) {
|
if (response?.status !== 200) {
|
||||||
alert.error(i18n.t("events.something-went-wrong") as string);
|
alert.error(i18n.t("events.something-went-wrong") as string);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the local event data to reflect the changes in the UI
|
||||||
|
timelineEvents.value[index] = response.data;
|
||||||
|
|
||||||
alert.success(i18n.t("events.event-updated") as string);
|
alert.success(i18n.t("events.event-updated") as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
edit: true,
|
edit: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
}"
|
}"
|
||||||
@update="$emit('update')"
|
@update="$emit('update', $event)"
|
||||||
@delete="$emit('delete')"
|
@delete="$emit('delete')"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -96,7 +96,7 @@ import RecipeCardMobile from "./RecipeCardMobile.vue";
|
|||||||
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
|
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
|
||||||
import { useStaticRoutes } from "~/composables/api";
|
import { useStaticRoutes } from "~/composables/api";
|
||||||
import { useTimelineEventTypes } from "~/composables/recipes/use-recipe-timeline-events";
|
import { useTimelineEventTypes } from "~/composables/recipes/use-recipe-timeline-events";
|
||||||
import type { Recipe, RecipeTimelineEventOut } from "~/lib/api/types/recipe";
|
import type { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate } from "~/lib/api/types/recipe";
|
||||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||||
import SafeMarkdown from "~/components/global/SafeMarkdown.vue";
|
import SafeMarkdown from "~/components/global/SafeMarkdown.vue";
|
||||||
|
|
||||||
@@ -113,11 +113,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
selected: [];
|
selected: [];
|
||||||
update: [];
|
update: [event: RecipeTimelineEventUpdate];
|
||||||
delete: [];
|
delete: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { $vuetify, $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
|
const display = useDisplay();
|
||||||
const { recipeTimelineEventImage } = useStaticRoutes();
|
const { recipeTimelineEventImage } = useStaticRoutes();
|
||||||
const { eventTypeOptions } = useTimelineEventTypes();
|
const { eventTypeOptions } = useTimelineEventTypes();
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ const route = useRoute();
|
|||||||
const groupSlug = computed(() => (route.params.groupSlug as string) || currentUser?.value?.groupSlug || "");
|
const groupSlug = computed(() => (route.params.groupSlug as string) || currentUser?.value?.groupSlug || "");
|
||||||
|
|
||||||
const useMobileFormat = computed(() => {
|
const useMobileFormat = computed(() => {
|
||||||
return $vuetify.display.smAndDown.value;
|
return display.smAndDown.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const attrs = computed(() => {
|
const attrs = computed(() => {
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-badge
|
<v-badge
|
||||||
:model-value="selected.length > 0"
|
v-memo="[selectedCount]"
|
||||||
|
:model-value="selectedCount > 0"
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
:content="selected.length"
|
:content="selectedCount"
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
size="small"
|
size="small"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="state.search"
|
v-model="state.search"
|
||||||
|
v-memo="[state.search]"
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
hide-details
|
hide-details
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
hide-details
|
hide-details
|
||||||
class="my-auto"
|
class="my-auto"
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="`${requireAll ? $t('search.has-all') : $t('search.has-any')}`"
|
:label="requireAllValue ? $t('search.has-all') : $t('search.has-any')"
|
||||||
/>
|
/>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -73,7 +75,8 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
:key="item.id"
|
:key="`radio-${item.id}`"
|
||||||
|
v-memo="[item.id, item.name, selectedRadio?.id]"
|
||||||
:value="item"
|
:value="item"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
>
|
>
|
||||||
@@ -101,7 +104,8 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
:key="item.id"
|
:key="`checkbox-${item.id}`"
|
||||||
|
v-memo="[item.id, item.name, selectedIds.has(item.id)]"
|
||||||
:value="item"
|
:value="item"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
>
|
>
|
||||||
@@ -134,6 +138,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { watchDebounced } from "@vueuse/core";
|
||||||
|
|
||||||
export interface SelectableItem {
|
export interface SelectableItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -165,6 +171,9 @@ export default defineNuxtComponent({
|
|||||||
menu: false,
|
menu: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use shallowRef for better performance with arrays
|
||||||
|
const debouncedSearch = shallowRef("");
|
||||||
|
|
||||||
const requireAllValue = computed({
|
const requireAllValue = computed({
|
||||||
get: () => props.requireAll,
|
get: () => props.requireAll,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
@@ -172,6 +181,7 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use shallowRef to prevent deep reactivity on large arrays
|
||||||
const selected = computed({
|
const selected = computed({
|
||||||
get: () => props.modelValue as SelectableItem[],
|
get: () => props.modelValue as SelectableItem[],
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
@@ -186,21 +196,40 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
() => state.search,
|
||||||
|
(newSearch) => {
|
||||||
|
debouncedSearch.value = newSearch;
|
||||||
|
},
|
||||||
|
{ debounce: 500, maxWait: 1500, immediate: false }, // Increased debounce time
|
||||||
|
);
|
||||||
|
|
||||||
const filtered = computed(() => {
|
const filtered = computed(() => {
|
||||||
if (!state.search) {
|
const items = props.items;
|
||||||
return props.items;
|
const search = debouncedSearch.value;
|
||||||
|
|
||||||
|
if (!search || search.length < 2) { // Only filter after 2 characters
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.items.filter(item => item.name.toLowerCase().includes(state.search.toLowerCase()));
|
const searchLower = search.toLowerCase();
|
||||||
|
return items.filter(item => item.name.toLowerCase().includes(searchLower));
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = computed(() => selected.value.length);
|
||||||
|
const selectedIds = computed(() => {
|
||||||
|
return new Set(selected.value.map(item => item.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCheckboxClick = (item: SelectableItem) => {
|
const handleCheckboxClick = (item: SelectableItem) => {
|
||||||
console.log(selected.value, item);
|
const currentSelection = selected.value;
|
||||||
if (selected.value.includes(item)) {
|
const isSelected = selectedIds.value.has(item.id);
|
||||||
selected.value = selected.value.filter(i => i !== item);
|
|
||||||
|
if (isSelected) {
|
||||||
|
selected.value = currentSelection.filter(i => i.id !== item.id);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
selected.value.push(item);
|
selected.value = [...currentSelection, item];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -221,6 +250,8 @@ export default defineNuxtComponent({
|
|||||||
state,
|
state,
|
||||||
selected,
|
selected,
|
||||||
selectedRadio,
|
selectedRadio,
|
||||||
|
selectedCount,
|
||||||
|
selectedIds,
|
||||||
filtered,
|
filtered,
|
||||||
handleCheckboxClick,
|
handleCheckboxClick,
|
||||||
handleRadioClick,
|
handleRadioClick,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-flex justify-center pb-6 mt-n1">
|
<div class="d-flex pb-6 mt-n1 ml-10">
|
||||||
<div style="flex-basis: 500px">
|
<div style="flex-basis: 500px">
|
||||||
<strong> {{ $t("user.password-strength", { strength: pwStrength.strength.value }) }}</strong>
|
<strong> {{ $t("user.password-strength", { strength: pwStrength.strength.value }) }}</strong>
|
||||||
<v-progress-linear
|
<v-progress-linear
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-card-title>
|
<v-card-title class="pt-0">
|
||||||
<v-icon
|
<v-icon
|
||||||
size="large"
|
size="large"
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<span class="headline"> {{ $t("user-registration.account-details") }}</span>
|
<span class="headline"> {{ $t("user-registration.account-details") }}</span>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-card-text>
|
<v-card-text class="mt-2">
|
||||||
<v-form
|
<v-form
|
||||||
ref="domAccountForm"
|
ref="domAccountForm"
|
||||||
@submit.prevent
|
@submit.prevent
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app dark>
|
<v-app dark>
|
||||||
<NuxtPwaManifest />
|
|
||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
|
|
||||||
<AppHeader>
|
<AppHeader>
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
absolute
|
absolute
|
||||||
:top-link="topLinks"
|
:top-link="topLinks"
|
||||||
:secondary-links="cookbookLinks || []"
|
:secondary-links="cookbookLinks || []"
|
||||||
:bottom-links="bottomLinks"
|
|
||||||
>
|
>
|
||||||
<v-menu
|
<v-menu
|
||||||
offset-y
|
offset-y
|
||||||
@@ -85,25 +83,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<template #bottom>
|
|
||||||
<v-list-item @click.stop="languageDialog = true">
|
|
||||||
<template #prepend>
|
|
||||||
<v-icon>{{ $globals.icons.translate }}</v-icon>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>{{ $t("sidebar.language") }}</v-list-item-title>
|
|
||||||
<LanguageDialog v-model="languageDialog" />
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item @click="toggleDark">
|
|
||||||
<template #prepend>
|
|
||||||
<v-icon>
|
|
||||||
{{ $vuetify.theme.current.dark ? $globals.icons.weatherSunny : $globals.icons.weatherNight }}
|
|
||||||
</v-icon>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>
|
|
||||||
{{ $vuetify.theme.current.dark ? $t("settings.theme.light-mode") : $t("settings.theme.dark-mode") }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
<v-main class="pt-12">
|
<v-main class="pt-12">
|
||||||
<v-scroll-x-transition>
|
<v-scroll-x-transition>
|
||||||
@@ -122,18 +101,17 @@ import { useAppInfo } from "~/composables/api";
|
|||||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||||
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
|
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
|
||||||
import { useToggleDarkMode } from "~/composables/use-utils";
|
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { $globals, $vuetify } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
|
const display = useDisplay();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const isAdmin = computed(() => $auth.user.value?.admin);
|
|
||||||
const route = useRoute();
|
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 || "");
|
||||||
|
|
||||||
@@ -191,13 +169,11 @@ export default defineNuxtComponent({
|
|||||||
const appInfo = useAppInfo();
|
const appInfo = useAppInfo();
|
||||||
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
|
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
|
||||||
|
|
||||||
const toggleDark = useToggleDarkMode();
|
|
||||||
|
|
||||||
const languageDialog = ref<boolean>(false);
|
const languageDialog = ref<boolean>(false);
|
||||||
|
|
||||||
const sidebar = ref<boolean>(false);
|
const sidebar = ref<boolean>(false);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
sidebar.value = $vuetify.display.mdAndUp.value;
|
sidebar.value = display.lgAndUp.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
function cookbookAsLink(cookbook: ReadCookBook): SideBarLink {
|
function cookbookAsLink(cookbook: ReadCookBook): SideBarLink {
|
||||||
@@ -286,19 +262,6 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const bottomLinks = computed<SideBarLink[]>(() =>
|
|
||||||
isAdmin.value
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
icon: $globals.icons.cog,
|
|
||||||
title: i18n.t("general.settings"),
|
|
||||||
to: "/admin/site-settings",
|
|
||||||
restricted: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
|
|
||||||
const topLinks = computed<SideBarLink[]>(() => [
|
const topLinks = computed<SideBarLink[]>(() => [
|
||||||
{
|
{
|
||||||
icon: $globals.icons.silverwareForkKnife,
|
icon: $globals.icons.silverwareForkKnife,
|
||||||
@@ -367,11 +330,9 @@ export default defineNuxtComponent({
|
|||||||
groupSlug,
|
groupSlug,
|
||||||
cookbookLinks,
|
cookbookLinks,
|
||||||
createLinks,
|
createLinks,
|
||||||
bottomLinks,
|
|
||||||
topLinks,
|
topLinks,
|
||||||
isOwnGroup,
|
isOwnGroup,
|
||||||
languageDialog,
|
languageDialog,
|
||||||
toggleDark,
|
|
||||||
sidebar,
|
sidebar,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-navigation-drawer v-model="showDrawer" class="d-flex flex-column d-print-none position-fixed">
|
<v-navigation-drawer v-model="showDrawer" class="d-flex flex-column d-print-none position-fixed" touchless>
|
||||||
|
<LanguageDialog v-model="languageDialog" />
|
||||||
<!-- User Profile -->
|
<!-- User Profile -->
|
||||||
<template v-if="loggedIn">
|
<template v-if="loggedIn">
|
||||||
<v-list-item lines="two" :to="userProfileLink" exact>
|
<v-list-item lines="two" :to="userProfileLink" exact>
|
||||||
@@ -82,30 +83,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Bottom Navigation Links -->
|
<!-- Bottom Navigation Links -->
|
||||||
<template v-if="bottomLinks" #append>
|
<template #append>
|
||||||
<v-list v-model:selected="bottomSelected" nav density="compact">
|
<v-list v-model:selected="bottomSelected" nav density="comfortable">
|
||||||
<template v-for="nav in bottomLinks">
|
<v-menu location="end bottom" :offset="15">
|
||||||
<div v-if="!nav.restricted || isOwnGroup" :key="nav.key || nav.title">
|
<template #activator="{ props }">
|
||||||
<v-list-item :key="nav.key || nav.title" exact link :to="nav.to" :href="nav.href"
|
<v-list-item v-bind="props" :prepend-icon="$globals.icons.cog" :title="$t('general.settings')" />
|
||||||
:target="nav.href ? '_blank' : null">
|
</template>
|
||||||
<template #prepend>
|
<v-list density="comfortable" color="primary">
|
||||||
<v-icon>{{ nav.icon }}</v-icon>
|
<v-list-item :prepend-icon="$globals.icons.translate" :title="$t('sidebar.language')" @click="languageDialog=true" />
|
||||||
</template>
|
<v-list-item :prepend-icon="$vuetify.theme.current.dark ? $globals.icons.weatherSunny : $globals.icons.weatherNight" :title="$vuetify.theme.current.dark ? $t('settings.theme.light-mode') : $t('settings.theme.dark-mode')" @click="toggleDark" />
|
||||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
<v-divider v-if="loggedIn" class="my-2" />
|
||||||
</v-list-item>
|
<v-list-item v-if="loggedIn" :prepend-icon="$globals.icons.cog" :title="$t('profile.user-settings')" to="/user/profile" />
|
||||||
</div>
|
<v-list-item v-if="canManage" :prepend-icon="$globals.icons.manageData" :title="$t('data-pages.data-management')" to="/group/data" />
|
||||||
</template>
|
<v-divider v-if="isAdmin" class="my-2" />
|
||||||
<slot name="bottom" />
|
<v-list-item v-if="isAdmin" :prepend-icon="$globals.icons.wrench" :title="$t('settings.admin-settings')" to="/admin/site-settings" />
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
</v-list>
|
</v-list>
|
||||||
</template>
|
</template>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useWindowSize } from "@vueuse/core";
|
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import type { SidebarLinks } from "~/types/application-types";
|
import type { SidebarLinks } from "~/types/application-types";
|
||||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||||
|
import { useToggleDarkMode } from "~/composables/use-utils";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -130,48 +133,34 @@ export default defineNuxtComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
bottomLinks: {
|
|
||||||
type: Array as () => SidebarLinks,
|
|
||||||
required: false,
|
|
||||||
default: () => ([]),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { loggedIn, isOwnGroup } = useLoggedInState();
|
const { loggedIn, isOwnGroup } = useLoggedInState();
|
||||||
|
const isAdmin = computed(() => $auth.user.value?.admin);
|
||||||
|
const canManage = computed(() => $auth.user.value?.canManage);
|
||||||
|
|
||||||
const userFavoritesLink = computed(() => $auth.user.value ? `/user/${$auth.user.value.id}/favorites` : undefined);
|
const userFavoritesLink = computed(() => $auth.user.value ? `/user/${$auth.user.value.id}/favorites` : undefined);
|
||||||
const userProfileLink = computed(() => $auth.user.value ? "/user/profile" : undefined);
|
const userProfileLink = computed(() => $auth.user.value ? "/user/profile" : undefined);
|
||||||
|
|
||||||
|
const toggleDark = useToggleDarkMode();
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dropDowns: {} as Record<string, boolean>,
|
dropDowns: {} as Record<string, boolean>,
|
||||||
topSelected: null as string[] | null,
|
topSelected: null as string[] | null,
|
||||||
secondarySelected: null as string[] | null,
|
secondarySelected: null as string[] | null,
|
||||||
bottomSelected: null as string[] | null,
|
bottomSelected: null as string[] | null,
|
||||||
hasOpenedBefore: false as boolean,
|
hasOpenedBefore: false as boolean,
|
||||||
|
languageDialog: false as boolean,
|
||||||
});
|
});
|
||||||
// model to control the drawer
|
// model to control the drawer
|
||||||
const showDrawer = computed({
|
const showDrawer = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: value => context.emit("update:modelValue", value),
|
set: value => context.emit("update:modelValue", value),
|
||||||
});
|
});
|
||||||
watch(showDrawer, () => {
|
|
||||||
if (window.innerWidth < 760 && state.hasOpenedBefore === false) {
|
|
||||||
state.hasOpenedBefore = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const { width: wWidth } = useWindowSize();
|
|
||||||
watch(wWidth, (w) => {
|
|
||||||
if (w > 760) {
|
|
||||||
showDrawer.value = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showDrawer.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || []), ...(props.bottomLinks || [])]);
|
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || [])]);
|
||||||
function initDropdowns() {
|
function initDropdowns() {
|
||||||
allLinks.value.forEach((link) => {
|
allLinks.value.forEach((link) => {
|
||||||
state.dropDowns[link.title] = link.childrenStartExpanded || false;
|
state.dropDowns[link.title] = link.childrenStartExpanded || false;
|
||||||
@@ -193,8 +182,11 @@ export default defineNuxtComponent({
|
|||||||
userProfileLink,
|
userProfileLink,
|
||||||
showDrawer,
|
showDrawer,
|
||||||
loggedIn,
|
loggedIn,
|
||||||
|
isAdmin,
|
||||||
|
canManage,
|
||||||
isOwnGroup,
|
isOwnGroup,
|
||||||
sessionUser: $auth.user,
|
sessionUser: $auth.user,
|
||||||
|
toggleDark,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
48
frontend/components/global/AppLogo.vue
Normal file
48
frontend/components/global/AppLogo.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="icon-container">
|
||||||
|
<v-divider class="icon-divider" />
|
||||||
|
<v-avatar
|
||||||
|
:class="['pa-2', 'icon-avatar']"
|
||||||
|
color="primary"
|
||||||
|
:size="size"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<svg
|
||||||
|
class="icon-white"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
:style="{ width: size + 'px', height: size + 'px' }"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</v-avatar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { size } = withDefaults(defineProps<{ size?: number }>(), { size: 75 });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.icon-white {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
.icon-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
}
|
||||||
|
.icon-divider {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: -2.5rem;
|
||||||
|
}
|
||||||
|
.icon-avatar {
|
||||||
|
border-color: rgba(0, 0, 0, 0.12);
|
||||||
|
border: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -33,9 +33,10 @@
|
|||||||
<!-- Check Box -->
|
<!-- Check Box -->
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-if="inputField.type === fieldTypes.BOOLEAN"
|
v-if="inputField.type === fieldTypes.BOOLEAN"
|
||||||
v-model="modelValue[inputField.varName]"
|
v-model="model[inputField.varName]"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
:hint="inputField.hint"
|
:hint="inputField.hint"
|
||||||
:hide-details="!inputField.hint"
|
:hide-details="!inputField.hint"
|
||||||
:persistent-hint="!!inputField.hint"
|
:persistent-hint="!!inputField.hint"
|
||||||
@@ -51,9 +52,9 @@
|
|||||||
<!-- Text Field -->
|
<!-- Text Field -->
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-else-if="inputField.type === fieldTypes.TEXT || inputField.type === fieldTypes.PASSWORD"
|
v-else-if="inputField.type === fieldTypes.TEXT || inputField.type === fieldTypes.PASSWORD"
|
||||||
v-model="modelValue[inputField.varName]"
|
v-model="model[inputField.varName]"
|
||||||
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
:type="inputField.type === fieldTypes.PASSWORD ? 'password' : 'text'"
|
:type="inputField.type === fieldTypes.PASSWORD ? 'password' : 'text'"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
flat
|
flat
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
:label="inputField.label"
|
:label="inputField.label"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:hint="inputField.hint || ''"
|
:hint="inputField.hint || ''"
|
||||||
:rules="!(inputField.disableUpdate && updateMode) ? [...rulesByKey(inputField.rules), ...defaultRules] : []"
|
:rules="!(inputField.disableUpdate && updateMode) ? [...rulesByKey(inputField.rules as any), ...defaultRules] : []"
|
||||||
lazy-validation
|
lazy-validation
|
||||||
@blur="emitBlur"
|
@blur="emitBlur"
|
||||||
/>
|
/>
|
||||||
@@ -70,9 +71,9 @@
|
|||||||
<!-- Text Area -->
|
<!-- Text Area -->
|
||||||
<v-textarea
|
<v-textarea
|
||||||
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
||||||
v-model="modelValue[inputField.varName]"
|
v-model="model[inputField.varName]"
|
||||||
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
flat
|
flat
|
||||||
rows="3"
|
rows="3"
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
:label="inputField.label"
|
:label="inputField.label"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:hint="inputField.hint || ''"
|
:hint="inputField.hint || ''"
|
||||||
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
|
:rules="[...rulesByKey(inputField.rules as any), ...defaultRules]"
|
||||||
lazy-validation
|
lazy-validation
|
||||||
@blur="emitBlur"
|
@blur="emitBlur"
|
||||||
/>
|
/>
|
||||||
@@ -89,12 +90,11 @@
|
|||||||
<!-- Option Select -->
|
<!-- Option Select -->
|
||||||
<v-select
|
<v-select
|
||||||
v-else-if="inputField.type === fieldTypes.SELECT"
|
v-else-if="inputField.type === fieldTypes.SELECT"
|
||||||
v-model="modelValue[inputField.varName]"
|
v-model="model[inputField.varName]"
|
||||||
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
|
:readonly="fieldState[inputField.varName]?.readonly"
|
||||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
:disabled="fieldState[inputField.varName]?.disabled"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
flat
|
flat
|
||||||
:prepend-icon="inputField.icons ? modelValue[inputField.varName] : null"
|
|
||||||
:label="inputField.label"
|
:label="inputField.label"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:items="inputField.options"
|
:items="inputField.options"
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
class="my-2 ml-auto"
|
class="my-2 ml-auto"
|
||||||
style="min-width: 200px"
|
style="min-width: 200px"
|
||||||
:color="modelValue[inputField.varName]"
|
:color="model[inputField.varName]"
|
||||||
dark
|
dark
|
||||||
v-bind="templateProps"
|
v-bind="templateProps"
|
||||||
>
|
>
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-color-picker
|
<v-color-picker
|
||||||
v-model="modelValue[inputField.varName]"
|
v-model="model[inputField.varName]"
|
||||||
value="#7417BE"
|
value="#7417BE"
|
||||||
hide-canvas
|
hide-canvas
|
||||||
hide-inputs
|
hide-inputs
|
||||||
@@ -138,11 +138,12 @@
|
|||||||
</v-menu>
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Object Type -->
|
||||||
<div v-else-if="inputField.type === fieldTypes.OBJECT">
|
<div v-else-if="inputField.type === fieldTypes.OBJECT">
|
||||||
<auto-form
|
<auto-form
|
||||||
v-model="modelValue[inputField.varName]"
|
v-model="model[inputField.varName]"
|
||||||
:color="color"
|
:color="color"
|
||||||
:items="inputField.items"
|
:items="(inputField as any).items"
|
||||||
@blur="emitBlur"
|
@blur="emitBlur"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,7 +151,7 @@
|
|||||||
<!-- List Type -->
|
<!-- List Type -->
|
||||||
<div v-else-if="inputField.type === fieldTypes.LIST">
|
<div v-else-if="inputField.type === fieldTypes.LIST">
|
||||||
<div
|
<div
|
||||||
v-for="(item, idx) in modelValue[inputField.varName]"
|
v-for="(item, idx) in model[inputField.varName]"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
@@ -160,15 +161,15 @@
|
|||||||
class="ml-5"
|
class="ml-5"
|
||||||
x-small
|
x-small
|
||||||
delete
|
delete
|
||||||
@click="removeByIndex(modelValue[inputField.varName], idx)"
|
@click="removeByIndex(model[inputField.varName], idx)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<v-divider class="mb-5 mx-2" />
|
<v-divider class="mb-5 mx-2" />
|
||||||
<auto-form
|
<auto-form
|
||||||
v-model="modelValue[inputField.varName][idx]"
|
v-model="model[inputField.varName][idx]"
|
||||||
:color="color"
|
:color="color"
|
||||||
:items="inputField.items"
|
:items="(inputField as any).items"
|
||||||
@blur="emitBlur"
|
@blur="emitBlur"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,7 +177,7 @@
|
|||||||
<v-spacer />
|
<v-spacer />
|
||||||
<BaseButton
|
<BaseButton
|
||||||
small
|
small
|
||||||
@click="modelValue[inputField.varName].push(getTemplate(inputField.items))"
|
@click="model[inputField.varName].push(getTemplate((inputField as any).items))"
|
||||||
>
|
>
|
||||||
{{ $t("general.new") }}
|
{{ $t("general.new") }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
@@ -197,7 +198,13 @@ const BLUR_EVENT = "blur";
|
|||||||
type ValidatorKey = keyof typeof validators;
|
type ValidatorKey = keyof typeof validators;
|
||||||
|
|
||||||
// Use defineModel for v-model
|
// Use defineModel for v-model
|
||||||
const modelValue = defineModel<[object, Array<any>]>();
|
const modelValue = defineModel<Record<string, any> | any[]>({
|
||||||
|
type: [Object, Array],
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// alias to avoid template TS complaining about possible undefined
|
||||||
|
const model = modelValue as any;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
updateMode: {
|
updateMode: {
|
||||||
@@ -238,26 +245,39 @@ const emit = defineEmits(["blur", "update:modelValue"]);
|
|||||||
|
|
||||||
function rulesByKey(keys?: ValidatorKey[] | null) {
|
function rulesByKey(keys?: ValidatorKey[] | null) {
|
||||||
if (keys === undefined || keys === null) {
|
if (keys === undefined || keys === null) {
|
||||||
return [];
|
return [] as any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = [] as ((v: string) => boolean | string)[];
|
const list: any[] = [];
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
const split = key.split(":");
|
const split = key.split(":");
|
||||||
const validatorKey = split[0] as ValidatorKey;
|
const validatorKey = split[0] as ValidatorKey;
|
||||||
if (validatorKey in validators) {
|
if (validatorKey in validators) {
|
||||||
if (split.length === 1) {
|
if (split.length === 1) {
|
||||||
list.push(validators[validatorKey]);
|
list.push((validators as any)[validatorKey]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
list.push(validators[validatorKey](split[1]));
|
list.push((validators as any)[validatorKey](split[1] as any));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
|
const defaultRules = computed<any[]>(() => rulesByKey(props.globalRules as any));
|
||||||
|
|
||||||
|
// Combined state map for readonly and disabled fields
|
||||||
|
const fieldState = computed<Record<string, { readonly: boolean; disabled: boolean }>>(() => {
|
||||||
|
const map: Record<string, { readonly: boolean; disabled: boolean }> = {};
|
||||||
|
(props.items || []).forEach((field: any) => {
|
||||||
|
const base = (field.disableUpdate && props.updateMode) || (!props.updateMode && field.disableCreate);
|
||||||
|
map[field.varName] = {
|
||||||
|
readonly: base || !!props.readonlyFields?.includes(field.varName),
|
||||||
|
disabled: base || !!props.disabledFields?.includes(field.varName),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
function removeByIndex(list: never[], index: number) {
|
function removeByIndex(list: never[], index: number) {
|
||||||
// Removes the item at the index
|
// Removes the item at the index
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { $vuetify } = useNuxtApp();
|
const display = useDisplay();
|
||||||
const hasHeading = computed(() => false);
|
const hasHeading = computed(() => false);
|
||||||
const hasAltHeading = computed(() => false);
|
const hasAltHeading = computed(() => false);
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
return {
|
return {
|
||||||
"v-card--material--has-heading": hasHeading,
|
"v-card--material--has-heading": hasHeading,
|
||||||
"mt-3": $vuetify.display.name.value === "xs" || $vuetify.display.name.value === "sm",
|
"mt-3": display.name.value === "xs" || display.name.value === "sm",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :style="`width: ${width}; height: 100%;`">
|
|
||||||
<LanguageDialog v-model="langDialog" />
|
|
||||||
<v-card>
|
|
||||||
<div>
|
|
||||||
<v-toolbar
|
|
||||||
width="100%"
|
|
||||||
color="primary"
|
|
||||||
class="d-flex justify-center"
|
|
||||||
style="margin-bottom: 4rem"
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
<v-toolbar-title class="headline text-h4 text-center mx-0">
|
|
||||||
Mealie
|
|
||||||
</v-toolbar-title>
|
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<div class="icon-container">
|
|
||||||
<v-divider class="icon-divider" />
|
|
||||||
<v-avatar
|
|
||||||
class="pa-2 icon-avatar"
|
|
||||||
color="primary"
|
|
||||||
size="75"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="icon-white"
|
|
||||||
style="width: 75"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</v-avatar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-center grow items-center my-4">
|
|
||||||
<slot :width="pageWidth" />
|
|
||||||
</div>
|
|
||||||
<div class="mx-2 my-4">
|
|
||||||
<v-progress-linear
|
|
||||||
v-if="wizardPage > 0"
|
|
||||||
:value="Math.ceil((wizardPage / maxPageNumber) * 100)"
|
|
||||||
striped
|
|
||||||
height="10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<v-divider class="ma-2" />
|
|
||||||
<v-card-actions width="100%">
|
|
||||||
<v-btn
|
|
||||||
v-if="prevButtonShow"
|
|
||||||
:disabled="!prevButtonEnable"
|
|
||||||
:color="prevButtonColor"
|
|
||||||
@click="decrementPage"
|
|
||||||
>
|
|
||||||
<v-icon v-if="prevButtonIconRef">
|
|
||||||
{{ prevButtonIconRef }}
|
|
||||||
</v-icon>
|
|
||||||
{{ prevButtonTextRef }}
|
|
||||||
</v-btn>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
v-if="nextButtonShow"
|
|
||||||
variant="elevated"
|
|
||||||
:disabled="!nextButtonEnable"
|
|
||||||
:color="nextButtonColorRef"
|
|
||||||
@click="incrementPage"
|
|
||||||
>
|
|
||||||
<div v-if="isSubmitting">
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
color="white"
|
|
||||||
size="24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<v-icon v-if="nextButtonIconRef && !nextButtonIconAfter">
|
|
||||||
{{ nextButtonIconRef }}
|
|
||||||
</v-icon>
|
|
||||||
{{ nextButtonTextRef }}
|
|
||||||
<v-icon v-if="nextButtonIconRef && nextButtonIconAfter">
|
|
||||||
{{ nextButtonIconRef }}
|
|
||||||
</v-icon>
|
|
||||||
</div>
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
<v-card-actions class="justify-center flex-column py-8">
|
|
||||||
<BaseButton
|
|
||||||
large
|
|
||||||
color="primary"
|
|
||||||
@click="langDialog = true"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
{{ $globals.icons.translate }}
|
|
||||||
</template>
|
|
||||||
{{ $t("language-dialog.choose-language") }}
|
|
||||||
</BaseButton>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default defineNuxtComponent({
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
minPageNumber: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
maxPageNumber: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: "1200px",
|
|
||||||
},
|
|
||||||
pageWidth: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: "600px",
|
|
||||||
},
|
|
||||||
prevButtonText: {
|
|
||||||
type: String,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
prevButtonIcon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
prevButtonColor: {
|
|
||||||
type: String,
|
|
||||||
default: "grey-darken-3",
|
|
||||||
},
|
|
||||||
prevButtonShow: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
prevButtonEnable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
nextButtonText: {
|
|
||||||
type: String,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
nextButtonIcon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
nextButtonIconAfter: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
nextButtonColor: {
|
|
||||||
type: String,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
nextButtonShow: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
nextButtonEnable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
nextButtonIsSubmit: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
isSubmitting: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue", "submit"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const { $globals } = useNuxtApp();
|
|
||||||
const ready = ref(false);
|
|
||||||
const langDialog = ref(false);
|
|
||||||
|
|
||||||
const wizardPage = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: value => context.emit("update:modelValue", value),
|
|
||||||
});
|
|
||||||
|
|
||||||
const prevButtonTextRef = computed(() => props.prevButtonText || i18n.t("general.back"));
|
|
||||||
const prevButtonIconRef = computed(() => props.prevButtonIcon || $globals.icons.back);
|
|
||||||
const nextButtonTextRef = computed(
|
|
||||||
() => props.nextButtonText || (
|
|
||||||
props.nextButtonIsSubmit ? i18n.t("general.submit") : i18n.t("general.next")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const nextButtonIconRef = computed(
|
|
||||||
() => props.nextButtonIcon || (
|
|
||||||
props.nextButtonIsSubmit ? $globals.icons.createAlt : $globals.icons.forward
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const nextButtonColorRef = computed(
|
|
||||||
() => props.nextButtonColor || (props.nextButtonIsSubmit ? "success" : "info"),
|
|
||||||
);
|
|
||||||
|
|
||||||
function goToPage(page: number) {
|
|
||||||
if (page < props.minPageNumber) {
|
|
||||||
goToPage(props.minPageNumber);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (page > props.maxPageNumber) {
|
|
||||||
goToPage(props.maxPageNumber);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
wizardPage.value = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decrementPage() {
|
|
||||||
goToPage(wizardPage.value - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementPage() {
|
|
||||||
if (props.nextButtonIsSubmit) {
|
|
||||||
context.emit("submit", wizardPage.value);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
goToPage(wizardPage.value + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ready.value = true;
|
|
||||||
|
|
||||||
return {
|
|
||||||
wizardPage,
|
|
||||||
ready,
|
|
||||||
langDialog,
|
|
||||||
prevButtonTextRef,
|
|
||||||
prevButtonIconRef,
|
|
||||||
nextButtonTextRef,
|
|
||||||
nextButtonIconRef,
|
|
||||||
nextButtonColorRef,
|
|
||||||
decrementPage,
|
|
||||||
incrementPage,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
.icon-primary {
|
|
||||||
fill: var(--v-primary-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-white {
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-divider {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: -2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-avatar {
|
|
||||||
border-color: rgba(0, 0, 0, 0.12);
|
|
||||||
border: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-off-white {
|
|
||||||
background: #f5f8fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preferred-width {
|
|
||||||
width: 840px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -39,7 +39,7 @@ export default defineNuxtComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const value = computed(() => {
|
const value = computed(() => {
|
||||||
const rawHtml = marked.parse(props.source || "", { async: false });
|
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
|
||||||
return sanitizeMarkdown(rawHtml);
|
return sanitizeMarkdown(rawHtml);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Composer } from "vue-i18n";
|
|||||||
import type { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
|
import type { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
|
||||||
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
|
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
|
||||||
import { PublicExploreApi } from "~/lib/api/client-public";
|
import { PublicExploreApi } from "~/lib/api/client-public";
|
||||||
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
async safe<T, U>(
|
async safe<T, U>(
|
||||||
@@ -56,8 +57,7 @@ function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
|
|||||||
export const useRequests = function (i18n?: Composer): ApiRequestInstance {
|
export const useRequests = function (i18n?: Composer): ApiRequestInstance {
|
||||||
const { $axios } = useNuxtApp();
|
const { $axios } = useNuxtApp();
|
||||||
if (!i18n) {
|
if (!i18n) {
|
||||||
// Only works in a setup block
|
i18n = useGlobalI18n();
|
||||||
i18n = useI18n();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$axios.defaults.headers.common["Accept-Language"] = i18n.locale.value;
|
$axios.defaults.headers.common["Accept-Language"] = i18n.locale.value;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function useUnitName(unit: CreateIngredientUnit | IngredientUnit | undefined, us
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true) {
|
export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true) {
|
||||||
const { quantity, food, unit, note } = ingredient;
|
const { quantity, food, unit, note, title } = ingredient;
|
||||||
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
||||||
const usePluralFood = (!quantity) || quantity * scale > 1;
|
const usePluralFood = (!quantity) || quantity * scale > 1;
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1,
|
|||||||
const foodName = useFoodName(food || undefined, usePluralFood);
|
const foodName = useFoodName(food || undefined, usePluralFood);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
title: title ? sanitizeIngredientHTML(title) : undefined,
|
||||||
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
||||||
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
|
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
|
||||||
name: foodName ? sanitizeIngredientHTML(foodName) : undefined,
|
name: foodName ? sanitizeIngredientHTML(foodName) : undefined,
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import type { Composer } from "vue-i18n";
|
import type { Composer } from "vue-i18n";
|
||||||
import { useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
import { useReadOnlyStore, useStore } from "../partials/use-store-factory";
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
|
||||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
const store: Ref<ReadCookBook[]> = ref([]);
|
const cookbooks: Ref<ReadCookBook[]> = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
export const useCookbookStore = function (i18n?: Composer) {
|
export const useCookbookStore = function (i18n?: Composer) {
|
||||||
const api = useUserApi(i18n);
|
const api = useUserApi(i18n);
|
||||||
return useStore<ReadCookBook>(store, loading, api.cookbooks);
|
const store = useStore<ReadCookBook>(cookbooks, loading, api.cookbooks);
|
||||||
|
|
||||||
|
const updateAll = async function (updateData: UpdateCookBook[]) {
|
||||||
|
loading.value = true;
|
||||||
|
updateData.forEach((cookbook, index) => {
|
||||||
|
cookbook.position = index;
|
||||||
|
});
|
||||||
|
const { data } = await api.cookbooks.updateAll(updateData);
|
||||||
|
loading.value = false;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
return { ...store, updateAll };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) {
|
export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) {
|
||||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||||
return useReadOnlyStore<ReadCookBook>(store, publicLoading, api.cookbooks);
|
return useReadOnlyStore<ReadCookBook>(cookbooks, publicLoading, api.cookbooks);
|
||||||
};
|
};
|
||||||
|
|||||||
10
frontend/composables/use-global-i18n.ts
Normal file
10
frontend/composables/use-global-i18n.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Composer } from "vue-i18n";
|
||||||
|
|
||||||
|
let i18n: Composer | null = null;
|
||||||
|
|
||||||
|
export function useGlobalI18n() {
|
||||||
|
if (!i18n) {
|
||||||
|
i18n = useI18n();
|
||||||
|
}
|
||||||
|
return i18n;
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Slovenčina (Slovak)",
|
name: "Slovenčina (Slovak)",
|
||||||
value: "sk-SK",
|
value: "sk-SK",
|
||||||
progress: 37,
|
progress: 46,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,7 +81,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Polski (Polish)",
|
name: "Polski (Polish)",
|
||||||
value: "pl-PL",
|
value: "pl-PL",
|
||||||
progress: 40,
|
progress: 42,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -93,7 +93,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Nederlands (Dutch)",
|
name: "Nederlands (Dutch)",
|
||||||
value: "nl-NL",
|
value: "nl-NL",
|
||||||
progress: 45,
|
progress: 49,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -123,7 +123,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Italiano (Italian)",
|
name: "Italiano (Italian)",
|
||||||
value: "it-IT",
|
value: "it-IT",
|
||||||
progress: 39,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -135,13 +135,13 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Magyar (Hungarian)",
|
name: "Magyar (Hungarian)",
|
||||||
value: "hu-HU",
|
value: "hu-HU",
|
||||||
progress: 41,
|
progress: 44,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Hrvatski (Croatian)",
|
name: "Hrvatski (Croatian)",
|
||||||
value: "hr-HR",
|
value: "hr-HR",
|
||||||
progress: 27,
|
progress: 28,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -159,7 +159,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Français (French)",
|
name: "Français (French)",
|
||||||
value: "fr-FR",
|
value: "fr-FR",
|
||||||
progress: 55,
|
progress: 64,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -189,7 +189,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Español (Spanish)",
|
name: "Español (Spanish)",
|
||||||
value: "es-ES",
|
value: "es-ES",
|
||||||
progress: 41,
|
progress: 42,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -201,19 +201,19 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "British English",
|
name: "British English",
|
||||||
value: "en-GB",
|
value: "en-GB",
|
||||||
progress: 23,
|
progress: 43,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ελληνικά (Greek)",
|
name: "Ελληνικά (Greek)",
|
||||||
value: "el-GR",
|
value: "el-GR",
|
||||||
progress: 39,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Deutsch (German)",
|
name: "Deutsch (German)",
|
||||||
value: "de-DE",
|
value: "de-DE",
|
||||||
progress: 66,
|
progress: 72,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -225,13 +225,13 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Čeština (Czech)",
|
name: "Čeština (Czech)",
|
||||||
value: "cs-CZ",
|
value: "cs-CZ",
|
||||||
progress: 41,
|
progress: 42,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Català (Catalan)",
|
name: "Català (Catalan)",
|
||||||
value: "ca-ES",
|
value: "ca-ES",
|
||||||
progress: 37,
|
progress: 38,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
465
frontend/composables/use-recipe-explorer-search.ts
Normal file
465
frontend/composables/use-recipe-explorer-search.ts
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
import { watchDebounced } from "@vueuse/shared";
|
||||||
|
import type { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||||
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
|
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
|
import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||||
|
import {
|
||||||
|
useCategoryStore,
|
||||||
|
usePublicCategoryStore,
|
||||||
|
useFoodStore,
|
||||||
|
usePublicFoodStore,
|
||||||
|
useHouseholdStore,
|
||||||
|
usePublicHouseholdStore,
|
||||||
|
useTagStore,
|
||||||
|
usePublicTagStore,
|
||||||
|
useToolStore,
|
||||||
|
usePublicToolStore,
|
||||||
|
} from "~/composables/store";
|
||||||
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
|
import { useUserSearchQuerySession, useUserSortPreferences } from "~/composables/use-users/preferences";
|
||||||
|
|
||||||
|
// Type for the composable return value
|
||||||
|
interface RecipeExplorerSearchState {
|
||||||
|
state: Ref<{
|
||||||
|
auto: boolean;
|
||||||
|
ready: boolean;
|
||||||
|
search: string;
|
||||||
|
orderBy: string;
|
||||||
|
orderDirection: "asc" | "desc";
|
||||||
|
requireAllCategories: boolean;
|
||||||
|
requireAllTags: boolean;
|
||||||
|
requireAllTools: boolean;
|
||||||
|
requireAllFoods: boolean;
|
||||||
|
}>;
|
||||||
|
selectedCategories: Ref<NoUndefinedField<RecipeCategory>[]>;
|
||||||
|
selectedFoods: Ref<IngredientFood[]>;
|
||||||
|
selectedHouseholds: Ref<NoUndefinedField<HouseholdSummary>[]>;
|
||||||
|
selectedTags: Ref<NoUndefinedField<RecipeTag>[]>;
|
||||||
|
selectedTools: Ref<NoUndefinedField<RecipeTool>[]>;
|
||||||
|
passedQueryWithSeed: ComputedRef<RecipeSearchQuery & { _searchSeed: string }>;
|
||||||
|
search: () => Promise<void>;
|
||||||
|
reset: () => void;
|
||||||
|
toggleOrderDirection: () => void;
|
||||||
|
setOrderBy: (value: string) => void;
|
||||||
|
filterItems: (item: RecipeCategory | RecipeTag | RecipeTool, urlPrefix: string) => void;
|
||||||
|
initialize: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memo storage for singleton instances
|
||||||
|
const memo: Record<string, RecipeExplorerSearchState> = {};
|
||||||
|
|
||||||
|
function createRecipeExplorerSearchState(groupSlug: ComputedRef<string>): RecipeExplorerSearchState {
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
const searchQuerySession = useUserSearchQuerySession();
|
||||||
|
const sortPreferences = useUserSortPreferences();
|
||||||
|
|
||||||
|
// State management
|
||||||
|
const state = ref({
|
||||||
|
auto: true,
|
||||||
|
ready: false,
|
||||||
|
search: "",
|
||||||
|
orderBy: "created_at",
|
||||||
|
orderDirection: "desc" as "asc" | "desc",
|
||||||
|
requireAllCategories: false,
|
||||||
|
requireAllTags: false,
|
||||||
|
requireAllTools: false,
|
||||||
|
requireAllFoods: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store references
|
||||||
|
const categories = isOwnGroup ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
|
||||||
|
const foods = isOwnGroup ? useFoodStore() : usePublicFoodStore(groupSlug.value);
|
||||||
|
const households = isOwnGroup ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
|
||||||
|
const tags = isOwnGroup ? useTagStore() : usePublicTagStore(groupSlug.value);
|
||||||
|
const tools = isOwnGroup ? useToolStore() : usePublicToolStore(groupSlug.value);
|
||||||
|
|
||||||
|
// Selected items
|
||||||
|
const selectedCategories = ref<NoUndefinedField<RecipeCategory>[]>([]);
|
||||||
|
const selectedFoods = ref<IngredientFood[]>([]);
|
||||||
|
const selectedHouseholds = ref<NoUndefinedField<HouseholdSummary>[]>([]);
|
||||||
|
const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
|
||||||
|
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
|
||||||
|
|
||||||
|
// Query defaults
|
||||||
|
const queryDefaults = {
|
||||||
|
search: "",
|
||||||
|
orderBy: "created_at",
|
||||||
|
orderDirection: "desc" as "asc" | "desc",
|
||||||
|
requireAllCategories: false,
|
||||||
|
requireAllTags: false,
|
||||||
|
requireAllTools: false,
|
||||||
|
requireAllFoods: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync sort preferences
|
||||||
|
watch(() => state.value.orderBy, (newValue) => {
|
||||||
|
sortPreferences.value.orderBy = newValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => state.value.orderDirection, (newValue) => {
|
||||||
|
sortPreferences.value.orderDirection = newValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
function toIDArray(array: { id: string }[]) {
|
||||||
|
return array.map(item => item.id).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcPassedQuery(): RecipeSearchQuery {
|
||||||
|
return {
|
||||||
|
search: state.value.search ? state.value.search : "",
|
||||||
|
categories: toIDArray(selectedCategories.value),
|
||||||
|
foods: toIDArray(selectedFoods.value),
|
||||||
|
households: toIDArray(selectedHouseholds.value),
|
||||||
|
tags: toIDArray(selectedTags.value),
|
||||||
|
tools: toIDArray(selectedTools.value),
|
||||||
|
requireAllCategories: state.value.requireAllCategories,
|
||||||
|
requireAllTags: state.value.requireAllTags,
|
||||||
|
requireAllTools: state.value.requireAllTools,
|
||||||
|
requireAllFoods: state.value.requireAllFoods,
|
||||||
|
orderBy: state.value.orderBy,
|
||||||
|
orderDirection: state.value.orderDirection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const passedQuery = ref<RecipeSearchQuery>(calcPassedQuery());
|
||||||
|
|
||||||
|
const passedQueryWithSeed = computed(() => {
|
||||||
|
return {
|
||||||
|
...passedQuery.value,
|
||||||
|
_searchSeed: Date.now().toString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait utility for async hydration
|
||||||
|
function waitUntilAndExecute(
|
||||||
|
condition: () => boolean,
|
||||||
|
callback: () => void,
|
||||||
|
opts = { timeout: 2000, interval: 500 },
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const state = {
|
||||||
|
timeout: undefined as number | undefined,
|
||||||
|
interval: undefined as number | undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const check = () => {
|
||||||
|
if (condition()) {
|
||||||
|
clearInterval(state.interval);
|
||||||
|
clearTimeout(state.timeout);
|
||||||
|
callback();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.interval = setInterval(check, opts.interval) as unknown as number;
|
||||||
|
state.timeout = setTimeout(() => {
|
||||||
|
clearInterval(state.interval);
|
||||||
|
reject(new Error("Timeout"));
|
||||||
|
}, opts.timeout) as unknown as number;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main functions
|
||||||
|
function reset() {
|
||||||
|
state.value.search = queryDefaults.search;
|
||||||
|
state.value.orderBy = queryDefaults.orderBy;
|
||||||
|
state.value.orderDirection = queryDefaults.orderDirection;
|
||||||
|
sortPreferences.value.orderBy = queryDefaults.orderBy;
|
||||||
|
sortPreferences.value.orderDirection = queryDefaults.orderDirection;
|
||||||
|
state.value.requireAllCategories = queryDefaults.requireAllCategories;
|
||||||
|
state.value.requireAllTags = queryDefaults.requireAllTags;
|
||||||
|
state.value.requireAllTools = queryDefaults.requireAllTools;
|
||||||
|
state.value.requireAllFoods = queryDefaults.requireAllFoods;
|
||||||
|
selectedCategories.value = [];
|
||||||
|
selectedFoods.value = [];
|
||||||
|
selectedHouseholds.value = [];
|
||||||
|
selectedTags.value = [];
|
||||||
|
selectedTools.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOrderDirection() {
|
||||||
|
state.value.orderDirection = state.value.orderDirection === "asc" ? "desc" : "asc";
|
||||||
|
sortPreferences.value.orderDirection = state.value.orderDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOrderBy(value: string) {
|
||||||
|
state.value.orderBy = value;
|
||||||
|
sortPreferences.value.orderBy = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
const oldQueryValueString = JSON.stringify(passedQuery.value);
|
||||||
|
const newQueryValue = calcPassedQuery();
|
||||||
|
const newQueryValueString = JSON.stringify(newQueryValue);
|
||||||
|
if (oldQueryValueString === newQueryValueString) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
passedQuery.value = newQueryValue;
|
||||||
|
const query = {
|
||||||
|
categories: passedQuery.value.categories,
|
||||||
|
foods: passedQuery.value.foods,
|
||||||
|
tags: passedQuery.value.tags,
|
||||||
|
tools: passedQuery.value.tools,
|
||||||
|
// Only add the query param if it's not the default value
|
||||||
|
...{
|
||||||
|
auto: state.value.auto ? undefined : "false",
|
||||||
|
search: passedQuery.value.search === queryDefaults.search ? undefined : passedQuery.value.search,
|
||||||
|
households: !passedQuery.value.households?.length || passedQuery.value.households?.length === households.store.value.length ? undefined : passedQuery.value.households,
|
||||||
|
requireAllCategories: passedQuery.value.requireAllCategories ? "true" : undefined,
|
||||||
|
requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined,
|
||||||
|
requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined,
|
||||||
|
requireAllFoods: passedQuery.value.requireAllFoods ? "true" : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await router.push({ query });
|
||||||
|
searchQuerySession.value.recipe = JSON.stringify(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterItems(item: RecipeCategory | RecipeTag | RecipeTool, urlPrefix: string) {
|
||||||
|
if (urlPrefix === "categories") {
|
||||||
|
const result = categories.store.value.filter(category => (category.id as string).includes(item.id as string));
|
||||||
|
selectedCategories.value = result as NoUndefinedField<RecipeCategory>[];
|
||||||
|
}
|
||||||
|
else if (urlPrefix === "tags") {
|
||||||
|
const result = tags.store.value.filter(tag => (tag.id as string).includes(item.id as string));
|
||||||
|
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
||||||
|
}
|
||||||
|
else if (urlPrefix === "tools") {
|
||||||
|
const result = tools.store.value.filter(tool => (tool.id).includes(item.id || ""));
|
||||||
|
selectedTools.value = result as NoUndefinedField<RecipeTool>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hydrateSearch() {
|
||||||
|
const query = router.currentRoute.value.query;
|
||||||
|
if (query.auto?.length) {
|
||||||
|
state.value.auto = query.auto === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.search?.length) {
|
||||||
|
state.value.search = query.search as string;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.value.search = queryDefaults.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.value.orderBy = sortPreferences.value.orderBy;
|
||||||
|
state.value.orderDirection = sortPreferences.value.orderDirection as "asc" | "desc";
|
||||||
|
|
||||||
|
if (query.requireAllCategories?.length) {
|
||||||
|
state.value.requireAllCategories = query.requireAllCategories === "true";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.value.requireAllCategories = queryDefaults.requireAllCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.requireAllTags?.length) {
|
||||||
|
state.value.requireAllTags = query.requireAllTags === "true";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.value.requireAllTags = queryDefaults.requireAllTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.requireAllTools?.length) {
|
||||||
|
state.value.requireAllTools = query.requireAllTools === "true";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.value.requireAllTools = queryDefaults.requireAllTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.requireAllFoods?.length) {
|
||||||
|
state.value.requireAllFoods = query.requireAllFoods === "true";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.value.requireAllFoods = queryDefaults.requireAllFoods;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (query.categories?.length) {
|
||||||
|
promises.push(
|
||||||
|
waitUntilAndExecute(
|
||||||
|
() => categories.store.value.length > 0,
|
||||||
|
() => {
|
||||||
|
const result = categories.store.value.filter(item =>
|
||||||
|
(query.categories as string[]).includes(item.id as string),
|
||||||
|
);
|
||||||
|
selectedCategories.value = result as NoUndefinedField<RecipeCategory>[];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedCategories.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.tags?.length) {
|
||||||
|
promises.push(
|
||||||
|
waitUntilAndExecute(
|
||||||
|
() => tags.store.value.length > 0,
|
||||||
|
() => {
|
||||||
|
const result = tags.store.value.filter(item => (query.tags as string[]).includes(item.id as string));
|
||||||
|
selectedTags.value = result as NoUndefinedField<RecipeTag>[];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedTags.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.tools?.length) {
|
||||||
|
promises.push(
|
||||||
|
waitUntilAndExecute(
|
||||||
|
() => tools.store.value.length > 0,
|
||||||
|
() => {
|
||||||
|
const result = tools.store.value.filter(item => (query.tools as string[]).includes(item.id));
|
||||||
|
selectedTools.value = result as NoUndefinedField<RecipeTool>[];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedTools.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.foods?.length) {
|
||||||
|
promises.push(
|
||||||
|
waitUntilAndExecute(
|
||||||
|
() => {
|
||||||
|
if (foods.store.value) {
|
||||||
|
return foods.store.value.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const result = foods.store.value?.filter(item => (query.foods as string[]).includes(item.id));
|
||||||
|
selectedFoods.value = result ?? [];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedFoods.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.households?.length) {
|
||||||
|
promises.push(
|
||||||
|
waitUntilAndExecute(
|
||||||
|
() => {
|
||||||
|
if (households.store.value) {
|
||||||
|
return households.store.value.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const result = households.store.value?.filter(item => (query.households as string[]).includes(item.id));
|
||||||
|
selectedHouseholds.value = result as NoUndefinedField<HouseholdSummary>[] ?? [];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedHouseholds.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initialize() {
|
||||||
|
// Restore the user's last search query
|
||||||
|
if (searchQuerySession.value.recipe && !(Object.keys(route.query).length > 0)) {
|
||||||
|
try {
|
||||||
|
const query = JSON.parse(searchQuerySession.value.recipe);
|
||||||
|
await router.replace({ query });
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
searchQuerySession.value.recipe = "";
|
||||||
|
router.replace({ query: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await hydrateSearch();
|
||||||
|
await search();
|
||||||
|
state.value.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for route query changes
|
||||||
|
watch(
|
||||||
|
() => route.query,
|
||||||
|
() => {
|
||||||
|
if (!Object.keys(route.query).length) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-search when parameters change
|
||||||
|
watchDebounced(
|
||||||
|
[
|
||||||
|
() => state.value.search,
|
||||||
|
() => state.value.requireAllCategories,
|
||||||
|
() => state.value.requireAllTags,
|
||||||
|
() => state.value.requireAllTools,
|
||||||
|
() => state.value.requireAllFoods,
|
||||||
|
() => state.value.orderBy,
|
||||||
|
() => state.value.orderDirection,
|
||||||
|
selectedCategories,
|
||||||
|
selectedFoods,
|
||||||
|
selectedHouseholds,
|
||||||
|
selectedTags,
|
||||||
|
selectedTools,
|
||||||
|
],
|
||||||
|
async () => {
|
||||||
|
if (state.value.ready && state.value.auto) {
|
||||||
|
await search();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
debounce: 500,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const composableInstance: RecipeExplorerSearchState = {
|
||||||
|
// State
|
||||||
|
state,
|
||||||
|
selectedCategories,
|
||||||
|
selectedFoods,
|
||||||
|
selectedHouseholds,
|
||||||
|
selectedTags,
|
||||||
|
selectedTools,
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
passedQueryWithSeed,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
search,
|
||||||
|
reset,
|
||||||
|
toggleOrderDirection,
|
||||||
|
setOrderBy,
|
||||||
|
filterItems,
|
||||||
|
initialize,
|
||||||
|
};
|
||||||
|
|
||||||
|
return composableInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRecipeExplorerSearch(groupSlug: ComputedRef<string>): RecipeExplorerSearchState {
|
||||||
|
const key = groupSlug.value;
|
||||||
|
|
||||||
|
if (!memo[key]) {
|
||||||
|
memo[key] = createRecipeExplorerSearchState(groupSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return memo[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearRecipeExplorerSearchState(groupSlug: string) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete memo[groupSlug];
|
||||||
|
}
|
||||||
@@ -5,26 +5,31 @@ const userRatings = ref<UserRatingSummary[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
|
|
||||||
export const useUserSelfRatings = function () {
|
const $auth = useMealieAuth();
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const api = useUserApi();
|
|
||||||
|
|
||||||
|
export const useUserSelfRatings = function () {
|
||||||
async function refreshUserRatings() {
|
async function refreshUserRatings() {
|
||||||
if (!$auth.user.value || loading.value) {
|
if (!$auth.user.value || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
const api = useUserApi();
|
||||||
|
|
||||||
const { data } = await api.users.getSelfRatings();
|
const { data } = await api.users.getSelfRatings();
|
||||||
userRatings.value = data?.ratings || [];
|
userRatings.value = data?.ratings || [];
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
|
async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
const api = useUserApi();
|
||||||
|
|
||||||
const userId = $auth.user.value?.id || "";
|
const userId = $auth.user.value?.id || "";
|
||||||
await api.users.setRating(userId, slug, rating, isFavorite);
|
await api.users.setRating(userId, slug, rating, isFavorite);
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
await refreshUserRatings();
|
await refreshUserRatings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,99 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
// CODE_GEN_ID: DATE_LOCALES
|
||||||
|
import * as afZA from "./lang/dateTimeFormats/af-ZA.json";
|
||||||
|
import * as arSA from "./lang/dateTimeFormats/ar-SA.json";
|
||||||
|
import * as bgBG from "./lang/dateTimeFormats/bg-BG.json";
|
||||||
|
import * as caES from "./lang/dateTimeFormats/ca-ES.json";
|
||||||
|
import * as csCZ from "./lang/dateTimeFormats/cs-CZ.json";
|
||||||
|
import * as daDK from "./lang/dateTimeFormats/da-DK.json";
|
||||||
|
import * as deDE from "./lang/dateTimeFormats/de-DE.json";
|
||||||
|
import * as elGR from "./lang/dateTimeFormats/el-GR.json";
|
||||||
|
import * as enGB from "./lang/dateTimeFormats/en-GB.json";
|
||||||
|
import * as enUS from "./lang/dateTimeFormats/en-US.json";
|
||||||
|
import * as esES from "./lang/dateTimeFormats/es-ES.json";
|
||||||
|
import * as etEE from "./lang/dateTimeFormats/et-EE.json";
|
||||||
|
import * as fiFI from "./lang/dateTimeFormats/fi-FI.json";
|
||||||
|
import * as frBE from "./lang/dateTimeFormats/fr-BE.json";
|
||||||
|
import * as frCA from "./lang/dateTimeFormats/fr-CA.json";
|
||||||
|
import * as frFR from "./lang/dateTimeFormats/fr-FR.json";
|
||||||
|
import * as glES from "./lang/dateTimeFormats/gl-ES.json";
|
||||||
|
import * as heIL from "./lang/dateTimeFormats/he-IL.json";
|
||||||
|
import * as hrHR from "./lang/dateTimeFormats/hr-HR.json";
|
||||||
|
import * as huHU from "./lang/dateTimeFormats/hu-HU.json";
|
||||||
|
import * as isIS from "./lang/dateTimeFormats/is-IS.json";
|
||||||
|
import * as itIT from "./lang/dateTimeFormats/it-IT.json";
|
||||||
|
import * as jaJP from "./lang/dateTimeFormats/ja-JP.json";
|
||||||
|
import * as koKR from "./lang/dateTimeFormats/ko-KR.json";
|
||||||
|
import * as ltLT from "./lang/dateTimeFormats/lt-LT.json";
|
||||||
|
import * as lvLV from "./lang/dateTimeFormats/lv-LV.json";
|
||||||
|
import * as nlNL from "./lang/dateTimeFormats/nl-NL.json";
|
||||||
|
import * as noNO from "./lang/dateTimeFormats/no-NO.json";
|
||||||
|
import * as plPL from "./lang/dateTimeFormats/pl-PL.json";
|
||||||
|
import * as ptBR from "./lang/dateTimeFormats/pt-BR.json";
|
||||||
|
import * as ptPT from "./lang/dateTimeFormats/pt-PT.json";
|
||||||
|
import * as roRO from "./lang/dateTimeFormats/ro-RO.json";
|
||||||
|
import * as ruRU from "./lang/dateTimeFormats/ru-RU.json";
|
||||||
|
import * as skSK from "./lang/dateTimeFormats/sk-SK.json";
|
||||||
|
import * as slSI from "./lang/dateTimeFormats/sl-SI.json";
|
||||||
|
import * as srSP from "./lang/dateTimeFormats/sr-SP.json";
|
||||||
|
import * as svSE from "./lang/dateTimeFormats/sv-SE.json";
|
||||||
|
import * as trTR from "./lang/dateTimeFormats/tr-TR.json";
|
||||||
|
import * as ukUA from "./lang/dateTimeFormats/uk-UA.json";
|
||||||
|
import * as viVN from "./lang/dateTimeFormats/vi-VN.json";
|
||||||
|
import * as zhCN from "./lang/dateTimeFormats/zh-CN.json";
|
||||||
|
import * as zhTW from "./lang/dateTimeFormats/zh-TW.json";
|
||||||
|
|
||||||
const datetimeFormats = {
|
const datetimeFormats = {
|
||||||
// CODE_GEN_ID: DATE_LOCALES
|
"af-ZA": afZA,
|
||||||
"af-ZA": require("./lang/dateTimeFormats/af-ZA.json"),
|
"ar-SA": arSA,
|
||||||
"ar-SA": require("./lang/dateTimeFormats/ar-SA.json"),
|
"bg-BG": bgBG,
|
||||||
"bg-BG": require("./lang/dateTimeFormats/bg-BG.json"),
|
"ca-ES": caES,
|
||||||
"ca-ES": require("./lang/dateTimeFormats/ca-ES.json"),
|
"cs-CZ": csCZ,
|
||||||
"cs-CZ": require("./lang/dateTimeFormats/cs-CZ.json"),
|
"da-DK": daDK,
|
||||||
"da-DK": require("./lang/dateTimeFormats/da-DK.json"),
|
"de-DE": deDE,
|
||||||
"de-DE": require("./lang/dateTimeFormats/de-DE.json"),
|
"el-GR": elGR,
|
||||||
"el-GR": require("./lang/dateTimeFormats/el-GR.json"),
|
"en-GB": enGB,
|
||||||
"en-GB": require("./lang/dateTimeFormats/en-GB.json"),
|
"en-US": enUS,
|
||||||
"en-US": require("./lang/dateTimeFormats/en-US.json"),
|
"es-ES": esES,
|
||||||
"es-ES": require("./lang/dateTimeFormats/es-ES.json"),
|
"et-EE": etEE,
|
||||||
"et-EE": require("./lang/dateTimeFormats/et-EE.json"),
|
"fi-FI": fiFI,
|
||||||
"fi-FI": require("./lang/dateTimeFormats/fi-FI.json"),
|
"fr-BE": frBE,
|
||||||
"fr-BE": require("./lang/dateTimeFormats/fr-BE.json"),
|
"fr-CA": frCA,
|
||||||
"fr-CA": require("./lang/dateTimeFormats/fr-CA.json"),
|
"fr-FR": frFR,
|
||||||
"fr-FR": require("./lang/dateTimeFormats/fr-FR.json"),
|
"gl-ES": glES,
|
||||||
"gl-ES": require("./lang/dateTimeFormats/gl-ES.json"),
|
"he-IL": heIL,
|
||||||
"he-IL": require("./lang/dateTimeFormats/he-IL.json"),
|
"hr-HR": hrHR,
|
||||||
"hr-HR": require("./lang/dateTimeFormats/hr-HR.json"),
|
"hu-HU": huHU,
|
||||||
"hu-HU": require("./lang/dateTimeFormats/hu-HU.json"),
|
"is-IS": isIS,
|
||||||
"is-IS": require("./lang/dateTimeFormats/is-IS.json"),
|
"it-IT": itIT,
|
||||||
"it-IT": require("./lang/dateTimeFormats/it-IT.json"),
|
"ja-JP": jaJP,
|
||||||
"ja-JP": require("./lang/dateTimeFormats/ja-JP.json"),
|
"ko-KR": koKR,
|
||||||
"ko-KR": require("./lang/dateTimeFormats/ko-KR.json"),
|
"lt-LT": ltLT,
|
||||||
"lt-LT": require("./lang/dateTimeFormats/lt-LT.json"),
|
"lv-LV": lvLV,
|
||||||
"lv-LV": require("./lang/dateTimeFormats/lv-LV.json"),
|
"nl-NL": nlNL,
|
||||||
"nl-NL": require("./lang/dateTimeFormats/nl-NL.json"),
|
"no-NO": noNO,
|
||||||
"no-NO": require("./lang/dateTimeFormats/no-NO.json"),
|
"pl-PL": plPL,
|
||||||
"pl-PL": require("./lang/dateTimeFormats/pl-PL.json"),
|
"pt-BR": ptBR,
|
||||||
"pt-BR": require("./lang/dateTimeFormats/pt-BR.json"),
|
"pt-PT": ptPT,
|
||||||
"pt-PT": require("./lang/dateTimeFormats/pt-PT.json"),
|
"ro-RO": roRO,
|
||||||
"ro-RO": require("./lang/dateTimeFormats/ro-RO.json"),
|
"ru-RU": ruRU,
|
||||||
"ru-RU": require("./lang/dateTimeFormats/ru-RU.json"),
|
"sk-SK": skSK,
|
||||||
"sk-SK": require("./lang/dateTimeFormats/sk-SK.json"),
|
"sl-SI": slSI,
|
||||||
"sl-SI": require("./lang/dateTimeFormats/sl-SI.json"),
|
"sr-SP": srSP,
|
||||||
"sr-SP": require("./lang/dateTimeFormats/sr-SP.json"),
|
"sv-SE": svSE,
|
||||||
"sv-SE": require("./lang/dateTimeFormats/sv-SE.json"),
|
"tr-TR": trTR,
|
||||||
"tr-TR": require("./lang/dateTimeFormats/tr-TR.json"),
|
"uk-UA": ukUA,
|
||||||
"uk-UA": require("./lang/dateTimeFormats/uk-UA.json"),
|
"vi-VN": viVN,
|
||||||
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
|
"zh-CN": zhCN,
|
||||||
"zh-CN": require("./lang/dateTimeFormats/zh-CN.json"),
|
"zh-TW": zhTW,
|
||||||
"zh-TW": require("./lang/dateTimeFormats/zh-TW.json"),
|
|
||||||
// END: DATE_LOCALES
|
|
||||||
};
|
};
|
||||||
|
// END: DATE_LOCALES
|
||||||
|
|
||||||
export default defineI18nConfig(() => {
|
export default defineI18nConfig(() => {
|
||||||
return {
|
return {
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: "en-US",
|
locale: "en-US",
|
||||||
availableLocales: Object.keys(datetimeFormats),
|
availableLocales: Object.keys(datetimeFormats),
|
||||||
datetimeFormats,
|
datetimeFormats: datetimeFormats as any,
|
||||||
fallbackLocale: "en-US",
|
fallbackLocale: "en-US",
|
||||||
fallbackWarn: true,
|
fallbackWarn: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Sien oorspronklike teks",
|
"see-original-text": "Sien oorspronklike teks",
|
||||||
"original-text-with-value": "Oorspronklike teks: {originalText}",
|
"original-text-with-value": "Oorspronklike teks: {originalText}",
|
||||||
"ingredient-linker": "Bestanddele koppelaar",
|
"ingredient-linker": "Bestanddele koppelaar",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Gekoppel aan 'n ander stap",
|
"linked-to-other-step": "Gekoppel aan 'n ander stap",
|
||||||
"auto": "Outomaties",
|
"auto": "Outomaties",
|
||||||
"cook-mode": "Kook modus",
|
"cook-mode": "Kook modus",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "عرض النص الأصلي",
|
"see-original-text": "عرض النص الأصلي",
|
||||||
"original-text-with-value": "النص الأصلي: {originalText}",
|
"original-text-with-value": "النص الأصلي: {originalText}",
|
||||||
"ingredient-linker": "رابط المكون",
|
"ingredient-linker": "رابط المكون",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "مرتبط بخطوة أخرى",
|
"linked-to-other-step": "مرتبط بخطوة أخرى",
|
||||||
"auto": "تلقائي",
|
"auto": "تلقائي",
|
||||||
"cook-mode": "وضع الطبخ",
|
"cook-mode": "وضع الطبخ",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Виж оригиналния текст",
|
"see-original-text": "Виж оригиналния текст",
|
||||||
"original-text-with-value": "Оригинален текст: {originalText}",
|
"original-text-with-value": "Оригинален текст: {originalText}",
|
||||||
"ingredient-linker": "Инструмент за свързване на съставки",
|
"ingredient-linker": "Инструмент за свързване на съставки",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Свързано към друга стъпка",
|
"linked-to-other-step": "Свързано към друга стъпка",
|
||||||
"auto": "Автоматично",
|
"auto": "Автоматично",
|
||||||
"cook-mode": "Режим на готвене",
|
"cook-mode": "Режим на готвене",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Mostra el text original",
|
"see-original-text": "Mostra el text original",
|
||||||
"original-text-with-value": "Text original: {originalText}",
|
"original-text-with-value": "Text original: {originalText}",
|
||||||
"ingredient-linker": "Enllaça ingredients",
|
"ingredient-linker": "Enllaça ingredients",
|
||||||
|
"unlinked": "No enllaçada",
|
||||||
"linked-to-other-step": "Enllaça a un altre pas",
|
"linked-to-other-step": "Enllaça a un altre pas",
|
||||||
"auto": "Automàtic",
|
"auto": "Automàtic",
|
||||||
"cook-mode": "Mode \"cuinant\"",
|
"cook-mode": "Mode \"cuinant\"",
|
||||||
|
|||||||
@@ -549,7 +549,7 @@
|
|||||||
"failed-to-add-recipes-to-list": "Přidání receptu do seznamu se nezdařilo",
|
"failed-to-add-recipes-to-list": "Přidání receptu do seznamu se nezdařilo",
|
||||||
"failed-to-add-recipe-to-mealplan": "Přidání receptu do jídelníčku selhalo",
|
"failed-to-add-recipe-to-mealplan": "Přidání receptu do jídelníčku selhalo",
|
||||||
"failed-to-add-to-list": "Přidání do seznamu se nezdařilo",
|
"failed-to-add-to-list": "Přidání do seznamu se nezdařilo",
|
||||||
"yield": "Úroda",
|
"yield": "Výnos",
|
||||||
"yields-amount-with-text": "Pro {amount} {text}",
|
"yields-amount-with-text": "Pro {amount} {text}",
|
||||||
"yield-text": "Text porcí",
|
"yield-text": "Text porcí",
|
||||||
"quantity": "Množství",
|
"quantity": "Množství",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Zobrazit původní text",
|
"see-original-text": "Zobrazit původní text",
|
||||||
"original-text-with-value": "Původní text: {originalText}",
|
"original-text-with-value": "Původní text: {originalText}",
|
||||||
"ingredient-linker": "Propojení ingrediencí",
|
"ingredient-linker": "Propojení ingrediencí",
|
||||||
|
"unlinked": "Zatím nepropojeno",
|
||||||
"linked-to-other-step": "Propojeno s jiným krokem receptu",
|
"linked-to-other-step": "Propojeno s jiným krokem receptu",
|
||||||
"auto": "Automaticky",
|
"auto": "Automaticky",
|
||||||
"cook-mode": "Režim vaření",
|
"cook-mode": "Režim vaření",
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
"development": "Udvikling",
|
"development": "Udvikling",
|
||||||
"docs": "Dokumenter",
|
"docs": "Dokumenter",
|
||||||
"download-log": "Download log",
|
"download-log": "Download log",
|
||||||
"download-recipe-json": "Sidst skrabet JSON",
|
"download-recipe-json": "Senest hentede JSON",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"log-lines": "Log linjer",
|
"log-lines": "Log-linjer",
|
||||||
"not-demo": "Ikke demo",
|
"not-demo": "Ikke demo",
|
||||||
"portfolio": "Portefølje",
|
"portfolio": "Portefølje",
|
||||||
"production": "Produktion",
|
"production": "Produktion",
|
||||||
@@ -39,13 +39,13 @@
|
|||||||
"category": {
|
"category": {
|
||||||
"categories": "Kategorier",
|
"categories": "Kategorier",
|
||||||
"category-created": "Kategori oprettet",
|
"category-created": "Kategori oprettet",
|
||||||
"category-creation-failed": "Oprettelse af kategorien fejlede",
|
"category-creation-failed": "Oprettelse af kategorien mislykkedes",
|
||||||
"category-deleted": "Kategori slettet",
|
"category-deleted": "Kategori slettet",
|
||||||
"category-deletion-failed": "Sletning af kategori fejlede",
|
"category-deletion-failed": "Sletning af kategori mislykkedes",
|
||||||
"category-filter": "Kategorifilter",
|
"category-filter": "Kategorifilter",
|
||||||
"category-update-failed": "Kategoriopdatering fejlede",
|
"category-update-failed": "Opdatering af kategori mislykkedes",
|
||||||
"category-updated": "Kategori opdateret",
|
"category-updated": "Kategori opdateret",
|
||||||
"uncategorized-count": "Ukategoriseret {count}",
|
"uncategorized-count": "Ikke kategoriseret {count}",
|
||||||
"create-a-category": "Opret en kategori",
|
"create-a-category": "Opret en kategori",
|
||||||
"category-name": "Kategorinavn",
|
"category-name": "Kategorinavn",
|
||||||
"category": "Kategori"
|
"category": "Kategori"
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
"events": {
|
"events": {
|
||||||
"apprise-url": "Apprise URL",
|
"apprise-url": "Apprise URL",
|
||||||
"database": "Database",
|
"database": "Database",
|
||||||
"delete-event": "Slet event",
|
"delete-event": "Slet hændelse",
|
||||||
"event-delete-confirmation": "Er du sikker på, at du vil slette denne hændelse?",
|
"event-delete-confirmation": "Er du sikker på, at du vil slette denne begivenhed?",
|
||||||
"event-deleted": "Hændelse slettet",
|
"event-deleted": "Hændelse slettet",
|
||||||
"event-updated": "Hændelse opdateret",
|
"event-updated": "Hændelse opdateret",
|
||||||
"new-notification-form-description": "Mealie bruger Apprise-biblioteket for at generere notifikationer. De giver mange muligheder for notifikationer til tjenester. Kig i deres wiki for en gennemgående guide til, hvordan en URL oprettes i din situation. Hvis muligt, kan valget af din type af notifikation omfatte flere ekstrafunktioner.",
|
"new-notification-form-description": "Mealie bruger Apprise-biblioteket for at generere notifikationer. De giver mange muligheder for notifikationer til tjenester. Kig i deres wiki for en gennemgående guide til, hvordan en URL oprettes i din situation. Hvis muligt, kan valget af din type af notifikation omfatte flere ekstrafunktioner.",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Vis den oprindelige tekst",
|
"see-original-text": "Vis den oprindelige tekst",
|
||||||
"original-text-with-value": "Oprindelig tekst: {originalText}",
|
"original-text-with-value": "Oprindelig tekst: {originalText}",
|
||||||
"ingredient-linker": "Ingrediens-linker",
|
"ingredient-linker": "Ingrediens-linker",
|
||||||
|
"unlinked": "Ikke forbundet endnu",
|
||||||
"linked-to-other-step": "Linket til andet trin",
|
"linked-to-other-step": "Linket til andet trin",
|
||||||
"auto": "Automatisk",
|
"auto": "Automatisk",
|
||||||
"cook-mode": "Tilberedningsvisning",
|
"cook-mode": "Tilberedningsvisning",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Neue Benachrichtigung",
|
"new-notification": "Neue Benachrichtigung",
|
||||||
"event-notifiers": "Ereignis-Benachrichtigungen",
|
"event-notifiers": "Ereignis-Benachrichtigungen",
|
||||||
"apprise-url-skipped-if-blank": "Apprise-URL (wird übersprungen, wenn leer)",
|
"apprise-url-skipped-if-blank": "Apprise-URL (wird übersprungen, wenn leer)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Da Apprise-URLs normalerweise sensible Informationen enthalten, wird dieses Feld während der Bearbeitung absichtlich leer gelassen. Wenn Sie die URL aktualisieren möchten, geben Sie hier die neue ein. Andernfalls lassen Sie diese leer, um die aktuelle URL zu behalten.",
|
||||||
"enable-notifier": "Benachrichtigen aktivieren",
|
"enable-notifier": "Benachrichtigen aktivieren",
|
||||||
"what-events": "Welche Ereignisse soll diese Benachrichtigung abonnieren?",
|
"what-events": "Welche Ereignisse soll diese Benachrichtigung abonnieren?",
|
||||||
"user-events": "Benutzer-Ereignisse",
|
"user-events": "Benutzer-Ereignisse",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Originaltext anzeigen",
|
"see-original-text": "Originaltext anzeigen",
|
||||||
"original-text-with-value": "Originaltext: {originalText}",
|
"original-text-with-value": "Originaltext: {originalText}",
|
||||||
"ingredient-linker": "Zutaten-Verlinkung",
|
"ingredient-linker": "Zutaten-Verlinkung",
|
||||||
|
"unlinked": "Nicht verbunden",
|
||||||
"linked-to-other-step": "In anderem Schritt verlinkt",
|
"linked-to-other-step": "In anderem Schritt verlinkt",
|
||||||
"auto": "Automatisch",
|
"auto": "Automatisch",
|
||||||
"cook-mode": "Koch-Modus",
|
"cook-mode": "Koch-Modus",
|
||||||
@@ -1169,7 +1170,7 @@
|
|||||||
"group-details": "Gruppendetails",
|
"group-details": "Gruppendetails",
|
||||||
"group-details-description": "Bevor du ein Konto erstellst, musst du eine Gruppe erstellen. Deine Gruppe wird nur dich enthalten, aber du kannst andere später einladen. Mitglieder in deiner Gruppe können Essenspläne, Einkaufslisten, Rezepte und vieles mehr teilen!",
|
"group-details-description": "Bevor du ein Konto erstellst, musst du eine Gruppe erstellen. Deine Gruppe wird nur dich enthalten, aber du kannst andere später einladen. Mitglieder in deiner Gruppe können Essenspläne, Einkaufslisten, Rezepte und vieles mehr teilen!",
|
||||||
"use-seed-data": "Musterdaten",
|
"use-seed-data": "Musterdaten",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
"use-seed-data-description": "Mealie enthält eine Sammlung von Lebensmitteln, Einheiten und Labels, die verwendet werden können, um deine Gruppe mit hilfreichen Daten für die Organisation deiner Rezepte zu füllen. Diese werden in die Sprache übersetzt, die Sie gerade ausgewählt haben. Sie können diese Daten jederzeit später hinzufügen oder ändern.",
|
||||||
"account-details": "Kontoinformationen"
|
"account-details": "Kontoinformationen"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -557,10 +557,11 @@
|
|||||||
"press-enter-to-create": "Πατήστε Enter για δημιουργία",
|
"press-enter-to-create": "Πατήστε Enter για δημιουργία",
|
||||||
"choose-food": "Επιλέξτε τρόφιμο",
|
"choose-food": "Επιλέξτε τρόφιμο",
|
||||||
"notes": "Σημειώσεις",
|
"notes": "Σημειώσεις",
|
||||||
"toggle-section": "Εναλλαγή τμημάτων",
|
"toggle-section": "Ενεργοποίηση/απενεργοποίηση τμήματος",
|
||||||
"see-original-text": "Προβολή Αρχικού Κειμένου",
|
"see-original-text": "Προβολή Αρχικού Κειμένου",
|
||||||
"original-text-with-value": "Αρχικό Κείμενο: {originalText}",
|
"original-text-with-value": "Αρχικό Κείμενο: {originalText}",
|
||||||
"ingredient-linker": "Συνδυασμός συστατικών",
|
"ingredient-linker": "Συνδυασμός συστατικών",
|
||||||
|
"unlinked": "Δεν έχει συνδεθεί ακόμα",
|
||||||
"linked-to-other-step": "Συνδεδεμένο με άλλο βήμα",
|
"linked-to-other-step": "Συνδεδεμένο με άλλο βήμα",
|
||||||
"auto": "Αυτόματο",
|
"auto": "Αυτόματο",
|
||||||
"cook-mode": "Λειτουργία Μαγειρέματος",
|
"cook-mode": "Λειτουργία Μαγειρέματος",
|
||||||
@@ -581,7 +582,7 @@
|
|||||||
"open-timeline": "Ανοιγμα χρονολόγιου",
|
"open-timeline": "Ανοιγμα χρονολόγιου",
|
||||||
"made-this": "Το έφτιαξα",
|
"made-this": "Το έφτιαξα",
|
||||||
"how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;",
|
"how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;",
|
||||||
"user-made-this": "Ο/η {user} το έφτιαξε αυτό",
|
"user-made-this": "Ο/η {user} έφτιαξε αυτό",
|
||||||
"added-to-timeline": "Προστέθηκε στο χρονολόγιο",
|
"added-to-timeline": "Προστέθηκε στο χρονολόγιο",
|
||||||
"failed-to-add-to-timeline": "Αποτυχία προσθήκης στο χρονολόγιο",
|
"failed-to-add-to-timeline": "Αποτυχία προσθήκης στο χρονολόγιο",
|
||||||
"failed-to-update-recipe": "Αποτυχία ενημέρωσης συνταγής",
|
"failed-to-update-recipe": "Αποτυχία ενημέρωσης συνταγής",
|
||||||
@@ -702,8 +703,8 @@
|
|||||||
"include": "Συμπερίληψη",
|
"include": "Συμπερίληψη",
|
||||||
"max-results": "Μέγιστα Αποτελέσματα",
|
"max-results": "Μέγιστα Αποτελέσματα",
|
||||||
"or": "Ή",
|
"or": "Ή",
|
||||||
"has-any": "Περιέχει",
|
"has-any": "Περιέχει τουλάχιστον",
|
||||||
"has-all": "Περιέχει τα πάντα",
|
"has-all": "Περιέχει όλα τα παρακάτω",
|
||||||
"clear-selection": "Απαλοιφή επιλογής",
|
"clear-selection": "Απαλοιφή επιλογής",
|
||||||
"results": "Αποτελέσματα",
|
"results": "Αποτελέσματα",
|
||||||
"search": "Αναζήτηση",
|
"search": "Αναζήτηση",
|
||||||
@@ -885,7 +886,7 @@
|
|||||||
"copy-as-text": "Αντιγραφή ως κείμενο",
|
"copy-as-text": "Αντιγραφή ως κείμενο",
|
||||||
"copy-as-markdown": "Αντιγραφή ως Markdown",
|
"copy-as-markdown": "Αντιγραφή ως Markdown",
|
||||||
"delete-checked": "Διαγραφή επιλεγμένων",
|
"delete-checked": "Διαγραφή επιλεγμένων",
|
||||||
"toggle-label-sort": "Εναλλαγή ταξινόμησης ετικετών",
|
"toggle-label-sort": "Ενεργοποίηση/απενεργοποίηση ταξινόμησης ετικετών",
|
||||||
"reorder-labels": "Αναδιάταξη ετικετών",
|
"reorder-labels": "Αναδιάταξη ετικετών",
|
||||||
"uncheck-all-items": "Αποεπιλογή όλων των αντικειμένων",
|
"uncheck-all-items": "Αποεπιλογή όλων των αντικειμένων",
|
||||||
"check-all-items": "Επιλογή όλων των αντικειμένων",
|
"check-all-items": "Επιλογή όλων των αντικειμένων",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "See Original Text",
|
"see-original-text": "See Original Text",
|
||||||
"original-text-with-value": "Original Text: {originalText}",
|
"original-text-with-value": "Original Text: {originalText}",
|
||||||
"ingredient-linker": "Ingredient Linker",
|
"ingredient-linker": "Ingredient Linker",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Linked to other step",
|
"linked-to-other-step": "Linked to other step",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Cook Mode",
|
"cook-mode": "Cook Mode",
|
||||||
@@ -1120,10 +1121,10 @@
|
|||||||
"data-exports-description": "This section provides links to available exports that are ready to download. These exports do expire, so be sure to grab them while they're still available.",
|
"data-exports-description": "This section provides links to available exports that are ready to download. These exports do expire, so be sure to grab them while they're still available.",
|
||||||
"data-exports": "Data Exports",
|
"data-exports": "Data Exports",
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"categorize": "Categorize",
|
"categorize": "Categorise",
|
||||||
"update-settings": "Update Settings",
|
"update-settings": "Update Settings",
|
||||||
"tag-recipes": "Tag Recipes",
|
"tag-recipes": "Tag Recipes",
|
||||||
"categorize-recipes": "Categorize Recipes",
|
"categorize-recipes": "Categorise Recipes",
|
||||||
"export-recipes": "Export Recipes",
|
"export-recipes": "Export Recipes",
|
||||||
"delete-recipes": "Delete Recipes",
|
"delete-recipes": "Delete Recipes",
|
||||||
"source-unit-will-be-deleted": "Source Unit will be deleted"
|
"source-unit-will-be-deleted": "Source Unit will be deleted"
|
||||||
@@ -1169,7 +1170,7 @@
|
|||||||
"group-details": "Group Details",
|
"group-details": "Group Details",
|
||||||
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
"group-details-description": "Before you create an account you'll need to create a group. Your group will only contain you, but you'll be able to invite others later. Members in your group can share meal plans, shopping lists, recipes, and more!",
|
||||||
"use-seed-data": "Use Seed Data",
|
"use-seed-data": "Use Seed Data",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organising your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
||||||
"account-details": "Account Details"
|
"account-details": "Account Details"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
@@ -1359,13 +1360,13 @@
|
|||||||
},
|
},
|
||||||
"cookbook": {
|
"cookbook": {
|
||||||
"cookbooks": "Cookbooks",
|
"cookbooks": "Cookbooks",
|
||||||
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
|
"description": "Cookbooks are another way to organise recipes by creating cross-sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the sidebar and all the recipes with the filters chosen will be displayed in the cookbook.",
|
||||||
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
|
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
|
||||||
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",
|
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",
|
||||||
"public-cookbook": "Public Cookbook",
|
"public-cookbook": "Public Cookbook",
|
||||||
"public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.",
|
"public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.",
|
||||||
"filter-options": "Filter Options",
|
"filter-options": "Filter Options",
|
||||||
"filter-options-description": "When require all is selected the cookbook will only include recipes that have all of the items selected. This applies to each subset of selectors and not a cross section of the selected items.",
|
"filter-options-description": "When require all is selected the cookbook will only include recipes that have all of the items selected. This applies to each subset of selectors and not a cross-section of the selected items.",
|
||||||
"require-all-categories": "Require All Categories",
|
"require-all-categories": "Require All Categories",
|
||||||
"require-all-tags": "Require All Tags",
|
"require-all-tags": "Require All Tags",
|
||||||
"require-all-tools": "Require All Tools",
|
"require-all-tools": "Require All Tools",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "See Original Text",
|
"see-original-text": "See Original Text",
|
||||||
"original-text-with-value": "Original Text: {originalText}",
|
"original-text-with-value": "Original Text: {originalText}",
|
||||||
"ingredient-linker": "Ingredient Linker",
|
"ingredient-linker": "Ingredient Linker",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Linked to other step",
|
"linked-to-other-step": "Linked to other step",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Cook Mode",
|
"cook-mode": "Cook Mode",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Nueva notificación",
|
"new-notification": "Nueva notificación",
|
||||||
"event-notifiers": "Notificaciones de eventos",
|
"event-notifiers": "Notificaciones de eventos",
|
||||||
"apprise-url-skipped-if-blank": "URL de Apprise (omitida si está en blanco)",
|
"apprise-url-skipped-if-blank": "URL de Apprise (omitida si está en blanco)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Dado que las URL de Apprise suelen contener información confidencial, este campo se deja en blanco intencionalmente durante la edición. Si desea actualizar la URL introdúzcala aquí, de lo contrario, déjelo en blanco para conservar la URL actual.\n",
|
||||||
"enable-notifier": "Habilitar notificador",
|
"enable-notifier": "Habilitar notificador",
|
||||||
"what-events": "¿A qué eventos debe suscribirse este notificador?",
|
"what-events": "¿A qué eventos debe suscribirse este notificador?",
|
||||||
"user-events": "Eventos de los usuarios",
|
"user-events": "Eventos de los usuarios",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Mostrar Texto Original",
|
"see-original-text": "Mostrar Texto Original",
|
||||||
"original-text-with-value": "Texto original: {originalText}",
|
"original-text-with-value": "Texto original: {originalText}",
|
||||||
"ingredient-linker": "Vincular ingredientes",
|
"ingredient-linker": "Vincular ingredientes",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Enlazado a otro paso",
|
"linked-to-other-step": "Enlazado a otro paso",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Modo Cocinar",
|
"cook-mode": "Modo Cocinar",
|
||||||
@@ -1169,7 +1170,7 @@
|
|||||||
"group-details": "Detalles del grupo",
|
"group-details": "Detalles del grupo",
|
||||||
"group-details-description": "Antes de crear una cuenta, debe crear un grupo. En el grupo sólo estará usted, pero puede invitar a otros más tarde. Los miembros de un grupo pueden compartir menús, listas de la compra, recetas y más...",
|
"group-details-description": "Antes de crear una cuenta, debe crear un grupo. En el grupo sólo estará usted, pero puede invitar a otros más tarde. Los miembros de un grupo pueden compartir menús, listas de la compra, recetas y más...",
|
||||||
"use-seed-data": "Utilizar datos de ejemplo",
|
"use-seed-data": "Utilizar datos de ejemplo",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
"use-seed-data-description": "Mealie incluye una colección de alimentos, unidades y etiquetas que puedes usar para completar tu grupo con datos útiles para organizar tus recetas. Estos datos están traducidos al idioma que hayas seleccionado. Siempre puedes añadir o modificar estos datos más adelante.\n",
|
||||||
"account-details": "Información de la cuenta"
|
"account-details": "Información de la cuenta"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Vaata originaalteksti",
|
"see-original-text": "Vaata originaalteksti",
|
||||||
"original-text-with-value": "Originaaltekst: {originalText}",
|
"original-text-with-value": "Originaaltekst: {originalText}",
|
||||||
"ingredient-linker": "Koostisosa linkija",
|
"ingredient-linker": "Koostisosa linkija",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Lingitud järgmise sammuga",
|
"linked-to-other-step": "Lingitud järgmise sammuga",
|
||||||
"auto": "Automaatne",
|
"auto": "Automaatne",
|
||||||
"cook-mode": "Küpsetusviis",
|
"cook-mode": "Küpsetusviis",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Katso Alkuperäinen Teksti",
|
"see-original-text": "Katso Alkuperäinen Teksti",
|
||||||
"original-text-with-value": "Alkuperäinen Teksti: {originalText}",
|
"original-text-with-value": "Alkuperäinen Teksti: {originalText}",
|
||||||
"ingredient-linker": "Ainesosan linkittäjä",
|
"ingredient-linker": "Ainesosan linkittäjä",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Linkitetty toiseen vaiheeseen",
|
"linked-to-other-step": "Linkitetty toiseen vaiheeseen",
|
||||||
"auto": "Automaattinen",
|
"auto": "Automaattinen",
|
||||||
"cook-mode": "Kokkitila",
|
"cook-mode": "Kokkitila",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Nouvelle notification",
|
"new-notification": "Nouvelle notification",
|
||||||
"event-notifiers": "Notifications d'événements",
|
"event-notifiers": "Notifications d'événements",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Comme les URL Apprise contiennent généralement des informations sensibles, ce champ est laissé intentionnellement vide lors de l'édition. Si vous souhaitez mettre à jour l'URL, veuillez entrer la nouvelle URL ici, sinon laisser vide pour conserver l'URL courante.",
|
||||||
"enable-notifier": "Activer la notification",
|
"enable-notifier": "Activer la notification",
|
||||||
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
||||||
"user-events": "Evénements utilisateur",
|
"user-events": "Evénements utilisateur",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"category-events": "Événements de catégories",
|
"category-events": "Événements de catégories",
|
||||||
"when-a-new-user-joins-your-group": "Lorsqu'un nouvel utilisateur rejoint votre groupe",
|
"when-a-new-user-joins-your-group": "Lorsqu'un nouvel utilisateur rejoint votre groupe",
|
||||||
"recipe-events": "Événements de recette",
|
"recipe-events": "Événements de recette",
|
||||||
"label-events": "Label Events"
|
"label-events": "Étiquette des événements"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Afficher le texte original",
|
"see-original-text": "Afficher le texte original",
|
||||||
"original-text-with-value": "Texte original : {originalText}",
|
"original-text-with-value": "Texte original : {originalText}",
|
||||||
"ingredient-linker": "Liaison d’ingrédients",
|
"ingredient-linker": "Liaison d’ingrédients",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Déjà associé à une autre étape",
|
"linked-to-other-step": "Déjà associé à une autre étape",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Mode Cuisine",
|
"cook-mode": "Mode Cuisine",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Nouvelle notification",
|
"new-notification": "Nouvelle notification",
|
||||||
"event-notifiers": "Notifications d'événements",
|
"event-notifiers": "Notifications d'événements",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignoré si vide)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Comme les URL Apprise contiennent généralement des informations sensibles, ce champ est laissé intentionnellement vide lors de l'édition. Si vous souhaitez mettre à jour l'URL, veuillez entrer la nouvelle URL ici, sinon laisser vide pour conserver l'URL courante.",
|
||||||
"enable-notifier": "Activer la notification",
|
"enable-notifier": "Activer la notification",
|
||||||
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
"what-events": "À quels événements cette notification doit-elle s'abonner ?",
|
||||||
"user-events": "Événements de l'utilisateur",
|
"user-events": "Événements de l'utilisateur",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"category-events": "Événements de catégories",
|
"category-events": "Événements de catégories",
|
||||||
"when-a-new-user-joins-your-group": "Lorsqu'un nouvel utilisateur rejoint votre groupe",
|
"when-a-new-user-joins-your-group": "Lorsqu'un nouvel utilisateur rejoint votre groupe",
|
||||||
"recipe-events": "Événements de recette",
|
"recipe-events": "Événements de recette",
|
||||||
"label-events": "Label Events"
|
"label-events": "Étiquette des événements"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Afficher le texte original",
|
"see-original-text": "Afficher le texte original",
|
||||||
"original-text-with-value": "Texte original: {originalText}",
|
"original-text-with-value": "Texte original: {originalText}",
|
||||||
"ingredient-linker": "Association d’ingrédients",
|
"ingredient-linker": "Association d’ingrédients",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Lié à une autre étape",
|
"linked-to-other-step": "Lié à une autre étape",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Mode Cuisine",
|
"cook-mode": "Mode Cuisine",
|
||||||
@@ -675,8 +676,8 @@
|
|||||||
"upload-another-image": "Télécharger une autre image",
|
"upload-another-image": "Télécharger une autre image",
|
||||||
"upload-images": "Télécharger des images",
|
"upload-images": "Télécharger des images",
|
||||||
"upload-more-images": "Télécharger d'autres images",
|
"upload-more-images": "Télécharger d'autres images",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Définir comme image de couverture de la recette",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Image de couverture"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Recherche de recette",
|
"recipe-finder": "Recherche de recette",
|
||||||
@@ -1169,7 +1170,7 @@
|
|||||||
"group-details": "Détails du groupe",
|
"group-details": "Détails du groupe",
|
||||||
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
"group-details-description": "Avant de créer un compte, vous devrez créer un groupe. Votre groupe ne contiendra que vous, mais vous pourrez inviter d’autres personnes plus tard. Les membres de votre groupe peuvent partager leur menu de la semaine, leurs listes d’achat, leurs recettes et plus encore !",
|
||||||
"use-seed-data": "Utiliser l'initialisation de données",
|
"use-seed-data": "Utiliser l'initialisation de données",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
"use-seed-data-description": "Mealie est livrée avec une collection d'aliments, d'unités et d'étiquettes qui peuvent être utilisés pour remplir votre groupe avec des données utiles pour organiser vos recettes. Ceux-ci sont traduits dans la langue que vous avez sélectionnée. Vous pouvez toujours ajouter ou modifier ces données plus tard.",
|
||||||
"account-details": "Détails du compte"
|
"account-details": "Détails du compte"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Afficher le texte original",
|
"see-original-text": "Afficher le texte original",
|
||||||
"original-text-with-value": "Texte original : {originalText}",
|
"original-text-with-value": "Texte original : {originalText}",
|
||||||
"ingredient-linker": "Liaison d’ingrédients",
|
"ingredient-linker": "Liaison d’ingrédients",
|
||||||
|
"unlinked": "Pas encore associée",
|
||||||
"linked-to-other-step": "Déjà associé à une autre étape",
|
"linked-to-other-step": "Déjà associé à une autre étape",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Mode Cuisine",
|
"cook-mode": "Mode Cuisine",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Mostrar Texto Orixinal",
|
"see-original-text": "Mostrar Texto Orixinal",
|
||||||
"original-text-with-value": "Texto Orixinal: {originalText}",
|
"original-text-with-value": "Texto Orixinal: {originalText}",
|
||||||
"ingredient-linker": "Conector de ingredientes",
|
"ingredient-linker": "Conector de ingredientes",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Ligado a outro paso",
|
"linked-to-other-step": "Ligado a outro paso",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Modo Cociñeiro",
|
"cook-mode": "Modo Cociñeiro",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "הטקסט המקורי",
|
"see-original-text": "הטקסט המקורי",
|
||||||
"original-text-with-value": "הטקסט המקורי: {originalText}",
|
"original-text-with-value": "הטקסט המקורי: {originalText}",
|
||||||
"ingredient-linker": "קישוריות רכיבים",
|
"ingredient-linker": "קישוריות רכיבים",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "קשור לצעד אחד",
|
"linked-to-other-step": "קשור לצעד אחד",
|
||||||
"auto": "אוטומטי",
|
"auto": "אוטומטי",
|
||||||
"cook-mode": "מצב בישול",
|
"cook-mode": "מצב בישול",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Nova Obavijest",
|
"new-notification": "Nova Obavijest",
|
||||||
"event-notifiers": "Obavještavatelji Događaja",
|
"event-notifiers": "Obavještavatelji Događaja",
|
||||||
"apprise-url-skipped-if-blank": "Apprise URL (preskočeno ako je prazno)",
|
"apprise-url-skipped-if-blank": "Apprise URL (preskočeno ako je prazno)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Ovo polje je namjerno ostavljeno prazno prilikom uređivanja jer Apprise poveznice tipično sadrže osjetljive informacije. Ako želite promijeniti poveznicu, molimo unesite novu ovdje, inače ostavite prazno da zadržite trenutnu poveznicu.",
|
||||||
"enable-notifier": "Omogući obavještavanje",
|
"enable-notifier": "Omogući obavještavanje",
|
||||||
"what-events": "Na koje događaje bi ovaj obavještavatelj trebao biti pretplaćen?",
|
"what-events": "Na koje događaje bi ovaj obavještavatelj trebao biti pretplaćen?",
|
||||||
"user-events": "Događaji Korisnika",
|
"user-events": "Događaji Korisnika",
|
||||||
@@ -300,12 +300,12 @@
|
|||||||
"household-recipe-preferences": "Postavke recepata u domaćinstvu",
|
"household-recipe-preferences": "Postavke recepata u domaćinstvu",
|
||||||
"default-recipe-preferences-description": "Ovo su zadane postavke, kada se u tvojem domaćinstvu izradi novi recept. Ove postavke se mogu promijeniti za pojedinačne recepte u izborniku postavki recepata.",
|
"default-recipe-preferences-description": "Ovo su zadane postavke, kada se u tvojem domaćinstvu izradi novi recept. Ove postavke se mogu promijeniti za pojedinačne recepte u izborniku postavki recepata.",
|
||||||
"allow-users-outside-of-your-household-to-see-your-recipes": "Dopustite korisnicima izvan vašega kućanstva da vide vaše recepte",
|
"allow-users-outside-of-your-household-to-see-your-recipes": "Dopustite korisnicima izvan vašega kućanstva da vide vaše recepte",
|
||||||
"allow-users-outside-of-your-household-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your household or with a pre-generated private link",
|
"allow-users-outside-of-your-household-to-see-your-recipes-description": "Kada je omogućeno, možete koristiti javnu povezncu dijeljene veze za dijeljenje određenih recepata bez autorizacije korisnika. Kada je onemogućeno, recepte možete dijeliti samo s korisnicima koji su u vašoj grupi ili s prethodno generiranom privatnom vezom",
|
||||||
"household-preferences": "Household Preferences"
|
"household-preferences": "Postavke recepata u domaćinstvu"
|
||||||
},
|
},
|
||||||
"meal-plan": {
|
"meal-plan": {
|
||||||
"create-a-new-meal-plan": "Kreirajte Novi Plan Obroka",
|
"create-a-new-meal-plan": "Kreirajte Novi Plan Obroka",
|
||||||
"update-this-meal-plan": "Update this Meal Plan",
|
"update-this-meal-plan": "Izmijenite ovaj Plan Obroka",
|
||||||
"dinner-this-week": "Večera Ove Sedmice",
|
"dinner-this-week": "Večera Ove Sedmice",
|
||||||
"dinner-today": "Večera Danas",
|
"dinner-today": "Večera Danas",
|
||||||
"dinner-tonight": "VEČERA NOĆAS",
|
"dinner-tonight": "VEČERA NOĆAS",
|
||||||
@@ -323,13 +323,13 @@
|
|||||||
"mealplan-settings": "Postavke Plana obroka",
|
"mealplan-settings": "Postavke Plana obroka",
|
||||||
"mealplan-update-failed": "Ažuriranje Plana obroka nije uspjelo",
|
"mealplan-update-failed": "Ažuriranje Plana obroka nije uspjelo",
|
||||||
"mealplan-updated": "Plan obroka je Ažuriran",
|
"mealplan-updated": "Plan obroka je Ažuriran",
|
||||||
"mealplan-households-description": "If no household is selected, recipes can be added from any household",
|
"mealplan-households-description": "Ako nijedno kućanstvo nije odabrano, recepti mogu biti dodani iz bilo kojeg kućanstva",
|
||||||
"any-category": "Any Category",
|
"any-category": "Bilo koja Kategorija",
|
||||||
"any-tag": "Any Tag",
|
"any-tag": "Bilo koja Oznaka",
|
||||||
"any-household": "Any Household",
|
"any-household": "Bilo koje Kućanstvo",
|
||||||
"no-meal-plan-defined-yet": "Plan obroka još nije definiran",
|
"no-meal-plan-defined-yet": "Plan obroka još nije definiran",
|
||||||
"no-meal-planned-for-today": "Nema Plan obroka za današnji dan",
|
"no-meal-planned-for-today": "Nema Plan obroka za današnji dan",
|
||||||
"numberOfDays-hint": "Number of days on page load",
|
"numberOfDays-hint": "Broj dana na očitavanju stranice",
|
||||||
"numberOfDays-label": "Default Days",
|
"numberOfDays-label": "Default Days",
|
||||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Samo recepti s ovim kategorijama bit će korišteni u planovima obroka",
|
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Samo recepti s ovim kategorijama bit će korišteni u planovima obroka",
|
||||||
"planner": "Planer",
|
"planner": "Planer",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Prikaži Izvorni Tekst",
|
"see-original-text": "Prikaži Izvorni Tekst",
|
||||||
"original-text-with-value": "Izvorni Tekst: {originalText}",
|
"original-text-with-value": "Izvorni Tekst: {originalText}",
|
||||||
"ingredient-linker": "Poveznik Sastojaka",
|
"ingredient-linker": "Poveznik Sastojaka",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Povezano s drugim korakom",
|
"linked-to-other-step": "Povezano s drugim korakom",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Način Kuhanja",
|
"cook-mode": "Način Kuhanja",
|
||||||
@@ -602,7 +603,7 @@
|
|||||||
"import-with-url": "Učitaj preko URL-a",
|
"import-with-url": "Učitaj preko URL-a",
|
||||||
"create-recipe": "Kreiraj recept",
|
"create-recipe": "Kreiraj recept",
|
||||||
"create-recipe-description": "Create a new recipe from scratch.",
|
"create-recipe-description": "Create a new recipe from scratch.",
|
||||||
"create-recipes": "Create Recipes",
|
"create-recipes": "Kreiraj recept",
|
||||||
"import-with-zip": "Učitaj pomoću .zip-a",
|
"import-with-zip": "Učitaj pomoću .zip-a",
|
||||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||||
@@ -628,7 +629,7 @@
|
|||||||
"import-from-html-or-json": "Import from HTML or JSON",
|
"import-from-html-or-json": "Import from HTML or JSON",
|
||||||
"import-from-html-or-json-description": "Import a single recipe from raw HTML or JSON. This is useful if you have a recipe from a site that Mealie can't scrape normally, or from some other external source.",
|
"import-from-html-or-json-description": "Import a single recipe from raw HTML or JSON. This is useful if you have a recipe from a site that Mealie can't scrape normally, or from some other external source.",
|
||||||
"json-import-format-description-colon": "To import via JSON, it must be in valid format:",
|
"json-import-format-description-colon": "To import via JSON, it must be in valid format:",
|
||||||
"json-editor": "JSON Editor",
|
"json-editor": "JSON uređivač",
|
||||||
"zip-files-must-have-been-exported-from-mealie": ".zip datoteke moraju biti izvezeni iz Mealie-a",
|
"zip-files-must-have-been-exported-from-mealie": ".zip datoteke moraju biti izvezeni iz Mealie-a",
|
||||||
"create-a-recipe-by-uploading-a-scan": "Izradite recept tako što ćete učitati skeniranu kopiju.",
|
"create-a-recipe-by-uploading-a-scan": "Izradite recept tako što ćete učitati skeniranu kopiju.",
|
||||||
"upload-a-png-image-from-a-recipe-book": "Učitajte png sliku iz kuharice",
|
"upload-a-png-image-from-a-recipe-book": "Učitajte png sliku iz kuharice",
|
||||||
@@ -641,19 +642,19 @@
|
|||||||
"report-deletion-failed": "Brisanje nije uspjelo",
|
"report-deletion-failed": "Brisanje nije uspjelo",
|
||||||
"recipe-debugger": "Ispravljač Pogrešaka Recepta",
|
"recipe-debugger": "Ispravljač Pogrešaka Recepta",
|
||||||
"recipe-debugger-description": "Preuzmite URL recepta koji želite ispraviti i zalijepite ga ovdje. URL će biti obrađen od strane scraper-a za recepte i rezultati će biti prikazani. Ako ne vidite nikakve povratne podatke, to znači da web stranica koju pokušavate obraditi nije podržana od strane Mealie-a ili njegove biblioteke za scraper-e.",
|
"recipe-debugger-description": "Preuzmite URL recepta koji želite ispraviti i zalijepite ga ovdje. URL će biti obrađen od strane scraper-a za recepte i rezultati će biti prikazani. Ako ne vidite nikakve povratne podatke, to znači da web stranica koju pokušavate obraditi nije podržana od strane Mealie-a ili njegove biblioteke za scraper-e.",
|
||||||
"use-openai": "Use OpenAI",
|
"use-openai": "Koristi OpenAI",
|
||||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||||
"debug": "Ispravljanje grešaka",
|
"debug": "Ispravljanje grešaka",
|
||||||
"tree-view": "Prikaz Stabla",
|
"tree-view": "Prikaz Stabla",
|
||||||
"recipe-servings": "Recipe Servings",
|
"recipe-servings": "Serviranja recepta",
|
||||||
"recipe-yield": "Konačna Količina Recepta",
|
"recipe-yield": "Konačna Količina Recepta",
|
||||||
"recipe-yield-text": "Recipe Yield Text",
|
"recipe-yield-text": "Recipe Yield Text",
|
||||||
"unit": "Jedinica",
|
"unit": "Jedinica",
|
||||||
"upload-image": "Učitavanje Slike",
|
"upload-image": "Učitavanje Slike",
|
||||||
"screen-awake": "Keep Screen Awake",
|
"screen-awake": "Zadrži ekran uključenim",
|
||||||
"remove-image": "Remove image",
|
"remove-image": "Ukloni sliku",
|
||||||
"nextStep": "Next step",
|
"nextStep": "Sljedeći korak",
|
||||||
"recipe-actions": "Recipe Actions",
|
"recipe-actions": "Akcije recepta",
|
||||||
"parser": {
|
"parser": {
|
||||||
"ingredient-parser": "Ingredient Parser",
|
"ingredient-parser": "Ingredient Parser",
|
||||||
"explanation": "To use the ingredient parser, click the 'Parse All' button to start the process. Once the processed ingredients are available, you can review the items and verify that they were parsed correctly. The model's confidence score is displayed on the right of the item title. This score is an average of all the individual scores and may not always be completely accurate.",
|
"explanation": "To use the ingredient parser, click the 'Parse All' button to start the process. Once the processed ingredients are available, you can review the items and verify that they were parsed correctly. The model's confidence score is displayed on the right of the item title. This score is an average of all the individual scores and may not always be completely accurate.",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Eredeti szöveg megjelenítése",
|
"see-original-text": "Eredeti szöveg megjelenítése",
|
||||||
"original-text-with-value": "Eredeti szöveg: {originalText}",
|
"original-text-with-value": "Eredeti szöveg: {originalText}",
|
||||||
"ingredient-linker": "Hozzávaló összekötő",
|
"ingredient-linker": "Hozzávaló összekötő",
|
||||||
|
"unlinked": "Még nincs csatolva",
|
||||||
"linked-to-other-step": "Egy másik lépéssel összekapcsolva",
|
"linked-to-other-step": "Egy másik lépéssel összekapcsolva",
|
||||||
"auto": "Automatikus",
|
"auto": "Automatikus",
|
||||||
"cook-mode": "Főzési mód",
|
"cook-mode": "Főzési mód",
|
||||||
|
|||||||
@@ -193,12 +193,12 @@
|
|||||||
"delete-with-name": "Eyða út {name}",
|
"delete-with-name": "Eyða út {name}",
|
||||||
"confirm-delete-generic-with-name": "Ertu viss um að þú viljir eyða út {name}?",
|
"confirm-delete-generic-with-name": "Ertu viss um að þú viljir eyða út {name}?",
|
||||||
"confirm-delete-own-admin-account": "Please note that you are trying to delete your own admin account! This action cannot be undone and will permanently delete your account?",
|
"confirm-delete-own-admin-account": "Please note that you are trying to delete your own admin account! This action cannot be undone and will permanently delete your account?",
|
||||||
"organizer": "Organizer",
|
"organizer": "Skipuleggjari",
|
||||||
"transfer": "Færa",
|
"transfer": "Færa",
|
||||||
"copy": "Afrita",
|
"copy": "Afrita",
|
||||||
"color": "Litur",
|
"color": "Litur",
|
||||||
"timestamp": "Tímastimpill",
|
"timestamp": "Tímastimpill",
|
||||||
"last-made": "Last Made",
|
"last-made": "Síðast gert",
|
||||||
"learn-more": "Læra meira",
|
"learn-more": "Læra meira",
|
||||||
"this-feature-is-currently-inactive": "This feature is currently inactive",
|
"this-feature-is-currently-inactive": "This feature is currently inactive",
|
||||||
"clipboard-not-supported": "Clipboard not supported",
|
"clipboard-not-supported": "Clipboard not supported",
|
||||||
@@ -214,16 +214,16 @@
|
|||||||
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
|
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
|
||||||
"clipboard-copy-failure": "Failed to copy to the clipboard.",
|
"clipboard-copy-failure": "Failed to copy to the clipboard.",
|
||||||
"confirm-delete-generic-items": "Are you sure you want to delete the following items?",
|
"confirm-delete-generic-items": "Are you sure you want to delete the following items?",
|
||||||
"organizers": "Organizers",
|
"organizers": "Skipuleggjarar",
|
||||||
"caution": "Varúð",
|
"caution": "Varúð",
|
||||||
"show-advanced": "Show Advanced",
|
"show-advanced": "Show Advanced",
|
||||||
"add-field": "Add Field",
|
"add-field": "Bæta við dálk",
|
||||||
"date-created": "Date Created",
|
"date-created": "Date Created",
|
||||||
"date-updated": "Date Updated"
|
"date-updated": "Dagsetning uppfærð"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
|
"are-you-sure-you-want-to-delete-the-group": "Ertu viss um að þú viljir eyða <b>{groupName}<b/>?",
|
||||||
"cannot-delete-default-group": "Cannot delete default group",
|
"cannot-delete-default-group": "Ekki hægt að eyða sjálfvöldum hóp",
|
||||||
"cannot-delete-group-with-users": "Cannot delete group with users",
|
"cannot-delete-group-with-users": "Cannot delete group with users",
|
||||||
"confirm-group-deletion": "Confirm Group Deletion",
|
"confirm-group-deletion": "Confirm Group Deletion",
|
||||||
"create-group": "Búa til hóp",
|
"create-group": "Búa til hóp",
|
||||||
@@ -237,18 +237,18 @@
|
|||||||
"group-token": "Group Token",
|
"group-token": "Group Token",
|
||||||
"group-with-value": "Group: {groupID}",
|
"group-with-value": "Group: {groupID}",
|
||||||
"groups": "Hópar",
|
"groups": "Hópar",
|
||||||
"manage-groups": "Manage Groups",
|
"manage-groups": "Umsjá hópa",
|
||||||
"user-group": "Notendahópur",
|
"user-group": "Notendahópur",
|
||||||
"user-group-created": "User Group Created",
|
"user-group-created": "Notendahópur búinn til",
|
||||||
"user-group-creation-failed": "User Group Creation Failed",
|
"user-group-creation-failed": "User Group Creation Failed",
|
||||||
"settings": {
|
"settings": {
|
||||||
"keep-my-recipes-private": "Keep My Recipes Private",
|
"keep-my-recipes-private": "Keep My Recipes Private",
|
||||||
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
|
"keep-my-recipes-private-description": "Sets your group and all recipes defaults to private. You can always change this later."
|
||||||
},
|
},
|
||||||
"manage-members": "Manage Members",
|
"manage-members": "Umsjá meðlima",
|
||||||
"manage-members-description": "Manage the permissions of the members in your household. {manage} allows the user to access the data-management page, and {invite} allows the user to generate invitation links for other users. Group owners cannot change their own permissions.",
|
"manage-members-description": "Manage the permissions of the members in your household. {manage} allows the user to access the data-management page, and {invite} allows the user to generate invitation links for other users. Group owners cannot change their own permissions.",
|
||||||
"manage": "Manage",
|
"manage": "Umsjá",
|
||||||
"manage-household": "Manage Household",
|
"manage-household": "Umsjá heimilis",
|
||||||
"invite": "Bjóða",
|
"invite": "Bjóða",
|
||||||
"looking-to-update-your-profile": "Viltu uppfæra prófílinn þinn?",
|
"looking-to-update-your-profile": "Viltu uppfæra prófílinn þinn?",
|
||||||
"default-recipe-preferences-description": "Þetta eru sjálfgefnar stillingar þegar ný uppskrift er búin til í hópnum þínum. Hægt er að breyta þeim fyrir einstakar uppskriftir í stillingavalmynd uppskrifta.",
|
"default-recipe-preferences-description": "Þetta eru sjálfgefnar stillingar þegar ný uppskrift er búin til í hópnum þínum. Hægt er að breyta þeim fyrir einstakar uppskriftir í stillingavalmynd uppskrifta.",
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
"disable-users-from-commenting-on-recipes-description": "Hides the comment section on the recipe page and disables commenting",
|
"disable-users-from-commenting-on-recipes-description": "Hides the comment section on the recipe page and disables commenting",
|
||||||
"disable-organizing-recipe-ingredients-by-units-and-food": "Disable organizing recipe ingredients by units and food",
|
"disable-organizing-recipe-ingredients-by-units-and-food": "Disable organizing recipe ingredients by units and food",
|
||||||
"disable-organizing-recipe-ingredients-by-units-and-food-description": "Hides the Food, Unit, and Amount fields for ingredients and treats ingredients as plain text fields",
|
"disable-organizing-recipe-ingredients-by-units-and-food-description": "Hides the Food, Unit, and Amount fields for ingredients and treats ingredients as plain text fields",
|
||||||
"general-preferences": "General Preferences",
|
"general-preferences": "Almenni valmöguleikar",
|
||||||
"group-recipe-preferences": "Group Recipe Preferences",
|
"group-recipe-preferences": "Group Recipe Preferences",
|
||||||
"report": "Skýrsla",
|
"report": "Skýrsla",
|
||||||
"report-with-id": "Report ID: {id}",
|
"report-with-id": "Report ID: {id}",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "See Original Text",
|
"see-original-text": "See Original Text",
|
||||||
"original-text-with-value": "Original Text: {originalText}",
|
"original-text-with-value": "Original Text: {originalText}",
|
||||||
"ingredient-linker": "Ingredient Linker",
|
"ingredient-linker": "Ingredient Linker",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Linked to other step",
|
"linked-to-other-step": "Linked to other step",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Cook Mode",
|
"cook-mode": "Cook Mode",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Vedi Testo Originale",
|
"see-original-text": "Vedi Testo Originale",
|
||||||
"original-text-with-value": "Testo originale: {originalText}",
|
"original-text-with-value": "Testo originale: {originalText}",
|
||||||
"ingredient-linker": "Linker degli Ingredienti",
|
"ingredient-linker": "Linker degli Ingredienti",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Collegato ad un altro passaggio",
|
"linked-to-other-step": "Collegato ad un altro passaggio",
|
||||||
"auto": "Automatico",
|
"auto": "Automatico",
|
||||||
"cook-mode": "Modalità di Cottura",
|
"cook-mode": "Modalità di Cottura",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "元のテキストを見る",
|
"see-original-text": "元のテキストを見る",
|
||||||
"original-text-with-value": "原文: {originalText}",
|
"original-text-with-value": "原文: {originalText}",
|
||||||
"ingredient-linker": "材料リンク",
|
"ingredient-linker": "材料リンク",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "他のステップにリンクしています",
|
"linked-to-other-step": "他のステップにリンクしています",
|
||||||
"auto": "自動",
|
"auto": "自動",
|
||||||
"cook-mode": "調理モード",
|
"cook-mode": "調理モード",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "See Original Text",
|
"see-original-text": "See Original Text",
|
||||||
"original-text-with-value": "Original Text: {originalText}",
|
"original-text-with-value": "Original Text: {originalText}",
|
||||||
"ingredient-linker": "Ingredient Linker",
|
"ingredient-linker": "Ingredient Linker",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Linked to other step",
|
"linked-to-other-step": "Linked to other step",
|
||||||
"auto": "자동",
|
"auto": "자동",
|
||||||
"cook-mode": "Cook Mode",
|
"cook-mode": "Cook Mode",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Rodyti originalų tekstą",
|
"see-original-text": "Rodyti originalų tekstą",
|
||||||
"original-text-with-value": "Originalus tekstas: {originalText}",
|
"original-text-with-value": "Originalus tekstas: {originalText}",
|
||||||
"ingredient-linker": "Ingredientų siejimas",
|
"ingredient-linker": "Ingredientų siejimas",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Susietas su kitu žingsniu",
|
"linked-to-other-step": "Susietas su kitu žingsniu",
|
||||||
"auto": "Automatiškai",
|
"auto": "Automatiškai",
|
||||||
"cook-mode": "Gaminimo režimas",
|
"cook-mode": "Gaminimo režimas",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Skatīt oriģinālo tekstu",
|
"see-original-text": "Skatīt oriģinālo tekstu",
|
||||||
"original-text-with-value": "Oriģinālais teksts: {originalText}",
|
"original-text-with-value": "Oriģinālais teksts: {originalText}",
|
||||||
"ingredient-linker": "Sastāvdaļu Linker",
|
"ingredient-linker": "Sastāvdaļu Linker",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Saistīts ar citu soli",
|
"linked-to-other-step": "Saistīts ar citu soli",
|
||||||
"auto": "Automātiski",
|
"auto": "Automātiski",
|
||||||
"cook-mode": "Gatavošanas režīms",
|
"cook-mode": "Gatavošanas režīms",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Nieuwe melding",
|
"new-notification": "Nieuwe melding",
|
||||||
"event-notifiers": "Meldingen van gebeurtenissen",
|
"event-notifiers": "Meldingen van gebeurtenissen",
|
||||||
"apprise-url-skipped-if-blank": "URL van Apprise (overgeslagen als veld leeg is)",
|
"apprise-url-skipped-if-blank": "URL van Apprise (overgeslagen als veld leeg is)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Aangezien Apprise URL's doorgaans gevoelige informatie bevatten, wordt dit veld opzettelijk leeg gelaten tijdens het bewerken. Als je de URL wilt bijwerken, vul dan de nieuwe hier in, anders laat het leeg om de huidige URL te behouden.",
|
||||||
"enable-notifier": "Activeer melding",
|
"enable-notifier": "Activeer melding",
|
||||||
"what-events": "Op welke gebeurtenissen moet deze melding zich abonneren?",
|
"what-events": "Op welke gebeurtenissen moet deze melding zich abonneren?",
|
||||||
"user-events": "Gebeurtenissen van gebruiker",
|
"user-events": "Gebeurtenissen van gebruiker",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Zie oorspronkelijke tekst",
|
"see-original-text": "Zie oorspronkelijke tekst",
|
||||||
"original-text-with-value": "Oorspronkelijke tekst: {originalText}",
|
"original-text-with-value": "Oorspronkelijke tekst: {originalText}",
|
||||||
"ingredient-linker": "Ingrediëntenkoppelaar",
|
"ingredient-linker": "Ingrediëntenkoppelaar",
|
||||||
|
"unlinked": "Nog niet gelinkt",
|
||||||
"linked-to-other-step": "Gekoppeld aan andere stap",
|
"linked-to-other-step": "Gekoppeld aan andere stap",
|
||||||
"auto": "Automatisch",
|
"auto": "Automatisch",
|
||||||
"cook-mode": "Kookmodus",
|
"cook-mode": "Kookmodus",
|
||||||
@@ -1021,7 +1022,7 @@
|
|||||||
"enable-advanced-content-description": "Schakelt geavanceerde functies, zoals recepten opschalen, API-sleutels, webhooks en gegevensbeheer in. Geen zorgen, je kan dit later altijd aanpassen",
|
"enable-advanced-content-description": "Schakelt geavanceerde functies, zoals recepten opschalen, API-sleutels, webhooks en gegevensbeheer in. Geen zorgen, je kan dit later altijd aanpassen",
|
||||||
"favorite-recipes": "Favoriete recepten",
|
"favorite-recipes": "Favoriete recepten",
|
||||||
"email-or-username": "E-mailadres of gebruikersnaam",
|
"email-or-username": "E-mailadres of gebruikersnaam",
|
||||||
"remember-me": "Herinner mij",
|
"remember-me": "Blijf ingelogd",
|
||||||
"please-enter-your-email-and-password": "Voer je e-mailadres en wachtwoord in",
|
"please-enter-your-email-and-password": "Voer je e-mailadres en wachtwoord in",
|
||||||
"invalid-credentials": "Ongeldige inloggegevens",
|
"invalid-credentials": "Ongeldige inloggegevens",
|
||||||
"account-locked-please-try-again-later": "Account geblokkeerd. Probeer het later opnieuw",
|
"account-locked-please-try-again-later": "Account geblokkeerd. Probeer het later opnieuw",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Se opprinnelig tekst",
|
"see-original-text": "Se opprinnelig tekst",
|
||||||
"original-text-with-value": "Opprinnelig tekst: {originalText}",
|
"original-text-with-value": "Opprinnelig tekst: {originalText}",
|
||||||
"ingredient-linker": "Tilknytt ingredienser",
|
"ingredient-linker": "Tilknytt ingredienser",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Tilknyttet et annet steg",
|
"linked-to-other-step": "Tilknyttet et annet steg",
|
||||||
"auto": "Automatisk",
|
"auto": "Automatisk",
|
||||||
"cook-mode": "Tilberedelsesmodus",
|
"cook-mode": "Tilberedelsesmodus",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Zobacz oryginalny tekst",
|
"see-original-text": "Zobacz oryginalny tekst",
|
||||||
"original-text-with-value": "Oryginalny tekst: {originalText}",
|
"original-text-with-value": "Oryginalny tekst: {originalText}",
|
||||||
"ingredient-linker": "Linkier do składników",
|
"ingredient-linker": "Linkier do składników",
|
||||||
|
"unlinked": "Jeszcze nie połączony",
|
||||||
"linked-to-other-step": "Powiązane z innym krokiem",
|
"linked-to-other-step": "Powiązane z innym krokiem",
|
||||||
"auto": "Automatycznie",
|
"auto": "Automatycznie",
|
||||||
"cook-mode": "Tryb Gotowania",
|
"cook-mode": "Tryb Gotowania",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Nova Notificação",
|
"new-notification": "Nova Notificação",
|
||||||
"event-notifiers": "Notificações de Eventos",
|
"event-notifiers": "Notificações de Eventos",
|
||||||
"apprise-url-skipped-if-blank": "URL Apprise (ignorado se estiver em branco)",
|
"apprise-url-skipped-if-blank": "URL Apprise (ignorado se estiver em branco)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Como URLs de notificação normalmente contém informações confidenciais, este campo foi deixando intencionalmente em branco quando editado. Se você deseja atualizar o URL, por favor insira o novo localizador aqui. Caso contrário, deixe em branco para manter o URL atual.",
|
||||||
"enable-notifier": "Habilitar Notificador",
|
"enable-notifier": "Habilitar Notificador",
|
||||||
"what-events": "A quais eventos este notificador deve subscrever?",
|
"what-events": "A quais eventos este notificador deve subscrever?",
|
||||||
"user-events": "Eventos do usuário",
|
"user-events": "Eventos do usuário",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Exibir texto original",
|
"see-original-text": "Exibir texto original",
|
||||||
"original-text-with-value": "Texto Original: {originalText}",
|
"original-text-with-value": "Texto Original: {originalText}",
|
||||||
"ingredient-linker": "Ingrediente do Linker",
|
"ingredient-linker": "Ingrediente do Linker",
|
||||||
|
"unlinked": "Ainda não vinculado",
|
||||||
"linked-to-other-step": "Ligado a outro passo",
|
"linked-to-other-step": "Ligado a outro passo",
|
||||||
"auto": "Automático",
|
"auto": "Automático",
|
||||||
"cook-mode": "Modo Cozinheiro",
|
"cook-mode": "Modo Cozinheiro",
|
||||||
@@ -675,8 +676,8 @@
|
|||||||
"upload-another-image": "Carregar outra imagem",
|
"upload-another-image": "Carregar outra imagem",
|
||||||
"upload-images": "Carregar imagens",
|
"upload-images": "Carregar imagens",
|
||||||
"upload-more-images": "Carregar mais imagens",
|
"upload-more-images": "Carregar mais imagens",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Definir como imagem de capa da receita",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Imagem de capa"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Localizador de Receitas",
|
"recipe-finder": "Localizador de Receitas",
|
||||||
@@ -1169,7 +1170,7 @@
|
|||||||
"group-details": "Detalhes do Grupo",
|
"group-details": "Detalhes do Grupo",
|
||||||
"group-details-description": "Antes de criar uma conta é necessário criar um grupo. O seu grupo só conterá você, mas você poderá convidar os outros mais tarde. Os membros do seu grupo podem compartilhar planos de refeição, listas de compras, receitas e muito mais!",
|
"group-details-description": "Antes de criar uma conta é necessário criar um grupo. O seu grupo só conterá você, mas você poderá convidar os outros mais tarde. Os membros do seu grupo podem compartilhar planos de refeição, listas de compras, receitas e muito mais!",
|
||||||
"use-seed-data": "Usar dados semeados",
|
"use-seed-data": "Usar dados semeados",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
"use-seed-data-description": "O Mealie vem com uma coleção de Alimentos, Unidades e Rótulos que podem ser usados para preencher seu grupo com dados úteis ou para organizar suas receitas. Eles são traduzidos para o idioma selecionado. Você sempre pode adicionar ou modificar esses dados posteriormente.",
|
||||||
"account-details": "Detalhes da Conta"
|
"account-details": "Detalhes da Conta"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Mostrar texto original",
|
"see-original-text": "Mostrar texto original",
|
||||||
"original-text-with-value": "Texto Original: {originalText}",
|
"original-text-with-value": "Texto Original: {originalText}",
|
||||||
"ingredient-linker": "Conector de ingredientes",
|
"ingredient-linker": "Conector de ingredientes",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Ligado a outro passo",
|
"linked-to-other-step": "Ligado a outro passo",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Modo Cozinheiro",
|
"cook-mode": "Modo Cozinheiro",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Vezi Textul Original",
|
"see-original-text": "Vezi Textul Original",
|
||||||
"original-text-with-value": "Text original: {originalText}",
|
"original-text-with-value": "Text original: {originalText}",
|
||||||
"ingredient-linker": "Legarea cu ingrediente",
|
"ingredient-linker": "Legarea cu ingrediente",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Conectat la alt pas",
|
"linked-to-other-step": "Conectat la alt pas",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Modul de gătire",
|
"cook-mode": "Modul de gătire",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Показать исходный текст",
|
"see-original-text": "Показать исходный текст",
|
||||||
"original-text-with-value": "Исходный текст: {originalText}",
|
"original-text-with-value": "Исходный текст: {originalText}",
|
||||||
"ingredient-linker": "Связка ингредиентов",
|
"ingredient-linker": "Связка ингредиентов",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Связан с другим шагом",
|
"linked-to-other-step": "Связан с другим шагом",
|
||||||
"auto": "Авто",
|
"auto": "Авто",
|
||||||
"cook-mode": "Режим готовки",
|
"cook-mode": "Режим готовки",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"new-notification": "Nové upozornenie",
|
"new-notification": "Nové upozornenie",
|
||||||
"event-notifiers": "Upozornenia udalostí",
|
"event-notifiers": "Upozornenia udalostí",
|
||||||
"apprise-url-skipped-if-blank": "Informačná URL (preskočená, ak je prázdna)",
|
"apprise-url-skipped-if-blank": "Informačná URL (preskočená, ak je prázdna)",
|
||||||
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
|
"apprise-url-is-left-intentionally-blank": "Keďže Apprise URL typicky obsahujú citlivé informácie, toto pole je ponechané zámerne prázdne počas úprav. Ak si prajete aktualizovať URL, prosím zadajte novú sem, inak ho nechajte prázdne pre zachovanie aktuálnej URL.",
|
||||||
"enable-notifier": "Zapnúť notifikátor",
|
"enable-notifier": "Zapnúť notifikátor",
|
||||||
"what-events": "Pre ktoré udalosti si želáte zapnúť notifikátor?",
|
"what-events": "Pre ktoré udalosti si želáte zapnúť notifikátor?",
|
||||||
"user-events": "Udalosti používateľa",
|
"user-events": "Udalosti používateľa",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"category-events": "Udalosti kategórií",
|
"category-events": "Udalosti kategórií",
|
||||||
"when-a-new-user-joins-your-group": "Keď sa k vašej skupine pripojí nový používateľ",
|
"when-a-new-user-joins-your-group": "Keď sa k vašej skupine pripojí nový používateľ",
|
||||||
"recipe-events": "Udalosti receptov",
|
"recipe-events": "Udalosti receptov",
|
||||||
"label-events": "Label Events"
|
"label-events": "Udalosti označení"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"add": "Pridať",
|
"add": "Pridať",
|
||||||
@@ -474,7 +474,7 @@
|
|||||||
"comment": "Komentár",
|
"comment": "Komentár",
|
||||||
"comments": "Komentáre",
|
"comments": "Komentáre",
|
||||||
"delete-confirmation": "Naozaj chcete odstrániť zvolený recept?",
|
"delete-confirmation": "Naozaj chcete odstrániť zvolený recept?",
|
||||||
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
"admin-delete-confirmation": "Budete mazať recept, ktorý nie je váš s použitím administrátorských oprávnení. Ste si istý?",
|
||||||
"delete-recipe": "Odstrániť recept",
|
"delete-recipe": "Odstrániť recept",
|
||||||
"description": "Popis",
|
"description": "Popis",
|
||||||
"disable-amount": "Vypnúť množstvá surovín",
|
"disable-amount": "Vypnúť množstvá surovín",
|
||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Pozrieť pôvodný text",
|
"see-original-text": "Pozrieť pôvodný text",
|
||||||
"original-text-with-value": "Pôvodný text: {originalText}",
|
"original-text-with-value": "Pôvodný text: {originalText}",
|
||||||
"ingredient-linker": "Prepojenie surovín",
|
"ingredient-linker": "Prepojenie surovín",
|
||||||
|
"unlinked": "Zatiaľ neprepojené",
|
||||||
"linked-to-other-step": "Prepojené s iným krokom",
|
"linked-to-other-step": "Prepojené s iným krokom",
|
||||||
"auto": "Automaticky",
|
"auto": "Automaticky",
|
||||||
"cook-mode": "Režim varenia",
|
"cook-mode": "Režim varenia",
|
||||||
@@ -585,11 +586,11 @@
|
|||||||
"added-to-timeline": "Pridané na časovú os",
|
"added-to-timeline": "Pridané na časovú os",
|
||||||
"failed-to-add-to-timeline": "Pridanie na časovú os skončilo chybou",
|
"failed-to-add-to-timeline": "Pridanie na časovú os skončilo chybou",
|
||||||
"failed-to-update-recipe": "Recept sa nepodarilo aktualizovať",
|
"failed-to-update-recipe": "Recept sa nepodarilo aktualizovať",
|
||||||
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
"added-to-timeline-but-failed-to-add-image": "Pridané na časovú os, ale zlyhalo pridanie obrázku",
|
||||||
"api-extras-description": "API dolnky receptov sú kľúčovou funkcionalitou Mealie API. Umožňujú používateľom vytvárať vlastné JSON páry kľúč/hodnota v rámci receptu, a využiť v aplikáciách tretích strán. Údaje uložené pod jednotlivými kľúčmi je možné využiť napríklad ako spúšťač automatizovaných procesov, či pri zasielaní vlastných správ do vami zvolených zariadení.",
|
"api-extras-description": "API dolnky receptov sú kľúčovou funkcionalitou Mealie API. Umožňujú používateľom vytvárať vlastné JSON páry kľúč/hodnota v rámci receptu, a využiť v aplikáciách tretích strán. Údaje uložené pod jednotlivými kľúčmi je možné využiť napríklad ako spúšťač automatizovaných procesov, či pri zasielaní vlastných správ do vami zvolených zariadení.",
|
||||||
"message-key": "Kľúč správy",
|
"message-key": "Kľúč správy",
|
||||||
"parse": "Analyzovať",
|
"parse": "Analyzovať",
|
||||||
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
"ingredients-not-parsed-description": "",
|
||||||
"attach-images-hint": "Pridaj obrázky ich potiahnutím a pustením na editor",
|
"attach-images-hint": "Pridaj obrázky ich potiahnutím a pustením na editor",
|
||||||
"drop-image": "Odstrániť obrázok",
|
"drop-image": "Odstrániť obrázok",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Povoľ množstvám prísad využívať túto vlastnosť",
|
"enable-ingredient-amounts-to-use-this-feature": "Povoľ množstvám prísad využívať túto vlastnosť",
|
||||||
@@ -610,7 +611,7 @@
|
|||||||
"create-from-images": "Vytvoriť z obrázka",
|
"create-from-images": "Vytvoriť z obrázka",
|
||||||
"should-translate-description": "Preložiť recept do môjho jazyka",
|
"should-translate-description": "Preložiť recept do môjho jazyka",
|
||||||
"please-wait-image-procesing": "Čakajte, prosím. Obrázok sa spracováva. Môže to chvíľku trvať.",
|
"please-wait-image-procesing": "Čakajte, prosím. Obrázok sa spracováva. Môže to chvíľku trvať.",
|
||||||
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
|
"please-wait-images-processing": "Prosím počkajte, obrázky sa spracúvajú. Toto môže chvíľu trvať.",
|
||||||
"bulk-url-import": "Hromadný URL import",
|
"bulk-url-import": "Hromadný URL import",
|
||||||
"debug-scraper": "Ladiť scraper",
|
"debug-scraper": "Ladiť scraper",
|
||||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Vytvoriť recept zadaním názvu. Všetky recepty musia mať jedinečné názvy.",
|
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Vytvoriť recept zadaním názvu. Všetky recepty musia mať jedinečné názvy.",
|
||||||
@@ -666,17 +667,17 @@
|
|||||||
"no-unit": "Bez jednotky",
|
"no-unit": "Bez jednotky",
|
||||||
"missing-unit": "Vytvoriť chýbajúcu jednotku: {unit}",
|
"missing-unit": "Vytvoriť chýbajúcu jednotku: {unit}",
|
||||||
"missing-food": "Vytvoriť chýbajúcu surovinu: {food}",
|
"missing-food": "Vytvoriť chýbajúcu surovinu: {food}",
|
||||||
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
"this-unit-could-not-be-parsed-automatically": "Túto jednotku nebolo možné parsovať automaticky",
|
||||||
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
"this-food-could-not-be-parsed-automatically": "Toto jedlo nebolo možné parsovať automaticky",
|
||||||
"no-food": "Žiadne suroviny"
|
"no-food": "Žiadne suroviny"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Resetovať počet porcií",
|
"reset-servings-count": "Resetovať počet porcií",
|
||||||
"not-linked-ingredients": "Ďalšie suroviny",
|
"not-linked-ingredients": "Ďalšie suroviny",
|
||||||
"upload-another-image": "Upload another image",
|
"upload-another-image": "Nahrať iný obrázok",
|
||||||
"upload-images": "Nahrať obrázky",
|
"upload-images": "Nahrať obrázky",
|
||||||
"upload-more-images": "Nahrať ďalšie obrázky",
|
"upload-more-images": "Nahrať ďalšie obrázky",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Nastaviť ako titulný obrázok receptu",
|
||||||
"cover-image": "Cover image"
|
"cover-image": "Titulný obrázok"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Hľadač receptov",
|
"recipe-finder": "Hľadač receptov",
|
||||||
@@ -1169,7 +1170,7 @@
|
|||||||
"group-details": "Podrobnosti o skupine",
|
"group-details": "Podrobnosti o skupine",
|
||||||
"group-details-description": "Pred vytvorením účtu musíte vytvoriť skupinu. Vaša skupina bude obsahovať iba vás, ale neskôr budete môcť pozvať ostatných. Členovia vašej skupiny môžu zdieľať stravovacie plány, nákupné zoznamy, recepty a ďalšie!",
|
"group-details-description": "Pred vytvorením účtu musíte vytvoriť skupinu. Vaša skupina bude obsahovať iba vás, ale neskôr budete môcť pozvať ostatných. Členovia vašej skupiny môžu zdieľať stravovacie plány, nákupné zoznamy, recepty a ďalšie!",
|
||||||
"use-seed-data": "Použiť predvolené dáta",
|
"use-seed-data": "Použiť predvolené dáta",
|
||||||
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
|
"use-seed-data-description": "Mealie sa dodáva so zbierkou ingrediencií, jednotiek a označení. Môžete ich použiť vo vašej skupine pre lepšiu organizáciu vašich receptov. Tieto sú preložené do jazyka, ktorý ste si práve zvolili. Tieto dáta môžete kedykoľvek doplniť alebo zmeniť.",
|
||||||
"account-details": "Detaily účtu"
|
"account-details": "Detaily účtu"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
@@ -1311,7 +1312,7 @@
|
|||||||
"welcome-user": "👋 Vitajte, {0}!",
|
"welcome-user": "👋 Vitajte, {0}!",
|
||||||
"description": "Spravujte svoj profil, recepty a nastavenia skupín.",
|
"description": "Spravujte svoj profil, recepty a nastavenia skupín.",
|
||||||
"invite-link": "Odkaz s pozvánkou",
|
"invite-link": "Odkaz s pozvánkou",
|
||||||
"get-invite-link": "Odkaz s pozvánkou",
|
"get-invite-link": "Vytvoriť odkaz s pozvánkou",
|
||||||
"get-public-link": "Vytvoriť verejný odkaz",
|
"get-public-link": "Vytvoriť verejný odkaz",
|
||||||
"account-summary": "Zhrnutie účtu",
|
"account-summary": "Zhrnutie účtu",
|
||||||
"account-summary-description": "Tu je súhrn informácií o vašej skupine.",
|
"account-summary-description": "Tu je súhrn informácií o vašej skupine.",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Prikaži izvirno besedilo",
|
"see-original-text": "Prikaži izvirno besedilo",
|
||||||
"original-text-with-value": "Originalno besedilo: {originalText}",
|
"original-text-with-value": "Originalno besedilo: {originalText}",
|
||||||
"ingredient-linker": "Povezovanje sestavin",
|
"ingredient-linker": "Povezovanje sestavin",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Povezano s naslednjim korakom",
|
"linked-to-other-step": "Povezano s naslednjim korakom",
|
||||||
"auto": "Samodejno",
|
"auto": "Samodejno",
|
||||||
"cook-mode": "Način kuhanja",
|
"cook-mode": "Način kuhanja",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "See Original Text",
|
"see-original-text": "See Original Text",
|
||||||
"original-text-with-value": "Original Text: {originalText}",
|
"original-text-with-value": "Original Text: {originalText}",
|
||||||
"ingredient-linker": "Повезивач састојака",
|
"ingredient-linker": "Повезивач састојака",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Повезан са другим кораком",
|
"linked-to-other-step": "Повезан са другим кораком",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Cook Mode",
|
"cook-mode": "Cook Mode",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Visa originaltext",
|
"see-original-text": "Visa originaltext",
|
||||||
"original-text-with-value": "Originaltext: {originalText}",
|
"original-text-with-value": "Originaltext: {originalText}",
|
||||||
"ingredient-linker": "Länka ingredienser",
|
"ingredient-linker": "Länka ingredienser",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Kopplat till annat steg",
|
"linked-to-other-step": "Kopplat till annat steg",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Matlagningsläge",
|
"cook-mode": "Matlagningsläge",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Orijinal Metni Göster",
|
"see-original-text": "Orijinal Metni Göster",
|
||||||
"original-text-with-value": "Orijinal Metin: {originalText}",
|
"original-text-with-value": "Orijinal Metin: {originalText}",
|
||||||
"ingredient-linker": "Malzeme Bağlayıcı",
|
"ingredient-linker": "Malzeme Bağlayıcı",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Başka bir adıma bağlı",
|
"linked-to-other-step": "Başka bir adıma bağlı",
|
||||||
"auto": "Otomatik",
|
"auto": "Otomatik",
|
||||||
"cook-mode": "Pişirme Modu",
|
"cook-mode": "Pişirme Modu",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "Переглянути оригінальний текст",
|
"see-original-text": "Переглянути оригінальний текст",
|
||||||
"original-text-with-value": "Оригінальний текст: {originalText}",
|
"original-text-with-value": "Оригінальний текст: {originalText}",
|
||||||
"ingredient-linker": "Зв'язування інгредієнтів",
|
"ingredient-linker": "Зв'язування інгредієнтів",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Зв'язано з іншим кроком",
|
"linked-to-other-step": "Зв'язано з іншим кроком",
|
||||||
"auto": "Авто",
|
"auto": "Авто",
|
||||||
"cook-mode": "Режим кухаря",
|
"cook-mode": "Режим кухаря",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "See Original Text",
|
"see-original-text": "See Original Text",
|
||||||
"original-text-with-value": "Original Text: {originalText}",
|
"original-text-with-value": "Original Text: {originalText}",
|
||||||
"ingredient-linker": "Ingredient Linker",
|
"ingredient-linker": "Ingredient Linker",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Linked to other step",
|
"linked-to-other-step": "Linked to other step",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Cook Mode",
|
"cook-mode": "Cook Mode",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "查看原文",
|
"see-original-text": "查看原文",
|
||||||
"original-text-with-value": "原文: {originalText}",
|
"original-text-with-value": "原文: {originalText}",
|
||||||
"ingredient-linker": "食材关联器",
|
"ingredient-linker": "食材关联器",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "已关联到其他步骤",
|
"linked-to-other-step": "已关联到其他步骤",
|
||||||
"auto": "自动",
|
"auto": "自动",
|
||||||
"cook-mode": "烹饪模式",
|
"cook-mode": "烹饪模式",
|
||||||
|
|||||||
@@ -561,6 +561,7 @@
|
|||||||
"see-original-text": "See Original Text",
|
"see-original-text": "See Original Text",
|
||||||
"original-text-with-value": "Original Text: {originalText}",
|
"original-text-with-value": "Original Text: {originalText}",
|
||||||
"ingredient-linker": "Ingredient Linker",
|
"ingredient-linker": "Ingredient Linker",
|
||||||
|
"unlinked": "Not linked yet",
|
||||||
"linked-to-other-step": "Linked to other step",
|
"linked-to-other-step": "Linked to other step",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"cook-mode": "Cook Mode",
|
"cook-mode": "Cook Mode",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
v-model="sidebar"
|
v-model="sidebar"
|
||||||
absolute
|
absolute
|
||||||
:top-link="topLinks"
|
:top-link="topLinks"
|
||||||
:bottom-links="bottomLinks"
|
|
||||||
:user="{ data: true }"
|
:user="{ data: true }"
|
||||||
:secondary-header="$t('sidebar.developer')"
|
:secondary-header="$t('sidebar.developer')"
|
||||||
:secondary-links="developerLinks"
|
:secondary-links="developerLinks"
|
||||||
@@ -36,13 +35,15 @@ import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
|
|||||||
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
|
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
|
||||||
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||||
import type { SidebarLinks } from "~/types/application-types";
|
import type { SidebarLinks } from "~/types/application-types";
|
||||||
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useGlobalI18n();
|
||||||
const { $globals, $vuetify } = useNuxtApp();
|
const display = useDisplay();
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
const sidebar = ref<boolean>(false);
|
const sidebar = ref<boolean>(false);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
sidebar.value = !$vuetify.display.md.value;
|
sidebar.value = display.lgAndUp.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const topLinks: SidebarLinks = [
|
const topLinks: SidebarLinks = [
|
||||||
@@ -112,13 +113,4 @@ const developerLinks: SidebarLinks = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const bottomLinks: SidebarLinks = [
|
|
||||||
{
|
|
||||||
icon: $globals.icons.heart,
|
|
||||||
title: i18n.t("about.support"),
|
|
||||||
href: "https://github.com/sponsors/hay-kot",
|
|
||||||
restricted: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app dark>
|
<v-app dark>
|
||||||
<NuxtPwaManifest />
|
|
||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
|
|
||||||
<AppHeader :menu="false" />
|
<AppHeader :menu="false" />
|
||||||
@@ -14,11 +13,10 @@
|
|||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||||
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
|
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
|
||||||
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
useGlobalI18n(); // ensure i18n is initialized
|
||||||
components: { TheSnackbar, AppHeader },
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app dark>
|
<v-app dark>
|
||||||
<NuxtPwaManifest />
|
|
||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
|
|
||||||
<v-banner
|
<v-banner
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||||
import { useAppInfo } from "~/composables/api";
|
import { useAppInfo } from "~/composables/api";
|
||||||
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: { TheSnackbar },
|
components: { TheSnackbar },
|
||||||
@@ -33,7 +33,7 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
const isDemo = computed(() => appInfo?.value?.demoStatus || false);
|
const isDemo = computed(() => appInfo?.value?.demoStatus || false);
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useGlobalI18n();
|
||||||
const version = computed(() => appInfo?.value?.version || i18n.t("about.unknown-version"));
|
const version = computed(() => appInfo?.value?.version || i18n.t("about.unknown-version"));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
<DefaultLayout />
|
<DefaultLayout />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import DefaultLayout from "@/components/Layout/DefaultLayout.vue";
|
import DefaultLayout from "@/components/Layout/DefaultLayout.vue";
|
||||||
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
useGlobalI18n(); // ensure i18n is initialized
|
||||||
components: { DefaultLayout },
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
props: {
|
props: {
|
||||||
error: {
|
error: {
|
||||||
@@ -58,7 +60,7 @@ export default defineNuxtComponent({
|
|||||||
layout: "basic",
|
layout: "basic",
|
||||||
});
|
});
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useGlobalI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ import {
|
|||||||
mdiCookie,
|
mdiCookie,
|
||||||
mdiBellPlus,
|
mdiBellPlus,
|
||||||
mdiLinkVariantPlus,
|
mdiLinkVariantPlus,
|
||||||
|
mdiTableEdit,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
@@ -240,6 +241,7 @@ export const icons = {
|
|||||||
linkVariantPlus: mdiLinkVariantPlus,
|
linkVariantPlus: mdiLinkVariantPlus,
|
||||||
lock: mdiLock,
|
lock: mdiLock,
|
||||||
logout: mdiLogout,
|
logout: mdiLogout,
|
||||||
|
manageData: mdiTableEdit,
|
||||||
menu: mdiMenu,
|
menu: mdiMenu,
|
||||||
messageText: mdiMessageText,
|
messageText: mdiMessageText,
|
||||||
newBox: mdiNewBox,
|
newBox: mdiNewBox,
|
||||||
@@ -324,5 +326,4 @@ export const icons = {
|
|||||||
preserveLines: mdiText,
|
preserveLines: mdiText,
|
||||||
preserveBlocks: mdiTextBoxOutline,
|
preserveBlocks: mdiTextBoxOutline,
|
||||||
flatten: mdiMinus,
|
flatten: mdiMinus,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineNuxtConfig } from "nuxt/config";
|
import { defineNuxtConfig } from "nuxt/config";
|
||||||
import commonjs from "vite-plugin-commonjs";
|
|
||||||
|
|
||||||
const AUTH_TOKEN = "mealie.auth.token";
|
const AUTH_TOKEN = "mealie.auth.token";
|
||||||
|
|
||||||
@@ -56,6 +55,7 @@ export default defineNuxtConfig({
|
|||||||
{ "rel": "shortcut icon", "type": "image/png", "href": "/icons/icon-x64.png", "data-n-head": "ssr" },
|
{ "rel": "shortcut icon", "type": "image/png", "href": "/icons/icon-x64.png", "data-n-head": "ssr" },
|
||||||
{ "rel": "apple-touch-icon", "type": "image/png", "href": "/icons/apple-touch-icon.png", "data-n-head": "ssr" },
|
{ "rel": "apple-touch-icon", "type": "image/png", "href": "/icons/apple-touch-icon.png", "data-n-head": "ssr" },
|
||||||
{ "rel": "mask-icon", "href": "/icons/safari-pinned-tab.svg", "data-n-head": "ssr" },
|
{ "rel": "mask-icon", "href": "/icons/safari-pinned-tab.svg", "data-n-head": "ssr" },
|
||||||
|
{ "rel": "manifest", "href": "/manifest.webmanifest", "data-n-head": "ssr" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -126,12 +126,6 @@ export default defineNuxtConfig({
|
|||||||
baseURL: process.env.SUB_PATH || "",
|
baseURL: process.env.SUB_PATH || "",
|
||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
|
||||||
plugins: [
|
|
||||||
commonjs(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
auth: {
|
auth: {
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
// disableServerSideAuth: true,
|
// disableServerSideAuth: true,
|
||||||
@@ -148,7 +142,7 @@ export default defineNuxtConfig({
|
|||||||
signInResponseTokenPointer: "/access_token",
|
signInResponseTokenPointer: "/access_token",
|
||||||
type: "Bearer",
|
type: "Bearer",
|
||||||
cookieName: AUTH_TOKEN,
|
cookieName: AUTH_TOKEN,
|
||||||
maxAgeInSeconds: 604800, // 7 days
|
maxAgeInSeconds: parseInt(process.env.TOKEN_TIME || "48") * 3600, // TOKEN_TIME is in hours
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
login: "/login",
|
login: "/login",
|
||||||
@@ -240,37 +234,57 @@ export default defineNuxtConfig({
|
|||||||
vueI18n: "./../i18n.config.ts", // note: we need to up one ../ because the default root of lang dir is the /frontend/i18n, which can not be configured
|
vueI18n: "./../i18n.config.ts", // note: we need to up one ../ because the default root of lang dir is the /frontend/i18n, which can not be configured
|
||||||
},
|
},
|
||||||
|
|
||||||
// PWA module configuration: https://go.nuxtjs.dev/pwa
|
// PWA module configuration: https://vite-pwa-org.netlify.app/frameworks/nuxt.html
|
||||||
pwa: {
|
pwa: {
|
||||||
mode: process.env.NODE_ENV === "production" ? "production" : "development",
|
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
useCredentials: true,
|
devOptions: {
|
||||||
|
enabled: false,
|
||||||
|
suppressWarnings: true,
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
navigateFallback: "/",
|
||||||
|
globPatterns: ["**/*.{js,css,html,png,svg,ico}"],
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
|
skipWaiting: true,
|
||||||
|
clientsClaim: true,
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
installPrompt: true,
|
||||||
|
periodicSyncForUpdates: 120,
|
||||||
|
},
|
||||||
|
includeAssets: ["favicon.ico", "apple-touch-icon.png", "safari-pinned-tab.svg"],
|
||||||
manifest: {
|
manifest: {
|
||||||
start_url: "/",
|
|
||||||
scope: "/",
|
|
||||||
lang: "en",
|
|
||||||
name: "Mealie",
|
name: "Mealie",
|
||||||
short_name: "Mealie",
|
short_name: "Mealie",
|
||||||
id: "mealie",
|
id: "/",
|
||||||
description: "Mealie is a recipe management and meal planning app",
|
start_url: "/",
|
||||||
theme_color: process.env.THEME_LIGHT_PRIMARY || "#E58325",
|
scope: "/",
|
||||||
background_color: "#FFFFFF",
|
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
|
background_color: "#FFFFFF",
|
||||||
|
theme_color: process.env.THEME_LIGHT_PRIMARY || "#E58325",
|
||||||
|
description: "Mealie is a recipe management and meal planning app",
|
||||||
|
lang: "en",
|
||||||
display_override: [
|
display_override: [
|
||||||
"standalone",
|
"standalone",
|
||||||
"minimal-ui",
|
"minimal-ui",
|
||||||
"browser",
|
"browser",
|
||||||
"window-controls-overlay",
|
"window-controls-overlay",
|
||||||
],
|
],
|
||||||
|
orientation: "portrait-primary",
|
||||||
|
categories: ["food", "lifestyle"],
|
||||||
|
prefer_related_applications: false,
|
||||||
|
handle_links: "preferred",
|
||||||
|
launch_handler: {
|
||||||
|
client_mode: ["focus-existing", "auto"],
|
||||||
|
},
|
||||||
|
edge_side_panel: {
|
||||||
|
preferred_width: 400,
|
||||||
|
},
|
||||||
share_target: {
|
share_target: {
|
||||||
action: "/r/create/url",
|
action: "/r/create/url",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
params: {
|
params: {
|
||||||
/* title and url are not currently used in Mealie. If there are issues
|
|
||||||
with sharing, uncommenting those lines might help solve the puzzle. */
|
|
||||||
// "title": "title",
|
|
||||||
text: "recipe_import_url",
|
text: "recipe_import_url",
|
||||||
// "url": "url",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
icons: [
|
icons: [
|
||||||
@@ -383,17 +397,6 @@ export default defineNuxtConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
prefer_related_applications: false,
|
|
||||||
handle_links: "preferred",
|
|
||||||
categories: [
|
|
||||||
"food",
|
|
||||||
],
|
|
||||||
launch_handler: {
|
|
||||||
client_mode: ["focus-existing", "auto"],
|
|
||||||
},
|
|
||||||
edge_side_panel: {
|
|
||||||
preferred_width: 400,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mealie",
|
"name": "mealie",
|
||||||
"version": "3.1.1",
|
"version": "3.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
@@ -19,10 +19,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@nuxt/eslint": "1.2.0",
|
|
||||||
"@nuxt/fonts": "^0.11.4",
|
"@nuxt/fonts": "^0.11.4",
|
||||||
"@nuxtjs/i18n": "^9.2.1",
|
"@nuxtjs/i18n": "^9.2.1",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
|
||||||
"@sidebase/nuxt-auth": "0.10.0",
|
"@sidebase/nuxt-auth": "0.10.0",
|
||||||
"@vite-pwa/nuxt": "0.10.6",
|
"@vite-pwa/nuxt": "0.10.6",
|
||||||
"@vueuse/core": "^12.7.0",
|
"@vueuse/core": "^12.7.0",
|
||||||
@@ -32,16 +30,16 @@
|
|||||||
"isomorphic-dompurify": "^2.22.0",
|
"isomorphic-dompurify": "^2.22.0",
|
||||||
"json-editor-vue": "^0.18.1",
|
"json-editor-vue": "^0.18.1",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.12",
|
||||||
"next-auth": "~4.21.1",
|
"next-auth": "~4.24.0",
|
||||||
"nuxt": "^3.15.4",
|
"nuxt": "^3.15.4",
|
||||||
"typescript": "5.3",
|
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.0",
|
||||||
"vite-plugin-commonjs": "^0.10.4",
|
|
||||||
"vue-advanced-cropper": "^2.8.9",
|
"vue-advanced-cropper": "^2.8.9",
|
||||||
"vue-draggable-plus": "^0.6.0",
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vuetify-nuxt-module": "0.18.3"
|
"vuetify": "^3.9.7",
|
||||||
|
"vuetify-nuxt-module": "^0.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nuxt/eslint": "1.2.0",
|
||||||
"@nuxt/types": "^2.18.1",
|
"@nuxt/types": "^2.18.1",
|
||||||
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
||||||
"@nuxtjs/eslint-module": "^4.1.0",
|
"@nuxtjs/eslint-module": "^4.1.0",
|
||||||
@@ -56,6 +54,8 @@
|
|||||||
"lint-staged": "^15.4.3",
|
"lint-staged": "^15.4.3",
|
||||||
"prettier": "^3.5.2",
|
"prettier": "^3.5.2",
|
||||||
"sass-embedded": "^1.85.1",
|
"sass-embedded": "^1.85.1",
|
||||||
|
"typescript": "5.3",
|
||||||
|
"vite-plugin-commonjs": "^0.10.4",
|
||||||
"vitest": "^3.0.7"
|
"vitest": "^3.0.7"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
max-height="125"
|
max-height="125"
|
||||||
max-width="125"
|
max-width="125"
|
||||||
:src="require('~/static/svgs/manage-group-settings.svg')"
|
src="/svgs/manage-group-settings.svg"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user