Merge branch 'develop' into HomeAssistantConnector

This commit is contained in:
Mikhail Epifanov 2024-01-29 09:57:10 +01:00 committed by GitHub
commit 8b5b063da6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 652 additions and 377 deletions

View File

@ -21,7 +21,7 @@ jobs:
suffix: "" suffix: ""
continue-on-error: false continue-on-error: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Get version number - name: Get version number
id: get_version id: get_version
@ -43,7 +43,7 @@ jobs:
path: ./recipes/plugins/open_data_plugin path: ./recipes/plugins/open_data_plugin
# Build Vue frontend # Build Vue frontend
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: '18'
cache: yarn cache: yarn

View File

@ -21,7 +21,7 @@ jobs:
suffix: "" suffix: ""
continue-on-error: false continue-on-error: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Get version number - name: Get version number
id: get_version id: get_version
@ -35,7 +35,7 @@ jobs:
fi fi
# Build Vue frontend # Build Vue frontend
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: '18'
cache: yarn cache: yarn

View File

@ -12,7 +12,7 @@ jobs:
python-version: ['3.10'] python-version: ['3.10']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python 3.10 - name: Set up Python 3.10
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@ -25,7 +25,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages # Override language selection by uncommenting this and choosing your languages
with: with:
languages: python, javascript languages: python, javascript
@ -47,6 +47,6 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3
with: with:
languages: javascript, python languages: javascript, python

View File

@ -9,8 +9,8 @@ jobs:
if: github.repository_owner == 'TandoorRecipes' if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
- run: pip install mkdocs-material mkdocs-include-markdown-plugin - run: pip install mkdocs-material mkdocs-include-markdown-plugin

View File

