Compare commits

..

67 Commits
2.0.3 ... 2.2.0

Author SHA1 Message Date
vabene1111
ed8f97e9e0 Merge branch 'develop'
# Conflicts:
#	vue3/src/locales/de.json
2025-09-10 20:36:33 +02:00
vabene1111
034f68fc28 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-09-10 20:34:55 +02:00
vabene1111
0158087a0b fixed ai test 2025-09-10 20:34:51 +02:00
vabene1111
cb6bfd741d Merge pull request #4023 from TandoorRecipes/dependabot/pip/django-4.2.24
Bump django from 4.2.22 to 4.2.24
2025-09-10 20:34:21 +02:00
vabene1111
afeee5f7cb fixed link for subpath setups 2025-09-10 20:16:22 +02:00
vabene1111
b43d6e08d4 food batch editor implementation 2025-09-10 16:52:35 +02:00
vabene1111
1188624376 food batch update dialog and first api functions 2025-09-10 07:54:42 +02:00
dependabot[bot]
9ac837c969 Bump django from 4.2.22 to 4.2.24
Bumps [django](https://github.com/django/django) from 4.2.22 to 4.2.24.
- [Commits](https://github.com/django/django/compare/4.2.22...4.2.24)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 4.2.24
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 02:19:20 +00:00
vabene1111
fc4b017d30 food batch update endpoint 2025-09-09 16:54:29 +02:00
vabene1111
4636ac28f9 ai system improvements 2025-09-09 16:30:54 +02:00
vabene1111
397912e87f WIP AI system 2025-09-09 14:58:32 +02:00
vabene1111
d0b860e623 shorter create message for model select 2025-09-09 13:36:05 +02:00
vabene1111
8a90ed1274 fixed ingredient parser error 2025-09-09 13:33:18 +02:00
vabene1111
163c2a53b6 fixed space overview 2025-09-09 07:55:31 +02:00
vabene1111
286d707347 lots of AI provider stuff 2025-09-09 07:54:59 +02:00
vabene1111
98d308aee9 fixed space overview 2025-09-09 07:54:45 +02:00
vabene1111
a7c5240227 ai provider system 2025-09-08 22:15:57 +02:00
vabene1111
75fcff8e70 font show sponsor link on hosted edition 2025-09-08 21:15:20 +02:00
vabene1111
2f27cf4deb fixed meal plan loading 2025-09-08 21:13:33 +02:00
vabene1111
686b595f45 fixed demo auto login 2025-09-08 20:55:20 +02:00
vabene1111
0f9f9e8f7c Merge pull request #4013 from tomtjes/fix-logo-font
Make logo consistent across end user systems
2025-09-08 20:15:19 +02:00
vabene1111
aba45657c3 fixed vite config 2025-09-08 08:38:53 +02:00
vabene1111
e6abdf8cd4 fixed vite config 2025-09-08 08:38:46 +02:00
vabene1111
6cedde7b2d plugin and hosted fixes
# Conflicts:
#	vue3/src/locales/de.json
2025-09-08 08:33:05 +02:00
vabene1111
741e9eb370 plugin and hosted fixes 2025-09-08 08:31:01 +02:00
tomtjes
7db523d8c4 convert text to path 2025-09-07 19:34:11 +00:00
vabene1111
41f0060c43 basics of AI provider system 2025-09-05 21:36:43 +02:00
vabene1111
5572833f64 dont display 0 amount in ingredient table 2025-09-05 18:02:09 +02:00
vabene1111
780e441a3b fixed migration tree 2025-09-05 17:50:02 +02:00
vabene1111
c4fd2d0b4e fixed timer localization 2025-09-05 17:46:18 +02:00
vabene1111
1c6618f452 Merge pull request #3999 from icedieler/patch-1
Update nginx configuration for manual setup
2025-09-05 17:30:00 +02:00
vabene1111
8c96a75a1e basics of ai database 2025-09-05 14:50:10 +02:00
vabene1111
44baa8322c Merge branch 'develop' 2025-09-04 22:24:18 +02:00
vabene1111
0fbb95438a added auto meal planner back 2025-09-04 22:23:50 +02:00
vabene1111
c56dd9563c fixed accidentally closing meal plan dialog when opened from recipe context menu 2025-09-04 21:41:51 +02:00
vabene1111
0008b7c975 fixed servings scaler missing on mobile 2025-09-04 21:38:21 +02:00
vabene1111
524f086cc5 added merged steps overview 2025-09-04 21:35:28 +02:00
vabene1111
8550387e0c added ability to delete external recipe file 2025-09-04 21:09:34 +02:00
vabene1111
1618f8df79 fixed meal plan data loading 2025-09-04 20:51:54 +02:00
vabene1111
22dfb2a410 Merge pull request #3998 from Valinor/WEBP-Support
Support WEBP format in image processing #3997
2025-09-04 20:49:21 +02:00
Matthias Lange
f099e2e5d3 Update nginx configuration for manual setup
Since v2 tandoor added user session tracking which requires the reverse proxy to add an extra header.

This change adds the `X-Forwarded-For` header to the example nginx configuration. This header fixes the issue described in #3943.
2025-09-03 10:22:49 +02:00
Valinor
6973c65142 Support WEBP format in image processing
Add support for WEBP file format in image processing.
2025-09-01 15:45:26 +02:00
vabene1111
a01f86a14e migrated comments, improved recipe activity, added editor 2025-08-31 12:32:12 +02:00
vabene1111
9704268fdc added proper query binding to ModelListPager 2025-08-31 09:42:27 +02:00
vabene1111
84cc4c1165 food create serializer case insensitive 2025-08-31 09:23:27 +02:00
vabene1111
5cb70becb8 ingredient parser case insenstiive 2025-08-31 09:23:18 +02:00
vabene1111
5f99abf459 food and unit plurals in shopping 2025-08-30 11:20:02 +02:00
vabene1111
4a8ddce391 added fuzzy filtering to UnitConversionApi 2025-08-30 11:08:16 +02:00
vabene1111
9a14a87c27 import log view improvement 2025-08-30 08:39:31 +02:00
vabene1111
c01634f9bd remove search links from unauthenticated recipe view 2025-08-30 08:31:15 +02:00
vabene1111
f055df3b4d fixed original text for pasted ingredients 2025-08-30 08:29:07 +02:00
vabene1111
a83f474d70 note 2025-08-30 08:08:54 +02:00
vabene1111
63d358df36 indicate private reciesp 2025-08-29 13:08:16 +02:00
vabene1111
e70548fcc0 added split/merge steps to recipe view 2025-08-28 18:20:26 +02:00
vabene1111
17b03905e6 half increment rating display 2025-08-28 17:47:43 +02:00
vabene1111
90403e6a13 Merge pull request #3960 from dertasiu/develop
Allow video file types to be uploaded
2025-08-28 17:45:40 +02:00
vabene1111
db400cae25 Merge pull request #3956 from c0mputerguru/devcontainer-updatevue3
Update devcontainer to work with new vue3 UI.
2025-08-28 17:45:03 +02:00
vabene1111
0f8eee4e0f Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2025-08-28 07:55:55 +02:00
vabene1111
1f532f6276 quick fix 2025-08-28 07:55:47 +02:00
c0mputerguru
b32715e493 Update documentation for vscode devcontainer about either starting vite or collecting static files prior to starting django. 2025-08-23 16:50:57 +00:00
c0mputerguru
0d19e12118 Remove dependencies from devcontainer tasks and have django run in debug mode. 2025-08-23 16:41:36 +00:00
dertasiu
96e5213fa6 Allow video files to be uploaded 2025-08-23 11:02:03 +02:00
vabene1111
44c567d20b Merge branch 'develop' 2025-08-23 09:07:19 +02:00
c0mputerguru
a71564a424 Update devcontainer to work with new vue3 UI.
Fixes #3925
2025-08-21 22:41:13 +00:00
vabene1111
8183e350c9 Merge branch 'develop' 2025-08-17 11:24:15 +02:00
vabene1111
9119d773f1 Merge branch 'develop' 2025-07-31 19:28:03 +02:00
vabene1111
27e5955c78 Merge branch 'develop' 2025-07-31 17:29:28 +02:00
116 changed files with 4176 additions and 547 deletions

View File

@@ -1,12 +1,7 @@
FROM python:3.10-alpine3.18
FROM python:3.13-alpine3.22
#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn
# Fix libxml error from xmlsec https://github.com/xmlsec/python-xmlsec/issues/257#issuecomment-1738620862
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.15/community/" | tee -a /etc/apk/repositories
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.15/main" | tee -a /etc/apk/repositories
RUN apk add --no-cache libxml2-dev=2.9.14-r2 xmlsec-dev=1.2.33-r0
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn libgcc libstdc++ nginx tini envsubst nodejs npm
#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1
@@ -24,8 +19,10 @@ RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt && \
rm -rf /tmp/pip-tmp && \
RUN apk add --no-cache --virtual .build-deps 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 rust && \
python -m pip install --upgrade pip && \
pip debug -v && \
pip install wheel==0.45.1 && \
pip install setuptools_rust==1.10.2 && \
pip install -r /tmp/pip-tmp/requirements.txt --no-cache-dir &&\
apk --purge del .build-deps

62
.vscode/tasks.json vendored
View File

@@ -14,28 +14,16 @@
},
{
"label": "Setup Dev Server",
"dependsOn": ["Run Migrations", "Yarn Build"]
"dependsOn": ["Run Migrations"]
},
{
"label": "Run Dev Server",
"type": "shell",
"type": "shell",
"dependsOn": ["Setup Dev Server"],
"command": "python3 manage.py runserver"
"command": "DEBUG=1 python3 manage.py runserver"
},
{
"label": "Yarn Install",
"dependsOn": ["Yarn Install - Vue", "Yarn Install - Vue3"]
},
{
"label": "Yarn Install - Vue",
"type": "shell",
"command": "yarn install --force",
"options": {
"cwd": "${workspaceFolder}/vue"
}
},
{
"label": "Yarn Install - Vue3",
"type": "shell",
"command": "yarn install --force",
"options": {
@@ -44,18 +32,6 @@
},
{
"label": "Generate API",
"dependsOn": ["Generate API - Vue", "Generate API - Vue3"]
},
{
"label": "Generate API - Vue",
"type": "shell",
"command": "openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:8000/openapi/",
"options": {
"cwd": "${workspaceFolder}/vue/src/utils/openapi"
}
},
{
"label": "Generate API - Vue3",
"type": "shell",
"command": "openapi-generator-cli generate -g typescript-fetch -i http://127.0.0.1:8000/openapi/",
"options": {
@@ -63,43 +39,19 @@
}
},
{
"label": "Yarn Serve",
"label": "Yarn Dev",
"type": "shell",
"command": "yarn serve",
"dependsOn": ["Yarn Install - Vue"],
"options": {
"cwd": "${workspaceFolder}/vue"
}
},
{
"label": "Vite Serve",
"type": "shell",
"command": "vite",
"dependsOn": ["Yarn Install - Vue3"],
"command": "yarn dev",
"dependsOn": ["Yarn Install"],
"options": {
"cwd": "${workspaceFolder}/vue3"
}
},
{
"label": "Yarn Build",
"dependsOn": ["Yarn Build - Vue", "Vite Build - Vue3"],
"group": "build"
},
{
"label": "Yarn Build - Vue",
"type": "shell",
"command": "yarn build",
"dependsOn": ["Yarn Install - Vue"],
"options": {
"cwd": "${workspaceFolder}/vue"
},
"group": "build"
},
{
"label": "Vite Build - Vue3",
"type": "shell",
"command": "vite build",
"dependsOn": ["Yarn Install - Vue3"],
"dependsOn": ["Yarn Install"],
"options": {
"cwd": "${workspaceFolder}/vue3"
},

View File

@@ -15,14 +15,14 @@
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
<a href="https://app.tandoor.dev/e/demo-auto-login/" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
</p>
<p align="center">
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://app.tandoor.dev/e/demo-auto-login/" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://community.tandoor.dev" target="_blank" rel="noopener noreferrer">Community</a> •
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
</p>

View File

@@ -17,7 +17,7 @@ from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, Im
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog, ConnectorConfig)
ViewLog, ConnectorConfig, AiProvider, AiLog)
admin.site.login = secure_admin_login(admin.site.login)
@@ -90,6 +90,20 @@ class SearchPreferenceAdmin(admin.ModelAdmin):
admin.site.register(SearchPreference, SearchPreferenceAdmin)
class AiProviderAdmin(admin.ModelAdmin):
list_display = ('name', 'space', 'model',)
search_fields = ('name', 'space', 'model',)
admin.site.register(AiProvider, AiProviderAdmin)
class AiLogAdmin(admin.ModelAdmin):
list_display = ('ai_provider', 'function', 'credit_cost', 'created_by', 'created_at',)
admin.site.register(AiLog, AiLogAdmin)
class StorageAdmin(admin.ModelAdmin):
list_display = ('name', 'method')
search_fields = ('name',)

View File

@@ -0,0 +1,80 @@
from decimal import Decimal
from django.utils import timezone
from django.db.models import Sum
from litellm import CustomLogger
from cookbook.models import AiLog
def get_monthly_token_usage(space):
"""
returns the number of credits the space has used in the current month
"""
token_usage = AiLog.objects.filter(space=space, credits_from_balance=False, created_at__month=timezone.now().month).aggregate(Sum('credit_cost'))['credit_cost__sum']
if token_usage is None:
token_usage = 0
return token_usage
def has_monthly_token(space):
"""
checks if the monthly credit limit has been exceeded
"""
return get_monthly_token_usage(space) < space.ai_credits_monthly
def can_perform_ai_request(space):
return (has_monthly_token(space) or space.ai_credits_balance > 0) and space.ai_enabled
class AiCallbackHandler(CustomLogger):
space = None
user = None
ai_provider = None
def __init__(self, space, user, ai_provider):
super().__init__()
self.space = space
self.user = user
self.ai_provider = ai_provider
def log_pre_api_call(self, model, messages, kwargs):
pass
def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
pass
def log_success_event(self, kwargs, response_obj, start_time, end_time):
self.create_ai_log(kwargs, response_obj, start_time, end_time)
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
self.create_ai_log(kwargs, response_obj, start_time, end_time)
def create_ai_log(self, kwargs, response_obj, start_time, end_time):
credit_cost = 0
credits_from_balance = False
if self.ai_provider.log_credit_cost:
credit_cost = kwargs.get("response_cost", 0) * 100
if (not has_monthly_token(self.space)) and self.space.ai_credits_balance > 0:
remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost))
if remaining_balance < 0:
remaining_balance = 0
self.space.ai_credits_balance = remaining_balance
credits_from_balance = True
self.space.save()
AiLog.objects.create(
created_by=self.user,
space=self.space,
ai_provider=self.ai_provider,
start_time=start_time,
end_time=end_time,
input_tokens=response_obj['usage']['prompt_tokens'],
output_tokens=response_obj['usage']['completion_tokens'],
function=AiLog.F_FILE_IMPORT,
credit_cost=credit_cost,
credits_from_balance=credits_from_balance,
)

View File

@@ -0,0 +1,22 @@
def add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
"""
given a model, the base and related field and the base and related ids, bulk create relation objects
"""
relation_objects = []
for b in base_ids:
for r in related_ids:
relation_objects.append(relation_model(**{base_field_name: b, related_field_name: r}))
relation_model.objects.bulk_create(relation_objects, ignore_conflicts=True, unique_fields=(base_field_name, related_field_name,))
def remove_from_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids, f'{related_field_name}__in': related_ids}).delete()
def remove_all_from_relation(relation_model, base_field_name, base_ids):
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids}).delete()
def set_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
remove_all_from_relation(relation_model, base_field_name, base_ids)
add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids)

View File

@@ -37,7 +37,7 @@ def get_filetype(name):
def is_file_type_allowed(filename, image_only=False):
is_file_allowed = False
allowed_file_types = ['.pdf', '.docx', '.xlsx', '.css']
allowed_file_types = ['.pdf', '.docx', '.xlsx', '.css', '.mp4', '.mov']
allowed_image_types = ['.png', '.jpg', '.jpeg', '.gif', '.webp']
check_list = allowed_image_types
if not image_only:
@@ -77,6 +77,8 @@ def handle_image(request, image_object, filetype):
file_format = 'JPEG'
if filetype == '.png':
file_format = 'PNG'
if filetype == '.webp':
file_format = 'WEBP'
if (image_object.size / 1000) > 500: # if larger than 500 kb compress
if filetype == '.jpeg' or filetype == '.jpg':

View File

