mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-13 12:30:14 -04:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a1798cd88 | ||
|
|
64f47c1589 | ||
|
|
326bb1eb8e | ||
|
|
80dc2ecfb7 | ||
|
|
b72082663f | ||
|
|
f46ae423d3 | ||
|
|
05cdff8ae7 | ||
|
|
0facdf73be | ||
|
|
cbad569134 | ||
|
|
1063433aa9 | ||
|
|
0ba22c81e7 | ||
|
|
0667177a2e | ||
|
|
6fcf22869b | ||
|
|
20b45e57e0 | ||
|
|
7a38a52158 | ||
|
|
e27eca5571 | ||
|
|
a90b2ccafd | ||
|
|
e0d8104643 | ||
|
|
53ee64828b | ||
|
|
6f7fba5ac1 | ||
|
|
89aed15905 | ||
|
|
aac48287a4 | ||
|
|
34daaa0476 | ||
|
|
af56a3e69d | ||
|
|
0908812b47 | ||
|
|
d910fbafe8 | ||
|
|
c7692426d5 | ||
|
|
b7a615add9 | ||
|
|
3167e23b6b | ||
|
|
8b582f8682 | ||
|
|
05f648d7fb | ||
|
|
1f19133870 | ||
|
|
98273da16e | ||
|
|
f857ca18da | ||
|
|
22a0e6d608 | ||
|
|
ed806b9fec | ||
|
|
ae8b489f97 | ||
|
|
71732d4766 | ||
|
|
6695314588 | ||
|
|
c115e6d83f | ||
|
|
e3e970213c | ||
|
|
7fe358e5e7 | ||
|
|
c7f3334479 | ||
|
|
d4467f65fb | ||
|
|
27e61ec6b1 | ||
|
|
6c6dc8103d | ||
|
|
35963dad2e | ||
|
|
acd0c2cb3e | ||
|
|
28d00f7dd5 | ||
|
|
fdd3d4b37a | ||
|
|
b09a85dfab | ||
|
|
b6ceece901 |
@@ -31,7 +31,6 @@
|
|||||||
"charliermarsh.ruff",
|
"charliermarsh.ruff",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"matangover.mypy",
|
"matangover.mypy",
|
||||||
"ms-python.black-formatter",
|
|
||||||
"ms-python.pylint",
|
"ms-python.pylint",
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
|
|||||||
240
.github/copilot-instructions.md
vendored
Normal file
240
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# Mealie Development Guide for AI Agents
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Mealie is a self-hosted recipe manager, meal planner, and shopping list application with a FastAPI backend (Python 3.12) and Nuxt 3 frontend (Vue 3 + TypeScript). It uses SQLAlchemy ORM with support for SQLite and PostgreSQL databases.
|
||||||
|
|
||||||
|
**Development vs Production:**
|
||||||
|
- **Development:** Frontend (port 3000) and backend (port 9000) run as separate processes
|
||||||
|
- **Production:** Frontend is statically generated and served via FastAPI's SPA module (`mealie/routes/spa/`) in a single container
|
||||||
|
|
||||||
|
## Architecture & Key Patterns
|
||||||
|
|
||||||
|
### Backend Architecture (mealie/)
|
||||||
|
|
||||||
|
**Repository-Service-Controller Pattern:**
|
||||||
|
- **Controllers** (`mealie/routes/**/controller_*.py`): Inherit from `BaseUserController` or `BaseAdminController`, handle HTTP concerns, delegate to services
|
||||||
|
- **Services** (`mealie/services/`): Business logic layer, inherit from `BaseService`, coordinate repos and external dependencies
|
||||||
|
- **Repositories** (`mealie/repos/`): Data access layer using SQLAlchemy, accessed via `AllRepositories` factory
|
||||||
|
- Get repos via dependency injection: `repos: AllRepositories = Depends(get_repositories)`
|
||||||
|
- All repos scoped to group/household context automatically
|
||||||
|
|
||||||
|
**Route Organization:**
|
||||||
|
- Routes in `mealie/routes/` organized by domain (auth, recipe, groups, households, admin)
|
||||||
|
- Use `APIRouter` with FastAPI dependency injection
|
||||||
|
- Apply `@router.get/post/put/delete` decorators with Pydantic response models
|
||||||
|
- Route controllers use `HttpRepo` mixin for common CRUD operations (see `mealie/routes/_base/mixins.py`)
|
||||||
|
|
||||||
|
**Schemas & Type Generation:**
|
||||||
|
- Pydantic schemas in `mealie/schema/` with strict separation: `*In`, `*Out`, `*Create`, `*Update` suffixes
|
||||||
|
- Auto-exported from submodules via `__init__.py` files (generated by `task dev:generate`)
|
||||||
|
- TypeScript types auto-generated from Pydantic schemas - **never manually edit** `frontend/lib/api/types/`
|
||||||
|
|
||||||
|
**Database & Sessions:**
|
||||||
|
- Session management via `Depends(generate_session)` in FastAPI routes
|
||||||
|
- Use `session_context()` context manager in services/scripts
|
||||||
|
- SQLAlchemy models in `mealie/db/models/`, migrations in `mealie/alembic/`
|
||||||
|
- Create migrations: `task py:migrate -- "description"`
|
||||||
|
|
||||||
|
### Frontend Architecture (frontend/)
|
||||||
|
|
||||||
|
**Component Organization (strict naming conventions):**
|
||||||
|
- **Domain Components** (`components/Domain/`): Feature-specific, prefix with domain (e.g., `AdminDashboard`)
|
||||||
|
- **Global Components** (`components/global/`): Reusable primitives, prefix with `Base` (e.g., `BaseButton`)
|
||||||
|
- **Layout Components** (`components/Layout/`): Layout-only, prefix with `App` if props or `The` if singleton
|
||||||
|
- **Page Components** (`components/` with page prefix): Last resort for breaking up complex pages
|
||||||
|
|
||||||
|
**API Client Pattern:**
|
||||||
|
- API clients in `frontend/lib/api/` extend `BaseAPI`, `BaseCRUDAPI`, or `BaseCRUDAPIReadOnly`
|
||||||
|
- Types imported from auto-generated `frontend/lib/api/types/` (DO NOT EDIT MANUALLY)
|
||||||
|
- Composables in `frontend/composables/` for shared state and API logic (e.g., `use-mealie-auth.ts`)
|
||||||
|
- Use `useAuthBackend()` for authentication state, `useMealieAuth()` for user management
|
||||||
|
|
||||||
|
**State Management:**
|
||||||
|
- Nuxt 3 composables for state (no Vuex)
|
||||||
|
- Auth state via `use-mealie-auth.ts` composable
|
||||||
|
- Prefer composables over global state stores
|
||||||
|
|
||||||
|
## Essential Commands (via Task/Taskfile.yml)
|
||||||
|
|
||||||
|
**Development workflow:**
|
||||||
|
```bash
|
||||||
|
task setup # Install all dependencies (Python + Node)
|
||||||
|
task dev:services # Start Postgres & Mailpit containers
|
||||||
|
task py # Start FastAPI backend (port 9000)
|
||||||
|
task ui # Start Nuxt frontend (port 3000)
|
||||||
|
task docs # Start MkDocs documentation server
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code generation (REQUIRED after schema changes):**
|
||||||
|
```bash
|
||||||
|
task dev:generate # Generate TypeScript types, schema exports, test helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing & Quality:**
|
||||||
|
```bash
|
||||||
|
task py:test # Run pytest (supports args: task py:test -- -k test_name)
|
||||||
|
task py:check # Format + lint + type-check + test (full validation)
|
||||||
|
task py:format # Ruff format
|
||||||
|
task py:lint # Ruff check
|
||||||
|
task py:mypy # Type checking
|
||||||
|
task ui:test # Vitest frontend tests
|
||||||
|
task ui:check # Frontend lint + test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
```bash
|
||||||
|
task py:migrate -- "description" # Generate Alembic migration
|
||||||
|
task py:postgres # Run backend with PostgreSQL config
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
```bash
|
||||||
|
task docker:prod # Build and run production Docker compose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Critical Development Practices
|
||||||
|
|
||||||
|
### Python Backend
|
||||||
|
|
||||||
|
1. **Always use `uv` for Python commands** (not `python` or `pip`):
|
||||||
|
```bash
|
||||||
|
uv run python mealie/app.py
|
||||||
|
uv run pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Type hints are mandatory:** Use mypy-compatible annotations, handle Optional types explicitly
|
||||||
|
|
||||||
|
3. **Dependency injection pattern:**
|
||||||
|
```python
|
||||||
|
from fastapi import Depends
|
||||||
|
from mealie.repos.all_repositories import get_repositories, AllRepositories
|
||||||
|
|
||||||
|
def my_route(
|
||||||
|
repos: AllRepositories = Depends(get_repositories),
|
||||||
|
user: PrivateUser = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
recipe = repos.recipes.get_one(recipe_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Settings & Configuration:**
|
||||||
|
- Get settings: `settings = get_app_settings()` (cached singleton)
|
||||||
|
- Get directories: `dirs = get_app_dirs()`
|
||||||
|
- Never instantiate `AppSettings()` directly
|
||||||
|
|
||||||
|
5. **Testing:**
|
||||||
|
- Fixtures in `tests/fixtures/`
|
||||||
|
- Use `api_client` fixture for integration tests
|
||||||
|
- Follow existing patterns in `tests/integration_tests/` and `tests/unit_tests/`
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
1. **Run code generation after backend schema changes:** `task dev:generate`
|
||||||
|
|
||||||
|
2. **TypeScript strict mode:** All code must pass type checking
|
||||||
|
|
||||||
|
3. **Component naming:** Follow strict conventions (see Architecture section above)
|
||||||
|
|
||||||
|
4. **API calls pattern:**
|
||||||
|
```typescript
|
||||||
|
const api = useUserApi();
|
||||||
|
const recipe = await api.recipes.getOne(recipeId);
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Composables for shared logic:** Prefer composables in `composables/` over inline code duplication
|
||||||
|
|
||||||
|
6. **Translations:** Only modify `en-US` locale files when adding new translation strings - other locales are managed via Crowdin and **must never be modified** (PRs modifying non-English locales will be rejected)
|
||||||
|
|
||||||
|
### Cross-Cutting Concerns
|
||||||
|
|
||||||
|
1. **Code generation is source of truth:** After Pydantic schema changes, run `task dev:generate` to update:
|
||||||
|
- TypeScript types (`frontend/lib/api/types/`)
|
||||||
|
- Schema exports (`mealie/schema/*/__init__.py`)
|
||||||
|
- Test data paths and routes
|
||||||
|
|
||||||
|
2. **Multi-tenancy:** All data scoped to **groups** and **households**:
|
||||||
|
- Groups contain multiple households
|
||||||
|
- Households contain recipes, meal plans, shopping lists
|
||||||
|
- Repositories automatically filter by group/household context
|
||||||
|
|
||||||
|
3. **Pre-commit hooks:** Install via `task setup:py`, enforces Ruff formatting/linting
|
||||||
|
|
||||||
|
4. **Testing before PRs:** Run `task py:check` and `task ui:check` before submitting PRs
|
||||||
|
|
||||||
|
## Pull Request Best Practices
|
||||||
|
|
||||||
|
### Before Submitting a PR
|
||||||
|
|
||||||
|
1. **Draft PRs are optional:** Create a draft PR early if you want feedback while working, or open directly as ready when complete
|
||||||
|
2. **Verify code generation:** If you modified Pydantic schemas, ensure `task dev:generate` was run
|
||||||
|
3. **Follow Conventional Commits:** Title your PR according to the conventional commits format (see PR template)
|
||||||
|
4. **Add release notes:** Include user-facing changes in the PR description
|
||||||
|
|
||||||
|
### What to Review
|
||||||
|
|
||||||
|
**Architecture & Patterns:**
|
||||||
|
- Does the code follow the repository-service-controller pattern?
|
||||||
|
- Are controllers delegating business logic to services?
|
||||||
|
- Are services coordinating repositories, not accessing the database directly?
|
||||||
|
- Is dependency injection used properly (`Depends(get_repositories)`, `Depends(get_current_user)`)?
|
||||||
|
|
||||||
|
**Data Scoping:**
|
||||||
|
- Are repositories correctly scoped to group/household context?
|
||||||
|
- Do route handlers properly validate group/household ownership before operations?
|
||||||
|
- Are multi-tenant boundaries enforced (users can't access other groups' data)?
|
||||||
|
|
||||||
|
**Type Safety:**
|
||||||
|
- Are type hints present on all functions and methods?
|
||||||
|
- Are Pydantic schemas using correct suffixes (`*In`, `*Out`, `*Create`, `*Update`)?
|
||||||
|
- For frontend, does TypeScript code pass strict type checking?
|
||||||
|
|
||||||
|
**Generated Files:**
|
||||||
|
- Verify `frontend/lib/api/types/` files weren't manually edited (they're auto-generated)
|
||||||
|
- Check that `mealie/schema/*/__init__.py` exports match actual schema files (auto-generated)
|
||||||
|
- If schemas changed, confirm generated files were updated via `task dev:generate`
|
||||||
|
|
||||||
|
**Code Quality:**
|
||||||
|
- Is the code readable and well-organized?
|
||||||
|
- Are complex operations documented with clear comments?
|
||||||
|
- Do component names follow the strict naming conventions (Domain/Global/Layout/Page prefixes)?
|
||||||
|
- Are composables used for shared frontend logic instead of duplication?
|
||||||
|
|
||||||
|
**Translations:**
|
||||||
|
- Were only `en-US` locale files modified for new translation strings?
|
||||||
|
- Verify no other locale files (managed by Crowdin) were touched
|
||||||
|
|
||||||
|
**Database Changes:**
|
||||||
|
- Are Alembic migrations included for schema changes?
|
||||||
|
- Are migrations tested against both SQLite and PostgreSQL?
|
||||||
|
|
||||||
|
### Review Etiquette
|
||||||
|
|
||||||
|
- Be constructive and specific in feedback
|
||||||
|
- Suggest code examples when proposing changes
|
||||||
|
- Focus on architecture and logic - formatting/linting is handled by CI
|
||||||
|
- Use "Approve" when ready to merge, "Request Changes" for blocking issues, "Comment" for non-blocking suggestions
|
||||||
|
|
||||||
|
## Common Gotchas
|
||||||
|
|
||||||
|
- **Don't manually edit generated files:** `frontend/lib/api/types/`, schema `__init__.py` files
|
||||||
|
- **Repository context:** Repos are group/household-scoped - passing wrong IDs causes 404s
|
||||||
|
- **Session handling:** Don't create sessions manually, use dependency injection or `session_context()`
|
||||||
|
- **Schema changes require codegen:** After changing Pydantic models, run `task dev:generate`
|
||||||
|
- **Translation files:** Only modify `en-US` locale files - all other locales are managed by Crowdin
|
||||||
|
- **Dev containers:** This project uses VS Code dev containers - leverage the pre-configured environment
|
||||||
|
- **Task commands:** Use `task` commands instead of direct tool invocation for consistency
|
||||||
|
|
||||||
|
## Key Files to Reference
|
||||||
|
|
||||||
|
- `Taskfile.yml` - All development commands and workflows
|
||||||
|
- `mealie/routes/_base/base_controllers.py` - Controller base classes and patterns
|
||||||
|
- `mealie/repos/repository_factory.py` - Repository factory and available repos
|
||||||
|
- `frontend/lib/api/base/base-clients.ts` - API client base classes
|
||||||
|
- `tests/conftest.py` - Test fixtures and setup
|
||||||
|
- `dev/code-generation/main.py` - Code generation entry point
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Documentation](https://docs.mealie.io/)
|
||||||
|
- [Contributors Guide](https://nightly.mealie.io/contributors/developers-guide/code-contributions/)
|
||||||
|
- [Discord](https://discord.gg/QuStdQGSGK)
|
||||||
132
.github/workflows/release.yml
vendored
132
.github/workflows/release.yml
vendored
@@ -5,17 +5,73 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
commit-version-bump:
|
||||||
|
name: Commit version bump to repository
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
outputs:
|
||||||
|
commit-sha: ${{ steps.commit.outputs.commit-sha }}
|
||||||
|
steps:
|
||||||
|
- name: Generate GitHub App Token
|
||||||
|
id: app-token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
|
||||||
|
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout 🛎
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Extract Version From Tag Name
|
||||||
|
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "mealie-commit-bot[bot]"
|
||||||
|
git config user.email "mealie-commit-bot[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Update all version strings
|
||||||
|
run: |
|
||||||
|
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
|
||||||
|
sed -i '/^name = "mealie"$/,/^version = / s/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' uv.lock
|
||||||
|
sed -i 's/\("version": "\)[^"]*"/\1${{ env.VERSION_NUM }}"/' frontend/package.json
|
||||||
|
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/installation-checklist.md
|
||||||
|
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||||
|
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
git add pyproject.toml frontend/package.json uv.lock docs/
|
||||||
|
git commit -m "chore: bump version to ${{ github.event.release.tag_name }}"
|
||||||
|
git push origin HEAD:${{ github.event.repository.default_branch }}
|
||||||
|
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Move release tag to new commit
|
||||||
|
run: |
|
||||||
|
git tag -f ${{ github.event.release.tag_name }}
|
||||||
|
git push -f origin ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
backend-tests:
|
backend-tests:
|
||||||
name: "Backend Server Tests"
|
name: "Backend Server Tests"
|
||||||
uses: ./.github/workflows/test-backend.yml
|
uses: ./.github/workflows/test-backend.yml
|
||||||
|
needs:
|
||||||
|
- commit-version-bump
|
||||||
|
|
||||||
frontend-tests:
|
frontend-tests:
|
||||||
name: "Frontend Tests"
|
name: "Frontend Tests"
|
||||||
uses: ./.github/workflows/test-frontend.yml
|
uses: ./.github/workflows/test-frontend.yml
|
||||||
|
needs:
|
||||||
|
- commit-version-bump
|
||||||
|
|
||||||
build-package:
|
build-package:
|
||||||
name: Build Package
|
name: Build Package
|
||||||
uses: ./.github/workflows/build-package.yml
|
uses: ./.github/workflows/build-package.yml
|
||||||
|
needs:
|
||||||
|
- commit-version-bump
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event.release.tag_name }}
|
tag: ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
@@ -43,10 +99,48 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
rollback-on-failure:
|
||||||
|
name: Rollback version commit if deployment fails
|
||||||
|
needs:
|
||||||
|
- commit-version-bump
|
||||||
|
- publish
|
||||||
|
if: always() && needs.publish.result == 'failure'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Generate GitHub App Token
|
||||||
|
id: app-token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
|
||||||
|
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout 🛎
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "mealie-commit-bot[bot]"
|
||||||
|
git config user.email "mealie-commit-bot[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Delete release tag
|
||||||
|
run: |
|
||||||
|
git push --delete origin ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
|
- name: Revert version bump commit
|
||||||
|
run: |
|
||||||
|
git revert --no-edit ${{ needs.commit-version-bump.outputs.commit-sha }}
|
||||||
|
git push origin HEAD:${{ github.event.repository.default_branch }}
|
||||||
|
|
||||||
notify-discord:
|
notify-discord:
|
||||||
name: Notify Discord
|
name: Notify Discord
|
||||||
needs:
|
needs:
|
||||||
- publish
|
- publish
|
||||||
|
if: success()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
@@ -55,41 +149,3 @@ jobs:
|
|||||||
uses: Ilshidur/action-discord@0.3.2
|
uses: Ilshidur/action-discord@0.3.2
|
||||||
with:
|
with:
|
||||||
args: "🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of Mealie has been released. See the release notes https://github.com/mealie-recipes/mealie/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}"
|
args: "🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of Mealie has been released. See the release notes https://github.com/mealie-recipes/mealie/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}"
|
||||||
|
|
||||||
update-image-tags:
|
|
||||||
name: Update image tag in sample docker-compose files
|
|
||||||
needs:
|
|
||||||
- publish
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout 🛎
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Extract Version From Tag Name
|
|
||||||
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Modify version strings
|
|
||||||
run: |
|
|
||||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/installation-checklist.md
|
|
||||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
|
||||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
|
||||||
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
|
|
||||||
sed -i 's/\("version": "\)[^"]*"/\1${{ env.VERSION_NUM }}"/' frontend/package.json
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@v6
|
|
||||||
# This doesn't currently work for us because it creates the PR but the workflows don't run.
|
|
||||||
# TODO: Provide a personal access token as a parameter here, that solves that problem.
|
|
||||||
# https://github.com/peter-evans/create-pull-request
|
|
||||||
with:
|
|
||||||
commit-message: "Update image tag, for release ${{ github.event.release.tag_name }}"
|
|
||||||
branch: "docs/newrelease-update-version-${{ github.event.release.tag_name }}"
|
|
||||||
labels: |
|
|
||||||
documentation
|
|
||||||
delete-branch: true
|
|
||||||
base: mealie-next
|
|
||||||
title: "docs(auto): Update image tag, for release ${{ github.event.release.tag_name }}"
|
|
||||||
body: "Auto-generated by `.github/workflows/release.yml`, on publish of release ${{ github.event.release.tag_name }}"
|
|
||||||
|
|||||||
@@ -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.14.7
|
rev: v0.14.8
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
###############################################
|
###############################################
|
||||||
# Frontend Build
|
# Frontend Build
|
||||||
###############################################
|
###############################################
|
||||||
FROM node:24@sha256:aa648b387728c25f81ff811799bbf8de39df66d7e2d9b3ab55cc6300cb9175d9 \
|
FROM node:24@sha256:20988bcdc6dc76690023eb2505dd273bdeefddcd0bde4bfd1efe4ebf8707f747 \
|
||||||
AS frontend-builder
|
AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|||||||
@@ -145,22 +145,95 @@ Setting the following environmental variables will change the theme of the front
|
|||||||
|
|
||||||
If using YAML sequence syntax, don't include any quotes:<br>`THEME_LIGHT_PRIMARY=#E58325` or `THEME_LIGHT_PRIMARY=E58325`
|
If using YAML sequence syntax, don't include any quotes:<br>`THEME_LIGHT_PRIMARY=#E58325` or `THEME_LIGHT_PRIMARY=E58325`
|
||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
| --------------------- | :-----: | --------------------------- |
|
| --------------------- | :-----: | ---------------------------------- |
|
||||||
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
|
| THEME_LIGHT_PRIMARY | #E58325 | Main brand color and headers |
|
||||||
| THEME_LIGHT_ACCENT | #007A99 | Light Theme Config Variable |
|
| THEME_LIGHT_ACCENT | #007A99 | Buttons and interactive elements |
|
||||||
| THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable |
|
| THEME_LIGHT_SECONDARY | #973542 | Navigation and sidebar backgrounds |
|
||||||
| THEME_LIGHT_SUCCESS | #43A047 | Light Theme Config Variable |
|
| THEME_LIGHT_SUCCESS | #43A047 | Success messages and confirmations |
|
||||||
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
|
| THEME_LIGHT_INFO | #1976D2 | Information alerts and tooltips |
|
||||||
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
|
| THEME_LIGHT_WARNING | #FF6D00 | Warning notifications |
|
||||||
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
|
| THEME_LIGHT_ERROR | #EF5350 | Error messages and alerts |
|
||||||
| THEME_DARK_PRIMARY | #E58325 | Dark Theme Config Variable |
|
| THEME_DARK_PRIMARY | #E58325 | Main brand color and headers |
|
||||||
| THEME_DARK_ACCENT | #007A99 | Dark Theme Config Variable |
|
| THEME_DARK_ACCENT | #007A99 | Buttons and interactive elements |
|
||||||
| THEME_DARK_SECONDARY | #973542 | Dark Theme Config Variable |
|
| THEME_DARK_SECONDARY | #973542 | Navigation and sidebar backgrounds |
|
||||||
| THEME_DARK_SUCCESS | #43A047 | Dark Theme Config Variable |
|
| THEME_DARK_SUCCESS | #43A047 | Success messages and confirmations |
|
||||||
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
|
| THEME_DARK_INFO | #1976D2 | Information alerts and tooltips |
|
||||||
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
|
| THEME_DARK_WARNING | #FF6D00 | Warning notifications |
|
||||||
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
|
| THEME_DARK_ERROR | #EF5350 | Error messages and alerts |
|
||||||
|
|
||||||
|
#### Theming Examples
|
||||||
|
|
||||||
|
The examples below provide copy-ready Docker Compose environment configurations for three different color palettes. Copy and paste the desired theme into your `docker-compose.yml` file's environment section.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
These themes are functional and ready to use, but they are provided primarily as examples. The color palettes can be adjusted or refined to better suit your preferences.
|
||||||
|
|
||||||
|
=== "Blue Theme"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
# Light mode colors
|
||||||
|
THEME_LIGHT_PRIMARY: '#5E9BD1'
|
||||||
|
THEME_LIGHT_ACCENT: '#A3C9E8'
|
||||||
|
THEME_LIGHT_SECONDARY: '#4F89BA'
|
||||||
|
THEME_LIGHT_SUCCESS: '#4CAF50'
|
||||||
|
THEME_LIGHT_INFO: '#4A9ED8'
|
||||||
|
THEME_LIGHT_WARNING: '#EAC46B'
|
||||||
|
THEME_LIGHT_ERROR: '#E57373'
|
||||||
|
# Dark mode colors
|
||||||
|
THEME_DARK_PRIMARY: '#5A8FBF'
|
||||||
|
THEME_DARK_ACCENT: '#90B8D9'
|
||||||
|
THEME_DARK_SECONDARY: '#406D96'
|
||||||
|
THEME_DARK_SUCCESS: '#81C784'
|
||||||
|
THEME_DARK_INFO: '#78B2C0'
|
||||||
|
THEME_DARK_WARNING: '#EBC86E'
|
||||||
|
THEME_DARK_ERROR: '#E57373'
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Green Theme"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
# Light mode colors
|
||||||
|
THEME_LIGHT_PRIMARY: '#75A86C'
|
||||||
|
THEME_LIGHT_ACCENT: '#A8D0A6'
|
||||||
|
THEME_LIGHT_SECONDARY: '#638E5E'
|
||||||
|
THEME_LIGHT_SUCCESS: '#4CAF50'
|
||||||
|
THEME_LIGHT_INFO: '#4A9ED8'
|
||||||
|
THEME_LIGHT_WARNING: '#EAC46B'
|
||||||
|
THEME_LIGHT_ERROR: '#E57373'
|
||||||
|
# Dark mode colors
|
||||||
|
THEME_DARK_PRIMARY: '#739B7A'
|
||||||
|
THEME_DARK_ACCENT: '#9FBE9D'
|
||||||
|
THEME_DARK_SECONDARY: '#56775E'
|
||||||
|
THEME_DARK_SUCCESS: '#81C784'
|
||||||
|
THEME_DARK_INFO: '#78B2C0'
|
||||||
|
THEME_DARK_WARNING: '#EBC86E'
|
||||||
|
THEME_DARK_ERROR: '#E57373'
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Pink Theme"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
# Light mode colors
|
||||||
|
THEME_LIGHT_PRIMARY: '#D97C96'
|
||||||
|
THEME_LIGHT_ACCENT: '#E891A7'
|
||||||
|
THEME_LIGHT_SECONDARY: '#C86C88'
|
||||||
|
THEME_LIGHT_SUCCESS: '#4CAF50'
|
||||||
|
THEME_LIGHT_INFO: '#2196F3'
|
||||||
|
THEME_LIGHT_WARNING: '#FFC107'
|
||||||
|
THEME_LIGHT_ERROR: '#E57373'
|
||||||
|
# Dark mode colors
|
||||||
|
THEME_DARK_PRIMARY: '#C2185B'
|
||||||
|
THEME_DARK_ACCENT: '#FF80AB'
|
||||||
|
THEME_DARK_SECONDARY: '#AD1457'
|
||||||
|
THEME_DARK_SUCCESS: '#81C784'
|
||||||
|
THEME_DARK_INFO: '#64B5F6'
|
||||||
|
THEME_DARK_WARNING: '#FFD54F'
|
||||||
|
THEME_DARK_ERROR: '#E57373'
|
||||||
|
```
|
||||||
|
|
||||||
### Docker Secrets
|
### Docker Secrets
|
||||||
|
|
||||||
|
|||||||
@@ -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.5.0`
|
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.7.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
|
||||||
|
|
||||||
|
|||||||
@@ -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.5.0 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.7.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.5.0 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.7.0 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -9,6 +9,23 @@
|
|||||||
- Create a Backup and Download from the UI
|
- Create a Backup and Download from the UI
|
||||||
- Upgrade
|
- Upgrade
|
||||||
|
|
||||||
|
!!! info "Improved Image Processing"
|
||||||
|
Starting with :octicons-tag-24: v3.7.0, we updated our image processing algorithm to improve image quality and compression. New image processing can be up to 40%-50% smaller on disk while providing higher resolution thumbnails. To take advantage of these improvements on older recipes, you can run our image-processing script:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker exec -it mealie bash
|
||||||
|
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/reprocess_images.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
- `--workers N`: Number of worker threads (default: 2, safe for low-powered devices)
|
||||||
|
- `--force-all`: Reprocess all recipes regardless of current image state
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```shell
|
||||||
|
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/reprocess_images.py --workers 8
|
||||||
|
```
|
||||||
|
|
||||||
## Upgrading to Mealie v1 or later
|
## Upgrading to Mealie v1 or later
|
||||||
If you are upgrading from pre-v1.0.0 to v1.0.0 or later (v2.0.0, etc.), make sure you read [Migrating to Mealie v1](./migrating-to-mealie-v1.md)!
|
If you are upgrading from pre-v1.0.0 to v1.0.0 or later (v2.0.0, etc.), make sure you read [Migrating to Mealie v1](./migrating-to-mealie-v1.md)!
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
121
frontend/components/Domain/Admin/Setup/EndPageContent.vue
Normal file
121
frontend/components/Domain/Admin/Setup/EndPageContent.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<v-container max-width="880" class="end-page-content">
|
||||||
|
<div class="d-flex flex-column ga-6">
|
||||||
|
<div>
|
||||||
|
<v-card-title class="text-h4 justify-center">
|
||||||
|
{{ $t('admin.setup.setup-complete') }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-subtitle class="justify-center">
|
||||||
|
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
|
||||||
|
</v-card-subtitle>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="section, idx in sections"
|
||||||
|
:key="idx"
|
||||||
|
class="d-flex flex-column ga-3"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-h6 pl-0">
|
||||||
|
{{ section.title }}
|
||||||
|
</v-card-title>
|
||||||
|
<div class="sections d-flex flex-column ga-2">
|
||||||
|
<v-card
|
||||||
|
v-for="link, linkIdx in section.links"
|
||||||
|
:key="linkIdx"
|
||||||
|
clas="link-card"
|
||||||
|
:href="link.to"
|
||||||
|
:title="link.text"
|
||||||
|
:subtitle="link.description"
|
||||||
|
:append-icon="$globals.icons.chevronRight"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-avatar :icon="link.icon || undefined" variant="tonal" :color="section.color" />
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
setup() {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
|
const sections = ref([
|
||||||
|
{
|
||||||
|
title: i18n.t("profile.data-migrations"),
|
||||||
|
color: "info",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
icon: $globals.icons.backupRestore,
|
||||||
|
to: "/admin/backups",
|
||||||
|
text: i18n.t("settings.backup.backup-restore"),
|
||||||
|
description: i18n.t("admin.setup.restore-from-v1-backup"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.import,
|
||||||
|
to: "/group/migrations",
|
||||||
|
text: i18n.t("migration.recipe-migration"),
|
||||||
|
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t("recipe.create-recipes"),
|
||||||
|
color: "success",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
icon: $globals.icons.createAlt,
|
||||||
|
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
|
||||||
|
text: i18n.t("recipe.create-recipe"),
|
||||||
|
description: i18n.t("recipe.create-recipe-description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.link,
|
||||||
|
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
|
||||||
|
text: i18n.t("recipe.import-with-url"),
|
||||||
|
description: i18n.t("recipe.scrape-recipe-description"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t("user.manage-users"),
|
||||||
|
color: "primary",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
icon: $globals.icons.group,
|
||||||
|
to: "/admin/manage/users",
|
||||||
|
text: i18n.t("user.manage-users"),
|
||||||
|
description: i18n.t("user.manage-users-description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.user,
|
||||||
|
to: "/user/profile",
|
||||||
|
text: i18n.t("profile.manage-user-profile"),
|
||||||
|
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return { sections };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.v-container {
|
||||||
|
.v-card-title,
|
||||||
|
.v-card-subtitle {
|
||||||
|
padding: 0;
|
||||||
|
white-space: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-item {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -83,6 +83,11 @@ const fieldDefs: FieldDefinition[] = [
|
|||||||
label: i18n.t("household.households"),
|
label: i18n.t("household.households"),
|
||||||
type: Organizer.Household,
|
type: Organizer.Household,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "user_id",
|
||||||
|
label: i18n.t("user.users"),
|
||||||
|
type: Organizer.User,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "created_at",
|
name: "created_at",
|
||||||
label: i18n.t("general.date-created"),
|
label: i18n.t("general.date-created"),
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ const MEAL_TYPE_OPTIONS = [
|
|||||||
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||||
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||||
{ title: i18n.t("meal-plan.side"), value: "side" },
|
{ title: i18n.t("meal-plan.side"), value: "side" },
|
||||||
|
{ title: i18n.t("meal-plan.snack"), value: "snack" },
|
||||||
|
{ title: i18n.t("meal-plan.drink"), value: "drink" },
|
||||||
|
{ title: i18n.t("meal-plan.dessert"), value: "dessert" },
|
||||||
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -103,6 +106,11 @@ const fieldDefs: FieldDefinition[] = [
|
|||||||
label: i18n.t("household.households"),
|
label: i18n.t("household.households"),
|
||||||
type: Organizer.Household,
|
type: Organizer.Household,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "user_id",
|
||||||
|
label: i18n.t("user.users"),
|
||||||
|
type: Organizer.User,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "last_made",
|
name: "last_made",
|
||||||
label: i18n.t("general.last-made"),
|
label: i18n.t("general.last-made"),
|
||||||
|
|||||||
@@ -1,283 +1,297 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card class="ma-0" style="overflow-x: auto;">
|
<v-card class="ma-0" flat fluid>
|
||||||
<v-card-text class="ma-0 pa-0">
|
<v-card-text class="ma-0 pa-0">
|
||||||
<v-container fluid class="ma-0 pa-0">
|
<VueDraggable
|
||||||
<VueDraggable
|
v-model="fields"
|
||||||
v-model="fields"
|
handle=".handle"
|
||||||
handle=".handle"
|
:delay="250"
|
||||||
:delay="250"
|
:delay-on-touch-only="true"
|
||||||
:delay-on-touch-only="true"
|
v-bind="{
|
||||||
v-bind="{
|
animation: 200,
|
||||||
animation: 200,
|
group: 'recipe-instructions',
|
||||||
group: 'recipe-instructions',
|
ghostClass: 'ghost',
|
||||||
ghostClass: 'ghost',
|
}"
|
||||||
}"
|
@start="drag = true"
|
||||||
@start="drag = true"
|
@end="onDragEnd"
|
||||||
@end="onDragEnd"
|
>
|
||||||
|
<v-row
|
||||||
|
v-for="(field, index) in fields"
|
||||||
|
:key="field.id"
|
||||||
|
class="d-flex flex-row flex-wrap mx-auto pb-2"
|
||||||
|
:class="$vuetify.display.xs ? (Math.floor(index / 1) % 2 === 0 ? 'bg-dark' : 'bg-light') : ''"
|
||||||
|
style="max-width: 100%;"
|
||||||
>
|
>
|
||||||
<v-row
|
<!-- drag handle -->
|
||||||
v-for="(field, index) in fields"
|
<v-col
|
||||||
:key="field.id"
|
:cols="config.items.icon.cols(index)"
|
||||||
class="d-flex flex-nowrap"
|
:sm="config.items.icon.sm(index)"
|
||||||
style="max-width: 100%;"
|
:class="$vuetify.display.smAndDown ? 'd-flex pa-0' : 'd-flex justify-end pr-6'"
|
||||||
>
|
>
|
||||||
<!-- drag handle -->
|
<v-icon class="handle my-auto" :size="28" style="cursor: move;">
|
||||||
<v-col
|
{{ $globals.icons.arrowUpDown }}
|
||||||
:cols="config.items.icon.cols"
|
</v-icon>
|
||||||
:class="config.col.class"
|
</v-col>
|
||||||
:style="config.items.icon.style"
|
|
||||||
|
<!-- and / or -->
|
||||||
|
<v-col
|
||||||
|
v-if="index != 0 || $vuetify.display.smAndUp"
|
||||||
|
:cols="config.items.logicalOperator.cols(index)"
|
||||||
|
:sm="config.items.logicalOperator.sm(index)"
|
||||||
|
:class="config.col.class"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-if="index"
|
||||||
|
:model-value="field.logicalOperator"
|
||||||
|
:items="[logOps.AND, logOps.OR]"
|
||||||
|
item-title="label"
|
||||||
|
item-value="value"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
|
||||||
>
|
>
|
||||||
<v-icon
|
<template #chip="{ item }">
|
||||||
class="handle"
|
<span :class="config.select.textClass" style="width: 100%;">
|
||||||
:size="24"
|
{{ item.raw.label }}
|
||||||
style="cursor: move;margin: auto;"
|
</span>
|
||||||
>
|
</template>
|
||||||
{{ $globals.icons.arrowUpDown }}
|
</v-select>
|
||||||
</v-icon>
|
</v-col>
|
||||||
</v-col>
|
|
||||||
<!-- and / or -->
|
<!-- left parenthesis -->
|
||||||
<v-col
|
<v-col
|
||||||
:cols="config.items.logicalOperator.cols"
|
v-if="showAdvanced"
|
||||||
:class="config.col.class"
|
:cols="config.items.leftParens.cols(index)"
|
||||||
:style="config.items.logicalOperator.style"
|
:sm="config.items.leftParens.sm(index)"
|
||||||
|
:class="config.col.class"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
:model-value="field.leftParenthesis"
|
||||||
|
:items="['', '(', '((', '(((']"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="setLeftParenthesisValue(field, index, $event)"
|
||||||
>
|
>
|
||||||
<v-select
|
<template #chip="{ item }">
|
||||||
v-if="index"
|
<span :class="config.select.textClass" style="width: 100%;">
|
||||||
:model-value="field.logicalOperator"
|
{{ item.raw }}
|
||||||
:items="[logOps.AND, logOps.OR]"
|
</span>
|
||||||
item-title="label"
|
</template>
|
||||||
item-value="value"
|
</v-select>
|
||||||
variant="underlined"
|
</v-col>
|
||||||
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
|
|
||||||
>
|
<!-- field name -->
|
||||||
<template #chip="{ item }">
|
<v-col
|
||||||
<span :class="config.select.textClass" style="width: 100%;">
|
:cols="config.items.fieldName.cols(index)"
|
||||||
{{ item.raw.label }}
|
:sm="config.items.fieldName.sm(index)"
|
||||||
</span>
|
:class="config.col.class"
|
||||||
</template>
|
>
|
||||||
</v-select>
|
<v-select
|
||||||
</v-col>
|
chips
|
||||||
<!-- left parenthesis -->
|
:model-value="field.label"
|
||||||
<v-col
|
:items="fieldDefs"
|
||||||
v-if="showAdvanced"
|
variant="underlined"
|
||||||
:cols="config.items.leftParens.cols"
|
item-title="label"
|
||||||
:class="config.col.class"
|
@update:model-value="setField(index, $event)"
|
||||||
:style="config.items.leftParens.style"
|
|
||||||
>
|
>
|
||||||
<v-select
|
<template #chip="{ item }">
|
||||||
:model-value="field.leftParenthesis"
|
<span :class="config.select.textClass" style="width: 100%;">
|
||||||
:items="['', '(', '((', '(((']"
|
{{ item.raw.label }}
|
||||||
variant="underlined"
|
</span>
|
||||||
@update:model-value="setLeftParenthesisValue(field, index, $event)"
|
</template>
|
||||||
>
|
</v-select>
|
||||||
<template #chip="{ item }">
|
</v-col>
|
||||||
<span :class="config.select.textClass" style="width: 100%;">
|
|
||||||
{{ item.raw }}
|
<!-- relational operator -->
|
||||||
</span>
|
<v-col
|
||||||
</template>
|
:cols="config.items.relationalOperator.cols(index)"
|
||||||
</v-select>
|
:sm="config.items.relationalOperator.sm(index)"
|
||||||
</v-col>
|
:class="config.col.class"
|
||||||
<!-- field name -->
|
>
|
||||||
<v-col
|
<v-select
|
||||||
:cols="config.items.fieldName.cols"
|
v-if="field.type !== 'boolean'"
|
||||||
:class="config.col.class"
|
:model-value="field.relationalOperatorValue"
|
||||||
:style="config.items.fieldName.style"
|
:items="field.relationalOperatorOptions"
|
||||||
|
item-title="label"
|
||||||
|
item-value="value"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
|
||||||
>
|
>
|
||||||
<v-select
|
<template #chip="{ item }">
|
||||||
chips
|
<span :class="config.select.textClass" style="width: 100%;">
|
||||||
:model-value="field.label"
|
{{ item.raw.label }}
|
||||||
:items="fieldDefs"
|
</span>
|
||||||
variant="underlined"
|
</template>
|
||||||
item-title="label"
|
</v-select>
|
||||||
@update:model-value="setField(index, $event)"
|
</v-col>
|
||||||
>
|
|
||||||
<template #chip="{ item }">
|
<!-- field value -->
|
||||||
<span :class="config.select.textClass" style="width: 100%;">
|
<v-col
|
||||||
{{ item.raw.label }}
|
:cols="config.items.fieldValue.cols(index)"
|
||||||
</span>
|
:sm="config.items.fieldValue.sm(index)"
|
||||||
</template>
|
:class="config.col.class"
|
||||||
</v-select>
|
>
|
||||||
</v-col>
|
<v-select
|
||||||
<!-- relational operator -->
|
v-if="field.fieldOptions"
|
||||||
<v-col
|
:model-value="field.values"
|
||||||
:cols="config.items.relationalOperator.cols"
|
:items="field.fieldOptions"
|
||||||
:class="config.col.class"
|
item-title="label"
|
||||||
:style="config.items.relationalOperator.style"
|
item-value="value"
|
||||||
|
multiple
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="setFieldValues(field, index, $event)"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-else-if="field.type === 'string'"
|
||||||
|
:model-value="field.value"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="setFieldValue(field, index, $event)"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-else-if="field.type === 'number'"
|
||||||
|
:model-value="field.value"
|
||||||
|
type="number"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="setFieldValue(field, index, $event)"
|
||||||
|
/>
|
||||||
|
<v-checkbox
|
||||||
|
v-else-if="field.type === 'boolean'"
|
||||||
|
:model-value="field.value"
|
||||||
|
@update:model-value="setFieldValue(field, index, $event!)"
|
||||||
|
/>
|
||||||
|
<v-menu
|
||||||
|
v-else-if="field.type === 'date'"
|
||||||
|
v-model="datePickers[index]"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
transition="scale-transition"
|
||||||
|
offset-y
|
||||||
|
max-width="290px"
|
||||||
|
min-width="auto"
|
||||||
>
|
>
|
||||||
<v-select
|
<template #activator="{ props: activatorProps }">
|
||||||
v-if="field.type !== 'boolean'"
|
<v-text-field
|
||||||
:model-value="field.relationalOperatorValue"
|
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
|
||||||
:items="field.relationalOperatorOptions"
|
persistent-hint
|
||||||
item-title="label"
|
:prepend-icon="$globals.icons.calendar"
|
||||||
item-value="value"
|
variant="underlined"
|
||||||
variant="underlined"
|
color="primary"
|
||||||
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
|
v-bind="activatorProps"
|
||||||
>
|
readonly
|
||||||
<template #chip="{ item }">
|
|
||||||
<span :class="config.select.textClass" style="width: 100%;">
|
|
||||||
{{ item.raw.label }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
</v-col>
|
|
||||||
<!-- field value -->
|
|
||||||
<v-col
|
|
||||||
:cols="config.items.fieldValue.cols"
|
|
||||||
:class="config.col.class"
|
|
||||||
:style="config.items.fieldValue.style"
|
|
||||||
>
|
|
||||||
<v-select
|
|
||||||
v-if="field.fieldOptions"
|
|
||||||
:model-value="field.values"
|
|
||||||
:items="field.fieldOptions"
|
|
||||||
item-title="label"
|
|
||||||
item-value="value"
|
|
||||||
multiple
|
|
||||||
variant="underlined"
|
|
||||||
@update:model-value="setFieldValues(field, index, $event)"
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-else-if="field.type === 'string'"
|
|
||||||
:model-value="field.value"
|
|
||||||
variant="underlined"
|
|
||||||
@update:model-value="setFieldValue(field, index, $event)"
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-else-if="field.type === 'number'"
|
|
||||||
:model-value="field.value"
|
|
||||||
type="number"
|
|
||||||
variant="underlined"
|
|
||||||
@update:model-value="setFieldValue(field, index, $event)"
|
|
||||||
/>
|
|
||||||
<v-checkbox
|
|
||||||
v-else-if="field.type === 'boolean'"
|
|
||||||
:model-value="field.value"
|
|
||||||
@update:model-value="setFieldValue(field, index, $event!)"
|
|
||||||
/>
|
|
||||||
<v-menu
|
|
||||||
v-else-if="field.type === 'date'"
|
|
||||||
v-model="datePickers[index]"
|
|
||||||
:close-on-content-click="false"
|
|
||||||
transition="scale-transition"
|
|
||||||
offset-y
|
|
||||||
max-width="290px"
|
|
||||||
min-width="auto"
|
|
||||||
>
|
|
||||||
<template #activator="{ props: activatorProps }">
|
|
||||||
<v-text-field
|
|
||||||
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
|
|
||||||
persistent-hint
|
|
||||||
:prepend-icon="$globals.icons.calendar"
|
|
||||||
variant="underlined"
|
|
||||||
color="primary"
|
|
||||||
v-bind="activatorProps"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<v-date-picker
|
|
||||||
:model-value="field.value ? new Date(field.value + 'T00:00:00') : null"
|
|
||||||
hide-header
|
|
||||||
:first-day-of-week="firstDayOfWeek"
|
|
||||||
:local="$i18n.locale"
|
|
||||||
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
|
|
||||||
/>
|
/>
|
||||||
</v-menu>
|
</template>
|
||||||
<RecipeOrganizerSelector
|
<v-date-picker
|
||||||
v-else-if="field.type === Organizer.Category"
|
:model-value="field.value ? new Date(field.value + 'T00:00:00') : null"
|
||||||
v-model="field.organizers"
|
hide-header
|
||||||
:selector-type="Organizer.Category"
|
:first-day-of-week="firstDayOfWeek"
|
||||||
:show-add="false"
|
:local="$i18n.locale"
|
||||||
:show-label="false"
|
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
|
||||||
:show-icon="false"
|
|
||||||
variant="underlined"
|
|
||||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
</v-menu>
|
||||||
v-else-if="field.type === Organizer.Tag"
|
<RecipeOrganizerSelector
|
||||||
v-model="field.organizers"
|
v-else-if="field.type === Organizer.Category"
|
||||||
:selector-type="Organizer.Tag"
|
v-model="field.organizers"
|
||||||
:show-add="false"
|
:selector-type="Organizer.Category"
|
||||||
:show-label="false"
|
:show-add="false"
|
||||||
:show-icon="false"
|
:show-label="false"
|
||||||
variant="underlined"
|
:show-icon="false"
|
||||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
variant="underlined"
|
||||||
/>
|
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
|
||||||
<RecipeOrganizerSelector
|
/>
|
||||||
v-else-if="field.type === Organizer.Tool"
|
<RecipeOrganizerSelector
|
||||||
v-model="field.organizers"
|
v-else-if="field.type === Organizer.Tag"
|
||||||
:selector-type="Organizer.Tool"
|
v-model="field.organizers"
|
||||||
:show-add="false"
|
:selector-type="Organizer.Tag"
|
||||||
:show-label="false"
|
:show-add="false"
|
||||||
:show-icon="false"
|
:show-label="false"
|
||||||
variant="underlined"
|
:show-icon="false"
|
||||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
variant="underlined"
|
||||||
/>
|
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
|
||||||
<RecipeOrganizerSelector
|
/>
|
||||||
v-else-if="field.type === Organizer.Food"
|
<RecipeOrganizerSelector
|
||||||
v-model="field.organizers"
|
v-else-if="field.type === Organizer.Tool"
|
||||||
:selector-type="Organizer.Food"
|
v-model="field.organizers"
|
||||||
:show-add="false"
|
:selector-type="Organizer.Tool"
|
||||||
:show-label="false"
|
:show-add="false"
|
||||||
:show-icon="false"
|
:show-label="false"
|
||||||
variant="underlined"
|
:show-icon="false"
|
||||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
variant="underlined"
|
||||||
/>
|
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
|
||||||
<RecipeOrganizerSelector
|
/>
|
||||||
v-else-if="field.type === Organizer.Household"
|
<RecipeOrganizerSelector
|
||||||
v-model="field.organizers"
|
v-else-if="field.type === Organizer.Food"
|
||||||
:selector-type="Organizer.Household"
|
v-model="field.organizers"
|
||||||
:show-add="false"
|
:selector-type="Organizer.Food"
|
||||||
:show-label="false"
|
:show-add="false"
|
||||||
:show-icon="false"
|
:show-label="false"
|
||||||
variant="underlined"
|
:show-icon="false"
|
||||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
variant="underlined"
|
||||||
/>
|
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
|
||||||
</v-col>
|
/>
|
||||||
<!-- right parenthesis -->
|
<RecipeOrganizerSelector
|
||||||
<v-col
|
v-else-if="field.type === Organizer.Household"
|
||||||
v-if="showAdvanced"
|
v-model="field.organizers"
|
||||||
:cols="config.items.rightParens.cols"
|
:selector-type="Organizer.Household"
|
||||||
:class="config.col.class"
|
:show-add="false"
|
||||||
:style="config.items.rightParens.style"
|
:show-label="false"
|
||||||
|
:show-icon="false"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
|
||||||
|
/>
|
||||||
|
<RecipeOrganizerSelector
|
||||||
|
v-else-if="field.type === Organizer.User"
|
||||||
|
v-model="field.organizers"
|
||||||
|
:selector-type="Organizer.User"
|
||||||
|
:show-add="false"
|
||||||
|
:show-label="false"
|
||||||
|
:show-icon="false"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- right parenthesis -->
|
||||||
|
<v-col
|
||||||
|
v-if="showAdvanced"
|
||||||
|
:cols="config.items.rightParens.cols(index)"
|
||||||
|
:sm="config.items.rightParens.sm(index)"
|
||||||
|
:class="config.col.class"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
:model-value="field.rightParenthesis"
|
||||||
|
:items="['', ')', '))', ')))']"
|
||||||
|
variant="underlined"
|
||||||
|
@update:model-value="setRightParenthesisValue(field, index, $event)"
|
||||||
>
|
>
|
||||||
<v-select
|
<template #chip="{ item }">
|
||||||
:model-value="field.rightParenthesis"
|
<span :class="config.select.textClass" style="width: 100%;">
|
||||||
:items="['', ')', '))', ')))']"
|
{{ item.raw }}
|
||||||
variant="underlined"
|
</span>
|
||||||
@update:model-value="setRightParenthesisValue(field, index, $event)"
|
</template>
|
||||||
>
|
</v-select>
|
||||||
<template #chip="{ item }">
|
</v-col>
|
||||||
<span :class="config.select.textClass" style="width: 100%;">
|
|
||||||
{{ item.raw }}
|
<!-- field actions -->
|
||||||
</span>
|
<v-col
|
||||||
</template>
|
v-if="!$vuetify.display.smAndDown || index === fields.length - 1"
|
||||||
</v-select>
|
:cols="config.items.fieldActions.cols(index)"
|
||||||
</v-col>
|
:sm="config.items.fieldActions.sm(index)"
|
||||||
<!-- field actions -->
|
:class="config.col.class"
|
||||||
<v-col
|
>
|
||||||
:cols="config.items.fieldActions.cols"
|
<BaseButtonGroup
|
||||||
:class="config.col.class"
|
:buttons="[
|
||||||
:style="config.items.fieldActions.style"
|
{
|
||||||
>
|
icon: $globals.icons.delete,
|
||||||
<BaseButtonGroup
|
text: $t('general.delete'),
|
||||||
:buttons="[
|
event: 'delete',
|
||||||
{
|
disabled: fields.length === 1,
|
||||||
icon: $globals.icons.delete,
|
},
|
||||||
text: $t('general.delete'),
|
]"
|
||||||
event: 'delete',
|
class="my-auto"
|
||||||
disabled: fields.length === 1,
|
@delete="removeField(index)"
|
||||||
},
|
/>
|
||||||
]"
|
</v-col>
|
||||||
class="my-auto"
|
</v-row>
|
||||||
@delete="removeField(index)"
|
</VueDraggable>
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</VueDraggable>
|
|
||||||
</v-container>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-row fluid class="d-flex justify-end pa-0 mx-2">
|
<v-row fluid class="d-flex justify-end ma-2">
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="showAdvanced"
|
v-model="showAdvanced"
|
||||||
@@ -305,6 +319,7 @@ import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerS
|
|||||||
import { Organizer } from "~/lib/api/types/non-generated";
|
import { Organizer } from "~/lib/api/types/non-generated";
|
||||||
import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
|
import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
|
||||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
|
import { useUserStore } from "~/composables/store/use-user-store";
|
||||||
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -344,6 +359,7 @@ const storeMap = {
|
|||||||
[Organizer.Tool]: useToolStore(),
|
[Organizer.Tool]: useToolStore(),
|
||||||
[Organizer.Food]: useFoodStore(),
|
[Organizer.Food]: useFoodStore(),
|
||||||
[Organizer.Household]: useHouseholdStore(),
|
[Organizer.Household]: useHouseholdStore(),
|
||||||
|
[Organizer.User]: useUserStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
function onDragEnd(event: any) {
|
function onDragEnd(event: any) {
|
||||||
@@ -602,46 +618,56 @@ function buildQueryFilterJSON(): QueryFilterJSON {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = computed(() => {
|
const config = computed(() => {
|
||||||
const baseColMaxWidth = 55;
|
const multiple = fields.value.length > 1;
|
||||||
|
const adv = state.showAdvanced;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
col: {
|
col: {
|
||||||
class: "d-flex justify-center align-end field-col pa-1",
|
class: "d-flex justify-center align-end py-0",
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
textClass: "d-flex justify-center text-center",
|
textClass: "d-flex justify-center text-center",
|
||||||
},
|
},
|
||||||
items: {
|
items: {
|
||||||
icon: {
|
icon: {
|
||||||
cols: 1,
|
cols: (_index: number) => 2,
|
||||||
|
sm: (_index: number) => 1,
|
||||||
style: "width: fit-content;",
|
style: "width: fit-content;",
|
||||||
},
|
},
|
||||||
leftParens: {
|
leftParens: {
|
||||||
cols: state.showAdvanced ? 1 : 0,
|
cols: (index: number) => (adv ? (index === 0 ? 2 : 0) : 0),
|
||||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
|
sm: (_index: number) => (adv ? 1 : 0),
|
||||||
},
|
},
|
||||||
logicalOperator: {
|
logicalOperator: {
|
||||||
cols: 1,
|
cols: (_index: number) => 0,
|
||||||
style: `min-width: ${baseColMaxWidth}px;`,
|
sm: (_index: number) => (multiple ? 1 : 0),
|
||||||
},
|
},
|
||||||
fieldName: {
|
fieldName: {
|
||||||
cols: state.showAdvanced ? 2 : 3,
|
cols: (index: number) => {
|
||||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
|
if (adv) return index === 0 ? 8 : 12;
|
||||||
|
return index === 0 ? 10 : 12;
|
||||||
|
},
|
||||||
|
sm: (_index: number) => (adv ? 2 : 3),
|
||||||
},
|
},
|
||||||
relationalOperator: {
|
relationalOperator: {
|
||||||
cols: 2,
|
cols: (_index: number) => 12,
|
||||||
style: `min-width: ${baseColMaxWidth * 2}px;`,
|
sm: (_index: number) => 2,
|
||||||
},
|
},
|
||||||
fieldValue: {
|
fieldValue: {
|
||||||
cols: state.showAdvanced ? 3 : 4,
|
cols: (index: number) => {
|
||||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
|
const last = index === fields.value.length - 1;
|
||||||
|
if (adv) return last ? 8 : 10;
|
||||||
|
return last ? 10 : 12;
|
||||||
|
},
|
||||||
|
sm: (_index: number) => (adv ? 3 : 4),
|
||||||
},
|
},
|
||||||
rightParens: {
|
rightParens: {
|
||||||
cols: state.showAdvanced ? 1 : 0,
|
cols: (index: number) => (adv ? (index === fields.value.length - 1 ? 2 : 0) : 0),
|
||||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
|
sm: (_index: number) => (adv ? 1 : 0),
|
||||||
},
|
},
|
||||||
fieldActions: {
|
fieldActions: {
|
||||||
cols: 1,
|
cols: (index: number) => (index === fields.value.length - 1 ? 2 : 0),
|
||||||
style: `min-width: ${baseColMaxWidth}px;`,
|
sm: (_index: number) => 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -651,5 +677,14 @@ const config = computed(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
* {
|
* {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
--bg-opactity: calc(var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-dark {
|
||||||
|
background-color: rgba(0, 0, 0, var(--bg-opactity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-light {
|
||||||
|
background-color: rgba(255, 255, 255, var(--bg-opactity));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
@click.self="$emit('click')"
|
@click.self="$emit('click')"
|
||||||
>
|
>
|
||||||
<RecipeCardImage
|
<RecipeCardImage
|
||||||
|
small
|
||||||
:icon-size="imageHeight"
|
:icon-size="imageHeight"
|
||||||
:height="imageHeight"
|
:height="imageHeight"
|
||||||
:slug="slug"
|
:slug="slug"
|
||||||
:recipe-id="recipeId"
|
:recipe-id="recipeId"
|
||||||
size="small"
|
|
||||||
:image-version="image"
|
:image-version="image"
|
||||||
>
|
>
|
||||||
<v-expand-transition v-if="description">
|
<v-expand-transition v-if="description">
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
cover
|
cover
|
||||||
>
|
>
|
||||||
<RecipeCardImage
|
<RecipeCardImage
|
||||||
|
tiny
|
||||||
:icon-size="100"
|
:icon-size="100"
|
||||||
:slug="slug"
|
:slug="slug"
|
||||||
:recipe-id="recipeId"
|
:recipe-id="recipeId"
|
||||||
size="small"
|
|
||||||
:image-version="image"
|
:image-version="image"
|
||||||
:height="height"
|
:height="height"
|
||||||
/>
|
/>
|
||||||
@@ -41,11 +41,11 @@
|
|||||||
name="avatar"
|
name="avatar"
|
||||||
>
|
>
|
||||||
<RecipeCardImage
|
<RecipeCardImage
|
||||||
|
tiny
|
||||||
:icon-size="100"
|
:icon-size="100"
|
||||||
:slug="slug"
|
:slug="slug"
|
||||||
:recipe-id="recipeId"
|
:recipe-id="recipeId"
|
||||||
:image-version="image"
|
:image-version="image"
|
||||||
size="small"
|
|
||||||
width="125"
|
width="125"
|
||||||
:height="height"
|
:height="height"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -45,31 +45,15 @@
|
|||||||
@confirm="addRecipeToPlan()"
|
@confirm="addRecipeToPlan()"
|
||||||
>
|
>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-menu
|
<v-date-picker
|
||||||
v-model="pickerMenu"
|
v-model="newMealdate"
|
||||||
:close-on-content-click="false"
|
class="mx-auto mb-3"
|
||||||
transition="scale-transition"
|
hide-header
|
||||||
offset-y
|
show-adjacent-months
|
||||||
max-width="290px"
|
color="primary"
|
||||||
min-width="auto"
|
:first-day-of-week="firstDayOfWeek"
|
||||||
>
|
:local="$i18n.locale"
|
||||||
<template #activator="{ props: activatorProps }">
|
/>
|
||||||
<v-text-field
|
|
||||||
:model-value="$d(newMealdate)"
|
|
||||||
: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-select
|
||||||
v-model="newMealType"
|
v-model="newMealType"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
@@ -207,7 +191,6 @@ const loading = ref(false);
|
|||||||
const menuItems = ref<ContextMenuItem[]>([]);
|
const menuItems = ref<ContextMenuItem[]>([]);
|
||||||
const newMealdate = ref(new Date());
|
const newMealdate = ref(new Date());
|
||||||
const newMealType = ref<PlanEntryType>("dinner");
|
const newMealType = ref<PlanEntryType>("dinner");
|
||||||
const pickerMenu = ref(false);
|
|
||||||
|
|
||||||
const newMealdateString = computed(() => {
|
const newMealdateString = computed(() => {
|
||||||
// Format the date to YYYY-MM-DD in the same timezone as newMealdate
|
// Format the date to YYYY-MM-DD in the same timezone as newMealdate
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
variant="solo"
|
variant="solo"
|
||||||
return-object
|
return-object
|
||||||
:items="units || []"
|
:items="units || []"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
class="mx-1"
|
class="mx-1"
|
||||||
:placeholder="$t('recipe.choose-unit')"
|
:placeholder="$t('recipe.choose-unit')"
|
||||||
@@ -114,6 +115,7 @@
|
|||||||
variant="solo"
|
variant="solo"
|
||||||
return-object
|
return-object
|
||||||
:items="foods || []"
|
:items="foods || []"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
class="mx-1 py-0"
|
class="mx-1 py-0"
|
||||||
:placeholder="$t('recipe.choose-food')"
|
:placeholder="$t('recipe.choose-food')"
|
||||||
@@ -171,6 +173,7 @@
|
|||||||
variant="solo"
|
variant="solo"
|
||||||
return-object
|
return-object
|
||||||
:items="search.data.value || []"
|
:items="search.data.value || []"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
class="mx-1 py-0"
|
class="mx-1 py-0"
|
||||||
:placeholder="$t('search.type-to-search')"
|
:placeholder="$t('search.type-to-search')"
|
||||||
@@ -225,6 +228,7 @@ import { ref, computed, reactive, toRefs } from "vue";
|
|||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
import { useNuxtApp } from "#app";
|
import { useNuxtApp } from "#app";
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||||
|
|||||||
@@ -4,17 +4,19 @@
|
|||||||
v-bind="inputAttrs"
|
v-bind="inputAttrs"
|
||||||
v-model:search="searchInput"
|
v-model:search="searchInput"
|
||||||
:items="items"
|
:items="items"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
:label="label"
|
:label="label"
|
||||||
chips
|
chips
|
||||||
closable-chips
|
closable-chips
|
||||||
item-title="name"
|
:item-title="itemTitle"
|
||||||
|
item-value="name"
|
||||||
multiple
|
multiple
|
||||||
:variant="variant"
|
:variant="variant"
|
||||||
:prepend-inner-icon="icon"
|
:prepend-inner-icon="icon"
|
||||||
:append-icon="showAdd ? $globals.icons.create : undefined"
|
:append-icon="showAdd ? $globals.icons.create : undefined"
|
||||||
return-object
|
return-object
|
||||||
auto-select-first
|
auto-select-first
|
||||||
class="pa-0"
|
class="pa-0 ma-0"
|
||||||
@update:model-value="resetSearchInput"
|
@update:model-value="resetSearchInput"
|
||||||
@click:append="dialog = true"
|
@click:append="dialog = true"
|
||||||
>
|
>
|
||||||
@@ -32,7 +34,6 @@
|
|||||||
{{ item.value }}
|
{{ item.value }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-if="showAdd"
|
v-if="showAdd"
|
||||||
#append
|
#append
|
||||||
@@ -52,11 +53,13 @@ import type { RecipeTool } from "~/lib/api/types/admin";
|
|||||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
|
import { useUserStore } from "~/composables/store/use-user-store";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
|
import type { UserSummary } from "~/lib/api/types/user";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectorType: RecipeOrganizer;
|
selectorType: RecipeOrganizer;
|
||||||
inputAttrs?: Record<string, any>;
|
inputAttrs?: Record<string, any>;
|
||||||
returnObject?: boolean;
|
|
||||||
showAdd?: boolean;
|
showAdd?: boolean;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
@@ -65,7 +68,6 @@ interface Props {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
inputAttrs: () => ({}),
|
inputAttrs: () => ({}),
|
||||||
returnObject: true,
|
|
||||||
showAdd: true,
|
showAdd: true,
|
||||||
showLabel: true,
|
showLabel: true,
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
@@ -78,7 +80,7 @@ const selected = defineModel<(
|
|||||||
| RecipeCategory
|
| RecipeCategory
|
||||||
| RecipeTool
|
| RecipeTool
|
||||||
| IngredientFood
|
| IngredientFood
|
||||||
| string
|
| UserSummary
|
||||||
)[] | undefined>({ required: true });
|
)[] | undefined>({ required: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -106,6 +108,8 @@ const label = computed(() => {
|
|||||||
return i18n.t("general.foods");
|
return i18n.t("general.foods");
|
||||||
case Organizer.Household:
|
case Organizer.Household:
|
||||||
return i18n.t("household.households");
|
return i18n.t("household.households");
|
||||||
|
case Organizer.User:
|
||||||
|
return i18n.t("user.users");
|
||||||
default:
|
default:
|
||||||
return i18n.t("general.organizer");
|
return i18n.t("general.organizer");
|
||||||
}
|
}
|
||||||
@@ -127,11 +131,19 @@ const icon = computed(() => {
|
|||||||
return $globals.icons.foods;
|
return $globals.icons.foods;
|
||||||
case Organizer.Household:
|
case Organizer.Household:
|
||||||
return $globals.icons.household;
|
return $globals.icons.household;
|
||||||
|
case Organizer.User:
|
||||||
|
return $globals.icons.user;
|
||||||
default:
|
default:
|
||||||
return $globals.icons.tags;
|
return $globals.icons.tags;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const itemTitle = computed(() =>
|
||||||
|
props.selectorType === Organizer.User
|
||||||
|
? (i: any) => i?.fullName ?? i?.name ?? ""
|
||||||
|
: "name",
|
||||||
|
);
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Store & Items Setup
|
// Store & Items Setup
|
||||||
|
|
||||||
@@ -141,28 +153,19 @@ const storeMap = {
|
|||||||
[Organizer.Tool]: useToolStore(),
|
[Organizer.Tool]: useToolStore(),
|
||||||
[Organizer.Food]: useFoodStore(),
|
[Organizer.Food]: useFoodStore(),
|
||||||
[Organizer.Household]: useHouseholdStore(),
|
[Organizer.Household]: useHouseholdStore(),
|
||||||
|
[Organizer.User]: useUserStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = computed(() => {
|
const activeStore = computed(() => {
|
||||||
const { store } = storeMap[props.selectorType];
|
const { store } = storeMap[props.selectorType];
|
||||||
return store.value;
|
return store.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = computed(() => {
|
const items = computed<any[]>(() => {
|
||||||
if (!props.returnObject) {
|
const list = (activeStore.value as unknown as any[]) ?? [];
|
||||||
return store.value.map(item => item.name);
|
return list;
|
||||||
}
|
|
||||||
return store.value;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function removeByIndex(index: number) {
|
|
||||||
if (selected.value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
|
||||||
selected.value = [...newSelected];
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCreated(item: any) {
|
function appendCreated(item: any) {
|
||||||
if (selected.value === undefined) {
|
if (selected.value === undefined) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const display = useDisplay();
|
const display = useDisplay();
|
||||||
const { recipeImage } = useStaticRoutes();
|
const { recipeImage, recipeSmallImage } = useStaticRoutes();
|
||||||
const { imageKey } = usePageState(props.recipe.slug);
|
const { imageKey } = usePageState(props.recipe.slug);
|
||||||
const { user } = usePageUser();
|
const { user } = usePageUser();
|
||||||
|
|
||||||
@@ -46,7 +46,9 @@ const imageHeight = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const recipeImageUrl = computed(() => {
|
const recipeImageUrl = computed(() => {
|
||||||
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
return display.smAndDown.value
|
||||||
|
? recipeSmallImage(props.recipe.id, props.recipe.image, imageKey.value)
|
||||||
|
: recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ defineEmits<{
|
|||||||
|
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const display = useDisplay();
|
const display = useDisplay();
|
||||||
const { recipeTimelineEventImage } = useStaticRoutes();
|
const { recipeTimelineEventSmallImage } = useStaticRoutes();
|
||||||
const { eventTypeOptions } = useTimelineEventTypes();
|
const { eventTypeOptions } = useTimelineEventTypes();
|
||||||
|
|
||||||
const { user: currentUser } = useMealieAuth();
|
const { user: currentUser } = useMealieAuth();
|
||||||
@@ -173,7 +173,7 @@ const eventImageUrl = computed<string>(() => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return recipeTimelineEventImage(props.event.recipeId, props.event.id);
|
return recipeTimelineEventSmallImage(props.event.recipeId, props.event.id);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
item-title="name"
|
item-title="name"
|
||||||
return-object
|
return-object
|
||||||
:items="items"
|
:items="items"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
:prepend-icon="icon || $globals.icons.tags"
|
:prepend-icon="icon || $globals.icons.tags"
|
||||||
auto-select-first
|
auto-select-first
|
||||||
clearable
|
clearable
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
|
|
||||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -122,6 +124,7 @@ export default defineNuxtComponent({
|
|||||||
itemIdVal,
|
itemIdVal,
|
||||||
searchInput,
|
searchInput,
|
||||||
emitCreate,
|
emitCreate,
|
||||||
|
normalizeFilter,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="selectedLocale"
|
v-model="selectedLocale"
|
||||||
:items="locales"
|
:items="locales"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
class="my-3"
|
class="my-3"
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -83,6 +85,7 @@ export default defineNuxtComponent({
|
|||||||
locale,
|
locale,
|
||||||
selectedLocale,
|
selectedLocale,
|
||||||
onLocaleSelect,
|
onLocaleSelect,
|
||||||
|
normalizeFilter,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const useStore = function <T extends BoundT>(
|
|||||||
return await storeActions.refresh(1, -1, params);
|
return await storeActions.refresh(1, -1, params);
|
||||||
},
|
},
|
||||||
flushStore() {
|
flushStore() {
|
||||||
store = ref([]);
|
store.value = [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,31 @@
|
|||||||
export { useCategoryStore, usePublicCategoryStore, useCategoryData } from "./use-category-store";
|
import { resetCategoryStore } from "./use-category-store";
|
||||||
export { useFoodStore, usePublicFoodStore, useFoodData } from "./use-food-store";
|
import { resetFoodStore } from "./use-food-store";
|
||||||
export { useHouseholdStore, usePublicHouseholdStore } from "./use-household-store";
|
import { resetHouseholdStore } from "./use-household-store";
|
||||||
export { useLabelStore, useLabelData } from "./use-label-store";
|
import { resetLabelStore } from "./use-label-store";
|
||||||
export { useTagStore, usePublicTagStore, useTagData } from "./use-tag-store";
|
import { resetTagStore } from "./use-tag-store";
|
||||||
export { useToolStore, usePublicToolStore, useToolData } from "./use-tool-store";
|
import { resetToolStore } from "./use-tool-store";
|
||||||
export { useUnitStore, useUnitData } from "./use-unit-store";
|
import { resetUnitStore } from "./use-unit-store";
|
||||||
|
import { resetCookbookStore } from "./use-cookbook-store";
|
||||||
|
import { resetUserStore } from "./use-user-store";
|
||||||
|
|
||||||
|
export { useCategoryStore, usePublicCategoryStore, useCategoryData, resetCategoryStore } from "./use-category-store";
|
||||||
|
export { useFoodStore, usePublicFoodStore, useFoodData, resetFoodStore } from "./use-food-store";
|
||||||
|
export { useHouseholdStore, usePublicHouseholdStore, resetHouseholdStore } from "./use-household-store";
|
||||||
|
export { useLabelStore, useLabelData, resetLabelStore } from "./use-label-store";
|
||||||
|
export { useTagStore, usePublicTagStore, useTagData, resetTagStore } from "./use-tag-store";
|
||||||
|
export { useToolStore, usePublicToolStore, useToolData, resetToolStore } from "./use-tool-store";
|
||||||
|
export { useUnitStore, useUnitData, resetUnitStore } from "./use-unit-store";
|
||||||
|
export { useCookbookStore, usePublicCookbookStore, resetCookbookStore } from "./use-cookbook-store";
|
||||||
|
export { useUserStore, resetUserStore } from "./use-user-store";
|
||||||
|
|
||||||
|
export function clearAllStores() {
|
||||||
|
resetCategoryStore();
|
||||||
|
resetFoodStore();
|
||||||
|
resetHouseholdStore();
|
||||||
|
resetLabelStore();
|
||||||
|
resetTagStore();
|
||||||
|
resetToolStore();
|
||||||
|
resetUnitStore();
|
||||||
|
resetCookbookStore();
|
||||||
|
resetUserStore();
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ const store: Ref<RecipeCategory[]> = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export function resetCategoryStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
publicLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useCategoryData = function () {
|
export const useCategoryData = function () {
|
||||||
return useData<RecipeCategory>({
|
return useData<RecipeCategory>({
|
||||||
id: "",
|
id: "",
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ const cookbooks: Ref<ReadCookBook[]> = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export function resetCookbookStore() {
|
||||||
|
cookbooks.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
publicLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useCookbookStore = function (i18n?: Composer) {
|
export const useCookbookStore = function (i18n?: Composer) {
|
||||||
const api = useUserApi(i18n);
|
const api = useUserApi(i18n);
|
||||||
const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, api.cookbooks);
|
const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, api.cookbooks);
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ const store: Ref<IngredientFood[]> = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export function resetFoodStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
publicLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useFoodData = function () {
|
export const useFoodData = function () {
|
||||||
return useData<IngredientFood>({
|
return useData<IngredientFood>({
|
||||||
id: "",
|
id: "",
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ const store: Ref<HouseholdSummary[]> = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export function resetHouseholdStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
publicLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useHouseholdStore = function (i18n?: Composer) {
|
export const useHouseholdStore = function (i18n?: Composer) {
|
||||||
const api = useUserApi(i18n);
|
const api = useUserApi(i18n);
|
||||||
return useReadOnlyStore<HouseholdSummary>("household", store, loading, api.households);
|
return useReadOnlyStore<HouseholdSummary>("household", store, loading, api.households);
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import { useUserApi } from "~/composables/api";
|
|||||||
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
|
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
export function resetLabelStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useLabelData = function () {
|
export const useLabelData = function () {
|
||||||
return useData<MultiPurposeLabelOut>({
|
return useData<MultiPurposeLabelOut>({
|
||||||
groupId: "",
|
groupId: "",
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ const store: Ref<RecipeTag[]> = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export function resetTagStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
publicLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useTagData = function () {
|
export const useTagData = function () {
|
||||||
return useData<RecipeTag>({
|
return useData<RecipeTag>({
|
||||||
id: "",
|
id: "",
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ const store: Ref<RecipeTool[]> = ref([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const publicLoading = ref(false);
|
const publicLoading = ref(false);
|
||||||
|
|
||||||
|
export function resetToolStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
publicLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useToolData = function () {
|
export const useToolData = function () {
|
||||||
return useData<RecipeToolWithOnHand>({
|
return useData<RecipeToolWithOnHand>({
|
||||||
id: "",
|
id: "",
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import { useUserApi } from "~/composables/api";
|
|||||||
const store: Ref<IngredientUnit[]> = ref([]);
|
const store: Ref<IngredientUnit[]> = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
export function resetUnitStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
export const useUnitData = function () {
|
export const useUnitData = function () {
|
||||||
return useData<IngredientUnit>({
|
return useData<IngredientUnit>({
|
||||||
id: "",
|
id: "",
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
|||||||
const store: Ref<UserSummary[]> = ref([]);
|
const store: Ref<UserSummary[]> = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
export function resetUserStore() {
|
||||||
|
store.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
class GroupUserAPIReadOnly extends BaseCRUDAPIReadOnly<UserSummary> {
|
class GroupUserAPIReadOnly extends BaseCRUDAPIReadOnly<UserSummary> {
|
||||||
baseRoute = "/api/groups/members";
|
baseRoute = "/api/groups/members";
|
||||||
itemRoute = (idOrUsername: string | number) => `/groups/members/${idOrUsername}`;
|
itemRoute = (idOrUsername: string | number) => `/groups/members/${idOrUsername}`;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import type { UserOut } from "~/lib/api/types/user";
|
import type { UserOut } from "~/lib/api/types/user";
|
||||||
|
import { clearAllStores } from "~/composables/store";
|
||||||
|
|
||||||
interface AuthData {
|
interface AuthData {
|
||||||
value: UserOut | null;
|
value: UserOut | null;
|
||||||
@@ -101,6 +102,13 @@ export const useAuthBackend = function (): AuthState {
|
|||||||
setToken(null);
|
setToken(null);
|
||||||
authUser.value = null;
|
authUser.value = null;
|
||||||
authStatus.value = "unauthenticated";
|
authStatus.value = "unauthenticated";
|
||||||
|
|
||||||
|
// Clear all cached store data to prevent data leakage between users
|
||||||
|
clearAllStores();
|
||||||
|
|
||||||
|
// Clear Nuxt's useAsyncData cache
|
||||||
|
clearNuxtData();
|
||||||
|
|
||||||
await router.push(callbackUrl || "/login");
|
await router.push(callbackUrl || "/login");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,30 +128,6 @@ export const useAuthBackend = function (): AuthState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-refresh user data periodically when authenticated
|
|
||||||
if (import.meta.client) {
|
|
||||||
let refreshInterval: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
watch(() => authStatus.value, (status) => {
|
|
||||||
if (status === "authenticated") {
|
|
||||||
refreshInterval = setInterval(() => {
|
|
||||||
if (tokenCookie.value) {
|
|
||||||
getSession().catch(() => {
|
|
||||||
// Ignore errors in background refresh
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 5 * 60 * 1000); // 5 minutes
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Clear interval when not authenticated
|
|
||||||
if (refreshInterval) {
|
|
||||||
clearInterval(refreshInterval);
|
|
||||||
refreshInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: computed(() => authUser.value),
|
data: computed(() => authUser.value),
|
||||||
status: computed(() => authStatus.value),
|
status: computed(() => authStatus.value),
|
||||||
@@ -15,6 +15,9 @@ export function usePlanTypeOptions() {
|
|||||||
{ text: i18n.t("meal-plan.lunch"), value: "lunch" },
|
{ text: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||||
{ text: i18n.t("meal-plan.dinner"), value: "dinner" },
|
{ text: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||||
{ text: i18n.t("meal-plan.side"), value: "side" },
|
{ text: i18n.t("meal-plan.side"), value: "side" },
|
||||||
|
{ text: i18n.t("meal-plan.snack"), value: "snack" },
|
||||||
|
{ text: i18n.t("meal-plan.drink"), value: "drink" },
|
||||||
|
{ text: i18n.t("meal-plan.dessert"), value: "dessert" },
|
||||||
] as PlanOption[];
|
] as PlanOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Українська (Ukrainian)",
|
name: "Українська (Ukrainian)",
|
||||||
value: "uk-UA",
|
value: "uk-UA",
|
||||||
progress: 93,
|
progress: 99,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Türkçe (Turkish)",
|
name: "Türkçe (Turkish)",
|
||||||
value: "tr-TR",
|
value: "tr-TR",
|
||||||
progress: 35,
|
progress: 36,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,7 +81,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Polski (Polish)",
|
name: "Polski (Polish)",
|
||||||
value: "pl-PL",
|
value: "pl-PL",
|
||||||
progress: 46,
|
progress: 53,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -93,7 +93,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Nederlands (Dutch)",
|
name: "Nederlands (Dutch)",
|
||||||
value: "nl-NL",
|
value: "nl-NL",
|
||||||
progress: 54,
|
progress: 55,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -165,7 +165,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Français canadien (Canadian French)",
|
name: "Français canadien (Canadian French)",
|
||||||
value: "fr-CA",
|
value: "fr-CA",
|
||||||
progress: 100,
|
progress: 99,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -219,7 +219,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Dansk (Danish)",
|
name: "Dansk (Danish)",
|
||||||
value: "da-DK",
|
value: "da-DK",
|
||||||
progress: 47,
|
progress: 52,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -237,7 +237,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Български (Bulgarian)",
|
name: "Български (Bulgarian)",
|
||||||
value: "bg-BG",
|
value: "bg-BG",
|
||||||
progress: 47,
|
progress: 49,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ref, watch, computed } from "vue";
|
import { ref, watch, computed } from "vue";
|
||||||
import { useAuthBackend } from "~/composables/useAuthBackend";
|
import { useAuthBackend } from "~/composables/use-auth-backend";
|
||||||
import type { UserOut } from "~/lib/api/types/user";
|
import type { UserOut } from "~/lib/api/types/user";
|
||||||
|
|
||||||
export const useMealieAuth = function () {
|
export const useMealieAuth = function () {
|
||||||
@@ -168,6 +168,7 @@ export function useQueryFilterBuilder() {
|
|||||||
|| type === Organizer.Tool
|
|| type === Organizer.Tool
|
||||||
|| type === Organizer.Food
|
|| type === Organizer.Food
|
||||||
|| type === Organizer.Household
|
|| type === Organizer.Household
|
||||||
|
|| type === Organizer.User
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
34
frontend/composables/use-utils.test.ts
Normal file
34
frontend/composables/use-utils.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { normalize, normalizeFilter } from "./use-utils";
|
||||||
|
|
||||||
|
describe("test normalize", () => {
|
||||||
|
test("base case", () => {
|
||||||
|
expect(normalize("banana")).not.toEqual(normalize("Potatoes"));
|
||||||
|
});
|
||||||
|
test("diacritics", () => {
|
||||||
|
expect(normalize("Rátàtôuile")).toEqual("ratatouile");
|
||||||
|
});
|
||||||
|
test("ligatures", () => {
|
||||||
|
expect(normalize("IJ")).toEqual("ij");
|
||||||
|
expect(normalize("æ")).toEqual("ae");
|
||||||
|
expect(normalize("œ")).toEqual("oe");
|
||||||
|
expect(normalize("ff")).toEqual("ff");
|
||||||
|
expect(normalize("fi")).toEqual("fi");
|
||||||
|
expect(normalize("st")).toEqual("st");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("test normalize filter", () => {
|
||||||
|
test("base case", () => {
|
||||||
|
const patternA = "Escargots persillés";
|
||||||
|
const patternB = "persillés";
|
||||||
|
|
||||||
|
expect(normalizeFilter(patternA, patternB)).toBeTruthy();
|
||||||
|
expect(normalizeFilter(patternB, patternA)).toBeFalsy();
|
||||||
|
});
|
||||||
|
test("normalize", () => {
|
||||||
|
const value = "Cœur de bœuf";
|
||||||
|
const query = "coeur";
|
||||||
|
expect(normalizeFilter(value, query)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useDark, useToggle } from "@vueuse/core";
|
import { useDark, useToggle } from "@vueuse/core";
|
||||||
|
import type { FilterFunction } from "vuetify";
|
||||||
|
|
||||||
export const useToggleDarkMode = () => {
|
export const useToggleDarkMode = () => {
|
||||||
const isDark = useDark();
|
const isDark = useDark();
|
||||||
@@ -18,6 +19,38 @@ export const titleCase = function (str: string) {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const replaceAllBuilder = (map: Map<string, string>): ((str: string) => string) => {
|
||||||
|
const re = new RegExp(Array.from(map.keys()).join("|"), "gi");
|
||||||
|
return str => str.replace(re, matched => map.get(matched)!);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeLigatures = replaceAllBuilder(new Map([
|
||||||
|
["œ", "oe"],
|
||||||
|
["æ", "ae"],
|
||||||
|
["ij", "ij"],
|
||||||
|
["ff", "ff"],
|
||||||
|
["fi", "fi"],
|
||||||
|
["fl", "fl"],
|
||||||
|
["st", "st"],
|
||||||
|
]));
|
||||||
|
|
||||||
|
export const normalize = (str: string) => {
|
||||||
|
if (!str) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = str.normalize("NFKD").toLowerCase();
|
||||||
|
normalized = normalized.replace(/\p{Diacritic}/gu, "");
|
||||||
|
normalized = normalizeLigatures(normalized);
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeFilter: FilterFunction = (value: string, query: string) => {
|
||||||
|
const normalizedValue = normalize(value);
|
||||||
|
const normalizeQuery = normalize(query);
|
||||||
|
return normalizedValue.includes(normalizeQuery);
|
||||||
|
};
|
||||||
|
|
||||||
export function uuid4() {
|
export function uuid4() {
|
||||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
||||||
(parseInt(c) ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (parseInt(c) / 4)))).toString(16),
|
(parseInt(c) ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (parseInt(c) / 4)))).toString(16),
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Ontbyt",
|
"breakfast": "Ontbyt",
|
||||||
"lunch": "Middagete",
|
"lunch": "Middagete",
|
||||||
"dinner": "Aandete",
|
"dinner": "Aandete",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Enige",
|
"type-any": "Enige",
|
||||||
"day-any": "Enige",
|
"day-any": "Enige",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Voer oorspronklike sleutelwoorde as merkers in",
|
"import-original-keywords-as-tags": "Voer oorspronklike sleutelwoorde as merkers in",
|
||||||
"stay-in-edit-mode": "Bly in redigeer modus",
|
"stay-in-edit-mode": "Bly in redigeer modus",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "الإفطار",
|
"breakfast": "الإفطار",
|
||||||
"lunch": "الغداء",
|
"lunch": "الغداء",
|
||||||
"dinner": "العشاء",
|
"dinner": "العشاء",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "أي",
|
"type-any": "أي",
|
||||||
"day-any": "أي",
|
"day-any": "أي",
|
||||||
"editor": "المحرر",
|
"editor": "المحرر",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "جرب الإضافة بالجملة",
|
"scrape-recipe-suggest-bulk-importer": "جرب الإضافة بالجملة",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "هل لديك بيانات HTML أو JSON خام؟",
|
"scrape-recipe-have-raw-html-or-json-data": "هل لديك بيانات HTML أو JSON خام؟",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "يمكنك الإضافة مباشرة باستخدام بيانات خام",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "يمكنك الإضافة مباشرة باستخدام بيانات خام",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "استيراد الكلمات المفتاحية الأصلية كوسوم",
|
"import-original-keywords-as-tags": "استيراد الكلمات المفتاحية الأصلية كوسوم",
|
||||||
"stay-in-edit-mode": "البقاء في وضع التعديل",
|
"stay-in-edit-mode": "البقاء في وضع التعديل",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Закуска",
|
"breakfast": "Закуска",
|
||||||
"lunch": "Обяд",
|
"lunch": "Обяд",
|
||||||
"dinner": "Вечеря",
|
"dinner": "Вечеря",
|
||||||
|
"snack": "Закуска",
|
||||||
|
"drink": "Питие",
|
||||||
|
"dessert": "Десерт",
|
||||||
"type-any": "Всички",
|
"type-any": "Всички",
|
||||||
"day-any": "Всички",
|
"day-any": "Всички",
|
||||||
"editor": "Редактор",
|
"editor": "Редактор",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Пробвайте масовото импорторане",
|
"scrape-recipe-suggest-bulk-importer": "Пробвайте масовото импорторане",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Имате ли сурови HTML или JSON данни?",
|
"scrape-recipe-have-raw-html-or-json-data": "Имате ли сурови HTML или JSON данни?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Можете да импортирате директно от сурови данни",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Можете да импортирате директно от сурови данни",
|
||||||
|
"scrape-recipe-website-being-blocked": "Блокиран ли е уебсайтът?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Опитайте вместо това да импортирате суровия HTML код.",
|
||||||
"import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети",
|
"import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети",
|
||||||
"stay-in-edit-mode": "Остани в режим на редакция",
|
"stay-in-edit-mode": "Остани в режим на редакция",
|
||||||
"parse-recipe-ingredients-after-import": "Анализиране на съставките на рецептата след импортиране",
|
"parse-recipe-ingredients-after-import": "Анализиране на съставките на рецептата след импортиране",
|
||||||
@@ -698,7 +703,7 @@
|
|||||||
"cover-image": "Изображение на корицата",
|
"cover-image": "Изображение на корицата",
|
||||||
"include-linked-recipes": "Влючване на свързаните рецепти",
|
"include-linked-recipes": "Влючване на свързаните рецепти",
|
||||||
"include-linked-recipe-ingredients": "Включване на съставките от свързаните рецепти",
|
"include-linked-recipe-ingredients": "Включване на съставките от свързаните рецепти",
|
||||||
"toggle-recipe": "Превключване на рецептата"
|
"toggle-recipe": "Вмъкни рецепта"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Търсачка на рецепти",
|
"recipe-finder": "Търсачка на рецепти",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Esmorzar",
|
"breakfast": "Esmorzar",
|
||||||
"lunch": "Dinar",
|
"lunch": "Dinar",
|
||||||
"dinner": "Sopar",
|
"dinner": "Sopar",
|
||||||
|
"snack": "Piscolabis",
|
||||||
|
"drink": "Beguda",
|
||||||
|
"dessert": "Postres",
|
||||||
"type-any": "Qualsevol",
|
"type-any": "Qualsevol",
|
||||||
"day-any": "Qualsevol",
|
"day-any": "Qualsevol",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -449,8 +452,8 @@
|
|||||||
"import-by-url": "Importa per URL",
|
"import-by-url": "Importa per URL",
|
||||||
"create-manually": "Crea una recepta manualment",
|
"create-manually": "Crea una recepta manualment",
|
||||||
"make-recipe-image": "Fes-la la imatge de la recepta",
|
"make-recipe-image": "Fes-la la imatge de la recepta",
|
||||||
"add-food": "Add Food",
|
"add-food": "Afegeix Aliment",
|
||||||
"add-recipe": "Add Recipe"
|
"add-recipe": "Afegeix Recepta"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"404-page-not-found": "404 - Pàgina no trobada",
|
"404-page-not-found": "404 - Pàgina no trobada",
|
||||||
@@ -480,7 +483,7 @@
|
|||||||
"comment": "Comentari",
|
"comment": "Comentari",
|
||||||
"comments": "Comentaris",
|
"comments": "Comentaris",
|
||||||
"delete-confirmation": "Estàs segur que vols suprimir-la?",
|
"delete-confirmation": "Estàs segur que vols suprimir-la?",
|
||||||
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
"admin-delete-confirmation": "Estàs a punt d'eliminar una recepta que no és teva utilitzant permisos d'administrador. N'estàs segur?",
|
||||||
"delete-recipe": "Suprimeix la recepta",
|
"delete-recipe": "Suprimeix la recepta",
|
||||||
"description": "Descripció",
|
"description": "Descripció",
|
||||||
"disable-amount": "Oculta les quantitats",
|
"disable-amount": "Oculta les quantitats",
|
||||||
@@ -517,9 +520,9 @@
|
|||||||
"recipe-deleted": "S'ha suprimit la recepta",
|
"recipe-deleted": "S'ha suprimit la recepta",
|
||||||
"recipe-image": "Imatge de la recepta",
|
"recipe-image": "Imatge de la recepta",
|
||||||
"recipe-image-updated": "S'ha actualitzat la imatge de la recepta",
|
"recipe-image-updated": "S'ha actualitzat la imatge de la recepta",
|
||||||
"delete-image": "Delete Recipe Image",
|
"delete-image": "Suprimir la imatge de la recepta",
|
||||||
"delete-image-confirmation": "Are you sure you want to delete this recipe image?",
|
"delete-image-confirmation": "Estàs segur que vols suprimir la imatge d'aquesta recepta?",
|
||||||
"recipe-image-deleted": "Recipe image deleted",
|
"recipe-image-deleted": "S'ha suprimit la imatge de la recepta",
|
||||||
"recipe-name": "Nom de la recepta",
|
"recipe-name": "Nom de la recepta",
|
||||||
"recipe-settings": "Opcions de la recepta",
|
"recipe-settings": "Opcions de la recepta",
|
||||||
"recipe-update-failed": "S'ha produït un error a l'actualitzar la recepta",
|
"recipe-update-failed": "S'ha produït un error a l'actualitzar la recepta",
|
||||||
@@ -565,7 +568,7 @@
|
|||||||
"choose-unit": "Tria el tipus d'unitat",
|
"choose-unit": "Tria el tipus d'unitat",
|
||||||
"press-enter-to-create": "Premeu enter per a crear-lo",
|
"press-enter-to-create": "Premeu enter per a crear-lo",
|
||||||
"choose-food": "Tria un aliment",
|
"choose-food": "Tria un aliment",
|
||||||
"choose-recipe": "Choose Recipe",
|
"choose-recipe": "Tria la recepta",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"toggle-section": "Nova secció",
|
"toggle-section": "Nova secció",
|
||||||
"see-original-text": "Mostra el text original",
|
"see-original-text": "Mostra el text original",
|
||||||
@@ -597,11 +600,11 @@
|
|||||||
"added-to-timeline": "Added to timeline",
|
"added-to-timeline": "Added to timeline",
|
||||||
"failed-to-add-to-timeline": "Failed to add to timeline",
|
"failed-to-add-to-timeline": "Failed to add to timeline",
|
||||||
"failed-to-update-recipe": "Failed to update recipe",
|
"failed-to-update-recipe": "Failed to update recipe",
|
||||||
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
"added-to-timeline-but-failed-to-add-image": "S'ha afegit a la línia de temps, però no s'ha pogut afegir la imatge",
|
||||||
"api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.",
|
"api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.",
|
||||||
"message-key": "Clau del missatge",
|
"message-key": "Clau del missatge",
|
||||||
"parse": "Analitzar",
|
"parse": "Analitzar",
|
||||||
"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": "Sembla que els teus ingredients encara no s'han analitzat. Feu clic al botó \"{parse}\" de sota per transformar els vostres ingredients en aliments estructurats.",
|
||||||
"attach-images-hint": "Afegeix imatges arrossegant i deixant anar la imatge a l'editor",
|
"attach-images-hint": "Afegeix imatges arrossegant i deixant anar la imatge a l'editor",
|
||||||
"drop-image": "Deixa anar la imatge",
|
"drop-image": "Deixa anar la imatge",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Habilita les quantitats d'ingredients per a poder fer servir aquesta característica",
|
"enable-ingredient-amounts-to-use-this-feature": "Habilita les quantitats d'ingredients per a poder fer servir aquesta característica",
|
||||||
@@ -619,10 +622,10 @@
|
|||||||
"create-recipe-from-an-image": "Crear una recepta a partir d'una imatge",
|
"create-recipe-from-an-image": "Crear una recepta a partir d'una imatge",
|
||||||
"create-recipe-from-an-image-description": "Crear una recepta pujant una imatge d'ella. Mealie intentarà extreure el text de la imatge mitjançant IA i crear-ne la recepta.",
|
"create-recipe-from-an-image-description": "Crear una recepta pujant una imatge d'ella. Mealie intentarà extreure el text de la imatge mitjançant IA i crear-ne la recepta.",
|
||||||
"crop-and-rotate-the-image": "Retalla i rota la imatge, per tal que només el text sigui visible, i estigui orientat correctament.",
|
"crop-and-rotate-the-image": "Retalla i rota la imatge, per tal que només el text sigui visible, i estigui orientat correctament.",
|
||||||
"create-from-images": "Create from Images",
|
"create-from-images": "Crear una recepta a partir d'una imatge",
|
||||||
"should-translate-description": "Tradueix la recepta a la meva llengua",
|
"should-translate-description": "Tradueix la recepta a la meva llengua",
|
||||||
"please-wait-image-procesing": "Si us plau, esperi, la imatge s'està processant. Això pot tardar un temps.",
|
"please-wait-image-procesing": "Si us plau, esperi, la imatge s'està processant. Això pot tardar un temps.",
|
||||||
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
|
"please-wait-images-processing": "Espereu, les imatges s'estan processant. Això pot trigar una estona.",
|
||||||
"bulk-url-import": "Importació d'URL en massa",
|
"bulk-url-import": "Importació d'URL en massa",
|
||||||
"debug-scraper": "Rastrejador de depuració",
|
"debug-scraper": "Rastrejador de depuració",
|
||||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crea la recepta proporcionant-ne un nom. Totes les receptes han de tenir un nom únic.",
|
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crea la recepta proporcionant-ne un nom. Totes les receptes han de tenir un nom únic.",
|
||||||
@@ -633,9 +636,11 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Prova l'importador a granel",
|
"scrape-recipe-suggest-bulk-importer": "Prova l'importador a granel",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Teniu dades HTML o JSON pla?",
|
"scrape-recipe-have-raw-html-or-json-data": "Teniu dades HTML o JSON pla?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Podeu importar directament des de les dades planes",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Podeu importar directament des de les dades planes",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importa les paraules clau originals com a tags",
|
"import-original-keywords-as-tags": "Importa les paraules clau originals com a tags",
|
||||||
"stay-in-edit-mode": "Segueix en el mode d'edició",
|
"stay-in-edit-mode": "Segueix en el mode d'edició",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Analitza els ingredients de la recepta després d'importar",
|
||||||
"import-from-zip": "Importa des d'un ZIP",
|
"import-from-zip": "Importa des d'un ZIP",
|
||||||
"import-from-zip-description": "Importa una sola recepta que ha estat importada d'una altra instància de Mealie.",
|
"import-from-zip-description": "Importa una sola recepta que ha estat importada d'una altra instància de Mealie.",
|
||||||
"import-from-html-or-json": "Importar des d'un HTML o JSON",
|
"import-from-html-or-json": "Importar des d'un HTML o JSON",
|
||||||
@@ -679,26 +684,26 @@
|
|||||||
"no-unit": "Sense unitat",
|
"no-unit": "Sense unitat",
|
||||||
"missing-unit": "Crear unitat que manca: {unit}",
|
"missing-unit": "Crear unitat que manca: {unit}",
|
||||||
"missing-food": "Crear menjar que manca: {food}",
|
"missing-food": "Crear menjar que manca: {food}",
|
||||||
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
"this-unit-could-not-be-parsed-automatically": "Aquesta unitat no s'ha pogut analitzar automàticament",
|
||||||
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
"this-food-could-not-be-parsed-automatically": "Aquest aliment no s'ha pogut analitzar automàticament",
|
||||||
"no-food": "Sense menjar",
|
"no-food": "Sense menjar",
|
||||||
"review-parsed-ingredients": "Review parsed ingredients",
|
"review-parsed-ingredients": "Revisió d'ingredients analitzats",
|
||||||
"confidence-score": "Confidence Score",
|
"confidence-score": "Puntuació de confiança",
|
||||||
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
|
"ingredient-parser-description": "Els teus ingredients s'han analitzat correctament. Si us plau, revisa els ingredients dels quals no estem segurs.",
|
||||||
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
|
"ingredient-parser-final-review-description": "Un cop revisats tots els ingredients, tindràs una oportunitat més de revisar tots els ingredients abans d'aplicar els canvis a la teva recepta.",
|
||||||
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
|
"add-text-as-alias-for-item": "Afegeix \"{text}\" com a àlies de {item}",
|
||||||
"delete-item": "Delete Item"
|
"delete-item": "Eliminar element"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reiniciar racions servides",
|
"reset-servings-count": "Reiniciar racions servides",
|
||||||
"not-linked-ingredients": "Ingredients addicionals",
|
"not-linked-ingredients": "Ingredients addicionals",
|
||||||
"upload-another-image": "Upload another image",
|
"upload-another-image": "Puja una altra imatge",
|
||||||
"upload-images": "Upload images",
|
"upload-images": "Puja imatges",
|
||||||
"upload-more-images": "Upload more images",
|
"upload-more-images": "Puja més imatges",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Estableix com a imatge de portada de recepta",
|
||||||
"cover-image": "Cover image",
|
"cover-image": "Imatge de portada",
|
||||||
"include-linked-recipes": "Include Linked Recipes",
|
"include-linked-recipes": "Inclou les receptes enllaçades",
|
||||||
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
|
"include-linked-recipe-ingredients": "Inclou els ingredients de la recepta enllaçada",
|
||||||
"toggle-recipe": "Toggle Recipe"
|
"toggle-recipe": "Alternar recepta"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Cercador de receptes",
|
"recipe-finder": "Cercador de receptes",
|
||||||
@@ -736,7 +741,7 @@
|
|||||||
"advanced": "Avançat",
|
"advanced": "Avançat",
|
||||||
"auto-search": "Cerca automàtica",
|
"auto-search": "Cerca automàtica",
|
||||||
"no-results": "No s'han trobat resultats",
|
"no-results": "No s'han trobat resultats",
|
||||||
"type-to-search": "Type to search..."
|
"type-to-search": "Escriviu per cercar..."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"add-a-new-theme": "Afegiu un nou tema",
|
"add-a-new-theme": "Afegiu un nou tema",
|
||||||
@@ -1075,8 +1080,8 @@
|
|||||||
"forgot-password": "Contrasenya oblidada",
|
"forgot-password": "Contrasenya oblidada",
|
||||||
"forgot-password-text": "Introdueix siusplau la teva adreça de correu electrònic i t'enviarem un enllaç per restablir la teva contrassenya.",
|
"forgot-password-text": "Introdueix siusplau la teva adreça de correu electrònic i t'enviarem un enllaç per restablir la teva contrassenya.",
|
||||||
"changes-reflected-immediately": "Els canvis en aquest usuari s'actualitzaran immediatament.",
|
"changes-reflected-immediately": "Els canvis en aquest usuari s'actualitzaran immediatament.",
|
||||||
"default-activity": "Default Activity",
|
"default-activity": "Activitat per defecte",
|
||||||
"default-activity-hint": "Select which page you'd like to navigate to upon logging in from this device"
|
"default-activity-hint": "Seleccioneu a quina pàgina voleu navegar en iniciar sessió des d'aquest dispositiu"
|
||||||
},
|
},
|
||||||
"language-dialog": {
|
"language-dialog": {
|
||||||
"translated": "traduït",
|
"translated": "traduït",
|
||||||
@@ -1194,7 +1199,7 @@
|
|||||||
"group-details": "Detalls del grup",
|
"group-details": "Detalls del grup",
|
||||||
"group-details-description": "Abans de crear un compte heu de crear un grup. Al grup només hi serà vostè, però després podeu convidar d'altres. Els membres d'un grup poden compartir menús, llistes de la compra, receptes i molt més!",
|
"group-details-description": "Abans de crear un compte heu de crear un grup. Al grup només hi serà vostè, però després podeu convidar d'altres. Els membres d'un grup poden compartir menús, llistes de la compra, receptes i molt més!",
|
||||||
"use-seed-data": "Afegiu dades predeterminades",
|
"use-seed-data": "Afegiu dades predeterminades",
|
||||||
"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 disposa d'una col·lecció d'aliments, unitats i etiquetes que es poden utilitzar per omplir el vostre grup amb dades útils per organitzar les vostres receptes. Aquests es tradueixen a l'idioma que heu seleccionat actualment. Sempre podeu afegir o modificar aquestes dades més endavant.",
|
||||||
"account-details": "Detalls del compte"
|
"account-details": "Detalls del compte"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Snídaně",
|
"breakfast": "Snídaně",
|
||||||
"lunch": "Oběd",
|
"lunch": "Oběd",
|
||||||
"dinner": "Večeře",
|
"dinner": "Večeře",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Libovolné",
|
"type-any": "Libovolné",
|
||||||
"day-any": "Libovolný",
|
"day-any": "Libovolný",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Vyzkoušejte hromadný import",
|
"scrape-recipe-suggest-bulk-importer": "Vyzkoušejte hromadný import",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Máte surová data HTML nebo JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "Máte surová data HTML nebo JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Můžete importovat přímo ze surových dat",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Můžete importovat přímo ze surových dat",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky",
|
"import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky",
|
||||||
"stay-in-edit-mode": "Zůstat v režimu úprav",
|
"stay-in-edit-mode": "Zůstat v režimu úprav",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Morgenmad",
|
"breakfast": "Morgenmad",
|
||||||
"lunch": "Frokost",
|
"lunch": "Frokost",
|
||||||
"dinner": "Aftensmad",
|
"dinner": "Aftensmad",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Alle",
|
"type-any": "Alle",
|
||||||
"day-any": "Alle",
|
"day-any": "Alle",
|
||||||
"editor": "Redigeringsværktøj",
|
"editor": "Redigeringsværktøj",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Prøv masseimport",
|
"scrape-recipe-suggest-bulk-importer": "Prøv masseimport",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Har rå HTML- eller JSON-data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Har rå HTML- eller JSON-data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importere direkte fra rå data",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importere direkte fra rå data",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importér originale nøgleord som mærker",
|
"import-original-keywords-as-tags": "Importér originale nøgleord som mærker",
|
||||||
"stay-in-edit-mode": "Bliv i redigeringstilstand",
|
"stay-in-edit-mode": "Bliv i redigeringstilstand",
|
||||||
"parse-recipe-ingredients-after-import": "Fortolk opskrift ingredienser efter import",
|
"parse-recipe-ingredients-after-import": "Fortolk opskrift ingredienser efter import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Frühstück",
|
"breakfast": "Frühstück",
|
||||||
"lunch": "Mittagessen",
|
"lunch": "Mittagessen",
|
||||||
"dinner": "Abendessen",
|
"dinner": "Abendessen",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Alle",
|
"type-any": "Alle",
|
||||||
"day-any": "Alle",
|
"day-any": "Alle",
|
||||||
"editor": "Bearbeiten",
|
"editor": "Bearbeiten",
|
||||||
@@ -565,7 +568,7 @@
|
|||||||
"choose-unit": "Einheit wählen",
|
"choose-unit": "Einheit wählen",
|
||||||
"press-enter-to-create": "Zum Erstellen Eingabetaste drücken",
|
"press-enter-to-create": "Zum Erstellen Eingabetaste drücken",
|
||||||
"choose-food": "Lebensmittel wählen",
|
"choose-food": "Lebensmittel wählen",
|
||||||
"choose-recipe": "Choose Recipe",
|
"choose-recipe": "Rezept wählen",
|
||||||
"notes": "Notizen",
|
"notes": "Notizen",
|
||||||
"toggle-section": "Überschrift ein-/ausblenden",
|
"toggle-section": "Überschrift ein-/ausblenden",
|
||||||
"see-original-text": "Originaltext anzeigen",
|
"see-original-text": "Originaltext anzeigen",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Probiere den Massenimporter aus",
|
"scrape-recipe-suggest-bulk-importer": "Probiere den Massenimporter aus",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Hast du Roh-HTML oder JSON Daten?",
|
"scrape-recipe-have-raw-html-or-json-data": "Hast du Roh-HTML oder JSON Daten?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kannst direkt von Rohdaten importieren",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kannst direkt von Rohdaten importieren",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importiere ursprüngliche Stichwörter als Schlagwörter",
|
"import-original-keywords-as-tags": "Importiere ursprüngliche Stichwörter als Schlagwörter",
|
||||||
"stay-in-edit-mode": "Im Bearbeitungsmodus bleiben",
|
"stay-in-edit-mode": "Im Bearbeitungsmodus bleiben",
|
||||||
"parse-recipe-ingredients-after-import": "Zutaten nach dem Import parsen",
|
"parse-recipe-ingredients-after-import": "Zutaten nach dem Import parsen",
|
||||||
@@ -736,7 +741,7 @@
|
|||||||
"advanced": "Erweitert",
|
"advanced": "Erweitert",
|
||||||
"auto-search": "Automatische Suche",
|
"auto-search": "Automatische Suche",
|
||||||
"no-results": "Keine Ergebnisse gefunden",
|
"no-results": "Keine Ergebnisse gefunden",
|
||||||
"type-to-search": "Type to search..."
|
"type-to-search": "Suchbegriff eingeben..."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"add-a-new-theme": "Neues Thema hinzufügen",
|
"add-a-new-theme": "Neues Thema hinzufügen",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Πρωινό",
|
"breakfast": "Πρωινό",
|
||||||
"lunch": "Μεσημεριανό",
|
"lunch": "Μεσημεριανό",
|
||||||
"dinner": "Βραδινό",
|
"dinner": "Βραδινό",
|
||||||
|
"snack": "Σνακ",
|
||||||
|
"drink": "Ποτό",
|
||||||
|
"dessert": "Επιδόρπιο",
|
||||||
"type-any": "Οτιδήποτε",
|
"type-any": "Οτιδήποτε",
|
||||||
"day-any": "Οποιαδήποτε",
|
"day-any": "Οποιαδήποτε",
|
||||||
"editor": "Επεξεργαστής κειμένου",
|
"editor": "Επεξεργαστής κειμένου",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Δοκιμάστε τον μαζικό εισαγωγέα συνταγών μας",
|
"scrape-recipe-suggest-bulk-importer": "Δοκιμάστε τον μαζικό εισαγωγέα συνταγών μας",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Εχουν ακατέργαστα δεδομένα HTML ή JSON;",
|
"scrape-recipe-have-raw-html-or-json-data": "Εχουν ακατέργαστα δεδομένα HTML ή JSON;",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Μπορείτε να κάνετε εισαγωγή απευθείας από ακατέργαστα δεδομένα",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Μπορείτε να κάνετε εισαγωγή απευθείας από ακατέργαστα δεδομένα",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Εισαγωγή αρχικών λέξεων-κλειδιών ως ετικέτες",
|
"import-original-keywords-as-tags": "Εισαγωγή αρχικών λέξεων-κλειδιών ως ετικέτες",
|
||||||
"stay-in-edit-mode": "Παραμονή σε λειτουργία επεξεργασίας",
|
"stay-in-edit-mode": "Παραμονή σε λειτουργία επεξεργασίας",
|
||||||
"parse-recipe-ingredients-after-import": "Ανάλυση συστατικών συνταγής μετά την εισαγωγή",
|
"parse-recipe-ingredients-after-import": "Ανάλυση συστατικών συνταγής μετά την εισαγωγή",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Breakfast",
|
"breakfast": "Breakfast",
|
||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"dinner": "Dinner",
|
"dinner": "Dinner",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Any",
|
"type-any": "Any",
|
||||||
"day-any": "Any",
|
"day-any": "Any",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Import original keywords as tags",
|
"import-original-keywords-as-tags": "Import original keywords as tags",
|
||||||
"stay-in-edit-mode": "Stay in Edit mode",
|
"stay-in-edit-mode": "Stay in Edit mode",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Breakfast",
|
"breakfast": "Breakfast",
|
||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"dinner": "Dinner",
|
"dinner": "Dinner",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Any",
|
"type-any": "Any",
|
||||||
"day-any": "Any",
|
"day-any": "Any",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Import original keywords as tags",
|
"import-original-keywords-as-tags": "Import original keywords as tags",
|
||||||
"stay-in-edit-mode": "Stay in Edit mode",
|
"stay-in-edit-mode": "Stay in Edit mode",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Desayuno",
|
"breakfast": "Desayuno",
|
||||||
"lunch": "Comida principal",
|
"lunch": "Comida principal",
|
||||||
"dinner": "Cena",
|
"dinner": "Cena",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Cualquiera",
|
"type-any": "Cualquiera",
|
||||||
"day-any": "Cualquier",
|
"day-any": "Cualquier",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Prueba el importador masivo",
|
"scrape-recipe-suggest-bulk-importer": "Prueba el importador masivo",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "¿Tiene datos HTML o JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "¿Tiene datos HTML o JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Puede importar directamente desde datos brutos",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Puede importar directamente desde datos brutos",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas",
|
"import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas",
|
||||||
"stay-in-edit-mode": "Permanecer en modo edición",
|
"stay-in-edit-mode": "Permanecer en modo edición",
|
||||||
"parse-recipe-ingredients-after-import": "Analizar los ingredientes de la receta después de importarla",
|
"parse-recipe-ingredients-after-import": "Analizar los ingredientes de la receta después de importarla",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Hommikusöök",
|
"breakfast": "Hommikusöök",
|
||||||
"lunch": "Lõuna",
|
"lunch": "Lõuna",
|
||||||
"dinner": "Õhtusöök",
|
"dinner": "Õhtusöök",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Kõik",
|
"type-any": "Kõik",
|
||||||
"day-any": "Kõik",
|
"day-any": "Kõik",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Proovi hulgiimportimist.",
|
"scrape-recipe-suggest-bulk-importer": "Proovi hulgiimportimist.",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Sul on töötlemata HTMLi või JSONi andmed?",
|
"scrape-recipe-have-raw-html-or-json-data": "Sul on töötlemata HTMLi või JSONi andmed?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Sa võid otse importida töötlemata andmetest",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Sa võid otse importida töötlemata andmetest",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Impordi originaal võtmesõnad siltidena",
|
"import-original-keywords-as-tags": "Impordi originaal võtmesõnad siltidena",
|
||||||
"stay-in-edit-mode": "Püsige redigeerimisrežiimis",
|
"stay-in-edit-mode": "Püsige redigeerimisrežiimis",
|
||||||
"parse-recipe-ingredients-after-import": "Tuvasta retsepti koostisosad pärast importimist",
|
"parse-recipe-ingredients-after-import": "Tuvasta retsepti koostisosad pärast importimist",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Aamiainen",
|
"breakfast": "Aamiainen",
|
||||||
"lunch": "Lounas",
|
"lunch": "Lounas",
|
||||||
"dinner": "Päivällinen",
|
"dinner": "Päivällinen",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Mikä tahansa",
|
"type-any": "Mikä tahansa",
|
||||||
"day-any": "Koska tahansa",
|
"day-any": "Koska tahansa",
|
||||||
"editor": "Editori",
|
"editor": "Editori",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Kokeile massasiirtotyökalua",
|
"scrape-recipe-suggest-bulk-importer": "Kokeile massasiirtotyökalua",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Onko sinulla raakaa HTML- tai JSON-dataa?",
|
"scrape-recipe-have-raw-html-or-json-data": "Onko sinulla raakaa HTML- tai JSON-dataa?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Voit tuoda raakadatan suoraan",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Voit tuoda raakadatan suoraan",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Tuo alkuperäiset avainsanat tunnisteiksi",
|
"import-original-keywords-as-tags": "Tuo alkuperäiset avainsanat tunnisteiksi",
|
||||||
"stay-in-edit-mode": "Pysy muokkaustilassa",
|
"stay-in-edit-mode": "Pysy muokkaustilassa",
|
||||||
"parse-recipe-ingredients-after-import": "Jäsennä reseptin ainesosat tuonnin jälkeen",
|
"parse-recipe-ingredients-after-import": "Jäsennä reseptin ainesosat tuonnin jälkeen",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Petit-déjeuner",
|
"breakfast": "Petit-déjeuner",
|
||||||
"lunch": "Déjeuner",
|
"lunch": "Déjeuner",
|
||||||
"dinner": "Souper",
|
"dinner": "Souper",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Tous",
|
"type-any": "Tous",
|
||||||
"day-any": "Tous",
|
"day-any": "Tous",
|
||||||
"editor": "Éditeur",
|
"editor": "Éditeur",
|
||||||
@@ -565,7 +568,7 @@
|
|||||||
"choose-unit": "Choisissez une unité",
|
"choose-unit": "Choisissez une unité",
|
||||||
"press-enter-to-create": "Clique sur Entrer pour créer",
|
"press-enter-to-create": "Clique sur Entrer pour créer",
|
||||||
"choose-food": "Choisissez un aliment",
|
"choose-food": "Choisissez un aliment",
|
||||||
"choose-recipe": "Choose Recipe",
|
"choose-recipe": "Choisissez la recette",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"toggle-section": "Activer/Désactiver la section",
|
"toggle-section": "Activer/Désactiver la section",
|
||||||
"see-original-text": "Afficher le texte original",
|
"see-original-text": "Afficher le texte original",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Essayez l’importateur de masse",
|
"scrape-recipe-suggest-bulk-importer": "Essayez l’importateur de masse",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
|
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||||
"stay-in-edit-mode": "Rester en mode édition",
|
"stay-in-edit-mode": "Rester en mode édition",
|
||||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||||
@@ -736,7 +741,7 @@
|
|||||||
"advanced": "Avancé",
|
"advanced": "Avancé",
|
||||||
"auto-search": "Recherche automatique",
|
"auto-search": "Recherche automatique",
|
||||||
"no-results": "Aucun résultat trouvé",
|
"no-results": "Aucun résultat trouvé",
|
||||||
"type-to-search": "Type to search..."
|
"type-to-search": "Tapez pour chercher..."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"add-a-new-theme": "Ajouter un nouveau thème",
|
"add-a-new-theme": "Ajouter un nouveau thème",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Petit déjeuner",
|
"breakfast": "Petit déjeuner",
|
||||||
"lunch": "Dîner",
|
"lunch": "Dîner",
|
||||||
"dinner": "Souper",
|
"dinner": "Souper",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Tous",
|
"type-any": "Tous",
|
||||||
"day-any": "Tous",
|
"day-any": "Tous",
|
||||||
"editor": "Éditeur",
|
"editor": "Éditeur",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Essayez l’importateur de masse",
|
"scrape-recipe-suggest-bulk-importer": "Essayez l’importateur de masse",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
|
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||||
"stay-in-edit-mode": "Rester en mode édition",
|
"stay-in-edit-mode": "Rester en mode édition",
|
||||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Petit-déjeuner",
|
"breakfast": "Petit-déjeuner",
|
||||||
"lunch": "Déjeuner",
|
"lunch": "Déjeuner",
|
||||||
"dinner": "Dîner",
|
"dinner": "Dîner",
|
||||||
|
"snack": "Goûter",
|
||||||
|
"drink": "Boissons",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Tous",
|
"type-any": "Tous",
|
||||||
"day-any": "Tous",
|
"day-any": "Tous",
|
||||||
"editor": "Éditeur",
|
"editor": "Éditeur",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Essayez l’importateur de masse",
|
"scrape-recipe-suggest-bulk-importer": "Essayez l’importateur de masse",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
|
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
|
||||||
|
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
|
||||||
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
|
||||||
"stay-in-edit-mode": "Rester en mode édition",
|
"stay-in-edit-mode": "Rester en mode édition",
|
||||||
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Almorzo",
|
"breakfast": "Almorzo",
|
||||||
"lunch": "Xantar",
|
"lunch": "Xantar",
|
||||||
"dinner": "Cea",
|
"dinner": "Cea",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Calquera",
|
"type-any": "Calquera",
|
||||||
"day-any": "Calquera",
|
"day-any": "Calquera",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Prove o importador en masa",
|
"scrape-recipe-suggest-bulk-importer": "Prove o importador en masa",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Ten datos HTML ou JSON en bruto?",
|
"scrape-recipe-have-raw-html-or-json-data": "Ten datos HTML ou JSON en bruto?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "É posível importar diretamente a partir de datos en bruto",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "É posível importar diretamente a partir de datos en bruto",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importar palavras-chave orixinais como etiquetas",
|
"import-original-keywords-as-tags": "Importar palavras-chave orixinais como etiquetas",
|
||||||
"stay-in-edit-mode": "Permanecer no modo de edición",
|
"stay-in-edit-mode": "Permanecer no modo de edición",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "ארוחת בוקר",
|
"breakfast": "ארוחת בוקר",
|
||||||
"lunch": "ארוחת צהריים",
|
"lunch": "ארוחת צהריים",
|
||||||
"dinner": "ארוחת ערב",
|
"dinner": "ארוחת ערב",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "הכל",
|
"type-any": "הכל",
|
||||||
"day-any": "הכל",
|
"day-any": "הכל",
|
||||||
"editor": "עורך",
|
"editor": "עורך",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "נסה את יכולת קריאת רשימה",
|
"scrape-recipe-suggest-bulk-importer": "נסה את יכולת קריאת רשימה",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "יש לך מידע גולמי ב-HTML או JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "יש לך מידע גולמי ב-HTML או JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "ניתן לייבא ישירות ממידע גולמי",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "ניתן לייבא ישירות ממידע גולמי",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "ייבוא שמות מפתח מקוריות כתגיות",
|
"import-original-keywords-as-tags": "ייבוא שמות מפתח מקוריות כתגיות",
|
||||||
"stay-in-edit-mode": "השאר במצב עריכה",
|
"stay-in-edit-mode": "השאר במצב עריכה",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Doručak",
|
"breakfast": "Doručak",
|
||||||
"lunch": "Ručak",
|
"lunch": "Ručak",
|
||||||
"dinner": "Večera",
|
"dinner": "Večera",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Bilo koji",
|
"type-any": "Bilo koji",
|
||||||
"day-any": "Bilo koji",
|
"day-any": "Bilo koji",
|
||||||
"editor": "Urednik",
|
"editor": "Urednik",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Isprobajte masovni uvoz",
|
"scrape-recipe-suggest-bulk-importer": "Isprobajte masovni uvoz",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Imate neobrađene HTML ili JSON podatke?",
|
"scrape-recipe-have-raw-html-or-json-data": "Imate neobrađene HTML ili JSON podatke?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Možete uvesti iz neobrađenih podataka izravno",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Možete uvesti iz neobrađenih podataka izravno",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Uvezi originalne ključne riječi kao oznake",
|
"import-original-keywords-as-tags": "Uvezi originalne ključne riječi kao oznake",
|
||||||
"stay-in-edit-mode": "Ostanite u načinu uređivanja",
|
"stay-in-edit-mode": "Ostanite u načinu uređivanja",
|
||||||
"parse-recipe-ingredients-after-import": "Parsiranje sastojaka recepta nakon uvoza",
|
"parse-recipe-ingredients-after-import": "Parsiranje sastojaka recepta nakon uvoza",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Reggeli",
|
"breakfast": "Reggeli",
|
||||||
"lunch": "Ebéd",
|
"lunch": "Ebéd",
|
||||||
"dinner": "Vacsora",
|
"dinner": "Vacsora",
|
||||||
|
"snack": "Rágcsa",
|
||||||
|
"drink": "Ital",
|
||||||
|
"dessert": "Desszert",
|
||||||
"type-any": "Bármely",
|
"type-any": "Bármely",
|
||||||
"day-any": "Bármely",
|
"day-any": "Bármely",
|
||||||
"editor": "Szerkesztő",
|
"editor": "Szerkesztő",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Próbálja ki a tömeges importálót",
|
"scrape-recipe-suggest-bulk-importer": "Próbálja ki a tömeges importálót",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Nyers HTML vagy JSON adatai vannak?",
|
"scrape-recipe-have-raw-html-or-json-data": "Nyers HTML vagy JSON adatai vannak?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "A nyers adatokból közvetlenül is importálhat",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "A nyers adatokból közvetlenül is importálhat",
|
||||||
|
"scrape-recipe-website-being-blocked": "A weboldal blokkolva van?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Próbálja meg inkább a nyers HTML-t importálni.",
|
||||||
"import-original-keywords-as-tags": "Eredeti kulcsszavak importálása címkeként",
|
"import-original-keywords-as-tags": "Eredeti kulcsszavak importálása címkeként",
|
||||||
"stay-in-edit-mode": "Maradjon Szerkesztés módban",
|
"stay-in-edit-mode": "Maradjon Szerkesztés módban",
|
||||||
"parse-recipe-ingredients-after-import": "Recept összetevőinek elemzése importálás után",
|
"parse-recipe-ingredients-after-import": "Recept összetevőinek elemzése importálás után",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Morgunverður",
|
"breakfast": "Morgunverður",
|
||||||
"lunch": "Hádegisverður",
|
"lunch": "Hádegisverður",
|
||||||
"dinner": "Kvöldverður",
|
"dinner": "Kvöldverður",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Allir",
|
"type-any": "Allir",
|
||||||
"day-any": "Alla",
|
"day-any": "Alla",
|
||||||
"editor": "Ritill",
|
"editor": "Ritill",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Prófaðu að setja inn margar uppskriftir í einu",
|
"scrape-recipe-suggest-bulk-importer": "Prófaðu að setja inn margar uppskriftir í einu",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Ertu með hrá HTML eða JSON gögn?",
|
"scrape-recipe-have-raw-html-or-json-data": "Ertu með hrá HTML eða JSON gögn?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Það er hægt að hlaða inn hráum gögnum beint",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Það er hægt að hlaða inn hráum gögnum beint",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Nota upprunanleg merki",
|
"import-original-keywords-as-tags": "Nota upprunanleg merki",
|
||||||
"stay-in-edit-mode": "Vera í breytingarham",
|
"stay-in-edit-mode": "Vera í breytingarham",
|
||||||
"parse-recipe-ingredients-after-import": "Greina innhald uppskriftar eftir að búið er að hlaða inn uppskrift",
|
"parse-recipe-ingredients-after-import": "Greina innhald uppskriftar eftir að búið er að hlaða inn uppskrift",
|
||||||
@@ -1217,7 +1222,7 @@
|
|||||||
"recipe-link-copied-message": "Tengill fyrir uppskriftina afritaður í klippispjald"
|
"recipe-link-copied-message": "Tengill fyrir uppskriftina afritaður í klippispjald"
|
||||||
},
|
},
|
||||||
"banner-experimental": {
|
"banner-experimental": {
|
||||||
"title": "Experimental Feature",
|
"title": "Tilraunareiginleiki",
|
||||||
"description": "This page contains experimental or still-baking features. Please excuse the mess.",
|
"description": "This page contains experimental or still-baking features. Please excuse the mess.",
|
||||||
"issue-link-text": "Track our progress here"
|
"issue-link-text": "Track our progress here"
|
||||||
},
|
},
|
||||||
@@ -1334,7 +1339,7 @@
|
|||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"welcome-user": "👋 Velkomin/Velkominn/Velkomið, {0}!",
|
"welcome-user": "👋 Velkomin/Velkominn/Velkomið, {0}!",
|
||||||
"description": "Umsjá með prófíl, uppskriftum og hóp stillingum.",
|
"description": "Umsjá með prófíl, uppskriftum og hópstillingum.",
|
||||||
"invite-link": "Boð tengill",
|
"invite-link": "Boð tengill",
|
||||||
"get-invite-link": "Fá boð tengil",
|
"get-invite-link": "Fá boð tengil",
|
||||||
"get-public-link": "Fá almennan tengil",
|
"get-public-link": "Fá almennan tengil",
|
||||||
@@ -1388,7 +1393,7 @@
|
|||||||
"hide-cookbooks-from-other-households": "Fela uppskriftarbækur frá öðrum heimilum",
|
"hide-cookbooks-from-other-households": "Fela uppskriftarbækur frá öðrum heimilum",
|
||||||
"hide-cookbooks-from-other-households-description": "Þegar þetta er valið þá munu aðeins uppskriftarbækur úr þínu heimili birtast á hliðarstikunni",
|
"hide-cookbooks-from-other-households-description": "Þegar þetta er valið þá munu aðeins uppskriftarbækur úr þínu heimili birtast á hliðarstikunni",
|
||||||
"public-cookbook": "Opin uppskriftarbók",
|
"public-cookbook": "Opin uppskriftarbók",
|
||||||
"public-cookbook-description": "Opnar uppskriftarbækur er hægt að deila með þeim sem ekki eru notendur í mealie kerfinu og eru þær sýnilegar á hópsíðunni þinni.",
|
"public-cookbook-description": "Opnar uppskriftabækur er hægt að deila með þeim sem ekki eru notendur í Mealie kerfinu og eru þær sýnilegar á hópsíðunni þinni.",
|
||||||
"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": "Þarf alla flokka",
|
"require-all-categories": "Þarf alla flokka",
|
||||||
@@ -1396,7 +1401,7 @@
|
|||||||
"require-all-tools": "Require All Tools",
|
"require-all-tools": "Require All Tools",
|
||||||
"cookbook-name": "Nafn á uppskriftabók",
|
"cookbook-name": "Nafn á uppskriftabók",
|
||||||
"cookbook-with-name": "Uppskriftabók {0}",
|
"cookbook-with-name": "Uppskriftabók {0}",
|
||||||
"household-cookbook-name": "{0} Uppskriftabók {1}",
|
"household-cookbook-name": "Uppskriftabók {0} {1}",
|
||||||
"create-a-cookbook": "Stofna uppskrifabók",
|
"create-a-cookbook": "Stofna uppskrifabók",
|
||||||
"cookbook": "Uppskriftabók"
|
"cookbook": "Uppskriftabók"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Colazione",
|
"breakfast": "Colazione",
|
||||||
"lunch": "Pranzo",
|
"lunch": "Pranzo",
|
||||||
"dinner": "Cena",
|
"dinner": "Cena",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Qualsiasi",
|
"type-any": "Qualsiasi",
|
||||||
"day-any": "Qualsiasi",
|
"day-any": "Qualsiasi",
|
||||||
"editor": "Modifica",
|
"editor": "Modifica",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Prova l'importatore massivo",
|
"scrape-recipe-suggest-bulk-importer": "Prova l'importatore massivo",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Hai dei dati grezzi HTML o JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "Hai dei dati grezzi HTML o JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "È possibile importare direttamente dai dati grezzi",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "È possibile importare direttamente dai dati grezzi",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importa parole chiave originali come tag",
|
"import-original-keywords-as-tags": "Importa parole chiave originali come tag",
|
||||||
"stay-in-edit-mode": "Rimani in modalità Modifica",
|
"stay-in-edit-mode": "Rimani in modalità Modifica",
|
||||||
"parse-recipe-ingredients-after-import": "Analizza gli ingredienti della ricetta dopo l'importazione",
|
"parse-recipe-ingredients-after-import": "Analizza gli ingredienti della ricetta dopo l'importazione",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "朝食",
|
"breakfast": "朝食",
|
||||||
"lunch": "昼食",
|
"lunch": "昼食",
|
||||||
"dinner": "夕食",
|
"dinner": "夕食",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "すべて",
|
"type-any": "すべて",
|
||||||
"day-any": "すべて",
|
"day-any": "すべて",
|
||||||
"editor": "エディタ",
|
"editor": "エディタ",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "一括インポートを試す",
|
"scrape-recipe-suggest-bulk-importer": "一括インポートを試す",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "生の HTML または JSON データをお持ちですか?",
|
"scrape-recipe-have-raw-html-or-json-data": "生の HTML または JSON データをお持ちですか?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "生データから直接インポートできます",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "生データから直接インポートできます",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "元のキーワードをタグとしてインポート",
|
"import-original-keywords-as-tags": "元のキーワードをタグとしてインポート",
|
||||||
"stay-in-edit-mode": "編集モードを維持",
|
"stay-in-edit-mode": "編集モードを維持",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
@@ -692,11 +697,11 @@
|
|||||||
"reset-servings-count": "サービング数をリセット",
|
"reset-servings-count": "サービング数をリセット",
|
||||||
"not-linked-ingredients": "追加の材料",
|
"not-linked-ingredients": "追加の材料",
|
||||||
"upload-another-image": "Upload another image",
|
"upload-another-image": "Upload another image",
|
||||||
"upload-images": "Upload images",
|
"upload-images": "画像のアップロード",
|
||||||
"upload-more-images": "Upload more images",
|
"upload-more-images": "Upload more images",
|
||||||
"set-as-cover-image": "Set as recipe cover image",
|
"set-as-cover-image": "Set as recipe cover image",
|
||||||
"cover-image": "Cover image",
|
"cover-image": "カバー画像",
|
||||||
"include-linked-recipes": "Include Linked Recipes",
|
"include-linked-recipes": "リンクされたレシピを含める",
|
||||||
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
|
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
|
||||||
"toggle-recipe": "Toggle Recipe"
|
"toggle-recipe": "Toggle Recipe"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "조식",
|
"breakfast": "조식",
|
||||||
"lunch": "점심",
|
"lunch": "점심",
|
||||||
"dinner": "저녁 식사",
|
"dinner": "저녁 식사",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "모두",
|
"type-any": "모두",
|
||||||
"day-any": "모두",
|
"day-any": "모두",
|
||||||
"editor": "편집기",
|
"editor": "편집기",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Import original keywords as tags",
|
"import-original-keywords-as-tags": "Import original keywords as tags",
|
||||||
"stay-in-edit-mode": "Stay in Edit mode",
|
"stay-in-edit-mode": "Stay in Edit mode",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Pusryčiai",
|
"breakfast": "Pusryčiai",
|
||||||
"lunch": "Pietūs",
|
"lunch": "Pietūs",
|
||||||
"dinner": "Vakarienė",
|
"dinner": "Vakarienė",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Bet kas",
|
"type-any": "Bet kas",
|
||||||
"day-any": "Bet kas",
|
"day-any": "Bet kas",
|
||||||
"editor": "Redagavimas",
|
"editor": "Redagavimas",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Įkelti pradinius raktažodžius kaip žymas",
|
"import-original-keywords-as-tags": "Įkelti pradinius raktažodžius kaip žymas",
|
||||||
"stay-in-edit-mode": "Toliau redaguoti",
|
"stay-in-edit-mode": "Toliau redaguoti",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Brokastis",
|
"breakfast": "Brokastis",
|
||||||
"lunch": "pusdienas",
|
"lunch": "pusdienas",
|
||||||
"dinner": "Vakariņas",
|
"dinner": "Vakariņas",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Jebkurš",
|
"type-any": "Jebkurš",
|
||||||
"day-any": "Jebkurš",
|
"day-any": "Jebkurš",
|
||||||
"editor": "Redaktors",
|
"editor": "Redaktors",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Izmēģiniet lielapjoma importētāju",
|
"scrape-recipe-suggest-bulk-importer": "Izmēģiniet lielapjoma importētāju",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Vai jums ir neapstrādāti HTML vai JSON dati?",
|
"scrape-recipe-have-raw-html-or-json-data": "Vai jums ir neapstrādāti HTML vai JSON dati?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Jūs varat importēt no neapstrādātiem datiem tieši",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Jūs varat importēt no neapstrādātiem datiem tieši",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importējiet oriģinālos atslēgvārdus kā tagus",
|
"import-original-keywords-as-tags": "Importējiet oriģinālos atslēgvārdus kā tagus",
|
||||||
"stay-in-edit-mode": "Palieciet rediģēšanas režīmā",
|
"stay-in-edit-mode": "Palieciet rediģēšanas režīmā",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Ontbijt",
|
"breakfast": "Ontbijt",
|
||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"dinner": "Diner",
|
"dinner": "Diner",
|
||||||
|
"snack": "Tussendoortje",
|
||||||
|
"drink": "Drankje",
|
||||||
|
"dessert": "Toetje",
|
||||||
"type-any": "Alle",
|
"type-any": "Alle",
|
||||||
"day-any": "Elke",
|
"day-any": "Elke",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Probeer importeren in bulk",
|
"scrape-recipe-suggest-bulk-importer": "Probeer importeren in bulk",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Heb je onbewerkte HTML of JSON gegevens?",
|
"scrape-recipe-have-raw-html-or-json-data": "Heb je onbewerkte HTML of JSON gegevens?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "U kunt direct importeren uit onbewerkte gegevens",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "U kunt direct importeren uit onbewerkte gegevens",
|
||||||
|
"scrape-recipe-website-being-blocked": "Wordt de website geblokkeerd?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Probeer de HTML broncode te importeren.",
|
||||||
"import-original-keywords-as-tags": "Importeer oorspronkelijke trefwoorden als labels",
|
"import-original-keywords-as-tags": "Importeer oorspronkelijke trefwoorden als labels",
|
||||||
"stay-in-edit-mode": "Blijf in bewerkingsmodus",
|
"stay-in-edit-mode": "Blijf in bewerkingsmodus",
|
||||||
"parse-recipe-ingredients-after-import": "Ontleed de ingrediënten van het recept na importeren",
|
"parse-recipe-ingredients-after-import": "Ontleed de ingrediënten van het recept na importeren",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Frokost",
|
"breakfast": "Frokost",
|
||||||
"lunch": "Lunsj",
|
"lunch": "Lunsj",
|
||||||
"dinner": "Middag",
|
"dinner": "Middag",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Enhver",
|
"type-any": "Enhver",
|
||||||
"day-any": "Enhver",
|
"day-any": "Enhver",
|
||||||
"editor": "Redigeringsverktøy",
|
"editor": "Redigeringsverktøy",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Prøv masseimportering",
|
"scrape-recipe-suggest-bulk-importer": "Prøv masseimportering",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Har du HTML- eller JSON-rådata?",
|
"scrape-recipe-have-raw-html-or-json-data": "Har du HTML- eller JSON-rådata?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importere fra rådata direkte",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importere fra rådata direkte",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importer originale søkeord som emneord",
|
"import-original-keywords-as-tags": "Importer originale søkeord som emneord",
|
||||||
"stay-in-edit-mode": "Forbli i redigeringsmodus",
|
"stay-in-edit-mode": "Forbli i redigeringsmodus",
|
||||||
"parse-recipe-ingredients-after-import": "Analyser oppskriftens ingredienser etter at importen er fullført",
|
"parse-recipe-ingredients-after-import": "Analyser oppskriftens ingredienser etter at importen er fullført",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Śniadanie",
|
"breakfast": "Śniadanie",
|
||||||
"lunch": "Obiad",
|
"lunch": "Obiad",
|
||||||
"dinner": "Kolacja",
|
"dinner": "Kolacja",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Dowolny",
|
"type-any": "Dowolny",
|
||||||
"day-any": "Dowolny",
|
"day-any": "Dowolny",
|
||||||
"editor": "Edytor",
|
"editor": "Edytor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Wypróbuj importer zbiorczy",
|
"scrape-recipe-suggest-bulk-importer": "Wypróbuj importer zbiorczy",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Masz dane HTML bądź JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "Masz dane HTML bądź JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Możesz zaimportować bezpośrednio z surowych danych",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Możesz zaimportować bezpośrednio z surowych danych",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importuj oryginalne słowa kluczowe jako tagi",
|
"import-original-keywords-as-tags": "Importuj oryginalne słowa kluczowe jako tagi",
|
||||||
"stay-in-edit-mode": "Pozostań w trybie edycji",
|
"stay-in-edit-mode": "Pozostań w trybie edycji",
|
||||||
"parse-recipe-ingredients-after-import": "Analizuj składniki receptury po zaimportowaniu",
|
"parse-recipe-ingredients-after-import": "Analizuj składniki receptury po zaimportowaniu",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Café da manhã",
|
"breakfast": "Café da manhã",
|
||||||
"lunch": "Almoço",
|
"lunch": "Almoço",
|
||||||
"dinner": "Jantar",
|
"dinner": "Jantar",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Qualquer",
|
"type-any": "Qualquer",
|
||||||
"day-any": "Qualquer",
|
"day-any": "Qualquer",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Tente o importador em massa",
|
"scrape-recipe-suggest-bulk-importer": "Tente o importador em massa",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Tem dados HTML ou JSON brutos?",
|
"scrape-recipe-have-raw-html-or-json-data": "Tem dados HTML ou JSON brutos?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Você pode importar diretamente de dados brutos",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Você pode importar diretamente de dados brutos",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importar palavras-chave originais como marcadores",
|
"import-original-keywords-as-tags": "Importar palavras-chave originais como marcadores",
|
||||||
"stay-in-edit-mode": "Permanecer no modo de edição",
|
"stay-in-edit-mode": "Permanecer no modo de edição",
|
||||||
"parse-recipe-ingredients-after-import": "Interpretar os ingredientes da receita após importar",
|
"parse-recipe-ingredients-after-import": "Interpretar os ingredientes da receita após importar",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Pequeno-almoço",
|
"breakfast": "Pequeno-almoço",
|
||||||
"lunch": "Almoço",
|
"lunch": "Almoço",
|
||||||
"dinner": "Jantar",
|
"dinner": "Jantar",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Qualquer",
|
"type-any": "Qualquer",
|
||||||
"day-any": "Qualquer",
|
"day-any": "Qualquer",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Experimente o importador em massa",
|
"scrape-recipe-suggest-bulk-importer": "Experimente o importador em massa",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Tem dados HTML ou JSON em bruto?",
|
"scrape-recipe-have-raw-html-or-json-data": "Tem dados HTML ou JSON em bruto?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "É possível importar diretamente a partir de dados em bruto",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "É possível importar diretamente a partir de dados em bruto",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importar palavras-chave originais como etiquetas",
|
"import-original-keywords-as-tags": "Importar palavras-chave originais como etiquetas",
|
||||||
"stay-in-edit-mode": "Permanecer no modo de edição",
|
"stay-in-edit-mode": "Permanecer no modo de edição",
|
||||||
"parse-recipe-ingredients-after-import": "Analisar ingredientes da receita após a importação",
|
"parse-recipe-ingredients-after-import": "Analisar ingredientes da receita após a importação",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Mic dejun",
|
"breakfast": "Mic dejun",
|
||||||
"lunch": "Prânz",
|
"lunch": "Prânz",
|
||||||
"dinner": "Cină",
|
"dinner": "Cină",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Oricare",
|
"type-any": "Oricare",
|
||||||
"day-any": "Oricare",
|
"day-any": "Oricare",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Încearcă importatorul în bulk",
|
"scrape-recipe-suggest-bulk-importer": "Încearcă importatorul în bulk",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Ai date de tip HTML sau JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "Ai date de tip HTML sau JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Poți importa datele direct",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Poți importa datele direct",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importă cuvintele cheie originale ca tag-uri",
|
"import-original-keywords-as-tags": "Importă cuvintele cheie originale ca tag-uri",
|
||||||
"stay-in-edit-mode": "Rămâi în modul Editare",
|
"stay-in-edit-mode": "Rămâi în modul Editare",
|
||||||
"parse-recipe-ingredients-after-import": "Analizează ingredientele rețetei după import",
|
"parse-recipe-ingredients-after-import": "Analizează ingredientele rețetei după import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Завтрак",
|
"breakfast": "Завтрак",
|
||||||
"lunch": "Обед",
|
"lunch": "Обед",
|
||||||
"dinner": "Ужин",
|
"dinner": "Ужин",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Любой",
|
"type-any": "Любой",
|
||||||
"day-any": "Любой",
|
"day-any": "Любой",
|
||||||
"editor": "Редактор",
|
"editor": "Редактор",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Воспользуйтесь массовым импортом",
|
"scrape-recipe-suggest-bulk-importer": "Воспользуйтесь массовым импортом",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "У Вас есть данные HTML или JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "У Вас есть данные HTML или JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Вы можете импортировать напрямую из необработанных данных",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Вы можете импортировать напрямую из необработанных данных",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Импортировать исходные ключевые слова как теги",
|
"import-original-keywords-as-tags": "Импортировать исходные ключевые слова как теги",
|
||||||
"stay-in-edit-mode": "Остаться в режиме редактирования",
|
"stay-in-edit-mode": "Остаться в режиме редактирования",
|
||||||
"parse-recipe-ingredients-after-import": "Распознавание ингредиентов рецепта после импорта",
|
"parse-recipe-ingredients-after-import": "Распознавание ингредиентов рецепта после импорта",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Raňajky",
|
"breakfast": "Raňajky",
|
||||||
"lunch": "Obed",
|
"lunch": "Obed",
|
||||||
"dinner": "Večera",
|
"dinner": "Večera",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Ľubovoľný",
|
"type-any": "Ľubovoľný",
|
||||||
"day-any": "Ľubovoľný",
|
"day-any": "Ľubovoľný",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Vyskúšajte hromadný importér",
|
"scrape-recipe-suggest-bulk-importer": "Vyskúšajte hromadný importér",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Máte surové údaje HTML alebo JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "Máte surové údaje HTML alebo JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Môžete importovať priamo nespracované údaje",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Môžete importovať priamo nespracované údaje",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Importovať pôvodné kľúčové slová ako štítky",
|
"import-original-keywords-as-tags": "Importovať pôvodné kľúčové slová ako štítky",
|
||||||
"stay-in-edit-mode": "Zostať v režime editovania",
|
"stay-in-edit-mode": "Zostať v režime editovania",
|
||||||
"parse-recipe-ingredients-after-import": "Analyzovať ingrediencie po importe",
|
"parse-recipe-ingredients-after-import": "Analyzovať ingrediencie po importe",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Zajtrk",
|
"breakfast": "Zajtrk",
|
||||||
"lunch": "Kosilo",
|
"lunch": "Kosilo",
|
||||||
"dinner": "Večerja",
|
"dinner": "Večerja",
|
||||||
|
"snack": "Prigrizek",
|
||||||
|
"drink": "Pijača",
|
||||||
|
"dessert": "Sladica",
|
||||||
"type-any": "Katerikoli",
|
"type-any": "Katerikoli",
|
||||||
"day-any": "Katerikoli",
|
"day-any": "Katerikoli",
|
||||||
"editor": "Urednik",
|
"editor": "Urednik",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Preizkusi masovni uvoz",
|
"scrape-recipe-suggest-bulk-importer": "Preizkusi masovni uvoz",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Imaš surove HTML ali JSON podatke?",
|
"scrape-recipe-have-raw-html-or-json-data": "Imaš surove HTML ali JSON podatke?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Uvoziš lahko neposredno iz surovih podatkov",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Uvoziš lahko neposredno iz surovih podatkov",
|
||||||
|
"scrape-recipe-website-being-blocked": "Je spletna stran blokirana?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Poskusite namesto tega uvoziti surovi HTML.",
|
||||||
"import-original-keywords-as-tags": "Uvozi izvorne ključne besede kot značke",
|
"import-original-keywords-as-tags": "Uvozi izvorne ključne besede kot značke",
|
||||||
"stay-in-edit-mode": "Urejaj naprej",
|
"stay-in-edit-mode": "Urejaj naprej",
|
||||||
"parse-recipe-ingredients-after-import": "Razčlenitev sestavin recepta po uvozu",
|
"parse-recipe-ingredients-after-import": "Razčlenitev sestavin recepta po uvozu",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Доручак",
|
"breakfast": "Доручак",
|
||||||
"lunch": "Ручак",
|
"lunch": "Ручак",
|
||||||
"dinner": "Вечера",
|
"dinner": "Вечера",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Било који",
|
"type-any": "Било који",
|
||||||
"day-any": "Било који",
|
"day-any": "Било који",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Увези оригиналне кључне речи као ознаке",
|
"import-original-keywords-as-tags": "Увези оригиналне кључне речи као ознаке",
|
||||||
"stay-in-edit-mode": "Stay in Edit mode",
|
"stay-in-edit-mode": "Stay in Edit mode",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Frukost",
|
"breakfast": "Frukost",
|
||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"dinner": "Middag",
|
"dinner": "Middag",
|
||||||
|
"snack": "Snacks",
|
||||||
|
"drink": "Drinkar",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Alla",
|
"type-any": "Alla",
|
||||||
"day-any": "Alla",
|
"day-any": "Alla",
|
||||||
"editor": "Redigerare",
|
"editor": "Redigerare",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Testa massimportören",
|
"scrape-recipe-suggest-bulk-importer": "Testa massimportören",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Har rå HTML eller JSON-data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Har rå HTML eller JSON-data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importera från rådata direkt",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importera från rådata direkt",
|
||||||
|
"scrape-recipe-website-being-blocked": "Är websidan blockerad?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Prova att importera HTML istället.",
|
||||||
"import-original-keywords-as-tags": "Importera ursprungliga sökord som taggar",
|
"import-original-keywords-as-tags": "Importera ursprungliga sökord som taggar",
|
||||||
"stay-in-edit-mode": "Stanna kvar i redigeringsläge",
|
"stay-in-edit-mode": "Stanna kvar i redigeringsläge",
|
||||||
"parse-recipe-ingredients-after-import": "Tolka receptingredienser efter import",
|
"parse-recipe-ingredients-after-import": "Tolka receptingredienser efter import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Kahvaltı",
|
"breakfast": "Kahvaltı",
|
||||||
"lunch": "Öğle Yemeği",
|
"lunch": "Öğle Yemeği",
|
||||||
"dinner": "Akşam Yemeği",
|
"dinner": "Akşam Yemeği",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Herhangi",
|
"type-any": "Herhangi",
|
||||||
"day-any": "Herhangi",
|
"day-any": "Herhangi",
|
||||||
"editor": "Editör",
|
"editor": "Editör",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Toplu ithalatçıyı deneyin",
|
"scrape-recipe-suggest-bulk-importer": "Toplu ithalatçıyı deneyin",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Ham HTML veya JSON verileriniz mi var?",
|
"scrape-recipe-have-raw-html-or-json-data": "Ham HTML veya JSON verileriniz mi var?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Ham veriden doğrudan içe aktarabilirsiniz",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Ham veriden doğrudan içe aktarabilirsiniz",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Orijinal anahtar kelimeleri etiket olarak içe aktar",
|
"import-original-keywords-as-tags": "Orijinal anahtar kelimeleri etiket olarak içe aktar",
|
||||||
"stay-in-edit-mode": "Düzenleme modunda kalın",
|
"stay-in-edit-mode": "Düzenleme modunda kalın",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Сніданок",
|
"breakfast": "Сніданок",
|
||||||
"lunch": "Обід",
|
"lunch": "Обід",
|
||||||
"dinner": "Вечеря",
|
"dinner": "Вечеря",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Будь-який",
|
"type-any": "Будь-який",
|
||||||
"day-any": "Будь-який",
|
"day-any": "Будь-який",
|
||||||
"editor": "Редактор",
|
"editor": "Редактор",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Спробуйте масовий розпізнавач",
|
"scrape-recipe-suggest-bulk-importer": "Спробуйте масовий розпізнавач",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Маєте необроблені дані HTML або JSON?",
|
"scrape-recipe-have-raw-html-or-json-data": "Маєте необроблені дані HTML або JSON?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "Ви можете імпортувати необроблені дані",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "Ви можете імпортувати необроблені дані",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Імпортувати оригінальні ключові слова як теги",
|
"import-original-keywords-as-tags": "Імпортувати оригінальні ключові слова як теги",
|
||||||
"stay-in-edit-mode": "Залишитися в режимі редактора",
|
"stay-in-edit-mode": "Залишитися в режимі редактора",
|
||||||
"parse-recipe-ingredients-after-import": "Розпізнавання інгредієнтів рецепту після імпорту",
|
"parse-recipe-ingredients-after-import": "Розпізнавання інгредієнтів рецепту після імпорту",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Breakfast",
|
"breakfast": "Breakfast",
|
||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"dinner": "Dinner",
|
"dinner": "Dinner",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Any",
|
"type-any": "Any",
|
||||||
"day-any": "Any",
|
"day-any": "Any",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Import original keywords as tags",
|
"import-original-keywords-as-tags": "Import original keywords as tags",
|
||||||
"stay-in-edit-mode": "Stay in Edit mode",
|
"stay-in-edit-mode": "Stay in Edit mode",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "早餐",
|
"breakfast": "早餐",
|
||||||
"lunch": "午餐",
|
"lunch": "午餐",
|
||||||
"dinner": "晚餐",
|
"dinner": "晚餐",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "甜点",
|
||||||
"type-any": "任意",
|
"type-any": "任意",
|
||||||
"day-any": "任意",
|
"day-any": "任意",
|
||||||
"editor": "编辑器",
|
"editor": "编辑器",
|
||||||
@@ -449,8 +452,8 @@
|
|||||||
"import-by-url": "通过网址导入食谱",
|
"import-by-url": "通过网址导入食谱",
|
||||||
"create-manually": "手动创建食谱",
|
"create-manually": "手动创建食谱",
|
||||||
"make-recipe-image": "将此设为食谱图片",
|
"make-recipe-image": "将此设为食谱图片",
|
||||||
"add-food": "Add Food",
|
"add-food": "添加食物",
|
||||||
"add-recipe": "Add Recipe"
|
"add-recipe": "添加食谱"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"404-page-not-found": "404-页面未找到",
|
"404-page-not-found": "404-页面未找到",
|
||||||
@@ -517,9 +520,9 @@
|
|||||||
"recipe-deleted": "食谱已删除",
|
"recipe-deleted": "食谱已删除",
|
||||||
"recipe-image": "食谱图片",
|
"recipe-image": "食谱图片",
|
||||||
"recipe-image-updated": "食谱图片已更新",
|
"recipe-image-updated": "食谱图片已更新",
|
||||||
"delete-image": "Delete Recipe Image",
|
"delete-image": "删除食谱图像",
|
||||||
"delete-image-confirmation": "Are you sure you want to delete this recipe image?",
|
"delete-image-confirmation": "您确定要删除此食谱图像吗?",
|
||||||
"recipe-image-deleted": "Recipe image deleted",
|
"recipe-image-deleted": "食谱图片已删除",
|
||||||
"recipe-name": "食谱名称",
|
"recipe-name": "食谱名称",
|
||||||
"recipe-settings": "食谱设置",
|
"recipe-settings": "食谱设置",
|
||||||
"recipe-update-failed": "食谱更新失败",
|
"recipe-update-failed": "食谱更新失败",
|
||||||
@@ -571,7 +574,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",
|
"unlinked": "未链接",
|
||||||
"linked-to-other-step": "已关联到其他步骤",
|
"linked-to-other-step": "已关联到其他步骤",
|
||||||
"auto": "自动",
|
"auto": "自动",
|
||||||
"cook-mode": "烹饪模式",
|
"cook-mode": "烹饪模式",
|
||||||
@@ -633,9 +636,11 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "试试批量导入器",
|
"scrape-recipe-suggest-bulk-importer": "试试批量导入器",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "有原始 HTML 或 JSON 数据?",
|
"scrape-recipe-have-raw-html-or-json-data": "有原始 HTML 或 JSON 数据?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "您可以直接从原始数据导入",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "您可以直接从原始数据导入",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "导入原始关键字作为标签",
|
"import-original-keywords-as-tags": "导入原始关键字作为标签",
|
||||||
"stay-in-edit-mode": "留在编辑模式",
|
"stay-in-edit-mode": "留在编辑模式",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "导入后解析食材",
|
||||||
"import-from-zip": "从Zip压缩包导入",
|
"import-from-zip": "从Zip压缩包导入",
|
||||||
"import-from-zip-description": "导入从另一个Mealie应用导出的单个食谱。",
|
"import-from-zip-description": "导入从另一个Mealie应用导出的单个食谱。",
|
||||||
"import-from-html-or-json": "从 HTML 或 JSON 导入",
|
"import-from-html-or-json": "从 HTML 或 JSON 导入",
|
||||||
@@ -683,10 +688,10 @@
|
|||||||
"this-food-could-not-be-parsed-automatically": "这种食物不能被自动解析",
|
"this-food-could-not-be-parsed-automatically": "这种食物不能被自动解析",
|
||||||
"no-food": "没有食物",
|
"no-food": "没有食物",
|
||||||
"review-parsed-ingredients": "Review parsed ingredients",
|
"review-parsed-ingredients": "Review parsed ingredients",
|
||||||
"confidence-score": "Confidence Score",
|
"confidence-score": "置信度",
|
||||||
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
|
"ingredient-parser-description": "您的食材已成功解析。请检查我们不确定的食材。",
|
||||||
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
|
"ingredient-parser-final-review-description": "一旦所有食材都检查完毕,在将更改应用到您的菜谱之前,您还有最后一次机会检查所有食材。",
|
||||||
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
|
"add-text-as-alias-for-item": "将“{text}”添加为“{item}”的别名",
|
||||||
"delete-item": "Delete Item"
|
"delete-item": "Delete Item"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "重置份量数量",
|
"reset-servings-count": "重置份量数量",
|
||||||
@@ -696,7 +701,7 @@
|
|||||||
"upload-more-images": "上传更多图片",
|
"upload-more-images": "上传更多图片",
|
||||||
"set-as-cover-image": "设置为食谱封面图片",
|
"set-as-cover-image": "设置为食谱封面图片",
|
||||||
"cover-image": "封面图片",
|
"cover-image": "封面图片",
|
||||||
"include-linked-recipes": "Include Linked Recipes",
|
"include-linked-recipes": "包含已链接的菜谱",
|
||||||
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
|
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
|
||||||
"toggle-recipe": "Toggle Recipe"
|
"toggle-recipe": "Toggle Recipe"
|
||||||
},
|
},
|
||||||
@@ -736,7 +741,7 @@
|
|||||||
"advanced": "高级",
|
"advanced": "高级",
|
||||||
"auto-search": "自动搜索",
|
"auto-search": "自动搜索",
|
||||||
"no-results": "未找到任何结果",
|
"no-results": "未找到任何结果",
|
||||||
"type-to-search": "Type to search..."
|
"type-to-search": "键入以搜索……"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"add-a-new-theme": "新增布景主题",
|
"add-a-new-theme": "新增布景主题",
|
||||||
@@ -879,7 +884,7 @@
|
|||||||
"ldap-ready": "LDAP 已就绪",
|
"ldap-ready": "LDAP 已就绪",
|
||||||
"ldap-ready-error-text": "某些LDAP环境变量尚未配置。(如果你不使用LDAP验证可以忽略该报错)",
|
"ldap-ready-error-text": "某些LDAP环境变量尚未配置。(如果你不使用LDAP验证可以忽略该报错)",
|
||||||
"ldap-ready-success-text": "LDAP所需的环境变量均已配置。",
|
"ldap-ready-success-text": "LDAP所需的环境变量均已配置。",
|
||||||
"build": "生成",
|
"build": "构建",
|
||||||
"recipe-scraper-version": "食谱刮削器版本",
|
"recipe-scraper-version": "食谱刮削器版本",
|
||||||
"oidc-ready": "OIDC 已就绪",
|
"oidc-ready": "OIDC 已就绪",
|
||||||
"oidc-ready-error-text": "某些OIDC环境变量尚未配置。(如果你不使用OIDC验证可以忽略该报错)",
|
"oidc-ready-error-text": "某些OIDC环境变量尚未配置。(如果你不使用OIDC验证可以忽略该报错)",
|
||||||
|
|||||||
@@ -342,6 +342,9 @@
|
|||||||
"breakfast": "Breakfast",
|
"breakfast": "Breakfast",
|
||||||
"lunch": "午餐",
|
"lunch": "午餐",
|
||||||
"dinner": "晚餐",
|
"dinner": "晚餐",
|
||||||
|
"snack": "Snack",
|
||||||
|
"drink": "Drink",
|
||||||
|
"dessert": "Dessert",
|
||||||
"type-any": "Any",
|
"type-any": "Any",
|
||||||
"day-any": "Any",
|
"day-any": "Any",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
@@ -633,6 +636,8 @@
|
|||||||
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
|
||||||
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
|
||||||
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
|
||||||
|
"scrape-recipe-website-being-blocked": "Website being blocked?",
|
||||||
|
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
|
||||||
"import-original-keywords-as-tags": "Import original keywords as tags",
|
"import-original-keywords-as-tags": "Import original keywords as tags",
|
||||||
"stay-in-edit-mode": "Stay in Edit mode",
|
"stay-in-edit-mode": "Stay in Edit mode",
|
||||||
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type PlanEntryType = "breakfast" | "lunch" | "dinner" | "side";
|
export type PlanEntryType = "breakfast" | "lunch" | "dinner" | "side" | "snack" | "drink" | "dessert";
|
||||||
export type PlanRulesDay = "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday" | "unset";
|
export type PlanRulesDay = "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday" | "unset";
|
||||||
export type PlanRulesType = "breakfast" | "lunch" | "dinner" | "side" | "unset";
|
export type PlanRulesType = "breakfast" | "lunch" | "dinner" | "side" | "snack" | "drink" | "dessert" | "unset";
|
||||||
export type LogicalOperator = "AND" | "OR";
|
export type LogicalOperator = "AND" | "OR";
|
||||||
export type RelationalKeyword = "IS" | "IS NOT" | "IN" | "NOT IN" | "CONTAINS ALL" | "LIKE" | "NOT LIKE";
|
export type RelationalKeyword = "IS" | "IS NOT" | "IN" | "NOT IN" | "CONTAINS ALL" | "LIKE" | "NOT LIKE";
|
||||||
export type RelationalOperator = "=" | "<>" | ">" | "<" | ">=" | "<=";
|
export type RelationalOperator = "=" | "<>" | ">" | "<" | ">=" | "<=";
|
||||||
@@ -53,7 +53,6 @@ export interface QueryFilterJSONPart {
|
|||||||
attributeName?: string | null;
|
attributeName?: string | null;
|
||||||
relationalOperator?: RelationalKeyword | RelationalOperator | null;
|
relationalOperator?: RelationalKeyword | RelationalOperator | null;
|
||||||
value?: string | string[] | null;
|
value?: string | string[] | null;
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
}
|
||||||
export interface PlanRulesSave {
|
export interface PlanRulesSave {
|
||||||
day?: PlanRulesDay;
|
day?: PlanRulesDay;
|
||||||
@@ -106,14 +105,12 @@ export interface RecipeCategory {
|
|||||||
groupId?: string | null;
|
groupId?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
}
|
||||||
export interface RecipeTag {
|
export interface RecipeTag {
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
groupId?: string | null;
|
groupId?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
}
|
||||||
export interface RecipeTool {
|
export interface RecipeTool {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -121,7 +118,6 @@ export interface RecipeTool {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
householdsWithTool?: string[];
|
householdsWithTool?: string[];
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
}
|
||||||
export interface SavePlanEntry {
|
export interface SavePlanEntry {
|
||||||
date: string;
|
date: string;
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export type RecipeOrganizer
|
|||||||
| "tags"
|
| "tags"
|
||||||
| "tools"
|
| "tools"
|
||||||
| "foods"
|
| "foods"
|
||||||
| "households";
|
| "households"
|
||||||
|
| "users";
|
||||||
|
|
||||||
export enum Organizer {
|
export enum Organizer {
|
||||||
Category = "categories",
|
Category = "categories",
|
||||||
@@ -37,4 +38,5 @@ export enum Organizer {
|
|||||||
Tool = "tools",
|
Tool = "tools",
|
||||||
Food = "foods",
|
Food = "foods",
|
||||||
Household = "households",
|
Household = "households",
|
||||||
|
User = "users",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ import {
|
|||||||
mdiWebhook,
|
mdiWebhook,
|
||||||
mdiWindowClose,
|
mdiWindowClose,
|
||||||
mdiWrench,
|
mdiWrench,
|
||||||
|
mdiHandWaveOutline,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
@@ -287,6 +288,7 @@ export const icons = {
|
|||||||
undo: mdiUndo,
|
undo: mdiUndo,
|
||||||
bread: mdiCookie,
|
bread: mdiCookie,
|
||||||
fileSign: mdiFileSign,
|
fileSign: mdiFileSign,
|
||||||
|
wave: mdiHandWaveOutline,
|
||||||
|
|
||||||
// Crud
|
// Crud
|
||||||
backArrow: mdiArrowLeftBoldOutline,
|
backArrow: mdiArrowLeftBoldOutline,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mealie",
|
"name": "mealie",
|
||||||
"version": "3.5.0",
|
"version": "3.7.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
|
|||||||
@@ -19,35 +19,45 @@
|
|||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<!-- Stepper Wizard -->
|
<!-- Stepper Wizard -->
|
||||||
<v-stepper v-model="currentPage" mobile-breakpoint="sm">
|
<v-stepper v-model="currentPage" mobile-breakpoint="sm" alt-labels>
|
||||||
<v-stepper-header>
|
<v-stepper-header>
|
||||||
<v-stepper-item
|
<v-stepper-item
|
||||||
:value="Pages.LANDING"
|
:value="Pages.LANDING"
|
||||||
|
:icon="$globals.icons.wave"
|
||||||
:complete="currentPage > Pages.LANDING"
|
:complete="currentPage > Pages.LANDING"
|
||||||
|
:color="getStepperColor(currentPage, Pages.LANDING)"
|
||||||
:title="$t('general.start')"
|
:title="$t('general.start')"
|
||||||
/>
|
/>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-stepper-item
|
<v-stepper-item
|
||||||
:value="Pages.USER_INFO"
|
:value="Pages.USER_INFO"
|
||||||
|
:icon="$globals.icons.user"
|
||||||
:complete="currentPage > Pages.USER_INFO"
|
:complete="currentPage > Pages.USER_INFO"
|
||||||
|
:color="getStepperColor(currentPage, Pages.USER_INFO)"
|
||||||
:title="$t('user-registration.account-details')"
|
:title="$t('user-registration.account-details')"
|
||||||
/>
|
/>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-stepper-item
|
<v-stepper-item
|
||||||
:value="Pages.PAGE_2"
|
:value="Pages.PAGE_2"
|
||||||
|
:icon="$globals.icons.cog"
|
||||||
:complete="currentPage > Pages.PAGE_2"
|
:complete="currentPage > Pages.PAGE_2"
|
||||||
|
:color="getStepperColor(currentPage, Pages.PAGE_2)"
|
||||||
:title="$t('settings.site-settings')"
|
:title="$t('settings.site-settings')"
|
||||||
/>
|
/>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-stepper-item
|
<v-stepper-item
|
||||||
:value="Pages.CONFIRM"
|
:value="Pages.CONFIRM"
|
||||||
|
:icon="$globals.icons.chefHat"
|
||||||
:complete="currentPage > Pages.CONFIRM"
|
:complete="currentPage > Pages.CONFIRM"
|
||||||
|
:color="getStepperColor(currentPage, Pages.CONFIRM)"
|
||||||
:title="$t('admin.maintenance.summary-title')"
|
:title="$t('admin.maintenance.summary-title')"
|
||||||
/>
|
/>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-stepper-item
|
<v-stepper-item
|
||||||
:value="Pages.END"
|
:value="Pages.END"
|
||||||
|
:icon="$globals.icons.check"
|
||||||
:complete="currentPage > Pages.END"
|
:complete="currentPage > Pages.END"
|
||||||
|
:color="getStepperColor(currentPage, Pages.END)"
|
||||||
:title="$t('admin.setup.setup-complete')"
|
:title="$t('admin.setup.setup-complete')"
|
||||||
/>
|
/>
|
||||||
</v-stepper-header>
|
</v-stepper-header>
|
||||||
@@ -82,6 +92,8 @@
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
size="large"
|
size="large"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
class="px-10"
|
||||||
|
rounded
|
||||||
:icon="$globals.icons.translate"
|
:icon="$globals.icons.translate"
|
||||||
@click="langDialog = true"
|
@click="langDialog = true"
|
||||||
>
|
>
|
||||||
@@ -95,6 +107,16 @@
|
|||||||
next-text="general.next"
|
next-text="general.next"
|
||||||
@click:next="onNext"
|
@click:next="onNext"
|
||||||
>
|
>
|
||||||
|
<template #next>
|
||||||
|
<v-btn
|
||||||
|
variant="flat"
|
||||||
|
color="success"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:text="$t('general.next')"
|
||||||
|
@click="onNext"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #prev />
|
<template #prev />
|
||||||
</v-stepper-actions>
|
</v-stepper-actions>
|
||||||
</v-stepper-window-item>
|
</v-stepper-window-item>
|
||||||
@@ -107,10 +129,19 @@
|
|||||||
<v-stepper-actions
|
<v-stepper-actions
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
prev-text="general.back"
|
prev-text="general.back"
|
||||||
next-text="general.next"
|
|
||||||
@click:prev="onPrev"
|
@click:prev="onPrev"
|
||||||
@click:next="onNext"
|
>
|
||||||
/>
|
<template #next>
|
||||||
|
<v-btn
|
||||||
|
variant="flat"
|
||||||
|
color="success"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:text="$t('general.next')"
|
||||||
|
@click="onNext"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-stepper-actions>
|
||||||
</v-stepper-window-item>
|
</v-stepper-window-item>
|
||||||
|
|
||||||
<!-- COMMON SETTINGS -->
|
<!-- COMMON SETTINGS -->
|
||||||
@@ -127,10 +158,19 @@
|
|||||||
<v-stepper-actions
|
<v-stepper-actions
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
prev-text="general.back"
|
prev-text="general.back"
|
||||||
next-text="general.next"
|
|
||||||
@click:prev="onPrev"
|
@click:prev="onPrev"
|
||||||
@click:next="onNext"
|
>
|
||||||
/>
|
<template #next>
|
||||||
|
<v-btn
|
||||||
|
variant="flat"
|
||||||
|
color="success"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:text="$t('general.next')"
|
||||||
|
@click="onNext"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-stepper-actions>
|
||||||
</v-stepper-window-item>
|
</v-stepper-window-item>
|
||||||
|
|
||||||
<!-- CONFIRMATION -->
|
<!-- CONFIRMATION -->
|
||||||
@@ -177,35 +217,7 @@
|
|||||||
|
|
||||||
<!-- END -->
|
<!-- END -->
|
||||||
<v-stepper-window-item :value="Pages.END">
|
<v-stepper-window-item :value="Pages.END">
|
||||||
<v-container max-width="880">
|
<EndPageContent />
|
||||||
<v-card-title class="text-h4 justify-center">
|
|
||||||
{{ $t('admin.setup.setup-complete') }}
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-title class="text-h6 justify-center">
|
|
||||||
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
|
|
||||||
</v-card-title>
|
|
||||||
<div
|
|
||||||
v-for="link, idx in setupCompleteLinks"
|
|
||||||
:key="idx"
|
|
||||||
class="px-4 pt-4"
|
|
||||||
>
|
|
||||||
<div v-if="link.section">
|
|
||||||
<v-divider v-if="idx" />
|
|
||||||
<v-card-text class="headline pl-0">
|
|
||||||
{{ link.section }}
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
<v-btn
|
|
||||||
:to="link.to"
|
|
||||||
color="info"
|
|
||||||
>
|
|
||||||
{{ link.text }}
|
|
||||||
</v-btn>
|
|
||||||
<v-card-text class="subtitle px-0 py-2">
|
|
||||||
{{ link.description }}
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</v-container>
|
|
||||||
<v-stepper-actions
|
<v-stepper-actions
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
prev-text="general.back"
|
prev-text="general.back"
|
||||||
@@ -266,11 +278,21 @@ useSeoMeta({
|
|||||||
});
|
});
|
||||||
|
|
||||||
enum Pages {
|
enum Pages {
|
||||||
LANDING = 0,
|
LANDING = 1,
|
||||||
USER_INFO = 1,
|
USER_INFO = 2,
|
||||||
PAGE_2 = 2,
|
PAGE_2 = 3,
|
||||||
CONFIRM = 3,
|
CONFIRM = 4,
|
||||||
END = 4,
|
END = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepperColor(currentPage: Pages, page: Pages) {
|
||||||
|
if (currentPage == page) {
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
if (currentPage > page) {
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -317,42 +339,6 @@ const confirmationData = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupCompleteLinks = ref([
|
|
||||||
{
|
|
||||||
section: i18n.t("profile.data-migrations"),
|
|
||||||
to: "/admin/backups",
|
|
||||||
text: i18n.t("settings.backup.backup-restore"),
|
|
||||||
description: i18n.t("admin.setup.restore-from-v1-backup"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: "/group/migrations",
|
|
||||||
text: i18n.t("migration.recipe-migration"),
|
|
||||||
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
section: i18n.t("recipe.create-recipes"),
|
|
||||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
|
|
||||||
text: i18n.t("recipe.create-recipe"),
|
|
||||||
description: i18n.t("recipe.create-recipe-description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
|
|
||||||
text: i18n.t("recipe.import-with-url"),
|
|
||||||
description: i18n.t("recipe.scrape-recipe-description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
section: i18n.t("user.manage-users"),
|
|
||||||
to: "/admin/manage/users",
|
|
||||||
text: i18n.t("user.manage-users"),
|
|
||||||
description: i18n.t("user.manage-users-description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: "/user/profile",
|
|
||||||
text: i18n.t("profile.manage-user-profile"),
|
|
||||||
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Page Navigation
|
// Page Navigation
|
||||||
const currentPage = ref(Pages.LANDING);
|
const currentPage = ref(Pages.LANDING);
|
||||||
@@ -548,7 +534,7 @@ async function onFinish() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.icon-white {
|
.icon-white {
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
@@ -575,4 +561,18 @@ async function onFinish() {
|
|||||||
.bg-off-white {
|
.bg-off-white {
|
||||||
background: #f5f8fa;
|
background: #f5f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-stepper-item__avatar.v-avatar.v-stepper-item__avatar.v-avatar {
|
||||||
|
width: 3rem !important; /** Override inline style :( */
|
||||||
|
height: 3rem !important; /** Override inline style :( */
|
||||||
|
margin-inline-end: 0; /** reset weird margin */
|
||||||
|
|
||||||
|
.v-icon {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-stepper--alt-labels .v-stepper-header .v-divider {
|
||||||
|
margin: 48px -42px 0 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
<p>{{ $t('recipe.scrape-recipe-description') }}</p>
|
<p>{{ $t('recipe.scrape-recipe-description') }}</p>
|
||||||
<p>
|
<p>
|
||||||
{{ $t('recipe.scrape-recipe-have-a-lot-of-recipes') }}
|
{{ $t('recipe.scrape-recipe-have-a-lot-of-recipes') }}
|
||||||
<a :href="bulkImporterTarget">{{ $t('recipe.scrape-recipe-suggest-bulk-importer') }}</a>.
|
<router-link :to="bulkImporterTarget">{{ $t('recipe.scrape-recipe-suggest-bulk-importer') }}</router-link>.
|
||||||
<br>
|
<br>
|
||||||
{{ $t('recipe.scrape-recipe-have-raw-html-or-json-data') }}
|
{{ $t('recipe.scrape-recipe-have-raw-html-or-json-data') }}
|
||||||
<a :href="htmlOrJsonImporterTarget">{{ $t('recipe.scrape-recipe-you-can-import-from-raw-data-directly') }}</a>.
|
<router-link :to="htmlOrJsonImporterTarget">{{ $t('recipe.scrape-recipe-you-can-import-from-raw-data-directly') }}</router-link>.
|
||||||
</p>
|
</p>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="recipeUrl"
|
v-model="recipeUrl"
|
||||||
@@ -81,10 +81,17 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="my-3 mx-2" />
|
<v-divider class="my-3 mx-2" />
|
||||||
|
|
||||||
<p>
|
<div class="force-url-white">
|
||||||
{{ $t("new-recipe.error-details") }}
|
<p>
|
||||||
</p>
|
{{ $t("recipe.scrape-recipe-website-being-blocked") }}
|
||||||
<div class="d-flex row justify-space-around my-3 force-white">
|
<router-link :to="htmlOrJsonImporterTarget">{{ $t("recipe.scrape-recipe-try-importing-raw-html-instead") }}</router-link>
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ $t("new-recipe.error-details") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex row justify-space-around my-3 force-url-white">
|
||||||
<a
|
<a
|
||||||
class="dark"
|
class="dark"
|
||||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
href="https://developers.google.com/search/docs/data-types/recipe"
|
||||||
@@ -202,6 +209,16 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
const domUrlForm = ref<VForm | null>(null);
|
const domUrlForm = ref<VForm | null>(null);
|
||||||
|
|
||||||
|
// Remove import URL from query params when leaving the page
|
||||||
|
const isLeaving = ref(false);
|
||||||
|
onBeforeRouteLeave((to) => {
|
||||||
|
if (isLeaving.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLeaving.value = true;
|
||||||
|
router.replace({ query: undefined }).then(() => router.push(to));
|
||||||
|
});
|
||||||
|
|
||||||
async function createByUrl(url: string | null, importKeywordsAsTags: boolean) {
|
async function createByUrl(url: string | null, importKeywordsAsTags: boolean) {
|
||||||
if (url === null) {
|
if (url === null) {
|
||||||
return;
|
return;
|
||||||
@@ -232,8 +249,8 @@ export default defineNuxtComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.force-white > a {
|
.force-url-white a {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -76,13 +76,15 @@
|
|||||||
can-confirm
|
can-confirm
|
||||||
@confirm="saveQueryFilter"
|
@confirm="saveQueryFilter"
|
||||||
>
|
>
|
||||||
<QueryFilterBuilder
|
<v-card-text>
|
||||||
:key="queryFilterMenuKey"
|
<QueryFilterBuilder
|
||||||
:initial-query-filter="queryFilterJSON"
|
:key="queryFilterMenuKey"
|
||||||
:field-defs="queryFilterBuilderFields"
|
:initial-query-filter="queryFilterJSON"
|
||||||
@input="(value) => queryFilterEditorValue = value"
|
:field-defs="queryFilterBuilderFields"
|
||||||
@input-j-s-o-n="(value) => queryFilterEditorValueJSON = value"
|
@input="(value) => queryFilterEditorValue = value"
|
||||||
/>
|
@input-j-s-o-n="(value) => queryFilterEditorValueJSON = value"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
<template #custom-card-action>
|
<template #custom-card-action>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color="error"
|
color="error"
|
||||||
@@ -653,6 +655,11 @@ export default defineNuxtComponent({
|
|||||||
label: i18n.t("household.households"),
|
label: i18n.t("household.households"),
|
||||||
type: Organizer.Household,
|
type: Organizer.Household,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "user_id",
|
||||||
|
label: i18n.t("user.users"),
|
||||||
|
type: Organizer.User,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function clearQueryFilter() {
|
function clearQueryFilter() {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
v-model="fromFood"
|
v-model="fromFood"
|
||||||
return-object
|
return-object
|
||||||
:items="foods"
|
:items="foods"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.foods.source-food')"
|
:label="$t('data-pages.foods.source-food')"
|
||||||
/>
|
/>
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
v-model="toFood"
|
v-model="toFood"
|
||||||
return-object
|
return-object
|
||||||
:items="foods"
|
:items="foods"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.foods.target-food')"
|
:label="$t('data-pages.foods.target-food')"
|
||||||
/>
|
/>
|
||||||
@@ -51,6 +53,7 @@
|
|||||||
v-model="locale"
|
v-model="locale"
|
||||||
:items="locales"
|
:items="locales"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
:label="$t('data-pages.select-language')"
|
:label="$t('data-pages.select-language')"
|
||||||
class="my-3"
|
class="my-3"
|
||||||
hide-details
|
hide-details
|
||||||
@@ -108,6 +111,7 @@
|
|||||||
v-model="createTarget.labelId"
|
v-model="createTarget.labelId"
|
||||||
clearable
|
clearable
|
||||||
:items="allLabels"
|
:items="allLabels"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.foods.food-label')"
|
:label="$t('data-pages.foods.food-label')"
|
||||||
@@ -164,6 +168,7 @@
|
|||||||
v-model="editTarget.labelId"
|
v-model="editTarget.labelId"
|
||||||
clearable
|
clearable
|
||||||
:items="allLabels"
|
:items="allLabels"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.foods.food-label')"
|
:label="$t('data-pages.foods.food-label')"
|
||||||
@@ -256,6 +261,7 @@
|
|||||||
v-model="bulkAssignLabelId"
|
v-model="bulkAssignLabelId"
|
||||||
clearable
|
clearable
|
||||||
:items="allLabels"
|
:items="allLabels"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.foods.food-label')"
|
:label="$t('data-pages.foods.food-label')"
|
||||||
@@ -346,6 +352,7 @@ import { useUserApi } from "~/composables/api";
|
|||||||
import type { CreateIngredientFood, IngredientFood, IngredientFoodAlias } from "~/lib/api/types/recipe";
|
import type { CreateIngredientFood, IngredientFood, IngredientFoodAlias } from "~/lib/api/types/recipe";
|
||||||
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
import { useFoodStore, useLabelStore } from "~/composables/store";
|
import { useFoodStore, useLabelStore } from "~/composables/store";
|
||||||
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||||
import type { VForm } from "~/types/auto-forms";
|
import type { VForm } from "~/types/auto-forms";
|
||||||
@@ -625,6 +632,7 @@ export default defineNuxtComponent({
|
|||||||
foods,
|
foods,
|
||||||
allLabels,
|
allLabels,
|
||||||
validators,
|
validators,
|
||||||
|
normalizeFilter,
|
||||||
// Create
|
// Create
|
||||||
createDialog,
|
createDialog,
|
||||||
domNewFoodForm,
|
domNewFoodForm,
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="locale"
|
v-model="locale"
|
||||||
:items="locales"
|
:items="locales"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.select-language')"
|
:label="$t('data-pages.select-language')"
|
||||||
class="my-3"
|
class="my-3"
|
||||||
@@ -186,6 +187,7 @@ import { useUserApi } from "~/composables/api";
|
|||||||
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
||||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
import { useLabelData, useLabelStore } from "~/composables/store";
|
import { useLabelData, useLabelStore } from "~/composables/store";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
@@ -316,6 +318,7 @@ export default defineNuxtComponent({
|
|||||||
tableHeaders,
|
tableHeaders,
|
||||||
labels: labelStore.store,
|
labels: labelStore.store,
|
||||||
validators,
|
validators,
|
||||||
|
normalizeFilter,
|
||||||
|
|
||||||
// create
|
// create
|
||||||
createLabel,
|
createLabel,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
v-model="fromUnit"
|
v-model="fromUnit"
|
||||||
return-object
|
return-object
|
||||||
:items="store"
|
:items="store"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.units.source-unit')"
|
:label="$t('data-pages.units.source-unit')"
|
||||||
/>
|
/>
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
v-model="toUnit"
|
v-model="toUnit"
|
||||||
return-object
|
return-object
|
||||||
:items="store"
|
:items="store"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
:label="$t('data-pages.units.target-unit')"
|
:label="$t('data-pages.units.target-unit')"
|
||||||
/>
|
/>
|
||||||
@@ -313,6 +315,7 @@ import { validators } from "~/composables/use-validators";
|
|||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import type { CreateIngredientUnit, IngredientUnit, IngredientUnitAlias } from "~/lib/api/types/recipe";
|
import type { CreateIngredientUnit, IngredientUnit, IngredientUnitAlias } from "~/lib/api/types/recipe";
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
import { useUnitStore } from "~/composables/store";
|
import { useUnitStore } from "~/composables/store";
|
||||||
import type { VForm } from "~/types/auto-forms";
|
import type { VForm } from "~/types/auto-forms";
|
||||||
|
|
||||||
@@ -536,6 +539,7 @@ export default defineNuxtComponent({
|
|||||||
tableHeaders,
|
tableHeaders,
|
||||||
store,
|
store,
|
||||||
validators,
|
validators,
|
||||||
|
normalizeFilter,
|
||||||
// Create
|
// Create
|
||||||
createDialog,
|
createDialog,
|
||||||
domNewUnitForm,
|
domNewUnitForm,
|
||||||
|
|||||||
@@ -22,34 +22,17 @@
|
|||||||
"
|
"
|
||||||
@close="resetDialog()"
|
@close="resetDialog()"
|
||||||
>
|
>
|
||||||
<v-card-text>
|
<v-card-text class="pb-2">
|
||||||
<v-menu
|
<v-date-picker
|
||||||
v-model="state.pickerMenu!"
|
v-model="newMeal.date"
|
||||||
:close-on-content-click="false"
|
class="mx-auto"
|
||||||
transition="scale-transition"
|
hide-header
|
||||||
offset-y
|
show-adjacent-months
|
||||||
max-width="290px"
|
color="primary"
|
||||||
min-width="auto"
|
:first-day-of-week="firstDayOfWeek"
|
||||||
>
|
:local="$i18n.locale"
|
||||||
<template #activator="{ props }">
|
/>
|
||||||
<v-text-field
|
<v-card-text class="pb-0">
|
||||||
:model-value="$d(newMeal.date)"
|
|
||||||
:label="$t('general.date')"
|
|
||||||
persistent-hint
|
|
||||||
:prepend-icon="$globals.icons.calendar"
|
|
||||||
v-bind="props"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<v-date-picker
|
|
||||||
v-model="newMeal.date"
|
|
||||||
hide-header
|
|
||||||
:first-day-of-week="firstDayOfWeek"
|
|
||||||
:local="$i18n.locale"
|
|
||||||
@update:model-value="state.pickerMenu = false"
|
|
||||||
/>
|
|
||||||
</v-menu>
|
|
||||||
<v-card-text>
|
|
||||||
<v-select
|
<v-select
|
||||||
v-model="newMeal.entryType"
|
v-model="newMeal.entryType"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
@@ -64,6 +47,7 @@
|
|||||||
v-model:search="search.query.value"
|
v-model:search="search.query.value"
|
||||||
:label="$t('meal-plan.meal-recipe')"
|
:label="$t('meal-plan.meal-recipe')"
|
||||||
:items="search.data.value"
|
:items="search.data.value"
|
||||||
|
:custom-filter="normalizeFilter"
|
||||||
:loading="search.loading.value"
|
:loading="search.loading.value"
|
||||||
cache-items
|
cache-items
|
||||||
item-title="name"
|
item-title="name"
|
||||||
@@ -76,8 +60,8 @@
|
|||||||
<v-textarea v-model="newMeal.text" rows="2" :label="$t('meal-plan.meal-note')" />
|
<v-textarea v-model="newMeal.text" rows="2" :label="$t('meal-plan.meal-note')" />
|
||||||
</template>
|
</template>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="my-0 py-0">
|
<v-card-actions class="py-0 px-4">
|
||||||
<v-switch v-model="dialog.note" class="mt-n3" :label="$t('meal-plan.note-only')" />
|
<v-switch v-model="dialog.note" class="mt-n3 mb-n4" :label="$t('meal-plan.note-only')" />
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
@@ -195,6 +179,26 @@
|
|||||||
text: $t('meal-plan.lunch'),
|
text: $t('meal-plan.lunch'),
|
||||||
event: 'randomLunch',
|
event: 'randomLunch',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.diceMultiple,
|
||||||
|
text: $t('meal-plan.side'),
|
||||||
|
event: 'randomSide',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.diceMultiple,
|
||||||
|
text: $t('meal-plan.snack'),
|
||||||
|
event: 'randomSnack',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.diceMultiple,
|
||||||
|
text: $t('meal-plan.drink'),
|
||||||
|
event: 'randomDrink',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.diceMultiple,
|
||||||
|
text: $t('meal-plan.dessert'),
|
||||||
|
event: 'randomDessert',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -218,6 +222,9 @@
|
|||||||
@random-lunch="randomMeal(plan.date, 'lunch')"
|
@random-lunch="randomMeal(plan.date, 'lunch')"
|
||||||
@random-dinner="randomMeal(plan.date, 'dinner')"
|
@random-dinner="randomMeal(plan.date, 'dinner')"
|
||||||
@random-side="randomMeal(plan.date, 'side')"
|
@random-side="randomMeal(plan.date, 'side')"
|
||||||
|
@random-snack="randomMeal(plan.date, 'snack')"
|
||||||
|
@random-drink="randomMeal(plan.date, 'drink')"
|
||||||
|
@random-dessert="randomMeal(plan.date, 'dessert')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -236,6 +243,7 @@ import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue";
|
|||||||
import type { PlanEntryType, UpdatePlanEntry } from "~/lib/api/types/meal-plan";
|
import type { PlanEntryType, UpdatePlanEntry } from "~/lib/api/types/meal-plan";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useHouseholdSelf } from "~/composables/use-households";
|
import { useHouseholdSelf } from "~/composables/use-households";
|
||||||
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
|
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
@@ -261,7 +269,6 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
const state = ref({
|
const state = ref({
|
||||||
dialog: false,
|
dialog: false,
|
||||||
pickerMenu: null as null | boolean,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const firstDayOfWeek = computed(() => {
|
||||||
@@ -411,6 +418,7 @@ export default defineNuxtComponent({
|
|||||||
getEntryTypeText,
|
getEntryTypeText,
|
||||||
requiredRule,
|
requiredRule,
|
||||||
isCreateDisabled,
|
isCreateDisabled,
|
||||||
|
normalizeFilter,
|
||||||
|
|
||||||
// Dialog
|
// Dialog
|
||||||
dialog,
|
dialog,
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ const plan = computed<Days[]>(() => {
|
|||||||
{ title: i18n.t("meal-plan.lunch"), meals: [] },
|
{ title: i18n.t("meal-plan.lunch"), meals: [] },
|
||||||
{ title: i18n.t("meal-plan.dinner"), meals: [] },
|
{ title: i18n.t("meal-plan.dinner"), meals: [] },
|
||||||
{ title: i18n.t("meal-plan.side"), meals: [] },
|
{ title: i18n.t("meal-plan.side"), meals: [] },
|
||||||
|
{ title: i18n.t("meal-plan.snack"), meals: [] },
|
||||||
|
{ title: i18n.t("meal-plan.drink"), meals: [] },
|
||||||
|
{ title: i18n.t("meal-plan.dessert"), meals: [] },
|
||||||
],
|
],
|
||||||
recipes: [],
|
recipes: [],
|
||||||
};
|
};
|
||||||
@@ -100,6 +103,15 @@ const plan = computed<Days[]>(() => {
|
|||||||
else if (meal.entryType === "side") {
|
else if (meal.entryType === "side") {
|
||||||
out.sections[3].meals.push(meal);
|
out.sections[3].meals.push(meal);
|
||||||
}
|
}
|
||||||
|
else if (meal.entryType === "snack") {
|
||||||
|
out.sections[4].meals.push(meal);
|
||||||
|
}
|
||||||
|
else if (meal.entryType === "drink") {
|
||||||
|
out.sections[5].meals.push(meal);
|
||||||
|
}
|
||||||
|
else if (meal.entryType === "dessert") {
|
||||||
|
out.sections[6].meals.push(meal);
|
||||||
|
}
|
||||||
|
|
||||||
if (meal.recipe) {
|
if (meal.recipe) {
|
||||||
out.recipes.push(meal.recipe);
|
out.recipes.push(meal.recipe);
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<v-form @submit.prevent="authenticate">
|
<v-form @submit.prevent="authenticate">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="$appInfo.allowPasswordLogin"
|
v-if="$appInfo.allowPasswordLogin"
|
||||||
|
id="username"
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
:prepend-inner-icon="$globals.icons.email"
|
:prepend-inner-icon="$globals.icons.email"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
autofocus
|
autofocus
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
name="login"
|
name="username"
|
||||||
:label="$t('user.email-or-username')"
|
:label="$t('user.email-or-username')"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
@@ -250,6 +251,11 @@ export default defineNuxtComponent({
|
|||||||
const data = await $axios.get<AppStartupInfo>("/api/app/about/startup-info");
|
const data = await $axios.get<AppStartupInfo>("/api/app/about/startup-info");
|
||||||
isDemo.value = data.data.isDemo;
|
isDemo.value = data.data.isDemo;
|
||||||
isFirstLogin.value = data.data.isFirstLogin;
|
isFirstLogin.value = data.data.isFirstLogin;
|
||||||
|
|
||||||
|
if (data.data.isFirstLogin) {
|
||||||
|
form.email = "changeme@example.com";
|
||||||
|
form.password = "MyPassword";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
@@ -370,6 +376,13 @@ export default defineNuxtComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
/* Fix password manager autofill detection - Vuetify uses opacity:0 during animation */
|
||||||
|
:deep(.v-field__input) {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
.max-button {
|
.max-button {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"formatter": "detailed",
|
"formatter": "detailed",
|
||||||
"filename": "${DATA_DIR}/mealie.log",
|
"filename": "${DATA_DIR}/mealie.log",
|
||||||
"maxBytes": 10000,
|
"maxBytes": 10000000,
|
||||||
"backupCount": 3
|
"backupCount": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
|||||||
) # "MONDAY", "TUESDAY", "WEDNESDAY", etc...
|
) # "MONDAY", "TUESDAY", "WEDNESDAY", etc...
|
||||||
entry_type: Mapped[str] = mapped_column(
|
entry_type: Mapped[str] = mapped_column(
|
||||||
String, nullable=False, default=""
|
String, nullable=False, default=""
|
||||||
) # "breakfast", "lunch", "dinner", "side"
|
) # "breakfast", "lunch", "dinner", etc ...
|
||||||
query_filter_string: Mapped[str] = mapped_column(String, nullable=False, default="")
|
query_filter_string: Mapped[str] = mapped_column(String, nullable=False, default="")
|
||||||
|
|
||||||
# Old filters - deprecated in favor of query filter strings
|
# Old filters - deprecated in favor of query filter strings
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"recipe": {
|
"recipe": {
|
||||||
"unique-name-error": "El nom de la recepta ha de ser únic",
|
"unique-name-error": "El nom de la recepta ha de ser únic",
|
||||||
"recipe-created": "Recepta creada",
|
"recipe-created": "Recepta creada",
|
||||||
"recipe-image-deleted": "Recipe image deleted",
|
"recipe-image-deleted": "S'ha suprimit la imatge de la recepta",
|
||||||
"recipe-defaults": {
|
"recipe-defaults": {
|
||||||
"ingredient-note": "1 tassa de farina",
|
"ingredient-note": "1 tassa de farina",
|
||||||
"step-text": "Passos de recepta i altres camps són compatibles amb sintaxi markdown.\n\n**Afegir un enllaç**\n\n[El meu enllaç](https://demo.mealie.io)\n"
|
"step-text": "Passos de recepta i altres camps són compatibles amb sintaxi markdown.\n\n**Afegir un enllaç**\n\n[El meu enllaç](https://demo.mealie.io)\n"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user