@ -96,7 +96,7 @@ Share some information on how you use Tandoor to help me improve the application
Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with a Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with a
[common clause](https://commonsclause.com/) selling exception. See [LICENSE.md](https://github.com/vabene1111/recipes/blob/develop/LICENSE.md) for details. [common clause](https://commonsclause.com/) selling exception. See [LICENSE.md](https://github.com/vabene1111/recipes/blob/develop/LICENSE.md) for details.
> NOTE: There appears to be a whole range of legal issues with licensing anything else then the standard completely open licenses. > NOTE: There appears to be a whole range of legal issues with licensing anything other than the standard completely open licenses.
> I am in the process of getting some professional legal advice to sort out these issues. > I am in the process of getting some professional legal advice to sort out these issues.
> Please also see [Issue 238](https://github.com/vabene1111/recipes/issues/238) for some discussion and **reasoning** regarding the topic. > Please also see [Issue 238](https://github.com/vabene1111/recipes/issues/238) for some discussion and **reasoning** regarding the topic.

View File

@ -76,4 +76,4 @@ echo "Done"
chmod -R 755 /opt/recipes/mediafiles chmod -R 755 /opt/recipes/mediafiles
exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi exec gunicorn -b "[::]:$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi

View File

@ -495,7 +495,7 @@ class SpacePreferenceForm(forms.ModelForm):
class Meta: class Meta:
model = Space model = Space
fields = ('food_inherit', 'reset_food_inherit', 'use_plural') fields = ('food_inherit', 'reset_food_inherit',)
help_texts = { help_texts = {
'food_inherit': _('Fields on food that should be inherited by default.'), 'food_inherit': _('Fields on food that should be inherited by default.'),

View File

@ -14,12 +14,14 @@ class IngredientObject(object):
unit = "" unit = ""
food = "" food = ""
note = "" note = ""
numeric_amount = 0
def __init__(self, ingredient): def __init__(self, ingredient):
if ingredient.no_amount: if ingredient.no_amount:
self.amount = "" self.amount = ""
else: else:
self.amount = f"<scalable-number v-bind:number='{bleach.clean(str(ingredient.amount))}' v-bind:factor='ingredient_factor'></scalable-number>" self.amount = f"<scalable-number v-bind:number='{bleach.clean(str(ingredient.amount))}' v-bind:factor='ingredient_factor'></scalable-number>"
self.numeric_amount = float(ingredient.amount)
if ingredient.unit: if ingredient.unit:
if ingredient.unit.plural_name in (None, ""): if ingredient.unit.plural_name in (None, ""):
self.unit = bleach.clean(str(ingredient.unit)) self.unit = bleach.clean(str(ingredient.unit))
@ -83,9 +85,12 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
for i in step.ingredients.all(): for i in step.ingredients.all():
ingredients.append(IngredientObject(i)) ingredients.append(IngredientObject(i))
def scale(number):
return f"<scalable-number v-bind:number='{bleach.clean(str(number))}' v-bind:factor='ingredient_factor'></scalable-number>"
try: try:
template = Template(instructions) template = Template(instructions)
instructions = template.render(ingredients=ingredients) instructions = template.render(ingredients=ingredients, scale=scale)
except TemplateSyntaxError: except TemplateSyntaxError:
return _('Could not parse template code.') + ' Error: Template Syntax broken' return _('Could not parse template code.') + ' Error: Template Syntax broken'
except UndefinedError: except UndefinedError:

View File

@ -4,11 +4,6 @@ from django.db import migrations, models
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
def fix_fdc_ids(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
Food = apps.get_model('cookbook', 'Food')
Food.objects.exclude(fdc_id__regex=r'^\d+$').exclude(fdc_id=None).update(fdc_id=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -17,7 +12,6 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(fix_fdc_ids),
migrations.AddField( migrations.AddField(
model_name='propertytype', model_name='propertytype',
name='fdc_id', name='fdc_id',

View File

@ -1,15 +1,23 @@
# Generated by Django 4.2.7 on 2023-11-29 19:44 # Generated by Django 4.2.7 on 2023-11-29 19:44
from django.db import migrations, models from django.db import migrations, models
from django_scopes import scopes_disabled
def fix_fdc_ids(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
Food = apps.get_model('cookbook', 'Food')
Food.objects.exclude(fdc_id__regex=r'^\d+$').exclude(fdc_id=None).update(fdc_id=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cookbook', '0204_propertytype_fdc_id'), ('cookbook', '0204_propertytype_fdc_id'),
] ]
operations = [ operations = [
migrations.RunPython(fix_fdc_ids),
migrations.AlterField( migrations.AlterField(
model_name='food', model_name='food',
name='fdc_id', name='fdc_id',

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-28 07:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0208_space_app_name_userpreference_max_owned_spaces'),
]
operations = [
migrations.RemoveField(
model_name='space',
name='use_plural',
),
]

View File

@ -303,7 +303,6 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
max_recipes = models.IntegerField(default=0) max_recipes = models.IntegerField(default=0)
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.')) max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
max_users = models.IntegerField(default=0) max_users = models.IntegerField(default=0)
use_plural = models.BooleanField(default=True)
allow_sharing = models.BooleanField(default=True) allow_sharing = models.BooleanField(default=True)
no_sharing_limit = models.BooleanField(default=False) no_sharing_limit = models.BooleanField(default=False)
demo = models.BooleanField(default=False) demo = models.BooleanField(default=False)

View File

@ -311,7 +311,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
fields = ( fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', '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', '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', 'use_plural', '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',)
read_only_fields = ( read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',

View File

@ -10,7 +10,7 @@
<title>{% block title %} <title>{% block title %}
{% endblock %}</title> {% endblock %}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover">
<meta name="robots" content="noindex,nofollow"/> <meta name="robots" content="noindex,nofollow"/>
<link rel="icon" href="{{ theme_values.logo_color_svg }}"> <link rel="icon" href="{{ theme_values.logo_color_svg }}">
@ -485,6 +485,14 @@
overflow-x: hidden; overflow-x: hidden;
} }
} }
#id_base_container {
padding-bottom: env(safe-area-inset-bottom);
}
.fixed-bottom {
padding-bottom: max(0.5rem, calc(env(safe-area-inset-bottom) - 0.5rem)) !important;
}
</style> </style>
</body> </body>

View File

@ -3,7 +3,7 @@ from django.templatetags.static import static
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from cookbook.models import UserPreference, UserFile, Space from cookbook.models import UserPreference, UserFile, Space
from recipes.settings import STICKY_NAV_PREF_DEFAULT, UNAUTHENTICATED_THEME_FROM_SPACE from recipes.settings import STICKY_NAV_PREF_DEFAULT, UNAUTHENTICATED_THEME_FROM_SPACE, FORCE_THEME_FROM_SPACE
register = template.Library() register = template.Library()
@ -15,11 +15,14 @@ def theme_values(request):
def get_theming_values(request): def get_theming_values(request):
space = None space = None
if getattr(request,'space',None): if getattr(request, 'space', None):
space = request.space space = request.space
if not request.user.is_authenticated and UNAUTHENTICATED_THEME_FROM_SPACE > 0: if not request.user.is_authenticated and UNAUTHENTICATED_THEME_FROM_SPACE > 0 and FORCE_THEME_FROM_SPACE == 0:
with scopes_disabled(): with scopes_disabled():
space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first()
if FORCE_THEME_FROM_SPACE:
with scopes_disabled():
space = Space.objects.filter(id=FORCE_THEME_FROM_SPACE).first()
themes = { themes = {
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css', UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',

View File

@ -1728,10 +1728,10 @@ def get_plan_ical(request, from_date, to_date):
).filter(space=request.user.userspace_set.filter(active=1).first().space).distinct().all() ).filter(space=request.user.userspace_set.filter(active=1).first().space).distinct().all()
if from_date is not None: if from_date is not None:
queryset = queryset.filter(date__gte=from_date) queryset = queryset.filter(from_date__gte=from_date)
if to_date is not None: if to_date is not None:
queryset = queryset.filter(date__lte=to_date) queryset = queryset.filter(to_date__lte=to_date)
cal = Calendar() cal = Calendar()

View File

@ -10,9 +10,12 @@ Tandoor can be installed as a progressive web app (PWA) on mobile and desktop de
#### Safari (iPhone/iPad) #### Safari (iPhone/iPad)
Open Tandoor, click Safari's share button, select `Add to Home Screen` Open Tandoor, click Safari's share button, select `Add to Home Screen`
### Chrome/Chromium #### Chrome/Chromium
Open Tandoor, click the `add Tandoor to the home screen` message that pops up at the bottom of the screen Open Tandoor, click the `add Tandoor to the home screen` message that pops up at the bottom of the screen
#### Firefox for Android
Open Tandoor, click on the `⋮` menu icon, then on `Install`
### Desktop browsers ### Desktop browsers
#### Google Chrome #### Google Chrome
@ -124,13 +127,13 @@ to your dream setup.
## How can I upgrade postgres (major versions)? ## How can I upgrade postgres (major versions)?
Postgres requires manual intervention when updating from one major version to another. The steps are roughly Postgres requires manual intervention when updating from one major version to another. The steps are roughly
1. use `pg_dumpall` to dump your database into SQL (for Docker `docker-compose exec -T <database_container_name> pg_dumpall -U <postgres_user_name> -f /path/to/dump.sql`) 1. use `pg_dumpall` to dump your database into SQL (for Docker `docker-compose exec -T <postgres_container_name> pg_dumpall -U <postgres_user_name> -f /path/to/dump.sql`)
2. stop the DB / down the container 2. stop the DB / down the container
3. move your postgres directory in order to keep it as a backup (e.g. `mv postgres postgres_old`) 3. move your postgres directory in order to keep it as a backup (e.g. `mv postgres postgres_old`)
4. update postgres to the new major version (for Docker just change the version number and pull) 4. update postgres to the new major version (for Docker just change the version number and pull)
5. start the db / up the container (do not start tandoor as it will automatically perform the database migrations which will conflict with loading the dump) 5. start the db / up the container (do not start tandoor as it will automatically perform the database migrations which will conflict with loading the dump)
6. if not using docker, you might need to create the same postgres user you had in the old database 6. if not using docker, you might need to create the same postgres user you had in the old database
7. load the postgres dump (for Docker `'/usr/local/bin/docker-compose exec -T <database_container_name> psql -U <postgres_user_name> postgres < /path/to/dump.sql`) 7. load the postgres dump (for Docker `'/usr/local/bin/docker-compose exec -T <postgres_container_name> psql -U <postgres_user_name> <postgres_database_name> < /path/to/dump.sql`)
If anything fails, go back to the old postgres version and data directory and try again. If anything fails, go back to the old postgres version and data directory and try again.

View File

@ -50,7 +50,7 @@ In order to prevent denial of service attacks on the RegEx engine the number of
and the length of the inputs that are processed are limited. Those limits should never be reached and the length of the inputs that are processed are limited. Those limits should never be reached
during normal usage. during normal usage.
## Instructtion Replace, Title Replace, Food Replace & Unit Replace ## Instruction Replace, Title Replace, Food Replace & Unit Replace
These work just like the Description Replace automation. These work just like the Description Replace automation.
Instruction, Food and Unit Replace will run against every iteration of the object in a recipe during import. Instruction, Food and Unit Replace will run against every iteration of the object in a recipe during import.

View File

@ -1,5 +1,5 @@
!!! info "WIP" !!! info "WIP"
While being around for a while there are still a lot of features that i plan on adding to the shopping list. While being around for a while there are still a lot of features that I plan on adding to the shopping list.
You can see an overview of what is still planned on [this](https://github.com/vabene1111/recipes/issues/114) issue. You can see an overview of what is still planned on [this](https://github.com/vabene1111/recipes/issues/114) issue.
@ -41,4 +41,4 @@ There are a few more features worth pointing out
1. You can export recipes for use in other applications (Google Keep, etc.) by using the export button 1. You can export recipes for use in other applications (Google Keep, etc.) by using the export button
2. In the export popup you can define a prefix to be put before each row in case an external app requires that 2. In the export popup you can define a prefix to be put before each row in case an external app requires that
3. Marking a shopping list as finished will hide it from the shopping list page 3. Marking a shopping list as finished will hide it from the shopping list page

View File

@ -204,6 +204,22 @@ server {
} }
``` ```
Tandoor does not support directly serving of images, as explained in the [Nginx vs Gunicorn"](#nginx-vs-gunicorn) section. If you are already using nginx to serve as a reverse proxy, you can configure it to serve images as well.
Add the following directly after the `location /` context:
```
location /media/ {
root /media/;
index index.html index.htm;
}
```
Make sure you also update your `docker-compose.yml` file to mount the `mediafiles` directory. If you are using the [Plain](#plain) deployment, you do not need to make any changes. If you are using nginx to act as a reverse proxy for other apps, it may not be optimal to have `mediafiles` mounted to `/media`. In that case, adjust the directory declarations as needed, utilizing nginx's [`alias`](https://nginx.org/en/docs/http/ngx_http_core_module.html#alias) if needed.
!!!note
Use `alias` if your mount point directory is not the same as the URL request path. Tandoor media files are requested from `$http_host/media/recipes/xxx.jpg`. This means if you are mounting to a directory that does **NOT** end in `./media`, you will need to use `alias`.
!!!note !!!note
Don't forget to [download and configure](#docker-compose) your ```.env``` file! Don't forget to [download and configure](#docker-compose) your ```.env``` file!

View File

@ -59,6 +59,155 @@ I used two paths `<sub path>` and `<www path>` for simplicity. In my case I have
I left out the TLS config in this example for simplicity. I left out the TLS config in this example for simplicity.
## Docker + Apache + Sub-Path
The following could prove to be useful if you are not using Traefik, but instead run Apache as your reverse proxy to route all calls for a shared (sub)domain to a sub path, e.g. https://mydomain.tld/tandoor
As a side note, I am using [Blocky](https://0xerr0r.github.io/blocky/) + [Consul](https://hub.docker.com/r/hashicorp/consul) + [Registrator](https://hub.docker.com/r/gliderlabs/registrator) as a DNS solution.
The relevant Apache config:
```
<Location /tandoor>
# in case you want to restrict access to specific IP addresses:
Require local
Require forward-dns [myhomedomain.useyourdomain.com]
Require ip [anylocalorremoteipyouwanttowhitelist]
# The following assumes that tandoor.service.consul.local resolves to the IP address of the Docker container.
ProxyPass http://tandoor.service.consul.local:8080/tandoor
ProxyPassReverse http://tandoor.service.consul.local:8080/tandoor
RequestHeader add X-Script-Name /tandoor
RequestHeader set X-Forwarded-Proto "https"
ProxyPreserveHost On
</Location>
<Location /tandoor/static>
Require local
Require forward-dns [myhomedomain.useyourdomain.com]
Require ip [anylocalorremoteipyouwanttowhitelist]
ProxyPass http://tandoor.service.consul.local:8080/tandoor/tandoor/static
ProxyPassReverse http://tandoor.service.consul.local:8080/tandoor/static
RequestHeader add X-Script-Name /tandoor
RequestHeader set X-Forwarded-Proto "https"
ProxyPreserveHost On
</Location>
```
and the relevant section from the docker-compose.yml:
```
tandoor:
restart: always
container_name: tandoor
image: vabene1111/recipes
environment:
- SCRIPT_NAME=/tandoor
- JS_REVERSE_SCRIPT_PREFIX=/tandoor
- STATIC_URL=/tandoor/static/
- MEDIA_URL=/tandoor/media/
- GUNICORN_MEDIA=0
- SECRET_KEY=${YOUR_TANDOOR_SECRET_KEY}
- POSTGRES_HOST=postgres.service.consul.local
- POSTGRES_PORT=${POSTGRES_PORT}
- POSTGRES_USER=${YOUR_TANDOOR_POSTGRES_USER}
- POSTGRES_PASSWORD=${YOUR_TANDOOR_POSTGRES_PASSWORD}
- POSTGRES_DB=${YOUR_TANDOOR_POSTGRES_DB}
labels:
# The following is relevant only if you are using Registrator and Consul
- "SERVICE_NAME=tandoor"
volumes:
- ${YOUR_DOCKER_VOLUME_BASE_DIR}/tandoor/static:/opt/recipes/staticfiles:rw
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes- vs-bind-mounts
- tandoor_nginx_config:/opt/recipes/nginx/conf.d
- ${YOUR_DOCKER_VOLUME_BASE_DIR}}/tandoor/media:/opt/recipes/mediafiles:rw
depends_on:
# You will have to set up postgres accordingly
- postgres
```
The relevant docker-compose.yml for Registrator, Consul, and Blocky, and Autoheal:
```
consul:
image: hashicorp/consul
container_name: consul
command: >
agent -server
-domain consul.local
-advertise=${YOUR_DOCKER_HOST_IP_ON_THE_LAN}
-client=0.0.0.0
-encrypt=${SOME_SECRET_KEY}
-datacenter=${YOUR_DC_NAME}
-bootstrap-expect=1
-ui
-log-level=info
environment:
- "CONSUL_LOCAL_CONFIG={\"skip_leave_on_interrupt\": true, \"dns_config\": { \"service_ttl\": { \"*\": \"0s\" } } }"
network_mode: "host"
restart: always
registrator:
image: gliderlabs/registrator:latest
container_name: registrator
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
command: >
-internal
-cleanup=true
-deregister="always"
-resync=60
consul://host.docker.internal:8500
restart: always
blocky:
image: spx01/blocky
container_name: blocky
restart: unless-stopped
healthcheck:
interval: 30s
timeout: 5s
start_period: 1m
labels:
# The following is only relevant if you use autoheal
autoheal: true
# Optional the instance hostname for logging purpose
hostname: blocky
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "1153:53/tcp"
- "1153:53/udp"
- 4000:4000
environment:
- TZ=YOUR_TIMEZONE # Optional to synchronize the log timestamp with host
volumes:
# Optional to synchronize the log timestamp with host
- /etc/localtime:/etc/localtime:ro
# config file
- ${YOUR_DOCKER_VOLUME_BASE_DIR}/blocky/config.yml:/app/config.yml
networks:
# in case you want to bind Blocky to an IP address
your-docker-network-name:
ipv4_address: 'some-ip-address-in-the-docker-network-subnet'
autoheal:
image: willfarrell/autoheal
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
environment:
- AUTOHEAL_CONTAINER_LABEL=autoheal
restart: always
container_name: autoheal
```
as well as a snippet of the Blocky configuration:
```
conditional:
fallbackUpstream: false
mapping:
consul.local: tcp+udp:host.docker.internal:8600
```
## WSL ## WSL
If you want to install Tandoor on the Windows Subsystem for Linux you can find a detailed post here: <https://github.com/TandoorRecipes/recipes/issues/1733>. If you want to install Tandoor on the Windows Subsystem for Linux you can find a detailed post here: <https://github.com/TandoorRecipes/recipes/issues/1733>.

View File

@ -45,7 +45,7 @@ To restore:
cat pgdump.sql | sudo docker exec -i docker_db_recipes_1 psql postgres -U djangouser cat pgdump.sql | sudo docker exec -i docker_db_recipes_1 psql postgres -U djangouser
``` ```
This connects to the postgres table instead of the actual dgangodb table, as the import function needs to delete the table, which can't be dropped off you're connected to it. This connects to the postgres table instead of the actual djangodb table, as the import function needs to delete the table, which can't be dropped off you're connected to it.
## Backup using export and import ## Backup using export and import
You can now export recipes from Tandoor using the export function. This method requires a working web interface. You can now export recipes from Tandoor using the export function. This method requires a working web interface.

View File

@ -570,6 +570,15 @@ With this setting you can specify the ID of a space of which the appearance sett
UNAUTHENTICATED_THEME_FROM_SPACE= UNAUTHENTICATED_THEME_FROM_SPACE=
``` ```
#### Force Theme
> default `0` - options `1-X` (space ID)
Similar to the Default theme but forces the theme upon all users (authenticated/unauthenticated) and all spaces
```
FORCE_THEME_FROM_SPACE=
```
### Rate Limiting / Performance ### Rate Limiting / Performance
#### Shopping auto sync #### Shopping auto sync

View File

@ -81,7 +81,7 @@ sudo mv -R ~/.docker/compose/postgres ~/.docker/compose/postgres.old
``` ```
8. Install postgres extensions 8. Install postgres extensions
``` bash ``` bash
docker exec -it {{database_container}} psql docker exec -it {{database_container}} psql postgres -U {{djangouser}}
``` ```
then then
``` psql ``` psql

View File

@ -1,5 +1,6 @@
server { server {
listen 80; listen 80;
listen [::]:80 ipv6only=on;
server_name localhost; server_name localhost;
client_max_body_size 128M; client_max_body_size 128M;

View File

@ -59,6 +59,7 @@ KJ_PREF_DEFAULT = bool(int(os.getenv('KJ_PREF_DEFAULT', False)))
STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True))) STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True)))
MAX_OWNED_SPACES_PREF_DEFAULT = int(os.getenv('MAX_OWNED_SPACES_PREF_DEFAULT', 100)) MAX_OWNED_SPACES_PREF_DEFAULT = int(os.getenv('MAX_OWNED_SPACES_PREF_DEFAULT', 100))
UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPACE', 0)) UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPACE', 0))
FORCE_THEME_FROM_SPACE = int(os.getenv('FORCE_THEME_FROM_SPACE', 0))
# minimum interval that users can set for automatic sync of shopping lists # minimum interval that users can set for automatic sync of shopping lists
SHOPPING_MIN_AUTOSYNC_INTERVAL = int( SHOPPING_MIN_AUTOSYNC_INTERVAL = int(

View File

@ -1,7 +1,7 @@
Django==4.2.7 Django==4.2.7
cryptography===41.0.6 cryptography===41.0.7
django-annoying==0.10.6 django-annoying==0.10.6
django-autocomplete-light==3.9.4 django-autocomplete-light==3.9.7
django-cleanup==8.0.0 django-cleanup==8.0.0
django-crispy-forms==2.0 django-crispy-forms==2.0
crispy-bootstrap4==2022.1 crispy-bootstrap4==2022.1
@ -14,7 +14,7 @@ bleach==6.0.0
gunicorn==20.1.0 gunicorn==20.1.0
lxml==4.9.3 lxml==4.9.3
Markdown==3.5.1 Markdown==3.5.1
Pillow==10.0.1 Pillow==10.2.0
psycopg2-binary==2.9.5 psycopg2-binary==2.9.5
python-dotenv==1.0.0 python-dotenv==1.0.0
requests==2.31.0 requests==2.31.0
@ -27,7 +27,7 @@ uritemplate==4.1.1
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
microdata==0.8.0 microdata==0.8.0
mock==5.1.0 mock==5.1.0
Jinja2==3.1.2 Jinja2==3.1.3
django-webpack-loader==1.8.1 django-webpack-loader==1.8.1
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82 git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
django-allauth==0.58.1 django-allauth==0.58.1
@ -38,13 +38,13 @@ pytest-asyncio==0.23.3
pytest-django==4.6.0 pytest-django==4.6.0
django-treebeard==4.7 django-treebeard==4.7
django-cors-headers==4.2.0 django-cors-headers==4.2.0
django-storages==1.13.2 django-storages==1.14.2
boto3==1.28.75 boto3==1.28.75
django-prometheus==2.2.0 django-prometheus==2.2.0
django-hCaptcha==0.2.0 django-hCaptcha==0.2.0
python-ldap==3.4.3 python-ldap==3.4.3
django-auth-ldap==4.4.0 django-auth-ldap==4.4.0
pytest-factoryboy==2.5.1 pytest-factoryboy==2.6.0
pyppeteer==1.0.2 pyppeteer==1.0.2
validators==0.20.0 validators==0.20.0
pytube==15.0.0 pytube==15.0.0

View File

@ -45,8 +45,8 @@
"vue-template-compiler": "2.7.14", "vue-template-compiler": "2.7.14",
"vue2-touch-events": "^3.2.2", "vue2-touch-events": "^3.2.2",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"workbox-webpack-plugin": "^6.5.4", "workbox-webpack-plugin": "^7.0.0",
"workbox-window": "^6.5.4" "workbox-window": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@kazupon/vue-i18n-loader": "^0.5.0", "@kazupon/vue-i18n-loader": "^0.5.0",
@ -65,11 +65,11 @@
"typescript": "~5.1.6", "typescript": "~5.1.6",
"vue-cli-plugin-i18n": "^2.3.2", "vue-cli-plugin-i18n": "^2.3.2",
"webpack-bundle-tracker": "1.8.1", "webpack-bundle-tracker": "1.8.1",
"workbox-background-sync": "^6.5.4", "workbox-background-sync": "^7.0.0",
"workbox-expiration": "^6.5.4", "workbox-expiration": "^6.5.4",
"workbox-navigation-preload": "^6.5.4", "workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^6.5.4", "workbox-precaching": "^6.5.4",
"workbox-routing": "^6.5.4", "workbox-routing": "^7.0.0",
"workbox-strategies": "^6.2.4" "workbox-strategies": "^6.2.4"
}, },
"eslintConfig": { "eslintConfig": {

View File

@ -62,7 +62,7 @@
</div> </div>
</div> </div>
<b-modal id="ingredient_edit_modal" :title="$t('Edit')" @hidden="destroyIngredientEditModal"> <b-modal id="ingredient_edit_modal" ref="ingredient_edit_modal" :title="$t('Edit')" @hidden="destroyIngredientEditModal">
<div v-if="current_edit_ingredient !== null"> <div v-if="current_edit_ingredient !== null">
<b-form-group v-bind:label="$t('Original_Text')" class="mb-3"> <b-form-group v-bind:label="$t('Original_Text')" class="mb-3">
<b-form-input v-model="current_edit_ingredient.original_text" type="text" disabled></b-form-input> <b-form-input v-model="current_edit_ingredient.original_text" type="text" disabled></b-form-input>
@ -88,8 +88,8 @@
<div class="row w-100"> <div class="row w-100">
<div class="col-auto justify-content-end"> <div class="col-auto justify-content-end">
<b-button class="mx-1" >{{ $t('Ok') }}</b-button> <b-button class="mx-1" @click="destroyIngredientEditModal()">{{ $t('Ok') }}</b-button>
<b-button class="mx-1" @click="removeIngredient(current_edit_step,current_edit_ingredient);" variant="danger">{{ $t('Delete') }}</b-button> <b-button class="mx-1" @click="removeIngredient(current_edit_step,current_edit_ingredient);destroyIngredientEditModal()" variant="danger">{{ $t('Delete') }}</b-button>
</div> </div>
</div> </div>
</template> </template>

View File

@ -80,80 +80,73 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row d-block d-lg-none"> <div class="d-block d-lg-none">
<div class="row"> <div class="row">
<div class="col"> <div class="col-12">
<div class=""> <div class="col-12 d-flex justify-content-center mt-2">
<div> <b-button-toolbar key-nav aria-label="Toolbar with button groups">
<div class="col-12"> <b-button-group class="mx-1">
<div class="col-12 d-flex justify-content-center mt-2"> <b-button v-html="'<<'" class="p-2 pr-3 pl-3"
<b-button-toolbar key-nav aria-label="Toolbar with button groups"> @click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
<b-button-group class="mx-1"> </b-button-group>
<b-button v-html="'<<'" class="p-2 pr-3 pl-3" <b-button-group class="mx-1">
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button> <b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
</b-button-group> class="fas fa-home"></i></b-button>
<b-button-group class="mx-1"> <b-form-datepicker right button-only button-variant="secondary" @context="datePickerChanged"></b-form-datepicker>
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i </b-button-group>
class="fas fa-home"></i></b-button> <b-button-group class="mx-1">
<b-form-datepicker right button-only button-variant="secondary" @context="datePickerChanged"></b-form-datepicker> <b-button v-html="'>>'" class="p-2 pr-3 pl-3"
</b-button-group> @click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
<b-button-group class="mx-1"> </b-button-group>
<b-button v-html="'>>'" class="p-2 pr-3 pl-3" </b-button-toolbar>
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button> </div>
</b-button-group> </div>
</b-button-toolbar> <div class="col-12 mt-2" style="padding-bottom: 60px">
<div v-for="day in mobileSimpleGrid" v-bind:key="day.day">
<b-list-group>
<b-list-group-item>
<div class="d-flex flex-row align-middle">
<h6 class="mb-0 mt-1 align-middle">{{ day.date_label }}</h6>
<div class="flex-grow-1 text-right">
<b-button class="btn-sm btn-outline-primary" @click="showMealPlanEditModal(null, day.create_default_date)"><i
class="fa fa-plus"></i></b-button>
</div>
</div> </div>
</div>
<div class="col-12 mt-2" style="padding-bottom: 60px">
<div v-for="day in mobileSimpleGrid" v-bind:key="day.day">
<b-list-group>
<b-list-group-item>
<div class="d-flex flex-row align-middle">
<h6 class="mb-0 mt-1 align-middle">{{ day.date_label }}</h6>
<div class="flex-grow-1 text-right"> </b-list-group-item>
<b-button class="btn-sm btn-outline-primary" @click="showMealPlanEditModal(null, day.create_default_date)"><i <b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id">
class="fa fa-plus"></i></b-button> <div class="d-flex flex-row align-items-center">
</div> <div>
</div> <b-img style="height: 50px; width: 50px; object-fit: cover"
:src="plan.entry.recipe.image" rounded="circle" v-if="plan.entry.recipe?.image"></b-img>
</b-list-group-item> <b-img style="height: 50px; width: 50px; object-fit: cover"
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id"> :src="image_placeholder" rounded="circle" v-else></b-img>
<div class="d-flex flex-row align-items-center"> </div>
<div> <div class="flex-grow-1 ml-2"
<b-img style="height: 50px; width: 50px; object-fit: cover" style="text-overflow: ellipsis; overflow-wrap: anywhere;">
:src="plan.entry.recipe.image" rounded="circle" v-if="plan.entry.recipe?.image"></b-img>
<b-img style="height: 50px; width: 50px; object-fit: cover"
:src="image_placeholder" rounded="circle" v-else></b-img>
</div>
<div class="flex-grow-1 ml-2"
style="text-overflow: ellipsis; overflow-wrap: anywhere;">
<span class="two-row-text"> <span class="two-row-text">
<a :href="resolveDjangoUrl('view_recipe', plan.entry.recipe.id)" v-if="plan.entry.recipe">{{ plan.entry.recipe.name }}</a> <a :href="resolveDjangoUrl('view_recipe', plan.entry.recipe.id)" v-if="plan.entry.recipe">{{ plan.entry.recipe.name }}</a>
<span v-else>{{ plan.entry.title }}</span> <br/> <span v-else>{{ plan.entry.title }}</span> <br/>
</span> </span>
<span v-if="plan.entry.note" class="two-row-text"> <span v-if="plan.entry.note" class="two-row-text">
<small>{{ plan.entry.note }}</small> <br/> <small>{{ plan.entry.note }}</small> <br/>
</span> </span>
<small class="text-muted"> <small class="text-muted">
<span v-if="plan.entry.shopping" class="font-light"><i class="fas fa-shopping-cart fa-xs "/></span> <span v-if="plan.entry.shopping" class="font-light"><i class="fas fa-shopping-cart fa-xs "/></span>
{{ plan.entry.meal_type_name }} {{ plan.entry.meal_type_name }}
<span v-if="plan.entry.recipe"> <span v-if="plan.entry.recipe">
- <i class="fa fa-clock"></i> {{ plan.entry.recipe.working_time + plan.entry.recipe.waiting_time }} {{ $t('min') }} - <i class="fa fa-clock"></i> {{ plan.entry.recipe.working_time + plan.entry.recipe.waiting_time }} {{ $t('min') }}
</span> </span>
</small> </small>
</div> </div>
<div class="hover-button"> <div class="hover-button">
<a class="pr-2" @click.stop="openContextMenu($event, {originalItem: plan})"><i class="fas fa-ellipsis-v"></i></a> <a class="pr-2" @click.stop="openContextMenu($event, {originalItem: plan})"><i class="fas fa-ellipsis-v"></i></a>
</div> </div>
</div>
</b-list-group-item>
</b-list-group>
</div> </div>
</div> </b-list-group-item>
</div> </b-list-group>
</div> </div>
</div> </div>
</div> </div>
@ -364,7 +357,7 @@ export default {
}, },
mobileSimpleGrid() { mobileSimpleGrid() {
let grid = []; let grid = [];
let currentDate = moment(); let currentDate = moment(this.showDate);
for (let x = 0; x < 7; x++) { for (let x = 0; x < 7; x++) {
let moment_date = currentDate.clone().add(x, "d"); let moment_date = currentDate.clone().add(x, "d");
grid.push({ grid.push({
@ -483,7 +476,7 @@ export default {
this.setShowDate(ctx.selectedDate) this.setShowDate(ctx.selectedDate)
}, },
setShowDate(d) { setShowDate(d) {
this.showDate = d this.showDate = d ?? new Date();
}, },
createEntryClick(data) { createEntryClick(data) {
this.mealplan_default_date = moment(data).format("YYYY-MM-DD") this.mealplan_default_date = moment(data).format("YYYY-MM-DD")

View File

@ -1321,3 +1321,9 @@ textarea:not(.form-control) {
border: 0 !important; border: 0 !important;
} }
</style> </style>
<style scoped>
.row.fixed-bottom {
margin: 0;
}
</style>

View File

@ -108,4 +108,4 @@ export default {
-o-transform: translate(-50%, 0); -o-transform: translate(-50%, 0);
transform: translate(-50%, 0); transform: translate(-50%, 0);
} }
</style> </style>

View File

@ -231,7 +231,7 @@ export default {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1; line-height: 1;
background-color: transparent; background-color: transparent;
border: 1px solid rgba(46, 46, 46, 0.5); border: 1px solid rgba(46, 46, 46, 0.1);
border-radius: 0.1875rem; border-radius: 0.1875rem;
z-index: 1001; z-index: 1001;
} }

View File

@ -20,7 +20,7 @@
<cookbook-toc :recipes="recipes" v-if="current_page === 1" v-on:switchRecipe="switchRecipe($event)"></cookbook-toc> <cookbook-toc :recipes="recipes" v-if="current_page === 1" v-on:switchRecipe="switchRecipe($event)"></cookbook-toc>
</transition> </transition>
<transition name="flip" mode="out-in"> <transition name="flip" mode="out-in">
<recipe-card :recipe="display_recipes[1].recipe_content" v-if="current_page > 1 && display_recipes.length === 2" :key="display_recipes[1].recipe" :use_plural="use_plural"></recipe-card> <recipe-card :recipe="display_recipes[1].recipe_content" v-if="current_page > 1 && display_recipes.length === 2" :key="display_recipes[1].recipe" ></recipe-card>
</transition> </transition>
</div> </div>
<div class="col-md-1" @click="swipeLeft" style="cursor: pointer"></div> <div class="col-md-1" @click="swipeLeft" style="cursor: pointer"></div>
@ -57,10 +57,7 @@ export default {
} }
}, },
mounted(){ mounted(){
let apiClient = new ApiApiFactory()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
}, },
data() { data() {
return { return {
@ -69,7 +66,6 @@ export default {
bounce_left: false, bounce_left: false,
bounce_right: false, bounce_right: false,
cookbook_editing: false, cookbook_editing: false,
use_plural: false,
} }
}, },
methods: { methods: {

View File

@ -76,8 +76,7 @@
<div class="col-md-10 offset-md-2"> <div class="col-md-10 offset-md-2">
<generic-horizontal-card v-for="child in item[children]" <generic-horizontal-card v-for="child in item[children]"
v-bind:key="child.id" v-bind:key="child.id"
:item="child" :model="model" :item="child" :model="model"
:use_plural="use_plural"
@item-action="$emit('item-action', $event)"></generic-horizontal-card> @item-action="$emit('item-action', $event)"></generic-horizontal-card>
</div> </div>
</div> </div>
@ -160,7 +159,6 @@ export default {
recipe_count: { type: String, default: "numrecipe" }, recipe_count: { type: String, default: "numrecipe" },
recipes: { type: String, default: "recipes" }, recipes: { type: String, default: "recipes" },
show_context_menu: { type: Boolean, default: true }, show_context_menu: { type: Boolean, default: true },
use_plural: { type: Boolean, default: false},
}, },
data() { data() {
return { return {

View File

@ -7,7 +7,7 @@
</template> </template>
<template v-else> <template v-else>
<td class="d-print-none" v-if="detailed" @click="done"> <td class="d-print-none align-baseline py-2" v-if="detailed" @click="done">
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i> <i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i> <i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
</td> </td>
@ -40,9 +40,9 @@
</template> </template>
</template> </template>
</td> </td>
<td v-if="detailed"> <td v-if="detailed" class="align-baseline">
<template v-if="ingredient.note"> <template v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable py-0 px-2"> <span class="d-print-none touchable py-0 px-2" v-b-popover.hover="ingredient.note">
<i class="far fa-comment"></i> <i class="far fa-comment"></i>
</span> </span>
@ -106,9 +106,22 @@ export default {
<style scoped> <style scoped>
/* increase size of hover/touchable space without changing spacing */ /* increase size of hover/touchable space without changing spacing */
.touchable { .touchable {
/* padding-right: 2em; --target-increase: 2em;
padding-left: 2em; */ display: inline-flex;
margin-right: -1em;
margin-left: -1em;
} }
.touchable::after {
content: "";
display: inline-block;
width: var(--target-increase);
margin-right: calc(var(--target-increase) * -1);
}
.touchable::before {
content: "";
display: inline-block;
width: var(--target-increase);
margin-left: calc(var(--target-increase) * -1);
}
</style> </style>

View File

@ -24,7 +24,6 @@
<ingredient-component <ingredient-component
:ingredient="i" :ingredient="i"
:ingredient_factor="ingredient_factor" :ingredient_factor="ingredient_factor"
:use_plural="use_plural"
:key="i.id" :key="i.id"
:detailed="detailed" :detailed="detailed"
@checked-state-changed="$emit('checked-state-changed', $event)" @checked-state-changed="$emit('checked-state-changed', $event)"
@ -64,7 +63,6 @@ export default {
recipe: {type: Number}, recipe: {type: Number},
ingredient_factor: {type: Number, default: 1}, ingredient_factor: {type: Number, default: 1},
servings: {type: Number, default: 1}, servings: {type: Number, default: 1},
use_plural: {type: Boolean, default: false},
detailed: {type: Boolean, default: true}, detailed: {type: Boolean, default: true},
header: {type: Boolean, default: false}, header: {type: Boolean, default: false},
recipe_list: {type: Number, default: undefined}, recipe_list: {type: Number, default: undefined},

View File

@ -34,7 +34,6 @@
<div v-for="i in r.steps.flatMap((s) => s.ingredients)" v-bind:key="i.id"> <div v-for="i in r.steps.flatMap((s) => s.ingredients)" v-bind:key="i.id">
<table class="table table-sm mb-0"> <table class="table table-sm mb-0">
<ingredient-component <ingredient-component
:use_plural="true"
:key="i.id" :key="i.id"
:detailed="true" :detailed="true"
:ingredient="i" :ingredient="i"

View File

@ -2,8 +2,7 @@
<div> <div>
<template v-if="recipe && recipe.loading"> <template v-if="recipe && recipe.loading">
<b-card no-body v-hover style="height: 100%"> <b-card no-body v-hover style="height: 100%">
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="placeholder_image" <b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="placeholder_image" v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<b-card-body class="p-4"> <b-card-body class="p-4">
<h6> <h6>
@ -20,30 +19,32 @@
</template> </template>
<template v-else> <template v-else>
<b-card no-body v-hover v-if="recipe" style="height: 100%"> <b-card no-body v-hover v-if="recipe" style="height: 100%">
<a :href="recipe_link"> <a :href="recipe_link">
<div class="content"> <div class="content">
<div class="content-overlay" v-if="recipe.description !== null && recipe.description !== ''"></div> <div class="content-overlay" v-if="recipe.description !== null && recipe.description !== ''"></div>
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image" <b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image" v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<div class="content-details" > <div class="content-details">
<p class="content-text"> <p class="content-text">
{{ recipe.description }} {{ recipe.description }}
</p> </p>
</div> </div>
<div class="card-img-overlay d-flex flex-column justify-content-left float-left text-left pt-2" style="width:40%" <b-row class="card-img-overlay pt-1">
v-if="recipe.working_time !== 0 || recipe.waiting_time !== 0"> <b-col cols="6">
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0 && recipe.working_time !== undefined"> <div v-if="recipe.working_time !== 0 || recipe.waiting_time !== 0">
<i <b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0 && recipe.working_time !== undefined">
class="fa fa-clock"></i> {{ working_time }} <i class="fa fa-clock"></i> {{ working_time }}
</b-badge> </b-badge>
<b-badge pill variant="secondary" class="mt-1 font-weight-normal" <b-badge pill variant="secondary" class="mt-1 font-weight-normal" v-if="recipe.waiting_time !== 0 && recipe.waiting_time !== undefined">
v-if="recipe.waiting_time !== 0 && recipe.waiting_time !== undefined"> <i class="fa fa-pause"></i> {{ waiting_time }}
<i class="fa fa-pause"></i> {{ waiting_time }} </b-badge>
</b-badge> </div>
</div> </b-col>
<b-col cols="6" class="text-right">
<recipe-rating :recipe="recipe" :pill="true"></recipe-rating>
</b-col>
</b-row>
</div> </div>
</a> </a>
@ -51,18 +52,20 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<div class="flex-grow-1"> <div class="flex-grow-1">
<a :href="recipe_link" class="text-body font-weight-bold two-row-text"> <a :href="recipe_link" class="text-body font-weight-bold two-row-text">
<template v-if="recipe !== null">{{ recipe.name }}</template> <template v-if="recipe !== null">{{ recipe.name }}</template>
<template v-else>{{ meal_plan.title }}</template> <template v-else>{{ meal_plan.title }}</template>
</a> </a>
</div> </div>
<div class="justify-content-end"> <div class="justify-content-end">
<recipe-context-menu :recipe="recipe" class="justify-content-end float-right align-items-end pr-0" <recipe-context-menu
:disabled_options="context_disabled_options" :recipe="recipe"
v-if="recipe !== null && show_context_menu"></recipe-context-menu> class="justify-content-end float-right align-items-end pr-0"
:disabled_options="context_disabled_options"
v-if="recipe !== null && show_context_menu"
></recipe-context-menu>
</div> </div>
</div> </div>
<b-card-text style="text-overflow: ellipsis"> <b-card-text style="text-overflow: ellipsis">
<template v-if="recipe !== null"> <template v-if="recipe !== null">
<div v-if="show_detail"> <div v-if="show_detail">
@ -71,34 +74,29 @@
<p class="mt-1 mb-1"> <p class="mt-1 mb-1">
<last-cooked :recipe="recipe"></last-cooked> <last-cooked :recipe="recipe"></last-cooked>
<keywords-component :recipe="recipe" :limit="3" :enable_keyword_links="enable_keyword_links" <keywords-component
style="margin-top: 4px; position: relative; z-index: 3;"></keywords-component> :recipe="recipe"
:limit="3"
:enable_keyword_links="enable_keyword_links"
style="margin-top: 4px; position: relative; z-index: 3"
></keywords-component>
</p> </p>
<transition name="fade" mode="in-out"> <transition name="fade" mode="in-out">
<div class="row mt-3" v-if="show_detail"> <div class="row mt-3" v-if="show_detail">
<div class="col-md-12"> <div class="col-md-12">
<h6 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t("Ingredients") }} <h6 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t("Ingredients") }}</h6>
</h6>
<ingredients-card <ingredients-card :steps="recipe.steps" :header="false" :detailed="false" :servings="recipe.servings" />
:steps="recipe.steps"
:header="false"
:detailed="false"
:servings="recipe.servings"/>
</div> </div>
</div> </div>
</transition> </transition>
<b-badge pill variant="info" v-if="recipe.internal !== undefined && !recipe.internal">{{ $t("External") }}</b-badge> <b-badge pill variant="info" v-if="recipe.internal !== undefined && !recipe.internal">{{ $t("External") }}</b-badge>
</template> </template>
</b-card-text> </b-card-text>
</b-card-body> </b-card-body>
</b-card> </b-card>
</template> </template>
</div> </div>
<!-- <!--
@ -123,14 +121,14 @@
</template> </template>
<script> <script>
import RecipeContextMenu from "@/components/RecipeContextMenu" import IngredientsCard from "@/components/IngredientsCard"
import KeywordsComponent from "@/components/KeywordsComponent" import KeywordsComponent from "@/components/KeywordsComponent"
import {resolveDjangoUrl, ResolveUrlMixin, calculateHourMinuteSplit} from "@/utils/utils" import LastCooked from "@/components/LastCooked"
import RecipeContextMenu from "@/components/RecipeContextMenu"
import RecipeRating from "@/components/RecipeRating" import RecipeRating from "@/components/RecipeRating"
import { ResolveUrlMixin, calculateHourMinuteSplit, resolveDjangoUrl } from "@/utils/utils"
import moment from "moment/moment" import moment from "moment/moment"
import Vue from "vue" import Vue from "vue"
import LastCooked from "@/components/LastCooked"
import IngredientsCard from "@/components/IngredientsCard"
Vue.prototype.moment = moment Vue.prototype.moment = moment
@ -141,19 +139,19 @@ export default {
LastCooked, LastCooked,
KeywordsComponent, KeywordsComponent,
"recipe-context-menu": RecipeContextMenu, "recipe-context-menu": RecipeContextMenu,
IngredientsCard IngredientsCard,
RecipeRating,
}, },
props: { props: {
recipe: Object, recipe: Object,
meal_plan: Object, meal_plan: Object,
use_plural: {type: Boolean, default: false},
footer_text: String, footer_text: String,
footer_icon: String, footer_icon: String,
detailed: {type: Boolean, default: true}, detailed: { type: Boolean, default: true },
show_context_menu: {type: Boolean, default: true}, show_context_menu: { type: Boolean, default: true },
context_disabled_options: Object, context_disabled_options: Object,
open_recipe_on_click: {type: Boolean, default: true}, open_recipe_on_click: { type: Boolean, default: true },
enable_keyword_links: {type: Boolean, default: true}, enable_keyword_links: { type: Boolean, default: true },
}, },
data() { data() {
return { return {
@ -161,8 +159,7 @@ export default {
} }
}, },
mounted() { mounted() {},
},
computed: { computed: {
show_detail: function () { show_detail: function () {
return this.recipe?.steps !== undefined && this.detailed return this.recipe?.steps !== undefined && this.detailed
@ -180,13 +177,13 @@ export default {
waiting_time: function () { waiting_time: function () {
return calculateHourMinuteSplit(this.recipe.waiting_time) return calculateHourMinuteSplit(this.recipe.waiting_time)
}, },
recipe_link: function (){ recipe_link: function () {
if(this.open_recipe_on_click){ if (this.open_recipe_on_click) {
return this.recipe.id !== undefined ? resolveDjangoUrl('view_recipe', this.recipe.id) : null return this.recipe.id !== undefined ? resolveDjangoUrl("view_recipe", this.recipe.id) : null
} else { } else {
return "#" return "#"
} }
} },
}, },
methods: {}, methods: {},
directives: { directives: {
@ -210,8 +207,7 @@ export default {
transition: opacity 0.5s; transition: opacity 0.5s;
} }
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
{
opacity: 0; opacity: 0;
} }
@ -257,9 +253,11 @@ export default {
.content-details { .content-details {
position: absolute; position: absolute;
text-align: center; text-align: center;
padding-left: 1em; padding: 1em 1em 0 1em;
padding-right: 1em;
width: 100%; width: 100%;
max-height: 100%;
overflow-y: scroll;
z-index: 1;
top: 50%; top: 50%;
left: 50%; left: 50%;
opacity: 0; opacity: 0;
@ -271,6 +269,10 @@ export default {
transition: all 0.3s ease-in-out 0s; transition: all 0.3s ease-in-out 0s;
} }
.content-details::-webkit-scrollbar {
display: none;
}
.content:hover .content-details { .content:hover .content-details {
top: 50%; top: 50%;
left: 50%; left: 50%;

View File

@ -1,36 +1,40 @@
<template> <template>
<div> <div>
<div class="dropdown d-print-none"> <div class="dropdown d-print-none">
<a class="btn shadow-none pr-0 pl-0" href="javascript:void(0);" role="button" id="dropdownMenuLink" <a class="btn shadow-none pr-0 pl-0" href="javascript:void(0);" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-ellipsis-v fa-lg"></i> <i class="fas fa-ellipsis-v fa-lg"></i>
</a> </a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink" > <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"><i <a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"
class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a> ><i class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a
>
<a class="dropdown-item" :href="resolveDjangoUrl('view_property_editor', recipe.id)" v-if="!disabled_options.edit"> <a class="dropdown-item" :href="resolveDjangoUrl('view_property_editor', recipe.id)" v-if="!disabled_options.edit">
<i class="fas fa-table"></i> {{ $t("Property_Editor") }}</a> <i class="fas fa-table"></i> {{ $t("Property_Editor") }}</a
>
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)" <a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)" v-if="!recipe.internal && !disabled_options.convert"
v-if="!recipe.internal && !disabled_options.convert"><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a> ><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a
>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)" v-if="!disabled_options.books"><i <button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)" v-if="!disabled_options.books">
class="fas fa-bookmark fa-fw"></i> {{ $t("Manage_Books") }} <i class="fas fa-bookmark fa-fw"></i> {{ $t("Manage_Books") }}
</button> </button>
</a> </a>
<a class="dropdown-item" v-if="recipe.internal && !disabled_options.shopping" @click="addToShopping" href="#" > <i <a class="dropdown-item" v-if="recipe.internal && !disabled_options.shopping" @click="addToShopping" href="#">
class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }} </a> <i class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }}
</a>
<a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);" v-if="!disabled_options.plan"><i <a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);" v-if="!disabled_options.plan"
class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }} </a> ><i class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }}
</a>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<button class="dropdown-item" @click="$bvModal.show(`id_modal_cook_log_${modal_id}`)" v-if="!disabled_options.log"><i <button class="dropdown-item" @click="$bvModal.show(`id_modal_cook_log_${modal_id}`)" v-if="!disabled_options.log">
class="fas fa-clipboard-list fa-fw"></i> {{ $t("Log_Cooking") }} <i class="fas fa-clipboard-list fa-fw"></i> {{ $t("Log_Cooking") }}
</button> </button>
</a> </a>
@ -41,56 +45,49 @@
</button> </button>
</a> </a>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<button class="dropdown-item" @click="copyToNew" v-if="!disabled_options.copy"><i class="fas fa-copy fa-fw"></i> <button class="dropdown-item" @click="copyToNew" v-if="!disabled_options.copy">
<i class="fas fa-copy fa-fw"></i>
{{ $t("copy_to_new") }} {{ $t("copy_to_new") }}
</button> </button>
</a> </a>
<a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank" <a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank" rel="noopener noreferrer" v-if="!disabled_options.export"
rel="noopener noreferrer" v-if="!disabled_options.export"><i class="fas fa-file-export fa-fw"></i> {{ $t("Export") }}</a> ><i class="fas fa-file-export fa-fw"></i> {{ $t("Export") }}</a
>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<button class="dropdown-item" @click="pinRecipe()" v-if="!disabled_options.pin"> <button class="dropdown-item" @click="pinRecipe()" v-if="!disabled_options.pin">
<i class="fas fa-thumbtack fa-fw"></i> <i class="fas fa-thumbtack fa-fw"></i>
{{ isPinned ? $t("Unpin") : $t("Pin")}} {{ isPinned ? $t("Unpin") : $t("Pin") }}
</button> </button>
</a> </a>
<a href="javascript:void(0);"> <a href="javascript:void(0);">
<button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal && !disabled_options.share" ><i <button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal && !disabled_options.share">
class="fas fa-share-alt fa-fw"></i> {{ $t("Share") }} <i class="fas fa-share-alt fa-fw"></i> {{ $t("Share") }}
</button> </button>
</a> </a>
</div> </div>
</div> </div>
<cook-log :recipe="recipe" :modal_id="modal_id"></cook-log> <cook-log :recipe="recipe" :modal_id="modal_id"></cook-log>
<add-recipe-to-book :recipe="recipe" :modal_id="modal_id" <add-recipe-to-book :recipe="recipe" :modal_id="modal_id" :entryEditing_inital_servings="servings_value"></add-recipe-to-book>
:entryEditing_inital_servings="servings_value"></add-recipe-to-book> <shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" :mealplan="undefined" />
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" :mealplan="undefined"/>
<b-modal :id="`modal-share-link_${modal_id}`" v-bind:title="$t('Share')" hide-footer> <b-modal :id="`modal-share-link_${modal_id}`" v-bind:title="$t('Share')" hide-footer>
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<label v-if="recipe_share_link !== undefined">{{ $t("Public share link") }}</label> <label v-if="recipe_share_link !== undefined">{{ $t("Public share link") }}</label>
<input ref="share_link_ref" class="form-control" v-model="recipe_share_link"/> <input ref="share_link_ref" class="form-control" v-model="recipe_share_link" />
<b-button class="mt-2 mb-3 d-none d-md-inline" variant="secondary" <b-button class="mt-2 mb-3 d-none d-md-inline" variant="secondary" @click="$bvModal.hide(`modal-share-link_${modal_id}`)">{{ $t("Close") }} </b-button>
@click="$bvModal.hide(`modal-share-link_${modal_id}`)">{{ $t("Close") }} <b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{ $t("Copy") }} </b-button>
</b-button> <b-button class="mt-2 mb-3 ml-2 float-right" variant="success" @click="shareIntend()">{{ $t("Share") }} <i class="fa fa-share-alt"></i></b-button>
<b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{
$t("Copy")
}}
</b-button>
<b-button class="mt-2 mb-3 ml-2 float-right" variant="success" @click="shareIntend()">{{
$t("Share")
}} <i class="fa fa-share-alt"></i></b-button>
</div> </div>
</div> </div>
</b-modal> </b-modal>
<meal-plan-edit-modal <meal-plan-edit-modal
:entry="entryEditing" :entry="entryEditing"
:entryEditing_inital_servings="servings_value"
@save-entry="saveMealPlan" @save-entry="saveMealPlan"
:modal_id="`modal-meal-plan_${modal_id}`" :modal_id="`modal-meal-plan_${modal_id}`"
:allow_delete="false" :allow_delete="false"
@ -100,16 +97,16 @@
</template> </template>
<script> <script>
import {makeToast, resolveDjangoUrl, ResolveUrlMixin, StandardToasts} from "@/utils/utils"
import CookLog from "@/components/CookLog" import CookLog from "@/components/CookLog"
import axios from "axios"
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
import MealPlanEditModal from "@/components/MealPlanEditModal" import MealPlanEditModal from "@/components/MealPlanEditModal"
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
import ShoppingModal from "@/components/Modals/ShoppingModal" import ShoppingModal from "@/components/Modals/ShoppingModal"
import { useMealPlanStore } from "@/stores/MealPlanStore"
import { ApiApiFactory } from "@/utils/openapi/api"
import { makeToast, resolveDjangoUrl, ResolveUrlMixin, StandardToasts } from "@/utils/utils"
import axios from "axios"
import moment from "moment" import moment from "moment"
import Vue from "vue" import Vue from "vue"
import {ApiApiFactory} from "@/utils/openapi/api"
import {useMealPlanStore} from "@/stores/MealPlanStore";
Vue.prototype.moment = moment Vue.prototype.moment = moment
@ -143,7 +140,7 @@ export default {
}, },
}, },
entryEditing: {}, entryEditing: {},
mealplan: undefined mealplan: undefined,
} }
}, },
props: { props: {
@ -154,19 +151,18 @@ export default {
}, },
disabled_options: { disabled_options: {
type: Object, type: Object,
default: () => ({print:true}), default: () => ({ print: true }),
}, },
}, },
mounted() { mounted() {
this.servings_value = this.servings === -1 ? this.recipe.servings : this.servings this.servings_value = this.servings === -1 ? this.recipe.servings : this.servings
let pinnedRecipes = JSON.parse(localStorage.getItem("pinned_recipes")) || [] let pinnedRecipes = JSON.parse(localStorage.getItem("pinned_recipes")) || []
this.isPinned = pinnedRecipes.some((r) => r.id == this.recipe.id); this.isPinned = pinnedRecipes.some((r) => r.id == this.recipe.id)
}, },
watch: { watch: {
recipe: { recipe: {
handler() { handler() {},
},
deep: true, deep: true,
}, },
servings: function (newVal) { servings: function (newVal) {
@ -174,14 +170,14 @@ export default {
}, },
}, },
methods: { methods: {
pinRecipe () { pinRecipe() {
let pinnedRecipes = JSON.parse(localStorage.getItem("pinned_recipes")) || [] let pinnedRecipes = JSON.parse(localStorage.getItem("pinned_recipes")) || []
if(this.isPinned) { if (this.isPinned) {
pinnedRecipes = pinnedRecipes.filter((r) => r.id !== this.recipe.id) pinnedRecipes = pinnedRecipes.filter((r) => r.id !== this.recipe.id)
makeToast(this.$t("Unpin"), this.$t("UnpinnedConfirmation", {recipe: this.recipe.name}), "info") makeToast(this.$t("Unpin"), this.$t("UnpinnedConfirmation", { recipe: this.recipe.name }), "info")
} else { } else {
pinnedRecipes.push({id: this.recipe.id, name: this.recipe.name}) pinnedRecipes.push({ id: this.recipe.id, name: this.recipe.name })
makeToast(this.$t("Pin"), this.$t("PinnedConfirmation", {recipe: this.recipe.name}), "info") makeToast(this.$t("Pin"), this.$t("PinnedConfirmation", { recipe: this.recipe.name }), "info")
} }
this.isPinned = !this.isPinned this.isPinned = !this.isPinned
localStorage.setItem("pinned_recipes", JSON.stringify(pinnedRecipes)) localStorage.setItem("pinned_recipes", JSON.stringify(pinnedRecipes))
@ -211,6 +207,7 @@ export default {
createMealPlan(data) { createMealPlan(data) {
this.entryEditing = this.options.entryEditing this.entryEditing = this.options.entryEditing
this.entryEditing.recipe = this.recipe this.entryEditing.recipe = this.recipe
this.entryEditing.servings = this.recipe.servings
this.entryEditing.from_date = moment(new Date()).format("YYYY-MM-DD") this.entryEditing.from_date = moment(new Date()).format("YYYY-MM-DD")
this.entryEditing.to_date = moment(new Date()).format("YYYY-MM-DD") this.entryEditing.to_date = moment(new Date()).format("YYYY-MM-DD")
this.$nextTick(function () { this.$nextTick(function () {
@ -218,17 +215,20 @@ export default {
}) })
}, },
createShareLink: function () { createShareLink: function () {
console.log('create') console.log("create")
axios.get(resolveDjangoUrl("api_share_link", this.recipe.id)).then((result) => { axios
console.log('success') .get(resolveDjangoUrl("api_share_link", this.recipe.id))
this.$bvModal.show(`modal-share-link_${this.modal_id}`) .then((result) => {
this.recipe_share_link = result.data.link console.log("success")
}).catch((err) => { this.$bvModal.show(`modal-share-link_${this.modal_id}`)
console.log('fail') this.recipe_share_link = result.data.link
if (err.response.status === 403) { })
makeToast(this.$t("Share"), this.$t("Sharing is not enabled for this space or your user account."), "danger") .catch((err) => {
} console.log("fail")
}) if (err.response.status === 403) {
makeToast(this.$t("Share"), this.$t("Sharing is not enabled for this space or your user account."), "danger")
}
})
}, },
copyShareLink: function () { copyShareLink: function () {
let share_input = this.$refs.share_link_ref let share_input = this.$refs.share_link_ref
@ -251,21 +251,21 @@ export default {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.retrieveRecipe(this.recipe.id).then((results) => { apiClient.retrieveRecipe(this.recipe.id).then((results) => {
let recipe = {...results.data, ...{id: undefined, name: recipe_name}} let recipe = { ...results.data, ...{ id: undefined, name: recipe_name } }
recipe.steps = recipe.steps.map((step) => { recipe.steps = recipe.steps.map((step) => {
return { return {
...step, ...step,
...{ ...{
id: undefined, id: undefined,
ingredients: step.ingredients.map((ingredient) => { ingredients: step.ingredients.map((ingredient) => {
return {...ingredient, ...{id: undefined}} return { ...ingredient, ...{ id: undefined } }
}), }),
}, },
} }
}) })
recipe.properties = recipe.properties.map(p => { recipe.properties = recipe.properties.map((p) => {
return { ...p, ...{ id: undefined, } } return { ...p, ...{ id: undefined } }
}) })
apiClient apiClient

View File

@ -1,24 +1,30 @@
<template> <template>
<div> <div>
<span class="d-inline" v-if="recipe.rating > 0"> <span class="d-inline" v-if="recipe.rating > 0">
<i class="fas fa-star fa-xs text-primary" v-for="i in Math.floor(recipe.rating)" v-bind:key="i"></i> <div v-if="!pill">
<i class="fas fa-star-half-alt fa-xs text-primary" v-if="recipe.rating % 1 > 0"></i> <i class="fas fa-star fa-xs text-primary" v-for="i in Math.floor(recipe.rating)" v-bind:key="i"></i>
<i class="far fa-star fa-xs text-secondary" v-for="i in (5 - Math.ceil(recipe.rating))" v-bind:key="i + 10"></i> <i class="fas fa-star-half-alt fa-xs text-primary" v-if="recipe.rating % 1 > 0"></i>
</span> <i class="far fa-star fa-xs text-secondary" v-for="i in 5 - Math.ceil(recipe.rating)" v-bind:key="i + 10"></i>
</div> </div>
<div v-else>
<b-badge pill variant="light" class="mt-1 font-weight-normal">
<i class="fas fa-star fa-xs text-dark" v-for="i in Math.floor(recipe.rating)" v-bind:key="i"></i>
<i class="fas fa-star-half-alt fa-xs text-dark" v-if="recipe.rating % 1 > 0"></i>
<i class="far fa-star fa-xs text-dark" v-for="i in 5 - Math.ceil(recipe.rating)" v-bind:key="i + 10"></i>
</b-badge>
</div>
</span>
</div>
</template> </template>
<script> <script>
export default { export default {
name: "RecipeRating", name: "RecipeRating",
props: { props: {
recipe: Object recipe: Object,
} pill: { required: false, type: Boolean, default: false },
},
} }
</script> </script>
<style scoped> <style scoped></style>
</style>

View File

@ -90,7 +90,6 @@
:index="index" :index="index"
:start_time="start_time" :start_time="start_time"
:force_ingredients="true" :force_ingredients="true"
:use_plural="use_plural"
></step-component> ></step-component>
</div> </div>
</div> </div>
@ -147,10 +146,6 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
use_plural: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
step_time: function() { step_time: function() {

View File

@ -536,7 +536,7 @@
"Unit_Replace": "Einheit Ersetzen", "Unit_Replace": "Einheit Ersetzen",
"quart": "\"Quart\" [qt] (US, Volumen)", "quart": "\"Quart\" [qt] (US, Volumen)",
"imperial_quart": "Engl. \"Quart\" [imp qt] (UK, Volumen)", "imperial_quart": "Engl. \"Quart\" [imp qt] (UK, Volumen)",
"err_importing_recipe": "Beim Importieren des Rezeptes ist ein Fehler aufgetreten!", "err_importing_recipe": "Es trat ein Fehler auf beim importieren des Rezepts!",
"property_type_fdc_hint": "Nur Nährwerte mit einer FDC ID können automatisch Daten aus der FDC Datenbank beziehen", "property_type_fdc_hint": "Nur Nährwerte mit einer FDC ID können automatisch Daten aus der FDC Datenbank beziehen",
"Property_Editor": "Nährwerte bearbeiten", "Property_Editor": "Nährwerte bearbeiten",
"CustomTheme": "Benutzerdefiniertes Theme", "CustomTheme": "Benutzerdefiniertes Theme",

View File

@ -534,5 +534,25 @@
"Food_Replace": "Zastąp produkt", "Food_Replace": "Zastąp produkt",
"Unit_Replace": "Zastąp jednostkę", "Unit_Replace": "Zastąp jednostkę",
"Alignment": "Wyrównanie", "Alignment": "Wyrównanie",
"make_now_count": "Najbardziej brakujące składniki" "make_now_count": "Najbardziej brakujące składniki",
"CustomTheme": "Własny motyw",
"CustomThemeHelp": "Zastąp style wybranego motywu, przesyłając własny plik CSS.",
"CustomLogoHelp": "Prześlij kwadratowe obrazy w różnych rozmiarach, aby zmienić logo w zakładce przeglądarki i zainstalowanej aplikacji internetowej.",
"Logo": "Logo",
"Show_Logo_Help": "Pokaż logo Tandoor lub przestrzeni na pasku nawigacyjnym.",
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
"err_importing_recipe": "Wystąpił błąd podczas importowania przepisu!",
"Properties_Food_Amount": "Właściwości ilości żywności",
"Properties_Food_Unit": "Właściwości jednostek żywności",
"FDC_Search": "Wyszukiwanie w FDC",
"property_type_fdc_hint": "Tylko właściwe typy z identyfikatorem FDC mogą automatycznie pobierać dane z bazy danych FDC",
"Property_Editor": "Edytor właściwości",
"FDC_ID": "Identyfikator FDC",
"FDC_ID_help": "Identyfikator bazy FDC",
"CustomImageHelp": "Prześlij obraz, który będzie wyświetlany w przeglądzie przestrzeni.",
"CustomNavLogoHelp": "Prześlij obraz, który będzie używany jako logo paska nawigacyjnego.",
"CustomLogos": "Własne loga",
"Show_Logo": "Pokaż logo",
"Nav_Text_Mode": "Tryb nawigacji tekstowej",
"Nav_Text_Mode_Help": "Zachowuje się inaczej dla każdego motywu."
} }

View File

@ -6049,9 +6049,9 @@ flush-write-stream@^1.0.0:
readable-stream "^2.3.6" readable-stream "^2.3.6"
follow-redirects@^1.0.0, follow-redirects@^1.15.0: follow-redirects@^1.0.0, follow-redirects@^1.15.0:
version "1.15.2" version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3: for-each@^0.3.3:
version "0.3.3" version "0.3.3"
@ -11848,13 +11848,13 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
workbox-background-sync@6.6.1, workbox-background-sync@^6.5.4: workbox-background-sync@7.0.0, workbox-background-sync@^7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5"
integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg== integrity sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA==
dependencies: dependencies:
idb "^7.0.1" idb "^7.0.1"
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-background-sync@^5.1.4: workbox-background-sync@^5.1.4:
version "5.1.4" version "5.1.4"
@ -11863,12 +11863,12 @@ workbox-background-sync@^5.1.4:
dependencies: dependencies:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-broadcast-update@6.6.1: workbox-broadcast-update@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e" resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz#7f611ca1a94ba8ac0aa40fa171c9713e0f937d22"
integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ== integrity sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ==
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-broadcast-update@^5.1.4: workbox-broadcast-update@^5.1.4:
version "5.1.4" version "5.1.4"
@ -11877,10 +11877,10 @@ workbox-broadcast-update@^5.1.4:
dependencies: dependencies:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-build@6.6.1: workbox-build@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0" resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.0.0.tgz#02ab5ef2991b3369b8b9395703f08912212769b4"
integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw== integrity sha512-CttE7WCYW9sZC+nUYhQg3WzzGPr4IHmrPnjKiu3AMXsiNQKx+l4hHl63WTrnicLmKEKHScWDH8xsGBdrYgtBzg==
dependencies: dependencies:
"@apideck/better-ajv-errors" "^0.3.1" "@apideck/better-ajv-errors" "^0.3.1"
"@babel/core" "^7.11.1" "@babel/core" "^7.11.1"
@ -11904,21 +11904,21 @@ workbox-build@6.6.1:
strip-comments "^2.0.1" strip-comments "^2.0.1"
tempy "^0.6.0" tempy "^0.6.0"
upath "^1.2.0" upath "^1.2.0"
workbox-background-sync "6.6.1" workbox-background-sync "7.0.0"
workbox-broadcast-update "6.6.1" workbox-broadcast-update "7.0.0"
workbox-cacheable-response "6.6.1" workbox-cacheable-response "7.0.0"
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-expiration "6.6.1" workbox-expiration "7.0.0"
workbox-google-analytics "6.6.1" workbox-google-analytics "7.0.0"
workbox-navigation-preload "6.6.1" workbox-navigation-preload "7.0.0"
workbox-precaching "6.6.1" workbox-precaching "7.0.0"
workbox-range-requests "6.6.1" workbox-range-requests "7.0.0"
workbox-recipes "6.6.1" workbox-recipes "7.0.0"
workbox-routing "6.6.1" workbox-routing "7.0.0"
workbox-strategies "6.6.1" workbox-strategies "7.0.0"
workbox-streams "6.6.1" workbox-streams "7.0.0"
workbox-sw "6.6.1" workbox-sw "7.0.0"
workbox-window "6.6.1" workbox-window "7.0.0"
workbox-build@^5.1.4: workbox-build@^5.1.4:
version "5.1.4" version "5.1.4"
@ -11962,12 +11962,12 @@ workbox-build@^5.1.4:
workbox-sw "^5.1.4" workbox-sw "^5.1.4"
workbox-window "^5.1.4" workbox-window "^5.1.4"
workbox-cacheable-response@6.6.1: workbox-cacheable-response@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9" resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz#ee27c036728189eed69d25a135013053277482d2"
integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag== integrity sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g==
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-cacheable-response@^5.1.4: workbox-cacheable-response@^5.1.4:
version "5.1.4" version "5.1.4"
@ -11981,18 +11981,23 @@ workbox-core@6.6.1:
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265" resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265"
integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw== integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw==
workbox-core@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.0.0.tgz#dec114ec923cc2adc967dd9be1b8a0bed50a3545"
integrity sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==
workbox-core@^5.1.4: workbox-core@^5.1.4:
version "5.1.4" version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-5.1.4.tgz#8bbfb2362ecdff30e25d123c82c79ac65d9264f4" resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-5.1.4.tgz#8bbfb2362ecdff30e25d123c82c79ac65d9264f4"
integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg== integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==
workbox-expiration@6.6.1, workbox-expiration@^6.5.4: workbox-expiration@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739" resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.0.0.tgz#3d90bcf2a7577241de950f89784f6546b66c2baa"
integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A== integrity sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ==
dependencies: dependencies:
idb "^7.0.1" idb "^7.0.1"
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-expiration@^5.1.4: workbox-expiration@^5.1.4:
version "5.1.4" version "5.1.4"
@ -12001,15 +12006,23 @@ workbox-expiration@^5.1.4:
dependencies: dependencies:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-google-analytics@6.6.1: workbox-expiration@^6.5.4:
version "6.6.1" version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d" resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739"
integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA== integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A==
dependencies: dependencies:
workbox-background-sync "6.6.1" idb "^7.0.1"
workbox-core "6.6.1" workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1" workbox-google-analytics@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz#603b2c4244af1e85de0fb26287d4e17d3293452a"
integrity sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==
dependencies:
workbox-background-sync "7.0.0"
workbox-core "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-google-analytics@^5.1.4: workbox-google-analytics@^5.1.4:
version "5.1.4" version "5.1.4"
@ -12021,12 +12034,12 @@ workbox-google-analytics@^5.1.4:
workbox-routing "^5.1.4" workbox-routing "^5.1.4"
workbox-strategies "^5.1.4" workbox-strategies "^5.1.4"
workbox-navigation-preload@6.6.1, workbox-navigation-preload@^6.5.4: workbox-navigation-preload@7.0.0, workbox-navigation-preload@^7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059" resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz#4913878dbbd97057181d57baa18d2bbdde085c6c"
integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA== integrity sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-navigation-preload@^5.1.4: workbox-navigation-preload@^5.1.4:
version "5.1.4" version "5.1.4"
@ -12035,14 +12048,14 @@ workbox-navigation-preload@^5.1.4:
dependencies: dependencies:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-precaching@6.6.1, workbox-precaching@^6.5.4: workbox-precaching@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2" resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.0.0.tgz#3979ba8033aadf3144b70e9fe631d870d5fbaa03"
integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A== integrity sha512-EC0vol623LJqTJo1mkhD9DZmMP604vHqni3EohhQVwhJlTgyKyOkMrZNy5/QHfOby+39xqC01gv4LjOm4HSfnA==
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-routing "6.6.1" workbox-routing "7.0.0"
workbox-strategies "6.6.1" workbox-strategies "7.0.0"
workbox-precaching@^5.1.4: workbox-precaching@^5.1.4:
version "5.1.4" version "5.1.4"
@ -12051,12 +12064,21 @@ workbox-precaching@^5.1.4:
dependencies: dependencies:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-range-requests@6.6.1: workbox-precaching@^6.5.4:
version "6.6.1" version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39" resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2"
integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g== integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A==
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-range-requests@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz#97511901e043df27c1aa422adcc999a7751f52ed"
integrity sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ==
dependencies:
workbox-core "7.0.0"
workbox-range-requests@^5.1.4: workbox-range-requests@^5.1.4:
version "5.1.4" version "5.1.4"
@ -12065,25 +12087,32 @@ workbox-range-requests@^5.1.4:
dependencies: dependencies:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-recipes@6.6.1: workbox-recipes@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae" resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.0.0.tgz#1a6a01c8c2dfe5a41eef0fed3fe517e8a45c6514"
integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g== integrity sha512-DntcK9wuG3rYQOONWC0PejxYYIDHyWWZB/ueTbOUDQgefaeIj1kJ7pdP3LZV2lfrj8XXXBWt+JDRSw1lLLOnww==
dependencies: dependencies:
workbox-cacheable-response "6.6.1" workbox-cacheable-response "7.0.0"
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-expiration "6.6.1" workbox-expiration "7.0.0"
workbox-precaching "6.6.1" workbox-precaching "7.0.0"
workbox-routing "6.6.1" workbox-routing "7.0.0"
workbox-strategies "6.6.1" workbox-strategies "7.0.0"
workbox-routing@6.6.1, workbox-routing@^6.5.4: workbox-routing@6.6.1:
version "6.6.1" version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581" resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581"
integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg== integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg==
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "6.6.1"
workbox-routing@7.0.0, workbox-routing@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.0.0.tgz#6668438a06554f60645aedc77244a4fe3a91e302"
integrity sha512-8YxLr3xvqidnbVeGyRGkaV4YdlKkn5qZ1LfEePW3dq+ydE73hUUJJuLmGEykW3fMX8x8mNdL0XrWgotcuZjIvA==
dependencies:
workbox-core "7.0.0"
workbox-routing@^5.1.4: workbox-routing@^5.1.4:
version "5.1.4" version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-5.1.4.tgz#3e8cd86bd3b6573488d1a2ce7385e547b547e970" resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-5.1.4.tgz#3e8cd86bd3b6573488d1a2ce7385e547b547e970"
@ -12098,6 +12127,13 @@ workbox-strategies@6.6.1, workbox-strategies@^6.2.4:
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "6.6.1"
workbox-strategies@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.0.0.tgz#dcba32b3f3074476019049cc490fe1a60ea73382"
integrity sha512-dg3qJU7tR/Gcd/XXOOo7x9QoCI9nk74JopaJaYAQ+ugLi57gPsXycVdBnYbayVj34m6Y8ppPwIuecrzkpBVwbA==
dependencies:
workbox-core "7.0.0"
workbox-strategies@^5.1.4: workbox-strategies@^5.1.4:
version "5.1.4" version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-5.1.4.tgz#96b1418ccdfde5354612914964074d466c52d08c" resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-5.1.4.tgz#96b1418ccdfde5354612914964074d466c52d08c"
@ -12106,13 +12142,13 @@ workbox-strategies@^5.1.4:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-routing "^5.1.4" workbox-routing "^5.1.4"
workbox-streams@6.6.1: workbox-streams@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26" resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.0.0.tgz#36722aecd04785f88b6f709e541c094fc658c0f9"
integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q== integrity sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ==
dependencies: dependencies:
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-routing "6.6.1" workbox-routing "7.0.0"
workbox-streams@^5.1.4: workbox-streams@^5.1.4:
version "5.1.4" version "5.1.4"
@ -12122,10 +12158,10 @@ workbox-streams@^5.1.4:
workbox-core "^5.1.4" workbox-core "^5.1.4"
workbox-routing "^5.1.4" workbox-routing "^5.1.4"
workbox-sw@6.6.1: workbox-sw@7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c" resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.0.0.tgz#7350126411e3de1409f7ec243df8d06bb5b08b86"
integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ== integrity sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA==
workbox-sw@^5.1.4: workbox-sw@^5.1.4:
version "5.1.4" version "5.1.4"
@ -12144,24 +12180,24 @@ workbox-webpack-plugin@^5.1.3, workbox-webpack-plugin@^6.1.0:
webpack-sources "^1.3.0" webpack-sources "^1.3.0"
workbox-build "^5.1.4" workbox-build "^5.1.4"
workbox-webpack-plugin@^6.5.4: workbox-webpack-plugin@^7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531" resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-7.0.0.tgz#6c61661a2cacde1239192a5877a041a2943d1a55"
integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA== integrity sha512-R1ZzCHPfzeJjLK2/TpKUhxSQ3fFDCxlWxgRhhSjMQLz3G2MlBnyw/XeYb34e7SGgSv0qG22zEhMIzjMNqNeKbw==
dependencies: dependencies:
fast-json-stable-stringify "^2.1.0" fast-json-stable-stringify "^2.1.0"
pretty-bytes "^5.4.1" pretty-bytes "^5.4.1"
upath "^1.2.0" upath "^1.2.0"
webpack-sources "^1.4.3" webpack-sources "^1.4.3"
workbox-build "6.6.1" workbox-build "7.0.0"
workbox-window@6.6.1, workbox-window@^6.5.4: workbox-window@7.0.0, workbox-window@^7.0.0:
version "6.6.1" version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e" resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.0.0.tgz#a683ab33c896e4f16786794eac7978fc98a25d08"
integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ== integrity sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==
dependencies: dependencies:
"@types/trusted-types" "^2.0.2" "@types/trusted-types" "^2.0.2"
workbox-core "6.6.1" workbox-core "7.0.0"
workbox-window@^5.1.4: workbox-window@^5.1.4:
version "5.1.4" version "5.1.4"