@@ -330,6 +330,24 @@ 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
users: can read all
admins: can read and write
superusers: can read and write + write providers without a space
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view): # user is either at least a user and the request is safe
return (has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS) or (has_group_permission(request.user, ['admin']) or request.user.is_superuser)
# editing of global providers allowed for superusers, space providers by admins and users can read only access
def has_object_permission(self, request, view, obj):
return ((obj.space is None and request.user.is_superuser)
or (obj.space == request.space and has_group_permission(request.user, ['admin']))
or (obj.space == request.space and has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS))
class CustomUserPermission(permissions.BasePermission):
"""

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.2.22 on 2025-08-31 09:11
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_comments(apps, schema_editor):
with scopes_disabled():
Comment = apps.get_model('cookbook', 'Comment')
CookLog = apps.get_model('cookbook', 'CookLog')
cook_logs = []
for c in Comment.objects.all():
cook_logs.append(CookLog(
recipe=c.recipe,
created_by=c.created_by,
created_at=c.created_at,
comment=c.text,
space=c.recipe.space,
))
CookLog.objects.bulk_create(cook_logs, unique_fields=('recipe', 'comment', 'created_at', 'created_by'))
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0222_alter_shoppinglistrecipe_created_by_and_more'),
]
operations = [
migrations.RunPython(migrate_comments),
]

View File

@@ -0,0 +1,60 @@
# Generated by Django 4.2.22 on 2025-09-05 06:51
import cookbook.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0223_auto_20250831_1111'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_credits_balance',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='space',
name='ai_credits_monthly',
field=models.IntegerField(default=100),
),
migrations.CreateModel(
name='AiProvider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('description', models.TextField(blank=True)),
('api_key', models.CharField(max_length=2048)),
('model_name', models.CharField(max_length=256)),
('url', models.CharField(blank=True, max_length=2048, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('space', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
),
migrations.CreateModel(
name='AiLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('function', models.CharField(max_length=64)),
('credit_cost', models.DecimalField(decimal_places=4, max_digits=16)),
('credits_from_balance', models.BooleanField(default=False)),
('input_tokens', models.IntegerField(default=0)),
('output_tokens', models.IntegerField(default=0)),
('start_time', models.DateTimeField(null=True)),
('end_time', models.DateTimeField(null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('ai_provider', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.aiprovider')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.22 on 2025-09-08 19:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0224_space_ai_credits_balance_space_ai_credits_monthly_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_enabled',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.22 on 2025-09-08 20:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0225_space_ai_enabled'),
]
operations = [
migrations.AddField(
model_name='aiprovider',
name='log_credit_cost',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='space',
name='ai_credits_monthly',
field=models.IntegerField(default=10000),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.2.22 on 2025-09-09 11:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0226_aiprovider_log_credit_cost_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='ai_default_provider',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_ai_default_provider', to='cookbook.aiprovider'),
),
migrations.AlterField(
model_name='space',
name='ai_credits_balance',
field=models.DecimalField(decimal_places=4, default=0, max_digits=16),
),
]

View File

@@ -329,6 +329,11 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=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)
ai_default_provider = models.ForeignKey("AiProvider", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_ai_default_provider')
internal_note = models.TextField(blank=True, null=True)
def safe_delete(self):
@@ -341,6 +346,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
BookmarkletImport.objects.filter(space=self).delete()
CustomFilter.objects.filter(space=self).delete()
AiLog.objects.filter(space=self).delete()
AiProvider.objects.filter(space=self).delete()
Property.objects.filter(space=self).delete()
PropertyType.objects.filter(space=self).delete()
@@ -393,6 +401,41 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
return self.name
class AiProvider(models.Model):
name = models.CharField(max_length=128)
description = models.TextField(blank=True)
# AiProviders can be global, so space=null is allowed (configurable by superusers)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
api_key = models.CharField(max_length=2048)
model_name = models.CharField(max_length=256)
url = models.CharField(max_length=2048, blank=True, null=True)
log_credit_cost = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class AiLog(models.Model, PermissionModelMixin):
F_FILE_IMPORT = 'FILE_IMPORT'
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
function = models.CharField(max_length=64)
credit_cost = models.DecimalField(max_digits=16, decimal_places=4)
# if credits from balance were used, else its from monthly quota
credits_from_balance = models.BooleanField(default=False)
input_tokens = models.IntegerField(default=0)
output_tokens = models.IntegerField(default=0)
start_time = models.DateTimeField(null=True)
end_time = models.DateTimeField(null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class ConnectorConfig(models.Model, PermissionModelMixin):
HOMEASSISTANT = 'HomeAssistant'
CONNECTER_TYPE = ((HOMEASSISTANT, 'HomeAssistant'),)

View File

@@ -24,6 +24,7 @@ from rest_framework.fields import IntegerField
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.property_helper import FoodPropertyHelper
@@ -36,7 +37,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
ShareLink, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields)
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog, AiProvider)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST
@@ -325,11 +326,52 @@ class UserFileViewSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'file', 'file_download', 'file_size_kb', 'preview', 'created_by', 'created_at')
class AiProviderSerializer(serializers.ModelSerializer):
api_key = serializers.CharField(required=False, write_only=True)
def create(self, validated_data):
validated_data = self.handle_global_space_logic(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
validated_data = self.handle_global_space_logic(validated_data)
return super().update(instance, validated_data)
def handle_global_space_logic(self, validated_data):
"""
allow superusers to create AI providers without a space but make sure everyone else only uses their own space
"""
if ('space' not in validated_data or not validated_data['space']) and self.context['request'].user.is_superuser:
validated_data['space'] = None
else:
validated_data['space'] = self.context['request'].space
return validated_data
class Meta:
model = AiProvider
fields = ('id', 'name', 'description', 'api_key', 'model_name', 'url', 'log_credit_cost', 'space', 'created_at', 'updated_at')
read_only_fields = ('created_at', 'updated_at',)
class AiLogSerializer(serializers.ModelSerializer):
ai_provider = AiProviderSerializer(read_only=True)
class Meta:
model = AiLog
fields = ('id', 'ai_provider', 'function', 'credit_cost', 'credits_from_balance', 'input_tokens', 'output_tokens', 'start_time', 'end_time', 'created_by', 'created_at',
'updated_at')
read_only_fields = ('__all__',)
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')
ai_default_provider = AiProviderSerializer(required=False, allow_null=True)
food_inherit = FoodInheritFieldSerializer(many=True)
image = UserFileViewSerializer(required=False, many=False, allow_null=True)
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
@@ -350,6 +392,10 @@ class SpaceSerializer(WritableNestedModelSerializer):
def get_recipe_count(self, obj):
return Recipe.objects.filter(space=obj).count()
@extend_schema_field(int)
def get_ai_monthly_credits_used(self, obj):
return get_monthly_token_usage(obj)
@extend_schema_field(float)
def get_file_size_mb(self, obj):
try:
@@ -360,16 +406,29 @@ class SpaceSerializer(WritableNestedModelSerializer):
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
def update(self, instance, validated_data):
if 'ai_enabled' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_enabled']
if 'ai_credits_monthly' in validated_data and not self.context['request'].user.is_superuser:
del validated_data['ai_credits_monthly']
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)
class Meta:
model = Space
fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'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',)
'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')
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
'demo',)
'demo', 'ai_monthly_credits_used')
class UserSpaceSerializer(WritableNestedModelSerializer):
@@ -787,7 +846,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
if plural_name := validated_data.pop('plural_name', None):
plural_name = plural_name.strip()
if food := Food.objects.filter(Q(name=name) | Q(plural_name=name)).first():
if food := Food.objects.filter(Q(name__iexact=name) | Q(plural_name__iexact=name)).first():
return food
space = validated_data.pop('space', self.context['request'].space)
@@ -1038,7 +1097,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
'internal', 'private', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
# TODO having these readonly fields makes "RecipeOverview.ts" (API Client) not generate the RecipeOverviewToJSON second else block which leads to errors when using the api
# TODO find a solution (custom schema?) to have these fields readonly (to save performance) and generate a proper client (two serializers would probably do the trick)
@@ -1134,6 +1193,35 @@ class RecipeBatchUpdateSerializer(serializers.Serializer):
clear_description = serializers.BooleanField(required=False, allow_null=True)
class FoodBatchUpdateSerializer(serializers.Serializer):
foods = serializers.ListField(child=serializers.IntegerField())
category = serializers.IntegerField(required=False, allow_null=True)
substitute_add = serializers.ListField(child=serializers.IntegerField())
substitute_remove = serializers.ListField(child=serializers.IntegerField())
substitute_set = serializers.ListField(child=serializers.IntegerField())
substitute_remove_all = serializers.BooleanField(default=False)
inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
inherit_fields_remove_all = serializers.BooleanField(default=False)
child_inherit_fields_add = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_remove = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_set = serializers.ListField(child=serializers.IntegerField())
child_inherit_fields_remove_all = serializers.BooleanField(default=False)
substitute_children = serializers.BooleanField(required=False, allow_null=True)
substitute_siblings = serializers.BooleanField(required=False, allow_null=True)
ignore_shopping = serializers.BooleanField(required=False, allow_null=True)
on_hand = serializers.BooleanField(required=False, allow_null=True)
parent_remove = serializers.BooleanField(required=False, allow_null=True)
parent_set = serializers.IntegerField(required=False, allow_null=True)
class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserSerializer(many=True, required=False)
@@ -1245,8 +1333,8 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
class AutoMealPlanSerializer(serializers.Serializer):
start_date = serializers.DateField()
end_date = serializers.DateField()
start_date = serializers.DateTimeField()
end_date = serializers.DateTimeField()
meal_type_id = serializers.IntegerField()
keyword_ids = serializers.ListField()
servings = CustomDecimalField()
@@ -1564,7 +1652,6 @@ class ServerSettingsSerializer(serializers.Serializer):
# TODO add all other relevant settings including path/url related ones?
shopping_min_autosync_interval = serializers.CharField()
enable_pdf_export = serializers.BooleanField()
enable_ai_import = serializers.BooleanField()
disable_external_connectors = serializers.BooleanField()
terms_url = serializers.CharField()
privacy_url = serializers.CharField()
@@ -1788,6 +1875,7 @@ class RecipeFromSourceResponseSerializer(serializers.Serializer):
class AiImportSerializer(serializers.Serializer):
ai_provider_id = serializers.IntegerField()
file = serializers.FileField(allow_null=True)
text = serializers.CharField(allow_null=True, allow_blank=True)
recipe_id = serializers.CharField(allow_null=True, allow_blank=True)

View File

@@ -51,11 +51,6 @@
{# {% endif %}#}
<p class="card-text"><small
class="text-muted">{% trans 'Owner' %}: {{ us.space.created_by }}</small>
{% if us.space.created_by != us.user %}
<p class="card-text"><small
class="text-muted"><a
href="{% url 'delete_user_space' us.pk %}">{% trans 'Leave Space' %}</a></small>
{% endif %}
<!--TODO add direct link to management page -->
</p>
</div>

View File

@@ -0,0 +1,162 @@
import json
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import MealType, PropertyType, AiProvider
LIST_URL = 'api:aiprovider-list'
DETAIL_URL = 'api:aiprovider-detail'
@pytest.fixture()
def obj_1(space_1, a1_s1):
return AiProvider.objects.get_or_create(name='test_1', space=space_1)[0]
@pytest.fixture
def obj_2(space_1, a1_s1):
return AiProvider.objects.get_or_create(name='test_2', space=None)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
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'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
obj_1.space = space_2
obj_1.save()
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 403],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 403],
['g1_s2', 403],
['u1_s2', 403],
['a1_s2', 403],
['s1_s1', 200],
])
def test_update_global(arg, request, obj_2):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_2.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 403],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test', 'api_key': 'test', 'model_name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(a1_s1, a1_s2, obj_1):
# admins cannot delete foreign space providers
r = a1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
# admins can delete their space providers
r = a1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert AiProvider.objects.count() == 0
def test_delete_global(a1_s1, s1_s1, obj_2):
# admins cant delete global providers
r = a1_s1.delete(
reverse(
DETAIL_URL,
args={obj_2.id}
)
)
assert r.status_code == 403
# superusers can delete global providers
r = s1_s1.delete(
reverse(
DETAIL_URL,
args={obj_2.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert AiProvider.objects.count() == 0

View File

@@ -298,3 +298,11 @@ def a1_s2(client, space_2):
@pytest.fixture()
def a2_s2(client, space_2):
return create_user(client, space_2, group='admin')
@pytest.fixture()
def s1_s1(client, space_1):
client = create_user(client, space_1, group='admin')
user = auth.get_user(client)
user.is_superuser = True
user.save()
return client

View File

@@ -61,6 +61,8 @@ router.register(r'search-preference', api.SearchPreferenceViewSet)
router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
router.register(r'access-token', api.AccessTokenViewSet)
router.register(r'ai-provider', api.AiProviderViewSet)
router.register(r'ai-log', api.AiLogViewSet)
router.register(r'localization', api.LocalizationViewSet, basename='localization')
router.register(r'server-settings', api.ServerSettingsViewSet, basename='server-settings')

View File

@@ -65,6 +65,8 @@ from cookbook.connectors.connector_manager import ConnectorManager, ActionType
from cookbook.forms import ImportForm, ImportExportBase
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.HelperFunctions import str2bool, validate_import_url
from cookbook.helper.ai_helper import has_monthly_token, can_perform_ai_request, AiCallbackHandler
from cookbook.helper.batch_edit_helper import add_to_relation, remove_from_relation, remove_all_from_relation, set_relation
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.open_data_importer import OpenDataImporter
@@ -74,7 +76,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
switch_user_active_space, CustomAiProviderPermission
)
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
@@ -85,7 +87,7 @@ from cookbook.models import (Automation, BookmarkletImport, ConnectorConfig, Coo
RecipeBookEntry, ShareLink, ShoppingListEntry,
ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields, AiLog, AiProvider
)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
@@ -110,12 +112,13 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au
UserSerializer, UserSpaceSerializer, ViewLogSerializer,
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer,
AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer,
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer,
AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer
)
from cookbook.version_info import TANDOOR_VERSION
from cookbook.views.import_export import get_integration
from recipes import settings
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT, AI_API_KEY, AI_MODEL_NAME
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, AI_RATELIMIT
DateExample = OpenApiExample('Date Format', value='1972-12-05', request_only=True)
BeforeDateExample = OpenApiExample('Before Date Format', value='-1972-12-05', request_only=True)
@@ -454,13 +457,11 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request,
serializer=self.serializer_class, tree=True)
self.queryset = self.queryset.filter(space=self.request.space)
# only order if not root_tree or tree mde because in these modes the sorting is relevant for the client
if not root_tree and not tree:
self.queryset = self.queryset.order_by(Lower('name').asc())
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
tree=True)
@@ -619,6 +620,29 @@ class SearchPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
return self.queryset.filter(user=self.request.user)
class AiProviderViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AiProvider.objects
serializer_class = AiProviderSerializer
permission_classes = [CustomAiProviderPermission & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
def get_queryset(self):
# read only access to all space and global AiProviders
with scopes_disabled():
return self.queryset.filter(Q(space=self.request.space) | Q(space__isnull=True))
class AiLogViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AiLog.objects
serializer_class = AiLogSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
http_method_names = ['get']
pagination_class = DefaultPagination
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class StorageViewSet(LoggingMixin, viewsets.ModelViewSet):
# TODO handle delete protect error and adjust test
queryset = Storage.objects
@@ -917,6 +941,94 @@ class FoodViewSet(LoggingMixin, TreeMixin):
content = {'error': True, 'msg': e.args[0]}
return Response(content, status=status.HTTP_403_FORBIDDEN)
@decorators.action(detail=False, methods=['PUT'], serializer_class=FoodBatchUpdateSerializer)
def batch_update(self, request):
serializer = self.serializer_class(data=request.data, partial=True)
if serializer.is_valid():
foods = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space)
safe_food_ids = Food.objects.filter(id__in=serializer.validated_data['foods'], space=self.request.space).values_list('id', flat=True)
if 'category' in serializer.validated_data:
foods.update(supermarket_category_id=serializer.validated_data['category'])
if 'ignore_shopping' in serializer.validated_data and serializer.validated_data['ignore_shopping'] is not None:
foods.update(ignore_shopping=serializer.validated_data['ignore_shopping'])
if 'on_hand' in serializer.validated_data and serializer.validated_data['on_hand'] is not None:
if serializer.validated_data['on_hand']:
user_relation = []
for f in safe_food_ids:
user_relation.append(Food.onhand_users.through(food_id=f, user_id=request.user.id))
Food.onhand_users.through.objects.bulk_create(user_relation, ignore_conflicts=True, unique_fields=('food_id', 'user_id',))
else:
Food.onhand_users.through.objects.filter(food_id__in=safe_food_ids, user_id=request.user.id).delete()
if 'substitute_children' in serializer.validated_data and serializer.validated_data['substitute_children'] is not None:
foods.update(substitute_children=serializer.validated_data['substitute_children'])
if 'substitute_siblings' in serializer.validated_data and serializer.validated_data['substitute_siblings'] is not None:
foods.update(substitute_siblings=serializer.validated_data['substitute_siblings'])
# ---------- substitutes -------------
if 'substitute_add' in serializer.validated_data:
add_to_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_add'])
if 'substitute_remove' in serializer.validated_data:
remove_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_remove'])
if 'substitute_set' in serializer.validated_data and len(serializer.validated_data['substitute_set']) > 0:
set_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_set'])
if 'substitute_remove_all' in serializer.validated_data and serializer.validated_data['substitute_remove_all']:
remove_all_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids)
# ---------- inherit fields -------------
if 'inherit_fields_add' in serializer.validated_data:
add_to_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_add'])
if 'inherit_fields_remove' in serializer.validated_data:
remove_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_remove'])
if 'inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['inherit_fields_set']) > 0:
set_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_set'])
if 'inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['inherit_fields_remove_all']:
remove_all_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids)
# ---------- child inherit fields -------------
if 'child_inherit_fields_add' in serializer.validated_data:
add_to_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_add'])
if 'child_inherit_fields_remove' in serializer.validated_data:
remove_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_remove'])
if 'child_inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['child_inherit_fields_set']) > 0:
set_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_set'])
if 'child_inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['child_inherit_fields_remove_all']:
remove_all_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids)
# ------- parent --------
if self.model.node_order_by:
node_location = 'sorted'
else:
node_location = 'last'
if 'parent_remove' in serializer.validated_data and serializer.validated_data['parent_remove']:
for f in foods:
f.move(Food.get_first_root_node(), f'{node_location}-sibling')
if 'parent_set' in serializer.validated_data:
parent_food = Food.objects.filter(space=request.space, id=serializer.validated_data['parent_set']).first()
if parent_food:
for f in foods:
f.move(parent_food, f'{node_location}-child')
return Response({}, 200)
return Response(serializer.errors, 400)
@extend_schema_view(list=extend_schema(parameters=[
OpenApiParameter(name='order_field', description='Field to order recipe books on', type=str,
@@ -1451,9 +1563,26 @@ class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
return Response(serializer.errors, 400)
@extend_schema(responses=RecipeSerializer(many=False))
@decorators.action(detail=True, pagination_class=None, methods=['PATCH'], serializer_class=RecipeSerializer)
def delete_external(self, request, pk):
obj = self.get_object()
if obj.get_space() != request.space and has_group_permission(request.user, ['user']):
raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403)
if obj.storage:
get_recipe_provider(obj).delete_file(obj)
obj.storage = None
obj.file_path = ''
obj.file_uid = ''
obj.save()
return Response(self.serializer_class(obj, many=False, context={'request': request}).data)
@extend_schema_view(list=extend_schema(
parameters=[OpenApiParameter(name='food_id', description='ID of food to filter for', type=int), ]))
parameters=[OpenApiParameter(name='food_id', description='ID of food to filter for', type=int),
OpenApiParameter(name='query', description='query that looks into food, base unit or converted unit by name', type=str), ]))
class UnitConversionViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UnitConversion.objects
serializer_class = UnitConversionSerializer
@@ -1465,6 +1594,10 @@ class UnitConversionViewSet(LoggingMixin, viewsets.ModelViewSet):
if food_id is not None:
self.queryset = self.queryset.filter(food_id=food_id)
query = self.request.query_params.get('query', None)
if query is not None:
self.queryset = self.queryset.filter(Q(food__name__icontains=query) | Q(base_unit__name__icontains=query) | Q(converted_unit__name__icontains=query))
return self.queryset.filter(space=self.request.space)
@@ -1981,6 +2114,24 @@ class AiImportView(APIView):
if serializer.is_valid():
# TODO max file size check
if 'ai_provider_id' not in serializer.validated_data:
response = {
'error': True,
'msg': _('You must select an AI provider to perform your request.'),
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
if not can_perform_ai_request(request.space):
response = {
'error': True,
'msg': _("You don't have any credits remaining to use AI or AI features are not enabled for your space."),
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
ai_provider = AiProvider.objects.filter(pk=serializer.validated_data['ai_provider_id']).filter(Q(space=request.space) | Q(space__isnull=True)).first()
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider)]
messages = []
uploaded_file = serializer.validated_data['file']
@@ -2049,7 +2200,15 @@ class AiImportView(APIView):
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
try:
ai_response = completion(api_key=AI_API_KEY, model=AI_MODEL_NAME, response_format={"type": "json_object"}, messages=messages, )
ai_request = {
'api_key': ai_provider.api_key,
'model': ai_provider.model_name,
'response_format': {"type": "json_object"},
'messages': messages,
}
if ai_provider.url:
ai_request['api_base'] = ai_provider.url
ai_response = completion(**ai_request)
except BadRequestError as err:
response = {
'error': True,
@@ -2354,7 +2513,6 @@ class ServerSettingsViewSet(viewsets.GenericViewSet):
# Attention: No login required, do not return sensitive data
s['shopping_min_autosync_interval'] = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
s['enable_pdf_export'] = settings.ENABLE_PDF_EXPORT
s['enable_ai_import'] = settings.AI_API_KEY != ''
s['disable_external_connectors'] = settings.DISABLE_EXTERNAL_CONNECTORS
s['terms_url'] = settings.TERMS_URL
s['privacy_url'] = settings.PRIVACY_URL
@@ -2507,7 +2665,7 @@ def meal_plans_to_ical(queryset, filename):
request=inline_serializer(name="IngredientStringSerializer", fields={'text': CharField()}),
responses=inline_serializer(name="ParsedIngredientSerializer",
fields={'amount': IntegerField(), 'unit': CharField(), 'food': CharField(),
'note': CharField()})
'note': CharField(), 'original_text': CharField()})
)
@api_view(['POST'])
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
@@ -2517,13 +2675,19 @@ def ingredient_from_string(request):
ingredient_parser = IngredientParser(request, False)
amount, unit, food, note = ingredient_parser.parse(text)
ingredient = {'amount': amount, 'unit': None, 'food': None, 'note': note}
ingredient = {'amount': amount, 'unit': None, 'food': None, 'note': note, 'original_text': text}
if food:
food, created = Food.objects.get_or_create(space=request.space, name=food)
ingredient['food'] = {'name': food.name, 'id': food.id}
if food_obj := Food.objects.filter(space=request.space).filter(Q(name=food) | Q(plural_name=food)).first():
ingredient['food'] = {'name': food_obj.name, 'id': food_obj.id}
else:
food_obj = Food.objects.create(space=request.space, name=food)
ingredient['food'] = {'name': food_obj.name, 'id': food_obj.id}
if unit:
unit, created = Unit.objects.get_or_create(space=request.space, name=unit)
ingredient['unit'] = {'name': unit.name, 'id': unit.id}
if unit_obj := Unit.objects.filter(space=request.space).filter(Q(name=unit) | Q(plural_name=unit)).first():
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
else:
unit_obj = Unit.objects.create(space=request.space, name=unit)
ingredient['unit'] = {'name': unit_obj.name, 'id': unit_obj.id}
return JsonResponse(ingredient, status=200)

View File

@@ -97,7 +97,8 @@ def space_overview(request):
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,)
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
user_space.groups.add(Group.objects.filter(name='admin').get())

View File

@@ -33,17 +33,26 @@ VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=esbenp.
<!-- prettier-ignore -->
!!! note
In order to debug vue yarn and vite servers must be started before starting the django server.
In order to hot reload vue, the `yarn dev` server must be started before starting the django server.
There are a number of built in tasks that are available. Here are a few of the key ones:
- `Setup Dev Server` - Runs all the prerequisite steps so that the dev server can be run inside VSCode.
- `Setup Tests` - Runs all prerequisites so tests can be run inside VSCode.
Once these are run, you should be able to run/debug a django server in VSCode as well as run/debug tests directly through VSCode.
There are also a few other tasks specified in case you have specific development needs:
Once these are run, there are 2 options. If you want to run a vue3 server in a hot reload mode for quick development of the frontend, you should run a development vue server:
- `Yarn Dev` - Runs development Vue.js vite server not connected to VSCode. Useful if you want to make Vue changes and see them in realtime.
If not, you need to build and copy the frontend to the django server. If you make changes to the frontend, you need to re-run this and restart the django server:
- `Collect Static Files` - Builds and collects the vue3 frontend so that it can be served via the django server.
Once either of those steps are done, you can start the django server:
- `Run Dev Server` - Runs a django development server not connected to VSCode.
There are also a few other tasks specified in case you have specific development needs:
- `Run all pytests` - Runs all the pytests outside of VSCode.
- `Yarn Serve` - Runs development Vue.js server not connected to VSCode. Useful if you want to make Vue changes and see them in realtime.
- `Serve Documentation` - Runs a documentation server. Useful if you want to see how changes to documentation show up.

18
docs/features/ai.md Normal file
View File

@@ -0,0 +1,18 @@
Tandoor has several AI based features. To allow maximum flexibility, you can configure different AI providers and select them based on the task you want to perform.
To prevent accidental cost escalation Tandoor has a robust system of tracking and limiting AI costs.
## Default Configuration
By default the AI features are enabled for every space. Each space has a spending limit of roughly 1 USD per month.
This can be changed using the [configuration variables](https://docs.tandoor.dev/system/configuration/#ai-integration)
You can change these settings any time using the django admin. If you do not care about AI cost you can enter a very high limit or disable cost tracking for your providers.
The limit resets on the first of every month.
## Configure AI Providers
When AI support is enabled for a space every user in a space can configure AI providers.
The models shown in the editor have been tested and work with Tandoor. Most other models that can parse images/files and return text should also work.
Superusers also have the ability to configure global AI providers that every space can use.
## AI Log
The AI Log allows you to track the usage of AI calls. Here you can also see the usage.

View File

@@ -196,6 +196,7 @@ server {
proxy_set_header Host $http_host;
proxy_pass http://unix:/var/www/recipes/recipes.sock;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
```

View File

@@ -472,15 +472,20 @@ S3_CUSTOM_DOMAIN= # when using a CDN/proxy to S3 (see https://github.com/Tandoor
#### AI Integration
To use AI to perform different tasks you need to configure an API key and the AI provider. [LiteLLM](https://www.litellm.ai/) is used
to make a standardized request to different AI providers of your liking.
Configuring this via environment parameters is a temporary solution. In the future I plan on adding support for multiple AI providers per Tandoor instance
with the option to select them for various tasks. For now only gemini 2.0 flash has been tested but feel free to try out other models.
Most AI features are configured trough the AI Provider settings in the Tandoor web interface. Some defaults can be set for new spaces on your instance.
Enables AI features for spaces by default
```
SPACE_AI_ENABLED=1
```
Sets the monthly default credit limit for AI usage
```
SPACE_AI_CREDITS_MONTHLY=100
```
Ratelimit for AI API
```
AI_API_KEY=
AI_MODEL_NAME=gemini/gemini-2.0-flash
AI_RATELIMIT=60/hour
```

View File

@@ -4,6 +4,8 @@ import traceback
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
#TODO clean existing links for when plugins are uninstalled or not necessary because it will just be empty links?
PLUGINS_DIRECTORY = os.path.join(BASE_DIR, 'recipes', 'plugins')
if os.path.isdir(PLUGINS_DIRECTORY):
for d in os.listdir(PLUGINS_DIRECTORY):

View File

@@ -59,6 +59,8 @@ SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
SPACE_DEFAULT_ALLOW_SHARING = extract_bool('SPACE_DEFAULT_ALLOW_SHARING', True)
SPACE_AI_ENABLED = extract_bool('SPACE_AI_ENABLED', True)
SPACE_AI_CREDITS_MONTHLY = int(os.getenv('SPACE_AI_CREDITS_MONTHLY', 10000))
INTERNAL_IPS = extract_comma_list('INTERNAL_IPS', '127.0.0.1')
@@ -137,8 +139,6 @@ HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
FDC_API_KEY = os.getenv('FDC_API_KEY', 'DEMO_KEY')
AI_API_KEY = os.getenv('AI_API_KEY', '')
AI_MODEL_NAME = os.getenv('AI_MODEL_NAME', 'gemini/gemini-2.0-flash')
AI_RATELIMIT = os.getenv('AI_RATELIMIT', '60/hour')
SHARING_ABUSE = extract_bool('SHARING_ABUSE', False)

View File

@@ -1,4 +1,4 @@
Django==4.2.22
Django==4.2.24
cryptography===45.0.5
django-annoying==0.10.6
django-cleanup==9.0.0

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@types/luxon": "^3.7.1",
"@types/sortablejs": "^1.15.8",
"@vueform/multiselect": "^2.6.11",
"@vueuse/core": "^13.6.0",
"@vueuse/router": "^13.6.0",
@@ -22,7 +23,6 @@
"vue-router": "^4.5.0",
"vue-simple-calendar": "7.1.0",
"vuedraggable": "^4.1.0",
"@types/sortablejs": "^1.15.8",
"vuetify": "^3.9.3"
},
"devDependencies": {
@@ -32,20 +32,21 @@
"@types/node": "^24.0.8",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"esbuild-register": "^3.6.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.3",
"vite": "6.3.5",
"vite-plugin-pwa": "^1.0.2",
"workbox-core": "^7.3.0",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8",
"workbox-background-sync": "^7.3.0",
"workbox-build": "^7.3.0",
"workbox-core": "^7.3.0",
"workbox-expiration": "^7.3.0",
"workbox-navigation-preload": "^7.3.0",
"workbox-precaching": "^7.3.0",
"workbox-routing": "^7.3.0",
"workbox-strategies": "^7.3.0",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8"
"workbox-window": "^7.3.0"
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,114 @@
<template>
<v-dialog max-width="600px" :activator="props.activator" v-model="dialog">
<v-card :loading="loading">
<v-closable-card-title v-model="dialog" :title="$t('Auto_Planner')" icon="fa-solid fa-calendar-plus"></v-closable-card-title>
<v-card-text>
<v-form>
<model-select model="MealType" v-model="autoMealPlan.mealTypeId" :object="false"></model-select>
<model-select model="Keyword" v-model="autoMealPlan.keywordIds" mode="tags" :object="false"></model-select>
<v-number-input :label="$t('Servings')" v-model="autoMealPlan.servings"></v-number-input>
<v-date-input :label="$t('Date')"
multiple="range"
v-model="dateRangeValue"
:first-day-of-week="useUserPreferenceStore().deviceSettings.mealplan_startingDayOfWeek"
:show-week="useUserPreferenceStore().deviceSettings.mealplan_displayWeekNumbers"
prepend-icon=""
prepend-inner-icon="$calendar"
></v-date-input>
<model-select model="User" v-model="autoMealPlan.shared" mode="tags"></model-select>
<v-checkbox v-model="autoMealPlan.addshopping" :label="$t('AddToShopping')" hide-details></v-checkbox>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn @click="dialog = false">{{ $t('Cancel') }}</v-btn>
<v-btn color="create" prepend-icon="fa-solid fa-person-running" @click="doAutoPlan()" :loading="loading">{{ $t('Create') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {ApiApi, AutoMealPlan} from "@/openapi";
import {onMounted, ref} from "vue";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {VDateInput} from 'vuetify/labs/VDateInput'
import {DateTime} from "luxon";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore.ts";
import {useMealPlanStore} from "@/stores/MealPlanStore.ts";
const emit = defineEmits(['change'])
const props = defineProps({
activator: {type: String, default: 'parent'},
})
const {t} = useI18n()
const dialog = defineModel<boolean>({default: false})
const loading = ref(false)
const dateRangeValue = ref([] as Date[])
const autoMealPlan = ref({} as AutoMealPlan)
onMounted(() => {
initializeRequest()
})
/**
* load default values for auto plan creation
*/
function initializeRequest() {
autoMealPlan.value = {
servings: 1,
startDate: DateTime.now().toJSDate(),
endDate: DateTime.now().plus({day: 7}).toJSDate(),
shared: useUserPreferenceStore().userSettings.planShare,
addshopping: useUserPreferenceStore().userSettings.mealplanAutoaddShopping,
} as AutoMealPlan
dateRangeValue.value = []
let currentDate = DateTime.fromJSDate(autoMealPlan.value.startDate).plus({day: 1}).toJSDate()
while (currentDate <= autoMealPlan.value.endDate) {
dateRangeValue.value.push(currentDate)
currentDate = DateTime.fromJSDate(currentDate).plus({day: 1}).toJSDate()
}
}
/**
* perform auto plan creation
*/
function doAutoPlan() {
let api = new ApiApi()
loading.value = true
autoMealPlan.value.startDate = dateRangeValue.value[0]
autoMealPlan.value.endDate = dateRangeValue.value[dateRangeValue.value.length - 1]
console.log('requesting auto plan from ', autoMealPlan.value.startDate, ' to ', autoMealPlan.value.endDate)
api.apiAutoPlanCreate({autoMealPlan: autoMealPlan.value}).then(r => {
dialog.value = false
useMealPlanStore().refreshLastUpdatedPeriod()
initializeRequest()
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS)
}).catch(err => {
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
}).finally(() => {
loading.value = false
})
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,186 @@
<template>
<v-dialog max-width="1200px" :activator="props.activator" v-model="dialog">
<v-card :loading="loading">
<v-closable-card-title
:title="$t('BatchEdit')"
:sub-title="$t('BatchEditUpdatingItemsCount', {type: $t('Foods'), count: updateItems.length})"
:icon="TFood.icon"
v-model="dialog"
></v-closable-card-title>
<v-divider></v-divider>
<v-card-text>
<v-form>
<v-row>
<v-col cols="12" md="6">
<v-card :title="$t('Miscellaneous')" prepend-icon="fa-solid fa-list" variant="flat">
<v-card-text>
<model-select model="SupermarketCategory" v-model="batchUpdateRequest.foodBatchUpdate.category" :object="false" allow-create mode="single">
</model-select>
<v-select :items="boolUpdateOptions" :label="$t('Ignore_Shopping')" clearable v-model="batchUpdateRequest.foodBatchUpdate.ignoreShopping"></v-select>
<v-select :items="boolUpdateOptions" :label="$t('OnHand')" clearable v-model="batchUpdateRequest.foodBatchUpdate.onHand"></v-select>
<v-spacer></v-spacer>
<v-label :text="$t('Substitutes')"></v-label>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="Food" v-model="batchUpdateRequest.foodBatchUpdate.substituteSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('Substitutes')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.substituteRemoveAll"></v-checkbox>
<v-select :items="boolUpdateOptions" :label="$t('substitute_siblings')" clearable v-model="batchUpdateRequest.foodBatchUpdate.substituteChildren"></v-select>
<v-select :items="boolUpdateOptions" :label="$t('substitute_children')" clearable v-model="batchUpdateRequest.foodBatchUpdate.substituteSiblings"></v-select>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card :title="$t('Hierarchy')" prepend-icon="fa-solid fa-folder-tree" variant="flat">
<v-card-text>
<model-select model="Food" :label="$t('Parent')" :object="false" allow-create clearable v-model="batchUpdateRequest.foodBatchUpdate.parentSet">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-select :items="boolUpdateOptions" :label="$t('RemoveParent')" clearable v-model="batchUpdateRequest.foodBatchUpdate.parentRemove"></v-select>
<v-spacer></v-spacer>
<v-label :text="$t('InheritFields')"></v-label>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('InheritFields')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.inheritFieldsRemoveAll"></v-checkbox>
<v-spacer></v-spacer>
<v-label :text="$t('ChildInheritFields')"></v-label>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsAdd" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-add"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsRemove" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-minus"></v-icon>
</template>
</model-select>
<model-select model="FoodInheritField" v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsSet" :object="false" allow-create mode="tags">
<template #prepend>
<v-icon icon="fa-solid fa-equals"></v-icon>
</template>
</model-select>
<v-checkbox :label="$t('RemoveAllType', {type: $t('ChildInheritFields')})" hide-details
v-model="batchUpdateRequest.foodBatchUpdate.childInheritFieldsRemoveAll"></v-checkbox>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn :disabled="loading" @click="dialog = false">{{ $t('Cancel') }}</v-btn>
<v-btn color="warning" :loading="loading" @click="batchUpdateFoods()" :disabled="updateItems.length < 1">{{ $t('Update') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {EditorSupportedModels, EditorSupportedTypes, getGenericModelFromString, TFood, TKeyword, TRecipe} from "@/types/Models.ts";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {useI18n} from "vue-i18n";
import {ApiApi, ApiFoodBatchUpdateUpdateRequest, ApiRecipeBatchUpdateUpdateRequest, Food, Recipe, RecipeOverview} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const emit = defineEmits(['change'])
const props = defineProps({
items: {type: Array as PropType<Array<Food>>, required: true},
activator: {type: String, default: 'parent'},
})
const {t} = useI18n()
const dialog = defineModel<boolean>({default: false})
const loading = ref(false)
const updateItems = ref([] as Food[])
const batchUpdateRequest = ref({foodBatchUpdate: {}} as ApiFoodBatchUpdateUpdateRequest)
const boolUpdateOptions = ref([
{value: true, title: t('Yes')},
{value: false, title: t('No')},
])
/**
* copy prop when dialog opens so that items remain when parent is updated after change is emitted
*/
watch(dialog, (newValue, oldValue) => {
if (!oldValue && newValue && props.items != undefined) {
batchUpdateRequest.value.foodBatchUpdate.foods = props.items.flatMap(r => r.id!)
updateItems.value = JSON.parse(JSON.stringify(props.items))
}
})
/**
* perform batch request to update recipes
*/
function batchUpdateFoods() {
let api = new ApiApi()
loading.value = true
api.apiFoodBatchUpdateUpdate(batchUpdateRequest.value).then(r => {
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
emit('change')
loading.value = false
})
}
</script>
<style scoped>
</style>

View File

@@ -14,6 +14,7 @@
<v-list-item link title="Space" @click="window = 'space'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Recipes')" @click="window = 'recipes'" prepend-icon="$recipes"></v-list-item>
<v-list-item link :title="$t('Import')" @click="window = 'import'" prepend-icon="$import"></v-list-item>
<v-list-item link :title="$t('AI')" @click="window = 'ai'" prepend-icon="$ai"></v-list-item>
<v-list-item link :title="$t('Unit')" @click="window = 'unit'" prepend-icon="fa-solid fa-scale-balanced"></v-list-item>
<v-list-item link :title="$t('Food')" @click="window = 'food'" prepend-icon="fa-solid fa-carrot"></v-list-item>
<v-list-item link :title="$t('Keyword')" @click="window = 'keyword'" prepend-icon="fa-solid fa-tags"></v-list-item>
@@ -45,7 +46,7 @@
<v-btn class="mt-2 ms-2" color="info" href="https://github.com/TandoorRecipes/recipes" target="_blank" prepend-icon="fa-solid fa-code-branch">GitHub
</v-btn>
<v-alert class="mt-3" border="start" variant="tonal" color="success">
<v-alert class="mt-3" border="start" variant="tonal" color="success" v-if="(!useUserPreferenceStore().serverSettings.hosted && !useUserPreferenceStore().activeSpace.demo)">
<v-alert-title>Did you know?</v-alert-title>
Tandoor is Open Source and available to anyone for free to host on their own server. Thousands of hours have been spend
making Tandoor what it is today. You can help make Tandoor even better by contributing or helping financing the effort.
@@ -105,6 +106,35 @@
<v-btn color="primary" variant="tonal" prepend-icon="$import" class="me-2" :to="{name: 'RecipeImportPage'}">{{ $t('Import') }}</v-btn>
</v-window-item>
<v-window-item value="ai">
<p class="mt-3">Tandoor has several functions that allow you to use AI to automatically perform certain tasks like importing recipes from a PDFs or images.
</p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To use AI you must first configure an AI Provider. This can also be done globally for all spaces by the person operating your Tandoor Server.
</p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Some AI Providers are available globally for every space to use. You can also configure additional AI Providers for your space only.
</p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To prevent accidental AI cost you can review your AI usage using the AI Log. The Server Administrator can also set AI usage limits for your space (either monthly or using a balance).
</p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Depending on your subscription you will have different AI Credits available for your space every month. Additionally you might have a Credit balance
that will be used once your monthly limit is reached.
</p>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'ModelListPage', params: {model: 'AiProvider'}}">
{{ $t('AiProvider') }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'ModelListPage', params: {model: 'AiLog'}}">
{{ $t('AiLog') }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$ai" class="me-2" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$import" class="me-2" :to="{name: 'RecipeImportPage'}">{{ $t('Import') }}</v-btn>
</v-window-item>
<v-window-item value="unit">
<p class="mt-3">Units allow you to measure how much of something you need in a recipe or on a shopping list.
@@ -337,6 +367,7 @@
<script setup lang="ts">
import {ref} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const drawer = defineModel()
const window = ref('start')

View File

@@ -10,7 +10,7 @@
<v-row>
<v-col>
<v-textarea :model-value="importLog.msg"></v-textarea>
<v-textarea :model-value="importLog.msg" max-rows="25" auto-grow></v-textarea>
</v-col>
</v-row>

View File

@@ -36,11 +36,16 @@
<td style="width: 1%; text-wrap: nowrap" class="pa-0 d-print-none" v-if="showCheckbox">
<v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn>
</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1"
v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)" v-if="!i.noAmount"></td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-if="i.noAmount"></td>
<!-- display calculated food amount or empty cell -->
<td style="width: 1%; text-wrap: nowrap"
class="pr-1"
v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)"
v-if="!i.noAmount && i.amount != 0">
</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-else></td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1">
<template v-if="i.unit && !i.noAmount"> {{ ingredientToUnitString(i, ingredientFactor) }}</template>
<template v-if="i.unit && !i.noAmount && i.amount != 0"> {{ ingredientToUnitString(i, ingredientFactor) }}</template>
</td>
<td>
<template v-if="i.food">

View File

@@ -131,9 +131,9 @@ function refreshVisiblePeriod(startDateUnknown: boolean) {
// load backwards to as on initial
if (startDateUnknown) {
useMealPlanStore().refreshFromAPI(DateTime.fromJSDate(calendarDate.value).minus({days: days}).toJSDate(), DateTime.now().plus({days: days}).toJSDate())
useMealPlanStore().refreshFromAPI(DateTime.fromJSDate(calendarDate.value).minus({days: days}).toJSDate(), DateTime.fromJSDate(calendarDate.value).plus({days: days}).toJSDate())
} else {
useMealPlanStore().refreshFromAPI(calendarDate.value, DateTime.now().plus({days: days}).toJSDate())
useMealPlanStore().refreshFromAPI(calendarDate.value, DateTime.fromJSDate(calendarDate.value).plus({days: days}).toJSDate())
}
}

View File

@@ -13,12 +13,17 @@
<template v-if="route.name == 'MealPlanPage'">
<v-divider></v-divider>
<v-list-item prepend-icon="fa-solid fa-calendar-plus" link>
{{$t('Auto_Planner')}}
<auto-plan-dialog></auto-plan-dialog>
</v-list-item>
<v-list-subheader>{{$t('Settings')}}</v-list-subheader>
<v-list-item>
<meal-plan-device-settings></meal-plan-device-settings>
</v-list-item>
</template>
</template>
<script setup lang="ts">
@@ -27,6 +32,7 @@ import {useRoute} from "vue-router";
import {getListModels} from "@/types/Models";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import MealPlanDeviceSettings from "@/components/settings/MealPlanDeviceSettings.vue";
import AutoPlanDialog from "@/components/dialogs/AutoPlanDialog.vue";
const route = useRoute()

View File

@@ -0,0 +1,27 @@
<template>
<i class="fa-solid fa-lock"></i>
<span v-if="props.showText" class="ms-1 me-1">{{ $t('Private_Recipe') }}</span>
<v-chip class="me-1 mb-1" :color="props.color" :size="props.size" :variant="props.variant" v-for="u in users" :key="u.id" prepend-icon="fa-solid fa-share-nodes"> {{ u.displayName }}
</v-chip>
</template>
<script setup lang="ts">
import {User} from "@/openapi";
import {PropType} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
showText: {type: Boolean, default: true},
users: {type: [] as PropType<Array<User>>, required: false},
size: {type: String, default: 'x-small'},
color: {type: String, default: ''},
variant: {type: String as PropType<NonNullable<"tonal" | "flat" | "text" | "elevated" | "outlined" | "plain"> | undefined>, default: 'tonal'},
})
</script>
<style scoped>
</style>

View File

@@ -1,40 +1,11 @@
<template>
<v-card class="mt-1" v-if="cookLogs.length > 0">
<v-card-title>{{ $t('Activity') }}</v-card-title>
<v-card-text>
<v-list>
<v-list-item v-for="c in cookLogs.sort((a,b) => a.createdAt! > b.createdAt! ? 1 : -1)" :key="c.id">
<template #prepend>
<v-avatar color="primary">V</v-avatar>
</template>
<v-list-item-title class="font-weight-bold">{{ c.createdBy.displayName }}
<v-rating density="comfortable" size="x-small" color="tandoor" class="float-right" v-model="c.rating" readonly v-if="c.rating != undefined"></v-rating>
</v-list-item-title>
{{ c.comment }}
<p v-if="c.servings != null && c.servings > 0">
{{ c.servings }}
<span v-if="recipe.servingsText != ''">{{ recipe.servingsText }}</span>
<span v-else-if="c.servings == 1">{{ $t('Serving') }}</span>
<span v-else>{{ $t('Servings') }}</span>
</p>
<p class="text-disabled">
{{ DateTime.fromJSDate(c.createdAt).toLocaleString(DateTime.DATETIME_SHORT) }}
</p>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<v-card class="mt-1 d-print-none" v-if="useUserPreferenceStore().isAuthenticated">
<v-card class="mt-1 d-print-none" v-if="useUserPreferenceStore().isAuthenticated" :loading="loading">
<v-card-text>
<v-textarea :label="$t('Comment')" rows="2" v-model="newCookLog.comment"></v-textarea>
<v-row de>
<v-row dense>
<v-col cols="12" md="4">
<v-label>{{$t('Rating')}}</v-label><br/>
<v-label>{{ $t('Rating') }}</v-label>
<br/>
<v-rating v-model="newCookLog.rating" clearable hover density="compact"></v-rating>
</v-col>
<v-col cols="12" md="4">
@@ -52,6 +23,48 @@
</v-card-actions>
</v-card>
<v-card class="mt-1" v-if="cookLogs.length > 0" :loading="loading">
<v-card-title>{{ $t('Activity') }}</v-card-title>
<v-card-text>
<v-list>
<v-list-item class="border-t-sm" v-for="c in cookLogs" :key="c.id" :link="c.createdBy.id == useUserPreferenceStore().userSettings?.user.id">
<template #prepend>
<v-avatar color="primary">{{ c.createdBy.displayName.charAt(0) }}</v-avatar>
</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>
<v-list-item-subtitle class="font-italic mt-1" v-if="c.servings != null && c.servings > 0">
{{ c.servings }}
<span v-if="recipe.servingsText != ''">{{ recipe.servingsText }}</span>
<span v-else-if="c.servings == 1">{{ $t('Serving') }}</span>
<span v-else>{{ $t('Servings') }}</span>
</v-list-item-subtitle>
<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-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 }">
<span v-bind="props">{{ DateTime.fromJSDate(c.createdAt).toRelative({style: 'narrow'}) }}</span>
</template>
</v-tooltip>
</v-list-item-action>
</template>
<model-edit-dialog model="CookLog" :item="c" v-if="c.createdBy.id == useUserPreferenceStore().userSettings?.user.id" @save="recLoadCookLog(props.recipe.id)" @delete="recLoadCookLog(props.recipe.id)"></model-edit-dialog>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</template>
@@ -63,6 +76,7 @@ import {DateTime} from "luxon";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {VDateInput} from 'vuetify/labs/VDateInput'
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
const props = defineProps({
recipe: {
@@ -74,21 +88,31 @@ const props = defineProps({
const newCookLog = ref({} as CookLog);
const cookLogs = ref([] as CookLog[])
const loading = ref(false)
onMounted(() => {
refreshActivity()
recLoadCookLog(props.recipe.id)
resetForm()
})
/**
* load cook logs from database for given recipe
* recursively load cook logs from database for given recipe
*/
function refreshActivity() {
function recLoadCookLog(recipeId: number, page: number = 1) {
const api = new ApiApi()
api.apiCookLogList({recipe: props.recipe.id}).then(r => {
// TODO pagination
loading.value = true
if(page == 1){
cookLogs.value = []
}
api.apiCookLogList({recipe: props.recipe.id, page: page}).then(r => {
if (r.results) {
cookLogs.value = r.results
cookLogs.value = cookLogs.value.concat(r.results)
if (r.next) {
recLoadCookLog(recipeId, page + 1)
} else {
cookLogs.value = cookLogs.value.sort((a, b) => a.createdAt! > b.createdAt! ? 1 : -1)
loading.value = false
}
}
})
}

View File

@@ -19,6 +19,10 @@
<!-- <p class="text-disabled">{{ props.recipe.createdBy.displayName}}</p>-->
<keywords-component variant="outlined" :keywords="props.recipe.keywords" :max-keywords="3" v-if="props.showKeywords">
<template #prepend>
<v-chip class="mb-1 me-1" size="x-small" label variant="outlined" v-if="recipe._private">
<private-recipe-badge :show-text="false"></private-recipe-badge>
</v-chip>
<v-chip class="mb-1 me-1" size="x-small" label variant="outlined" color="info"
v-if="props.recipe.internal == false">
{{ $t('External') }}
@@ -100,6 +104,7 @@ import {Recipe, RecipeOverview} from "@/openapi";
import RecipeContextMenu from "@/components/inputs/RecipeContextMenu.vue";
import RecipeImage from "@/components/display/RecipeImage.vue";
import {useRouter} from "vue-router";
import PrivateRecipeBadge from "@/components/display/PrivateRecipeBadge.vue";
const props = defineProps({
recipe: {type: {} as PropType<Recipe | RecipeOverview>, required: true,},

View File

@@ -28,6 +28,7 @@
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated"></recipe-context-menu>
</v-sheet>
<keywords-component variant="flat" class="ms-1" :keywords="recipe.keywords"></keywords-component>
<private-recipe-badge :users="recipe.shared" v-if="recipe._private"></private-recipe-badge>
<v-rating v-model="recipe.rating" size="x-small" v-if="recipe.rating" half-increments readonly></v-rating>
<v-sheet class="ps-2 text-disabled">
{{ recipe.description }}
@@ -35,8 +36,7 @@
</v-card>
</v-card>
<!-- only display values if not all are default (e.g. for external recipes) -->
<v-card class="mt-1" v-if="recipe.workingTime != 0 || recipe.waitingTime != 0 || recipe.servings != 1">
<v-card class="mt-1">
<v-container>
<v-row class="text-center text-body-2">
<v-col class="pt-1 pb-1">
@@ -85,6 +85,8 @@
<i>{{ recipe.description }}</i>
</p>
<private-recipe-badge :users="recipe.shared" v-if="recipe._private"></private-recipe-badge>
<v-rating v-model="recipe.rating" size="x-small" v-if="recipe.rating" readonly></v-rating>
<keywords-component variant="flat" class="mt-4" :keywords="recipe.keywords"></keywords-component>
@@ -119,11 +121,16 @@
<template v-if="recipe.filePath">
<external-recipe-viewer class="mt-2" :recipe="recipe"></external-recipe-viewer>
<v-card :title="$t('AI')" prepend-icon="$ai" @click="aiConvertRecipe()" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading"
<v-card :title="$t('AI')" prepend-icon="$ai" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading || !useUserPreferenceStore().activeSpace.aiEnabled"
v-if="!recipe.internal">
<v-card-text>
Convert the recipe using AI
{{$t('ConvertUsingAI')}}
<model-select model="AiProvider" v-model="selectedAiProvider">
<template #append>
<v-btn @click="aiConvertRecipe()" icon="fa-solid fa-person-running" color="success"></v-btn>
</template>
</model-select>
</v-card-text>
</v-card>
</template>
@@ -147,7 +154,7 @@
:title="$t('CreatedBy')"
:subtitle="recipe.createdBy.displayName"
prepend-icon="fa-solid fa-user"
:to="{name: 'SearchPage', query: {createdby: recipe.createdBy.id!}}">
:to="(useUserPreferenceStore().isAuthenticated) ? {name: 'SearchPage', query: {createdby: recipe.createdBy.id!}}: undefined">
</v-card>
</v-col>
<v-col cols="12" md="3">
@@ -156,7 +163,7 @@
:title="$t('Created')"
:subtitle="DateTime.fromJSDate(recipe.createdAt).toLocaleString(DateTime.DATETIME_MED)"
prepend-icon="$create"
:to="{name: 'SearchPage', query: {createdon: DateTime.fromJSDate(recipe.createdAt).toISODate()}}">
:to="(useUserPreferenceStore().isAuthenticated) ? {name: 'SearchPage', query: {createdon: DateTime.fromJSDate(recipe.createdAt).toISODate()}} : undefined">
</v-card>
</v-col>
<v-col cols="12" md="3">
@@ -165,7 +172,7 @@
:title="$t('Updated')"
:subtitle="DateTime.fromJSDate(recipe.updatedAt).toLocaleString(DateTime.DATETIME_MED)"
prepend-icon="$edit"
:to="{name: 'SearchPage', query: {updatedon: DateTime.fromJSDate(recipe.updatedAt).toISODate()}}">
:to="(useUserPreferenceStore().isAuthenticated) ? {name: 'SearchPage', query: {updatedon: DateTime.fromJSDate(recipe.updatedAt).toISODate()}}: undefined">
</v-card>
</v-col>
<v-col cols="12" md="3" v-if="recipe.sourceUrl">
@@ -189,7 +196,7 @@
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {ApiApi, Recipe} from "@/openapi"
import {AiProvider, ApiApi, Recipe} from "@/openapi"
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue"
import StepsOverview from "@/components/display/StepsOverview.vue";
import RecipeActivity from "@/components/display/RecipeActivity.vue";
@@ -204,6 +211,8 @@ import PropertyView from "@/components/display/PropertyView.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useFileApi} from "@/composables/useFileApi.ts";
import PrivateRecipeBadge from "@/components/display/PrivateRecipeBadge.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const {request, release} = useWakeLock()
const {doAiImport, fileApiLoading} = useFileApi()
@@ -214,6 +223,8 @@ const recipe = defineModel<Recipe>({required: true})
const servings = ref(1)
const showFullRecipeName = ref(false)
const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider)
/**
* factor for multiplying ingredient amounts based on recipe base servings and user selected servings
*/
@@ -246,7 +257,7 @@ onBeforeUnmount(() => {
function aiConvertRecipe() {
let api = new ApiApi()
doAiImport(null, '', recipe.value.id!).then(r => {
doAiImport(selectedAiProvider.value.id!,null, '', recipe.value.id!).then(r => {
if (r.recipe) {
recipe.value.internal = true
recipe.value.steps = r.recipe.steps

View File

@@ -16,16 +16,15 @@
<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 class="ms-1" v-if="a.unit">{{ a.unit.name }}</span>
<span class="ms-1" v-if="a.unit">{{ pluralString(a.unit, a.amount) }}</span>
</span>
</b>
</span>
<br/>
</span>
</div>
<div class="d-flex flex-column flex-grow-1 align-self-center">
{{ shoppingListFood.food.name }} <br/>
{{ pluralString(shoppingListFood.food, (amounts.length > 1 || (amounts.length == 1 && amounts[0].amount > 1) ? 2 : 1)) }} <br/>
<span v-if="infoRow"><small class="text-disabled">{{ infoRow }}</small></span>
</div>
</div>
@@ -59,6 +58,7 @@ import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping";
import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils";
import ShoppingLineItemDialog from "@/components/dialogs/ShoppingLineItemDialog.vue";
import {pluralString} from "@/utils/model_utils.ts";
const emit = defineEmits(['clicked'])

View File

@@ -1,10 +1,19 @@
<template>
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title><i class="far fa-list-alt fa-fw me-2"></i> {{ $t('StepsOverview') }}</v-expansion-panel-title>
<v-expansion-panel-title>
<i class="far fa-list-alt fa-fw me-2"></i> {{ $t('StepsOverview') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-container>
<v-row v-for="(s, i) in props.steps">
<v-row>
<v-col>
<v-btn-toggle density="compact" v-model="useUserPreferenceStore().deviceSettings.recipe_mergeStepOverview" border divided>
<v-btn :value="false" prepend-icon="fa-solid fa-folder-tree">{{ $t('Structured') }}</v-btn>
<v-btn :value="true" prepend-icon="fa-solid fa-arrows-to-circle">{{ $t('Summary') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-for="(s, i) in props.steps" v-if="!useUserPreferenceStore().deviceSettings.recipe_mergeStepOverview">
<v-col class="pa-1" cols="12" md="6">
<b v-if="s.showAsHeader">{{ i + 1 }}. {{ s.name }} </b>
<ingredients-table v-model="s.ingredients" :ingredient-factor="props.ingredientFactor"></ingredients-table>
@@ -21,7 +30,13 @@
</template>
</v-col>
</v-row>
</v-container>
<v-row v-if="useUserPreferenceStore().deviceSettings.recipe_mergeStepOverview">
<v-col class="pa-1" cols="12" md="6">
<ingredients-table v-model="mergedIngredients" :ingredient-factor="props.ingredientFactor" :show-checkbox="false"></ingredients-table>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
@@ -30,10 +45,10 @@
</template>
<script setup lang="ts">
import {PropType} from 'vue'
import {Step} from "@/openapi";
import {computed, PropType, ref} from 'vue'
import {Ingredient, Step} from "@/openapi";
import IngredientsTable from "@/components/display/IngredientsTable.vue";
import StepView from "@/components/display/StepView.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
steps: {
@@ -46,6 +61,70 @@ const props = defineProps({
},
})
const showMergedIngredients = ref(false)
const mergedIngredients = computed(() => {
// Function to collect all ingredients from recipe steps
const getAllIngredients = () => {
const ingredients: Array<Ingredient> = [];
// Add ingredients from steps
props.steps.forEach(step => {
step.ingredients.forEach(ingredient => {
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
ingredients.push(ingredient);
}
});
// Add ingredients from step recipes if they exist
if (step.stepRecipeData) {
step.stepRecipeData.steps?.forEach((subStep: Step) => {
subStep.ingredients.forEach((ingredient: Ingredient) => {
if (ingredient.food && !ingredient.isHeader && !ingredient.noAmount) {
ingredients.push(ingredient);
}
});
});
}
});
return ingredients;
};
// Get all ingredients
const allIngredients = getAllIngredients();
// Create a map to group and sum ingredients by food and unit
const groupedIngredients = new Map<string, Ingredient>();
allIngredients.forEach(ingredient => {
if (!ingredient.food || !ingredient.unit) return;
// Create a unique key for food-unit combination
const key = `${ingredient.food.id}-${ingredient.unit.id}`;
if (groupedIngredients.has(key)) {
// If this food-unit combination already exists, sum the amounts
const existingIngredient = groupedIngredients.get(key)!;
existingIngredient.amount += ingredient.amount;
} else {
// Create a new entry with the adjusted amount
const clonedIngredient = {...ingredient};
groupedIngredients.set(key, clonedIngredient);
}
});
// Convert the map back to an array
const result = Array.from(groupedIngredients.values());
// Sort alphabetically by food name
return result.sort((a, b) => {
const foodNameA = a.food?.name.toLowerCase() || '';
const foodNameB = b.food?.name.toLowerCase() || '';
return foodNameA.localeCompare(foodNameB);
});
})
</script>

View File

@@ -15,6 +15,8 @@
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
const emit = defineEmits(['stop'])
import {computed, onMounted, ref} from "vue";
@@ -24,6 +26,8 @@ const props = defineProps({
seconds: {type: Number, required: true}
})
const {t} = useI18n()
const initialDurationSeconds = ref(props.seconds)
const durationSeconds = ref(initialDurationSeconds.value)
const timerRunning = ref(true)

View File

@@ -58,7 +58,6 @@
<template v-if="hasMoreItems && !loading" #afterlist>
<span class="text-disabled font-italic text-caption ms-3">{{ $t('ModelSelectResultsHelp') }}</span>
</template>
</Multiselect>
<template #append v-if="$slots.append">
@@ -73,7 +72,7 @@
import {computed, onBeforeMount, onMounted, PropType, ref, useTemplateRef} from "vue"
import {EditorSupportedModels, GenericModel, getGenericModelFromString} from "@/types/Models"
import Multiselect from '@vueform/multiselect'
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
import {ErrorMessageType, MessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
const {t} = useI18n()
@@ -171,7 +170,7 @@ function search(query: string) {
*/
async function createObject(object: any, select$: Multiselect) {
return await modelClass.value.create({name: object[itemLabel.value]}).then((createdObj: any) => {
useMessageStore().addMessage(MessageType.SUCCESS, 'Created', 5000, createdObj)
useMessageStore().addPreparedMessage(PreparedMessage.CREATE_SUCCESS, createdObj)
emit('create', object)
return createdObj
}).catch((err: any) => {

View File

@@ -6,9 +6,8 @@
<v-list-item :to="{ name: 'ModelEditPage', params: {model: 'recipe', id: recipe.id} }" prepend-icon="$edit">
{{ $t('Edit') }}
</v-list-item>
<v-list-item prepend-icon="$mealplan" link>
<v-list-item prepend-icon="$mealplan" @click="mealPlanDialog = true">
{{ $t('Add_to_Plan') }}
<model-edit-dialog model="MealPlan" :itemDefaults="{recipe: recipe, servings: recipe.servings}"></model-edit-dialog>
</v-list-item>
<v-list-item prepend-icon="$shopping" link>
{{ $t('Add_to_Shopping') }}
@@ -30,11 +29,12 @@
</v-menu>
</v-btn>
<model-edit-dialog model="MealPlan" :itemDefaults="{recipe: recipe, servings: recipe.servings}" :close-after-create="false" :close-after-save="false" v-model="mealPlanDialog"></model-edit-dialog>
</template>
<script setup lang="ts">
import {nextTick, PropType} from 'vue'
import {nextTick, PropType, ref} from 'vue'
import {Recipe, RecipeFlat, RecipeOverview} from "@/openapi";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import RecipeShareDialog from "@/components/dialogs/RecipeShareDialog.vue";
@@ -46,6 +46,8 @@ const props = defineProps({
size: {type: String, default: 'medium'},
})
const mealPlanDialog = ref(false)
function openPrintView() {
print()
}

View File

@@ -61,7 +61,7 @@
<div v-if="!mobile">
<vue-draggable v-model="step.ingredients" handle=".drag-handle" :on-sort="sortIngredients" :empty-insert-threshold="25" group="ingredients">
<v-row v-for="(ingredient, index) in step.ingredients" :key="ingredient.id" dense>
<v-col cols="12" class="pa-0 ma-0 text-center text-disabled">
<v-col cols="12" class="pa-0 ma-0 text-center text-disabled" v-if="ingredient.originalText">
<v-icon icon="$import" size="x-small"></v-icon>
{{ ingredient.originalText }}
</v-col>
@@ -306,6 +306,7 @@ function parseAndInsertIngredients() {
r.forEach(i => {
console.log(i)
step.value.ingredients.push({
originalText: i.value.originalText,
amount: i.value.amount,
food: i.value.food,
unit: i.value.unit,

View File

@@ -0,0 +1,105 @@
<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>
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
<v-text-field :label="$t('APIKey')" v-model="editingObj.apiKey"></v-text-field>
<v-combobox :label="$t('Model')" :items="aiModels" v-model="editingObj.modelName" hide-details>
</v-combobox>
<p class="mt-2 mb-2">{{ $t('AiModelHelp') }} <a href="https://docs.litellm.ai/docs/providers" target="_blank">LiteLLM</a></p>
<v-checkbox :label="$t('LogCredits')" :hint="$t('LogCreditsHelp')" v-model="editingObj.logCreditCost" v-if="useUserPreferenceStore().userSettings.user.isSuperuser" persistent-hint
class="mb-2"></v-checkbox>
<v-text-field :label="$t('Url')" v-model="editingObj.url"></v-text-field>
<v-checkbox :label="$t('Global')" :hint="$t('GlobalHelp')" v-model="globalProvider" v-if="useUserPreferenceStore().userSettings.user.isSuperuser" persistent-hint
class="mb-2"></v-checkbox>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {AiProvider} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import editor from "mavon-editor";
const props = defineProps({
item: {type: {} as PropType<AiProvider>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<AiProvider>, required: false, default: {} as AiProvider},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<AiProvider>('AiProvider', 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 aiModels = ref(['gemini/gemini-2.5-pro', 'gemini/gemini-2.5-flash', 'gemini/gemini-2.5-flash-lite', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'])
const globalProvider = ref(false)
watch(() => globalProvider.value, () => {
if (globalProvider.value) {
editingObj.value.space = undefined
} else {
editingObj.value.space = useUserPreferenceStore().activeSpace.id!
}
})
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor() {
setupState(props.item, props.itemId, {
itemDefaults: props.itemDefaults,
newItemFunction: () => {
editingObj.value.logCreditCost = true
editingObj.value.space = useUserPreferenceStore().activeSpace.id!
},
}).then(() => {
globalProvider.value = editingObj.value.space == undefined
})
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,81 @@
<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>
<v-form :disabled="loading">
<v-textarea :label="$t('Comment')" rows="2" v-model="editingObj.comment"></v-textarea>
<v-row dense>
<v-col cols="12" md="4">
<v-label>{{ $t('Rating') }}</v-label>
<br/>
<v-rating v-model="editingObj.rating" clearable hover density="compact"></v-rating>
</v-col>
<v-col cols="12" md="4">
<v-number-input :label="$t('Servings')" v-model="editingObj.servings" :precision="2"></v-number-input>
</v-col>
<v-col cols="12" md="4">
<v-date-input :label="$t('Date')" v-model="editingObj.createdAt"></v-date-input>
</v-col>
</v-row>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, watch} from "vue";
import {CookLog} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
const props = defineProps({
item: {type: {} as PropType<CookLog>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<CookLog>, required: false, default: {} as CookLog},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<CookLog>('CookLog', 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)
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor() {
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}
</script>
<style scoped>
</style>

View File

@@ -54,7 +54,7 @@
<properties-editor v-model="editingObj.properties" :amount-for="propertiesAmountFor"></properties-editor>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 60px;"></v-spacer>
<v-spacer style="margin-top: 80px;"></v-spacer>
</v-form>
</v-tabs-window-item>
@@ -106,7 +106,7 @@
</v-card>
</v-form>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 60px;"></v-spacer>
<v-spacer style="margin-top: 80px;"></v-spacer>
</v-tabs-window-item>
<v-tabs-window-item value="hierarchy">
@@ -119,6 +119,9 @@
mode="tags"></ModelSelect>
<ModelSelect model="FoodInheritField" v-model="editingObj.childInheritFields" :label="$t('ChildInheritFields')" :hint="$t('ChildInheritFields_help')"
mode="tags"></ModelSelect>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 100px;"></v-spacer>
</v-tabs-window-item>
<v-tabs-window-item value="misc">

View File

@@ -16,7 +16,7 @@
<v-date-input :label="$t('Valid Until')" v-model="editingObj.validUntil"></v-date-input>
<v-textarea :label="$t('Note')" v-model="editingObj.internalNote"></v-textarea>
<v-checkbox :label="$t('Reusable')" v-model="editingObj.reusable"></v-checkbox>
<v-text-field :label="$t('Link')" readonly :model-value="inviteLinkUrl(editingObj)">
<v-text-field :label="$t('Link')" readonly :model-value="inviteLinkUrl(editingObj)" v-if="isUpdate()">
<template #append-inner>
<btn-copy variant="plain" color="undefined" :copy-value="inviteLinkUrl(editingObj)"></btn-copy>
</template>
@@ -37,6 +37,7 @@ import {DateTime} from "luxon";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useDjangoUrls} from "@/composables/useDjangoUrls.ts";
const props = defineProps({
@@ -91,7 +92,7 @@ function initializeEditor(){
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return `${location.protocol}//${location.host}/invite/${inviteLink.uuid}`
return useDjangoUrls().getDjangoUrl(`/invite/${inviteLink.uuid}`)
}

View File

@@ -63,8 +63,8 @@
</v-col>
</v-row>
<!-- <closable-help-alert :text="$t('RecipeStepsHelp')" :action-text="$t('Steps')" @click="tab='steps'"></closable-help-alert>-->
<v-btn @click="tab='steps'" class="float-right" variant="tonal" append-icon="fa-solid fa-arrow-right">{{$t('Steps')}} </v-btn>
<!-- <closable-help-alert :text="$t('RecipeStepsHelp')" :action-text="$t('Steps')" @click="tab='steps'"></closable-help-alert>-->
<v-btn @click="tab='steps'" class="float-right" variant="tonal" append-icon="fa-solid fa-arrow-right">{{ $t('Steps') }}</v-btn>
</v-form>
</v-tabs-window-item>
@@ -77,12 +77,19 @@
</v-row>
<v-row>
<v-col class="text-center">
<v-btn-group density="compact">
<v-btn-group density="compact" divided border>
<v-btn color="success" prepend-icon="fa-solid fa-plus" @click="addStep()">{{ $t('Add_Step') }}</v-btn>
<v-btn color="warning" @click="dialogStepManager = true">
<v-btn color="warning" @click="dialogStepManager = true" :disabled="editingObj.steps.length < 2">
<v-icon icon="fa-solid fa-arrow-down-1-9"></v-icon>
</v-btn>
<v-btn prepend-icon="fa-solid fa-maximize" @click="handleSplitAllSteps" :disabled="editingObj.steps.length < 1"><span
v-if="!mobile">{{ $t('Split') }}</span></v-btn>
<v-btn prepend-icon="fa-solid fa-minimize" @click="handleMergeAllSteps" :disabled="editingObj.steps.length < 2"><span
v-if="!mobile">{{ $t('Merge') }}</span></v-btn>
</v-btn-group>
</v-col>
</v-row>
@@ -101,16 +108,25 @@
<v-text-field :label="$t('Imported_From')" v-model="editingObj.sourceUrl"></v-text-field>
<v-checkbox :label="$t('Private_Recipe')" persistent-hint :hint="$t('Private_Recipe_Help')" v-model="editingObj._private"></v-checkbox>
<model-select mode="tags" model="User" :label="$t('Share')" persistent-hint v-model="editingObj.shared"
<model-select mode="tags" model="User" :label="$t('Share')" persistent-hint v-model="editingObj.shared"
append-to-body v-if="editingObj._private"></model-select>
<div class="mt-2" v-if="editingObj.filePath">
{{ $t('ExternalRecipe') }}
<v-text-field readonly v-model="editingObj.filePath"></v-text-field>
<v-btn prepend-icon="$delete" color="error" :loading="loading">{{ $t('delete_title', {type: $t('ExternalRecipe')}) }}
<delete-confirm-dialog :object-name="editingObj.filePath" :model-name="$t('ExternalRecipe')" @delete="deleteExternalFile()"></delete-confirm-dialog>
</v-btn>
</div>
</v-form>
</v-tabs-window-item>
</v-tabs-window>
</v-card-text>
<v-card-text v-if="isSpaceAtRecipeLimit(useUserPreferenceStore().activeSpace)">
<v-alert color="warning" icon="fa-solid fa-triangle-exclamation">
{{$t('SpaceLimitReached')}}
{{ $t('SpaceLimitReached') }}
<v-btn color="success" variant="flat" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
</v-alert>
</v-card-text>
@@ -138,7 +154,7 @@
<script setup lang="ts">
import {onMounted, PropType, ref, shallowRef, watch} from "vue";
import {Ingredient, Recipe, Step} from "@/openapi";
import {ApiApi, Ingredient, Recipe, Step} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
@@ -151,7 +167,9 @@ import ClosableHelpAlert from "@/components/display/ClosableHelpAlert.vue";
import {useDisplay} from "vuetify";
import {isSpaceAtRecipeLimit} from "@/utils/logic_utils";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import SpaceSettings from "@/components/settings/SpaceSettings.vue";
import {mergeAllSteps, splitAllSteps} from "@/utils/step_utils.ts";
import DeleteConfirmDialog from "@/components/dialogs/DeleteConfirmDialog.vue";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
const props = defineProps({
@@ -188,7 +206,7 @@ onMounted(() => {
/**
* component specific state setup logic
*/
function initializeEditor(){
function initializeEditor() {
setupState(props.item, props.itemId, {
newItemFunction: () => {
editingObj.value.steps = [] as Step[]
@@ -249,6 +267,33 @@ function deleteStepAtIndex(index: number) {
editingObj.value.steps.splice(index, 1)
}
function handleMergeAllSteps(): void {
if (editingObj.value.steps) {
mergeAllSteps(editingObj.value.steps)
}
}
function handleSplitAllSteps(): void {
if (editingObj.value.steps) {
splitAllSteps(editingObj.value.steps, '\n')
}
}
/**
* deletes the external file for the recipe
*/
function deleteExternalFile() {
let api = new ApiApi()
loading.value = true
api.apiRecipeDeleteExternalPartialUpdate({id: editingObj.value.id!, patchedRecipe: editingObj.value}).then(r => {
editingObj.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
}).finally(() => {
loading.value = false
})
}
</script>
<style scoped>

View File

@@ -20,9 +20,9 @@
<v-text-field :label="$t('Username')" v-model="editingObj.username" v-if="editingObj.method == 'NEXTCLOUD' || editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Password')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.password" v-if="editingObj.method == 'NEXTCLOUD'"></v-text-field>
<v-text-field :label="$t('Access_Token')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.token" v-if="editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Access_Token')" :hint="$t('StoragePasswordTokenHelp')" persistent-hint v-model="editingObj.token" v-if="editingObj.method == 'DB'"></v-text-field>
<v-text-field :label="$t('Path')" v-model="editingObj.path"></v-text-field>
<v-text-field :label="$t('Path')" v-model="editingObj.path"></v-text-field>
</v-form>
</v-card-text>
@@ -33,7 +33,7 @@
<script setup lang="ts">
import {onMounted, PropType, watch} from "vue";
import { Storage } from "@/openapi";
import {Storage} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
@@ -64,7 +64,7 @@ onMounted(() => {
/**
* component specific state setup logic
*/
function initializeEditor(){
function initializeEditor() {
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}

View File

@@ -54,6 +54,7 @@ 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()
@@ -111,7 +112,7 @@ function deleteInviteLink(inviteLink: InviteLink) {
* @param inviteLink InviteLink object to create url for
*/
function inviteLinkUrl(inviteLink: InviteLink) {
return `${location.protocol}//${location.host}/invite/${inviteLink.uuid}`
return useDjangoUrls().getDjangoUrl(`/invite/${inviteLink.uuid}`)
}
</script>

View File

@@ -78,19 +78,38 @@
<v-textarea v-model="space.message" :label="$t('Message')"></v-textarea>
<v-btn color="success" @click="updateSpace()" prepend-icon="$save">{{ $t('Save') }}</v-btn>
<!-- <model-select v-model="space.foodInherit" model="FoodInheritField" mode="tags"></model-select>-->
<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>
<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>
<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>

View File

@@ -1,7 +1,7 @@
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import {ref} from "vue";
import {getCookie} from "@/utils/cookie";
import {RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
import {AiProvider, RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
/**
@@ -86,7 +86,7 @@ export function useFileApi() {
* @param text text to import
* @param recipeId id of a recipe to use as import base (for external recipes
*/
function doAiImport(file: File | null, text: string = '', recipeId: string = '') {
function doAiImport(providerId: number, file: File | null, text: string = '', recipeId: string = '') {
let formData = new FormData()
if (file != null) {
@@ -96,6 +96,7 @@ export function useFileApi() {
}
formData.append('text', text)
formData.append('recipe_id', recipeId)
formData.append('ai_provider_id', providerId)
fileApiLoading.value = true
return fetch(getDjangoUrl(`api/ai-import/`), {

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "",
@@ -14,6 +15,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",
@@ -44,6 +51,7 @@
"Color": "",
"Coming_Soon": "",
"Completed": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -58,6 +66,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -102,10 +111,13 @@
"FoodOnHand": "",
"Food_Alias": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -148,10 +160,13 @@
"Keywords": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Make_Header": "",
"Make_Ingredient": "",
"ManageSubscription": "",
"Manage_Books": "",
"Meal_Plan": "",
"Meal_Plan_Days": "",
@@ -164,6 +179,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -255,6 +272,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Categories": "",
"Shopping_Category": "",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "Добави",
@@ -14,6 +15,12 @@
"Added_by": "Добавено от",
"Added_on": "Добавено",
"Advanced": "Разширено",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "Приложение",
"Apply": "",
"Are_You_Sure": "Сигурен ли си?",
@@ -44,6 +51,7 @@
"Color": "Цвят",
"Coming_Soon": "Очаквайте скоро",
"Completed": "Завършено",
"ConvertUsingAI": "",
"Copy": "Копиране",
"Copy_template_reference": "Копирайте препратка към шаблона",
"CountMore": "...+{count} още",
@@ -55,6 +63,7 @@
"Create_New_Meal_Type": "Добавете нов тип хранене",
"Create_New_Shopping Category": "Създайте нова категория за пазаруване",
"Create_New_Unit": "Добавяне на нова единица",
"Credits": "",
"Current_Period": "Текущ период",
"Custom Filter": "Персонализиран филтър",
"DELETE_ERROR": "",
@@ -99,10 +108,13 @@
"FoodOnHand": "Имате {храна} под ръка.",
"Food_Alias": "Псевдоним на храната",
"Foods": "Храни",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Групирай по",
"Hide_Food": "Скриване на храна",
"Hide_Keyword": "Скриване на ключови думи",
@@ -143,10 +155,13 @@
"Keywords": "Ключови думи",
"Link": "Връзка",
"Load_More": "Зареди още",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Дневник на Готвене",
"Log_Recipe_Cooking": "Дневник на Рецепта за готвене",
"Make_Header": "Направете заглавие",
"Make_Ingredient": "Направете съставка",
"ManageSubscription": "",
"Manage_Books": "Управление на Книги",
"Meal_Plan": "План на хранене",
"Meal_Plan_Days": "Бъдещи планове за хранене",
@@ -158,6 +173,8 @@
"Merge_Keyword": "Обединяване на ключова дума",
"MissingProperties": "",
"Month": "Месец",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Премести",
"MoveCategory": "Премести към: ",
"Move_Down": "Премести надолу",
@@ -248,6 +265,7 @@
"Selected": "Избрано",
"Servings": "Порции",
"Settings": "Настройки",
"SettingsOnlySuperuser": "",
"Share": "Споделяне",
"Shopping_Categories": "Категории за пазаруване",
"Shopping_Category": "Категория за пазаруване",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Afegit per",
"Added_on": "Afegit el",
"Advanced": "Avançat",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineació",
"Amount": "Quantitat",
"App": "Aplicació",
@@ -57,6 +64,7 @@
"Comments_setting": "Mostrar comentaris",
"Completed": "Completat",
"Conversion": "Conversió",
"ConvertUsingAI": "",
"Copy": "Copiar",
"Copy Link": "Copiar Enllaç",
"Copy Token": "Copiar Token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Afegir nova Categoria de Compres",
"Create_New_Unit": "Afegir nova unitat",
"Created": "Creada",
"Credits": "",
"Current_Period": "Període Actual",
"Custom Filter": "Filtre Personalitzat",
"CustomImageHelp": "Carregar una imatge per mostrar a la vista general de lespai.",
@@ -143,10 +152,13 @@
"Food_Alias": "Àlies per l'aliment",
"Food_Replace": "Aliment equivalent",
"Foods": "Aliments",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupat per",
"Hide_Food": "Amagar Aliment",
"Hide_Keyword": "Amaga les paraules clau",
@@ -197,11 +209,14 @@
"Learn_More": "Saber-me més",
"Link": "Enllaç",
"Load_More": "Carregueu-ne més",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registreu el que s'ha cuinat",
"Log_Recipe_Cooking": "Registre de receptes",
"Logo": "Logotip",
"Make_Header": "Establiu capçalera",
"Make_Ingredient": "Establiu ingredient",
"ManageSubscription": "",
"Manage_Books": "Gestioneu els llibres",
"Manage_Emails": "Administrar correus",
"Meal_Plan": "Pla d'àpats",
@@ -215,6 +230,8 @@
"Message": "Missatge",
"MissingProperties": "",
"Month": "Mes",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Moure",
"MoveCategory": "Moure a: ",
"Move_Down": "Moveu avall",
@@ -327,6 +344,7 @@
"Selected": "Seleccionat",
"Servings": "Racions",
"Settings": "Opcions",
"SettingsOnlySuperuser": "",
"Share": "Compartir",
"ShoppingBackgroundSyncWarning": "Error de la connexió, esperant per sincronitzar ...",
"Shopping_Categories": "Categoria de compres",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Přidáno uživatelem",
"Added_on": "Přidáno v",
"Advanced": "Rozšířené",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Zarovnání",
"Amount": "Množství",
"App": "Aplikace",
@@ -57,6 +64,7 @@
"Comments_setting": "Zobrazit komentáře",
"Completed": "Dokončeno",
"Conversion": "Převod",
"ConvertUsingAI": "",
"Copy": "Kopírovat",
"Copy Link": "Kopírovat odkaz",
"Copy Token": "Kopírovat token",
@@ -73,6 +81,7 @@
"Create_New_Shopping Category": "Vytvořit novou nákupní kategorii",
"Create_New_Shopping_Category": "Přidat novou nákupní kategorii",
"Create_New_Unit": "Přidat novou jednotku",
"Credits": "",
"Current_Period": "Současné období",
"Custom Filter": "Uživatelský filtr",
"CustomImageHelp": "Nahrajte obrázek, který se zobrazí v přehledu prostoru.",
@@ -142,10 +151,13 @@
"Food_Alias": "Přezdívka potraviny",
"Food_Replace": "Nahrazení v potravině",
"Foods": "Potraviny",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Seskupit podle",
"Hide_Food": "Skrýt potravinu",
"Hide_Keyword": "Skrýt štítky",
@@ -195,11 +207,14 @@
"Learn_More": "Zjistit víc",
"Link": "Odkaz",
"Load_More": "Načíst další",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zaznamenat vaření",
"Log_Recipe_Cooking": "Záznam vaření receptu",
"Logo": "Logo",
"Make_Header": "Použij jako nadpis",
"Make_Ingredient": "Použij jako ingredienci",
"ManageSubscription": "",
"Manage_Books": "Spravovat kuchařky",
"Manage_Emails": "Spravovat emaily",
"Meal_Plan": "Jídelníček",
@@ -213,6 +228,8 @@
"Message": "Zpráva",
"MissingProperties": "",
"Month": "Měsíc",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Přesunout",
"MoveCategory": "Přesunout do: ",
"Move_Down": "Dolů",
@@ -324,6 +341,7 @@
"Selected": "Vybrané",
"Servings": "Porce",
"Settings": "Nastavení",
"SettingsOnlySuperuser": "",
"Share": "Sdílet",
"Shopping_Categories": "Kategorie nákupního seznamu",
"Shopping_Category": "Kategorie nákupního seznamu",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Tilføjet af",
"Added_on": "Tilføjet den",
"Advanced": "Avanceret",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"Amount": "Mængde",
"App": "App",
@@ -57,6 +64,7 @@
"Comments_setting": "Vis kommentarer",
"Completed": "Afsluttet",
"Conversion": "Konversion",
"ConvertUsingAI": "",
"Copy": "Kopier",
"Copy Link": "Kopier link",
"Copy Token": "Kopier token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Opret ny indkøbskategori",
"Create_New_Unit": "Tilføj ny enhed",
"Created": "Skabt",
"Credits": "",
"Current_Period": "Nuværende periode",
"Custom Filter": "Tilpasset filter",
"CustomImageHelp": "Upload et billede for at vise dets plade i område-oversigten.",
@@ -143,10 +152,13 @@
"Food_Alias": "Alternativt navn til mad",
"Food_Replace": "Erstat ingrediens",
"Foods": "Mad",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupper efter",
"Hide_Food": "Skjul mad",
"Hide_Keyword": "Skjul nøgleord",
@@ -197,11 +209,14 @@
"Learn_More": "Lær mere",
"Link": "Link",
"Load_More": "Indlæs mere",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Noter tilberedning",
"Log_Recipe_Cooking": "Noter tilberedning af opskrift",
"Logo": "Logo",
"Make_Header": "Opret rubrik",
"Make_Ingredient": "Opret ingredient",
"ManageSubscription": "",
"Manage_Books": "Administrer bøger",
"Manage_Emails": "Håndter Emails",
"Meal_Plan": "Madplan",
@@ -215,6 +230,8 @@
"Message": "Besked",
"MissingProperties": "",
"Month": "Måned",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flyt",
"MoveCategory": "Flyt til: ",
"Move_Down": "Flyt ned",
@@ -327,6 +344,7 @@
"Selected": "Valgt",
"Servings": "Serveringer",
"Settings": "Indstillinger",
"SettingsOnlySuperuser": "",
"Share": "Del",
"ShoppingBackgroundSyncWarning": "Dårligt netværk, afventer synkronisering ...",
"Shopping_Categories": "Indkøbskategorier",

View File

@@ -1,7 +1,9 @@
{
"AI": "AI",
"AIImportSubtitle": "Verwende AI um Fotos von Rezepten zu importieren.",
"AISettingsHostedHelp": "AI Verfügbarkeit und Credit Limits können über die Tarifverwaltung geändert werden. ",
"API": "API",
"APIKey": "API Schlüssel",
"API_Browser": "API Browser",
"API_Documentation": "API Dokumentation",
"AccessTokenHelp": "Zugriffsschlüssel für die REST Schnittstelle.",
@@ -28,6 +30,12 @@
"Admin": "Admin",
"Advanced": "Erweitert",
"Advanced Search Settings": "Erweiterte Sucheinstellungen",
"AiCreditsBalance": "Credit Guthaben",
"AiLog": "AI Protokoll",
"AiLogHelp": "Eine Übersicht der AI Anfragen.",
"AiModelHelp": "Die Liste enthält Modelle die offiziell Unterstützt und getestet wurden. Weitere modelle können manuell eingetragen werden.",
"AiProvider": "AI Anbieter",
"AiProviderHelp": "Je nach Präferenz können verschiedene AI Anbieter angelegt werden. Diese können auch Space übergreifend sein.",
"Alignment": "Ausrichtung",
"AllRecipes": "Alle Rezepte",
"Amount": "Menge",
@@ -89,6 +97,7 @@
"Continue": "Weiter",
"Conversion": "Umrechnung",
"ConversionsHelp": "Mit Umrechnungen kann die Menge eines Lebensmittels in verschiedenen Einheiten ausgerechnet werden. Aktuell wird dies nur zur berechnung von Eigenschaften verwendet, später jedoch sollen auch andere Funktionen von Tandoor davon profitieren. ",
"ConvertUsingAI": "Mithilfe von AI Umwandeln",
"CookLog": "Kochprotokoll",
"CookLogHelp": "Einträge im Kochprotokoll für Rezepte. ",
"Cooked": "Gekocht",
@@ -111,6 +120,7 @@
"Create_New_Unit": "Neue Einheit hinzufügen",
"Created": "Erstellt",
"CreatedBy": "Erstellt von",
"Credits": "Credits",
"Ctrl+K": "Strg+K",
"Current_Period": "Aktueller Zeitraum",
"Custom Filter": "Benutzerdefinierter Filter",
@@ -204,11 +214,14 @@
"Food_Replace": "Essen Ersetzen",
"Foods": "Lebensmittel",
"Friday": "Freitag",
"FromBalance": "Guthaben verwendet",
"Fulltext": "Volltext",
"FulltextHelp": "Felder welche im Volltext durchsucht werden sollen. Tipp: Die Suchtypen 'web', 'raw' und 'phrase' funktionieren nur mit Volltext-Feldern.",
"Fuzzy": "Unscharf",
"FuzzySearchHelp": "Verwende unscharfe Suche um Einträge auch bei Unterschieden in der Schreibweise zu finden.",
"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.",
"GroupBy": "Gruppieren nach",
"HeaderWarning": "Achtung: Durch ändern auf Überschrift werden Menge/Einheit/Lebensmittel gelöscht",
"Headline": "Überschrift",
@@ -276,13 +289,15 @@
"Link": "Link",
"Load": "Laden",
"Load_More": "Weitere laden",
"LogCredits": "Credits Protokollieren",
"LogCreditsHelp": "Protokolliere die Credit Kosten der AI Anfragen. Ohne diese Protokollierung können Nutzer unbgerenzt viele Anfragen stellen.",
"Log_Cooking": "Kochen protokollieren",
"Log_Recipe_Cooking": "Kochen protokollieren",
"Logo": "Logo",
"Logout": "Ausloggen",
"Make_Header": "In Überschrift wandeln",
"Make_Ingredient": "In Zutat umwandeln",
"ManageSubscription": "Tarfi verwalten",
"ManageSubscription": "Tarif verwalten",
"Manage_Books": "Bücher verwalten",
"Manage_Emails": "E-Mails verwalten",
"MealPlanHelp": "Ein Speiseplan ist ein Eintrag im Kalender zur Planung von Mahlzeiten. Er muss entweder ein Rezept oder einen Titel erhalten und kann mit der Einkaufsliste verknüpft werden. ",
@@ -301,9 +316,12 @@
"Miscellaneous": "Sonstige",
"MissingConversion": "Fehlende Umrechnung",
"MissingProperties": "Fehlende Eigenschaften",
"Model": "Modell",
"ModelSelectResultsHelp": "Für mehr Ergebnisse suchen",
"Monday": "Montag",
"Month": "Monat",
"MonthlyCredits": "Monatliche Credits",
"MonthlyCreditsUsed": "Monatliche Credits verwendet",
"More": "Mehr",
"Move": "Verschieben",
"MoveCategory": "Verschieben nach: ",
@@ -457,6 +475,7 @@
"Servings": "Portionen",
"ServingsText": "Portionstext",
"Settings": "Einstellungen",
"SettingsOnlySuperuser": "Einige Einstellungen können nur vom Server Administrator verändert werden.",
"Share": "Teilen",
"ShopLater": "Später kaufen",
"ShopNow": "Jetzt kaufen",
@@ -511,10 +530,12 @@
"Storage": "Externer Speicher",
"StorageHelp": "Externe Speicherorte an denen Rezepte als Dateien (Foto/PDF) abgelegt und mit Tandor syncronisiert werden können.",
"StoragePasswordTokenHelp": "Das hinterlegte Passwort/Token kann nicht angezeigt werden. Es wird nur aktualisiert wenn etwas neues in das Feld eingegeben wird. ",
"Structured": "Strukturiert",
"SubstituteOnHand": "Du hast eine Alternative vorrätig.",
"Substitutes": "Alternativen",
"Success": "Erfolgreich",
"SuccessClipboard": "Einkaufsliste wurde in die Zwischenablage kopiert",
"Summary": "Zusammenfassung",
"Sunday": "Sonntag",
"Supermarket": "Supermarkt",
"SupermarketCategoriesOnly": "Nur Supermarktkategorien",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Προστέθηκε από",
"Added_on": "Προστέθηκε στις",
"Advanced": "Για προχωρημένους",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Ευθυγράμμιση",
"Amount": "Ποσότητα",
"App": "Εφαρμογή",
@@ -57,6 +64,7 @@
"Comments_setting": "Εμφάνιση σχολίων",
"Completed": "Ολοκληρωμένο",
"Conversion": "Μετατροπή",
"ConvertUsingAI": "",
"Copy": "Αντιγραφή",
"Copy Link": "Αντιγραφή συνδέσμου",
"Copy Token": "Αντιγραφή token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Προσθήκη νέας κατηγορίας αγορών",
"Create_New_Unit": "Προσθήκη νέας μονάδας μέτρησης",
"Created": "Δημιουργήθηκε",
"Credits": "",
"Current_Period": "Τρέχουσα περίοδος",
"Custom Filter": "Προσαρμοσμένο φίλτρο",
"CustomImageHelp": "Ανεβάστε μια εικόνα για να εμφανίζεται στην επισκόπηση χώρου",
@@ -143,10 +152,13 @@
"Food_Alias": "Ψευδώνυμο φαγητού",
"Food_Replace": "Αντικατάσταση Φαγητού",
"Foods": "Φαγητά",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Ομαδοποίηση κατά",
"Hide_Food": "Απόκρυψη φαγητού",
"Hide_Keyword": "Απόκρυψη λέξεων-κλειδί",
@@ -197,11 +209,14 @@
"Learn_More": "Μάθετε περισσότερα",
"Link": "Σύνδεσμος",
"Load_More": "Φόρτωση περισσότερων",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Καταγραφή μαγειρέματος",
"Log_Recipe_Cooking": "Καταγραφή εκτέλεσης συνταγής",
"Logo": "Λογότυπο",
"Make_Header": "Δημιουργία κεφαλίδας",
"Make_Ingredient": "Δημιουργία υλικού",
"ManageSubscription": "",
"Manage_Books": "Διαχείριση βιβλίων",
"Manage_Emails": "Διαχείριση email",
"Meal_Plan": "Πρόγραμμα γευμάτων",
@@ -215,6 +230,8 @@
"Message": "Μήνυμα",
"MissingProperties": "",
"Month": "Μήνας",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Μετακίνηση",
"MoveCategory": "Μετακίνηση σε: ",
"Move_Down": "Μετακίνηση κάτω",
@@ -327,6 +344,7 @@
"Selected": "Επιλεγμένο",
"Servings": "Μερίδες",
"Settings": "Ρυθμίσεις",
"SettingsOnlySuperuser": "",
"Share": "Κοινοποίηση",
"ShoppingBackgroundSyncWarning": "Κακό δίκτυο, αναμονή συγχρονισμού...",
"Shopping_Categories": "Κατηγορίες αγορών",

View File

@@ -1,7 +1,9 @@
{
"AI": "AI",
"AIImportSubtitle": "Use AI to import images of recipes.",
"AISettingsHostedHelp": "You can enable AI features or change available credits by managing your subscription.",
"API": "API",
"APIKey": "API key",
"API_Browser": "API Browser",
"API_Documentation": "API Docs",
"AccessTokenHelp": "Access keys for the REST API.",
@@ -26,6 +28,12 @@
"Added_on": "Added On",
"Admin": "Admin",
"Advanced": "Advanced",
"AiCreditsBalance": "Credit Balance",
"AiLog": "AI Log",
"AiLogHelp": "Overview of your spaces AI requests. ",
"AiModelHelp": "The list contains model that are offically tested and supported. You can add additional models if you want.",
"AiProvider": "AI Provider",
"AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.",
"Alignment": "Alignment",
"AllRecipes": "All Recipes",
"Amount": "Amount",
@@ -87,6 +95,7 @@
"Continue": "Continue",
"Conversion": "Conversion",
"ConversionsHelp": "With conversions you can calculate the amount of a food in different units. Currently this is only used for property calculation, later it might also be used in other parts of tandoor. ",
"ConvertUsingAI": "Convert using AI",
"CookLog": "Cook Log",
"CookLogHelp": "Entries in the cook log for recipes. ",
"Cooked": "Cooked",
@@ -109,6 +118,7 @@
"Create_New_Unit": "Add New Unit",
"Created": "Created",
"CreatedBy": "Created by",
"Credits": "Credits",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Current Period",
"Custom Filter": "Custom Filter",
@@ -202,11 +212,14 @@
"Food_Replace": "Food Replace",
"Foods": "Foods",
"Friday": "Friday",
"FromBalance": "From Balance",
"Fulltext": "Fulltext",
"FulltextHelp": "Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Use fuzzy search to find entries even when there are differences in how the word is written.",
"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. ",
"GroupBy": "Group By",
"HeaderWarning": "Warning: Changing to a Heading deletes the Amount/Unit/Food",
"Headline": "Headline",
@@ -274,6 +287,8 @@
"Link": "Link",
"Load": "Load",
"Load_More": "Load More",
"LogCredits": "Log Credits.",
"LogCreditsHelp": "Log credit cost of AI requests. Without this users can perform as many AI requests as they want. ",
"Log_Cooking": "Log Cooking",
"Log_Recipe_Cooking": "Log Recipe Cooking",
"Logo": "Logo",
@@ -299,9 +314,12 @@
"Miscellaneous": "Miscellaneous",
"MissingConversion": "Missing Conversion",
"MissingProperties": "Missing Properties",
"Model": "Model",
"ModelSelectResultsHelp": "Search for more results",
"Monday": "Monday",
"Month": "Month",
"MonthlyCredits": "Monthly Credits",
"MonthlyCreditsUsed": "Monthly credits used",
"More": "More",
"Move": "Move",
"MoveCategory": "Move To: ",
@@ -455,6 +473,7 @@
"Servings": "Servings",
"ServingsText": "Servings Text",
"Settings": "Settings",
"SettingsOnlySuperuser": "Some Settings can only be changed by the Server Administrator.",
"Share": "Share",
"ShopLater": "Shop later",
"ShopNow": "Shop now",
@@ -509,10 +528,12 @@
"Storage": "External Storage",
"StorageHelp": "External storage locations where recipe files (image/pdf) can be stored and synced with Tandoor.",
"StoragePasswordTokenHelp": "The stored password/token will never be displayed. It is only changed if something new is entered into the field. ",
"Structured": "Structured",
"SubstituteOnHand": "You have a substitute on hand.",
"Substitutes": "Substitutes",
"Success": "Success",
"SuccessClipboard": "Shopping list copied to clipboard",
"Summary": "Summary",
"Sunday": "Sunday",
"Supermarket": "Supermarket",
"SupermarketCategoriesOnly": "Supermarket Categories Only",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Usar IA para importar imágenes de recetas.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "Añadido el",
"Admin": "Administrador",
"Advanced": "Avanzado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineación",
"AllRecipes": "Todas las recetas",
"Amount": "Cantidad",
@@ -85,6 +92,7 @@
"Continue": "Continuar",
"Conversion": "Conversión",
"ConversionsHelp": "Con las conversiones puedes calcular la cantidad de un alimento en diferentes unidades. Actualmente esto solo se usa para el cálculo de propiedades, en un futuro podría ser usado en otras partes de Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Historial de cocina",
"CookLogHelp": "Entradas en el historial de cocina para recetas. ",
"Cooked": "Cocinado",
@@ -107,6 +115,7 @@
"Create_New_Unit": "Añadir nueva unidad",
"Created": "Creada",
"CreatedBy": "Creado por",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Periodo actual",
"Custom Filter": "Filtro personalizado",
@@ -200,7 +209,10 @@
"Food_Replace": "Sustituir Alimento",
"Foods": "Alimentos",
"Friday": "Viernes",
"FromBalance": "",
"GettingStarted": "Primeros pasos",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupar por",
"HeaderWarning": "Advertencia: Cambiar a un encabezado eliminará la cantidad/unidad/alimento",
"Headline": "Encabezado",
@@ -266,6 +278,8 @@
"Link": "Enlace",
"Load": "Cargar",
"Load_More": "Cargar más",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registrar cocinada",
"Log_Recipe_Cooking": "Registro de recetas",
"Logo": "Logotipo",
@@ -294,6 +308,8 @@
"ModelSelectResultsHelp": "Buscar más resultados",
"Monday": "Lunes",
"Month": "Mes",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Más",
"Move": "Mover",
"MoveCategory": "Mover a: ",
@@ -441,6 +457,7 @@
"Servings": "Raciones",
"ServingsText": "Texto de la porción",
"Settings": "Opciones",
"SettingsOnlySuperuser": "",
"Share": "Compartir",
"ShopLater": "Comprar después",
"ShopNow": "Comprar ahora",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -17,6 +18,12 @@
"Added_on": "Lisätty",
"Advanced": "Edistynyt",
"Advanced Search Settings": "Tarkennetun Haun Asetukset",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Tasaus",
"Amount": "Määrä",
"App": "Applikaatio",
@@ -55,6 +62,7 @@
"Comments_setting": "Näytä Kommentit",
"Completed": "Valmis",
"Conversion": "Muuntaminen",
"ConvertUsingAI": "",
"Copy": "Kopioi",
"Copy Link": "Kopioi Linkki",
"Copy Token": "Kopioi Token",
@@ -71,6 +79,7 @@
"Create_New_Shopping_Category": "Lisää uusi ostoskategoria",
"Create_New_Unit": "Lisää Uusi Yksikkö",
"Created": "Luotu",
"Credits": "",
"Current_Period": "Nykyinen Jakso",
"Custom Filter": "Mukautettu Suodatin",
"CustomImageHelp": "Lataa kuva näytettäväksi tilan yleiskatsauksessa.",
@@ -140,10 +149,13 @@
"Food_Alias": "Ruoan nimimerkki",
"Food_Replace": "Korvaa Ruoka",
"Foods": "Ruuat",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Ryhmittely peruste",
"Hide_Food": "Piilota Ruoka",
"Hide_Keyword": "Piilota avainsana",
@@ -191,11 +203,14 @@
"Learn_More": "Lisätietoja",
"Link": "Linkki",
"Load_More": "Lataa Lisää",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Kirjaa kokkaus",
"Log_Recipe_Cooking": "Kirjaa Reseptin valmistus",
"Logo": "Logo",
"Make_Header": "Valmista Otsikko",
"Make_Ingredient": "Valmista Ainesosa",
"ManageSubscription": "",
"Manage_Books": "Hallinnoi kirjoja",
"Manage_Emails": "Hallinnoi sähköposteja",
"Meal_Plan": "Ateriasuunnitelma",
@@ -209,6 +224,8 @@
"Message": "Viesti",
"MissingProperties": "",
"Month": "Kuukausi",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Siirry",
"MoveCategory": "Siirrä paikkaan: ",
"Move_Down": "Siirry alas",
@@ -316,6 +333,7 @@
"Selected": "Valittu",
"Servings": "Annokset",
"Settings": "Asetukset",
"SettingsOnlySuperuser": "",
"Share": "Jaa",
"ShoppingBackgroundSyncWarning": "Huono verkkoyhteys, odotetaan synkronointia ...",
"Shopping_Categories": "Ostoskategoriat",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Utiliser l'IA pour importer des images de recettes.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Admin",
"Advanced": "Avancé",
"Advanced Search Settings": "Paramètres de recherche avancée",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alignement",
"AllRecipes": "Toutes les recettes",
"Amount": "Quantité",
@@ -88,6 +95,7 @@
"Continue": "Continuer",
"Conversion": "Conversion",
"ConversionsHelp": "Avec les conversions, vous pouvez calculer une quantité dans différentes unités. Actuellement, c'est utilisé uniquement pour le calcul des propriétés, mais ça pourrait être utilisé dans d'autres parties de Tandoor dans le futur. ",
"ConvertUsingAI": "",
"CookLog": "Journal de cuisine",
"CookLogHelp": "Entrées dans le journal de cuisine pour les recettes. ",
"Cooked": "Cuit",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Ajouter une nouvelle unité",
"Created": "Créé",
"CreatedBy": "Créé par",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Période actuelle",
"Custom Filter": "Filtre personnalisé",
@@ -203,11 +212,14 @@
"Food_Replace": "Remplacer l'aliment",
"Foods": "Aliments",
"Friday": "Vendredi",
"FromBalance": "",
"Fulltext": "Texte intégral",
"FulltextHelp": "Champs de recherche en texte intégral. Remarque : les méthodes de recherche \"web\", \"phrase\" et \"raw\" ne fonctionnent qu'avec des champs en texte intégral.",
"Fuzzy": "Approximatif",
"FuzzySearchHelp": "Utilisez la recherche approximative pour trouver des entrées même lorsqu'il existe des différences dans la façon dont le mot est écrit.",
"GettingStarted": "Commencer",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grouper par",
"HeaderWarning": "Attention : Changer pour un En-tête supprimera la quantité / l'unité / l'aliment",
"Headline": "En-tête",
@@ -275,6 +287,8 @@
"Link": "Lien",
"Load": "Chargement",
"Load_More": "Charger plus",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Marquer comme cuisiné",
"Log_Recipe_Cooking": "Marquer la recette comme cuisinée",
"Logo": "Logo",
@@ -301,6 +315,8 @@
"ModelSelectResultsHelp": "Chercher plus de résultats",
"Monday": "Lundi",
"Month": "Mois",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Plus",
"Move": "Déplacer",
"MoveCategory": "Déplacer vers : ",
@@ -454,6 +470,7 @@
"Servings": "Portions",
"ServingsText": "Texte des portions",
"Settings": "Paramètres",
"SettingsOnlySuperuser": "",
"Share": "Partager",
"ShopLater": "Acheter plus tard",
"ShopNow": "Acheter maintenant",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "נוסף ע\"י",
"Added_on": "נוסף ב",
"Advanced": "מתקדם",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "יישור",
"Amount": "כמות",
"App": "אפליקציה",
@@ -57,6 +64,7 @@
"Comments_setting": "הצג תגובות",
"Completed": "הושלם",
"Conversion": "עברית",
"ConvertUsingAI": "",
"Copy": "העתקה",
"Copy Link": "העתק קישור",
"Copy Token": "העתק טוקן",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "הוסף קטגוריות קניות חדשה",
"Create_New_Unit": "הוסף יחידה",
"Created": "נוצר",
"Credits": "",
"Current_Period": "תקופה נוכחית",
"Custom Filter": "פילטר מותאם",
"CustomImageHelp": "העלאת תמונה שתראה באזור הסקירה.",
@@ -143,10 +152,13 @@
"Food_Alias": "שם כינוי לאוכל",
"Food_Replace": "החלף אוכל",
"Foods": "מאכלים",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "אסוף לפי",
"Hide_Food": "הסתר אוכל",
"Hide_Keyword": "הסתר מילות מפתח",
@@ -197,11 +209,14 @@
"Learn_More": "למד עוד",
"Link": "קישור",
"Load_More": "טען עוד",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "רשום הכנת מתכון",
"Log_Recipe_Cooking": "רשום בישול מתכון",
"Logo": "לוגו",
"Make_Header": "הפוך לכותרת",
"Make_Ingredient": "הפוך למרכיב",
"ManageSubscription": "",
"Manage_Books": "נהל ספרים",
"Manage_Emails": "נהל כתובות דואר אלקטרוני",
"Meal_Plan": "תוכנית ארוחה",
@@ -215,6 +230,8 @@
"Message": "הודעה",
"MissingProperties": "",
"Month": "חודש",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "העברה",
"MoveCategory": "העבר אל: ",
"Move_Down": "העברה למטה",
@@ -327,6 +344,7 @@
"Selected": "נבחר",
"Servings": "מנות",
"Settings": "הגדרות",
"SettingsOnlySuperuser": "",
"Share": "שיתוף",
"ShoppingBackgroundSyncWarning": "בעיית תקשורת, מחכה לסנכון...",
"Shopping_Categories": "קטגוריות קניות",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Dodao",
"Added_on": "Dodano",
"Advanced": "Napredno",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnanje",
"Amount": "Količina",
"App": "Aplikacija",
@@ -57,6 +64,7 @@
"Comments_setting": "Prikaži komentare",
"Completed": "Završeno",
"Conversion": "Konverzija",
"ConvertUsingAI": "",
"Copy": "Kopiraj",
"Copy Link": "Kopiraj vezu",
"Copy Token": "Kopiraj token",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Dodaj novu kategoriju za kupovinu",
"Create_New_Unit": "Dodaj novu jedinicu",
"Created": "Stvoreno",
"Credits": "",
"Current_Period": "Trenutno razdoblje",
"Custom Filter": "Prilagođeni filtar",
"CustomImageHelp": "Učitaj sliku za prikaz u pregledu prostora.",
@@ -143,10 +152,13 @@
"Food_Alias": "Nadimci namirnice",
"Food_Replace": "Zamjena namirnica",
"Foods": "Namirnice",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupiraj po",
"Hide_Food": "Sakrij namirnicu",
"Hide_Keyword": "Sakrij ključne riječi",
@@ -197,11 +209,14 @@
"Learn_More": "Saznajte više",
"Link": "Poveznica",
"Load_More": "Učitaj više",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zapis kuhanja",
"Log_Recipe_Cooking": "Dnevnik recepata kuhanja",
"Logo": "Logotip",
"Make_Header": "Napravi zaglavlje",
"Make_Ingredient": "Napravi sastojak",
"ManageSubscription": "",
"Manage_Books": "Upravljaj knjigama",
"Manage_Emails": "Upravljanje e-poštom",
"Meal_Plan": "Plan obroka",
@@ -215,6 +230,8 @@
"Message": "Poruka",
"MissingProperties": "",
"Month": "Mjesec",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Premjesti",
"MoveCategory": "Premjesti u: ",
"Move_Down": "Premjesti dolje",
@@ -327,6 +344,7 @@
"Selected": "Odabrano",
"Servings": "Porcije",
"Settings": "Postavke",
"SettingsOnlySuperuser": "",
"Share": "Podijeli",
"ShoppingBackgroundSyncWarning": "Loša mreža, čeka se sinkronizacija...",
"Shopping_Categories": "Kategorije Kupovine",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Hozzádta",
"Added_on": "Hozzáadva",
"Advanced": "Haladó",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Igazítás",
"Amount": "Összeg",
"App": "Applikáció",
@@ -56,6 +63,7 @@
"Comments_setting": "Hozzászólások megjelenítése",
"Completed": "Kész",
"Conversion": "Konverzió",
"ConvertUsingAI": "",
"Copy": "Másolás",
"Copy Link": "Link másolása",
"Copy Token": "Token másolása",
@@ -71,6 +79,7 @@
"Create_New_Shopping Category": "Új vásárlási kategória létrehozása",
"Create_New_Shopping_Category": "Új vásárlási kategória hozzáadása",
"Create_New_Unit": "Új mértékegység hozzáadása",
"Credits": "",
"Current_Period": "Jelenlegi periódus",
"Custom Filter": "Egyéni szűrő",
"DELETE_ERROR": "",
@@ -126,10 +135,13 @@
"Food_Alias": "",
"Food_Replace": "Étel cseréje",
"Foods": "Alapanyagok",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Csoportosítva",
"Hide_Food": "Alapanyag elrejtése",
"Hide_Keyword": "Kulcsszavak elrejtése",
@@ -179,10 +191,13 @@
"Learn_More": "Tudjon meg többet",
"Link": "Link",
"Load_More": "Továbbiak betöltése",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Főzés naplózása",
"Log_Recipe_Cooking": "Főzés naplózása",
"Make_Header": "Átalakítás címsorra",
"Make_Ingredient": "Összetevő létrehozása",
"ManageSubscription": "",
"Manage_Books": "Könyvek kezelése",
"Manage_Emails": "Levelezés kezelése",
"Meal_Plan": "Menüterv",
@@ -196,6 +211,8 @@
"Message": "Üzenet",
"MissingProperties": "",
"Month": "Hónap",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mozgatás",
"MoveCategory": "Áthelyezés ide: ",
"Move_Down": "Lefelé mozgatás",
@@ -300,6 +317,7 @@
"Selected": "Kiválasztott",
"Servings": "Adag",
"Settings": "Beállítások",
"SettingsOnlySuperuser": "",
"Share": "Megosztás",
"Shopping_Categories": "Vásárlási kategóriák",
"Shopping_Category": "Vásárlási kategória",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "",
@@ -8,6 +9,12 @@
"Add_to_Plan": "Ավելացնել պլանին",
"Add_to_Shopping": "Ավելացնել գնումներին",
"Advanced Search Settings": "Ընդլայնված փնտրման կարգավորումներ",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Apply": "",
"Automate": "Ավտոմատացնել",
"BatchDeleteConfirm": "",
@@ -22,11 +29,13 @@
"Categories": "",
"Category": "",
"Close": "",
"ConvertUsingAI": "",
"Copy": "",
"Create": "Ստեղծել",
"Create_New_Food": "Ավելացնել նոր սննդամթերք",
"Create_New_Keyword": "Ավելացնել նոր բանալի բառ",
"Create_New_Shopping Category": "Ստեղծել գնումների նոր կատեգորիա",
"Credits": "",
"DELETE_ERROR": "",
"Date": "",
"Delete": "",
@@ -51,10 +60,13 @@
"File": "",
"Files": "",
"Food": "Սննդամթերք",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"Hide_Food": "Թաքցնել սննդամթերքը",
"Hide_Keywords": "Թաքցնել բանալի բառը",
"Hide_Recipes": "Թաքցնել բաղադրատոմսերը",
@@ -69,14 +81,19 @@
"Keywords": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Գրանցել եփելը",
"Log_Recipe_Cooking": "Գրանցել բաղադրատոմսի օգտագործում",
"ManageSubscription": "",
"Manage_Books": "Կարգավորել Գրքերը",
"Meal_Plan": "Ճաշացուցակ",
"Merge": "Միացնել",
"MergeAutomateHelp": "",
"Merge_Keyword": "Միացնել բանալի բառը",
"MissingProperties": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Տեղափոխել",
"Move_Food": "Տեղափոխել սննդամթերքը",
"Move_Keyword": "Տեղափոխել բանալի բառը",
@@ -123,6 +140,7 @@
"Selected": "",
"Servings": "",
"Settings": "Կարգավորումներ",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Category": "Գնումների կատեգորիա",
"Shopping_list": "Գնումների ցուցակ",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",
@@ -48,6 +55,7 @@
"Coming_Soon": "",
"Comments_setting": "",
"Completed": "",
"ConvertUsingAI": "",
"Copy": "Salin",
"Copy Link": "Salin Tautan",
"Copy Token": "Salin Token",
@@ -63,6 +71,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -114,10 +123,13 @@
"FoodOnHand": "",
"Food_Alias": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -164,10 +176,13 @@
"Last_name": "",
"Link": "Link",
"Load_More": "Muat lebih banyak",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Log Memasak",
"Log_Recipe_Cooking": "Log Resep Memasak",
"Make_Header": "Buat Header",
"Make_Ingredient": "Buat bahan",
"ManageSubscription": "",
"Manage_Books": "Kelola Buku",
"Manage_Emails": "",
"Meal_Plan": "rencana makan",
@@ -181,6 +196,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Bergerak",
"MoveCategory": "",
"Move_Down": "Pindahkan kebawah",
@@ -276,6 +293,7 @@
"Selected": "Terpilih",
"Servings": "Porsi",
"Settings": "Pengaturan",
"SettingsOnlySuperuser": "",
"Share": "Bagikan",
"Shopping_Categories": "Kategori Belanja",
"Shopping_Category": "Kategori Belanja",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "",
"App": "",
@@ -57,6 +64,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Created": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"CustomImageHelp": "",
@@ -142,10 +151,13 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -196,11 +208,14 @@
"Learn_More": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Logo": "",
"Make_Header": "",
"Make_Ingredient": "",
"ManageSubscription": "",
"Manage_Books": "",
"Manage_Emails": "",
"Meal_Plan": "",
@@ -214,6 +229,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -326,6 +343,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"ShoppingBackgroundSyncWarning": "",
"Shopping_Categories": "",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Utilizza IA per importare le immagini delle ricette.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Amministratore",
"Advanced": "Avanzate",
"Advanced Search Settings": "Impostazioni avanzate di ricerca",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Allineamento",
"AllRecipes": "Tutte le ricette",
"Amount": "Quantità",
@@ -88,6 +95,7 @@
"Continue": "Continua",
"Conversion": "Conversione",
"ConversionsHelp": "Con le conversioni è possibile calcolare la quantità di un alimento in diverse unità. Attualmente, questo metodo viene utilizzato solo per il calcolo delle proprietà, ma in futuro potrebbe essere utilizzato anche in altre parti del tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Registro di cucina",
"CookLogHelp": "Le voci nel registro di cucina per le ricette. ",
"Cooked": "Cucinati",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Aggiungi nuova unità",
"Created": "Creata",
"CreatedBy": "Creata da",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Periodo attuale",
"Custom Filter": "Filtro personalizzato",
@@ -203,11 +212,14 @@
"Food_Replace": "Sostituisci alimento",
"Foods": "Alimenti",
"Friday": "Venerdì",
"FromBalance": "",
"Fulltext": "Fulltext",
"FulltextHelp": "Campi per la ricerca full text. Nota: i metodi di ricerca 'web', 'phrase', e 'raw' funzionano solo con i campi fulltext.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Utilizza la ricerca fuzzy per trovare voci anche quando ci sono differenze nel modo in cui la parola è scritta.",
"GettingStarted": "Iniziamo",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Raggruppa per",
"HeaderWarning": "Attenzione: la modifica in un'intestazione elimina l'importo/unità/alimento",
"Headline": "Intestazione",
@@ -275,6 +287,8 @@
"Link": "Collegamento",
"Load": "Carica",
"Load_More": "Carica altro",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registro ricette cucinate",
"Log_Recipe_Cooking": "Aggiungi a ricette cucinate",
"Logo": "Logo",
@@ -303,6 +317,8 @@
"ModelSelectResultsHelp": "Cerca altri risultati",
"Monday": "Lunedì",
"Month": "Mese",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Altro",
"Move": "Sposta",
"MoveCategory": "Sposta in: ",
@@ -456,6 +472,7 @@
"Servings": "Porzioni",
"ServingsText": "Testo porzioni",
"Settings": "Impostazioni",
"SettingsOnlySuperuser": "",
"Share": "Condividi",
"ShopLater": "Compra dopo",
"ShopNow": "Compra subito",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "Suma",
"App": "",
@@ -56,6 +63,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -72,6 +80,7 @@
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"DELETE_ERROR": "",
@@ -128,10 +137,13 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -181,10 +193,13 @@
"Learn_More": "",
"Link": "",
"Load_More": "Įkelti daugiau",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Užregistruoti patiekalo gaminimą",
"Log_Recipe_Cooking": "Užregistruoti recepto pagaminimą",
"Make_Header": "Padaryti antraštę",
"Make_Ingredient": "Padaryti ingredientą",
"ManageSubscription": "",
"Manage_Books": "Tvarkyti knygas",
"Manage_Emails": "",
"Meal_Plan": "Maisto planas",
@@ -198,6 +213,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "Nuleisti žemyn",
@@ -304,6 +321,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"Shopping_Categories": "",
"Shopping_Category": "",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "",
"Added_on": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"Amount": "",
"App": "",
@@ -57,6 +64,7 @@
"Comments_setting": "",
"Completed": "",
"Conversion": "",
"ConvertUsingAI": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Created": "",
"Credits": "",
"Current_Period": "",
"Custom Filter": "",
"CustomImageHelp": "",
@@ -143,10 +152,13 @@
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "",
"Hide_Food": "",
"Hide_Keyword": "",
@@ -197,11 +209,14 @@
"Learn_More": "",
"Link": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Logo": "",
"Make_Header": "",
"Make_Ingredient": "",
"ManageSubscription": "",
"Manage_Books": "",
"Manage_Emails": "",
"Meal_Plan": "",
@@ -215,6 +230,8 @@
"Message": "",
"MissingProperties": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "",
"MoveCategory": "",
"Move_Down": "",
@@ -327,6 +344,7 @@
"Selected": "",
"Servings": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"ShoppingBackgroundSyncWarning": "",
"Shopping_Categories": "",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Lagt til av",
"Added_on": "Lagt til",
"Advanced": "Avansert",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"Amount": "Mengde",
"App": "App",
@@ -55,6 +62,7 @@
"Comments_setting": "",
"Completed": "Fullført",
"Conversion": "Omregn enhet",
"ConvertUsingAI": "",
"Copy": "Kopier",
"Copy Link": "Kopier lenke",
"Copy Token": "Kopier Token",
@@ -71,6 +79,7 @@
"Create_New_Shopping Category": "Opprett ny handle kategori",
"Create_New_Shopping_Category": "Opprett new handle kategori",
"Create_New_Unit": "Opprett ny enhet",
"Credits": "",
"Current_Period": "Gjeldende periode",
"Custom Filter": "Egendefinert Filter",
"CustomImageHelp": "Last opp et bilde for å vise \"space\"-oversikten.",
@@ -134,10 +143,13 @@
"FoodOnHand": "Du har {food} på lager.",
"Food_Alias": "Matrett Alias",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupér",
"Hide_Food": "Skjul Matrett",
"Hide_Keyword": "Skjul nøkkelord",
@@ -188,10 +200,13 @@
"Learn_More": "Lær mer",
"Link": "Lenke",
"Load_More": "Last inn flere",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Loggfør tilbereding",
"Log_Recipe_Cooking": "Logg oppskriftsbruk",
"Make_Header": "Bruk som overskrift",
"Make_Ingredient": "Bruk som ingrediens",
"ManageSubscription": "",
"Manage_Books": "Administrer bøker",
"Manage_Emails": "Administrer e-poster",
"Meal_Plan": "Måltidsplan",
@@ -205,6 +220,8 @@
"Message": "Melding",
"MissingProperties": "",
"Month": "Måned",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flytt",
"MoveCategory": "Flytt til: ",
"Move_Down": "Flytt ned",
@@ -311,6 +328,7 @@
"Selected": "Valgte",
"Servings": "Porsjoner",
"Settings": "Innstillinger",
"SettingsOnlySuperuser": "",
"Share": "Del",
"ShoppingBackgroundSyncWarning": "Dårlig nettverkstilkobling, venter på synkronisering...",
"Shopping_Categories": "Butikk Kategorier",

View File

@@ -1,6 +1,7 @@
{
"AI": "AI",
"AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -28,6 +29,12 @@
"Admin": "Beheer",
"Advanced": "Geavanceerd",
"Advanced Search Settings": "Geavanceerde zoekinstellingen",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Afstemming",
"AllRecipes": "Alle recepten",
"Amount": "Hoeveelheid",
@@ -89,6 +96,7 @@
"Continue": "Doorgaan",
"Conversion": "Omrekening",
"ConversionsHelp": "Met omrekeningen kun je de hoeveelheid van een ingrediënt in verschillende eenheden berekenen. Momenteel wordt dit alleen gebruikt voor het berekenen van eigenschappen, later kan het ook in andere onderdelen van Tandoor gebruikt worden. ",
"ConvertUsingAI": "",
"CookLog": "Kooklogboek",
"CookLogHelp": "Items in het kooklogboek voor recepten. ",
"Cooked": "Gekookt",
@@ -111,6 +119,7 @@
"Create_New_Unit": "Voeg nieuwe eenheid toe",
"Created": "Gemaakt",
"CreatedBy": "Gemaakt door",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Huidige periode",
"Custom Filter": "Aangepast filter",
@@ -204,11 +213,14 @@
"Food_Replace": "Voedingsmiddelen vervangen",
"Foods": "Voedingsmiddelen",
"Friday": "Vrijdag",
"FromBalance": "",
"Fulltext": "Volledige tekst",
"FulltextHelp": "Velden voor volledige tekstzoekopdrachten. Opmerking: de zoekmethoden web, zin en ruw werken alleen met volledige tekstvelden.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Gebruik fuzzy search om items te vinden, zelfs als het woord anders is gespeld.",
"GettingStarted": "Aan de slag",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Groepeer per",
"HeaderWarning": "Waarschuwing: Het wijzigen naar een kop verwijdert de hoeveelheid/eenheid/voedingsmiddel",
"Headline": "Koptekst",
@@ -276,6 +288,8 @@
"Link": "Link",
"Load": "Laden",
"Load_More": "Laad meer",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registreer bereiding",
"Log_Recipe_Cooking": "Bereiding registreren",
"Logo": "Logo",
@@ -304,6 +318,8 @@
"ModelSelectResultsHelp": "Zoek naar meer resultaten",
"Monday": "Maandag",
"Month": "Maand",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Meer",
"Move": "Verplaats",
"MoveCategory": "Verplaats naar: ",
@@ -457,6 +473,7 @@
"Servings": "Porties",
"ServingsText": "Portie tekst",
"Settings": "Instellingen",
"SettingsOnlySuperuser": "",
"Share": "Deel",
"ShopLater": "Later boodschappen doen",
"ShopNow": "Nu boodschappen doen",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -25,6 +26,12 @@
"Admin": "Administator",
"Advanced": "Zaawansowany",
"Advanced Search Settings": "Ustawienia zaawansowanego wyszukiwania",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Wyrównanie",
"AllRecipes": "Wszystkie przepisy",
"Amount": "Ilość",
@@ -83,6 +90,7 @@
"Confirm": "Potwierdź",
"Continue": "Kontynuuj",
"Conversion": "Konwersja",
"ConvertUsingAI": "",
"Copy": "Kopiuj",
"Copy Link": "Skopiuj link",
"Copy Token": "Kopiuj Token",
@@ -100,6 +108,7 @@
"Create_New_Shopping_Category": "Dodaj nową kategorię zakupów",
"Create_New_Unit": "Dodaj nowa jednostkę",
"Created": "Utworzony",
"Credits": "",
"Current_Period": "Bieżący okres",
"Custom Filter": "Filtr niestandardowy",
"CustomImageHelp": "Prześlij obraz, który będzie wyświetlany w przeglądzie przestrzeni.",
@@ -169,10 +178,13 @@
"Food_Alias": "Alias żywności",
"Food_Replace": "Zastąp produkt",
"Foods": "Żywność",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupuj według",
"Hide_Food": "Ukryj żywność",
"Hide_Keyword": "Ukryj słowa kluczowe",
@@ -223,11 +235,14 @@
"Learn_More": "Dowiedz się więcej",
"Link": "Link",
"Load_More": "Załaduj więcej",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zanotuj ugotowanie",
"Log_Recipe_Cooking": "Zaloguj gotowanie przepisu",
"Logo": "Logo",
"Make_Header": "Utwórz nagłówek",
"Make_Ingredient": "Utwórz składnik",
"ManageSubscription": "",
"Manage_Books": "Zarządzaj książkami",
"Manage_Emails": "Zarządzaj e-mailami",
"Meal_Plan": "Plan posiłków",
@@ -241,6 +256,8 @@
"Message": "Wiadomość",
"MissingProperties": "",
"Month": "Miesiąc",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Przenieś",
"MoveCategory": "Przenieś do: ",
"Move_Down": "Przesunąć w dół",
@@ -353,6 +370,7 @@
"Selected": "Wybrane",
"Servings": "Porcje",
"Settings": "Ustawienia",
"SettingsOnlySuperuser": "",
"Share": "Udostępnij",
"ShoppingBackgroundSyncWarning": "Słaba sieć, oczekiwanie na synchronizację...",
"Shopping_Categories": "Kategorie zakupów",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "Adicionar",
@@ -14,6 +15,12 @@
"Added_by": "Adicionado por",
"Added_on": "Adicionado a",
"Advanced": "Avançado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alinhamento",
"Amount": "Quantidade",
"Apply": "",
@@ -46,6 +53,7 @@
"Coming_Soon": "",
"Completed": "Completo",
"Conversion": "Conversão",
"ConvertUsingAI": "",
"Copy": "Copiar",
"Copy Link": "Copiar Ligação",
"Copy Token": "Copiar Chave",
@@ -60,6 +68,7 @@
"Create_New_Shopping Category": "Criar nova categoria de Compras",
"Create_New_Shopping_Category": "Adicionar nova categoria de compras",
"Create_New_Unit": "Adicionar nova unidade",
"Credits": "",
"Current_Period": "Período atual",
"Custom Filter": "",
"CustomImageHelp": "Fazer upload de uma image para mostrar na visão geral do espaço.",
@@ -114,10 +123,13 @@
"FoodOnHand": "Tem {food} disponível.",
"Food_Alias": "Alcunha da comida",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupar por",
"Hide_Food": "Esconder comida",
"Hide_Keyword": "",
@@ -155,10 +167,13 @@
"Learn_More": "Aprenda mais",
"Link": "Ligação",
"Load_More": "Carregar Mais",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registrar Culinária",
"Log_Recipe_Cooking": "Registrar Receitas de Culinária",
"Make_Header": "Tornar cabeçalho",
"Make_Ingredient": "Fazer ingrediente",
"ManageSubscription": "",
"Manage_Books": "Gerenciar Livros",
"Meal_Plan": "Plano de Refeição",
"Meal_Plan_Days": "Planos de alimentação futuros",
@@ -170,6 +185,8 @@
"Merge_Keyword": "Unir palavra-chave",
"MissingProperties": "",
"Month": "Mês",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mover",
"MoveCategory": "Mover para: ",
"Move_Down": "Mover para baixo",
@@ -266,6 +283,7 @@
"Selected": "Selecionado",
"Servings": "Doses",
"Settings": "Definições",
"SettingsOnlySuperuser": "",
"Share": "Partilhar",
"Shopping_Categories": "Categorias de Compras",
"Shopping_Category": "Categoria de Compras",

View File

@@ -1,6 +1,7 @@
{
"AI": "IA",
"AIImportSubtitle": "Use IA para importar imagens das receitas.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "Incluído Em",
"Admin": "Administrador",
"Advanced": "Avançado",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alinhamento",
"AllRecipes": "Todas Receitas",
"Amount": "Quantidade",
@@ -87,6 +94,7 @@
"Continue": "Continuar",
"Conversion": "Conversão",
"ConversionsHelp": "Com conversões, você pode calcular a quantidade de um alimento em diferentes unidades. Atualmente, isso é usado apenas para cálculo de propriedades, posteriormente poderá ser usado em outras partes do Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Registro de cozimento",
"CookLogHelp": "Entradas no registro de cozimento para receitas. ",
"Cooked": "Cozido",
@@ -109,6 +117,7 @@
"Create_New_Unit": "Incluir Nova Unidade",
"Created": "Criado",
"CreatedBy": "Criado por",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Período Atual",
"Custom Filter": "Filtro Customizado",
@@ -202,11 +211,14 @@
"Food_Replace": "Substituir Alimento",
"Foods": "Alimentos",
"Friday": "Sexta-feira",
"FromBalance": "",
"Fulltext": "Texto completo",
"FulltextHelp": "Campos para pesquisa textual completa. Observação: os métodos de pesquisa 'web', 'phrase' e 'raw' só funcionam com campos de pesquisa textual completa.",
"Fuzzy": "Fuzzy",
"FuzzySearchHelp": "Use pesquisa fuzzy para encontrar registros mesmo quando existem diferenças na grafia das palavras utilizadas.",
"GettingStarted": "Começando",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Agrupar Por",
"HeaderWarning": "Alerta: Mudanças de Cabeçalho apagam a Quantidade/Unidade/Alimento",
"Headline": "Título",
@@ -274,6 +286,8 @@
"Link": "Link",
"Load": "Carregar",
"Load_More": "Carregar mais",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Registro de Cozinha",
"Log_Recipe_Cooking": "Registrar receitas feitas",
"Logo": "Logotipo",
@@ -296,6 +310,8 @@
"Message": "Mensagem",
"MissingProperties": "",
"Month": "Mês",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mover",
"MoveCategory": "Mover Para: ",
"Move_Down": "Mover para baixo",
@@ -402,6 +418,7 @@
"Selected": "Selecionado",
"Servings": "Porções",
"Settings": "Configurações",
"SettingsOnlySuperuser": "",
"Share": "Compartilhar",
"ShoppingBackgroundSyncWarning": "Rede ruim, aguardando sincronização...",
"Shopping_Categories": "Categorias de Mercado",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -17,6 +18,12 @@
"Added_on": "Adăugat la",
"Advanced": "Avansat",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Amount": "Cantitate",
"App": "Aplicație",
"Apply": "",
@@ -53,6 +60,7 @@
"Coming_Soon": "În curând",
"Comments_setting": "Afișează comentarii",
"Completed": "Completat",
"ConvertUsingAI": "",
"Copy": "Copie",
"Copy Link": "Copiere link",
"Copy Token": "Copiere token",
@@ -69,6 +77,7 @@
"Create_New_Shopping Category": "Creați o nouă categorie de cumpărături",
"Create_New_Shopping_Category": "Adaugă categorie de cumpărături nouă",
"Create_New_Unit": "Adaugă unitate nouă",
"Credits": "",
"Current_Period": "Perioada curentă",
"Custom Filter": "Filtru personalizat",
"DELETE_ERROR": "",
@@ -121,10 +130,13 @@
"FoodOnHand": "Aveți {food} la îndemână.",
"Food_Alias": "Pseudonim mâncare",
"Foods": "Alimente",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Grupat de",
"Hide_Food": "Ascunde mâncare",
"Hide_Keyword": "Ascunde cuvintele cheie",
@@ -173,10 +185,13 @@
"Last_name": "Nume de familie",
"Link": "Link",
"Load_More": "Încărcați mai mult",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Jurnal de pregătire",
"Log_Recipe_Cooking": "Jurnalul rețetelor de pregătire",
"Make_Header": "Creare antet",
"Make_Ingredient": "Create ingredient",
"ManageSubscription": "",
"Manage_Books": "Gestionarea cărților",
"Manage_Emails": "Gestionarea e-mailurilor",
"Meal_Plan": "Plan de alimentare",
@@ -190,6 +205,8 @@
"Message": "Mesaj",
"MissingProperties": "",
"Month": "Lună",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Mută",
"MoveCategory": "Mută la: ",
"Move_Down": "Deplasați-vă în jos",
@@ -288,6 +305,7 @@
"Selected": "Selectat",
"Servings": "Porții",
"Settings": "Setări",
"SettingsOnlySuperuser": "",
"Share": "Împărtășire",
"Shopping_Categories": "Categorii de cumpărături",
"Shopping_Category": "Categorie de cumpărături",

View File

@@ -1,6 +1,7 @@
{
"AI": "AI",
"AIImportSubtitle": "Используй AI для импорта изображений рецептов.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Админ",
"Advanced": "Расширенный",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Выравнивание",
"AllRecipes": "Все рецепты",
"Amount": "Количество",
@@ -88,6 +95,7 @@
"Continue": "Продолжить",
"Conversion": "Преобразование",
"ConversionsHelp": "С помощью преобразований вы можете рассчитывать количество продукта в разных единицах измерения. В настоящее время это используется только для расчёта свойств, но в будущем может применяться и в других частях Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Журнал приготовления",
"CookLogHelp": "История приготовлений по рецептам. ",
"Cooked": "Приготовлено",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Добавить единицу измерения",
"Created": "Создано",
"CreatedBy": "Создано пользователем",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Текущий период",
"Custom Filter": "Пользовательский фильтр",
@@ -203,11 +212,14 @@
"Food_Replace": "Замена продукта",
"Foods": "Продукты",
"Friday": "Пятница",
"FromBalance": "",
"Fulltext": "Полнотекстовый",
"FulltextHelp": "Поля, используемые в полнотекстовом поиске. Важно: методы поиска web, phrase и raw применимы только к полнотекстовым полям.",
"Fuzzy": "Нечёткий",
"FuzzySearchHelp": "Нечёткий поиск позволяет находить записи, даже если в написании есть ошибки или отличия.",
"GettingStarted": "Начало работы",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Сгруппировать по",
"HeaderWarning": "Внимание: при преобразовании в заголовок удаляются данные о количестве, единице/измерения/продукте.",
"Headline": "Заголовок",
@@ -275,6 +287,8 @@
"Link": "Гиперссылка",
"Load": "Загрузить",
"Load_More": "Загрузить еще",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Журнал приготовления",
"Log_Recipe_Cooking": "Журнал приготовления",
"Logo": "Логотип",
@@ -302,6 +316,8 @@
"ModelSelectResultsHelp": "Показать больше результатов",
"Monday": "Понедельник",
"Month": "Месяц",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Ещё",
"Move": "Переместить",
"MoveCategory": "Переместить в: ",
@@ -454,6 +470,7 @@
"Servings": "Порции",
"ServingsText": "Описание порций",
"Settings": "Настройки",
"SettingsOnlySuperuser": "",
"Share": "Поделиться",
"ShopLater": "Купить позже",
"ShopNow": "Купить сейчас",

View File

@@ -1,6 +1,7 @@
{
"AI": "Umetna inteligenca",
"AIImportSubtitle": "Uporabite umetno inteligenco za uvoz slik receptov.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -27,6 +28,12 @@
"Admin": "Skrbnik",
"Advanced": "Napredno",
"Advanced Search Settings": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnava",
"AllRecipes": "Vsi recepti",
"Amount": "Količina",
@@ -88,6 +95,7 @@
"Continue": "Nadaljuj",
"Conversion": "Pogovor",
"ConversionsHelp": "S pretvorbami lahko izračunate količino živila v različnih enotah. Trenutno se to uporablja le za izračun lastnosti, kasneje pa se lahko uporabi tudi v drugih delih Tandoorja. ",
"ConvertUsingAI": "",
"CookLog": "Kuharski dnevnik",
"CookLogHelp": "Vnosi v dnevnik kuhanja za recepte. ",
"Cooked": "Kuhano",
@@ -110,6 +118,7 @@
"Create_New_Unit": "Dodaj novo enoto",
"Created": "Ustvarjeno",
"CreatedBy": "Ustvaril/a",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Trenutno obdobje",
"Custom Filter": "Filter po meri",
@@ -203,11 +212,14 @@
"Food_Replace": "Zamenjava živila",
"Foods": "Živila",
"Friday": "Petek",
"FromBalance": "",
"Fulltext": "Celotno besedilo",
"FulltextHelp": "Polja za iskanje po celotnem besedilu. Opomba: metode iskanja »splet«, »fraza« in »surovo« delujejo samo s polji po celotnem besedilu.",
"Fuzzy": "Nejasno",
"FuzzySearchHelp": "Uporabite mehko iskanje za iskanje vnosov, tudi če obstajajo razlike v načinu pisanja besede.",
"GettingStarted": "Začetek",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Združi po",
"HeaderWarning": "Opozorilo: Sprememba naslova izbriše količino/enoto/hrano",
"Headline": "Glavni naslov",
@@ -275,6 +287,8 @@
"Link": "Hiperpovezava",
"Load": "Naloži",
"Load_More": "Naloži več",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Zgodovina kuhanja",
"Log_Recipe_Cooking": "Beleži kuharski recept",
"Logo": "Logotip",
@@ -303,6 +317,8 @@
"ModelSelectResultsHelp": "Išči več rezultatov",
"Monday": "Ponedeljek",
"Month": "Mesec",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "Več",
"Move": "Premakni",
"MoveCategory": "Premakni v: ",
@@ -456,6 +472,7 @@
"Servings": "Porcije",
"ServingsText": "Besedilo o porcijah",
"Settings": "Nastavitve",
"SettingsOnlySuperuser": "",
"Share": "Deli",
"ShopLater": "Nakupujte pozneje",
"ShopNow": "Nakupujte zdaj",

View File

@@ -1,5 +1,6 @@
{
"AIImportSubtitle": "Använd AI för att importera bilder av recept.",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "Tillagd på",
"Admin": "Administratör",
"Advanced": "Avancerat",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Orientering",
"AllRecipes": "Alla recept",
"Amount": "Mängd",
@@ -87,6 +94,7 @@
"Continue": "Fortsätt",
"Conversion": "Omvandling",
"ConversionsHelp": "Med omvandlingar kan du beräkna mängden av ett livsmedel i olika enheter. För närvarande används detta endast för egenskapsberäkning, senare kan det även användas i andra delar av Tandoor. ",
"ConvertUsingAI": "",
"CookLog": "Tillagningslogg",
"CookLogHelp": "Poster i tillagningsloggen för recept. ",
"Cooked": "Tillagad",
@@ -109,6 +117,7 @@
"Create_New_Unit": "Lägg till enhet",
"Created": "Skapad",
"CreatedBy": "Skapad av",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "Nuvarande period",
"Custom Filter": "Anpassat filter",
@@ -180,10 +189,13 @@
"Food_Alias": "Alias för livsmedel",
"Food_Replace": "Ersätt ingrediens",
"Foods": "Livsmedel",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Gruppera enligt",
"Hide_Food": "Dölj livsmedel",
"Hide_Keyword": "Dölj nyckelord",
@@ -234,11 +246,14 @@
"Learn_More": "Läs mer",
"Link": "Länk",
"Load_More": "Ladda mer",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Logga tillagning",
"Log_Recipe_Cooking": "Logga tillagningen av receptet",
"Logo": "Logga",
"Make_Header": "Skapa rubrik",
"Make_Ingredient": "Skapa ingrediens",
"ManageSubscription": "",
"Manage_Books": "Hantera böcker",
"Manage_Emails": "Hantera mejladresser",
"Meal_Plan": "Måltidsplanering",
@@ -252,6 +267,8 @@
"Message": "Meddelande",
"MissingProperties": "",
"Month": "Månad",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Flytta",
"MoveCategory": "Flytta till: ",
"Move_Down": "Flytta ned",
@@ -364,6 +381,7 @@
"Selected": "Vald",
"Servings": "Portioner",
"Settings": "Inställningar",
"SettingsOnlySuperuser": "",
"Share": "Dela",
"ShoppingBackgroundSyncWarning": "Dålig uppkoppling, inväntar synkronisering...",
"Shopping_Categories": "Shopping kategorier",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "Ekleyen",
"Added_on": "Eklenme Zamanı",
"Advanced": "Gelişmiş",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Hizalama",
"Amount": "Miktar",
"App": "Uygulama",
@@ -57,6 +64,7 @@
"Comments_setting": "Yorumları Göster",
"Completed": "Tamamlandı",
"Conversion": "Dönüşüm",
"ConvertUsingAI": "",
"Copy": "Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"Copy Token": "Anahtarı Kopyala",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "Yeni Alışveriş Kategorisi Ekle",
"Create_New_Unit": "Yeni Birim Ekle",
"Created": "Oluşturuldu",
"Credits": "",
"Current_Period": "Mevcut Dönem",
"Custom Filter": "Özel Filtre",
"CustomImageHelp": "Alan genel bakışında gösterilecek bir resim yükleyin.",
@@ -143,10 +152,13 @@
"Food_Alias": "Yiyecek Takma Adı",
"Food_Replace": "Yiyecek Değiştir",
"Foods": "Yiyecekler",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "Gruplandırma Ölçütü",
"Hide_Food": "Yiyeceği Gizle",
"Hide_Keyword": "Anahtar kelimeleri gizle",
@@ -197,11 +209,14 @@
"Learn_More": "Daha Fazla",
"Link": "Bağlantı",
"Load_More": "Daha Fazla Yükle",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Günlük Pişirme",
"Log_Recipe_Cooking": "Günlük Tarif Pişirme",
"Logo": "Logo",
"Make_Header": "Başlık Oluştur",
"Make_Ingredient": "Malzeme Oluştur",
"ManageSubscription": "",
"Manage_Books": "Kitapları Yönet",
"Manage_Emails": "E-postaları Yönet",
"Meal_Plan": "Yemek Planı",
@@ -215,6 +230,8 @@
"Message": "Mesaj",
"MissingProperties": "",
"Month": "Ay",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Taşı",
"MoveCategory": "Taşı: ",
"Move_Down": "Aşağıya Taşı",
@@ -327,6 +344,7 @@
"Selected": "Seçilen",
"Servings": "Servis Sayısı",
"Settings": "Ayarlar",
"SettingsOnlySuperuser": "",
"Share": "Paylaş",
"ShoppingBackgroundSyncWarning": "Kötü bağlantı, senkronizasyon bekleniyor...",
"Shopping_Categories": "Alışveriş Kategorileri",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API_Browser": "",
"API_Documentation": "",
"Add": "Додати",
@@ -14,6 +15,12 @@
"Added_by": "Додано",
"Added_on": "Додано На",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Вирівнювання",
"Amount": "Кількість",
"App": "",
@@ -50,6 +57,7 @@
"Coming_Soon": "",
"Completed": "Виконано",
"Conversion": "Конвертування",
"ConvertUsingAI": "",
"Copy": "Копіювати",
"Copy Link": "Скопіювати Посилання",
"Copy Token": "Скопіювати Токен",
@@ -64,6 +72,7 @@
"Create_New_Shopping Category": "Створити Нову Категорію Покупок",
"Create_New_Shopping_Category": "Додати Нову Категорію Покупок",
"Create_New_Unit": "Додати Нову Одиницю",
"Credits": "",
"Current_Period": "Теперішній Період",
"Custom Filter": "",
"CustomImageHelp": "Завантажте зображення що буде показуватись у огляді простору.",
@@ -124,10 +133,13 @@
"FoodOnHand": "Ви маєте {food} на руках.",
"Food_Alias": "Найменування Їжі",
"Foods": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "По Групі",
"Hide_Food": "Сховати Їжу",
"Hide_Keyword": "",
@@ -172,10 +184,13 @@
"Learn_More": "Дізнатися Більше",
"Link": "Посилання",
"Load_More": "Завантажити більше",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "Занотувати приготування",
"Log_Recipe_Cooking": "Журнал приготування",
"Make_Header": "Створити Заголовок",
"Make_Ingredient": "Створити Інгрідієнт",
"ManageSubscription": "",
"Manage_Books": "Управління Книжкою",
"Meal_Plan": "План Харчування",
"Meal_Plan_Days": "Майбутній план харчування",
@@ -187,6 +202,8 @@
"Merge_Keyword": "Об'єднати Ключове слово",
"MissingProperties": "",
"Month": "Місяць",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "Перемістити",
"MoveCategory": "Перемістити До: ",
"Move_Down": "Перемістити вниз",
@@ -289,6 +306,7 @@
"Selected": "Вибрано",
"Servings": "Порції",
"Settings": "Налаштування",
"SettingsOnlySuperuser": "",
"Share": "Поділитися",
"Shopping_Categories": "Категорії Покупок",
"Shopping_Category": "Категорія Покупок",

View File

@@ -1,4 +1,5 @@
{
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -16,6 +17,12 @@
"Added_by": "添加者",
"Added_on": "添加到",
"Advanced": "高级",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "校准",
"Amount": "数量",
"App": "应用",
@@ -57,6 +64,7 @@
"Comments_setting": "显示评论",
"Completed": "完成",
"Conversion": "转换",
"ConvertUsingAI": "",
"Copy": "复制",
"Copy Link": "复制链接",
"Copy Token": "复制令牌",
@@ -74,6 +82,7 @@
"Create_New_Shopping_Category": "添加新的购物类别",
"Create_New_Unit": "添加新的单位",
"Created": "已创建",
"Credits": "",
"Current_Period": "本期",
"Custom Filter": "自定义筛选器",
"CustomImageHelp": "上传图片以在空间概览中显示。",
@@ -143,10 +152,13 @@
"Food_Alias": "食物别名",
"Food_Replace": "食物替换",
"Foods": "食物",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"Global": "",
"GlobalHelp": "",
"GroupBy": "分组",
"Hide_Food": "隐藏食物",
"Hide_Keyword": "隐藏关键词",
@@ -197,11 +209,14 @@
"Learn_More": "了解更多",
"Link": "链接",
"Load_More": "加载更多",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "烹饪记录",
"Log_Recipe_Cooking": "食谱烹饪记录",
"Logo": "徽标",
"Make_Header": "显示注意事项",
"Make_Ingredient": "制作食材",
"ManageSubscription": "",
"Manage_Books": "烹饪手册管理",
"Manage_Emails": "管理电子邮件",
"Meal_Plan": "用餐计划",
@@ -215,6 +230,8 @@
"Message": "信息",
"MissingProperties": "",
"Month": "月份",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"Move": "移动",
"MoveCategory": "移动到: ",
"Move_Down": "下移",
@@ -327,6 +344,7 @@
"Selected": "选定",
"Servings": "份量",
"Settings": "设置",
"SettingsOnlySuperuser": "",
"Share": "分享",
"ShoppingBackgroundSyncWarning": "网络状况不佳,正在等待进行同步……",
"Shopping_Categories": "购物类别",

View File

@@ -1,6 +1,7 @@
{
"AI": "人工智慧",
"AIImportSubtitle": "以人工智慧匯入食譜圖片。",
"AISettingsHostedHelp": "",
"API": "API",
"API_Browser": "",
"API_Documentation": "",
@@ -26,6 +27,12 @@
"Added_on": "添加於",
"Admin": "管理者",
"Advanced": "高級",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "對齊",
"AllRecipes": "所有食譜",
"Amount": "數量",
@@ -87,6 +94,7 @@
"Continue": "繼續",
"Conversion": "轉換",
"ConversionsHelp": "透過轉換功能,您可以計算食物在不同單位下的數量。目前這僅用於屬性計算,未來也可能用於 Tandoor 的其他部分。 ",
"ConvertUsingAI": "",
"CookLog": "烹飪記錄",
"CookLogHelp": "食譜的烹飪記錄條目。 ",
"Cooked": "已烹飪",
@@ -109,6 +117,7 @@
"Create_New_Unit": "建立新單位",
"Created": "建立",
"CreatedBy": "建立者",
"Credits": "",
"Ctrl+K": "Ctrl+K",
"Current_Period": "當前期間",
"Custom Filter": "自定義篩選器",
@@ -202,11 +211,14 @@
"Food_Replace": "食物替換",
"Foods": "食物",
"Friday": "星期五",
"FromBalance": "",
"Fulltext": "全文",
"FulltextHelp": "全文搜索的字段。注意:'web'、'phrase' 和 'raw' 搜索方法僅對全文欄位有效。",
"Fuzzy": "模糊",
"FuzzySearchHelp": "使用模糊搜索來查找條目,即使單詞的寫法存在差異。",
"GettingStarted": "開始使用",
"Global": "",
"GlobalHelp": "",
"GroupBy": "分組依據",
"HeaderWarning": "警告:變更為標題會刪除數量/單位/食物",
"Headline": "標題",
@@ -274,6 +286,8 @@
"Link": "連結",
"Load": "載入",
"Load_More": "載入更多",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "記錄烹飪",
"Log_Recipe_Cooking": "記錄食譜烹飪",
"Logo": "標誌",
@@ -302,6 +316,8 @@
"ModelSelectResultsHelp": "搜尋更多結果",
"Monday": "星期一",
"Month": "月",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "更多",
"Move": "移動",
"MoveCategory": "移動至: ",
@@ -455,6 +471,7 @@
"Servings": "份量",
"ServingsText": "份量文字",
"Settings": "設定",
"SettingsOnlySuperuser": "",
"Share": "分享",
"ShopLater": "稍後購物",
"ShopNow": "立即購物",

View File

@@ -3,6 +3,8 @@ apis/ApiTokenAuthApi.ts
apis/index.ts
index.ts
models/AccessToken.ts
models/AiLog.ts
models/AiProvider.ts
models/AlignmentEnum.ts
models/AuthToken.ts
models/AutoMealPlan.ts
@@ -27,6 +29,7 @@ models/ExportRequest.ts
models/FdcQuery.ts
models/FdcQueryFoods.ts
models/Food.ts
models/FoodBatchUpdate.ts
models/FoodInheritField.ts
models/FoodShoppingUpdate.ts
models/FoodSimple.ts
@@ -57,6 +60,8 @@ models/OpenDataStoreCategory.ts
models/OpenDataUnit.ts
models/OpenDataUnitTypeEnum.ts
models/OpenDataVersion.ts
models/PaginatedAiLogList.ts
models/PaginatedAiProviderList.ts
models/PaginatedAutomationList.ts
models/PaginatedBookmarkletImportListList.ts
models/PaginatedConnectorConfigList.ts
@@ -103,6 +108,7 @@ models/PaginatedUserSpaceList.ts
models/PaginatedViewLogList.ts
models/ParsedIngredient.ts
models/PatchedAccessToken.ts
models/PatchedAiProvider.ts
models/PatchedAutomation.ts
models/PatchedBookmarkletImport.ts
models/PatchedConnectorConfig.ts

View File

@@ -16,6 +16,8 @@
import * as runtime from '../runtime';
import type {
AccessToken,
AiLog,
AiProvider,
AutoMealPlan,
Automation,
BookmarkletImport,
@@ -28,6 +30,7 @@ import type {
ExportRequest,
FdcQuery,
Food,
FoodBatchUpdate,
FoodInheritField,
FoodShoppingUpdate,
Group,
@@ -49,6 +52,8 @@ import type {
OpenDataStore,
OpenDataUnit,
OpenDataVersion,
PaginatedAiLogList,
PaginatedAiProviderList,
PaginatedAutomationList,
PaginatedBookmarkletImportListList,
PaginatedConnectorConfigList,
@@ -95,6 +100,7 @@ import type {
PaginatedViewLogList,
ParsedIngredient,
PatchedAccessToken,
PatchedAiProvider,
PatchedAutomation,
PatchedBookmarkletImport,
PatchedConnectorConfig,
@@ -179,6 +185,10 @@ import type {
import {
AccessTokenFromJSON,
AccessTokenToJSON,
AiLogFromJSON,
AiLogToJSON,
AiProviderFromJSON,
AiProviderToJSON,
AutoMealPlanFromJSON,
AutoMealPlanToJSON,
AutomationFromJSON,
@@ -203,6 +213,8 @@ import {
FdcQueryToJSON,
FoodFromJSON,
FoodToJSON,
FoodBatchUpdateFromJSON,
FoodBatchUpdateToJSON,
FoodInheritFieldFromJSON,
FoodInheritFieldToJSON,
FoodShoppingUpdateFromJSON,
@@ -245,6 +257,10 @@ import {
OpenDataUnitToJSON,
OpenDataVersionFromJSON,
OpenDataVersionToJSON,
PaginatedAiLogListFromJSON,
PaginatedAiLogListToJSON,
PaginatedAiProviderListFromJSON,
PaginatedAiProviderListToJSON,
PaginatedAutomationListFromJSON,
PaginatedAutomationListToJSON,
PaginatedBookmarkletImportListListFromJSON,
@@ -337,6 +353,8 @@ import {
ParsedIngredientToJSON,
PatchedAccessTokenFromJSON,
PatchedAccessTokenToJSON,
PatchedAiProviderFromJSON,
PatchedAiProviderToJSON,
PatchedAutomationFromJSON,
PatchedAutomationToJSON,
PatchedBookmarkletImportFromJSON,
@@ -522,11 +540,48 @@ export interface ApiAccessTokenUpdateRequest {
}
export interface ApiAiImportCreateRequest {
aiProviderId: number;
file: string | null;
text: string | null;
recipeId: string | null;
}
export interface ApiAiLogListRequest {
page?: number;
pageSize?: number;
}
export interface ApiAiLogRetrieveRequest {
id: number;
}
export interface ApiAiProviderCreateRequest {
aiProvider: Omit<AiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAiProviderDestroyRequest {
id: number;
}
export interface ApiAiProviderListRequest {
page?: number;
pageSize?: number;
}
export interface ApiAiProviderPartialUpdateRequest {
id: number;
patchedAiProvider?: Omit<PatchedAiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAiProviderRetrieveRequest {
id: number;
}
export interface ApiAiProviderUpdateRequest {
id: number;
aiProvider: Omit<AiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAutoPlanCreateRequest {
autoMealPlan: AutoMealPlan;
}
@@ -759,6 +814,11 @@ export interface ApiEnterpriseSocialRecipeCreateRequest {
recipe: Omit<Recipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
}
export interface ApiEnterpriseSocialRecipeDeleteExternalPartialUpdateRequest {
id: number;
patchedRecipe?: Omit<PatchedRecipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
}
export interface ApiEnterpriseSocialRecipeDestroyRequest {
id: number;
}
@@ -905,6 +965,10 @@ export interface ApiFdcSearchRetrieveRequest {
query?: string;
}
export interface ApiFoodBatchUpdateUpdateRequest {
foodBatchUpdate: FoodBatchUpdate;
}
export interface ApiFoodCreateRequest {
food: Omit<Food, 'shopping'|'parent'|'numchild'|'fullName'|'substituteOnhand'>;
}
@@ -978,6 +1042,7 @@ export interface ApiGroupRetrieveRequest {
}
export interface ApiImportCreateRequest {
aiProviderId: number;
file: string | null;
text: string | null;
recipeId: string | null;
@@ -1512,6 +1577,11 @@ export interface ApiRecipeCreateRequest {
recipe: Omit<Recipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
}
export interface ApiRecipeDeleteExternalPartialUpdateRequest {
id: number;
patchedRecipe?: Omit<PatchedRecipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
}
export interface ApiRecipeDestroyRequest {
id: number;
}
@@ -1722,7 +1792,7 @@ export interface ApiSpaceListRequest {
export interface ApiSpacePartialUpdateRequest {
id: number;
patchedSpace?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'>;
patchedSpace?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'>;
}
export interface ApiSpaceRetrieveRequest {
@@ -1941,6 +2011,7 @@ export interface ApiUnitConversionListRequest {
foodId?: number;
page?: number;
pageSize?: number;
query?: string;
}
export interface ApiUnitConversionPartialUpdateRequest {
@@ -2358,6 +2429,13 @@ export class ApiApi extends runtime.BaseAPI {
* given an image or PDF file convert its content to a structured recipe using AI and the scraping system
*/
async apiAiImportCreateRaw(requestParameters: ApiAiImportCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<RecipeFromSourceResponse>> {
if (requestParameters['aiProviderId'] == null) {
throw new runtime.RequiredError(
'aiProviderId',
'Required parameter "aiProviderId" was null or undefined when calling apiAiImportCreate().'
);
}
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
@@ -2401,6 +2479,10 @@ export class ApiApi extends runtime.BaseAPI {
formParams = new URLSearchParams();
}
if (requestParameters['aiProviderId'] != null) {
formParams.append('ai_provider_id', requestParameters['aiProviderId'] as any);
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
@@ -2432,6 +2514,319 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogListRaw(requestParameters: ApiAiLogListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedAiLogList>> {
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/ai-log/`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedAiLogListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogList(requestParameters: ApiAiLogListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedAiLogList> {
const response = await this.apiAiLogListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogRetrieveRaw(requestParameters: ApiAiLogRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiLog>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiLogRetrieve().'
);
}
const queryParameters: any = {};
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/ai-log/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiLogFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiLogRetrieve(requestParameters: ApiAiLogRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiLog> {
const response = await this.apiAiLogRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderCreateRaw(requestParameters: ApiAiProviderCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['aiProvider'] == null) {
throw new runtime.RequiredError(
'aiProvider',
'Required parameter "aiProvider" was null or undefined when calling apiAiProviderCreate().'
);
}
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/ai-provider/`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: AiProviderToJSON(requestParameters['aiProvider']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderCreate(requestParameters: ApiAiProviderCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderDestroyRaw(requestParameters: ApiAiProviderDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderDestroy().'
);
}
const queryParameters: any = {};
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/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'DELETE',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderDestroy(requestParameters: ApiAiProviderDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.apiAiProviderDestroyRaw(requestParameters, initOverrides);
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderListRaw(requestParameters: ApiAiProviderListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedAiProviderList>> {
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/ai-provider/`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedAiProviderListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderList(requestParameters: ApiAiProviderListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedAiProviderList> {
const response = await this.apiAiProviderListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderPartialUpdateRaw(requestParameters: ApiAiProviderPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderPartialUpdate().'
);
}
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/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: PatchedAiProviderToJSON(requestParameters['patchedAiProvider']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderPartialUpdate(requestParameters: ApiAiProviderPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderPartialUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderRetrieveRaw(requestParameters: ApiAiProviderRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderRetrieve().'
);
}
const queryParameters: any = {};
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/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderRetrieve(requestParameters: ApiAiProviderRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderUpdateRaw(requestParameters: ApiAiProviderUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<AiProvider>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiAiProviderUpdate().'
);
}
if (requestParameters['aiProvider'] == null) {
throw new runtime.RequiredError(
'aiProvider',
'Required parameter "aiProvider" was null or undefined when calling apiAiProviderUpdate().'
);
}
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/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PUT',
headers: headerParameters,
query: queryParameters,
body: AiProviderToJSON(requestParameters['aiProvider']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderUpdate(requestParameters: ApiAiProviderUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<AiProvider> {
const response = await this.apiAiProviderUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -4410,6 +4805,46 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiEnterpriseSocialRecipeDeleteExternalPartialUpdateRaw(requestParameters: ApiEnterpriseSocialRecipeDeleteExternalPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Recipe>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiEnterpriseSocialRecipeDeleteExternalPartialUpdate().'
);
}
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/enterprise-social-recipe/{id}/delete_external/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: PatchedRecipeToJSON(requestParameters['patchedRecipe']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiEnterpriseSocialRecipeDeleteExternalPartialUpdate(requestParameters: ApiEnterpriseSocialRecipeDeleteExternalPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Recipe> {
const response = await this.apiEnterpriseSocialRecipeDeleteExternalPartialUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -5528,6 +5963,46 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiFoodBatchUpdateUpdateRaw(requestParameters: ApiFoodBatchUpdateUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<FoodBatchUpdate>> {
if (requestParameters['foodBatchUpdate'] == null) {
throw new runtime.RequiredError(
'foodBatchUpdate',
'Required parameter "foodBatchUpdate" was null or undefined when calling apiFoodBatchUpdateUpdate().'
);
}
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/food/batch_update/`,
method: 'PUT',
headers: headerParameters,
query: queryParameters,
body: FoodBatchUpdateToJSON(requestParameters['foodBatchUpdate']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => FoodBatchUpdateFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiFoodBatchUpdateUpdate(requestParameters: ApiFoodBatchUpdateUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<FoodBatchUpdate> {
const response = await this.apiFoodBatchUpdateUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -6201,6 +6676,13 @@ export class ApiApi extends runtime.BaseAPI {
/**
*/
async apiImportCreateRaw(requestParameters: ApiImportCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<RecipeFromSourceResponse>> {
if (requestParameters['aiProviderId'] == null) {
throw new runtime.RequiredError(
'aiProviderId',
'Required parameter "aiProviderId" was null or undefined when calling apiImportCreate().'
);
}
if (requestParameters['file'] == null) {
throw new runtime.RequiredError(
'file',
@@ -6244,6 +6726,10 @@ export class ApiApi extends runtime.BaseAPI {
formParams = new URLSearchParams();
}
if (requestParameters['aiProviderId'] != null) {
formParams.append('ai_provider_id', requestParameters['aiProviderId'] as any);
}
if (requestParameters['file'] != null) {
formParams.append('file', requestParameters['file'] as any);
}
@@ -10818,6 +11304,46 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiRecipeDeleteExternalPartialUpdateRaw(requestParameters: ApiRecipeDeleteExternalPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Recipe>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiRecipeDeleteExternalPartialUpdate().'
);
}
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/recipe/{id}/delete_external/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: PatchedRecipeToJSON(requestParameters['patchedRecipe']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiRecipeDeleteExternalPartialUpdate(requestParameters: ApiRecipeDeleteExternalPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Recipe> {
const response = await this.apiRecipeDeleteExternalPartialUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -14492,6 +15018,10 @@ export class ApiApi extends runtime.BaseAPI {
queryParameters['page_size'] = requestParameters['pageSize'];
}
if (requestParameters['query'] != null) {
queryParameters['query'] = requestParameters['query'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {

View File

@@ -0,0 +1,157 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { AiProvider } from './AiProvider';
import {
AiProviderFromJSON,
AiProviderFromJSONTyped,
AiProviderToJSON,
} from './AiProvider';
/**
*
* @export
* @interface AiLog
*/
export interface AiLog {
/**
*
* @type {number}
* @memberof AiLog
*/
id?: number;
/**
*
* @type {AiProvider}
* @memberof AiLog
*/
readonly aiProvider: AiProvider;
/**
*
* @type {string}
* @memberof AiLog
*/
_function: string;
/**
*
* @type {number}
* @memberof AiLog
*/
creditCost: number;
/**
*
* @type {boolean}
* @memberof AiLog
*/
creditsFromBalance?: boolean;
/**
*
* @type {number}
* @memberof AiLog
*/
inputTokens?: number;
/**
*
* @type {number}
* @memberof AiLog
*/
outputTokens?: number;
/**
*
* @type {Date}
* @memberof AiLog
*/
startTime?: Date;
/**
*
* @type {Date}
* @memberof AiLog
*/
endTime?: Date;
/**
*
* @type {number}
* @memberof AiLog
*/
createdBy?: number;
/**
*
* @type {Date}
* @memberof AiLog
*/
readonly createdAt: Date;
/**
*
* @type {Date}
* @memberof AiLog
*/
readonly updatedAt: Date;
}
/**
* Check if a given object implements the AiLog interface.
*/
export function instanceOfAiLog(value: object): value is AiLog {
if (!('aiProvider' in value) || value['aiProvider'] === undefined) return false;
if (!('_function' in value) || value['_function'] === undefined) return false;
if (!('creditCost' in value) || value['creditCost'] === undefined) return false;
if (!('createdAt' in value) || value['createdAt'] === undefined) return false;
if (!('updatedAt' in value) || value['updatedAt'] === undefined) return false;
return true;
}
export function AiLogFromJSON(json: any): AiLog {
return AiLogFromJSONTyped(json, false);
}
export function AiLogFromJSONTyped(json: any, ignoreDiscriminator: boolean): AiLog {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'aiProvider': AiProviderFromJSON(json['ai_provider']),
'_function': json['function'],
'creditCost': json['credit_cost'],
'creditsFromBalance': json['credits_from_balance'] == null ? undefined : json['credits_from_balance'],
'inputTokens': json['input_tokens'] == null ? undefined : json['input_tokens'],
'outputTokens': json['output_tokens'] == null ? undefined : json['output_tokens'],
'startTime': json['start_time'] == null ? undefined : (new Date(json['start_time'])),
'endTime': json['end_time'] == null ? undefined : (new Date(json['end_time'])),
'createdBy': json['created_by'] == null ? undefined : json['created_by'],
'createdAt': (new Date(json['created_at'])),
'updatedAt': (new Date(json['updated_at'])),
};
}
export function AiLogToJSON(value?: Omit<AiLog, 'aiProvider'|'createdAt'|'updatedAt'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'function': value['_function'],
'credit_cost': value['creditCost'],
'credits_from_balance': value['creditsFromBalance'],
'input_tokens': value['inputTokens'],
'output_tokens': value['outputTokens'],
'start_time': value['startTime'] == null ? undefined : ((value['startTime'] as any).toISOString()),
'end_time': value['endTime'] == null ? undefined : ((value['endTime'] as any).toISOString()),
'created_by': value['createdBy'],
};
}

View File

@@ -0,0 +1,134 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface AiProvider
*/
export interface AiProvider {
/**
*
* @type {number}
* @memberof AiProvider
*/
id?: number;
/**
*
* @type {string}
* @memberof AiProvider
*/
name: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
description?: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
apiKey?: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
modelName: string;
/**
*
* @type {string}
* @memberof AiProvider
*/
url?: string;
/**
*
* @type {boolean}
* @memberof AiProvider
*/
logCreditCost?: boolean;
/**
*
* @type {number}
* @memberof AiProvider
*/
space?: number;
/**
*
* @type {Date}
* @memberof AiProvider
*/
readonly createdAt: Date;
/**
*
* @type {Date}
* @memberof AiProvider
*/
readonly updatedAt: Date;
}
/**
* Check if a given object implements the AiProvider interface.
*/
export function instanceOfAiProvider(value: object): value is AiProvider {
if (!('name' in value) || value['name'] === undefined) return false;
if (!('modelName' in value) || value['modelName'] === undefined) return false;
if (!('createdAt' in value) || value['createdAt'] === undefined) return false;
if (!('updatedAt' in value) || value['updatedAt'] === undefined) return false;
return true;
}
export function AiProviderFromJSON(json: any): AiProvider {
return AiProviderFromJSONTyped(json, false);
}
export function AiProviderFromJSONTyped(json: any, ignoreDiscriminator: boolean): AiProvider {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'],
'description': json['description'] == null ? undefined : json['description'],
'apiKey': json['api_key'] == null ? undefined : json['api_key'],
'modelName': json['model_name'],
'url': json['url'] == null ? undefined : json['url'],
'logCreditCost': json['log_credit_cost'] == null ? undefined : json['log_credit_cost'],
'space': json['space'] == null ? undefined : json['space'],
'createdAt': (new Date(json['created_at'])),
'updatedAt': (new Date(json['updated_at'])),
};
}
export function AiProviderToJSON(value?: Omit<AiProvider, 'createdAt'|'updatedAt'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'description': value['description'],
'api_key': value['apiKey'],
'model_name': value['modelName'],
'url': value['url'],
'log_credit_cost': value['logCreditCost'],
'space': value['space'],
};
}

View File

@@ -109,8 +109,8 @@ export function AutoMealPlanToJSON(value?: AutoMealPlan | null): any {
}
return {
'start_date': ((value['startDate']).toISOString().substring(0,10)),
'end_date': ((value['endDate']).toISOString().substring(0,10)),
'start_date': ((value['startDate']).toISOString()),
'end_date': ((value['endDate']).toISOString()),
'meal_type_id': value['mealTypeId'],
'keyword_ids': value['keywordIds'],
'servings': value['servings'],

View File

@@ -0,0 +1,222 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface FoodBatchUpdate
*/
export interface FoodBatchUpdate {
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
foods: Array<number>;
/**
*
* @type {number}
* @memberof FoodBatchUpdate
*/
category?: number;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
substituteAdd: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
substituteRemove: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
substituteSet: Array<number>;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
substituteRemoveAll?: boolean;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
inheritFieldsAdd: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
inheritFieldsRemove: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
inheritFieldsSet: Array<number>;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
inheritFieldsRemoveAll?: boolean;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
childInheritFieldsAdd: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
childInheritFieldsRemove: Array<number>;
/**
*
* @type {Array<number>}
* @memberof FoodBatchUpdate
*/
childInheritFieldsSet: Array<number>;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
childInheritFieldsRemoveAll?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
substituteChildren?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
substituteSiblings?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
ignoreShopping?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
onHand?: boolean;
/**
*
* @type {boolean}
* @memberof FoodBatchUpdate
*/
parentRemove?: boolean;
/**
*
* @type {number}
* @memberof FoodBatchUpdate
*/
parentSet?: number;
}
/**
* Check if a given object implements the FoodBatchUpdate interface.
*/
export function instanceOfFoodBatchUpdate(value: object): value is FoodBatchUpdate {
if (!('foods' in value) || value['foods'] === undefined) return false;
if (!('substituteAdd' in value) || value['substituteAdd'] === undefined) return false;
if (!('substituteRemove' in value) || value['substituteRemove'] === undefined) return false;
if (!('substituteSet' in value) || value['substituteSet'] === undefined) return false;
if (!('inheritFieldsAdd' in value) || value['inheritFieldsAdd'] === undefined) return false;
if (!('inheritFieldsRemove' in value) || value['inheritFieldsRemove'] === undefined) return false;
if (!('inheritFieldsSet' in value) || value['inheritFieldsSet'] === undefined) return false;
if (!('childInheritFieldsAdd' in value) || value['childInheritFieldsAdd'] === undefined) return false;
if (!('childInheritFieldsRemove' in value) || value['childInheritFieldsRemove'] === undefined) return false;
if (!('childInheritFieldsSet' in value) || value['childInheritFieldsSet'] === undefined) return false;
return true;
}
export function FoodBatchUpdateFromJSON(json: any): FoodBatchUpdate {
return FoodBatchUpdateFromJSONTyped(json, false);
}
export function FoodBatchUpdateFromJSONTyped(json: any, ignoreDiscriminator: boolean): FoodBatchUpdate {
if (json == null) {
return json;
}
return {
'foods': json['foods'],
'category': json['category'] == null ? undefined : json['category'],
'substituteAdd': json['substitute_add'],
'substituteRemove': json['substitute_remove'],
'substituteSet': json['substitute_set'],
'substituteRemoveAll': json['substitute_remove_all'] == null ? undefined : json['substitute_remove_all'],
'inheritFieldsAdd': json['inherit_fields_add'],
'inheritFieldsRemove': json['inherit_fields_remove'],
'inheritFieldsSet': json['inherit_fields_set'],
'inheritFieldsRemoveAll': json['inherit_fields_remove_all'] == null ? undefined : json['inherit_fields_remove_all'],
'childInheritFieldsAdd': json['child_inherit_fields_add'],
'childInheritFieldsRemove': json['child_inherit_fields_remove'],
'childInheritFieldsSet': json['child_inherit_fields_set'],
'childInheritFieldsRemoveAll': json['child_inherit_fields_remove_all'] == null ? undefined : json['child_inherit_fields_remove_all'],
'substituteChildren': json['substitute_children'] == null ? undefined : json['substitute_children'],
'substituteSiblings': json['substitute_siblings'] == null ? undefined : json['substitute_siblings'],
'ignoreShopping': json['ignore_shopping'] == null ? undefined : json['ignore_shopping'],
'onHand': json['on_hand'] == null ? undefined : json['on_hand'],
'parentRemove': json['parent_remove'] == null ? undefined : json['parent_remove'],
'parentSet': json['parent_set'] == null ? undefined : json['parent_set'],
};
}
export function FoodBatchUpdateToJSON(value?: FoodBatchUpdate | null): any {
if (value == null) {
return value;
}
return {
'foods': value['foods'],
'category': value['category'],
'substitute_add': value['substituteAdd'],
'substitute_remove': value['substituteRemove'],
'substitute_set': value['substituteSet'],
'substitute_remove_all': value['substituteRemoveAll'],
'inherit_fields_add': value['inheritFieldsAdd'],
'inherit_fields_remove': value['inheritFieldsRemove'],
'inherit_fields_set': value['inheritFieldsSet'],
'inherit_fields_remove_all': value['inheritFieldsRemoveAll'],
'child_inherit_fields_add': value['childInheritFieldsAdd'],
'child_inherit_fields_remove': value['childInheritFieldsRemove'],
'child_inherit_fields_set': value['childInheritFieldsSet'],
'child_inherit_fields_remove_all': value['childInheritFieldsRemoveAll'],
'substitute_children': value['substituteChildren'],
'substitute_siblings': value['substituteSiblings'],
'ignore_shopping': value['ignoreShopping'],
'on_hand': value['onHand'],
'parent_remove': value['parentRemove'],
'parent_set': value['parentSet'],
};
}

View File

@@ -0,0 +1,101 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { AiLog } from './AiLog';
import {
AiLogFromJSON,
AiLogFromJSONTyped,
AiLogToJSON,
} from './AiLog';
/**
*
* @export
* @interface PaginatedAiLogList
*/
export interface PaginatedAiLogList {
/**
*
* @type {number}
* @memberof PaginatedAiLogList
*/
count: number;
/**
*
* @type {string}
* @memberof PaginatedAiLogList
*/
next?: string;
/**
*
* @type {string}
* @memberof PaginatedAiLogList
*/
previous?: string;
/**
*
* @type {Array<AiLog>}
* @memberof PaginatedAiLogList
*/
results: Array<AiLog>;
/**
*
* @type {Date}
* @memberof PaginatedAiLogList
*/
timestamp?: Date;
}
/**
* Check if a given object implements the PaginatedAiLogList interface.
*/
export function instanceOfPaginatedAiLogList(value: object): value is PaginatedAiLogList {
if (!('count' in value) || value['count'] === undefined) return false;
if (!('results' in value) || value['results'] === undefined) return false;
return true;
}
export function PaginatedAiLogListFromJSON(json: any): PaginatedAiLogList {
return PaginatedAiLogListFromJSONTyped(json, false);
}
export function PaginatedAiLogListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedAiLogList {
if (json == null) {
return json;
}
return {
'count': json['count'],
'next': json['next'] == null ? undefined : json['next'],
'previous': json['previous'] == null ? undefined : json['previous'],
'results': ((json['results'] as Array<any>).map(AiLogFromJSON)),
'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])),
};
}
export function PaginatedAiLogListToJSON(value?: PaginatedAiLogList | null): any {
if (value == null) {
return value;
}
return {
'count': value['count'],
'next': value['next'],
'previous': value['previous'],
'results': ((value['results'] as Array<any>).map(AiLogToJSON)),
'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()),
};
}

View File

@@ -0,0 +1,101 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { AiProvider } from './AiProvider';
import {
AiProviderFromJSON,
AiProviderFromJSONTyped,
AiProviderToJSON,
} from './AiProvider';
/**
*
* @export
* @interface PaginatedAiProviderList
*/
export interface PaginatedAiProviderList {
/**
*
* @type {number}
* @memberof PaginatedAiProviderList
*/
count: number;
/**
*
* @type {string}
* @memberof PaginatedAiProviderList
*/
next?: string;
/**
*
* @type {string}
* @memberof PaginatedAiProviderList
*/
previous?: string;
/**
*
* @type {Array<AiProvider>}
* @memberof PaginatedAiProviderList
*/
results: Array<AiProvider>;
/**
*
* @type {Date}
* @memberof PaginatedAiProviderList
*/
timestamp?: Date;
}
/**
* Check if a given object implements the PaginatedAiProviderList interface.
*/
export function instanceOfPaginatedAiProviderList(value: object): value is PaginatedAiProviderList {
if (!('count' in value) || value['count'] === undefined) return false;
if (!('results' in value) || value['results'] === undefined) return false;
return true;
}
export function PaginatedAiProviderListFromJSON(json: any): PaginatedAiProviderList {
return PaginatedAiProviderListFromJSONTyped(json, false);
}
export function PaginatedAiProviderListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedAiProviderList {
if (json == null) {
return json;
}
return {
'count': json['count'],
'next': json['next'] == null ? undefined : json['next'],
'previous': json['previous'] == null ? undefined : json['previous'],
'results': ((json['results'] as Array<any>).map(AiProviderFromJSON)),
'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])),
};
}
export function PaginatedAiProviderListToJSON(value?: PaginatedAiProviderList | null): any {
if (value == null) {
return value;
}
return {
'count': value['count'],
'next': value['next'],
'previous': value['previous'],
'results': ((value['results'] as Array<any>).map(AiProviderToJSON)),
'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()),
};
}

View File

@@ -43,6 +43,12 @@ export interface ParsedIngredient {
* @memberof ParsedIngredient
*/
note: string;
/**
*
* @type {string}
* @memberof ParsedIngredient
*/
originalText: string;
}
/**
@@ -53,6 +59,7 @@ export function instanceOfParsedIngredient(value: object): value is ParsedIngred
if (!('unit' in value) || value['unit'] === undefined) return false;
if (!('food' in value) || value['food'] === undefined) return false;
if (!('note' in value) || value['note'] === undefined) return false;
if (!('originalText' in value) || value['originalText'] === undefined) return false;
return true;
}
@@ -70,6 +77,7 @@ export function ParsedIngredientFromJSONTyped(json: any, ignoreDiscriminator: bo
'unit': json['unit'],
'food': json['food'],
'note': json['note'],
'originalText': json['original_text'],
};
}
@@ -83,6 +91,7 @@ export function ParsedIngredientToJSON(value?: ParsedIngredient | null): any {
'unit': value['unit'],
'food': value['food'],
'note': value['note'],
'original_text': value['originalText'],
};
}

View File

@@ -0,0 +1,130 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface PatchedAiProvider
*/
export interface PatchedAiProvider {
/**
*
* @type {number}
* @memberof PatchedAiProvider
*/
id?: number;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
name?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
description?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
apiKey?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
modelName?: string;
/**
*
* @type {string}
* @memberof PatchedAiProvider
*/
url?: string;
/**
*
* @type {boolean}
* @memberof PatchedAiProvider
*/
logCreditCost?: boolean;
/**
*
* @type {number}
* @memberof PatchedAiProvider
*/
space?: number;
/**
*
* @type {Date}
* @memberof PatchedAiProvider
*/
readonly createdAt?: Date;
/**
*
* @type {Date}
* @memberof PatchedAiProvider
*/
readonly updatedAt?: Date;
}
/**
* Check if a given object implements the PatchedAiProvider interface.
*/
export function instanceOfPatchedAiProvider(value: object): value is PatchedAiProvider {
return true;
}
export function PatchedAiProviderFromJSON(json: any): PatchedAiProvider {
return PatchedAiProviderFromJSONTyped(json, false);
}
export function PatchedAiProviderFromJSONTyped(json: any, ignoreDiscriminator: boolean): PatchedAiProvider {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'apiKey': json['api_key'] == null ? undefined : json['api_key'],
'modelName': json['model_name'] == null ? undefined : json['model_name'],
'url': json['url'] == null ? undefined : json['url'],
'logCreditCost': json['log_credit_cost'] == null ? undefined : json['log_credit_cost'],
'space': json['space'] == null ? undefined : json['space'],
'createdAt': json['created_at'] == null ? undefined : (new Date(json['created_at'])),
'updatedAt': json['updated_at'] == null ? undefined : (new Date(json['updated_at'])),
};
}
export function PatchedAiProviderToJSON(value?: Omit<PatchedAiProvider, 'createdAt'|'updatedAt'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'description': value['description'],
'api_key': value['apiKey'],
'model_name': value['modelName'],
'url': value['url'],
'log_credit_cost': value['logCreditCost'],
'space': value['space'],
};
}

View File

@@ -31,6 +31,12 @@ import {
SpaceNavTextColorEnumFromJSONTyped,
SpaceNavTextColorEnumToJSON,
} from './SpaceNavTextColorEnum';
import type { AiProvider } from './AiProvider';
import {
AiProviderFromJSON,
AiProviderFromJSONTyped,
AiProviderToJSON,
} from './AiProvider';
import type { FoodInheritField } from './FoodInheritField';
import {
FoodInheritFieldFromJSON,
@@ -212,6 +218,36 @@ export interface PatchedSpace {
* @memberof PatchedSpace
*/
logoColorSvg?: UserFileView;
/**
*
* @type {number}
* @memberof PatchedSpace
*/
aiCreditsMonthly?: number;
/**
*
* @type {number}
* @memberof PatchedSpace
*/
aiCreditsBalance?: number;
/**
*
* @type {number}
* @memberof PatchedSpace
*/
readonly aiMonthlyCreditsUsed?: number;
/**
*
* @type {boolean}
* @memberof PatchedSpace
*/
aiEnabled?: boolean;
/**
*
* @type {AiProvider}
* @memberof PatchedSpace
*/
aiDefaultProvider?: AiProvider;
}
/**
@@ -258,10 +294,15 @@ export function PatchedSpaceFromJSONTyped(json: any, ignoreDiscriminator: boolea
'logoColor192': json['logo_color_192'] == null ? undefined : UserFileViewFromJSON(json['logo_color_192']),
'logoColor512': json['logo_color_512'] == null ? undefined : UserFileViewFromJSON(json['logo_color_512']),
'logoColorSvg': json['logo_color_svg'] == null ? undefined : UserFileViewFromJSON(json['logo_color_svg']),
'aiCreditsMonthly': json['ai_credits_monthly'] == null ? undefined : json['ai_credits_monthly'],
'aiCreditsBalance': json['ai_credits_balance'] == null ? undefined : json['ai_credits_balance'],
'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']),
};
}
export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'> | null): any {
export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'> | null): any {
if (value == null) {
return value;
}
@@ -284,6 +325,10 @@ export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'creat
'logo_color_192': UserFileViewToJSON(value['logoColor192']),
'logo_color_512': UserFileViewToJSON(value['logoColor512']),
'logo_color_svg': UserFileViewToJSON(value['logoColorSvg']),
'ai_credits_monthly': value['aiCreditsMonthly'],
'ai_credits_balance': value['aiCreditsBalance'],
'ai_enabled': value['aiEnabled'],
'ai_default_provider': AiProviderToJSON(value['aiDefaultProvider']),
};
}

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