Compare commits

..

55 Commits
2.2.0 ... 2.2.1

Author SHA1 Message Date
vabene1111
b72897b222 Merge branch 'develop' 2025-09-18 18:17:58 +02:00
vabene1111
bca1ebbf99 various fixes 2025-09-18 18:10:58 +02:00
vabene1111
f0342d4568 fixed some tests 2025-09-17 18:04:02 +02:00
vabene1111
81f62de500 Merge pull request #4050 from TandoorRecipes/revert-4040-ipv6
Revert "feat: make nginx respect ipv6 disable fixes #3996"
2025-09-17 16:20:13 +02:00
vabene1111
f783949a61 Revert "feat: make nginx respect ipv6 disable fixes #3996" 2025-09-17 16:20:03 +02:00
vabene1111
820fad1b5c Merge pull request #4040 from wilmardo/ipv6
feat: make nginx respect ipv6 disable fixes #3996
2025-09-17 16:19:44 +02:00
vabene1111
1169abd942 mealie docs update 2025-09-17 07:55:29 +02:00
vabene1111
48e175f58f mealie importer working with settings 2025-09-17 07:50:56 +02:00
wilmardo
5450e18342 feat: make nginx respect ipv6 disable fixes #3996
Signed-off-by: wilmardo <info@wilmardenouden.nl>
2025-09-16 15:01:54 +02:00
vabene1111
ea590f8e49 mealie importer options 2025-09-16 08:00:22 +02:00
vabene1111
13626ca11b mealie importer improvements 2025-09-16 07:48:58 +02:00
vabene1111
f53fe1e3c4 import comments 2025-09-15 22:12:52 +02:00
vabene1111
d177316b47 mealie 1.0 importer WIP 2025-09-15 22:05:15 +02:00
vabene1111
338db1fac2 fixed default properties view 2025-09-15 21:26:51 +02:00
vabene1111
377619473c small fixes 2025-09-15 07:49:17 +02:00
vabene1111
000962c5bb moved create space to its own file 2025-09-14 12:05:09 +02:00
vabene1111
9228c1d59f fixed ai import layout 2025-09-14 11:55:58 +02:00
vabene1111
27007de7a0 improved start page with little recipes 2025-09-14 11:12:46 +02:00
vabene1111
29c99b66a1 fixed thermomix special symbol parser 2025-09-14 11:11:22 +02:00
vabene1111
bc179f430d Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-09-14 11:03:53 +02:00
vabene1111
58c412ad95 space and user space api updates 2025-09-14 09:57:57 +02:00
vabene1111
4f248afe76 overhauld space management and settings system 2025-09-14 08:48:49 +02:00
vabene1111
f722d24eaa wip space editor 2025-09-14 07:37:31 +02:00
vabene1111
723b74509f moved space stuff to database and reworked invite link backend logic 2025-09-11 21:44:40 +02:00
vabene1111
ad4b1393dd various improvements 2025-09-11 18:58:44 +02:00
vabene1111
04bab7072c WIP stepper and language select component 2025-09-11 07:55:06 +02:00
vabene1111
6391cee9eb Merge pull request #4025 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vite-7.1.5
Bump vite from 7.1.4 to 7.1.5 in /vue3
2025-09-11 07:07:50 +02:00
vabene1111
14884fc0d4 Merge pull request #3985 from TandoorRecipes/dependabot/github_actions/awalsh128/cache-apt-pkgs-action-1.5.3
Bump awalsh128/cache-apt-pkgs-action from 1.5.1 to 1.5.3
2025-09-11 07:07:44 +02:00
vabene1111
f2191f79dd auto space creation and redirect to welcome page 2025-09-10 22:18:09 +02:00
vabene1111
c2533d9ea2 add migration shortcut 2025-09-10 21:28:10 +02:00
dependabot[bot]
db72fdb1bb Bump vite from 7.1.4 to 7.1.5 in /vue3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.4 to 7.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 19:03:01 +00:00
dependabot[bot]
78252662cb Bump awalsh128/cache-apt-pkgs-action from 1.5.1 to 1.5.3
Bumps [awalsh128/cache-apt-pkgs-action](https://github.com/awalsh128/cache-apt-pkgs-action) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/awalsh128/cache-apt-pkgs-action/releases)
- [Commits](https://github.com/awalsh128/cache-apt-pkgs-action/compare/v1.5.1...v1.5.3)

---
updated-dependencies:
- dependency-name: awalsh128/cache-apt-pkgs-action
  dependency-version: 1.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 18:59:37 +00:00
vabene1111
4e078bf477 updated to django 5 2025-09-10 20:56:47 +02:00
vabene1111
2e9e226fe0 Merge pull request #3994 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vite-plugin-pwa-1.0.3
Bump vite-plugin-pwa from 1.0.2 to 1.0.3 in /vue3
2025-09-10 20:44:54 +02:00
dependabot[bot]
18cfbd80ab Bump vite-plugin-pwa from 1.0.2 to 1.0.3 in /vue3
Bumps [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v1.0.2...v1.0.3)

---
updated-dependencies:
- dependency-name: vite-plugin-pwa
  dependency-version: 1.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 18:42:09 +00:00
vabene1111
4d284b4fff Merge pull request #3984 from TandoorRecipes/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-09-10 20:42:06 +02:00
vabene1111
b1128dd134 Merge pull request #3986 from TandoorRecipes/dependabot/pip/django-storages-1.14.6
Bump django-storages from 1.14.2 to 1.14.6
2025-09-10 20:41:46 +02:00
vabene1111
3aebf58406 Merge pull request #3987 from TandoorRecipes/dependabot/pip/djangorestframework-3.16.1
Bump djangorestframework from 3.15.2 to 3.16.1
2025-09-10 20:41:39 +02:00
vabene1111
f3816a77df Merge pull request #3988 from TandoorRecipes/dependabot/pip/django-prometheus-2.4.1
Bump django-prometheus from 2.3.1 to 2.4.1
2025-09-10 20:41:31 +02:00
vabene1111
e4183d79ab Merge pull request #3989 from TandoorRecipes/dependabot/pip/drf-spectacular-sidecar-2025.8.1
Bump drf-spectacular-sidecar from 2025.7.1 to 2025.8.1
2025-09-10 20:41:25 +02:00
vabene1111
f4aa1a083f Merge pull request #3990 from TandoorRecipes/dependabot/pip/python-dotenv-1.1.1
Bump python-dotenv from 1.0.0 to 1.1.1
2025-09-10 20:41:18 +02:00
vabene1111
ed5508b576 Merge pull request #3991 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vue-i18n-11.1.11
Bump vue-i18n from 11.1.10 to 11.1.11 in /vue3
2025-09-10 20:40:57 +02:00
vabene1111
040e247487 Merge pull request #3992 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vue-tsc-3.0.6
Bump vue-tsc from 2.2.10 to 3.0.6 in /vue3
2025-09-10 20:40:50 +02:00
vabene1111
5d28c7b17d Merge pull request #3995 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vuetify-3.9.6
Bump vuetify from 3.9.3 to 3.9.6 in /vue3
2025-09-10 20:40:39 +02:00
vabene1111
15b2df07f2 Merge pull request #3993 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vite-7.1.3
Bump vite from 6.3.5 to 7.1.3 in /vue3
2025-09-10 20:40:26 +02:00
dependabot[bot]
7be7c5b954 Bump vite from 6.3.5 to 7.1.3 in /vue3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 7.1.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 06:53:43 +00:00
dependabot[bot]
0853a9ec64 Bump vuetify from 3.9.3 to 3.9.6 in /vue3
Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.9.3 to 3.9.6.
- [Release notes](https://github.com/vuetifyjs/vuetify/releases)
- [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.9.6/packages/vuetify)

---
updated-dependencies:
- dependency-name: vuetify
  dependency-version: 3.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 06:51:27 +00:00
dependabot[bot]
fa3daee965 Bump vue-tsc from 2.2.10 to 3.0.6 in /vue3
Bumps [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) from 2.2.10 to 3.0.6.
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.0.6/packages/tsc)

---
updated-dependencies:
- dependency-name: vue-tsc
  dependency-version: 3.0.6
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 06:42:43 +00:00
dependabot[bot]
774c05e76f Bump vue-i18n from 11.1.10 to 11.1.11 in /vue3
Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 11.1.10 to 11.1.11.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.1.11/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-version: 11.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 01:07:24 +00:00
dependabot[bot]
b08c39e284 Bump python-dotenv from 1.0.0 to 1.1.1
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.0.0 to 1.1.1.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.1.1)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:39 +00:00
dependabot[bot]
ae036cfa9a Bump drf-spectacular-sidecar from 2025.7.1 to 2025.8.1
Bumps [drf-spectacular-sidecar](https://github.com/tfranzel/drf-spectacular-sidecar) from 2025.7.1 to 2025.8.1.
- [Commits](https://github.com/tfranzel/drf-spectacular-sidecar/compare/2025.7.1...2025.8.1)

---
updated-dependencies:
- dependency-name: drf-spectacular-sidecar
  dependency-version: 2025.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:35 +00:00
dependabot[bot]
37628c1735 Bump django-prometheus from 2.3.1 to 2.4.1
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.3.1 to 2.4.1.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/django-commons/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/compare/v2.3.1...v2.4.1)

---
updated-dependencies:
- dependency-name: django-prometheus
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:31 +00:00
dependabot[bot]
530a6db35c Bump djangorestframework from 3.15.2 to 3.16.1
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.15.2 to 3.16.1.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.15.2...3.16.1)

---
updated-dependencies:
- dependency-name: djangorestframework
  dependency-version: 3.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:28 +00:00
dependabot[bot]
2930093da0 Bump django-storages from 1.14.2 to 1.14.6
Bumps [django-storages](https://github.com/jschneier/django-storages) from 1.14.2 to 1.14.6.
- [Changelog](https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jschneier/django-storages/compare/1.14.2...1.14.6)

---
updated-dependencies:
- dependency-name: django-storages
  dependency-version: 1.14.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:05:24 +00:00
dependabot[bot]
b7e63a466b Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:02:38 +00:00
94 changed files with 3559 additions and 758 deletions

View File

@@ -21,7 +21,7 @@ jobs:
suffix: ""
continue-on-error: false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get version number
id: get_version

View File

@@ -12,8 +12,8 @@ jobs:
python-version: ["3.12"]
node-version: ["22"]
steps:
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@v1.5.1
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: libsasl2-dev python3-dev libxml2-dev libxmlsec1-dev libxslt-dev libxmlsec1-openssl libxslt-dev libldap2-dev libssl-dev gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl
version: 1.0

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -12,7 +12,7 @@ jobs:
if: github.repository_owner == 'TandoorRecipes' && ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: 3.x

View File

@@ -26,6 +26,7 @@ class ImportExportBase(forms.Form):
PAPRIKA = 'PAPRIKA'
NEXTCLOUD = 'NEXTCLOUD'
MEALIE = 'MEALIE'
MEALIE1 = 'MEALIE1'
CHOWDOWN = 'CHOWDOWN'
SAFFRON = 'SAFFRON'
CHEFTAP = 'CHEFTAP'
@@ -46,7 +47,7 @@ class ImportExportBase(forms.Form):
PDF = 'PDF'
GOURMET = 'GOURMET'
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'),
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (MEALIE1, 'Mealie1'), (CHOWDOWN, 'Chowdown'),
(SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'),
(DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
@@ -75,6 +76,11 @@ class ImportForm(ImportExportBase):
files = MultipleFileField(required=True)
duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'),
required=False)
meal_plans = forms.BooleanField(required=False)
shopping_lists = forms.BooleanField(required=False)
nutrition_per_serving = forms.BooleanField(required=False) # some managers (e.g. mealie) do not specify what the nutrition's relate to so we let the user choose
class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
all = forms.BooleanField(required=False)

View File

@@ -5,6 +5,7 @@ from django.db.models import Sum
from litellm import CustomLogger
from cookbook.models import AiLog
from recipes import settings
def get_monthly_token_usage(space):
@@ -61,6 +62,8 @@ class AiCallbackHandler(CustomLogger):
remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost))
if remaining_balance < 0:
remaining_balance = 0
if settings.HOSTED:
self.space.ai_enabled = False
self.space.ai_credits_balance = remaining_balance
credits_from_balance = True

View File

@@ -3,17 +3,19 @@ import inspect
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Group
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
from oauth2_provider.models import AccessToken
from rest_framework import permissions
from rest_framework.permissions import SAFE_METHODS
from cookbook.models import Recipe, ShareLink, UserSpace
import random
from cookbook.models import Recipe, ShareLink, UserSpace, Space
def get_allowed_groups(groups_required):
@@ -330,6 +332,7 @@ class CustomRecipePermission(permissions.BasePermission):
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS)
or has_group_permission(request.user, ['user'])) and obj.space == request.space
class CustomAiProviderPermission(permissions.BasePermission):
"""
Custom permission class for the AiProvider api endpoint
@@ -455,3 +458,36 @@ class IsReadOnlyDRF(permissions.BasePermission):
def has_permission(self, request, view):
return request.method in SAFE_METHODS
class IsCreateDRF(permissions.BasePermission):
message = 'You cannot interact with this object, you can only create'
def has_permission(self, request, view):
return request.method == 'POST'
def create_space_for_user(user, name=None):
with scopes_disabled():
if not name:
name = f"{user.username}'s Space"
if Space.objects.filter(name=name).exists():
name = f'{name} #{random.randrange(1, 10 ** 5)}'
created_space = Space(name=name,
created_by=user,
max_file_storage_mb=settings.SPACE_DEFAULT_MAX_FILES,
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
ai_enabled=settings.SPACE_AI_ENABLED,
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,
space_setup_completed=False, )
created_space.save()
UserSpace.objects.filter(user=user).update(active=False)
user_space = UserSpace.objects.create(space=created_space, user=user, active=True)
user_space.groups.add(Group.objects.filter(name='admin').get())
return user_space

View File

@@ -319,10 +319,10 @@ def clean_instruction_string(instruction):
.replace("", _('reverse rotation')) \
.replace("", _('careful rotation')) \
.replace("", _('knead')) \
.replace("Andicken ", _('thicken')) \
.replace("Erwärmen ", _('warm up')) \
.replace("Fermentieren ", _('ferment')) \
.replace("Sous-vide ", _("sous-vide"))
.replace("", _('thicken')) \
.replace("", _('warm up')) \
.replace("", _('ferment')) \
.replace("", _("sous-vide"))
def parse_instructions(instructions):
@@ -403,6 +403,8 @@ def parse_servings_text(servings):
def parse_time(recipe_time):
if not recipe_time:
return 0
if type(recipe_time) not in [int, float]:
try:
recipe_time = float(re.search(r'\d+', recipe_time).group())

View File

@@ -1,8 +1,15 @@
from django.contrib.auth.models import Group
from django.http import HttpResponseRedirect
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from psycopg2.errors import UniqueViolation
from rest_framework.exceptions import AuthenticationFailed
import random
from cookbook.helper.permission_helper import create_space_for_user
from cookbook.models import Space, UserSpace
from cookbook.views import views
from recipes import settings
@@ -34,16 +41,28 @@ class ScopeMiddleware:
if request.path.startswith(prefix + '/switch-space/'):
return self.get_response(request)
with scopes_disabled():
if request.user.userspace_set.count() == 0 and not reverse('account_logout') in request.path:
return views.space_overview(request)
if request.path.startswith(prefix + '/invite/'):
return self.get_response(request)
# get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point)
user_space = request.user.userspace_set.filter(active=True).first()
if not user_space:
return views.space_overview(request)
if not user_space and request.user.userspace_set.count() > 0:
# if the users has a userspace but nothing is active, activate the first one
user_space = request.user.userspace_set.first()
if user_space:
user_space.active = True
user_space.save()
if not user_space:
if 'signup_token' in request.session:
# if user is authenticated, has no space but a signup token (InviteLink) is present, redirect to invite link logic
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
else:
# if user does not yet have a space create one for him
user_space = create_space_for_user(request.user)
# TODO remove the need for this view
if user_space.groups.count() == 0 and not reverse('account_logout') in request.path:
return views.no_groups(request)

View File

@@ -26,6 +26,12 @@ class Integration:
files = None
export_type = None
ignored_recipes = []
import_log = None
import_duplicates = False
import_meal_plans = True
import_shopping_lists = True
nutrition_per_serving = False
def __init__(self, request, export_type):
"""
@@ -102,7 +108,7 @@ class Integration:
"""
return True
def do_import(self, files, il, import_duplicates):
def do_import(self, files, il, import_duplicates, meal_plans=True, shopping_lists=True, nutrition_per_serving=False):
"""
Imports given files
:param import_duplicates: if true duplicates are imported as well
@@ -111,6 +117,12 @@ class Integration:
:return: HttpResponseRedirect to the recipe search showing all imported recipes
"""
with scope(space=self.request.space):
self.import_log = il
self.import_duplicates = import_duplicates
self.import_meal_plans = meal_plans
self.import_shopping_lists = shopping_lists
self.nutrition_per_serving = nutrition_per_serving
try:
self.files = files
@@ -166,20 +178,24 @@ class Integration:
il.total_recipes = len(new_file_list)
file_list = new_file_list
for z in file_list:
try:
if not hasattr(z, 'filename') or isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += self.get_recipe_processed_msg(recipe)
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
except Exception as e:
traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
if isinstance(self, cookbook.integration.mealie1.Mealie1):
# since the mealie 1.0 export is a backup and not a classic recipe export we treat it a bit differently
recipes = self.get_recipe_from_file(import_zip)
else:
for z in file_list:
try:
if not hasattr(z, 'filename') or isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += self.get_recipe_processed_msg(recipe)
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
except Exception as e:
traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
import_zip.close()
elif '.json' in f['name'] or '.xml' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name'] or '.melarecipe' in f['name']:
data_list = self.split_recipe_file(f['file'])

View File

@@ -0,0 +1,342 @@
import json
import re
import traceback
import uuid
from decimal import Decimal
from io import BytesIO
from zipfile import ZipFile
from gettext import gettext as _
from django.db import transaction
from cookbook.helper import ingredient_parser
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step, Food, Unit, SupermarketCategory, PropertyType, Property, MealType, MealPlan, CookLog, ShoppingListEntry
class Mealie1(Integration):
"""
integration for mealie past version 1.0
"""
def get_recipe_from_file(self, file):
mealie_database = json.loads(BytesIO(file.read('database.json')).getvalue().decode("utf-8"))
self.import_log.total_recipes = len(mealie_database['recipes'])
self.import_log.msg += f"Importing {len(mealie_database["categories"]) + len(mealie_database["tags"])} tags and categories as keywords...\n"
self.import_log.save()
keywords_categories_dict = {}
for c in mealie_database['categories']:
if keyword := Keyword.objects.filter(name=c['name'], space=self.request.space).first():
keywords_categories_dict[c['id']] = keyword.pk
else:
keyword = Keyword.objects.create(name=c['name'], space=self.request.space)
keywords_categories_dict[c['id']] = keyword.pk
keywords_tags_dict = {}
for t in mealie_database['tags']:
if keyword := Keyword.objects.filter(name=t['name'], space=self.request.space).first():
keywords_tags_dict[t['id']] = keyword.pk
else:
keyword = Keyword.objects.create(name=t['name'], space=self.request.space)
keywords_tags_dict[t['id']] = keyword.pk
self.import_log.msg += f"Importing {len(mealie_database["multi_purpose_labels"])} multi purpose labels as supermarket categories...\n"
self.import_log.save()
supermarket_categories_dict = {}
for m in mealie_database['multi_purpose_labels']:
if supermarket_category := SupermarketCategory.objects.filter(name=m['name'], space=self.request.space).first():
supermarket_categories_dict[m['id']] = supermarket_category.pk
else:
supermarket_category = SupermarketCategory.objects.create(name=m['name'], space=self.request.space)
supermarket_categories_dict[m['id']] = supermarket_category.pk
self.import_log.msg += f"Importing {len(mealie_database["ingredient_foods"])} foods...\n"
self.import_log.save()
foods_dict = {}
for f in mealie_database['ingredient_foods']:
if food := Food.objects.filter(name=f['name'], space=self.request.space).first():
foods_dict[f['id']] = food.pk
else:
food = {'name': f['name'],
'plural_name': f['plural_name'],
'description': f['description'],
'space': self.request.space}
if f['label_id'] and f['label_id'] in supermarket_categories_dict:
food['supermarket_category_id'] = supermarket_categories_dict[f['label_id']]
food = Food.objects.create(**food)
if f['on_hand']:
food.onhand_users.add(self.request.user)
foods_dict[f['id']] = food.pk
self.import_log.msg += f"Importing {len(mealie_database["ingredient_units"])} units...\n"
self.import_log.save()
units_dict = {}
for u in mealie_database['ingredient_units']:
if unit := Unit.objects.filter(name=u['name'], space=self.request.space).first():
units_dict[u['id']] = unit.pk
else:
unit = Unit.objects.create(name=u['name'], plural_name=u['plural_name'], description=u['description'], space=self.request.space)
units_dict[u['id']] = unit.pk
recipes_dict = {}
recipe_property_factor_dict = {}
recipes = []
recipe_keyword_relation = []
for r in mealie_database['recipes']:
if Recipe.objects.filter(space=self.request.space, name=r['name']).exists() and not self.import_duplicates:
self.import_log.msg += f"Ignoring {r['name']} because a recipe with this name already exists.\n"
self.import_log.save()
else:
recipe = Recipe.objects.create(
waiting_time=parse_time(r['perform_time']),
working_time=parse_time(r['prep_time']),
description=r['description'][:512],
name=r['name'],
source_url=r['org_url'],
servings=r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1,
servings_text=r['recipe_yield'].strip() if r['recipe_yield'] else "",
internal=True,
created_at=r['created_at'],
space=self.request.space,
created_by=self.request.user,
)
if not self.nutrition_per_serving:
recipe_property_factor_dict[r['id']] = recipe.servings
self.import_log.msg += self.get_recipe_processed_msg(recipe)
self.import_log.imported_recipes += 1
self.import_log.save()
recipes.append(recipe)
recipes_dict[r['id']] = recipe.pk
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipe.pk, keyword_id=self.keyword.pk))
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
self.import_log.msg += f"Importing {len(mealie_database["recipe_instructions"])} instructions...\n"
self.import_log.save()
steps_relation = []
first_step_of_recipe_dict = {}
for s in mealie_database['recipe_instructions']:
if s['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if s['summary'] else ""),
order=s['position'],
name=s['title'],
space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[s['recipe_id']], step_id=step.pk))
if s['recipe_id'] not in first_step_of_recipe_dict:
first_step_of_recipe_dict[s['recipe_id']] = step.pk
for n in mealie_database['notes']:
if n['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=n['text'],
name=n['title'],
order=100,
space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[n['recipe_id']], step_id=step.pk))
Recipe.steps.through.objects.bulk_create(steps_relation)
ingredient_parser = IngredientParser(self.request, True)
self.import_log.msg += f"Importing {len(mealie_database["recipes_ingredients"])} ingredients...\n"
self.import_log.save()
ingredients_relation = []
for i in mealie_database['recipes_ingredients']:
if i['recipe_id'] in recipes_dict:
if i['title']:
title_ingredient = Ingredient.objects.create(
note=i['title'],
is_header=True,
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=title_ingredient.pk))
if i['food_id']:
ingredient = Ingredient.objects.create(
food_id=foods_dict[i['food_id']] if i['food_id'] in foods_dict else None,
unit_id=units_dict[i['unit_id']] if i['unit_id'] in units_dict else None,
original_text=i['original_text'],
order=i['position'],
amount=i['quantity'],
note=i['note'],
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
elif i['note'].strip():
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
ingredient = Ingredient.objects.create(
food=f,
unit=u,
amount=amount,
note=note,
original_text=i['original_text'],
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
Step.ingredients.through.objects.bulk_create(ingredients_relation)
self.import_log.msg += f"Importing {len(mealie_database["recipes_to_categories"]) + len(mealie_database["recipes_to_tags"])} category and keyword relations...\n"
self.import_log.save()
recipe_keyword_relation = []
for rC in mealie_database['recipes_to_categories']:
if rC['recipe_id'] in recipes_dict:
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rC['recipe_id']], keyword_id=keywords_categories_dict[rC['category_id']]))
for rT in mealie_database['recipes_to_tags']:
if rT['recipe_id'] in recipes_dict:
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rT['recipe_id']], keyword_id=keywords_tags_dict[rT['tag_id']]))
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
self.import_log.msg += f"Importing {len(mealie_database["recipe_nutrition"])} properties...\n"
self.import_log.save()
property_types_dict = {
'calories': PropertyType.objects.get_or_create(name=_('Calories'), space=self.request.space, defaults={'unit': 'kcal', 'fdc_id': 1008})[0],
'carbohydrate_content': PropertyType.objects.get_or_create(name=_('Carbohydrates'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1005})[0],
'cholesterol_content': PropertyType.objects.get_or_create(name=_('Cholesterol'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1253})[0],
'fat_content': PropertyType.objects.get_or_create(name=_('Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1004})[0],
'fiber_content': PropertyType.objects.get_or_create(name=_('Fiber'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1079})[0],
'protein_content': PropertyType.objects.get_or_create(name=_('Protein'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1003})[0],
'saturated_fat_content': PropertyType.objects.get_or_create(name=_('Saturated Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1258})[0],
'sodium_content': PropertyType.objects.get_or_create(name=_('Sodium'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1093})[0],
'sugar_content': PropertyType.objects.get_or_create(name=_('Sugar'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1063})[0],
'trans_fat_content': PropertyType.objects.get_or_create(name=_('Trans Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1257})[0],
'unsaturated_fat_content': PropertyType.objects.get_or_create(name=_('Unsaturated Fat'), space=self.request.space, defaults={'unit': 'g'})[0],
}
with transaction.atomic():
recipe_properties_relation = []
properties_relation = []
for r in mealie_database['recipe_nutrition']:
if r['recipe_id'] in recipes_dict:
for key in property_types_dict:
if r[key]:
properties_relation.append(
Property(property_type_id=property_types_dict[key].pk,
property_amount=Decimal(str(r[key])) / (
Decimal(str(recipe_property_factor_dict[r['recipe_id']])) if r['recipe_id'] in recipe_property_factor_dict else 1),
open_data_food_slug=r['recipe_id'],
space=self.request.space))
properties = Property.objects.bulk_create(properties_relation)
property_ids = []
for p in properties:
recipe_properties_relation.append(Recipe.properties.through(recipe_id=recipes_dict[p.open_data_food_slug], property_id=p.pk))
property_ids.append(p.pk)
Recipe.properties.through.objects.bulk_create(recipe_properties_relation, ignore_conflicts=True)
Property.objects.filter(id__in=property_ids).update(open_data_food_slug=None)
# delete unused property types
for pT in property_types_dict:
try:
property_types_dict[pT].delete()
except:
pass
self.import_log.msg += f"Importing {len(mealie_database["recipe_comments"]) + len(mealie_database["recipe_timeline_events"])} comments and cook logs...\n"
self.import_log.save()
cook_log_list = []
for c in mealie_database['recipe_comments']:
if c['recipe_id'] in recipes_dict:
cook_log_list.append(CookLog(
recipe_id=recipes_dict[c['recipe_id']],
comment=c['text'],
created_at=c['created_at'],
created_by=self.request.user,
space=self.request.space,
))
for c in mealie_database['recipe_timeline_events']:
if c['recipe_id'] in recipes_dict:
if c['event_type'] == 'comment':
cook_log_list.append(CookLog(
recipe_id=recipes_dict[c['recipe_id']],
comment=c['message'],
created_at=c['created_at'],
created_by=self.request.user,
space=self.request.space,
))
CookLog.objects.bulk_create(cook_log_list)
if self.import_meal_plans:
self.import_log.msg += f"Importing {len(mealie_database["group_meal_plans"])} meal plans...\n"
self.import_log.save()
meal_types_dict = {}
meal_plans = []
for m in mealie_database['group_meal_plans']:
if m['recipe_id'] in recipes_dict:
if not m['entry_type'] in meal_types_dict:
meal_type = MealType.objects.get_or_create(name=m['entry_type'], created_by=self.request.user, space=self.request.space)[0]
meal_types_dict[m['entry_type']] = meal_type.pk
meal_plans.append(MealPlan(
recipe_id=recipes_dict[m['recipe_id']] if m['recipe_id'] else None,
title=m['title'] if m['title'] else "",
note=m['text'] if m['text'] else "",
from_date=m['date'],
to_date=m['date'],
meal_type_id=meal_types_dict[m['entry_type']],
created_by=self.request.user,
space=self.request.space,
))
MealPlan.objects.bulk_create(meal_plans)
if self.import_shopping_lists:
self.import_log.msg += f"Importing {len(mealie_database["shopping_list_items"])} shopping list items...\n"
self.import_log.save()
shopping_list_items = []
for sli in mealie_database['shopping_list_items']:
if not sli['checked']:
if sli['food_id']:
shopping_list_items.append(ShoppingListEntry(
amount=sli['quantity'],
unit_id=units_dict[sli['unit_id']] if sli['unit_id'] else None,
food_id=foods_dict[sli['food_id']] if sli['food_id'] else None,
created_by=self.request.user,
space=self.request.space,
))
elif not sli['food_id'] and sli['note'].strip():
amount, unit, food, note = ingredient_parser.parse(sli['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
shopping_list_items.append(ShoppingListEntry(
amount=amount,
unit=u,
food=f,
created_by=self.request.user,
space=self.request.space,
))
ShoppingListEntry.objects.bulk_create(shopping_list_items)
self.import_log.msg += f"Importing Images. This might take some time ...\n"
self.import_log.save()
for r in mealie_database['recipes']:
try:
if recipe := Recipe.objects.filter(pk=recipes_dict[r['id']]).first():
self.import_recipe_image(recipe, BytesIO(file.read(f'data/recipes/{str(uuid.UUID(str(r['id'])))}/images/original.webp')), filetype='.webp')
except Exception:
pass
return recipes
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2025-09-10 20:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0001_squashed_0227_space_ai_default_provider_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='space_setup_completed',
field=models.BooleanField(default=True),
),
]

View File

@@ -329,6 +329,8 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
space_setup_completed = models.BooleanField(default=True)
ai_enabled = models.BooleanField(default=True)
ai_credits_monthly = models.IntegerField(default=100)
ai_credits_balance = models.DecimalField(default=0, max_digits=16, decimal_places=4)

View File

@@ -26,7 +26,7 @@ from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.ai_helper import get_monthly_token_usage
from cookbook.helper.image_processing import is_file_type_allowed
from cookbook.helper.permission_helper import above_space_limit
from cookbook.helper.permission_helper import above_space_limit, create_space_for_user
from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
@@ -367,12 +367,12 @@ class AiLogSerializer(serializers.ModelSerializer):
class SpaceSerializer(WritableNestedModelSerializer):
created_by = UserSerializer(read_only=True)
user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used')
user_count = serializers.SerializerMethodField('get_user_count', read_only=True)
recipe_count = serializers.SerializerMethodField('get_recipe_count', read_only=True)
file_size_mb = serializers.SerializerMethodField('get_file_size_mb', read_only=True)
ai_monthly_credits_used = serializers.SerializerMethodField('get_ai_monthly_credits_used', read_only=True)
ai_default_provider = AiProviderSerializer(required=False, allow_null=True)
food_inherit = FoodInheritFieldSerializer(many=True)
food_inherit = FoodInheritFieldSerializer(many=True, required=False)
image = UserFileViewSerializer(required=False, many=False, allow_null=True)
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
custom_space_theme = UserFileViewSerializer(required=False, many=False, allow_null=True)
@@ -404,9 +404,26 @@ class SpaceSerializer(WritableNestedModelSerializer):
return 0
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
if Space.objects.filter(created_by=self.context['request'].user).count() >= self.context['request'].user.userpreference.max_owned_spaces:
raise serializers.ValidationError(
_('You have the reached the maximum amount of spaces that can be owned by you.') + f' ({self.context['request'].user.userpreference.max_owned_spaces})')
name = None
if 'name' in validated_data:
name = validated_data['name']
user_space = create_space_for_user(self.context['request'].user, name)
return user_space.space
def update(self, instance, validated_data):
validated_data = self.filter_superuser_parameters(validated_data)
if 'name' in validated_data:
if Space.objects.filter(Q(name=validated_data['name']), ~Q(pk=instance.pk)).exists():
raise ValidationError(_('Space Name must be unique.'))
return super().update(instance, validated_data)
def filter_superuser_parameters(self, validated_data):
if 'ai_enabled' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_enabled']
@@ -416,7 +433,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
if 'ai_credits_balance' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_credits_balance']
return super().update(instance, validated_data)
return validated_data
class Meta:
model = Space
@@ -425,7 +442,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color',
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg', 'ai_credits_monthly',
'ai_credits_balance', 'ai_monthly_credits_used', 'ai_enabled', 'ai_default_provider')
'ai_credits_balance', 'ai_monthly_credits_used', 'ai_enabled', 'ai_default_provider', 'space_setup_completed')
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
'demo', 'ai_monthly_credits_used')

View File

@@ -41,6 +41,12 @@ def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
obj_1.space = None
obj_1.save()
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],

View File

@@ -7,6 +7,7 @@ from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import UserSpace
from recipes import settings
LIST_URL = 'api:space-list'
DETAIL_URL = 'api:space-detail'
@@ -45,7 +46,6 @@ def test_list_multiple(u1_s1, space_1, space_2):
assert u1_response['id'] == space_1.id
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
@@ -70,9 +70,9 @@ def test_update(arg, request, space_1, a1_s1):
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 405],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
@@ -90,3 +90,59 @@ def test_delete(u1_s1, u1_s2, a1_s1, space_1):
# event the space owner cannot delete his space over the api (this might change later but for now it's only available in the UI)
r = a1_s1.delete(reverse(DETAIL_URL, args={space_1.id}))
assert r.status_code == 405
def test_superuser_parameters(space_1, a1_s1, s1_s1):
# ------- test as normal user -------
response = a1_s1.post(reverse(LIST_URL), {'name': 'test', 'ai_enabled': not settings.SPACE_AI_ENABLED, 'ai_credits_monthly': settings.SPACE_AI_CREDITS_MONTHLY + 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 201
response = json.loads(response.content)
assert response['ai_enabled'] == settings.SPACE_AI_ENABLED
assert response['ai_credits_monthly'] == settings.SPACE_AI_CREDITS_MONTHLY
assert response['ai_credits_balance'] == 0
space_1.created_by = auth.get_user(a1_s1)
space_1.ai_enabled = False
space_1.ai_credits_monthly = 0
space_1.ai_credits_balance = 0
space_1.save()
response = a1_s1.patch(reverse(DETAIL_URL, args={space_1.id}), {'ai_enabled': True, 'ai_credits_monthly': 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 200
space_1.refresh_from_db()
assert space_1.ai_enabled == False
assert space_1.ai_credits_monthly == 0
assert space_1.ai_credits_balance == 0
# ------- test as superuser -------
response = s1_s1.post(reverse(LIST_URL),
{'name': 'test', 'ai_enabled': not settings.SPACE_AI_ENABLED, 'ai_credits_monthly': settings.SPACE_AI_CREDITS_MONTHLY + 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 201
response = json.loads(response.content)
assert response['ai_enabled'] == settings.SPACE_AI_ENABLED
assert response['ai_credits_monthly'] == settings.SPACE_AI_CREDITS_MONTHLY
assert response['ai_credits_balance'] == 0
space_1.created_by = auth.get_user(s1_s1)
space_1.ai_enabled = False
space_1.ai_credits_monthly = 0
space_1.ai_credits_balance = 0
space_1.save()
response = s1_s1.patch(reverse(DETAIL_URL, args={space_1.id}), {'ai_enabled': True, 'ai_credits_monthly': 100, 'ai_credits_balance': 100},
content_type='application/json')
assert response.status_code == 200
space_1.refresh_from_db()
assert space_1.ai_enabled == True
assert space_1.ai_credits_monthly == 100
assert space_1.ai_credits_balance == 100

View File

@@ -5,6 +5,8 @@ from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import UserSpace
LIST_URL = 'api:userspace-list'
DETAIL_URL = 'api:userspace-detail'
@@ -13,10 +15,10 @@ DETAIL_URL = 'api:userspace-detail'
['a_u', 403, 0],
['g1_s1', 200, 1], # sees only own user space
['u1_s1', 200, 1],
['a1_s1', 200, 3], # sees user space of all users in space
['a2_s1', 200, 1],
['a1_s1', 200, 4], # admins can see all other members
['a2_s1', 200, 4],
])
def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1, a2_s1):
space_1.created_by = auth.get_user(a1_s1)
space_1.save()
@@ -27,6 +29,18 @@ def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
assert len(json.loads(result.content)['results']) == arg[2]
def test_list_all_personal(space_2, u1_s1):
result = u1_s1.get(reverse('api:userspace-all-personal'))
assert result.status_code == 200
assert len(json.loads(result.content)) == 1
UserSpace.objects.create(user=auth.get_user(u1_s1), space=space_2)
result = u1_s1.get(reverse('api:userspace-all-personal'))
assert result.status_code == 200
assert len(json.loads(result.content)) == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],

View File

@@ -78,10 +78,11 @@ urlpatterns = [
path('setup/', views.setup, name='view_setup'),
path('no-group/', views.no_groups, name='view_no_group'),
path('space-overview/', views.space_overview, name='view_space_overview'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('no-perm/', views.no_perm, name='view_no_perm'),
#path('space-overview/', views.space_overview, name='view_space_overview'),
#path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
#path('no-perm/', views.no_perm, name='view_no_perm'),
path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('invite/<slug:token>/', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'),
path('plugin/update/', views.plugin_update, name='view_plugin_update'),

View File

@@ -18,8 +18,6 @@ import litellm
import redis
import requests
from PIL import UnidentifiedImageError
from PIL.ImImagePlugin import number
from PIL.features import check
from django.contrib import messages
from django.contrib.auth.models import Group, User
from django.contrib.postgres.search import TrigramSimilarity
@@ -35,7 +33,6 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from django.utils.datetime_safe import date
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from drf_spectacular.types import OpenApiTypes
@@ -76,7 +73,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, Cus
CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF,
above_space_limit,
group_required, has_group_permission, is_space_owner,
switch_user_active_space, CustomAiProviderPermission
switch_user_active_space, CustomAiProviderPermission, IsCreateDRF
)
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
@@ -134,7 +131,7 @@ class LoggingMixin(object):
if settings.REDIS_HOST:
try:
d = date.today().isoformat()
d = timezone.now().isoformat()
space = request.space
endpoint = request.resolver_match.url_name
@@ -182,7 +179,10 @@ class StandardFilterModelViewSet(viewsets.ModelViewSet):
queryset = self.queryset
query = self.request.query_params.get('query', None)
if query is not None:
queryset = queryset.filter(name__icontains=query)
try:
queryset = queryset.filter(name__icontains=query)
except FieldError:
pass
updated_at = self.request.query_params.get('updated_at', None)
if updated_at is not None:
@@ -544,9 +544,9 @@ class GroupViewSet(LoggingMixin, viewsets.ModelViewSet):
class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Space.objects
serializer_class = SpaceSerializer
permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
permission_classes = [((IsReadOnlyDRF | IsCreateDRF) & CustomIsGuest) | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
http_method_names = ['get', 'patch']
http_method_names = ['get', 'post', 'put', 'patch']
def get_queryset(self):
return self.queryset.filter(
@@ -565,7 +565,7 @@ class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UserSpace.objects
serializer_class = UserSpaceSerializer
permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
permission_classes = [(CustomIsSpaceOwner | (IsReadOnlyDRF & CustomIsUser) | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
http_method_names = ['get', 'put', 'patch', 'delete']
pagination_class = DefaultPagination
@@ -579,10 +579,23 @@ class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
if internal_note is not None:
self.queryset = self.queryset.filter(internal_note=internal_note)
if is_space_owner(self.request.user, self.request.space):
# >= admins can see all users, guest/user can only see themselves
if has_group_permission(self.request.user, ['admin']):
return self.queryset.filter(space=self.request.space)
else:
return self.queryset.filter(user=self.request.user, space=self.request.space)
return self.queryset.filter(space=self.request.space, user=self.request.user)
@extend_schema(responses=UserSpaceSerializer(many=True))
@decorators.action(detail=False, pagination_class=DefaultPagination, methods=['GET'], serializer_class=UserSpaceSerializer, )
def all_personal(self, request):
"""
return all userspaces for the user requesting the endpoint
:param request:
:return:
"""
with scopes_disabled():
self.queryset = self.queryset.filter(user=self.request.user)
return Response(self.serializer_class(self.queryset.all(), many=True, context={'request': self.request}).data)
class UserPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
@@ -1222,7 +1235,19 @@ class IngredientViewSet(LoggingMixin, viewsets.ModelViewSet):
return self.serializer_class
def get_queryset(self):
queryset = self.queryset.filter(step__recipe__space=self.request.space)
queryset = self.queryset.prefetch_related('food',
'food__properties',
'food__properties__property_type',
'food__inherit_fields',
'food__supermarket_category',
'food__onhand_users',
'food__substitute',
'food__child_inherit_fields',
'unit',
'unit__unit_conversion_base_relation',
'unit__unit_conversion_base_relation__base_unit',
'unit__unit_conversion_converted_relation',
'unit__unit_conversion_converted_relation__converted_unit', ).filter(step__recipe__space=self.request.space)
food = self.request.query_params.get('food', None)
if food and re.match(r'^(\d)+$', food):
queryset = queryset.filter(food_id=food)
@@ -1893,8 +1918,8 @@ class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
if internal_note is not None:
self.queryset = self.queryset.filter(internal_note=internal_note)
unused = self.request.query_params.get('unused', False)
if unused:
used = self.request.query_params.get('used', False)
if not used:
self.queryset = self.queryset.filter(used_by=None)
if is_space_owner(self.request.user, self.request.space):
@@ -2267,7 +2292,13 @@ class AppImportView(APIView):
files = []
for f in request.FILES.getlist('files'):
files.append({'file': io.BytesIO(f.read()), 'name': f.name})
t = threading.Thread(target=integration.do_import, args=[files, il, form.cleaned_data['duplicates']])
t = threading.Thread(target=integration.do_import,
args=[files, il, form.cleaned_data['duplicates']],
kwargs={'meal_plans': form.cleaned_data['meal_plans'],
'shopping_lists': form.cleaned_data['shopping_lists'],
'nutrition_per_serving': form.cleaned_data['nutrition_per_serving']
}
)
t.setDaemon(True)
t.start()

View File

@@ -17,6 +17,7 @@ from cookbook.integration.copymethat import CopyMeThat
from cookbook.integration.default import Default
from cookbook.integration.domestica import Domestica
from cookbook.integration.mealie import Mealie
from cookbook.integration.mealie1 import Mealie1
from cookbook.integration.mealmaster import MealMaster
from cookbook.integration.melarecipes import MelaRecipes
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
@@ -45,6 +46,8 @@ def get_integration(request, export_type):
return NextcloudCookbook(request, export_type)
if export_type == ImportExportBase.MEALIE:
return Mealie(request, export_type)
if export_type == ImportExportBase.MEALIE1:
return Mealie1(request, export_type)
if export_type == ImportExportBase.CHOWDOWN:
return Chowdown(request, export_type)
if export_type == ImportExportBase.SAFFRON:

View File

@@ -54,7 +54,7 @@ def hook(request, token):
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, created_by=request.user, space=request.space)
ShoppingListEntry.objects.create(food=f, unit=u, amount=max(1, amount), created_by=request.user, space=request.space)
return JsonResponse({'data': data['message']['text']})
except Exception:

View File

@@ -21,7 +21,7 @@ from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.templatetags.static import static
from django.urls import reverse, reverse_lazy
from django.utils.datetime_safe import date
from django.utils import timezone
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from drf_spectacular.views import SpectacularRedocView, SpectacularSwaggerView
@@ -42,6 +42,9 @@ def index(request, path=None, resource=None):
if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS:
return HttpResponseRedirect(reverse_lazy('view_setup'))
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'):
return render(request, 'frontend/tandoor.html', {})
else:
@@ -98,7 +101,7 @@ def space_overview(request):
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
ai_enabled=settings.SPACE_AI_ENABLED,
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,)
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY, )
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
user_space.groups.add(Group.objects.filter(name='admin').get())
@@ -223,7 +226,7 @@ def system(request):
total_stats = ['All', int(r.get('api:request-count'))]
for i in range(0, 6):
d = (date.today() - timedelta(days=i)).isoformat()
d = (timezone.now() - timedelta(days=i)).isoformat()
api_stats[0].append(d)
api_space_stats[0].append(d)
total_stats.append(int(r.get(f'api:request-count:{d}')) if r.get(f'api:request-count:{d}') else 0)
@@ -234,7 +237,7 @@ def system(request):
endpoint = x[0].decode('utf-8')
endpoint_stats = [endpoint, x[1]]
for i in range(0, 6):
d = (date.today() - timedelta(days=i)).isoformat()
d = (timezone.now() - timedelta(days=i)).isoformat()
endpoint_stats.append(r.zscore(f'api:endpoint-request-count:{d}', endpoint))
api_stats.append(endpoint_stats)
@@ -243,7 +246,7 @@ def system(request):
if space := Space.objects.filter(pk=s).first():
space_stats = [space.name, x[1]]
for i in range(0, 6):
d = (date.today() - timedelta(days=i)).isoformat()
d = (timezone.now() - timedelta(days=i)).isoformat()
space_stats.append(r.zscore(f'api:space-request-count:{d}', s))
api_space_stats.append(space_stats)
@@ -322,7 +325,7 @@ def invite_link(request, token):
try:
token = UUID(token, version=4)
except ValueError:
messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!'))
print('Malformed Invite Link supplied!')
return HttpResponseRedirect(reverse('index'))
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
@@ -331,22 +334,17 @@ def invite_link(request, token):
link.used_by = request.user
link.save()
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False)
if request.user.userspace_set.count() == 1:
user_space.active = True
user_space.save()
UserSpace.objects.filter(user=request.user).update(active=False)
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=True)
user_space.groups.add(link.group)
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.'))
return HttpResponseRedirect(reverse('view_space_overview'))
return HttpResponseRedirect(reverse('index'))
else:
request.session['signup_token'] = str(token)
return HttpResponseRedirect(reverse('account_signup'))
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('view_space_overview'))
return HttpResponseRedirect(reverse('index'))
def report_share_abuse(request, token):

View File

@@ -97,10 +97,17 @@ Follow these steps to import your recipes
Mealie provides structured data similar to nextcloud.
!!! WARNING "Versions"
There are two different versions of the Mealie importer. One for all backups created prior to Version 1.0 and one for all after.
!!! INFO "Versions"
The Mealie UI does not indicate weather or not nutrition information is stored per serving or per recipe. This choice is left to the user. During the import you will have to choose
how Tandoor should treat your nutrition data.
To migrate your recipes
1. Go to your Mealie settings and create a new Backup.
2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server).
1. Go to your Mealie admin settings and create a new backup.
2. Download the backup.
3. Upload the entire `.zip` file to the importer page and import everything.
## Chowdown

View File

@@ -565,8 +565,6 @@ else:
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
@@ -594,8 +592,18 @@ LANGUAGES = [
AWS_ENABLED = True if os.getenv('S3_ACCESS_KEY', False) else False
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
# Serve static files with gzip
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
if os.getenv('S3_ACCESS_KEY', ''):
DEFAULT_FILE_STORAGE = 'cookbook.helper.CustomStorageClass.CachedS3Boto3Storage'
STORAGES['default']['BACKEND'] = 'cookbook.helper.CustomStorageClass.CachedS3Boto3Storage'
AWS_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY', '')
AWS_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', '')
@@ -610,14 +618,9 @@ if os.getenv('S3_ACCESS_KEY', ''):
if os.getenv('S3_CUSTOM_DOMAIN', ''):
AWS_S3_CUSTOM_DOMAIN = os.getenv('S3_CUSTOM_DOMAIN', '')
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
else:
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
# Serve static files with gzip
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, "mediafiles"))
# settings for cross site origin (CORS)
# all origins allowed to support bookmarklet

View File

@@ -1,12 +1,12 @@
Django==4.2.24
Django==5.2.6
cryptography===45.0.5
django-annoying==0.10.6
django-cleanup==9.0.0
django-crispy-forms==2.4
crispy-bootstrap4==2025.6
djangorestframework==3.15.2
djangorestframework==3.16.1
drf-spectacular==0.27.1
drf-spectacular-sidecar==2025.7.1
drf-spectacular-sidecar==2025.8.1
drf-writable-nested==0.7.2
django-oauth-toolkit==2.4.0
django-debug-toolbar==4.3.0
@@ -16,7 +16,7 @@ lxml==5.3.1
Markdown==3.7
Pillow==11.3.0
psycopg2-binary==2.9.10
python-dotenv==1.0.0
python-dotenv==1.1.1
requests==2.32.4
six==1.17.0
webdavclient3==3.14.6
@@ -33,9 +33,9 @@ recipe-scrapers==15.8.0
django-scopes==2.0.0
django-treebeard==4.7.1
django-cors-headers==4.6.0
django-storages==1.14.2
django-storages==1.14.6
boto3==1.28.75
django-prometheus==2.3.1
django-prometheus==2.4.1
django-hCaptcha==0.2.0
python-ldap==3.4.4
django-auth-ldap==4.6.0

View File

@@ -19,11 +19,11 @@
"pinia": "^3.0.2",
"vue": "^3.5.13",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "^11.1.10",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.0",
"vue-simple-calendar": "7.1.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.9.3"
"vuetify": "^3.9.7"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
@@ -35,10 +35,10 @@
"esbuild-register": "^3.6.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.3",
"vite": "6.3.5",
"vite-plugin-pwa": "^1.0.2",
"vite": "7.1.5",
"vite-plugin-pwa": "^1.0.3",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8",
"vue-tsc": "^3.0.6",
"workbox-background-sync": "^7.3.0",
"workbox-build": "^7.3.0",
"workbox-core": "^7.3.0",

View File

@@ -156,13 +156,16 @@ const router = useRouter()
const isPrintMode = useMediaQuery('print')
onMounted(() => {
useUserPreferenceStore()
useUserPreferenceStore().init()
})
/**
* global title update handler, might be overridden by page specific handlers
*/
router.afterEach((to, from) => {
if(to.name == 'StartPage' && !useUserPreferenceStore().activeSpace.spaceSetupCompleted != undefined &&!useUserPreferenceStore().activeSpace.spaceSetupCompleted && useUserPreferenceStore().activeSpace.createdBy.id! == useUserPreferenceStore().userSettings.user.id!){
router.push({name: 'WelcomePage'})
}
nextTick(() => {
if (to.meta.title) {
title.value = t(to.meta.title)

View File

@@ -18,6 +18,7 @@ let routes = [
{path: '/', component: () => import("@/pages/StartPage.vue"), name: 'StartPage' },
{path: '/search', redirect: {name: 'StartPage'}},
{path: '/test', component: () => import("@/pages/TestPage.vue"), name: 'view_test'},
{path: '/welcome', component: () => import("@/pages/WelcomePage.vue"), name: 'WelcomePage', meta: {title: 'Welcome'}},
{path: '/help', component: () => import("@/pages/HelpPage.vue"), name: 'HelpPage', meta: {title: 'Help'}},
{
path: '/settings', component: () => import("@/pages/SettingsPage.vue"), name: 'SettingsPage', redirect: '/settings/account',
@@ -28,8 +29,6 @@ let routes = [
{path: 'meal-plan', component: () => import("@/components/settings/MealPlanSettings.vue"), name: 'MealPlanSettings', meta: {title: 'Settings'}},
{path: 'search', component: () => import("@/components/settings/SearchSettings.vue"), name: 'SearchSettings', meta: {title: 'Settings'}},
{path: 'space', component: () => import("@/components/settings/SpaceSettings.vue"), name: 'SpaceSettings', meta: {title: 'Settings'}},
{path: 'space-members', component: () => import("@/components/settings/SpaceMemberSettings.vue"), name: 'SpaceMemberSettings', meta: {title: 'Settings'}},
{path: 'user-space', component: () => import("@/components/settings/UserSpaceSettings.vue"), name: 'UserSpaceSettings', meta: {title: 'Settings'}},
{path: 'open-data-import', component: () => import("@/components/settings/OpenDataImportSettings.vue"), name: 'OpenDataImportSettings', meta: {title: 'Settings'}},
{path: 'export', component: () => import("@/components/settings/ExportDataSettings.vue"), name: 'ExportDataSettings', meta: {title: 'Settings'}},
{path: 'api', component: () => import("@/components/settings/ApiSettings.vue"), name: 'ApiSettings', meta: {title: 'Settings'}},

View File

@@ -1,6 +1,6 @@
<template>
<v-row v-if=" props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
<v-row v-if="props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
<v-col>
<v-progress-linear :model-value="(props.importLog.importedRecipes/props.importLog.totalRecipes)*100" height="24" color="primary">
{{ props.importLog.importedRecipes }} / {{ props.importLog.totalRecipes }}
@@ -8,9 +8,9 @@
</v-col>
</v-row>
<v-row>
<v-row v-if="props.importLog.importedRecipes != undefined && props.importLog.totalRecipes != undefined">
<v-col>
<v-textarea :model-value="importLog.msg" max-rows="25" auto-grow></v-textarea>
<v-textarea :model-value="importLog.msg" max-rows="25" :loading="importLog.running" auto-grow></v-textarea>
</v-col>
</v-row>

View File

@@ -11,7 +11,7 @@
<recipe-image :height="itemHeight" :width="itemHeight" :recipe="mealPlan.recipe"></recipe-image>
</div>
<div class="flex-column flex-grow-0 pa-1">
<span class="font-light" :class="{'two-line-text': detailedItems,'one-line-text': !detailedItems,}">
<span class="font-light" :class="{'three-line-text': detailedItems,'one-line-text': !detailedItems,}">
<i class="fas fa-shopping-cart fa-xs float-left" v-if="mealPlan.shopping"/>
{{ itemTitle }}
</span>
@@ -82,4 +82,13 @@ const itemTitle = computed(() => {
overflow: hidden;
text-overflow: ellipsis;
}
.three-line-text {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -98,9 +98,9 @@ const planItems = computed(() => {
*/
const calendarItemHeight = computed(() => {
if (lgAndUp.value && useUserPreferenceStore().deviceSettings.mealplan_displayPeriod == 'week') {
return '2.6rem'
return '3.5rem'
} else {
return '1.3rem'
return '1.6rem'
}
})

View File

@@ -120,6 +120,7 @@ const hasFoodProperties = computed(() => {
let propertiesFound = false
for (const [key, fp] of Object.entries(recipe.value.foodProperties)) {
if (fp.total_value !== 0) {
console.log(fp, fp.total_value)
propertiesFound = true
}
}
@@ -189,7 +190,7 @@ const dialogProperty = ref<undefined | PropertyWrapper>(undefined)
const loading = ref(false)
onMounted(() => {
if (!hasFoodProperties) {
if (!hasFoodProperties.value) {
sourceSelectedToShow.value = "recipe"
}
})

View File

@@ -33,9 +33,8 @@
</template>
<v-list-item-title class="font-weight-bold">
{{ c.createdBy.displayName }}
</v-list-item-title>
<v-list-item-subtitle>{{ c.comment }}</v-list-item-subtitle>
<span>{{ c.comment }}</span>
<v-list-item-subtitle class="font-italic mt-1" v-if="c.servings != null && c.servings > 0">
@@ -49,7 +48,7 @@
<template #append>
<v-list-item-action class="flex-column align-end">
<v-rating density="comfortable" size="x-small" color="tandoor" v-model="c.rating" half-increments readonly
v-if="c.rating != undefined"></v-rating>
v-if="c.rating != undefined" style="overflow: hidden"></v-rating>
<v-spacer></v-spacer>
<v-tooltip location="top" :text="DateTime.fromJSDate(c.createdAt).toLocaleString(DateTime.DATETIME_MED)" v-if="c.createdAt != undefined">
<template v-slot:activator="{ props }">
@@ -121,6 +120,7 @@ function recLoadCookLog(recipeId: number, page: number = 1) {
* reset new cook log from with proper defaults
*/
function resetForm() {
newCookLog.value = {} as CookLog
newCookLog.value.servings = props.recipe.servings
newCookLog.value.createdAt = new Date()
newCookLog.value.recipe = props.recipe.id!

View File

@@ -1,5 +1,5 @@
<template>
<v-list-item class="swipe-container border-t-sm" :id="itemContainerId" @touchend="handleSwipe()"
<v-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0" :id="itemContainerId" @touchend="handleSwipe()" @click="dialog = true;"
v-if="isShoppingListFoodVisible(props.shoppingListFood, useUserPreferenceStore().deviceSettings)"
>
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">-->
@@ -7,15 +7,15 @@
<!-- </div>-->
<div class="flex-grow-1 p-2" @click="dialog = true;">
<div class="flex-grow-1 p-2">
<div class="d-flex">
<div class="d-flex flex-column pr-2">
<div class="d-flex flex-column pr-2 pl-4">
<span v-for="a in amounts" v-bind:key="a.key">
<span>
<i class="fas fa-check text-success fa-fw" v-if="a.checked"></i>
<i class="fas fa-clock-rotate-left text-info fa-fw" v-if="a.delayed"></i> <b>
<span :class="{'text-disabled': a.checked || a.delayed}" class="text-no-wrap">
<span v-if="amounts.length > 1 || (amounts.length == 1 && a.amount != 1)">{{ $n(a.amount) }}</span>
<span v-if="amounts.length > 1 || (amounts.length == 1 && a.amount != 1) || a.unit">{{ $n(a.amount) }}</span>
<span class="ms-1" v-if="a.unit">{{ pluralString(a.unit, a.amount) }}</span>
</span>
</b>
@@ -30,10 +30,13 @@
</div>
</div>
<template v-slot:[checkBtnSlot]>
<v-btn color="success" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
</v-btn>
<div class="ps-3 pe-3" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);">
<v-btn color="success" size="large"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
</v-btn>
</div>
<!-- <i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
</template>

View File

@@ -0,0 +1,72 @@
<template>
<v-row v-if="props.space.name != undefined">
<v-col cols="12" md="4">
<v-card :to="{name: 'SearchPage'}">
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
<v-card-text>{{ $n(props.space.recipeCount) }} / {{ props.space.maxRecipes == 0 ? '∞' : $n(props.space.maxRecipes) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveRecipeLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.recipeCount / props.space.maxRecipes) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :to="{name: 'ModelListPage', params: {model: 'UserSpace'}}">
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
<v-card-text>{{ $n(props.space.userCount) }} / {{ props.space.maxUsers == 0 ? '∞' : $n(props.space.maxUsers) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveUserLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.userCount / props.space.maxUsers) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card :to="{name: 'ModelListPage', params: {model: 'UserFile'}}">
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
<v-card-text v-if="props.space.maxFileStorageMb > -1">{{ $n(Math.round(props.space.fileSizeMb)) }} /
{{ props.space.maxFileStorageMb == 0 ? '' : $n(props.space.maxFileStorageMb) }}
MB
</v-card-text>
<v-card-text v-if="props.space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
<v-progress-linear v-if="props.space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(props.space) ? 'error' : 'success'" height="10"
:model-value="(props.space.fileSizeMb / props.space.maxFileStorageMb) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('MonthlyCredits') }}</v-card-title>
<v-card-text>{{ $n(props.space.aiMonthlyCreditsUsed) }} / {{ $n(props.space.aiCreditsMonthly) }} {{ $t('Credits') }}
</v-card-text>
<v-progress-linear :model-value="props.space.aiMonthlyCreditsUsed" :max="props.space.aiCreditsMonthly" height="10"
></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
<v-card-title><i class="fa-solid hand-holding-dollar"></i> {{ $t('AiCreditsBalance') }}</v-card-title>
<v-card-text>{{ $n(props.space.aiCreditsBalance) }} {{ $t('Credits') }}
</v-card-text>
<v-progress-linear height="10"
></v-progress-linear>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import {PropType} from "vue";
import {Space} from "@/openapi";
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils.ts";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
space: {type: {} as PropType<Space>, required: true},
})
</script>
<style scoped>
</style>

View File

@@ -71,7 +71,7 @@ const mergedIngredients = computed(() => {
// Add ingredients from steps
props.steps.forEach(step => {
step.ingredients.forEach(ingredient => {
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
if (ingredient.food && !ingredient.isHeader ) {
ingredients.push(ingredient);
}
});
@@ -80,7 +80,7 @@ const mergedIngredients = computed(() => {
if (step.stepRecipeData) {
step.stepRecipeData.steps?.forEach((subStep: Step) => {
subStep.ingredients.forEach((ingredient: Ingredient) => {
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
if (ingredient.food && !ingredient.isHeader) {
ingredients.push(ingredient);
}
});

View File

@@ -0,0 +1,47 @@
<template>
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
</v-alert>
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
</v-alert>
</template>
<script setup lang="ts">
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,51 @@
<template>
<v-select
:label="$t('Language')"
v-model="$i18n.locale"
:items="availableLocalizations"
item-title="language"
item-value="code"
@update:model-value="updateLanguage()"
></v-select>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, Localization} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useI18n} from "vue-i18n";
const availableLocalizations = ref([] as Localization[])
const {locale} = useI18n()
onMounted(() => {
const api = new ApiApi()
api.apiLocalizationList().then(r => {
availableLocalizations.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
/**
* update the django language cookie
* this is used by django to inject the language into the template which in turn
* sets the frontend language in i18n.ts when the frontend is initialized
*/
function updateLanguage() {
const expires = new Date();
expires.setTime(expires.getTime() + (100 * 365 * 24 * 60 * 60 * 1000));
document.cookie = `django_language=${locale.value}; expires=${expires.toUTCString()}; path=/`;
location.reload()
}
</script>
<style scoped>
</style>

View File

@@ -98,7 +98,7 @@
<script setup lang="ts">
import {ApiApi, UserFile, UserFileFromJSON} from "@/openapi";
import {onMounted, ref} from "vue";
import {onMounted, ref, watch} from "vue";
import {DateTime} from "luxon";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {getCookie} from "@/utils/cookie";
@@ -131,8 +131,13 @@ const tableHeaders = ref([
])
onMounted(() => {
//TODO move to open function of file tab
loadFiles()
})
watch(() => dialog.value, (value, oldValue) => {
if (value && !oldValue) {
loadFiles()
}
})
function loadFiles() {

View File

@@ -0,0 +1,142 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
<v-card-text class="pa-0">
<v-tabs v-model="tab" :disabled="loading" grow>
<v-tab value="space">{{ $t('Space') }}</v-tab>
<v-tab value="cosmetic">{{ $t('Cosmetic') }}</v-tab>
<v-tab value="ai">{{ $t('AI') }}</v-tab>
</v-tabs>
</v-card-text>
<v-card-text>
<v-tabs-window v-model="tab">
<v-tabs-window-item value="space">
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<user-file-field v-model="editingObj.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
<v-textarea v-model="editingObj.message" :label="$t('Message')" clearable></v-textarea>
<space-limits-info :space="editingObj" :show-thank-you="false" v-if="isUpdate()"></space-limits-info>
</v-form>
</v-tabs-window-item>
<v-tabs-window-item value="cosmetic">
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
<v-color-picker v-model="editingObj.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
<v-btn class="mb-4" @click="editingObj.navBgColor = ''">{{ $t('Reset') }}</v-btn>
<user-file-field v-model="editingObj.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>
<user-file-field v-model="editingObj.logoColor32" :label="$t('Logo') + ' 32x32px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor128" :label="$t('Logo') + ' 128x128px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor144" :label="$t('Logo') + ' 144x144px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor180" :label="$t('Logo') + ' 180x180px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor192" :label="$t('Logo') + ' 192x192px'"></user-file-field>
<user-file-field v-model="editingObj.logoColor512" :label="$t('Logo') + ' 512x512px'"></user-file-field>
<user-file-field v-model="editingObj.logoColorSvg" :label="$t('Logo') + ' SVG'"></user-file-field>
<user-file-field v-model="editingObj.customSpaceTheme" :label="$t('CustomTheme') + ' CSS'"></user-file-field>
</v-tabs-window-item>
<v-tabs-window-item value="ai">
<p class="text-disabled font-italic text-body-2">
<span v-if="useUserPreferenceStore().serverSettings.hosted">
{{ $t('AISettingsHostedHelp') }}
</span>
<span v-else>
{{ $t('SettingsOnlySuperuser') }}
</span>
</p>
<v-checkbox v-model="editingObj.aiEnabled" :label="$t('Enabled')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser" hide-details></v-checkbox>
<template v-if="editingObj.aiEnabled">
<model-select model="AiProvider" :label="$t('Default')" v-model="editingObj.aiDefaultProvider"></model-select>
<v-number-input v-model="editingObj.aiCreditsMonthly" :precision="2" :label="$t('MonthlyCredits')"
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
<v-number-input v-model="editingObj.aiCreditsBalance" :precision="4" :label="$t('AiCreditsBalance')"
:disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
</template>
</v-tabs-window-item>
</v-tabs-window>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {ApiApi, ConnectorConfig, Space} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import UserFileField from "@/components/inputs/UserFileField.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import editor from "mavon-editor";
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
const props = defineProps({
item: {type: {} as PropType<Space>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<Space>, required: false, default: {} as Space},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {
setupState,
deleteObject,
saveObject,
isUpdate,
editingObjName,
loading,
editingObj,
editingObjChanged,
modelClass
} = useModelEditorFunctions<Space>('Space', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
const tab = ref("space")
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor() {
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}
</script>
<style scoped>
</style>

View File

@@ -2,7 +2,10 @@
<v-form>
<p class="text-h6">{{ $t('Profile') }}</p>
<v-divider class="mb-3"></v-divider>
<v-text-field :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
<thank-you-note></thank-you-note>
<v-text-field class="mt-3" :label="$t('Username')" v-model="user.username" disabled :hint="$t('theUsernameCannotBeChanged')" persistent-hint></v-text-field>
<!-- <v-label>Avatar</v-label><br/>-->
<!-- <v-avatar class="mt-3 mb-3" style="height: 10vh; width: 10vh" color="info">V</v-avatar> Feature coming in a future Version of Tandoor.-->
@@ -39,6 +42,7 @@ import {ApiApi, User} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import ThankYouNote from "@/components/display/ThankYouNote.vue";
const {getDjangoUrl} = useDjangoUrls()

View File

@@ -3,14 +3,7 @@
<p class="text-h6">{{ $t('Cosmetic') }}</p>
<v-divider class="mb-3"></v-divider>
<v-select
:label="$t('Language')"
v-model="$i18n.locale"
:items="availableLocalizations"
item-title="language"
item-value="code"
@update:model-value="updateLanguage()"
></v-select>
<language-select></language-select>
<v-label>{{$t('Nav_Color')}}</v-label>
<v-color-picker v-model="useUserPreferenceStore().userSettings.navBgColor" mode="hex" :modes="['hex']" show-swatches :swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
@@ -54,10 +47,10 @@ import {ApiApi, Localization} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import LanguageSelect from "@/components/inputs/LanguageSelect.vue";
const {locale, t} = useI18n()
const {t} = useI18n()
const availableLocalizations = ref([] as Localization[])
const availableDefaultPages = ref([
{page: 'SEARCH', label: t('Search')},
{page: 'SHOPPING', label: t('Shopping_list')},
@@ -65,29 +58,10 @@ const availableDefaultPages = ref([
{page: 'BOOKS', label: t('Books')},
])
onMounted(() => {
const api = new ApiApi()
api.apiLocalizationList().then(r => {
availableLocalizations.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
/**
* update the django language cookie
* this is used by django to inject the language into the template which in turn
* sets the frontend language in i18n.ts when the frontend is initialized
*/
function updateLanguage() {
const expires = new Date();
expires.setTime(expires.getTime() + (100 * 365 * 24 * 60 * 60 * 1000));
document.cookie = `django_language=${locale.value}; expires=${expires.toUTCString()}; path=/`;
location.reload()
}
</script>
<style scoped>

View File

@@ -1,10 +1,9 @@
<template>
<p class="text-h6">{{ $t('Open_Data_Import') }}</p>
<p class="text-h4">{{ $t('Open_Data_Import') }}</p>
<v-divider></v-divider>
<p class="text-subtitle-2">{{ $t('Data_Import_Info') }}</p>
<v-btn href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{ $t('Learn_More') }}</v-btn>
<p class="text-subtitle-1">{{ $t('Data_Import_Info') }} <a href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{ $t('Learn_More') }}</a></p>
<v-select :items="metadata.versions" :label="$t('Language')" class="mt-2" v-model="requestData.selectedVersion" :loading="loading"></v-select>
<v-select :items="metadata.versions" :label="$t('Language')" class="mt-4" v-model="requestData.selectedVersion" :loading="loading"></v-select>
<v-row v-if="requestData.selectedVersion">
<v-col>
@@ -29,10 +28,10 @@
<td>{{ metadata[requestData.selectedVersion][d] }}</td>
<td>
<template v-if="responseData[d]">
<i class="fas fa-plus-circle"></i> {{ responseData[d].totalCreated }} {{ $t('Created') }} <br/>
<i class="fas fa-pencil-alt"></i> {{ responseData[d].totalUpdated }} {{ $t('Updated') }} <br/>
<i class="fas fa-forward"></i> {{ responseData[d].totalUntouched}} {{ $t('Unchanged') }} <br/>
<i class="fas fa-exclamation-circle"></i> {{ responseData[d].totalErrored }} {{ $t('Error') }}
<p v-if="responseData[d].totalCreated > 0" ><i class="fas fa-plus-circle"></i> {{ responseData[d].totalCreated }} {{ $t('Created') }}</p>
<p v-if="responseData[d].totalUpdated > 0"><i class="fas fa-pencil-alt"></i> {{ responseData[d].totalUpdated }} {{ $t('Updated') }}</p>
<p v-if="responseData[d].totalUntouched > 0"><i class="fas fa-forward"></i> {{ responseData[d].totalUntouched }} {{ $t('Unchanged') }}</p>
<p v-if="responseData[d].totalErrored > 0"><i class="fas fa-exclamation-circle"></i> {{ responseData[d].totalErrored }} {{ $t('Error') }}</p>
</template>
</td>
</tr>
@@ -102,7 +101,6 @@ function importOpenData() {
})
api.apiImportOpenDataCreate({importOpenData: requestData.value}).then(r => {
console.log(r)
responseData.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)

View File

@@ -1,122 +0,0 @@
<template>
<v-form>
<p class="text-h6">{{ $t('SpaceMembers') }}</p>
<v-divider></v-divider>
<p class="text-subtitle-2">{{ $t('SpaceMemberHelp') }}</p>
<v-data-table :items="spaceUserSpaces" :headers="userTableHeaders" density="compact" :hide-default-footer="spaceUserSpaces.length < 10" class="mt-3">
<template #item.groups="{item}">
<span v-for="g in item.groups">{{ g.name }}&nbsp;</span>
</template>
<template #item.edit="{item}">
<v-btn color="edit" size="small" v-if="item.user.id != useUserPreferenceStore().activeSpace.createdBy.id">
<v-icon icon="$edit"></v-icon>
<model-edit-dialog model="UserSpace" :item="item" @delete="deleteUserSpace(item)" class="mt-2"></model-edit-dialog>
</v-btn>
<v-chip color="edit" v-else>{{ $t('Owner') }}</v-chip>
</template>
</v-data-table>
<p class="text-h6 mt-3">{{ $t('Invites') }}
<v-btn size="small" class="float-right" prepend-icon="$create" color="create">
{{ $t('New') }}
<model-edit-dialog model="InviteLink" @delete="deleteInviteLink" @create="item => spaceInviteLinks.push(item)" class="mt-2"></model-edit-dialog>
</v-btn>
</p>
<v-divider class="mb-3"></v-divider>
<v-data-table :items="spaceInviteLinks" :headers="inviteTableHeaders" density="compact" :hide-default-footer="spaceInviteLinks.length < 10">
<template #item.reusable="{item}">
<v-icon icon="fa-solid fa-check" color="success" v-if="item.reusable"></v-icon>
<v-icon icon="fa-solid fa-times" color="error" v-if="!item.reusable"></v-icon>
</template>
<template #item.edit="{item}">
<btn-copy size="small" :copy-value="inviteLinkUrl(item)" class="me-1"></btn-copy>
<v-btn color="edit" size="small">
<v-icon icon="$edit"></v-icon>
<model-edit-dialog model="InviteLink" :item="item" @delete="deleteInviteLink(item)" class="mt-2"></model-edit-dialog>
</v-btn>
</template>
</v-data-table>
</v-form>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, InviteLink, UserSpace} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
const {t} = useI18n()
const spaceUserSpaces = ref([] as UserSpace[])
const spaceInviteLinks = ref([] as InviteLink[])
const userTableHeaders = [
{title: t('Username'), key: 'user.username'},
{title: t('Role'), key: 'groups'},
{title: t('Edit'), key: 'edit', align: 'end'},
]
const inviteTableHeaders = [
{title: 'ID', key: 'id'},
{title: t('Email'), key: 'email'},
{title: t('Role'), key: 'group.name'},
{title: t('Reusable'), key: 'reusable'},
{title: t('Edit'), key: 'edit', align: 'end'},
]
onMounted(() => {
const api = new ApiApi()
api.apiUserSpaceList().then(r => {
spaceUserSpaces.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
api.apiInviteLinkList({unused: true}).then(r => {
spaceInviteLinks.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
/**
* delete userspace from client list (database handled by editor)
* @param userSpace UserSpace object that was deleted
*/
function deleteUserSpace(userSpace: UserSpace) {
spaceUserSpaces.value.splice(spaceUserSpaces.value.indexOf(userSpace) - 1, 1)
}
/**
* delete invite link from client list (database handled by editor)
* @param inviteLink InviteLink object that was deleted
*/
function deleteInviteLink(inviteLink: InviteLink) {
spaceInviteLinks.value.splice(spaceInviteLinks.value.indexOf(inviteLink) - 1, 1)
}
/**
* returns url for invite link
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return useDjangoUrls().getDjangoUrl(`/invite/${inviteLink.uuid}`)
}
</script>
<style scoped>
</style>

View File

@@ -3,163 +3,17 @@
<p class="text-h6">{{ useUserPreferenceStore().activeSpace.name }}</p>
<v-divider class="mb-3"></v-divider>
<v-row v-if="space.name != undefined">
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-book"></i> {{ $t('Recipes') }}</v-card-title>
<v-card-text>{{ $n(space.recipeCount) }} / {{ space.maxRecipes == 0 ? '∞' : $n(space.maxRecipes) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveRecipeLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.recipeCount / space.maxRecipes) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-users"></i> {{ $t('Users') }}</v-card-title>
<v-card-text>{{ $n(space.userCount) }} / {{ space.maxUsers == 0 ? '∞' : $n(space.maxUsers) }}</v-card-text>
<v-progress-linear :color="isSpaceAboveUserLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.userCount / space.maxUsers) * 100"></v-progress-linear>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title><i class="fa-solid fa-file"></i> {{ $t('Files') }}</v-card-title>
<v-card-text v-if="space.maxFileStorageMb > -1">{{ $n(Math.round(space.fileSizeMb)) }} / {{ space.maxFileStorageMb == 0 ? '' : $n(space.maxFileStorageMb) }}
MB
</v-card-text>
<v-card-text v-if="space.maxFileStorageMb == -1">{{ $t('file_upload_disabled') }}</v-card-text>
<v-progress-linear v-if="space.maxFileStorageMb > -1" :color="isSpaceAboveStorageLimit(space) ? 'error' : 'success'" height="10"
:model-value="(space.fileSizeMb / space.maxFileStorageMb) * 100"></v-progress-linear>
</v-card>
</v-col>
</v-row>
<v-divider class="mt-3 mb-3"></v-divider>
<v-alert color="primary" variant="tonal" v-if="useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://tandoor.dev/manage" target="_blank">{{ $t('ManageSubscription') }}</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextHosted') }}</p>
</v-alert>
<v-alert color="primary" variant="tonal" v-if="!useUserPreferenceStore().serverSettings.hosted">
<v-alert-title>
<v-row>
<v-col>
<v-avatar image="../../assets/logo_color.svg" class="me-2"></v-avatar>
{{ $t('ThankYou') }}!
</v-col>
<v-col>
<v-btn color="primary" class="float-right" href="https://github.com/sponsors/vabene1111" target="_blank"><i class="fa-brands fa-github"></i> GitHub Sponsors
</v-btn>
</v-col>
</v-row>
</v-alert-title>
<p class="mt-2">{{ $t('ThanksTextSelfhosted') }}</p>
</v-alert>
<p class="text-h6 mt-2">{{ $t('Settings') }}</p>
<v-divider class="mb-2"></v-divider>
<user-file-field v-model="space.image" :label="$t('Image')" :hint="$t('CustomImageHelp')" persistent-hint></user-file-field>
<v-textarea v-model="space.message" :label="$t('Message')"></v-textarea>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<p class="text-h6 mt-2">{{ $t('AI') }}</p>
<v-divider class="mb-2"></v-divider>
<p class="text-disabled font-italic text-body-2">
<span v-if="useUserPreferenceStore().serverSettings.hosted">
{{ $t('AISettingsHostedHelp') }}
</span>
<span v-else>
{{ $t('SettingsOnlySuperuser') }}
</span>
</p>
<v-checkbox v-model="space.aiEnabled" :label="$t('Enabled')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser" hide-details></v-checkbox>
<template v-if="space.aiEnabled">
<model-select model="AiProvider" :label="$t('Default')" v-model="space.aiDefaultProvider"></model-select>
<v-number-input v-model="space.aiCreditsMonthly" :precision="2" :label="$t('MonthlyCredits')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
<v-number-input v-model="space.aiCreditsBalance" :precision="4" :label="$t('AiCreditsBalance')" :disabled="!useUserPreferenceStore().userSettings.user.isSuperuser"></v-number-input>
</template>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<v-divider class="mt-4 mb-2"></v-divider>
<h2>{{ $t('Cosmetic') }}</h2>
<span>{{ $t('Space_Cosmetic_Settings') }}</span>
<v-label class="mt-4">{{ $t('Nav_Color') }}</v-label>
<v-color-picker v-model="space.navBgColor" class="mb-4" mode="hex" :modes="['hex']" show-swatches
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
<v-btn class="mb-4" @click="space.navBgColor = ''">{{ $t('Reset') }}</v-btn>
<user-file-field v-model="space.navLogo" :label="$t('Logo')" :hint="$t('CustomNavLogoHelp')" persistent-hint></user-file-field>
<user-file-field v-model="space.logoColor32" :label="$t('Logo') + ' 32x32px'"></user-file-field>
<user-file-field v-model="space.logoColor128" :label="$t('Logo') + ' 128x128px'"></user-file-field>
<user-file-field v-model="space.logoColor144" :label="$t('Logo') + ' 144x144px'"></user-file-field>
<user-file-field v-model="space.logoColor180" :label="$t('Logo') + ' 180x180px'"></user-file-field>
<user-file-field v-model="space.logoColor192" :label="$t('Logo') + ' 192x192px'"></user-file-field>
<user-file-field v-model="space.logoColor512" :label="$t('Logo') + ' 512x512px'"></user-file-field>
<user-file-field v-model="space.logoColorSvg" :label="$t('Logo') + ' SVG'"></user-file-field>
<user-file-field v-model="space.customSpaceTheme" :label="$t('CustomTheme') + ' CSS'"></user-file-field>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<space-editor :item-id="useUserPreferenceStore().activeSpace.id!"></space-editor>
</v-form>
</template>
<script setup lang="ts">
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {onMounted, ref} from "vue";
import {ApiApi, Space} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import UserFileField from "@/components/inputs/UserFileField.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {isSpaceAboveRecipeLimit, isSpaceAboveStorageLimit, isSpaceAboveUserLimit} from "@/utils/logic_utils";
const space = ref({} as Space)
onMounted(() => {
let api = new ApiApi()
api.apiSpaceCurrentRetrieve().then(r => {
space.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
function updateSpace() {
let api = new ApiApi()
api.apiSpacePartialUpdate({id: space.value.id, patchedSpace: space.value}).then(r => {
space.value = r
useUserPreferenceStore().activeSpace = Object.assign({}, space.value)
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS, space.value)
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
}
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import SpaceLimitsInfo from "@/components/display/SpaceLimitsInfo.vue";
import SpaceEditor from "@/components/model_editors/SpaceEditor.vue";
</script>
<style scoped>

View File

@@ -1,53 +0,0 @@
<template>
<v-row>
<v-col>
<p class="text-h6">
{{ $t('YourSpaces') }}
<v-btn color="create" prepend-icon="$add" class="float-right" size="small" :href="getDjangoUrl('space-overview')">{{$t('New')}}</v-btn>
</p>
<v-divider></v-divider>
</v-col>
</v-row>
<v-row>
<v-col cols="6" v-for="s in spaces" :key="s.id">
<v-card @click="useUserPreferenceStore().switchSpace(s)">
<v-img height="200px" cover :src="(s.image !== undefined) ? s.image?.preview : recipeDefaultImage" :alt="$t('Image')"></v-img>
<v-card-title>{{ s.name }}
<v-chip variant="tonal" density="compact" color="error" v-if="s.id == useUserPreferenceStore().activeSpace.id">{{ $t('active') }}</v-chip>
</v-card-title>
<v-card-subtitle>{{ $t('created_by') }} {{ s.createdBy.displayName }}</v-card-subtitle>
</v-card>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {ApiApi, Space} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import recipeDefaultImage from '../../assets/recipe_no_image.svg'
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
const {getDjangoUrl} = useDjangoUrls()
const spaces = ref([] as Space[])
onMounted(() => {
const api = new ApiApi()
api.apiSpaceList().then(r => {
spaces.value = r.results
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
})
</script>
<style scoped>
</style>

View File

@@ -2,6 +2,7 @@ import {useDjangoUrls} from "@/composables/useDjangoUrls";
import {ref} from "vue";
import {getCookie} from "@/utils/cookie";
import {AiProvider, RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
import {tr} from "vuetify/locale";
/**
@@ -117,12 +118,18 @@ export function useFileApi() {
* @param files array to import
* @param app app to import
* @param includeDuplicates if recipes that were found as duplicates should be imported as well
* @param mealPlans if meal plans should be imported
* @param shoppingLists if shopping lists should be imported
* @param nutritionPerServing if nutrition information should be treated as per serving (if false its treated as per recipe)
* @returns Promise resolving to the import ID of the app import
*/
function doAppImport(files: File[], app: string, includeDuplicates: boolean) {
function doAppImport(files: File[], app: string, includeDuplicates: boolean, mealPlans: boolean = true, shoppingLists: boolean = true, nutritionPerServing: boolean = false,) {
let formData = new FormData()
formData.append('type', app);
formData.append('duplicates', includeDuplicates ? 'true' : 'false')
formData.append('meal_plans', mealPlans ? 'true' : 'false')
formData.append('shopping_lists', shoppingLists ? 'true' : 'false')
formData.append('nutrition_per_serving', nutritionPerServing ? 'true' : 'false')
files.forEach(file => {
formData.append('files', file)
})
@@ -141,4 +148,4 @@ export function useFileApi() {
}
return {fileApiLoading, createOrUpdateUserFile, updateRecipeImage, doAiImport, doAppImport}
}
}

View File

@@ -2,6 +2,7 @@
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -59,6 +60,8 @@
"CountMore": "",
"Create": "",
"Create Food": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -118,6 +121,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -133,6 +137,9 @@
"IgnoredFood": "",
"Image": "",
"Import": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -158,6 +165,7 @@
"Keyword": "",
"Keyword_Alias": "",
"Keywords": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
@@ -209,6 +217,8 @@
"NotInShopping": "",
"Note": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -284,8 +294,13 @@
"Show_as_header": "",
"Single": "",
"Size": "",
"Skip": "",
"Sort_by_new": "",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Starting_Day": "",
"StartsWith": "",
"StartsWithHelp": "",
@@ -332,6 +347,8 @@
"Website": "",
"Week": "",
"Week_Numbers": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -2,6 +2,7 @@
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "Добави",
"AddChild": "",
"AddFoodToShopping": "Добавете {food} към списъка си за пазаруване",
@@ -57,6 +58,8 @@
"CountMore": "...+{count} още",
"Create": "Създаване",
"Create Food": "Създайте храна",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Създайте запис за план за хранене",
"Create_New_Food": "Добавете нова храна",
"Create_New_Keyword": "Добавяне на нова ключова дума",
@@ -115,6 +118,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Групирай по",
"Hide_Food": "Скриване на храна",
"Hide_Keyword": "Скриване на ключови думи",
@@ -130,6 +134,9 @@
"IgnoredFood": "{food} е настроен да игнорира пазаруването.",
"Image": "Изображение",
"Import": "Импортиране",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Възникна грешка по време на импортирането ви. Моля, разгънете подробностите в долната част на страницата, за да ги видите.",
"Import_Not_Yet_Supported": "Импортирането все още не се поддържа",
"Import_Result_Info": "Импортирани са {imported} от {total} рецепти",
@@ -153,6 +160,7 @@
"Keyword": "Ключова дума",
"Keyword_Alias": "Псевдоним на ключова дума",
"Keywords": "Ключови думи",
"LeaveSpace": "",
"Link": "Връзка",
"Load_More": "Зареди още",
"LogCredits": "",
@@ -202,6 +210,8 @@
"NotInShopping": "{food} не е в списъка ви за пазаруване.",
"Note": "Бележка",
"Nutrition": "Хранителни стойности",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Вие сте офлайн, списъкът за пазаруване може да не се синхронизира.",
"Ok": "Отвори",
"OnHand": "В момента под ръка",
@@ -277,8 +287,13 @@
"Show_as_header": "Показване като заглавка",
"Single": "Единичен",
"Size": "Размер",
"Skip": "",
"Sort_by_new": "Сортиране по ново",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Starting_Day": "Начален ден от седмицата",
"StartsWith": "",
"StartsWithHelp": "",
@@ -323,6 +338,8 @@
"Website": "уебсайт",
"Week": "Седмица",
"Week_Numbers": "Номера на седмиците",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Година",
"Yes": "",
"add_keyword": "Добавяне на ключова дума",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Compte",
"Active": "",
"Add": "Afegir",
"AddChild": "",
"AddFoodToShopping": "Afegeix {food} a la llista de la compra",
@@ -74,6 +75,8 @@
"Create": "Crear",
"Create Food": "Crear aliment/ingredient",
"Create Recipe": "Crear una recepta",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crear una entrada de la planificació d'àpats",
"Create_New_Food": "Afegir nou ingredient",
"Create_New_Keyword": "Afegir nova Paraula Clau",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupat per",
"Hide_Food": "Amagar Aliment",
"Hide_Keyword": "Amaga les paraules clau",
@@ -177,6 +181,9 @@
"Image": "Imatge",
"Import": "Importar",
"Import Recipe": "Importar Recepta",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "S'ha produït un error durant la importació. Si us plau, amplia els detalls a la part inferior de la pàgina per veure'l.",
"Import_Not_Yet_Supported": "Importació encara no suportada",
"Import_Result_Info": "{imported} de {total} receptes s'han importat",
@@ -207,6 +214,7 @@
"Language": "Llenguatge",
"Last_name": "Cognoms",
"Learn_More": "Saber-me més",
"LeaveSpace": "",
"Link": "Enllaç",
"Load_More": "Carregueu-ne més",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "Nota",
"Number of Objects": "Nombre d'Objectes",
"Nutrition": "Valors nutricionals",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Estàs desconnectat, la llista de la compra no pot actualitzar-se.",
"Ok": "Ok",
"OnHand": "Ja en tinc",
@@ -361,9 +371,14 @@
"Show_as_header": "Mostreu com a títol",
"Single": "Únic/a",
"Size": "Mida",
"Skip": "",
"Social_Authentication": "Identificació amb Xarxes Socials",
"Sort_by_new": "Ordenar a partir del més nou",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Un administrador de l'espai podria canviar algunes configuracions estètiques i tindrien prioritat sobre la configuració dels usuaris per a aquest espai.",
"Split_All_Steps": "Dividir totes les files en passos separats.",
"StartDate": "Data d'inici",
@@ -430,6 +445,8 @@
"Week": "Setmana",
"Week_Numbers": "Números de la setmana",
"Welcome": "Benvingut/da",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Any",
"Yes": "",
"add_keyword": "Afegir Paraula Clau",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Účet",
"Active": "",
"Add": "Přidat",
"AddChild": "",
"AddFoodToShopping": "Přidat {food} na váš nákupní seznam",
@@ -74,6 +75,8 @@
"Create": "Vytvořit",
"Create Food": "Vytvořit potravinu",
"Create Recipe": "Vytvořit recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Vytvořit položku v jídelníčku",
"Create_New_Food": "Přidat novou potravinu",
"Create_New_Keyword": "Přidat nový štítek",
@@ -158,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Seskupit podle",
"Hide_Food": "Skrýt potravinu",
"Hide_Keyword": "Skrýt štítky",
@@ -176,6 +180,9 @@
"Image": "Obrázek",
"Import": "Import",
"Import Recipe": "Importovat recept",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Během importu došlo k chybě. Pro více informací rozbalte Detaily na konci stránky.",
"Import_Not_Yet_Supported": "Import není zatím podporován",
"Import_Result_Info": "{imported} z {total} receptů naimportováno",
@@ -205,6 +212,7 @@
"Language": "Jazyk",
"Last_name": "Příjmení",
"Learn_More": "Zjistit víc",
"LeaveSpace": "",
"Link": "Odkaz",
"Load_More": "Načíst další",
"LogCredits": "",
@@ -265,6 +273,8 @@
"Note": "Poznámka",
"Number of Objects": "Počet Objektů",
"Nutrition": "Výživové hodnoty",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Jste offline, nákupní seznam nemusí být synchronizován.",
"Ok": "Ok",
"OnHand": "Momentálně k dispozici",
@@ -356,9 +366,14 @@
"Show_as_header": "Nastav jako nadpis",
"Single": "Jednoduchý",
"Size": "Velikost",
"Skip": "",
"Social_Authentication": "Přihlašování pomocí účtů sociálních sítí",
"Sort_by_new": "Seřadit od nejnovějšího",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Některá kosmetická nastavení mohou měnit správci prostoru a budou mít přednost před nastavením klienta pro daný prostor.",
"Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.",
"StartDate": "Počáteční datum",
@@ -422,6 +437,8 @@
"Week": "Týden",
"Week_Numbers": "Číslo týdne",
"Welcome": "Vítejte",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Rok",
"Yes": "",
"add_keyword": "Přidat štítek",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Bruger",
"Active": "",
"Add": "Tilføj",
"AddChild": "",
"AddFoodToShopping": "Tilføj {food} til indkøbsliste",
@@ -74,6 +75,8 @@
"Create": "Opret",
"Create Food": "Opret mad",
"Create Recipe": "Opret opskrift",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Indsæt punkt i madplan",
"Create_New_Food": "Tilføj ny mad",
"Create_New_Keyword": "Tilføj nyt nøgleord",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupper efter",
"Hide_Food": "Skjul mad",
"Hide_Keyword": "Skjul nøgleord",
@@ -177,6 +181,9 @@
"Image": "Billede",
"Import": "Importer",
"Import Recipe": "Importer opskrift",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Der opstod en fejl under din importering. Udvid detaljerne i bunden af siden for at se fejlen.",
"Import_Not_Yet_Supported": "Import endnu ikke understøttet",
"Import_Result_Info": "{imported} af {total} opskrifter blev importeret",
@@ -207,6 +214,7 @@
"Language": "Sprog",
"Last_name": "Efternavn",
"Learn_More": "Lær mere",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Indlæs mere",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "Note",
"Number of Objects": "Antal objekter",
"Nutrition": "Næring",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Du er offline, indkøbslisten er måske ikke synkroniseret.",
"Ok": "Åben",
"OnHand": "Til rådighed",
@@ -361,9 +371,14 @@
"Show_as_header": "Vis som rubrik",
"Single": "Enkel",
"Size": "Størrelse",
"Skip": "",
"Social_Authentication": "Social authenticering",
"Sort_by_new": "Sorter efter nylige",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Visse kosmetiske indstillinger kan ændres af område-administratorer og vil overskrive klient-indstillinger for pågældende område.",
"Split_All_Steps": "Opdel rækker i separate trin.",
"StartDate": "Startdato",
@@ -430,6 +445,8 @@
"Week": "Uge",
"Week_Numbers": "Ugenumre",
"Welcome": "Velkommen",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "År",
"Yes": "",
"add_keyword": "Tilføj nøgleord",

View File

@@ -10,6 +10,7 @@
"Access_Token": "Zugriffstoken",
"Account": "Konto",
"Actions": "Aktionen",
"Active": "Aktiv",
"Activity": "Aktivität",
"Add": "Hinzufügen",
"AddAll": "Alle Hinzufügen",
@@ -111,6 +112,8 @@
"Create": "Erstellen",
"Create Food": "Zutat erstellen",
"Create Recipe": "Rezept erstellen",
"CreateFirstRecipe": "Erstelle dein erstes Rezept mit dem Rezepteditor.",
"CreateInvitation": "Einladung erstellen",
"Create_Meal_Plan_Entry": "Neuer Eintrag",
"Create_New_Food": "Neues Lebensmittel hinzufügen",
"Create_New_Keyword": "Neues Schlagwort hinzufügen",
@@ -222,6 +225,7 @@
"GettingStarted": "Erste Schritte",
"Global": "Global",
"GlobalHelp": "Globale AI Anbieter können von Nutzern aller Spaces verwendet werden. Sie können nur dich Instanz Admins (Superusers) erstellt und bearbeitet werden.",
"Group": "Gruppe",
"GroupBy": "Gruppieren nach",
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
"Headline": "Überschrift",
@@ -247,7 +251,10 @@
"Import": "Importieren",
"Import Recipe": "Rezept importieren",
"ImportAll": "Alle importieren",
"ImportFirstRecipe": "Importiere dein erstes Rezept von einer von tausenden Websites oder nutze einen der anderen Importer um bestehende Sammlungen, Dokumente oder URL Listen zu importieren. ",
"ImportIntoTandoor": "In Tandoor importieren",
"ImportMealPlans": "Speisepläne importieren",
"ImportShoppingList": "Einkaufslisten importieren",
"Import_Error": "Es ist ein Fehler beim Importieren aufgetreten. Bitte sieh dir die ausgeklappten Details unten auf der Seite an.",
"Import_Not_Yet_Supported": "Importieren wird noch nicht unterstützt",
"Import_Result_Info": "{imported} von insgesamt {total} Rezepten wurden importiert",
@@ -286,6 +293,7 @@
"Last": "Letztes",
"Last_name": "Nachname",
"Learn_More": "Mehr erfahren",
"LeaveSpace": "Space verlassen",
"Link": "Link",
"Load": "Laden",
"Load_More": "Weitere laden",
@@ -363,6 +371,8 @@
"Note": "Notiz",
"Number of Objects": "Anzahl von Objekten",
"Nutrition": "Nährwerte",
"NutritionsPerServing": "Nährwerte pro Portion",
"NutritionsPerServingHelp": "Manche Anwendungen spezifizieren nicht, ob Nährwerte pro Portion oder pro Rezept anzugeben sind. Standardmäßig werden Sie daher pro Rezept importiert. Wähle diese Option um Sie als pro Portion zu behandeln.",
"OfflineAlert": "Du bist offline. Deine Einkaufsliste wird nicht synchronisiert.",
"Ok": "Ok",
"OnHand": "Aktuell vorrätig",
@@ -499,17 +509,21 @@
"Show_as_header": "Als Überschrift",
"Single": "Einzeln",
"Size": "Größe",
"Skip": "Überspringen",
"Social_Authentication": "Login über Drittanbieter",
"Sort_by_new": "Nach Neueste sortieren",
"Source": "Quelle",
"SourceImportHelp": "Importiere JSON im schema.org/recipe format oder eine HTML Seite mit json+ld Rezept bzw. microdata.",
"SourceImportSubtitle": "Importiere JSON oder HTML manuell.",
"Space": "Space",
"SpaceHelp": "Alle deine Daten sind sicher in deinem Space gespeichert und können nur von dir und den anderen Mitgliedern genutzt werden.",
"SpaceLimitExceeded": "Dein Space hat ein Limit überschritten, manche Funktionen wurden eingeschränkt.",
"SpaceLimitReached": "Dieser Space hat ein Limit erreicht. Es können keine neuen Objekte von diesem Typ angelegt werden.",
"SpaceMemberHelp": "Füge Benutzer hinzu indem du Einladungen erstellst und Sie an die gewünschte Person sendest.",
"SpaceMembers": "Space Mitglieder",
"SpaceMembersHelp": "Benutzer und Ihre Rechte in einem Space. ",
"SpaceMembersHelp": "Benutzer und Ihre Rechte in einem Space. Füge weitere Nutzer mit Einladungslinks hinzu.",
"SpaceName": "Space Name",
"SpacePrivateObjectsHelp": "Einige Objekte sind Standardmäßig privat, können aber mit Mitgliedern deines Spaces geteilt werden.",
"SpaceSettings": "Space Einstellungen",
"Space_Cosmetic_Settings": "Kosmetische Einstellungen auf Space Ebene überschreiben die Einstellungen der einzelnen Nutzer.",
"Split": "Aufteilen",
@@ -621,6 +635,8 @@
"Week": "Woche",
"Week_Numbers": "Kalenderwochen",
"Welcome": "Willkommen",
"WelcomeSettingsHelp": "Bitte wähle die grundlegenden Einstellungen für deinen Space. Du kannst Sie später jederzeit in den Einstellungen ändern.",
"WelcometoTandoor": "Willkommen bei Tandoor",
"WorkingTime": "Arbeitszeit",
"Year": "Jahr",
"Yes": "Ja",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Λογαριασμός",
"Active": "",
"Add": "Προσθήκη",
"AddChild": "",
"AddFoodToShopping": "Προσθήκη του φαγητού {food} στη λίστα αγορών σας",
@@ -74,6 +75,8 @@
"Create": "Δημιουργία",
"Create Food": "Δημιουργία φαγητού",
"Create Recipe": "Δημιουργία συνταγής",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Δημιουργία εγγραφής στο πρόγραμμα γευμάτων",
"Create_New_Food": "Προσθήκη νέου φαγητού",
"Create_New_Keyword": "Προσθήκη νέας λέξης-κλειδί",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Ομαδοποίηση κατά",
"Hide_Food": "Απόκρυψη φαγητού",
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",
@@ -177,6 +181,9 @@
"Image": "Εικόνα",
"Import": "Εισαγωγή",
"Import Recipe": "Εισαγωγή συνταγής",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Συνέβη ένα σφάλμα κατά την εισαγωγή. Για να το δείτε, εμφανίστε τις λεπτομέρειες στο κάτω μέρος της σελίδας.",
"Import_Not_Yet_Supported": "Η εισαγωγή δεν υποστηρίζεται ακόμη",
"Import_Result_Info": "Έγινε εισαγωγή {imported} από τις {total} συνταγές",
@@ -207,6 +214,7 @@
"Language": "Γλώσσα",
"Last_name": "Επίθετο",
"Learn_More": "Μάθετε περισσότερα",
"LeaveSpace": "",
"Link": "Σύνδεσμος",
"Load_More": "Φόρτωση περισσότερων",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "Σημείωση",
"Number of Objects": "Αριθμός αντικειμένων",
"Nutrition": "Διατροφική αξία",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Είστε εκτός σύνδεσης, η λίστα αγορών μπορεί να μην συγχρονιστεί.",
"Ok": "ΟΚ",
"OnHand": "Τώρα διαθέσιμα",
@@ -361,9 +371,14 @@
"Show_as_header": "Εμφάνιση ως κεφαλίδα",
"Single": "Ενικός",
"Size": "Μέγεθος",
"Skip": "",
"Social_Authentication": "Ταυτοποίηση μέσω κοινωνικών δικτύων",
"Sort_by_new": "Ταξινόμηση κατά νέο",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Ορισμένες ρυθμίσεις εμφάνισης μπορούν να αλλάξουν από τους διαχειριστές του χώρου και θα παρακάμψουν τις ρυθμίσεις πελάτη για αυτόν τον χώρο.",
"Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.",
"StartDate": "Ημερομηνία Έναρξης",
@@ -430,6 +445,8 @@
"Week": "Εβδομάδα",
"Week_Numbers": "Αριθμοί εδομάδων",
"Welcome": "Καλώς ήρθατε",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Έτος",
"Yes": "",
"add_keyword": "Προσθήκη λέξης-κλειδί",

View File

@@ -10,6 +10,7 @@
"Access_Token": "Access Token",
"Account": "Account",
"Actions": "Actions",
"Active": "Active",
"Activity": "Activity",
"Add": "Add",
"AddAll": "Add all",
@@ -109,6 +110,8 @@
"Create": "Create",
"Create Food": "Create Food",
"Create Recipe": "Create Recipe",
"CreateFirstRecipe": "Create your first recipe using the recipe editor.",
"CreateInvitation": "Create invitation",
"Create_Meal_Plan_Entry": "Create meal plan entry",
"Create_New_Food": "Add New Food",
"Create_New_Keyword": "Add New Keyword",
@@ -220,6 +223,7 @@
"GettingStarted": "Getting Started",
"Global": "Global",
"GlobalHelp": "Global AI Providers can be used by users of all spaces. They can only be created and edited by superusers. ",
"Group": "Group",
"GroupBy": "Group By",
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
"Headline": "Headline",
@@ -245,7 +249,10 @@
"Import": "Import",
"Import Recipe": "Import Recipe",
"ImportAll": "Import all",
"ImportFirstRecipe": "Import your first recipe from one of thousands of websites or use one of the other importers to import your existing collection, documents or URL lists.",
"ImportIntoTandoor": "Import into Tandoor",
"ImportMealPlans": "Import mealplans",
"ImportShoppingList": "Import shoppinglists",
"Import_Error": "An Error occurred during your import. Please expand the Details at the bottom of the page to view it.",
"Import_Not_Yet_Supported": "Import not yet supported",
"Import_Result_Info": "{imported} of {total} recipes were imported",
@@ -284,6 +291,7 @@
"Last": "Last",
"Last_name": "Last Name",
"Learn_More": "Learn More",
"LeaveSpace": "Leave Space",
"Link": "Link",
"Load": "Load",
"Load_More": "Load More",
@@ -361,6 +369,8 @@
"Note": "Note",
"Number of Objects": "Number of Objects",
"Nutrition": "Nutrition",
"NutritionsPerServing": "Nutritions per Serving",
"NutritionsPerServingHelp": "Some applications do not specify if nutritions are per recipe or per serving. By default Tandoor treats them as per recipe. Check this box to treat them as per serving. ",
"OfflineAlert": "You are offline, shopping list may not syncronize.",
"Ok": "Ok",
"OnHand": "Currently On Hand",
@@ -497,17 +507,21 @@
"Show_as_header": "Show as header",
"Single": "Single",
"Size": "Size",
"Skip": "Skip",
"Social_Authentication": "Social Authentication",
"Sort_by_new": "Sort by new",
"Source": "Source",
"SourceImportHelp": "Import JSON in schema.org/recipe format or html pages with json+ld recipe or microdata.",
"SourceImportSubtitle": "Import JSON or HTML manually.",
"Space": "Space",
"SpaceHelp": "All your data is part of your space and can only be acccessed by space members. ",
"SpaceLimitExceeded": "Your space has surpassed one of its limits, some functions might be restricted.",
"SpaceLimitReached": "This Space has reached a limit. No more objects of this type can be created.",
"SpaceMemberHelp": "Add users to your space by creating an Invite Link and sending it to the person you want to add.",
"SpaceMembers": "Space Members",
"SpaceMembersHelp": "Users and their permissions in a space. ",
"SpaceMembersHelp": "Users and their permissions in a space. Add additional users using invite links.",
"SpaceName": "Space Name",
"SpacePrivateObjectsHelp": " Some things are private by default an can be shared with members of your space.",
"SpaceSettings": "Space Settings",
"Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.",
"Split": "Split",
@@ -619,6 +633,8 @@
"Week": "Week",
"Week_Numbers": "Week numbers",
"Welcome": "Welcome",
"WelcomeSettingsHelp": "Please choose the basic settings for your Tandoor space. You can change all of these later trough the settings.",
"WelcometoTandoor": "Welcome to Tandoor",
"WorkingTime": "Working time",
"Year": "Year",
"Yes": "Yes",

View File

@@ -9,6 +9,7 @@
"Access_Token": "Token de acceso",
"Account": "Cuenta",
"Actions": "Acciones",
"Active": "",
"Activity": "Actividad",
"Add": "Añadir",
"AddAll": "Agregar todo",
@@ -106,6 +107,8 @@
"Create": "Crear",
"Create Food": "Crear alimento",
"Create Recipe": "Crear receta",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crear entrada de plan de comidas",
"Create_New_Food": "Añadir nuevo alimento",
"Create_New_Keyword": "Añadir nueva palabra clave",
@@ -213,6 +216,7 @@
"GettingStarted": "Primeros pasos",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por",
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
"Headline": "Encabezado",
@@ -236,7 +240,10 @@
"Import": "Importar",
"Import Recipe": "Importar Receta",
"ImportAll": "Importar todo",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importar a Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Ocurrió un Error ocurrió durante la importación. Por favor, expanda los Detalles al final de la página para verlo.",
"Import_Not_Yet_Supported": "Importación no soportada todavía",
"Import_Result_Info": "{imported} de {total} recetas fueron importadas",
@@ -275,6 +282,7 @@
"Last": "Último",
"Last_name": "Apellidos",
"Learn_More": "Saber Más",
"LeaveSpace": "",
"Link": "Enlace",
"Load": "Cargar",
"Load_More": "Cargar más",
@@ -351,6 +359,8 @@
"Note": "Nota",
"Number of Objects": "Número de Objetos",
"Nutrition": "Nutrición",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Estas desconectado, la lista de la compra puede no sincronizarse.",
"Ok": "Ok",
"OnHand": "Actualmente en Posesión",
@@ -480,16 +490,20 @@
"Show_as_header": "Mostrar como encabezado",
"Single": "Simple",
"Size": "Tamaño",
"Skip": "",
"Social_Authentication": "Autenticación Social",
"Sort_by_new": "Ordenar por novedades",
"SourceImportHelp": "Importar JSON en formato schema.org/recipe o páginas HTML con recetas en formato JSON+LD o microdatos.",
"SourceImportSubtitle": "Importar JSON o HTML manualmente.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Tu espacio ha sobrepasado uno de sus límites, algunas funciones podrían estar restringidas.",
"SpaceLimitReached": "Este espacio ha alcanzado un límite. No se pueden crear más objetos de este tipo.",
"SpaceMemberHelp": "Agrega usuarios a tu espacio creando un enlace de invitación y enviándolo a la persona que quieras agregar.",
"SpaceMembers": "Miembros del espacio",
"SpaceMembersHelp": "Usuarios y sus permisos en un espacio. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Ajustes del espacio",
"Space_Cosmetic_Settings": "Algunos ajustes de apariencia pueden ser cambiados por los administradores del espacio y anularán los ajustes del cliente para ese espacio.",
"Split": "Dividir",
@@ -595,6 +609,8 @@
"Week": "Semana",
"Week_Numbers": "numero de semana",
"Welcome": "Bienvenido/a",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Tiempo de trabajo",
"Year": "Año",
"Yes": "",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Tili",
"Active": "",
"Add": "Lisää",
"AddChild": "",
"AddFoodToShopping": "Lisää {food} ostoslistaan",
@@ -71,6 +72,8 @@
"CountMore": "...+{count} enemmän",
"Create": "Luo",
"Create Food": "Luo Ruoka",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Luo ateriasuunnitelma merkintä",
"Create_New_Food": "Lisää Uusi Ruoka",
"Create_New_Keyword": "Lisää Uusi Avainsana",
@@ -156,6 +159,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Ryhmittely peruste",
"Hide_Food": "Piilota Ruoka",
"Hide_Keyword": "Piilota avainsana",
@@ -174,6 +178,9 @@
"Image": "Kuva",
"Import": "Tuo",
"Import Recipe": "Tuo Resepti",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Not_Yet_Supported": "Tuontia ei vielä tueta",
"Import_Supported": "Tuonti tuettu",
"Import_finished": "Tuonti valmistui",
@@ -201,6 +208,7 @@
"Language": "Kieli",
"Last_name": "Sukunimi",
"Learn_More": "Lisätietoja",
"LeaveSpace": "",
"Link": "Linkki",
"Load_More": "Lataa Lisää",
"LogCredits": "",
@@ -257,6 +265,8 @@
"Note": "Lisätiedot",
"Number of Objects": "Objektien määrä",
"Nutrition": "Ravitsemus",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Olet offline-tilassa, ostoslista ei välttämättä synkronoidu.",
"Ok": "Ok",
"OnHand": "Tällä hetkellä saatavilla",
@@ -349,9 +359,14 @@
"Show_as_header": "Näytä otsikkona",
"Single": "Yksittäinen",
"Size": "Koko",
"Skip": "",
"Social_Authentication": "Sosiaalinen Todennus",
"Sort_by_new": "Lajittele uusien mukaan",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Jaa kaikki rivit erillisiin vaiheisiin.",
"StartDate": "Aloituspäivä",
"Starting_Day": "Viikon aloituspäivä",
@@ -410,6 +425,8 @@
"Week": "Viikko",
"Week_Numbers": "Viikkonumerot",
"Welcome": "Tervetuloa",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Vuosi",
"Yes": "",
"add_keyword": "Lisää Avainsana",

View File

@@ -9,6 +9,7 @@
"Access_Token": "Jeton d'accès",
"Account": "Compte",
"Actions": "Actions",
"Active": "",
"Activity": "Activité",
"Add": "Ajouter",
"AddAll": "Tout ajouter",
@@ -109,6 +110,8 @@
"Create": "Créer",
"Create Food": "Créer un aliment",
"Create Recipe": "Créer une recette",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Créer une entrée de menu",
"Create_New_Food": "Ajouter un nouvel aliment",
"Create_New_Keyword": "Ajouter un nouveau mot-clé",
@@ -220,6 +223,7 @@
"GettingStarted": "Commencer",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grouper par",
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
"Headline": "En-tête",
@@ -245,7 +249,10 @@
"Import": "Importer",
"Import Recipe": "Importer une recette",
"ImportAll": "Tout importer",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importer dans Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Une erreur est survenue pendant votre importation. Veuillez développer les détails au bas de la page pour la consulter.",
"Import_Not_Yet_Supported": "Importation pas encore prise en charge",
"Import_Result_Info": "{imported} sur {total} recettes ont été importées",
@@ -284,6 +291,7 @@
"Last": "Dernier",
"Last_name": "Nom",
"Learn_More": "Apprenez-en plus",
"LeaveSpace": "",
"Link": "Lien",
"Load": "Chargement",
"Load_More": "Charger plus",
@@ -358,6 +366,8 @@
"Note": "Notes",
"Number of Objects": "Nombre d'objets",
"Nutrition": "Valeurs nutritionnelles",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Vous êtes déconnecté, votre liste de courses peut ne pas être synchronisée.",
"Ok": "D'accord",
"OnHand": "Disponible actuellement",
@@ -494,17 +504,21 @@
"Show_as_header": "Montrer comme en-tête",
"Single": "Unique",
"Size": "Taille",
"Skip": "",
"Social_Authentication": "Authentification Sociale",
"Sort_by_new": "Trier par nouveautés",
"Source": "Source",
"SourceImportHelp": "Importez du JSON au format schema.org/recipe ou des pages HTML avec une recette json+ld ou des microdonnées.",
"SourceImportSubtitle": "Importez en JSON ou HTML manuellement.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Votre groupe a dépassé une de ses limites, certaines fonctions pourraient être restreintes.",
"SpaceLimitReached": "Ce groupe a atteint sa limite. Aucun nouvel objet de ce type ne peut être créé.",
"SpaceMemberHelp": "Ajoutez des utilisateurs à votre espace en créant un lien d'invitation et en l'envoyant à la personne que vous souhaitez ajouter.",
"SpaceMembers": "Membres du groupe",
"SpaceMembersHelp": "Utilisateurs et permissions dans un groupe. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Paramètres du groupe",
"Space_Cosmetic_Settings": "Certains paramètres cosmétiques peuvent être modifiés par un administrateur de l'espace et seront prioritaires sur les paramètres des utilisateurs pour cet espace.",
"Split": "Diviser",
@@ -614,6 +628,8 @@
"Week": "Semaine",
"Week_Numbers": "Numéro de semaine",
"Welcome": "Bienvenue",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Temps de préparation",
"Year": "Année",
"Yes": "",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "חשבון",
"Active": "",
"Add": "הוספה",
"AddChild": "",
"AddFoodToShopping": "הוסף {מזון} לרשימת הקניות",
@@ -74,6 +75,8 @@
"Create": "יצירה",
"Create Food": "צור מאכל",
"Create Recipe": "צור מתכון",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "צור רשימת תכנון אוכל",
"Create_New_Food": "הוסף אוכל חדש",
"Create_New_Keyword": "הוסף מילת מפתח",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "אסוף לפי",
"Hide_Food": "הסתר אוכל",
"Hide_Keyword": "הסתר מילות מפתח",
@@ -177,6 +181,9 @@
"Image": "תמונה",
"Import": "ייבוא",
"Import Recipe": "ייבא מתכון",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "שגיאה בעת ייבוא. הרחב את הפירוט בסוף עמוד זה לראות מידע נוסף.",
"Import_Not_Yet_Supported": "ייבוא לא נתמך עדיין",
"Import_Result_Info": "{imported} מתוך {total} מתכונים יובאו",
@@ -207,6 +214,7 @@
"Language": "שפה",
"Last_name": "שם משפחה",
"Learn_More": "למד עוד",
"LeaveSpace": "",
"Link": "קישור",
"Load_More": "טען עוד",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "הערה",
"Number of Objects": "מספר אובייקטים",
"Nutrition": "תזונה",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "אתה במצב מנותק, רשימת הקניות לא בהכרח מסונכרנת.",
"Ok": "אישור",
"OnHand": "כרגע נגיש",
@@ -361,9 +371,14 @@
"Show_as_header": "הצג בתור כותרת",
"Single": "בודד",
"Size": "גודל",
"Skip": "",
"Social_Authentication": "אימות חברתי",
"Sort_by_new": "סדר ע\"י חדש",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "חלק מהגדרות הקוסמטיות יכולות להיות מעודכנות על ידי מנהל המרחב וידרסו את הגדרות הקליינט עבור מרחב זה.",
"Split_All_Steps": "פצל את כל השורות לצעדים נפרדים.",
"StartDate": "תאריך התחלה",
@@ -430,6 +445,8 @@
"Week": "שבוע",
"Week_Numbers": "מספר השבוע",
"Welcome": "ברוכים הבאים",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "שנה",
"Yes": "",
"add_keyword": "הוסף מילת מפתח",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Korisnički račun",
"Active": "",
"Add": "Dodaj",
"AddChild": "",
"AddFoodToShopping": "Dodaj {food} na svoj popis za kupovinu",
@@ -74,6 +75,8 @@
"Create": "Stvori",
"Create Food": "Kreiraj namirnicu",
"Create Recipe": "Kreiraj recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Kreirajte unos plana obroka",
"Create_New_Food": "Dodaj novu namirnicu",
"Create_New_Keyword": "Dodaj novu ključnu riječ",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupiraj po",
"Hide_Food": "Sakrij namirnicu",
"Hide_Keyword": "Sakrij ključne riječi",
@@ -177,6 +181,9 @@
"Image": "Slika",
"Import": "Uvoz",
"Import Recipe": "Uvezi recept",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Došlo je do pogreške tijekom uvoza. Molimo proširite pojedinosti na dnu stranice kako bi vidjeli grešku.",
"Import_Not_Yet_Supported": "Uvoz još nije podržan",
"Import_Result_Info": "Uvezeno je {imported} od {total} recepata",
@@ -207,6 +214,7 @@
"Language": "Jezik",
"Last_name": "Prezime",
"Learn_More": "Saznajte više",
"LeaveSpace": "",
"Link": "Poveznica",
"Load_More": "Učitaj više",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "Bilješka",
"Number of Objects": "Broj objekata",
"Nutrition": "Nutritivna vrijednost",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Nisi na mreži, popis za kupnju se možda neće sinkronizirati.",
"Ok": "Ok",
"OnHand": "Trenutno pri ruci",
@@ -361,9 +371,14 @@
"Show_as_header": "Prikaži kao zaglavlje",
"Single": "Jedna",
"Size": "Veličina",
"Skip": "",
"Social_Authentication": "Autentifikacija putem društvenih mreža",
"Sort_by_new": "Poredaj po novom",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Neke kozmetičke postavke mogu promijeniti administratori prostora i one će poništiti postavke klijenta za taj prostor.",
"Split_All_Steps": "Podijeli sve retke u zasebne korake.",
"StartDate": "Početni datum",
@@ -430,6 +445,8 @@
"Week": "Tjedan",
"Week_Numbers": "Brojevi tjedana",
"Welcome": "Dobrodošli",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Godina",
"Yes": "",
"add_keyword": "Dodaj ključnu riječ",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Fiók",
"Active": "",
"Add": "Hozzáadás",
"AddChild": "",
"AddFoodToShopping": "{food} hozzáadása bevásárlólistához",
@@ -72,6 +73,8 @@
"Create": "Létrehozás",
"Create Food": "Alapanyag létrehozása",
"Create Recipe": "Recept létrehozása",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Menüterv bejegyzés létrehozása",
"Create_New_Food": "Új alapanyag hozzáadása",
"Create_New_Keyword": "Új kulcsszó hozzáadása",
@@ -142,6 +145,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Csoportosítva",
"Hide_Food": "Alapanyag elrejtése",
"Hide_Keyword": "Kulcsszavak elrejtése",
@@ -160,6 +164,9 @@
"Image": "Kép",
"Import": "Import",
"Import Recipe": "Recept importálása",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Hiba történt az importálás során. Kérjük, a megtekintéshez bontsa ki az oldal alján található Részletek menüpontot.",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "{total}-ból/ből {imported} recept importálva",
@@ -189,6 +196,7 @@
"Language": "Nyelv",
"Last_name": "Vezetéknév",
"Learn_More": "Tudjon meg többet",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Továbbiak betöltése",
"LogCredits": "",
@@ -245,6 +253,8 @@
"Note": "Megjegyzés",
"Number of Objects": "Objektumok száma",
"Nutrition": "Tápérték",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Ön éppen offline állapotban van, a bevásárlólista nem biztos, hogy szinkronizálódik.",
"Ok": "Ok",
"OnHand": "Jelenleg készleten",
@@ -329,8 +339,13 @@
"Show_as_header": "Megjelenítés címként",
"Single": "Egyetlen",
"Size": "Méret",
"Skip": "",
"Sort_by_new": "Rendezés legújabbak szerint",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Ossza fel az összes sort különálló lépésekbe.",
"StartDate": "Kezdés dátuma",
"Starting_Day": "A hét kezdőnapja",
@@ -390,6 +405,8 @@
"Week": "Hét",
"Week_Numbers": "",
"Welcome": "Üdvözöljük",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Év",
"Yes": "",
"add_keyword": "Kulcsszó hozzáadása",

View File

@@ -2,6 +2,7 @@
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "",
"AddChild": "",
"Add_nutrition_recipe": "Ավելացնել սննդայնություն բաղադրատոմսին",
@@ -32,6 +33,8 @@
"ConvertUsingAI": "",
"Copy": "",
"Create": "Ստեղծել",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_New_Food": "Ավելացնել նոր սննդամթերք",
"Create_New_Keyword": "Ավելացնել նոր բանալի բառ",
"Create_New_Shopping Category": "Ստեղծել գնումների նոր կատեգորիա",
@@ -67,6 +70,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"Hide_Food": "Թաքցնել սննդամթերքը",
"Hide_Keywords": "Թաքցնել բանալի բառը",
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",
@@ -75,10 +79,14 @@
"IgnoreAccents": "",
"IgnoreAccentsHelp": "",
"Import": "Ներմուծել",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_finished": "Ներմուծումն ավարտված է",
"Information": "Տեղեկություն",
"Ingredients": "",
"Keywords": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
@@ -106,6 +114,8 @@
"NoUnit": "",
"No_Results": "Արդյունքներ չկան",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"Ok": "",
"Open": "",
"Parent": "Ծնող",
@@ -146,8 +156,13 @@
"Shopping_list": "Գնումների ցուցակ",
"Show_as_header": "Ցույց տալ որպես խորագիր",
"Size": "",
"Skip": "",
"Sort_by_new": "Տեսակավորել ըստ նորերի",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"StartsWith": "",
"StartsWithHelp": "",
"Step": "",
@@ -167,6 +182,8 @@
"View_Recipes": "Դիտել բաղադրատոմսերը",
"Visibility": "",
"Waiting": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Yes": "",
"all_fields_optional": "Բոլոր տողերը կամավոր են և կարող են մնալ դատարկ։",
"and": "և",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "Tambahkan",
"AddChild": "",
"AddFoodToShopping": "",
@@ -64,6 +65,8 @@
"CountMore": "",
"Create": "Membuat",
"Create Food": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -130,6 +133,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -147,6 +151,9 @@
"IgnoredFood": "",
"Image": "Gambar",
"Import": "Impor",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -174,6 +181,7 @@
"Keywords": "Kata Kunci",
"Language": "",
"Last_name": "",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Muat lebih banyak",
"LogCredits": "",
@@ -228,6 +236,8 @@
"NotInShopping": "",
"Note": "Catatan",
"Nutrition": "Nutrisi",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "Membuka",
"OnHand": "",
@@ -305,9 +315,14 @@
"Show_as_header": "Tampilkan sebagai tajuk",
"Single": "",
"Size": "Ukuran",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "Urutkan berdasarkan baru",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Starting_Day": "",
"StartsWith": "",
"StartsWithHelp": "",
@@ -357,6 +372,8 @@
"Website": "",
"Week": "",
"Week_Numbers": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -74,6 +75,8 @@
"Create": "",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -158,6 +161,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -176,6 +180,9 @@
"Image": "",
"Import": "",
"Import Recipe": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -206,6 +213,7 @@
"Language": "",
"Last_name": "",
"Learn_More": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
@@ -267,6 +275,8 @@
"Note": "",
"Number of Objects": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -359,9 +369,14 @@
"Show_as_header": "",
"Single": "",
"Size": "",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "",
"Split_All_Steps": "",
"StartDate": "",
@@ -428,6 +443,8 @@
"Week": "",
"Week_Numbers": "",
"Welcome": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -9,6 +9,7 @@
"Access_Token": "Token di accesso",
"Account": "Account",
"Actions": "Azioni",
"Active": "",
"Activity": "Attività",
"Add": "Aggiungi",
"AddAll": "Aggiungi tutto",
@@ -109,6 +110,8 @@
"Create": "Crea",
"Create Food": "Crea alimento",
"Create Recipe": "Crea ricetta",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crea voce nel piano alimentare",
"Create_New_Food": "Aggiungi nuovo alimento",
"Create_New_Keyword": "Aggiungi nuova parola chiave",
@@ -220,6 +223,7 @@
"GettingStarted": "Iniziamo",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Raggruppa per",
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
"Headline": "Intestazione",
@@ -245,7 +249,10 @@
"Import": "Importa",
"Import Recipe": "Importa ricetta",
"ImportAll": "Importa tutto",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importa in Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Si è verificato un errore durante l'importazione. Per avere maggiori informazioni, espandi la sezione dettagli in fondo alla pagina.",
"Import_Not_Yet_Supported": "Importazione non ancora supportata",
"Import_Result_Info": "{imported} di {total} ricette sono state importate",
@@ -284,6 +291,7 @@
"Last": "Ultimo",
"Last_name": "Cognome",
"Learn_More": "Scopri altro",
"LeaveSpace": "",
"Link": "Collegamento",
"Load": "Carica",
"Load_More": "Carica altro",
@@ -360,6 +368,8 @@
"Note": "Nota",
"Number of Objects": "Numero di oggetti",
"Nutrition": "Nutrienti",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Sei offline, le liste della spesa potrebbero non sincronizzarsi.",
"Ok": "Ok",
"OnHand": "Attualmente disponibili",
@@ -496,17 +506,21 @@
"Show_as_header": "Mostra come intestazione",
"Single": "Singolo",
"Size": "Dimensione",
"Skip": "",
"Social_Authentication": "Autenticazione social",
"Sort_by_new": "Prima i nuovi",
"Source": "Fonte",
"SourceImportHelp": "Importa JSON nel formato schema.org/recipe o pagine HTML con ricetta json+ld o microdati.",
"SourceImportSubtitle": "Importa manualmente JSON o HTML.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Il tuo spazio ha superato uno dei suoi limiti, alcune funzioni potrebbero essere limitate.",
"SpaceLimitReached": "Questo spazio ha raggiunto il limite. Non è possibile creare altri oggetti di questo tipo.",
"SpaceMemberHelp": "Aggiungi utenti al tuo spazio creando un collegamento di invito e inviandolo alla persona che desideri aggiungere.",
"SpaceMembers": "Membri dello spazio",
"SpaceMembersHelp": "Utenti e relativi permessi in uno spazio. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Impostazioni spazio",
"Space_Cosmetic_Settings": "Alcune impostazioni cosmetiche possono essere modificate dagli amministratori dell'istanza e sovrascriveranno le impostazioni client per quell'istanza.",
"Split": "Dividi",
@@ -616,6 +630,8 @@
"Week": "Settimana",
"Week_Numbers": "Numeri della settimana",
"Welcome": "Benvenuto",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Orario lavorativo",
"Year": "Anno",
"Yes": "",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -73,6 +74,8 @@
"Create": "",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -144,6 +147,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -162,6 +166,9 @@
"Image": "",
"Import": "",
"Import Recipe": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -191,6 +198,7 @@
"Language": "",
"Last_name": "",
"Learn_More": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "Įkelti daugiau",
"LogCredits": "",
@@ -248,6 +256,8 @@
"Note": "",
"Number of Objects": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -333,9 +343,14 @@
"Show_as_header": "Rodyti kaip antraštę",
"Single": "",
"Size": "",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "Rūšiuoti pagal naujumą",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "",
"StartDate": "",
"Starting_Day": "",
@@ -398,6 +413,8 @@
"Week": "",
"Week_Numbers": "",
"Welcome": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "",
"AddChild": "",
"AddFoodToShopping": "",
@@ -74,6 +75,8 @@
"Create": "",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -177,6 +181,9 @@
"Image": "",
"Import": "",
"Import Recipe": "",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -207,6 +214,7 @@
"Language": "",
"Last_name": "",
"Learn_More": "",
"LeaveSpace": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "",
"Number of Objects": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
@@ -361,9 +371,14 @@
"Show_as_header": "",
"Single": "",
"Size": "",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "",
"Split_All_Steps": "",
"StartDate": "",
@@ -430,6 +445,8 @@
"Week": "",
"Week_Numbers": "",
"Welcome": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "",
"Yes": "",
"add_keyword": "",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "",
"Active": "",
"Add": "Legg til",
"AddChild": "",
"AddFoodToShopping": "Legg til {food] i handlelisten din",
@@ -72,6 +73,8 @@
"Create": "Opprett",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Opprett måltidsplanoppføring",
"Create_New_Food": "Opprett ny matrett",
"Create_New_Keyword": "Opprett nytt nøkkelord",
@@ -150,6 +153,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupér",
"Hide_Food": "Skjul Matrett",
"Hide_Keyword": "Skjul nøkkelord",
@@ -168,6 +172,9 @@
"Image": "Bilde",
"Import": "Importer",
"Import Recipe": "Importer oppskrift",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -198,6 +205,7 @@
"Language": "Språk",
"Last_name": "Etternavn",
"Learn_More": "Lær mer",
"LeaveSpace": "",
"Link": "Lenke",
"Load_More": "Last inn flere",
"LogCredits": "",
@@ -254,6 +262,8 @@
"Note": "Merk",
"Number of Objects": "Antall objekter",
"Nutrition": "Næringsinnhold",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Du er ikke koblet til internett. Det kan hende handlelisten ikke synkroniserer.",
"Ok": "Ok",
"OnHand": "På lager",
@@ -343,9 +353,14 @@
"Show_as_header": "Vis som overskrift",
"Single": "",
"Size": "Størrelse",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "Sorter etter nyest",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "",
"StartDate": "Startdato",
"Starting_Day": "Dag uken skal state på",
@@ -407,6 +422,8 @@
"Week": "Uke",
"Week_Numbers": "Ukenummer",
"Welcome": "Velkommen",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "År",
"Yes": "",
"add_keyword": "",

View File

@@ -9,6 +9,7 @@
"Access_Token": "Toegangstoken",
"Account": "Account",
"Actions": "Acties",
"Active": "",
"Activity": "Activiteit",
"Add": "Voeg toe",
"AddAll": "Voeg alles toe",
@@ -110,6 +111,8 @@
"Create": "Aanmaken",
"Create Food": "Maak voedingsmiddel",
"Create Recipe": "Maak recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Maak maaltijdplan",
"Create_New_Food": "Voeg nieuw voedingsmiddel toe",
"Create_New_Keyword": "Voeg nieuw trefwoord toe",
@@ -221,6 +224,7 @@
"GettingStarted": "Aan de slag",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Groepeer per",
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
"Headline": "Koptekst",
@@ -246,7 +250,10 @@
"Import": "Importeer",
"Import Recipe": "Recept importeren",
"ImportAll": "Alles importeren",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importeer in Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Er is een fout opgetreden tijdens je import. Breid de details aan de onderzijde van de pagina uit om ze te bekijken.",
"Import_Not_Yet_Supported": "Import nog niet ondersteund",
"Import_Result_Info": "{imported} van {total} recepten zijn geïmporteerd",
@@ -285,6 +292,7 @@
"Last": "Laatste",
"Last_name": "Achternaam",
"Learn_More": "Meer informatie",
"LeaveSpace": "",
"Link": "Link",
"Load": "Laden",
"Load_More": "Laad meer",
@@ -361,6 +369,8 @@
"Note": "Notitie",
"Number of Objects": "Aantal objecten",
"Nutrition": "Voedingswaarde",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Je bent offline, boodschappenlijst synchroniseert mogelijk niet.",
"Ok": "Ok",
"OnHand": "Momenteel op voorraad",
@@ -497,17 +507,21 @@
"Show_as_header": "Toon als koptekst",
"Single": "Enkele",
"Size": "Grootte",
"Skip": "",
"Social_Authentication": "Authenticeren met sociale media-account",
"Sort_by_new": "Sorteer op nieuw",
"Source": "Bron",
"SourceImportHelp": "Importeer JSON in schema.org/recipe-formaat of html-paginas met json+ld-recepten of microdata.",
"SourceImportSubtitle": "Importeer handmatig JSON of HTML.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Je ruimte heeft een limiet overschreden, sommige functies zijn mogelijk beperkt.",
"SpaceLimitReached": "Deze ruimte heeft een limiet bereikt. Er kunnen geen objecten van dit type meer worden aangemaakt.",
"SpaceMemberHelp": "Voeg gebruikers toe aan je ruimte door een uitnodigingslink aan te maken en naar de persoon te sturen die je wilt toevoegen.",
"SpaceMembers": "Gebruikers van de ruimte",
"SpaceMembersHelp": "Gebruikers en hun rechten in een ruimte. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Ruimte-instellingen",
"Space_Cosmetic_Settings": "Sommige weergave instellingen kunnen worden geforceerd door de administrator van de 'Ruimte' en zullen de persoonlijke instellingen voor die 'Ruimte' overschrijven.",
"Split": "Splitsen",
@@ -617,6 +631,8 @@
"Week": "Week",
"Week_Numbers": "Weeknummers",
"Welcome": "Welkom",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Bereidingstijd",
"Year": "Jaar",
"Yes": "",

View File

@@ -7,6 +7,7 @@
"Access_Token": "Token Dostępu",
"Account": "Konto",
"Actions": "Akcje",
"Active": "",
"Activity": "Aktywność",
"Add": "Dodaj",
"AddAll": "Dodaj wszystkie",
@@ -100,6 +101,8 @@
"Create": "Stwórz",
"Create Food": "Twórz jedzenie",
"Create Recipe": "Utwórz przepis",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Utwórz wpis planu posiłków",
"Create_New_Food": "Dodaj nową żywność",
"Create_New_Keyword": "Dodaj nowe słowo kluczowe",
@@ -185,6 +188,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupuj według",
"Hide_Food": "Ukryj żywność",
"Hide_Keyword": "Ukryj słowa kluczowe",
@@ -203,6 +207,9 @@
"Image": "Obraz",
"Import": "Importuj",
"Import Recipe": "Importuj przepis",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Podczas importowania wystąpił błąd. Rozwiń Szczegóły na dole strony, aby go wyświetlić.",
"Import_Not_Yet_Supported": "Importowanie jeszcze nie wspierane",
"Import_Result_Info": "{imported} z {total} przepisów zostało zaimportowanych",
@@ -233,6 +240,7 @@
"Language": "Język",
"Last_name": "Nazwisko",
"Learn_More": "Dowiedz się więcej",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Załaduj więcej",
"LogCredits": "",
@@ -294,6 +302,8 @@
"Note": "Notatka",
"Number of Objects": "Ilość obiektów",
"Nutrition": "Odżywianie",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Jesteś offline, lista zakupów może nie być zsynchronizowana.",
"Ok": "Ok",
"OnHand": "Obecnie posiadane",
@@ -387,9 +397,14 @@
"Show_as_header": "Pokaż jako nagłówek",
"Single": "Pojedynczy",
"Size": "Rozmiar",
"Skip": "",
"Social_Authentication": "Uwierzytelnianie społecznościowe",
"Sort_by_new": "Sortuj według nowych",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
"Split_All_Steps": "Traktuj każdy wiersz jako osobne kroki.",
"StartDate": "Data początkowa",
@@ -456,6 +471,8 @@
"Week": "Tydzień",
"Week_Numbers": "Numery tygodni",
"Welcome": "Witamy",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Rok",
"Yes": "",
"add_keyword": "Dodaj słowo kluczowe",

View File

@@ -2,6 +2,7 @@
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "Adicionar",
"AddChild": "",
"AddFoodToShopping": "Adicionar {food} à sua lista de compras",
@@ -61,6 +62,8 @@
"CountMore": "...+{count} mais",
"Create": "Criar",
"Create Food": "Criar Comida",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Criar entrada para plano de refeições",
"Create_New_Food": "Adicionar nova comida",
"Create_New_Keyword": "Adicionar nova palavra-chave",
@@ -130,6 +133,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar por",
"Hide_Food": "Esconder comida",
"Hide_Keyword": "",
@@ -145,6 +149,9 @@
"IgnoredFood": "{food} está definida para ignorar compras.",
"Image": "Image",
"Import": "Importar",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_finished": "Importação terminada",
"Information": "Informação",
"Ingredient Editor": "Editor de Ingredientes",
@@ -165,6 +172,7 @@
"Keywords": "Palavras-chave",
"Language": "Linguagem",
"Learn_More": "Aprenda mais",
"LeaveSpace": "",
"Link": "Ligação",
"Load_More": "Carregar Mais",
"LogCredits": "",
@@ -214,6 +222,8 @@
"Note": "Nota",
"Number of Objects": "Número de objetos",
"Nutrition": "Nutrição",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Está offline, lista das compras poderá não sincronizar.",
"Ok": "Ok",
"OnHand": "Atualmente disponível",
@@ -295,8 +305,13 @@
"Show_Week_Numbers": "Mostrar números das semanas?",
"Show_as_header": "Mostrar como cabeçalho",
"Size": "Tamanho",
"Skip": "",
"Sort_by_new": "Ordenar por mais recente",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"StartDate": "Data de início",
"Starting_Day": "Dia de início da semana",
"StartsWith": "",
@@ -346,6 +361,8 @@
"Week": "Semana",
"Week_Numbers": "Números das semanas",
"Welcome": "Bem-vindo",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Ano",
"Yes": "",
"add_keyword": "Adicionar Palavra Chave",

View File

@@ -9,6 +9,7 @@
"Access_Token": "Token de acesso",
"Account": "Conta",
"Actions": "Ações",
"Active": "",
"Activity": "Atividade",
"Add": "Adicionar",
"AddAll": "Adicionar todos",
@@ -108,6 +109,8 @@
"Create": "Criar",
"Create Food": "Criar Alimento",
"Create Recipe": "Criar Receita",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Criar Plano de Refeição",
"Create_New_Food": "Incluir Novo Alimento",
"Create_New_Keyword": "Incluir Nova Palavra-Chave",
@@ -219,6 +222,7 @@
"GettingStarted": "Começando",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Agrupar Por",
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
"Headline": "Título",
@@ -244,7 +248,10 @@
"Import": "Importar",
"Import Recipe": "Importar Receita",
"ImportAll": "Importar todos",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Importar para Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Ocorreu um erro durante a importação. Expanda os detalhes na parte inferior da página para visualizá-los.",
"Import_Not_Yet_Supported": "Importação ainda não suportada",
"Import_Result_Info": "{imported} de {total} receitas foram importadas",
@@ -283,6 +290,7 @@
"Last": "Último",
"Last_name": "Último Nome",
"Learn_More": "Aprender Mais",
"LeaveSpace": "",
"Link": "Link",
"Load": "Carregar",
"Load_More": "Carregar mais",
@@ -348,6 +356,8 @@
"Note": "Nota",
"Number of Objects": "Número de Objetos",
"Nutrition": "Nutrição",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Você está offline, a lista de compras não pode ser sincronizada.",
"Ok": "Ok",
"OnHand": "Atualmente disponível",
@@ -435,9 +445,14 @@
"Show_as_header": "Mostrar como título",
"Single": "Simples",
"Size": "Tamanho",
"Skip": "",
"Social_Authentication": "Autenticação social",
"Sort_by_new": "Ordenar por novos",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Algumas configurações cosméticas podem ser alteradas pelos administradores do espaço e substituirão as configurações do cliente para esse espaço.",
"Split_All_Steps": "Divida todas as linhas em etapas separadas.",
"StartDate": "Data Início",
@@ -500,6 +515,8 @@
"Week": "Semana",
"Week_Numbers": "Números da Semana",
"Welcome": "Bem vindo",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Ano",
"Yes": "",
"add_keyword": "Incluir Palavra-Chave",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Cont",
"Active": "",
"Add": "Adaugă",
"AddChild": "",
"AddFoodToShopping": "Adăugă {food} în lista de cumpărături",
@@ -70,6 +71,8 @@
"Create": "Creează",
"Create Food": "Creare mâncare",
"Create Recipe": "Crearea rețetei",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Crearea înregistrării în planul de alimentare",
"Create_New_Food": "Adaugă mâncare nouă",
"Create_New_Keyword": "Adaugă cuvânt cheie nou",
@@ -137,6 +140,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Grupat de",
"Hide_Food": "Ascunde mâncare",
"Hide_Keyword": "Ascunde cuvintele cheie",
@@ -155,6 +159,9 @@
"Image": "Imagine",
"Import": "Importă",
"Import Recipe": "Importă rețeta",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "A apărut o eroare în timpul importului. Vă rugăm să extindeți detaliile din partea de jos a paginii pentru a le vizualiza.",
"Import_Not_Yet_Supported": "Importul încă nu este compatibil",
"Import_Result_Info": "{imported} din {total} rețete au fost importate",
@@ -183,6 +190,7 @@
"Keywords": "Cuvinte cheie",
"Language": "Limba",
"Last_name": "Nume de familie",
"LeaveSpace": "",
"Link": "Link",
"Load_More": "Încărcați mai mult",
"LogCredits": "",
@@ -237,6 +245,8 @@
"NotInShopping": "{food} nu se află în lista de cumpărături.",
"Note": "Notă",
"Nutrition": "Nutriție",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Sunteți offline, este posibil ca lista de cumpărături să nu se sincronizeze.",
"Ok": "Ok",
"OnHand": "În prezent, la îndemână",
@@ -317,9 +327,14 @@
"Show_as_header": "Afișare ca antet",
"Single": "Singur",
"Size": "Marime",
"Skip": "",
"Social_Authentication": "Autentificare socială",
"Sort_by_new": "Sortare după nou",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Împărțiți toate rândurile în pași separați.",
"Starting_Day": "Ziua de început a săptămânii",
"StartsWith": "",
@@ -376,6 +391,8 @@
"Website": "Site web",
"Week": "Săptămână",
"Week_Numbers": "Numerele săptămânii",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "An",
"Yes": "",
"add_keyword": "Adăugare cuvânt cheie",

View File

@@ -9,6 +9,7 @@
"Access_Token": "Токен доступа",
"Account": "Аккаунт",
"Actions": "Действия",
"Active": "",
"Activity": "Активность",
"Add": "Добавить",
"AddAll": "Добавить все",
@@ -109,6 +110,8 @@
"Create": "Создать",
"Create Food": "Создать продукт",
"Create Recipe": "Создать рецепт",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Создать плана питания",
"Create_New_Food": "Добавить новую еду",
"Create_New_Keyword": "Добавить ключевое слово",
@@ -220,6 +223,7 @@
"GettingStarted": "Начало работы",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Сгруппировать по",
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
"Headline": "Заголовок",
@@ -245,7 +249,10 @@
"Import": "Импорт",
"Import Recipe": "Импортировать рецепт",
"ImportAll": "Импортировать всё",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Импорт в Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Во время импорта произошла ошибка. Для просмотра разверните \"Подробности\" в нижней части страницы.",
"Import_Not_Yet_Supported": "Импорт пока не поддерживается",
"Import_Result_Info": "{imported} из {total} рецептов были импортированы",
@@ -284,6 +291,7 @@
"Last": "Последний",
"Last_name": "Фамилия",
"Learn_More": "Узнать больше",
"LeaveSpace": "",
"Link": "Гиперссылка",
"Load": "Загрузить",
"Load_More": "Загрузить еще",
@@ -358,6 +366,8 @@
"Note": "Заметка",
"Number of Objects": "Количество (шт.)",
"Nutrition": "Питательность",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Вы находитесь вне сети, список покупок может не синхронизироваться.",
"Ok": "Открыть",
"OnHand": "В Наличии",
@@ -494,17 +504,21 @@
"Show_as_header": "Показывать как заголовок",
"Single": "Одиночный",
"Size": "Размер",
"Skip": "",
"Social_Authentication": "Социальная аутентификация",
"Sort_by_new": "Сортировка по новизне",
"Source": "Источник",
"SourceImportHelp": "Импортируйте JSON в формате schema.org/recipe или HTML-страницы с рецептами в формате JSON-LD или микроданных.",
"SourceImportSubtitle": "Импортировать JSON или HTML вручную.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Ваше пространство превысило один из лимитов, некоторые функции могут быть ограничены.",
"SpaceLimitReached": "В этом пространстве достигнут лимит. Новые объекты данного типа создавать нельзя.",
"SpaceMemberHelp": "Для добавления пользователей создайте пригласительную ссылку и передайте её человеку, которого хотите пригласить.",
"SpaceMembers": "Участники пространства",
"SpaceMembersHelp": "Пользователи и их права доступа в пространстве. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Настройки пространства",
"Space_Cosmetic_Settings": "Администраторы пространства могут менять некоторые визуальные настройки, которые будут переопределять настройки клиента для данного пространства.",
"Split": "Разделить",
@@ -614,6 +628,8 @@
"Week": "Неделя",
"Week_Numbers": "Номер недели",
"Welcome": "Добро пожаловать",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Время работы",
"Year": "Год",
"Yes": "",

View File

@@ -9,6 +9,7 @@
"Access_Token": "Dostopni žeton",
"Account": "Račun",
"Actions": "Dejanja",
"Active": "",
"Activity": "Aktivnost",
"Add": "Dodaj",
"AddAll": "Dodaj vse",
@@ -109,6 +110,8 @@
"Create": "Ustvari",
"Create Food": "Ustvari živilo",
"Create Recipe": "Ustvari recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Ustvari vnos za načrtovan obrok",
"Create_New_Food": "Dodaj Novo Hrano",
"Create_New_Keyword": "Dodaj novo ključno besedo",
@@ -220,6 +223,7 @@
"GettingStarted": "Začetek",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Združi po",
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
"Headline": "Glavni naslov",
@@ -245,7 +249,10 @@
"Import": "Uvozi",
"Import Recipe": "Uvozi recept",
"ImportAll": "Uvozi vse",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "Uvozi v Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Med uvozom je prišlo do napake. Za ogled razširite podrobnosti na dnu strani.",
"Import_Not_Yet_Supported": "Uvoz še ni podprt",
"Import_Result_Info": "Uvoženih je bilo {imported} od {total} receptov",
@@ -284,6 +291,7 @@
"Last": "Zadnji",
"Last_name": "Priimek",
"Learn_More": "Preberite Več",
"LeaveSpace": "",
"Link": "Hiperpovezava",
"Load": "Naloži",
"Load_More": "Naloži več",
@@ -360,6 +368,8 @@
"Note": "Opomba",
"Number of Objects": "Število predmetov",
"Nutrition": "Prehrana",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Si v načinu brez povezave, nakupovalni listek se mogoče ne bo sinhroniziral.",
"Ok": "V redu",
"OnHand": "Trenutno imam v roki",
@@ -496,17 +506,21 @@
"Show_as_header": "Prikaži kot glavo",
"Single": "Ena",
"Size": "Velikost",
"Skip": "",
"Social_Authentication": "Socialna avtentikacija",
"Sort_by_new": "Razvrsti po novih",
"Source": "Vir",
"SourceImportHelp": "Uvozite JSON v formatu schema.org/recipe ali na straneh html z receptom json+ld ali mikropodatki.",
"SourceImportSubtitle": "Ročno uvozite JSON ali HTML.",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "Vaš prostor je presegel eno od svojih omejitev, nekatere funkcije so morda omejene.",
"SpaceLimitReached": "Ta prostor je dosegel omejitev. Te vrste predmetov ni mogoče ustvariti več.",
"SpaceMemberHelp": "Dodajte uporabnike v svoj prostor tako, da ustvarite povezavo za povabilo in jo pošljete osebi, ki jo želite dodati.",
"SpaceMembers": "Člani prostora",
"SpaceMembersHelp": "Uporabniki in njihova dovoljenja v prostoru. ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "Nastavitve prostora",
"Space_Cosmetic_Settings": "Nekatere kozmetične nastavitve lahko spremenijo skrbniki prostora in bodo preglasile nastavitve odjemalca za ta prostor.",
"Split": "Razdelitev",
@@ -616,6 +630,8 @@
"Week": "Teden",
"Week_Numbers": "Števila tednov",
"Welcome": "Dobrodošli",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "Delovni čas",
"Year": "Leto",
"Yes": "",

View File

@@ -8,6 +8,7 @@
"Access_Token": "Åtkomstnyckel",
"Account": "Konto",
"Actions": "Åtgärder",
"Active": "",
"Activity": "Aktivitet",
"Add": "Lägg till",
"AddAll": "Lägg till alla",
@@ -108,6 +109,8 @@
"Create": "Skapa",
"Create Food": "Skapa livsmedel",
"Create Recipe": "Skapa recept",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Skapa en måltidsplan",
"Create_New_Food": "Lägg till nytt livsmedel",
"Create_New_Keyword": "Lägg till nytt nyckelord",
@@ -196,6 +199,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Gruppera enligt",
"Hide_Food": "Dölj livsmedel",
"Hide_Keyword": "Dölj nyckelord",
@@ -214,6 +218,9 @@
"Image": "Bild",
"Import": "Importera",
"Import Recipe": "Importera recept",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "Ett fel uppstod under din import. Expandera informationen längst ner på sidan för att se den.",
"Import_Not_Yet_Supported": "Import stöds inte ännu",
"Import_Result_Info": "{imported} av totalt {total} recept blev importerat",
@@ -244,6 +251,7 @@
"Language": "Språk",
"Last_name": "Efternamn",
"Learn_More": "Läs mer",
"LeaveSpace": "",
"Link": "Länk",
"Load_More": "Ladda mer",
"LogCredits": "",
@@ -305,6 +313,8 @@
"Note": "Anteckning",
"Number of Objects": "Antal objekt",
"Nutrition": "Näringsinnehåll",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Du är offline, inköpslistan kanske inte synkroniseras.",
"Ok": "Öppna",
"OnHand": "För närvarande till hands",
@@ -398,9 +408,14 @@
"Show_as_header": "Visa som rubrik",
"Single": "Enstaka",
"Size": "Storlek",
"Skip": "",
"Social_Authentication": "Social autentisering",
"Sort_by_new": "Sortera efter ny",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Vissa kosmetiska inställningar kan ändras av hushålls-administratörer och skriver över klientinställningar för det hushållet.",
"Split_All_Steps": "Dela upp alla rader i separata steg.",
"StartDate": "Startdatum",
@@ -467,6 +482,8 @@
"Week": "Vecka",
"Week_Numbers": "Veckonummer",
"Welcome": "Välkommen",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "År",
"Yes": "",
"add_keyword": "Lägg till nyckelord",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "Hesap",
"Active": "",
"Add": "Ekle",
"AddChild": "",
"AddFoodToShopping": "{food}'ı alışveriş listenize ekleyin",
@@ -74,6 +75,8 @@
"Create": "Oluştur",
"Create Food": "Yiyecek Oluştur",
"Create Recipe": "Tarif Oluştur",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Yemek planı girişi oluştur",
"Create_New_Food": "Yeni Yiyecek Ekle",
"Create_New_Keyword": "Yeni Anahtar Kelime Ekle",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "Gruplandırma Ölçütü",
"Hide_Food": "Yiyeceği Gizle",
"Hide_Keyword": "Anahtar kelimeleri gizle",
@@ -177,6 +181,9 @@
"Image": "Resim",
"Import": "İçeriye Aktar",
"Import Recipe": "Tarif İçe Aktar",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "İçeri aktarma sırasında bir hata oluştu. Görüntülemek için lütfen sayfanın altındaki Ayrıntıları genişletin.",
"Import_Not_Yet_Supported": "İçe aktarma henüz desteklenmiyor",
"Import_Result_Info": "{total} tariften {imported} tanesi içe aktarıldı",
@@ -207,6 +214,7 @@
"Language": "Dil",
"Last_name": "Soyisim",
"Learn_More": "Daha Fazla",
"LeaveSpace": "",
"Link": "Bağlantı",
"Load_More": "Daha Fazla Yükle",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "Not",
"Number of Objects": "Nesne Sayısı",
"Nutrition": "Besin Değeri",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Çevrimdışısınız, alışveriş listesi senkronize edilemeyebilir.",
"Ok": "Tamam",
"OnHand": "Şu anda Elinizde",
@@ -361,9 +371,14 @@
"Show_as_header": "Başlık olarak göster",
"Single": "Tek",
"Size": "Boyut",
"Skip": "",
"Social_Authentication": "Sosyal Kimlik Doğrulama",
"Sort_by_new": "Yeniye göre sırala",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Bazı kozmetik ayarlar alan yöneticileri tarafından değiştirilebilir ve o alanın istemci ayarlarını geçersiz kılar.",
"Split_All_Steps": "Tüm satırları ayrı adımlara bölün.",
"StartDate": "Başlangıç Tarihi",
@@ -430,6 +445,8 @@
"Week": "Hafta",
"Week_Numbers": "Hafta numaraları",
"Welcome": "Hoşgeldiniz",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Yıl",
"Yes": "",
"add_keyword": "Anahtar Kelime Ekle",

View File

@@ -2,6 +2,7 @@
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Active": "",
"Add": "Додати",
"AddChild": "",
"AddFoodToShopping": "Додати {food} до вашого списку покупок",
@@ -65,6 +66,8 @@
"CountMore": "...+{count} більше",
"Create": "Створити",
"Create Food": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "Створити запис в плані харчування",
"Create_New_Food": "Додати Нову Їжу",
"Create_New_Keyword": "Додати Нове Ключове слово",
@@ -140,6 +143,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "По Групі",
"Hide_Food": "Сховати Їжу",
"Hide_Keyword": "",
@@ -155,6 +159,9 @@
"IgnoredFood": "{food} ігнорується в покупках.",
"Image": "Зображення",
"Import": "Імпорт",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
@@ -182,6 +189,7 @@
"Keywords": "Ключові слова",
"Language": "Мова",
"Learn_More": "Дізнатися Більше",
"LeaveSpace": "",
"Link": "Посилання",
"Load_More": "Завантажити більше",
"LogCredits": "",
@@ -234,6 +242,8 @@
"Note": "Нотатка",
"Number of Objects": "Кількість Об'єктів",
"Nutrition": "Харчова цінність",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "Ви офлайн, список покупок може не синхронізуватися.",
"Ok": "Відкрити",
"OnHand": "Зараз На Руках",
@@ -319,8 +329,13 @@
"Show_as_header": "Показати як заголовок",
"Single": "",
"Size": "Розмір",
"Skip": "",
"Sort_by_new": "Сортувати за новими",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"StartDate": "Початкова дата",
"Starting_Day": "Початковий день тижня",
"StartsWith": "",
@@ -373,6 +388,8 @@
"Week": "Неділя",
"Week_Numbers": "Номер тижня",
"Welcome": "Вітаємо",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "Рік",
"Yes": "",
"add_keyword": "",

View File

@@ -4,6 +4,7 @@
"API_Browser": "",
"API_Documentation": "",
"Account": "账户",
"Active": "",
"Add": "添加",
"AddChild": "",
"AddFoodToShopping": "添加 {food} 到购物清单",
@@ -74,6 +75,8 @@
"Create": "创建",
"Create Food": "创建食物",
"Create Recipe": "创建食谱",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "创建用餐计划条目",
"Create_New_Food": "添加新的食物",
"Create_New_Keyword": "添加新的关键词",
@@ -159,6 +162,7 @@
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "分组",
"Hide_Food": "隐藏食物",
"Hide_Keyword": "隐藏关键词",
@@ -177,6 +181,9 @@
"Image": "图片",
"Import": "导入",
"Import Recipe": "导入食谱",
"ImportFirstRecipe": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "导入时发生错误。 请跳转至页面底部的详细信息进行查看。",
"Import_Not_Yet_Supported": "导入尚未支持",
"Import_Result_Info": "导入 {imported} 个,共 {total} 个食谱已导入",
@@ -207,6 +214,7 @@
"Language": "语言",
"Last_name": "姓",
"Learn_More": "了解更多",
"LeaveSpace": "",
"Link": "链接",
"Load_More": "加载更多",
"LogCredits": "",
@@ -268,6 +276,8 @@
"Note": "笔记",
"Number of Objects": "对象数量",
"Nutrition": "营养",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "您处于离线状态,购物清单可能无法同步。",
"Ok": "确认",
"OnHand": "目前",
@@ -361,9 +371,14 @@
"Show_as_header": "显示标题",
"Single": "单个",
"Size": "大小",
"Skip": "",
"Social_Authentication": "社交认证",
"Sort_by_new": "按新旧排序",
"Space": "",
"SpaceHelp": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "空间管理员可以更改某些装饰设置,并将覆盖该空间的客户端设置。",
"Split_All_Steps": "将所有行拆分为单独的步骤。",
"StartDate": "开始日期",
@@ -430,6 +445,8 @@
"Week": "星期",
"Week_Numbers": "周数",
"Welcome": "欢迎",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"Year": "年",
"Yes": "",
"add_keyword": "添加关键字",

View File

@@ -9,6 +9,7 @@
"Access_Token": "訪問令牌",
"Account": "賬戶",
"Actions": "動作",
"Active": "",
"Activity": "活動",
"Add": "新增",
"AddAll": "增加全部",
@@ -108,6 +109,8 @@
"Create": "建立",
"Create Food": "建立食物",
"Create Recipe": "建立食譜",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "建立餐飲計劃條目",
"Create_New_Food": "建立新食物",
"Create_New_Keyword": "建立新關鍵字",
@@ -219,6 +222,7 @@
"GettingStarted": "開始使用",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "分組依據",
"HeaderWarning": "警告:變更為標題會刪除數量/單位/食物",
"Headline": "標題",
@@ -244,7 +248,10 @@
"Import": "匯入",
"Import Recipe": "匯入食譜",
"ImportAll": "全部匯入",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "匯入到 Tandoor",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "導入時發生錯誤。 請跳轉至頁面底部的詳細資訊進行查看。",
"Import_Not_Yet_Supported": "匯入尚不支援",
"Import_Result_Info": "匯入結果資訊",
@@ -283,6 +290,7 @@
"Last": "最後",
"Last_name": "姓",
"Learn_More": "了解更多",
"LeaveSpace": "",
"Link": "連結",
"Load": "載入",
"Load_More": "載入更多",
@@ -359,6 +367,8 @@
"Note": "備註",
"Number of Objects": "對象數量",
"Nutrition": "營養",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "您處於離線狀態,購物清單可能無法同步。",
"Ok": "確定",
"OnHand": "手頭有",
@@ -495,17 +505,21 @@
"Show_as_header": "顯示為標題",
"Single": "單一",
"Size": "大小",
"Skip": "",
"Social_Authentication": "社交認證",
"Sort_by_new": "按最新排序",
"Source": "來源",
"SourceImportHelp": "匯入 schema.org/recipe 格式的 JSON 或包含 json+ld 食譜或微資料的 HTML 頁面。",
"SourceImportSubtitle": "手動匯入 JSON 或 HTML。",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "您的空間已超過其中一個限制,某些功能可能會受到限制。",
"SpaceLimitReached": "此空間已達到限制。無法再建立此類型的物件。",
"SpaceMemberHelp": "透過建立邀請連結並發送給您要新增的人來將使用者新增到您的空間。",
"SpaceMembers": "空間成員",
"SpaceMembersHelp": "空間中的使用者及其權限。 ",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "空間設定",
"Space_Cosmetic_Settings": "空間管理員可以更改某些裝飾設置,並將覆蓋該空間的客戶端設置。",
"Split": "分割",
@@ -615,6 +629,8 @@
"Week": "週",
"Week_Numbers": "週數",
"Welcome": "歡迎",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "製作時間",
"Year": "年",
"Yes": "",

View File

@@ -1785,6 +1785,10 @@ export interface ApiShoppingListRecipeUpdateRequest {
shoppingListRecipe: Omit<ShoppingListRecipe, 'recipeData'|'mealPlanData'|'createdBy'>;
}
export interface ApiSpaceCreateRequest {
space?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'>;
}
export interface ApiSpaceListRequest {
page?: number;
pageSize?: number;
@@ -1799,6 +1803,11 @@ export interface ApiSpaceRetrieveRequest {
id: number;
}
export interface ApiSpaceUpdateRequest {
id: number;
space?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'>;
}
export interface ApiStepCreateRequest {
step: Omit<Step, 'instructionsMarkdown'|'stepRecipeData'|'numrecipe'>;
}
@@ -2139,6 +2148,11 @@ export interface ApiUserRetrieveRequest {
id: number;
}
export interface ApiUserSpaceAllPersonalListRequest {
page?: number;
pageSize?: number;
}
export interface ApiUserSpaceDestroyRequest {
id: number;
}
@@ -13083,6 +13097,39 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiSpaceCreateRaw(requestParameters: ApiSpaceCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Space>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/space/`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: SpaceToJSON(requestParameters['space']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => SpaceFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiSpaceCreate(requestParameters: ApiSpaceCreateRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Space> {
const response = await this.apiSpaceCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -13228,6 +13275,46 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiSpaceUpdateRaw(requestParameters: ApiSpaceUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Space>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiSpaceUpdate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/space/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PUT',
headers: headerParameters,
query: queryParameters,
body: SpaceToJSON(requestParameters['space']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => SpaceFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiSpaceUpdate(requestParameters: ApiSpaceUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Space> {
const response = await this.apiSpaceUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -16152,6 +16239,44 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* return all userspaces for the user requesting the endpoint :param request: :return:
*/
async apiUserSpaceAllPersonalListRaw(requestParameters: ApiUserSpaceAllPersonalListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedUserSpaceList>> {
const queryParameters: any = {};
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
if (requestParameters['pageSize'] != null) {
queryParameters['page_size'] = requestParameters['pageSize'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/user-space/all_personal/`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedUserSpaceListFromJSON(jsonValue));
}
/**
* return all userspaces for the user requesting the endpoint :param request: :return:
*/
async apiUserSpaceAllPersonalList(requestParameters: ApiUserSpaceAllPersonalListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedUserSpaceList> {
const response = await this.apiUserSpaceAllPersonalListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/

View File

@@ -248,6 +248,12 @@ export interface PatchedSpace {
* @memberof PatchedSpace
*/
aiDefaultProvider?: AiProvider;
/**
*
* @type {boolean}
* @memberof PatchedSpace
*/
spaceSetupCompleted?: boolean;
}
/**
@@ -299,6 +305,7 @@ export function PatchedSpaceFromJSONTyped(json: any, ignoreDiscriminator: boolea
'aiMonthlyCreditsUsed': json['ai_monthly_credits_used'] == null ? undefined : json['ai_monthly_credits_used'],
'aiEnabled': json['ai_enabled'] == null ? undefined : json['ai_enabled'],
'aiDefaultProvider': json['ai_default_provider'] == null ? undefined : AiProviderFromJSON(json['ai_default_provider']),
'spaceSetupCompleted': json['space_setup_completed'] == null ? undefined : json['space_setup_completed'],
};
}
@@ -329,6 +336,7 @@ export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'creat
'ai_credits_balance': value['aiCreditsBalance'],
'ai_enabled': value['aiEnabled'],
'ai_default_provider': AiProviderToJSON(value['aiDefaultProvider']),
'space_setup_completed': value['spaceSetupCompleted'],
};
}

View File

@@ -121,7 +121,7 @@ export interface Space {
* @type {Array<FoodInheritField>}
* @memberof Space
*/
foodInherit: Array<FoodInheritField>;
foodInherit?: Array<FoodInheritField>;
/**
*
* @type {number}
@@ -248,6 +248,12 @@ export interface Space {
* @memberof Space
*/
aiDefaultProvider?: AiProvider;
/**
*
* @type {boolean}
* @memberof Space
*/
spaceSetupCompleted?: boolean;
}
/**
@@ -261,7 +267,6 @@ export function instanceOfSpace(value: object): value is Space {
if (!('maxUsers' in value) || value['maxUsers'] === undefined) return false;
if (!('allowSharing' in value) || value['allowSharing'] === undefined) return false;
if (!('demo' in value) || value['demo'] === undefined) return false;
if (!('foodInherit' in value) || value['foodInherit'] === undefined) return false;
if (!('userCount' in value) || value['userCount'] === undefined) return false;
if (!('recipeCount' in value) || value['recipeCount'] === undefined) return false;
if (!('fileSizeMb' in value) || value['fileSizeMb'] === undefined) return false;
@@ -289,7 +294,7 @@ export function SpaceFromJSONTyped(json: any, ignoreDiscriminator: boolean): Spa
'maxUsers': json['max_users'],
'allowSharing': json['allow_sharing'],
'demo': json['demo'],
'foodInherit': ((json['food_inherit'] as Array<any>).map(FoodInheritFieldFromJSON)),
'foodInherit': json['food_inherit'] == null ? undefined : ((json['food_inherit'] as Array<any>).map(FoodInheritFieldFromJSON)),
'userCount': json['user_count'],
'recipeCount': json['recipe_count'],
'fileSizeMb': json['file_size_mb'],
@@ -311,6 +316,7 @@ export function SpaceFromJSONTyped(json: any, ignoreDiscriminator: boolean): Spa
'aiMonthlyCreditsUsed': json['ai_monthly_credits_used'],
'aiEnabled': json['ai_enabled'] == null ? undefined : json['ai_enabled'],
'aiDefaultProvider': json['ai_default_provider'] == null ? undefined : AiProviderFromJSON(json['ai_default_provider']),
'spaceSetupCompleted': json['space_setup_completed'] == null ? undefined : json['space_setup_completed'],
};
}
@@ -323,7 +329,7 @@ export function SpaceToJSON(value?: Omit<Space, 'createdBy'|'createdAt'|'maxReci
'id': value['id'],
'name': value['name'],
'message': value['message'],
'food_inherit': ((value['foodInherit'] as Array<any>).map(FoodInheritFieldToJSON)),
'food_inherit': value['foodInherit'] == null ? undefined : ((value['foodInherit'] as Array<any>).map(FoodInheritFieldToJSON)),
'image': UserFileViewToJSON(value['image']),
'nav_logo': UserFileViewToJSON(value['navLogo']),
'space_theme': SpaceThemeEnumToJSON(value['spaceTheme']),
@@ -341,6 +347,7 @@ export function SpaceToJSON(value?: Omit<Space, 'createdBy'|'createdAt'|'maxReci
'ai_credits_balance': value['aiCreditsBalance'],
'ai_enabled': value['aiEnabled'],
'ai_default_provider': AiProviderToJSON(value['aiDefaultProvider']),
'space_setup_completed': value['spaceSetupCompleted'],
};
}

View File

@@ -35,6 +35,17 @@
<database-model-col model="MealType"></database-model-col>
</v-row>
<v-row>
<v-col>
<h2>{{ $t('Space') }}</h2>
</v-col>
</v-row>
<v-row dense>
<database-model-col model="Space"></database-model-col>
<database-model-col model="UserSpace"></database-model-col>
<database-model-col model="InviteLink"></database-model-col>
</v-row>
<template v-if="useUserPreferenceStore().activeSpace.aiEnabled">
<v-row>
<v-col>

View File

@@ -34,16 +34,19 @@
</v-card-actions>
<v-card-text v-if="genericModel.model.name == 'AiLog'">
{{ $t('MonthlyCreditsUsed') }} ({{ useUserPreferenceStore().activeSpace.aiMonthlyCreditsUsed }} / {{ useUserPreferenceStore().activeSpace.aiCreditsMonthly }})
{{ $t('MonthlyCreditsUsed') }} ({{ useUserPreferenceStore().activeSpace.aiMonthlyCreditsUsed }} / {{
useUserPreferenceStore().activeSpace.aiCreditsMonthly
}})
{{ $t('AiCreditsBalance') }} : {{ useUserPreferenceStore().activeSpace.aiCreditsBalance }}
<v-progress-linear :model-value="useUserPreferenceStore().activeSpace.aiMonthlyCreditsUsed" :max="useUserPreferenceStore().activeSpace.aiCreditsMonthly"></v-progress-linear>
<v-progress-linear :model-value="useUserPreferenceStore().activeSpace.aiMonthlyCreditsUsed"
:max="useUserPreferenceStore().activeSpace.aiCreditsMonthly"></v-progress-linear>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field prepend-inner-icon="$search" :label="$t('Search')" v-model="query" clearable></v-text-field>
<v-text-field prepend-inner-icon="$search" :label="$t('Search')" v-model="query" v-if="!genericModel.model.disableSearch" clearable></v-text-field>
<v-data-table-server
v-model="selectedItems"
@@ -82,13 +85,20 @@
<v-chip label v-if="item.space == null" color="success">{{ $t('Global') }}</v-chip>
<v-chip label v-else color="info">{{ $t('Space') }}</v-chip>
</template>
<template v-slot:item.groups="{ item }" v-if="genericModel.model.name == 'UserSpace'">
{{ item.groups.flatMap((x: Group) => x.name).join(', ') }}
</template>
<template v-slot:item.active="{ item }" v-if="genericModel.model.name == 'Space'">
<v-chip label v-if="item.id == useUserPreferenceStore().activeSpace.id!" color="success">{{ $t('Active') }}</v-chip>
<v-chip label v-else color="info" @click="useUserPreferenceStore().switchSpace(item)">{{ $t('Select') }}</v-chip>
</template>
<template v-slot:item.action="{ item }">
<v-btn class="float-right" icon="$menu" variant="plain">
<v-icon icon="$menu"></v-icon>
<v-menu activator="parent" close-on-content-click>
<v-list density="compact">
<v-list-item prepend-icon="$edit" :to="{name: 'ModelEditPage', params: {model: model, id: item.id}}"
v-if="!genericModel.model.disableCreate && !genericModel.model.disableUpdate && !genericModel.model.disableDelete">
v-if="!(genericModel.model.disableCreate && genericModel.model.disableUpdate && genericModel.model.disableDelete)">
{{ $t('Edit') }}
</v-list-item>
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" v-if="genericModel.model.isMerge" link>
@@ -111,6 +121,11 @@
<v-list-item prepend-icon="fa-solid fa-rotate" v-if="genericModel.model.name == 'RecipeImport'" @click="importRecipe(item)">
{{ $t('Import') }}
</v-list-item>
<v-list-item prepend-icon="fa-solid fa-arrow-right-from-bracket"
v-if="genericModel.model.name == 'Space' && item.createdBy.id != useUserPreferenceStore().userSettings.user.id!"
@click="leaveSpace(item)">
{{ $t('LeaveSpace') }}
</v-list-item>
</v-list>
</v-menu>
</v-btn>
@@ -144,7 +159,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelMergeDialog from "@/components/dialogs/ModelMergeDialog.vue";
import {VDataTableUpdateOptions} from "@/vuetify";
import SyncDialog from "@/components/dialogs/SyncDialog.vue";
import {ApiApi, ApiRecipeListRequest, RecipeImport} from "@/openapi";
import {ApiApi, ApiRecipeListRequest, Group, RecipeImport, Space, UserSpace} from "@/openapi";
import {useTitle} from "@vueuse/core";
import RecipeShareDialog from "@/components/dialogs/RecipeShareDialog.vue";
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
@@ -261,6 +276,26 @@ function importAllRecipes() {
})
}
/**
* leave the selected space as a user
* @param space to leave
*/
function leaveSpace(space: Space) {
let api = new ApiApi()
useUserPreferenceStore().userSpaces.forEach((us: UserSpace) => {
if (us.space == space.id!) {
loading.value = true
api.apiUserSpaceDestroy({id: us.id!}).then(r => {
}).catch(err => {
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
}).finally(() => {
loading.value = false
})
}
})
}
</script>
<style scoped>

View File

@@ -141,14 +141,14 @@
<div v-if="importType == 'ai'">
<v-row>
<v-col md="6">
<ModelSelect model="AiProvider" v-model="selectedAiProvider">
<v-col cols="12" md="6">
<ModelSelect model="AiProvider" v-model="selectedAiProvider" hide-details>
<template #append>
<v-btn icon="$settings" :to="{name:'ModelListPage', params: {model: 'AiProvider'}}" color="success"></v-btn>
</template>
</ModelSelect>
</v-col>
<v-col md="6">
<v-col cols="12" md="6">
<v-btn-toggle class="mb-2" border divided v-model="aiMode">
<v-btn value="file">{{ $t('File') }}</v-btn>
<v-btn value="text">{{ $t('Text') }}</v-btn>
@@ -250,7 +250,7 @@
<v-row>
<v-col>
<model-select model="Keyword" v-model="keywordSelect">
<model-select model="Keyword" v-model="keywordSelect" allow-create>
<template #append>
<v-btn icon="$add" color="success"
@click="keywordSelect.importKeyword = true; importResponse.recipe.keywords.push(keywordSelect); keywordSelect= null"
@@ -434,6 +434,11 @@
<v-checkbox v-model="appImportDuplicates"></v-checkbox>
</template>
</v-alert>
<div v-if="importApp == 'MEALIE1'">
<v-checkbox v-model="appImportMealPlans" :label="$t('ImportMealPlans')" hide-details></v-checkbox>
<v-checkbox v-model="appImportShoppingLists" :label="$t('ImportShoppingList')" hide-details></v-checkbox>
<v-checkbox v-model="appImportNutritionsPerServing" :label="$t('NutritionsPerServing')" :hint="$t('NutritionsPerServingHelp')" persistent-hint></v-checkbox>
</div>
<v-stepper-actions>
<template #prev>
@@ -658,6 +663,9 @@ const urlListImportedRecipes = ref([] as Recipe[])
const sourceImportText = ref("")
const appImportFiles = ref<File[]>([])
const appImportDuplicates = ref(false)
const appImportMealPlans = ref(true)
const appImportShoppingLists = ref(true)
const appImportNutritionsPerServing = ref(false)
const appImportLog = ref<null | ImportLog>(null)
const image = ref<null | File>(null)
const aiMode = ref<'file' | 'text'>('file')
@@ -770,7 +778,7 @@ function loadRecipeFromAiImport() {
}
function appImport() {
doAppImport(appImportFiles.value, importApp.value, appImportDuplicates.value).then(r => {
doAppImport(appImportFiles.value, importApp.value, appImportDuplicates.value, appImportMealPlans.value, appImportShoppingLists.value, appImportNutritionsPerServing.value).then(r => {
stepper.value = 'import_log'
recLoadImportLog(r)
})

View File

@@ -13,9 +13,7 @@
<v-list-item :to="{name: 'SearchSettings'}" prepend-icon="$search">{{ $t('Search') }}</v-list-item>
<v-divider></v-divider>
<v-list-subheader>Space</v-list-subheader>
<v-list-item :to="{name: 'UserSpaceSettings'}" prepend-icon="$spaces">{{ $t('YourSpaces') }}</v-list-item>
<v-list-item :to="{name: 'SpaceSettings'}" prepend-icon="$settings">{{ $t('SpaceSettings') }}</v-list-item>
<v-list-item :to="{name: 'SpaceMemberSettings'}" prepend-icon="fa-solid fa-users">{{ $t('SpaceMembers') }}</v-list-item>
<v-list-item :to="{name: 'OpenDataImportSettings'}" prepend-icon="fa-solid fa-cloud-arrow-down">{{ $t('Open_Data_Import') }}</v-list-item>
<v-list-item :to="{name: 'ExportDataSettings'}" prepend-icon="fa-solid fa-file-export">{{ $t('Export') }}</v-list-item>
<v-divider></v-divider>

View File

@@ -3,29 +3,53 @@
<horizontal-meal-plan-window v-if="useUserPreferenceStore().deviceSettings.start_showMealPlan"></horizontal-meal-plan-window>
<v-card v-if="totalRecipes == 0" class="mt-5 mb-5">
<v-card-title><i class="fa-solid fa-eye-slash"></i> {{ $t('search_no_recipes') }}</v-card-title>
<v-card-title class="text-center"><i class="fa-solid fa-eye-slash"></i> {{ $t('search_no_recipes') }}</v-card-title>
<v-card-text>
<v-btn-group divided>
<v-btn size="large" color="success" prepend-icon="$create" :to="{ name: 'ModelEditPage', params: {model: 'recipe'} }">{{ $t('Create Recipe') }}</v-btn>
<v-btn size="large" color="primary" prepend-icon="fa-solid fa-globe" :to="{ name: 'RecipeImportPage', params: {} }">{{ $t('Import Recipe') }}</v-btn>
</v-btn-group>
<v-card
:title="$t('Create Recipe')"
variant="outlined"
:to="{name: 'ModelEditPage', params: {model: 'Recipe'}}"
prepend-icon="$recipes"
append-icon="fa-solid fa-arrow-right"
class="mb-4">
<template #subtitle>
<p class="text-wrap">
{{ $t('CreateFirstRecipe') }}
</p>
</template>
</v-card>
<v-card
:title="$t('Import')"
variant="outlined"
:to="{name: 'RecipeImportPage', params: {}}"
prepend-icon="$import"
append-icon="fa-solid fa-arrow-right">
<template #subtitle>
<p class="text-wrap">
{{ $t('ImportFirstRecipe') }}
</p>
</template>
</v-card>
</v-card-text>
</v-card>
<template v-if="totalRecipes > 0">
<horizontal-recipe-scroller :skeletons="4" mode="recent" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="new"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="keyword" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="random" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="created_by" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="2" mode="rating" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="keyword" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="random" v-if="totalRecipes > 25"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="recent"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="new"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="keyword"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="random"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="created_by"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="2" mode="rating"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="keyword"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="random"></horizontal-recipe-scroller>
<v-row>
<v-col class="text-center">
<v-btn size="x-large" rounded="xl" prepend-icon="$search" variant="tonal" :to="{name: 'SearchPage', params: {query: ''}}">{{ $t('View_Recipes') }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col class="text-center">
<v-btn size="x-large" rounded="xl" prepend-icon="$search" variant="tonal" :to="{name: 'SearchPage', params: {query: ''}}">{{ $t('View_Recipes') }}</v-btn>
</v-col>
</v-row>
</template>
</v-container>
@@ -38,6 +62,7 @@ import HorizontalRecipeScroller from "@/components/display/HorizontalRecipeWindo
import HorizontalMealPlanWindow from "@/components/display/HorizontalMealPlanWindow.vue"
import SearchPage from "@/pages/SearchPage.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useRouter} from "vue-router";
const totalRecipes = ref(-1)

View File

@@ -0,0 +1,255 @@
<template>
<v-container>
<v-stepper editable v-model="stepper">
<v-stepper-header>
<v-stepper-item :title="$t('Settings')" value="1"></v-stepper-item>
<v-divider></v-divider>
<v-stepper-item :title="$t('Open Data')" value="2"></v-stepper-item>
<v-divider></v-divider>
<v-stepper-item :title="$t('Invites')" value="3"></v-stepper-item>
<v-divider></v-divider>
<v-stepper-item :title="$t('GettingStarted')" value="4"></v-stepper-item>
</v-stepper-header>
<v-stepper-window>
<v-stepper-window-item value="1">
<v-card flat>
<v-card-title class="text-h4">{{ $t('WelcometoTandoor') }} <span class="text-tandoor">{{useUserPreferenceStore().userSettings.user.displayName}}</span></v-card-title>
<v-card-text v-if="space">
<p class="text-subtitle-1 mb-4">{{ $t('WelcomeSettingsHelp') }}</p>
<v-text-field v-model="space.name" :label="$t('Name')"></v-text-field>
<v-select :label="$t('Theme')" v-model="useUserPreferenceStore().userSettings.theme"
:items="[{title: 'Tandoor', value: 'TANDOOR'}, {title: 'Tandoor Dark', value: 'TANDOOR_DARK'}, ]">
</v-select>
<v-text-field v-model="useUserPreferenceStore().userSettings.defaultUnit" :label="$t('Default_Unit')"></v-text-field>
<v-checkbox :label="$t('Use_Fractions')" :hint="$t('Use_Fractions_Help')" persistent-hint v-model="useUserPreferenceStore().userSettings.useFractions"></v-checkbox>
</v-card-text>
</v-card>
<v-stepper-actions>
<template #prev>
<v-spacer></v-spacer>
</template>
<template #next>
<v-btn @click="finishWelcome()" color="warning" class="me-2" :loading="loading">{{ $t('Skip') }}</v-btn>
<v-btn @click="updateSpaceAndUserSettings()" :loading="loading" color="success">{{ $t('Next') }}</v-btn>
</template>
</v-stepper-actions>
</v-stepper-window-item>
<v-stepper-window-item value="2">
<v-card flat>
<v-card-text>
<open-data-import-settings></open-data-import-settings>
</v-card-text>
</v-card>
<v-stepper-actions>
<template #prev>
<v-btn @click="stepper = '1'">{{ $t('Back') }}</v-btn>
</template>
<template #next>
<v-btn @click="stepper = '3'" color="success">{{ $t('Next') }}</v-btn>
</template>
</v-stepper-actions>
</v-stepper-window-item>
<v-stepper-window-item value="3">
<v-card flat>
<v-card-text class="text-center">
<v-card variant="outlined">
<v-card-title class="text-h4 pb-0 mb-0 text-center">{{ $t('Space') }}</v-card-title>
<v-card-subtitle class="text-subtitle-1 text-center mb-4">{{ $t('SpaceHelp') }}</v-card-subtitle>
<v-card-text>
<v-row>
<v-col class="text-center" v-for="model in [TRecipe, TFood, TUnit, TSupermarket, TKeyword]">
<v-icon :icon="model.icon" size="x-large"></v-icon>
<p class="text-h6">{{ $t(model.localizationKey) }}</p>
</v-col>
<v-col class="text-center">
<v-icon icon="fa-solid fa-ellipsis" size="x-large"></v-icon>
<p class="text-h6">{{ $t('More') }}</p>
</v-col>
</v-row>
<div class="border-md border-opacity-75 border-dotted rounded mt-5 w-md-75 ml-auto mr-auto">
<v-card-subtitle class="text-subtitle-1 text-center mb-4 mt-2 text-wrap">
{{ $t('SpacePrivateObjectsHelp') }}
</v-card-subtitle>
<v-row>
<v-col class="text-center" v-for="model in [TMealPlan, TShoppingListEntry, TRecipeBook]">
<v-icon :icon="model.icon" size="x-large"></v-icon>
<p class="text-h6">{{ $t(model.localizationKey) }}</p>
</v-col>
</v-row>
</div>
</v-card-text>
</v-card>
<v-btn size="x-large" class="mt-4" variant="outlined">{{ $t('CreateInvitation') }}
<model-edit-dialog model="InviteLink" :close-after-create="false" :close-after-save="false"></model-edit-dialog>
</v-btn>
</v-card-text>
</v-card>
<v-stepper-actions>
<template #prev>
<v-btn @click="stepper = '2'" color="success">{{ $t('Back') }}</v-btn>
</template>
<template #next>
<v-btn @click="stepper = '4'" color="success">{{ $t('Next') }}</v-btn>
</template>
</v-stepper-actions>
</v-stepper-window-item>
<v-stepper-window-item value="4">
<v-card flat>
<v-card-text>
<v-card
:title="$t('Create Recipe')"
variant="outlined"
@click="finishWelcome({name: 'ModelEditPage', params: {model: 'Recipe'}})"
prepend-icon="$recipes"
append-icon="fa-solid fa-arrow-right"
class="mb-4">
<template #subtitle>
<p class="text-wrap">
{{ $t('CreateFirstRecipe') }}
</p>
</template>
</v-card>
<v-card
:title="$t('Import')"
variant="outlined"
@click="finishWelcome({name: 'RecipeImportPage', params: {}})"
prepend-icon="$import"
append-icon="fa-solid fa-arrow-right">
<template #subtitle>
<p class="text-wrap">
{{ $t('ImportFirstRecipe') }}
</p>
</template>
</v-card>
</v-card-text>
</v-card>
<v-stepper-actions>
<template #prev>
<v-btn @click="stepper = '2'" color="success">{{ $t('Back') }}</v-btn>
</template>
<template #next>
<v-btn @click="finishWelcome()" color="success" :disabled="false">{{ $t('Finish') }}</v-btn>
</template>
</v-stepper-actions>
</v-stepper-window-item>
</v-stepper-window>
</v-stepper>
</v-container>
</template>
<script setup lang="ts">
import {ApiApi, Space} from "@/openapi";
import {onMounted, ref} from "vue";
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import OpenDataImportSettings from "@/components/settings/OpenDataImportSettings.vue";
import {TFood, TKeyword, TMealPlan, TRecipe, TRecipeBook, TShoppingListEntry, TSupermarket, TUnit} from "@/types/Models.ts";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import {RouteLocationRaw, useRouter} from "vue-router";
const router = useRouter()
const space = ref<undefined | Space>(undefined)
const stepper = ref("1")
const loading = ref(false)
onMounted(() => {
loadSpace()
})
/**
* save setup completion and redirect to target page
* @param target
*/
function finishWelcome(target: RouteLocationRaw = {name: 'StartPage'}) {
if (space.value) {
space.value.spaceSetupCompleted = true
loading.value = true
updateSpace().then(() => {
router.push(target)
loading.value = false
})
} else {
useMessageStore().addMessage(MessageType.ERROR, "Space not loaded yet", 5000)
}
}
/**
* load active space data
*/
function loadSpace() {
let api = new ApiApi()
api.apiSpaceCurrentRetrieve().then(r => {
space.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
/**
* update both the space and user settings
*/
function updateSpaceAndUserSettings() {
let promises = [] as Promise<any>[]
loading.value = true
promises.push(updateSpace())
promises.push(useUserPreferenceStore().updateUserSettings(true))
Promise.allSettled(promises).then(r => {
loading.value = false
stepper.value = "2"
})
}
/**
* update space in database
*/
function updateSpace() {
let api = new ApiApi()
return api.apiSpacePartialUpdate({id: space.value.id, patchedSpace: space.value}).then(r => {
space.value = r
useUserPreferenceStore().activeSpace = Object.assign({}, space.value)
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
}
</script>
<style scoped>
</style>

View File

@@ -67,7 +67,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
function loadUserSettings() {
console.log('loading user settings from DB')
let api = new ApiApi()
api.apiUserPreferenceList().then(r => {
return api.apiUserPreferenceList().then(r => {
if (r.length == 1) {
userSettings.value = r[0]
isAuthenticated.value = true
@@ -85,13 +85,15 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
/**
* persist changes to user settings to DB
*/
function updateUserSettings() {
function updateUserSettings(silent: boolean = false) {
let api = new ApiApi()
api.apiUserPreferencePartialUpdate({user: userSettings.value.user.id!, patchedUserPreference: userSettings.value}).then(r => {
return api.apiUserPreferencePartialUpdate({user: userSettings.value.user.id!, patchedUserPreference: userSettings.value}).then(r => {
userSettings.value = r
updateTheme()
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS)
if (!silent) {
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS)
}
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
@@ -102,7 +104,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/
function loadServerSettings() {
let api = new ApiApi()
api.apiServerSettingsCurrentRetrieve().then(r => {
return api.apiServerSettingsCurrentRetrieve().then(r => {
serverSettings.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -114,7 +116,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/
function loadActiveSpace() {
let api = new ApiApi()
api.apiSpaceCurrentRetrieve().then(r => {
return api.apiSpaceCurrentRetrieve().then(r => {
activeSpace.value = r
}).catch(err => {
if (err.response.status != 403) {
@@ -128,7 +130,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/
function loadUserSpaces() {
let api = new ApiApi()
api.apiUserSpaceList().then(r => {
return api.apiUserSpaceAllPersonalList().then(r => {
userSpaces.value = r.results
}).catch(err => {
if (err.response.status != 403) {
@@ -144,7 +146,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
function loadSpaces() {
let api = new ApiApi()
api.apiSpaceList().then(r => {
return api.apiSpaceList().then(r => {
spaces.value = r.results
}).catch(err => {
if (err.response.status != 403) {
@@ -160,9 +162,10 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
let api = new ApiApi()
api.apiSwitchActiveSpaceRetrieve({spaceId: space.id!}).then(r => {
loadActiveSpace()
router.push({name: 'StartPage'}).then(() => {
location.reload()
loadActiveSpace().then(() => {
router.push({name: 'StartPage'}).then(() => {
location.reload()
})
})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -221,15 +224,20 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
}
}
// always load settings on first initialization of store
loadUserSettings()
loadServerSettings()
loadActiveSpace()
loadUserSpaces()
loadSpaces()
updateTheme()
function init() {
const promises = [] as Promise<any>[]
promises.push(loadUserSettings())
promises.push(loadServerSettings())
promises.push(loadActiveSpace())
promises.push(loadUserSpaces())
promises.push(loadSpaces())
updateTheme()
return Promise.allSettled(promises)
}
return {
init,
deviceSettings,
userSettings,
serverSettings,

View File

@@ -1,13 +1,13 @@
import {
AccessToken, AiLog, AiProvider,
ApiApi, ApiKeywordMoveUpdateRequest, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter,
Food,
Food, FoodInheritField,
Ingredient,
InviteLink, Keyword,
MealPlan,
MealType,
Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry, Space,
Step,
Supermarket,
SupermarketCategory, Sync, SyncLog,
@@ -101,6 +101,7 @@ export type Model = {
disableCreate?: boolean | undefined,
disableUpdate?: boolean | undefined,
disableDelete?: boolean | undefined,
disableSearch?: boolean | undefined,
// disable showing this model as an option in the ModelListPage
disableListView?: boolean | undefined,
@@ -148,6 +149,8 @@ export type EditorSupportedModels =
| 'SearchFields'
| 'AiProvider'
| 'AiLog'
| 'Space'
| 'FoodInheritField'
// used to type methods/parameters in conjunction with configuration type
export type EditorSupportedTypes =
@@ -184,6 +187,8 @@ export type EditorSupportedTypes =
| SearchFields
| AiProvider
| AiLog
| Space
| FoodInheritField
export const TFood = {
name: 'Food',
@@ -655,7 +660,8 @@ export const TUserSpace = {
disableCreate: true,
tableHeaders: [
{title: 'User', key: 'user'},
{title: 'User', key: 'user.displayName'},
{title: 'Group', key: 'groups'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
@@ -669,19 +675,40 @@ export const TInviteLink = {
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/InviteLinkEditor.vue`)),
disableListView: true,
disableSearch: true,
isPaginated: true,
toStringKeys: ['email', 'role'],
tableHeaders: [
{title: 'Email', key: 'email'},
{title: 'Role', key: 'group'},
{title: 'Role', key: 'group.name'},
{title: 'Valid Until', key: 'validUntil'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TInviteLink)
export const TSpace = {
name: 'Space',
localizationKey: 'Space',
localizationKeyDescription: 'SpaceHelp',
icon: 'fa-solid fa-hard-drive',
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/SpaceEditor.vue`)),
disableDelete: true,
isPaginated: true,
toStringKeys: ['name'],
tableHeaders: [
{title: 'Name', key: 'name'},
{title: 'Owner', key: 'createdBy.displayName'},
{title: 'Active', key: 'active'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TSpace)
export const TStorage = {
name: 'Storage',
localizationKey: 'Storage',

View File

@@ -17,7 +17,8 @@ export const INTEGRATIONS: Array<Integration> = [
{id: 'COOKMATE', name: "Cookmate", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#cookmate'},
{id: 'COPYMETHAT', name: "CopyMeThat", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#copymethat'},
{id: 'DOMESTICA', name: "Domestica", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#domestica'},
{id: 'MEALIE', name: "Mealie", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#mealie'},
{id: 'MEALIE', name: "Mealie 0.x", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#mealie'},
{id: 'MEALIE1', name: "Mealie 1.x", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#mealie'},
{id: 'MEALMASTER', name: "Mealmaster", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#mealmaster'},
{id: 'MELARECIPES', name: "Melarecipes", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#melarecipes'},
{id: 'NEXTCLOUD', name: "Nextcloud Cookbook", import: true, export: true, helpUrl: 'https://docs.tandoor.dev/features/import_export/#nextcloud'},

View File

@@ -959,26 +959,26 @@
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz#8249de9b7e22fcb3ceb5e66090c30a1d5492b81a"
integrity sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==
"@intlify/core-base@11.1.10":
version "11.1.10"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.10.tgz#4731748992bc6d8e723ca6c2cc5aa5a4c90cf7a5"
integrity sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==
"@intlify/core-base@11.1.11":
version "11.1.11"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.11.tgz#e36893a7d37a3a75fae30977fc58c1d8cf3c853f"
integrity sha512-1Z0N8jTfkcD2Luq9HNZt+GmjpFe4/4PpZF3AOzoO1u5PTtSuXZcfhwBatywbfE2ieB/B5QHIoOFmCXY2jqVKEQ==
dependencies:
"@intlify/message-compiler" "11.1.10"
"@intlify/shared" "11.1.10"
"@intlify/message-compiler" "11.1.11"
"@intlify/shared" "11.1.11"
"@intlify/message-compiler@11.1.10":
version "11.1.10"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.10.tgz#ff5c92c311cd72144126f5c128912adb4e911207"
integrity sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==
"@intlify/message-compiler@11.1.11":
version "11.1.11"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.11.tgz#ba10641f86af0e991ac9def0385bd345c8f150fb"
integrity sha512-7PC6neomoc/z7a8JRjPBbu0T2TzR2MQuY5kn2e049MP7+o32Ve7O8husylkA7K9fQRe4iNXZWTPnDJ6vZdtS1Q==
dependencies:
"@intlify/shared" "11.1.10"
"@intlify/shared" "11.1.11"
source-map-js "^1.0.2"
"@intlify/shared@11.1.10":
version "11.1.10"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.10.tgz#d869aa8fbc1aa307f26a58848fea6df3c9785b6f"
integrity sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==
"@intlify/shared@11.1.11":
version "11.1.11"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.11.tgz#6bba3b86617c05767356e4ca939c9e300563a083"
integrity sha512-RIBFTIqxZSsxUqlcyoR7iiC632bq7kkOwYvZlvcVObHfrF4NhuKc4FKvu8iPCrEO+e3XsY7/UVpfgzg+M7ETzA==
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.8"
@@ -1079,105 +1079,110 @@
estree-walker "^2.0.2"
picomatch "^4.0.2"
"@rollup/rollup-android-arm-eabi@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz#a3e4e4b2baf0bade6918cf5135c3ef7eee653196"
integrity sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==
"@rollup/rollup-android-arm-eabi@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz#7d41dc45adcfcb272504ebcea9c8a5b2c659e963"
integrity sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==
"@rollup/rollup-android-arm64@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz#63566b0e76c62d4f96d44448f38a290562280200"
integrity sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==
"@rollup/rollup-android-arm64@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz#6c708fae2c9755e994c42d56c34a94cb77020650"
integrity sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==
"@rollup/rollup-darwin-arm64@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz#60a51a61b22b1f4fdf97b4adf5f0f447f492759d"
integrity sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==
"@rollup/rollup-darwin-arm64@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz#85ccf92ab114e434c83037a175923a525635cbb4"
integrity sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==
"@rollup/rollup-darwin-x64@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz#bfe3059440f7032de11e749ece868cd7f232e609"
integrity sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==
"@rollup/rollup-darwin-x64@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz#0af089f3d658d05573208dabb3a392b44d7f4630"
integrity sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==
"@rollup/rollup-freebsd-arm64@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz#d5d4c6cd3b8acb7493b76227d8b2b4a2d732a37b"
integrity sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==
"@rollup/rollup-freebsd-arm64@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz#46c22a16d18180e99686647543335567221caa9c"
integrity sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==
"@rollup/rollup-freebsd-x64@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz#cb4e1547b572cd0144c5fbd6c4a0edfed5fe6024"
integrity sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==
"@rollup/rollup-freebsd-x64@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz#819ffef2f81891c266456952962a13110c8e28b5"
integrity sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==
"@rollup/rollup-linux-arm-gnueabihf@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz#feb81bd086f6a469777f75bec07e1bdf93352e69"
integrity sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==
"@rollup/rollup-linux-arm-gnueabihf@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz#7fe283c14793e607e653a3214b09f8973f08262a"
integrity sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==
"@rollup/rollup-linux-arm-musleabihf@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz#68bff1c6620c155c9d8f5ee6a83c46eb50486f18"
integrity sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==
"@rollup/rollup-linux-arm-musleabihf@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz#066e92eb22ea30560414ec800a6d119ba0b435ac"
integrity sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==
"@rollup/rollup-linux-arm64-gnu@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz#dbc5036a85e3ca3349887c8bdbebcfd011e460b0"
integrity sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==
"@rollup/rollup-linux-arm64-gnu@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz#480d518ea99a8d97b2a174c46cd55164f138cc37"
integrity sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==
"@rollup/rollup-linux-arm64-musl@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz#72efc633aa0b93531bdfc69d70bcafa88e6152fc"
integrity sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==
"@rollup/rollup-linux-arm64-musl@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz#ed7db3b8999b60dd20009ddf71c95f3af49423c8"
integrity sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==
"@rollup/rollup-linux-loongarch64-gnu@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz#9b6a49afde86c8f57ca11efdf8fd8d7c52048817"
integrity sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==
"@rollup/rollup-linux-loongarch64-gnu@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz#16a6927a35f5dbc505ff874a4e1459610c0c6f46"
integrity sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==
"@rollup/rollup-linux-powerpc64le-gnu@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz#93cb96073efab0cdbf419c8dfc44b5e2bd815139"
integrity sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==
"@rollup/rollup-linux-ppc64-gnu@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz#a006700469be0041846c45b494c35754e6a04eea"
integrity sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==
"@rollup/rollup-linux-riscv64-gnu@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz#028708f73c8130ae924e5c3755de50fe93687249"
integrity sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==
"@rollup/rollup-linux-riscv64-gnu@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz#0fcc45b2ec8a0e54218ca48849ea6d596f53649c"
integrity sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==
"@rollup/rollup-linux-riscv64-musl@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz#878bfb158b2cf6671b7611fd58e5c80d9144ac6c"
integrity sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==
"@rollup/rollup-linux-riscv64-musl@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz#d6e617eec9fe6f5859ee13fad435a16c42b469f2"
integrity sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==
"@rollup/rollup-linux-s390x-gnu@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz#59b4ebb2129d34b7807ed8c462ff0baaefca9ad4"
integrity sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==
"@rollup/rollup-linux-s390x-gnu@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz#b147760d63c6f35b4b18e6a25a2a760dd3ea0c05"
integrity sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==
"@rollup/rollup-linux-x64-gnu@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz#597d40f60d4b15bedbbacf2491a69c5b67a58e93"
integrity sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==
"@rollup/rollup-linux-x64-gnu@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz#fc0be1da374f85e7e85dccaf1ff12d7cfc9fbe3d"
integrity sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==
"@rollup/rollup-linux-x64-musl@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz#0a062d6fee35ec4fbb607b2a9d933a9372ccf63a"
integrity sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==
"@rollup/rollup-linux-x64-musl@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz#54c79932e0f9a3c992b034c82325be3bcde0d067"
integrity sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==
"@rollup/rollup-win32-arm64-msvc@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz#41ffab489857987c75385b0fc8cccf97f7e69d0a"
integrity sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==
"@rollup/rollup-openharmony-arm64@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz#fc48e74d413623ac02c1d521bec3e5e784488fdc"
integrity sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==
"@rollup/rollup-win32-ia32-msvc@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz#d9fb61d98eedfa52720b6ed9f31442b3ef4b839f"
integrity sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==
"@rollup/rollup-win32-arm64-msvc@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz#8ce3d1181644406362cf1e62c90e88ab083e02bb"
integrity sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==
"@rollup/rollup-win32-x64-msvc@4.44.0":
version "4.44.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz#a36e79b6ccece1533f777a1bca1f89c13f0c5f62"
integrity sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==
"@rollup/rollup-win32-ia32-msvc@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz#dd2dfc896eac4b2689d55f01c6d51c249263f805"
integrity sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==
"@rollup/rollup-win32-x64-msvc@4.50.1":
version "4.50.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz#13f758c97b9fbbac56b6928547a3ff384e7cfb3e"
integrity sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
@@ -1257,24 +1262,24 @@
dependencies:
"@rolldown/pluginutils" "1.0.0-beta.19"
"@volar/language-core@2.4.14", "@volar/language-core@~2.4.11":
version "2.4.14"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.14.tgz#dac7573014d4f3bafb186cb16888ffea5698be71"
integrity sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==
"@volar/language-core@2.4.23":
version "2.4.23"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.23.tgz#deb6dbc5fdbafa9bb7ba691fc59cb196cdb856d3"
integrity sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==
dependencies:
"@volar/source-map" "2.4.14"
"@volar/source-map" "2.4.23"
"@volar/source-map@2.4.14":
version "2.4.14"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.14.tgz#cdcecd533c2e767449b2414cc22327d2bda7ef95"
integrity sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==
"@volar/source-map@2.4.23":
version "2.4.23"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.23.tgz#d476e11a3a669d89858a5eb38b02342be39b0e44"
integrity sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==
"@volar/typescript@~2.4.11":
version "2.4.14"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.14.tgz#b99a1025dd6a8b751e96627ebcb0739ceed0e5f1"
integrity sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==
"@volar/typescript@2.4.23":
version "2.4.23"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.23.tgz#b9b114ea01ad0ad977139edda0239fdafdb21ad7"
integrity sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==
dependencies:
"@volar/language-core" "2.4.14"
"@volar/language-core" "2.4.23"
path-browserify "^1.0.1"
vscode-uri "^3.0.8"
@@ -1360,19 +1365,19 @@
dependencies:
rfdc "^1.4.1"
"@vue/language-core@2.2.10":
version "2.2.10"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.2.10.tgz#5ae1e71a4e16dd59d1e4bac167f4b9c8c04d9f17"
integrity sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==
"@vue/language-core@3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-3.0.6.tgz#5e9d2f58f3a91465c5d86e460f0239f9c6e8300d"
integrity sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A==
dependencies:
"@volar/language-core" "~2.4.11"
"@volar/language-core" "2.4.23"
"@vue/compiler-dom" "^3.5.0"
"@vue/compiler-vue2" "^2.7.16"
"@vue/shared" "^3.5.0"
alien-signals "^1.0.3"
minimatch "^9.0.3"
alien-signals "^2.0.5"
muggle-string "^0.4.1"
path-browserify "^1.0.1"
picomatch "^4.0.2"
"@vue/reactivity@3.5.17":
version "3.5.17"
@@ -1475,10 +1480,10 @@ ajv@^8.6.0:
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
alien-signals@^1.0.3:
version "1.0.13"
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-1.0.13.tgz#8d6db73462f742ee6b89671fbd8c37d0b1727a7e"
integrity sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==
alien-signals@^2.0.5:
version "2.0.7"
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-2.0.7.tgz#8c695e01878081046f1486e7e332380db35fb7e6"
integrity sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==
ansi-styles@^4.1.0:
version "4.3.0"
@@ -1977,10 +1982,10 @@ fast-uri@^3.0.1:
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748"
integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==
fdir@^6.4.4:
version "6.4.6"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.6.tgz#2b268c0232697063111bbf3f64810a2a741ba281"
integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==
fdir@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
filelist@^1.0.4:
version "1.0.4"
@@ -2583,13 +2588,6 @@ minimatch@^5.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.3:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
@@ -2695,10 +2693,10 @@ picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
picomatch@^4.0.2, picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
pinia@^3.0.2:
version "3.0.3"
@@ -2712,7 +2710,7 @@ possible-typed-array-names@^1.0.0:
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
postcss@^8.5.3, postcss@^8.5.6:
postcss@^8.5.6:
version "8.5.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
@@ -2831,33 +2829,34 @@ rollup@^2.43.1:
optionalDependencies:
fsevents "~2.3.2"
rollup@^4.34.9:
version "4.44.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.44.0.tgz#0e10b98339b306edad1e612f1e5590a79aef521c"
integrity sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==
rollup@^4.43.0:
version "4.50.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.50.1.tgz#6f0717c34aacc65cc727eeaaaccc2afc4e4485fd"
integrity sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==
dependencies:
"@types/estree" "1.0.8"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.44.0"
"@rollup/rollup-android-arm64" "4.44.0"
"@rollup/rollup-darwin-arm64" "4.44.0"
"@rollup/rollup-darwin-x64" "4.44.0"
"@rollup/rollup-freebsd-arm64" "4.44.0"
"@rollup/rollup-freebsd-x64" "4.44.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.44.0"
"@rollup/rollup-linux-arm-musleabihf" "4.44.0"
"@rollup/rollup-linux-arm64-gnu" "4.44.0"
"@rollup/rollup-linux-arm64-musl" "4.44.0"
"@rollup/rollup-linux-loongarch64-gnu" "4.44.0"
"@rollup/rollup-linux-powerpc64le-gnu" "4.44.0"
"@rollup/rollup-linux-riscv64-gnu" "4.44.0"
"@rollup/rollup-linux-riscv64-musl" "4.44.0"
"@rollup/rollup-linux-s390x-gnu" "4.44.0"
"@rollup/rollup-linux-x64-gnu" "4.44.0"
"@rollup/rollup-linux-x64-musl" "4.44.0"
"@rollup/rollup-win32-arm64-msvc" "4.44.0"
"@rollup/rollup-win32-ia32-msvc" "4.44.0"
"@rollup/rollup-win32-x64-msvc" "4.44.0"
"@rollup/rollup-android-arm-eabi" "4.50.1"
"@rollup/rollup-android-arm64" "4.50.1"
"@rollup/rollup-darwin-arm64" "4.50.1"
"@rollup/rollup-darwin-x64" "4.50.1"
"@rollup/rollup-freebsd-arm64" "4.50.1"
"@rollup/rollup-freebsd-x64" "4.50.1"
"@rollup/rollup-linux-arm-gnueabihf" "4.50.1"
"@rollup/rollup-linux-arm-musleabihf" "4.50.1"
"@rollup/rollup-linux-arm64-gnu" "4.50.1"
"@rollup/rollup-linux-arm64-musl" "4.50.1"
"@rollup/rollup-linux-loongarch64-gnu" "4.50.1"
"@rollup/rollup-linux-ppc64-gnu" "4.50.1"
"@rollup/rollup-linux-riscv64-gnu" "4.50.1"
"@rollup/rollup-linux-riscv64-musl" "4.50.1"
"@rollup/rollup-linux-s390x-gnu" "4.50.1"
"@rollup/rollup-linux-x64-gnu" "4.50.1"
"@rollup/rollup-linux-x64-musl" "4.50.1"
"@rollup/rollup-openharmony-arm64" "4.50.1"
"@rollup/rollup-win32-arm64-msvc" "4.50.1"
"@rollup/rollup-win32-ia32-msvc" "4.50.1"
"@rollup/rollup-win32-x64-msvc" "4.50.1"
fsevents "~2.3.2"
rrweb-cssom@^0.8.0:
@@ -3160,13 +3159,13 @@ terser@^5.17.4:
commander "^2.20.0"
source-map-support "~0.5.20"
tinyglobby@^0.2.10, tinyglobby@^0.2.13:
version "0.2.14"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"
integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==
tinyglobby@^0.2.10, tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.4.4"
picomatch "^4.0.2"
fdir "^6.5.0"
picomatch "^4.0.3"
tldts-core@^6.1.86:
version "6.1.86"
@@ -3324,10 +3323,10 @@ update-browserslist-db@^1.1.3:
escalade "^3.2.0"
picocolors "^1.1.1"
vite-plugin-pwa@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-1.0.2.tgz#ad70181256633c56ce7aa85c66377d916b9e8296"
integrity sha512-O3UwjsCnoDclgJANoOgzzqW7SFgwXE/th2OmUP/ILxHKwzWxxKDBu+B/Xa9Cv4IgSVSnj2HgRVIJ7F15+vQFkA==
vite-plugin-pwa@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-1.0.3.tgz#607a7fda7170920280d85b1d2dbac293c3d2efdf"
integrity sha512-/OpqIpUldALGxcsEnv/ekQiQ5xHkQ53wcoN5ewX4jiIDNGs3W+eNcI1WYZeyOLmzoEjg09D7aX0O89YGjen1aw==
dependencies:
debug "^4.3.6"
pretty-bytes "^6.1.1"
@@ -3344,17 +3343,17 @@ vite-plugin-vuetify@^2.1.1:
debug "^4.3.3"
upath "^2.0.1"
vite@6.3.5:
version "6.3.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.5.tgz#fec73879013c9c0128c8d284504c6d19410d12a3"
integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==
vite@7.1.5:
version "7.1.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.5.tgz#4dbcb48c6313116689be540466fc80faa377be38"
integrity sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==
dependencies:
esbuild "^0.25.0"
fdir "^6.4.4"
picomatch "^4.0.2"
postcss "^8.5.3"
rollup "^4.34.9"
tinyglobby "^0.2.13"
fdir "^6.5.0"
picomatch "^4.0.3"
postcss "^8.5.6"
rollup "^4.43.0"
tinyglobby "^0.2.15"
optionalDependencies:
fsevents "~2.3.3"
@@ -3370,13 +3369,13 @@ vue-draggable-plus@^0.6.0:
dependencies:
"@types/sortablejs" "^1.15.8"
vue-i18n@^11.1.10:
version "11.1.10"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.10.tgz#04578ea4213f96c37939a08f516a648a6d0b84a1"
integrity sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==
vue-i18n@^11.1.11:
version "11.1.11"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.11.tgz#b38ed214896540cf7a68932dfa565d9d4fbbffac"
integrity sha512-LvyteQoXeQiuILbzqv13LbyBna/TEv2Ha+4ZWK2AwGHUzZ8+IBaZS0TJkCgn5izSPLcgZwXy9yyTrewCb2u/MA==
dependencies:
"@intlify/core-base" "11.1.10"
"@intlify/shared" "11.1.10"
"@intlify/core-base" "11.1.11"
"@intlify/shared" "11.1.11"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.5.0:
@@ -3393,13 +3392,13 @@ vue-simple-calendar@7.1.0:
dependencies:
vue "^3.4.15"
vue-tsc@^2.2.8:
version "2.2.10"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.2.10.tgz#7b51a666cb90788884efd0caedc69fc1fc9c5b78"
integrity sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==
vue-tsc@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-3.0.6.tgz#09376812d2f1fab64e3592ab93281dc62e6d2754"
integrity sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg==
dependencies:
"@volar/typescript" "~2.4.11"
"@vue/language-core" "2.2.10"
"@volar/typescript" "2.4.23"
"@vue/language-core" "3.0.6"
vue@^3.4.15, vue@^3.5.13:
version "3.5.17"
@@ -3419,10 +3418,10 @@ vuedraggable@^4.1.0:
dependencies:
sortablejs "1.14.0"
vuetify@^3.9.3:
version "3.9.3"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.9.3.tgz#b97dab322132e82fb247c469f674d5610bd19396"
integrity sha512-0eruHdmRoAMBo/08RLDkTdtdu1vfkx+/PurUIDW2tz/k2GCp51e7KwgCn4uVyzH88KRgf2PKiz5UI5f93Xn05w==
vuetify@^3.9.7:
version "3.9.7"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.9.7.tgz#aea996f35111f25dd7e31ab956fbb40911841c24"
integrity sha512-Ib8PB3ItcguCol8f0DXLpoGyy7FvoOYW23SEWqXX+in1CSItJZHxUXXGSus94m5JWqYqQrFiwCykbHm7UWPi4Q==
w3c-xmlserializer@^5.0.0:
version "5.0.0"