Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a41ad8c6ed | ||
|
|
9c38c89c44 | ||
|
|
998440d064 | ||
|
|
b01d12c377 | ||
|
|
1dee574a08 | ||
|
|
257c4461a3 | ||
|
|
01f4257190 | ||
|
|
d7b7dd6c83 | ||
|
|
23c2eab682 | ||
|
|
def346d16d | ||
|
|
cc324b29ae | ||
|
|
9d58f9b266 | ||
|
|
30b2776f3c | ||
|
|
60d23d0686 | ||
|
|
edf649dea6 | ||
|
|
29b4a3cd22 | ||
|
|
f3a5148628 | ||
|
|
33abd777e0 | ||
|
|
739055caf6 | ||
|
|
8c29bd3439 | ||
|
|
2c4d0b692b | ||
|
|
946b79b77a | ||
|
|
cd154d09b2 | ||
|
|
236c930b54 | ||
|
|
980c847e36 | ||
|
|
91700771e6 | ||
|
|
abb6ad5fd0 | ||
|
|
ac7af02f77 | ||
|
|
525b398687 | ||
|
|
fafc836ccc | ||
|
|
c617b829e5 | ||
|
|
5b1e827d45 | ||
|
|
e33b62be2a | ||
|
|
60c33b499c | ||
|
|
c205dff523 | ||
|
|
ce69899c4b | ||
|
|
a4183e3453 | ||
|
|
ab39408a24 | ||
|
|
8c6c98483c | ||
|
|
ae095ab572 | ||
|
|
65356bc21a | ||
|
|
3aed5de3fc | ||
|
|
a4e9e54dae | ||
|
|
8f698e437e | ||
|
|
ab0d36825a | ||
|
|
4e2f6c57f1 | ||
|
|
de4cb8ba83 | ||
|
|
375f43c596 | ||
|
|
3034945e7e | ||
|
|
8e5effa532 | ||
|
|
3b81d3b18a | ||
|
|
d0f8b5773d | ||
|
|
14910162dc | ||
|
|
035f780d27 | ||
|
|
f10161ee92 | ||
|
|
b1a100a8c5 | ||
|
|
7db39d32d1 | ||
|
|
10921f9a64 | ||
|
|
ba1c44172e | ||
|
|
fd2dc15a15 | ||
|
|
47124488bb | ||
|
|
6e680c972a | ||
|
|
1fd2eb37ae | ||
|
|
923a59791a | ||
|
|
1fcc2c755a | ||
|
|
d5f7a883df | ||
|
|
17f9eef551 | ||
|
|
ca1ab33291 | ||
|
|
6e6ae80c46 | ||
|
|
aa6e109162 | ||
|
|
a6e4b778c1 | ||
|
|
31c7cb7906 | ||
|
|
d954b5cf48 | ||
|
|
e5c2f5570f |
@@ -38,3 +38,6 @@ RUN apt-get update \
|
||||
libwebp-dev \
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1
|
||||
|
||||
# create directory used for Docker Secrets
|
||||
RUN mkdir -p /run/secrets
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
},
|
||||
"extensions": [
|
||||
"charliermarsh.ruff",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"matangover.mypy",
|
||||
"ms-python.black-formatter",
|
||||
|
||||
9
.github/workflows/release.yml
vendored
@@ -61,10 +61,15 @@ jobs:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Version From Tag Name
|
||||
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
|
||||
|
||||
- name: Modify version strings
|
||||
run: |
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
|
||||
sed -i 's/^\s*"version": "[^"]*"/"version": "${{ env.VERSION_NUM }}"/' frontend/package.json
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
|
||||
32
.github/workflows/scheduled-checks.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Scheduled Checks
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every monday at 7 AM
|
||||
- cron: 0 7 * * 1
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update pre-commit Hooks
|
||||
uses: vrslev/pre-commit-autoupdate@v1.0.0
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
# This doesn't currently work for us because it creates the PR but the workflows don't run.
|
||||
# TODO: Provide a personal access token as a parameter here, that solves that problem.
|
||||
# https://github.com/peter-evans/create-pull-request
|
||||
with:
|
||||
commit-message: "Update pre-commit hooks"
|
||||
branch: "fix/update-pre-commit-hooks"
|
||||
delete-branch: true
|
||||
base: mealie-next
|
||||
title: "fix(auto): Update pre-commit hooks"
|
||||
body: "Auto-generated by `.github/workflows/scheduled-checks.yml`"
|
||||
@@ -12,6 +12,6 @@ repos:
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.5.0
|
||||
rev: v0.5.5
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
|
||||
@@ -148,7 +148,7 @@ tasks:
|
||||
- poetry run python mealie/app.py
|
||||
|
||||
py:migrate:
|
||||
desc: generates a new database migration file e.g. task py:migrate "add new column"
|
||||
desc: generates a new database migration file e.g. task py:migrate -- "add new column"
|
||||
cmds:
|
||||
- poetry run alembic revision --autogenerate -m "{{ .CLI_ARGS }}"
|
||||
- task: py:format
|
||||
|
||||
@@ -13,7 +13,7 @@ from text_unidecode import unidecode
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.db.models._model_utils import GUID
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5ab195a474eb"
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2024-03-18 02:28:15.896959
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
@@ -34,7 +34,7 @@ def new_user_rating(user_id: Any, recipe_id: Any, rating: float | None = None, i
|
||||
else:
|
||||
id = "%.32x" % uuid4().int
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
return {
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
|
||||
@@ -92,6 +92,9 @@ RUN apt-get update \
|
||||
libldap-2.5 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# create directory used for Docker Secrets
|
||||
RUN mkdir -p /run/secrets
|
||||
|
||||
# copying poetry and venv into image
|
||||
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
|
||||
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
||||
|
||||
@@ -9,7 +9,7 @@ How exactly you need to modify it is of course highly contextual to the change y
|
||||
|
||||
## Using Alembic to generate upgrade script
|
||||
|
||||
In your dev container you can run something like (change the message) `task py:migrate "Add creation tag to group preferences"` to have Alembic generate an upgrade script for you.
|
||||
In your dev container you can run something like (change the message) `task py:migrate -- "Add creation tag to group preferences"` to have Alembic generate an upgrade script for you.
|
||||
|
||||
The script Alembic generates, will be limited! (Perhaps there's a way to resolve that? Haven't looked into it yet)
|
||||
For example, Alembic generated a script _similar_ to this (it has been modified already to have accurate foreign key names, for instance):
|
||||
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Don't forget to change the <code>mydomain.duckns</code> into your personal domain and the <code>duckdnstoken</code> into your token and remove the brackets.
|
||||
Don't forget to change the <code>mydomain.duckdns</code> into your personal domain and the <code>duckdnstoken</code> into your token and remove the brackets.
|
||||
|
||||
## Step 3: Change the config files
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
## How do I enable "smart" ingredient handling?
|
||||
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And making shopping lists from reciepes that have shared ingredients can yield multiple lines of the same ingredient. **But**, mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And, making shopping lists from recipes that have shared ingredients can yield multiple lines of the same ingredient. **But**, Mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
### Set up your Foods and Units
|
||||
Do the following just **once**. Doing this applies to your whole group, so be careful.
|
||||
|
||||
1. Click on your name in the upper left corner to get to your settings
|
||||
2. In the bottom right, select `Manage Data`
|
||||
3. In the Management page, make sure that a little orange button says `Foods`
|
||||
4. If your Foods database is empty, click `Seed` and choose your language. You should end up with a list of foods. (Wait bit for seeding to happen, and try not to seed more than once or you will have duplicates)
|
||||
4. If your Foods database is empty, click `Seed` and choose your language. You should end up with a list of foods. (Wait a bit for seeding to happen, and try not to seed more than once or you will have duplicates)
|
||||
5. Click the little orange `Foods` button and now choose `Units`.
|
||||
6. Click `Seed` and choose your language. You should end up with a list of units (e.g. `tablespoon`)
|
||||
|
||||
@@ -33,9 +33,9 @@ Do the following for each recipe you want to intelligently handle ingredients.
|
||||
|
||||
Scaling up this recipe or adding it to a Shopping List will now smartly take care of ingredient amounts and duplicate combinations.
|
||||
|
||||
## Is it Safe to Upgrade Mealie?
|
||||
## Is it safe to upgrade Mealie?
|
||||
|
||||
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Not that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
|
||||
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Note that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
|
||||
|
||||
- [Migration From v0.5.x](./migrating-to-mealie-v1.md)
|
||||
|
||||
@@ -45,7 +45,7 @@ You can change the theme by settings the environment variables.
|
||||
|
||||
- [Backend Config - Themeing](./installation/backend-config.md#themeing)
|
||||
|
||||
## How can I change the Login Session Timeout?
|
||||
## How can I change the login session timeout?
|
||||
|
||||
Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
|
||||
|
||||
@@ -53,7 +53,7 @@ Login session can be configured by setting the `TOKEN_TIME` variable on the back
|
||||
|
||||
## Can I serve Mealie on a subpath?
|
||||
|
||||
No. Due to limitations from the Javascript Framework, mealie doesn't support serving Mealie on a subpath.
|
||||
No. Due to limitations from the JavaScript Framework, Mealie doesn't support serving Mealie on a subpath.
|
||||
|
||||
## Can I install Mealie without docker?
|
||||
|
||||
@@ -130,8 +130,8 @@ stateDiagram-v2
|
||||
|
||||
For more information, check out the [Permissions and Public Access guide](./usage/permissions-and-public-access.md).
|
||||
|
||||
## Can I use fail2ban with mealie?
|
||||
Yes, mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux.
|
||||
## Can I use fail2ban with Mealie?
|
||||
Yes, Mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux.
|
||||
|
||||
Your fail2ban usage should look like the following:
|
||||
```
|
||||
@@ -139,11 +139,11 @@ Use datepattern : %d-%b-%y %H:%M:%S : Day-MON-Year2 24hour:Minute:Second
|
||||
Use failregex line : ^ERROR:\s+Incorrect username or password from <HOST>
|
||||
```
|
||||
|
||||
## Why An API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based on Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
## Why an API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based on Meal Plan data to remind you to defrost the chicken, marinate the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
|
||||
## Why a Database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project, it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in control of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
## Why a database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project, it is a valid concern to be worried about your data. Mealie specifically addresses this concern by providing automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in control of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
|
||||
As to why we need a database?
|
||||
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
|
||||
### General
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------- | :-------------------: | ----------------------------------------------------------------------------------- |
|
||||
| PUID | 911 | UserID permissions between host OS and container |
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
|
||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||
| API_DOCS | True | Turns on/off access to the API documentation locally. |
|
||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
|
||||
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
|
||||
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug, trace) |
|
||||
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run the daily tasks. |
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------- | :-------------------: | --------------------------------------------------------------------------------------------------------- |
|
||||
| PUID | 911 | UserID permissions between host OS and container |
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
|
||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||
| API_DOCS | True | Turns on/off access to the API documentation locally |
|
||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
|
||||
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
|
||||
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug, trace) |
|
||||
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
|
||||
|
||||
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as apart of a security review of the application.
|
||||
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application.
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ The following steps were tested on a Ubuntu 20.04 server, but should work for mo
|
||||
4. Create a docker-compose.yaml file in the mealie directory: `touch docker-compose.yaml`
|
||||
5. Use the text editor of your choice to edit the file and copy the contents of the docker-compose template for the deployment type you want to use: `nano docker-compose.yaml` or `vi docker-compose.yaml`
|
||||
|
||||
## Step 2: Customizing The `docker-compose.yaml` files.
|
||||
## Step 3: Customizing The `docker-compose.yaml` files.
|
||||
|
||||
After you've decided setup the files it's important to set a few ENV variables to ensure that you can use all the features of Mealie. I recommend that you verify and check that:
|
||||
|
||||
@@ -67,7 +67,7 @@ After you've decided setup the files it's important to set a few ENV variables t
|
||||
- [x] You've set the [`BASE_URL`](./backend-config.md#general) variable.
|
||||
- [x] You've set the `DEFAULT_EMAIL` and `DEFAULT_GROUP` variable.
|
||||
|
||||
## Step 3: Startup
|
||||
## Step 4: Startup
|
||||
|
||||
After you've configured your database and updated the `docker-compose.yaml` files, you can start Mealie by running the following command in the directory where you've added your `docker-compose.yaml`.
|
||||
|
||||
@@ -87,11 +87,11 @@ You should see the containers start up without error. You should now be able to
|
||||
|
||||
**Password:** MyPassword
|
||||
|
||||
## Step 4: Validate Installation
|
||||
## Step 5: Validate Installation
|
||||
|
||||
After the startup is complete, you should see a login screen. Use the default credentials above to log in and navigate to `/admin/site-settings`. Here, you'll find a summary of your configuration details and their respective status. Before proceeding, you should validate that the configuration is correct. For any warnings or errors the page will display an error and notify you of what you need to verify.
|
||||
|
||||
## Step 5: Backup
|
||||
## Step 6: Backup
|
||||
|
||||
While v1.0.0 is a great step to data-stability and security, it's not a backup. Mealie provides a full site data backup mechanism through the UI.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.10.1 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.10.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
# Set Backend ENV Variables Here
|
||||
ALLOW_SIGNUP: true
|
||||
ALLOW_SIGNUP: false
|
||||
PUID: 1000
|
||||
PGID: 1000
|
||||
TZ: America/Anchorage
|
||||
|
||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.10.1 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.10.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
# Set Backend ENV Variables Here
|
||||
ALLOW_SIGNUP: true
|
||||
ALLOW_SIGNUP: false
|
||||
PUID: 1000
|
||||
PGID: 1000
|
||||
TZ: America/Anchorage
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
icon: $globals.icons.testTube,
|
||||
text: $tc('general.test'),
|
||||
event: 'test',
|
||||
// TODO: There is no functionality hooked up to this. Enable it when there is
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<v-list-item-subtitle>
|
||||
<SafeMarkdown :source="description" />
|
||||
</v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap justify-start">
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" />
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-end align-center">
|
||||
<slot name="actions">
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup && showRecipeContent" :recipe-id="recipeId" show-always />
|
||||
@@ -83,6 +86,7 @@ import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import RecipeRating from "./RecipeRating.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
@@ -91,6 +95,7 @@ export default defineComponent({
|
||||
RecipeContextMenu,
|
||||
RecipeRating,
|
||||
RecipeCardImage,
|
||||
RecipeChips,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
@@ -114,6 +119,10 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: "abc123",
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@@ -237,30 +237,40 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
let currentTitle = "";
|
||||
const onHandIngs: ShoppingListIngredient[] = [];
|
||||
const shoppingListIngredientSections = shoppingListIngredients.reduce((sections, ing) => {
|
||||
// if title append new section to the end of the array
|
||||
if (ing.ingredient.title) {
|
||||
currentTitle = ing.ingredient.title;
|
||||
}
|
||||
|
||||
// If this is the first item in the section, create a new section
|
||||
if (sections.length === 0 || currentTitle !== sections[sections.length - 1].sectionName) {
|
||||
if (sections.length) {
|
||||
// Add the on-hand ingredients to the previous section
|
||||
sections[sections.length - 1].ingredients.push(...onHandIngs);
|
||||
onHandIngs.length = 0;
|
||||
}
|
||||
sections.push({
|
||||
sectionName: ing.ingredient.title,
|
||||
ingredients: [ing],
|
||||
sectionName: currentTitle,
|
||||
ingredients: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Store the on-hand ingredients for later
|
||||
if (ing.ingredient.food?.onHand) {
|
||||
onHandIngs.push(ing);
|
||||
return sections;
|
||||
}
|
||||
|
||||
// append new section if first
|
||||
if (sections.length === 0) {
|
||||
sections.push({
|
||||
sectionName: "",
|
||||
ingredients: [ing],
|
||||
});
|
||||
return sections;
|
||||
}
|
||||
|
||||
// otherwise add ingredient to last section in the array
|
||||
// Add the ingredient to previous section
|
||||
sections[sections.length - 1].ingredients.push(ing);
|
||||
return sections;
|
||||
}, [] as ShoppingListIngredientSection[]);
|
||||
|
||||
// Add remaining on-hand ingredients to the previous section
|
||||
shoppingListIngredientSections[shoppingListIngredientSections.length - 1].ingredients.push(...onHandIngs);
|
||||
|
||||
recipeSectionMap.set(recipe.slug, {
|
||||
recipeId: recipe.id,
|
||||
recipeName: recipe.name,
|
||||
|
||||
@@ -126,8 +126,8 @@
|
||||
<RecipeCardSection
|
||||
v-if="state.ready"
|
||||
class="mt-n5"
|
||||
:icon="$globals.icons.search"
|
||||
:title="$tc('search.results')"
|
||||
:icon="$globals.icons.silverwareForkKnife"
|
||||
:title="$tc('general.recipes')"
|
||||
:recipes="recipes"
|
||||
:query="passedQueryWithSeed"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<v-icon left>
|
||||
{{ $globals.icons.calendar }}
|
||||
</v-icon>
|
||||
{{ $t('recipe.last-made-date', { date: value ? new Date(value+"Z").toLocaleDateString($i18n.locale) : $t("general.never") } ) }}
|
||||
{{ $t('recipe.last-made-date', { date: value ? new Date(value).toLocaleDateString($i18n.locale) : $t("general.never") } ) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,11 +199,7 @@ export default defineComponent({
|
||||
await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp);
|
||||
|
||||
// update recipe in parent so the user can see it
|
||||
// we remove the trailing "Z" since this is how the API returns it
|
||||
context.emit(
|
||||
"input", newTimelineEvent.value.timestamp
|
||||
.substring(0, newTimelineEvent.value.timestamp.length - 1)
|
||||
);
|
||||
context.emit("input", newTimelineEvent.value.timestamp);
|
||||
}
|
||||
|
||||
// update the image, if provided
|
||||
|
||||
@@ -114,9 +114,9 @@ export default defineComponent({
|
||||
options: {
|
||||
ignoreLocation: true,
|
||||
shouldSort: true,
|
||||
threshold: 0.6,
|
||||
threshold: 0.2,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
distance: 20,
|
||||
findAllMatches: true,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<template v-if="!useMobileFormat" #opposite>
|
||||
<v-chip v-if="event.timestamp" label large>
|
||||
<v-icon class="mr-1"> {{ $globals.icons.calendar }} </v-icon>
|
||||
{{ new Date(event.timestamp+"Z").toLocaleDateString($i18n.locale) }}
|
||||
{{ new Date(event.timestamp).toLocaleDateString($i18n.locale) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-card
|
||||
@@ -25,7 +25,7 @@
|
||||
<v-col v-if="useMobileFormat" align-self="center" class="pr-0">
|
||||
<v-chip label>
|
||||
<v-icon> {{ $globals.icons.calendar }} </v-icon>
|
||||
{{ new Date(event.timestamp+"Z").toLocaleDateString($i18n.locale) }}
|
||||
{{ new Date(event.timestamp || "").toLocaleDateString($i18n.locale) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col v-else cols="9" style="margin: auto; text-align: center;">
|
||||
|
||||
@@ -69,13 +69,13 @@
|
||||
</v-row>
|
||||
<v-row v-if="!listItem.checked && recipeList && recipeList.length && displayRecipeRefs" no-gutters class="mb-2">
|
||||
<v-col cols="auto" style="width: 100%;">
|
||||
<RecipeList :recipes="recipeList" :list-item="listItem" :disabled="isOffline" small tile />
|
||||
<RecipeList :recipes="recipeList" :list-item="listItem" :disabled="$nuxt.isOffline" small tile />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="listItem.checked" no-gutters class="mb-2">
|
||||
<v-col cols="auto">
|
||||
<div class="text-caption font-weight-light font-italic">
|
||||
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updateAt+"Z").toLocaleDateString($i18n.locale)}) }}
|
||||
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updateAt || "").toLocaleDateString($i18n.locale)}) }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -136,10 +136,6 @@ export default defineComponent({
|
||||
type: Map<string, RecipeSummary>,
|
||||
default: undefined,
|
||||
},
|
||||
isOffline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
|
||||
@@ -157,9 +157,9 @@
|
||||
|
||||
const topLinks = computed<SidebarLinks>(() => [
|
||||
{
|
||||
icon: $globals.icons.search,
|
||||
icon: $globals.icons.silverwareForkKnife,
|
||||
to: `/g/${groupSlug.value}`,
|
||||
title: i18n.tc("sidebar.search"),
|
||||
title: i18n.tc("general.recipes"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -64,7 +64,6 @@ export const useGroupWebhooks = function () {
|
||||
newDt.setMinutes(Number(minutes));
|
||||
|
||||
updateData.scheduledTime = `${pad(newDt.getUTCHours(), 2)}:${pad(newDt.getUTCMinutes(), 2)}`;
|
||||
console.log(updateData.scheduledTime);
|
||||
|
||||
const payload = {
|
||||
...updateData,
|
||||
@@ -85,7 +84,14 @@ export const useGroupWebhooks = function () {
|
||||
if (data) {
|
||||
this.refreshAll();
|
||||
}
|
||||
loading.value = false;
|
||||
},
|
||||
|
||||
async testOne(id: string | number) {
|
||||
loading.value = true;
|
||||
await api.groupWebhooks.testOne(id);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const webhooks = actions.getAll();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { computed, ref } from "@nuxtjs/composition-api";
|
||||
import { computed, reactive, watch } from "@nuxtjs/composition-api";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ShoppingListItemOut } from "~/lib/api/types/group";
|
||||
import { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/group";
|
||||
import { RequestResponse } from "~/lib/api/types/non-generated";
|
||||
|
||||
const localStorageKey = "shopping-list-queue";
|
||||
const queueTimeout = 48 * 60 * 60 * 1000; // 48 hours
|
||||
const queueTimeout = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
type ItemQueueType = "create" | "update" | "delete";
|
||||
|
||||
@@ -23,27 +24,73 @@ interface Storage {
|
||||
export function useShoppingListItemActions(shoppingListId: string) {
|
||||
const api = useUserApi();
|
||||
const storage = useLocalStorage(localStorageKey, {} as Storage, { deep: true });
|
||||
const queue = storage.value[shoppingListId] ||= { create: [], update: [], delete: [], lastUpdate: Date.now()};
|
||||
const queue = reactive(getQueue());
|
||||
const queueEmpty = computed(() => !queue.create.length && !queue.update.length && !queue.delete.length);
|
||||
if (queueEmpty.value) {
|
||||
queue.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
const isOffline = ref(false);
|
||||
storage.value[shoppingListId] = { ...queue }
|
||||
watch(
|
||||
() => queue,
|
||||
(value) => {
|
||||
storage.value[shoppingListId] = { ...value }
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
function removeFromQueue(queue: ShoppingListItemOut[], item: ShoppingListItemOut): boolean {
|
||||
const index = queue.findIndex(i => i.id === item.id);
|
||||
function isValidQueueObject(obj: any): obj is ShoppingListQueue {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasRequiredProps = "create" in obj && "update" in obj && "delete" in obj && "lastUpdate" in obj;
|
||||
if (!hasRequiredProps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const arraysValid = Array.isArray(obj.create) && Array.isArray(obj.update) && Array.isArray(obj.delete);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const lastUpdateValid = typeof obj.lastUpdate === "number" && !isNaN(new Date(obj.lastUpdate).getTime());
|
||||
|
||||
return arraysValid && lastUpdateValid;
|
||||
}
|
||||
|
||||
function createEmptyQueue(): ShoppingListQueue {
|
||||
const newQueue = { create: [], update: [], delete: [], lastUpdate: Date.now() };
|
||||
return newQueue;
|
||||
}
|
||||
|
||||
function getQueue(): ShoppingListQueue {
|
||||
try {
|
||||
const fetchedQueue = storage.value[shoppingListId];
|
||||
if (!isValidQueueObject(fetchedQueue)) {
|
||||
console.log("Invalid queue object in local storage; resetting queue.");
|
||||
return createEmptyQueue();
|
||||
} else {
|
||||
return fetchedQueue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error validating queue object in local storage; resetting queue.", error);
|
||||
return createEmptyQueue();
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromQueue(itemQueue: ShoppingListItemOut[], item: ShoppingListItemOut): boolean {
|
||||
const index = itemQueue.findIndex(i => i.id === item.id);
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
queue.splice(index, 1);
|
||||
itemQueue.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
const response = await api.shopping.lists.getOne(shoppingListId);
|
||||
handleResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -90,44 +137,59 @@ export function useShoppingListItemActions(shoppingListId: string) {
|
||||
if (itemQueueType === "delete" || itemQueueType === "all") {
|
||||
queue.delete = itemIds ? queue.delete.filter(item => !itemIds.includes(item.id)) : [];
|
||||
}
|
||||
if (queueEmpty.value) {
|
||||
queue.lastUpdate = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// Set the storage value explicitly so changes are saved in the browser.
|
||||
storage.value[shoppingListId] = { ...queue };
|
||||
function checkUpdateState(list: ShoppingListOut) {
|
||||
const cutoffDate = new Date(queue.lastUpdate + queueTimeout).toISOString();
|
||||
if (list.updateAt && list.updateAt > cutoffDate) {
|
||||
// If the queue is too far behind the shopping list to reliably do updates, we clear the queue
|
||||
console.log("Out of sync with server; clearing queue");
|
||||
clearQueueItems("all");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the response from the backend and sets the isOffline flag if necessary.
|
||||
* Processes the queue items and returns whether the processing was successful.
|
||||
*/
|
||||
function handleResponse(response: any) {
|
||||
// TODO: is there a better way of checking for network errors?
|
||||
isOffline.value = response.error?.message?.includes("Network Error") || false;
|
||||
}
|
||||
|
||||
async function processQueueItems(
|
||||
action: (items: ShoppingListItemOut[]) => Promise<any>,
|
||||
action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>,
|
||||
itemQueueType: ItemQueueType,
|
||||
) {
|
||||
const queueItems = getQueueItems(itemQueueType);
|
||||
if (!queueItems.length) {
|
||||
return;
|
||||
): Promise<boolean> {
|
||||
let queueItems: ShoppingListItemOut[];
|
||||
try {
|
||||
queueItems = getQueueItems(itemQueueType);
|
||||
if (!queueItems.length) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error fetching queue items of type ${itemQueueType}:`, error);
|
||||
clearQueueItems(itemQueueType);
|
||||
return false;
|
||||
}
|
||||
|
||||
const itemsToProcess = [...queueItems];
|
||||
await action(itemsToProcess)
|
||||
.then((response) => {
|
||||
handleResponse(response);
|
||||
if (!isOffline.value) {
|
||||
clearQueueItems(itemQueueType, itemsToProcess.map(item => item.id));
|
||||
}
|
||||
});
|
||||
try {
|
||||
const itemsToProcess = [...queueItems];
|
||||
await action(itemsToProcess)
|
||||
.then(() => {
|
||||
if (window.$nuxt.isOnline) {
|
||||
clearQueueItems(itemQueueType, itemsToProcess.map(item => item.id));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Error processing queue items of type ${itemQueueType}:`, error);
|
||||
clearQueueItems(itemQueueType);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function process() {
|
||||
if(
|
||||
!queue.create.length &&
|
||||
!queue.update.length &&
|
||||
!queue.delete.length
|
||||
) {
|
||||
if(queueEmpty.value) {
|
||||
queue.lastUpdate = Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,26 +197,23 @@ export function useShoppingListItemActions(shoppingListId: string) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
checkUpdateState(data);
|
||||
|
||||
const cutoffDate = new Date(queue.lastUpdate + queueTimeout).toISOString();
|
||||
if (data.updateAt && data.updateAt > cutoffDate) {
|
||||
// If the queue is too far behind the shopping list to reliably do updates, we clear the queue
|
||||
clearQueueItems("all");
|
||||
} else {
|
||||
// We send each bulk request one at a time, since the backend may merge items
|
||||
await processQueueItems((items) => api.shopping.items.deleteMany(items), "delete");
|
||||
await processQueueItems((items) => api.shopping.items.updateMany(items), "update");
|
||||
await processQueueItems((items) => api.shopping.items.createMany(items), "create");
|
||||
}
|
||||
// We send each bulk request one at a time, since the backend may merge items
|
||||
// "failures" here refers to an actual error, rather than failing to reach the backend
|
||||
let failures = 0;
|
||||
if (!(await processQueueItems((items) => api.shopping.items.deleteMany(items), "delete"))) failures++;
|
||||
if (!(await processQueueItems((items) => api.shopping.items.updateMany(items), "update"))) failures++;
|
||||
if (!(await processQueueItems((items) => api.shopping.items.createMany(items), "create"))) failures++;
|
||||
|
||||
// If we're online, the queue is fully processed, so we're up to date
|
||||
if (!isOffline.value) {
|
||||
// If we're online, or the queue is empty, the queue is fully processed, so we're up to date
|
||||
// Otherwise, if all three queue processes failed, we've already reset the queue, so we need to reset the date
|
||||
if (window.$nuxt.isOnline || queueEmpty.value || failures === 3) {
|
||||
queue.lastUpdate = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isOffline,
|
||||
getList,
|
||||
createItem,
|
||||
updateItem,
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissies",
|
||||
"administrator": "Administrateur",
|
||||
"user-can-invite-other-to-group": "Gebruiker kan ander na groep nooi",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Gebruiker kan groep bestuur",
|
||||
"user-can-organize-group-data": "Gebruiker kan groepdata organiseer",
|
||||
"enable-advanced-features": "Aktiveer gevorderde funksies",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Права",
|
||||
"administrator": "Администратор",
|
||||
"user-can-invite-other-to-group": "Потребителя може да добавя други в групата",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Потребителя може да управлява групата",
|
||||
"user-can-organize-group-data": "Потребителя може да организира данните на групата",
|
||||
"enable-advanced-features": "Включване на разширени функции",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permisos",
|
||||
"administrator": "Administrador",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -145,11 +145,11 @@
|
||||
"save": "Uložit",
|
||||
"settings": "Nastavení",
|
||||
"share": "Sdílet",
|
||||
"show-all": "Show All",
|
||||
"show-all": "Zobrazit vše",
|
||||
"shuffle": "Náhodně",
|
||||
"sort": "Seřadit",
|
||||
"sort-ascending": "Sort Ascending",
|
||||
"sort-descending": "Sort Descending",
|
||||
"sort-ascending": "Řadit vzestupně",
|
||||
"sort-descending": "Řadit sestupně",
|
||||
"sort-alphabetically": "Abecedně",
|
||||
"status": "Stav",
|
||||
"subject": "Předmět",
|
||||
@@ -161,7 +161,7 @@
|
||||
"test": "Test",
|
||||
"themes": "Motivy",
|
||||
"thursday": "Čtvrtek",
|
||||
"title": "Title",
|
||||
"title": "Název",
|
||||
"token": "Token",
|
||||
"tuesday": "Úterý",
|
||||
"type": "Typ",
|
||||
@@ -208,7 +208,7 @@
|
||||
"upload-file": "Nahrát soubor",
|
||||
"created-on-date": "Vytvořeno dne: {0}",
|
||||
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
|
||||
"clipboard-copy-failure": "Failed to copy to the clipboard.",
|
||||
"clipboard-copy-failure": "Zkopírování do schránky se nezdařilo.",
|
||||
"confirm-delete-generic-items": "Are you sure you want to delete the following items?",
|
||||
"organizers": "Organizers",
|
||||
"caution": "Caution"
|
||||
@@ -247,7 +247,7 @@
|
||||
"group-preferences": "Nastavení skupiny",
|
||||
"private-group": "Soukromá skupina",
|
||||
"private-group-description": "Setting your group to private will default all public view options to default. This overrides an individual recipes public view settings.",
|
||||
"enable-public-access": "Enable Public Access",
|
||||
"enable-public-access": "Povolit veřejný přístup",
|
||||
"enable-public-access-description": "Make group recipes public by default, and allow visitors to view recipes without logging-in",
|
||||
"allow-users-outside-of-your-group-to-see-your-recipes": "Povolit uživatelům mimo vaši skupinu vidět vaše recepty",
|
||||
"allow-users-outside-of-your-group-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your group or with a pre-generated private link",
|
||||
@@ -794,10 +794,10 @@
|
||||
"food": "Jídlo",
|
||||
"note": "Poznámka",
|
||||
"label": "Popisek",
|
||||
"save-label": "Save Label",
|
||||
"save-label": "Uložit štítek",
|
||||
"linked-item-warning": "Tato položka je propojena s jedním nebo více recepty. Úprava jednotky nebo jídla bude mít neočekávané důsledky při přidání nebo odebrání receptu z tohoto seznamu.",
|
||||
"toggle-food": "Přepnout typ položky",
|
||||
"manage-labels": "Manage Labels",
|
||||
"manage-labels": "Spravovat štítky",
|
||||
"are-you-sure-you-want-to-delete-this-item": "Are you sure you want to delete this item?",
|
||||
"copy-as-text": "Copy as Text",
|
||||
"copy-as-markdown": "Copy as Markdown",
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
@@ -1018,9 +1018,9 @@
|
||||
"labels": {
|
||||
"seed-dialog-text": "Naplnit databázi s běžnými popisky používanými ve vašem jazyce.",
|
||||
"edit-label": "Upravit štítek",
|
||||
"new-label": "New Label",
|
||||
"new-label": "Nový štítek",
|
||||
"labels": "Štítky",
|
||||
"assign-label": "Assign Label"
|
||||
"assign-label": "Přiřadit štítek"
|
||||
},
|
||||
"recipes": {
|
||||
"purge-exports": "Purge Exports",
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
"clipboard-copy-failure": "Kopiering til udklipsholderen mislykkedes.",
|
||||
"confirm-delete-generic-items": "Er du sikker på at du ønsker at slette de valgte emner?",
|
||||
"organizers": "Organisatorer",
|
||||
"caution": "Caution"
|
||||
"caution": "Bemærk"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Er du sikker på, du vil slette <b>{groupName}<b/>?",
|
||||
@@ -292,8 +292,8 @@
|
||||
"mealplan-updated": "Madplanen blev ændret",
|
||||
"no-meal-plan-defined-yet": "Ingen madplan er defineret",
|
||||
"no-meal-planned-for-today": "Ingen ret er planlagt til i dag",
|
||||
"numberOfDays-hint": "Number of days on page load",
|
||||
"numberOfDays-label": "Default Days",
|
||||
"numberOfDays-hint": "Antal dage ved sideindlæsning",
|
||||
"numberOfDays-label": "Standarddage",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Kun opskrifter med disse kategorier vil blive brugt i madplaner",
|
||||
"planner": "Planlæg madplan",
|
||||
"quick-week": "Hurtig uge",
|
||||
@@ -383,7 +383,7 @@
|
||||
},
|
||||
"recipekeeper": {
|
||||
"title": "Recipe Keeper",
|
||||
"description-long": "Mealie can import recipes from Recipe Keeper. Export your recipes in zip format, then upload the .zip file below."
|
||||
"description-long": "Mealie kan importere opskrifter fra Recipe Keeper. Eksportér dine opskrifter i zip-format, og upload derefter .zip-filen nedenfor."
|
||||
}
|
||||
},
|
||||
"new-recipe": {
|
||||
@@ -449,8 +449,8 @@
|
||||
"ingredients": "Ingredienser",
|
||||
"insert-ingredient": "Indsæt Ingrediens",
|
||||
"insert-section": "Indsæt sektion",
|
||||
"insert-above": "Insert Above",
|
||||
"insert-below": "Insert Below",
|
||||
"insert-above": "Indsæt ovenover",
|
||||
"insert-below": "Indsæt nedenunder",
|
||||
"instructions": "Instruktioner",
|
||||
"key-name-required": "Nøglenavn påkrævet",
|
||||
"landscape-view-coming-soon": "Liggende visning (Kommer snart)",
|
||||
@@ -586,8 +586,8 @@
|
||||
"report-deletion-failed": "Sletning af rapport mislykkedes",
|
||||
"recipe-debugger": "Fejlsøgning af opskrifter",
|
||||
"recipe-debugger-description": "Indsæt URL'en på hjemmesiden, der indeholder den opskrift, du vil fejlsøge. URL-adressen vil blive læst og resultaterne vil blive vist. Hvis ingen data bliver vist, er indhentning af opskrifter fra hjemmesiden endnu ikke understøttet af Mealie.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"use-openai": "Brug OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Brug OpenAI til at fortolke resultaterne i stedet for at stole på scraper biblioteket. Når du opretter en opskrift via URL, gøres dette automatisk, hvis skraberbiblioteket fejler, men du kan teste det manuelt her.",
|
||||
"debug": "Fejlsøgning",
|
||||
"tree-view": "Træ visning",
|
||||
"recipe-yield": "Udbytte af opskrift",
|
||||
@@ -640,7 +640,7 @@
|
||||
"backup-created-at-response-export_path": "Backup oprettet ved {path}",
|
||||
"backup-deleted": "Backup slettet",
|
||||
"restore-success": "Gendannelse lykkedes",
|
||||
"restore-fail": "Restore failed. Check your server logs for more details",
|
||||
"restore-fail": "Gendannelse mislykkedes. Tjek dine serverlogs for flere detaljer",
|
||||
"backup-tag": "Backupnavn",
|
||||
"create-heading": "Opret en backup",
|
||||
"delete-backup": "Slet backup",
|
||||
@@ -810,11 +810,11 @@
|
||||
"items-checked-count": "Ingen elementer markeret|Et element markeret|{count} elementer er markeret",
|
||||
"no-label": "Ingen etiket",
|
||||
"completed-on": "Afsluttet den {date}",
|
||||
"you-are-offline": "You are offline",
|
||||
"you-are-offline-description": "Not all features are available while offline. You can still add, modify, and remove items, but you will not be able to sync your changes to the server until you are back online.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Are you sure you want to check all items?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?"
|
||||
"you-are-offline": "Du er offline",
|
||||
"you-are-offline-description": "Ikke alle funktioner er tilgængelige mens offline. Du kan stadig tilføje, modificere, og fjerne elementer, men du vil ikke kunne synkronisere dine ændringer til serveren, før du er online igen.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Er du sikker på, at du vil markere alle elementer?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Er du sikker på, at du vil fjerne markeringen af alle elementer?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Er du sikker på, at du vil sletter de valgte elementer?"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Alle opskr.",
|
||||
@@ -955,7 +955,7 @@
|
||||
"user-details": "Brugerdetaljer",
|
||||
"user-name": "Brugernavn",
|
||||
"authentication-method": "Godkendelsesmetode",
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"authentication-method-hint": "Dette angiver, hvordan en bruger vil logge ind på Mealie. Hvis du ikke er sikker, vælg 'Mealie'",
|
||||
"permissions": "Rettigheder",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "Bruger kan invitere andre til gruppen",
|
||||
@@ -990,8 +990,8 @@
|
||||
"food-data": "Oplysninger om fødevare",
|
||||
"example-food-singular": "fx.: grøntsag",
|
||||
"example-food-plural": "fx.: grøntsager",
|
||||
"label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels.",
|
||||
"on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list."
|
||||
"label-overwrite-warning": "Dette vil tildele den valgte etiket til alle udvalgte fødevarer og potentielt overskrive dine eksisterende etiketter.",
|
||||
"on-hand-checkbox-label": "Ændring af dette flag ændrer markeringen, så denne fødevare ikke er markeret som standard, når du tilføjer en opskrift til en indkøbsliste."
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "Opret standard enheder i dit sprog.",
|
||||
@@ -1020,7 +1020,7 @@
|
||||
"edit-label": "Redigér etiket",
|
||||
"new-label": "Ny etiket",
|
||||
"labels": "Etiketter",
|
||||
"assign-label": "Assign Label"
|
||||
"assign-label": "Tildel etiket"
|
||||
},
|
||||
"recipes": {
|
||||
"purge-exports": "Tøm Eksport",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Αυτό καθορίζει τον τρόπο με τον οποίο ένας χρήστης θα ταυτοποιηθεί με το Mealie. Αν δεν είστε σίγουροι, επιλέξτε 'Mealie'",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "Ο χρήστης μπορεί να προσκαλέσει άλλους στην ομάδα",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Ενεργοποίηση χαρακτηριστικών για προχωρημένους",
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
"group-management": "Group Management",
|
||||
"admin-group-management": "Admin Group Management",
|
||||
"admin-group-management-text": "Changes to this group will be reflected immediately.",
|
||||
"group-id-value": "Group Id: {0}"
|
||||
"group-id-value": "Group ID: {0}"
|
||||
},
|
||||
"meal-plan": {
|
||||
"create-a-new-meal-plan": "Create a New Meal Plan",
|
||||
@@ -283,18 +283,18 @@
|
||||
"meal-planner": "Meal Planner",
|
||||
"meal-plans": "Meal Plans",
|
||||
"mealplan-categories": "MEALPLAN CATEGORIES",
|
||||
"mealplan-created": "Mealplan created",
|
||||
"mealplan-creation-failed": "Mealplan creation failed",
|
||||
"mealplan-created": "Meal plan created",
|
||||
"mealplan-creation-failed": "Meal plan creation failed",
|
||||
"mealplan-deleted": "Mealplan deleted",
|
||||
"mealplan-deletion-failed": "Mealplan deletion failed",
|
||||
"mealplan-settings": "Mealplan Settings",
|
||||
"mealplan-update-failed": "Mealplan update failed",
|
||||
"mealplan-updated": "Mealplan Updated",
|
||||
"mealplan-deletion-failed": "Meal plan deletion failed",
|
||||
"mealplan-settings": "Meal plan settings",
|
||||
"mealplan-update-failed": "Meal plan update failed",
|
||||
"mealplan-updated": "Meal plan updated",
|
||||
"no-meal-plan-defined-yet": "No meal plan defined yet",
|
||||
"no-meal-planned-for-today": "No meal planned for today",
|
||||
"numberOfDays-hint": "Number of days on page load",
|
||||
"numberOfDays-label": "Default Days",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Only recipes with these categories will be used in Meal Plans",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Only the recipes with these categories will be used in Meal Plans",
|
||||
"planner": "Planner",
|
||||
"quick-week": "Quick Week",
|
||||
"side": "Side",
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie'",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie'",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permisos",
|
||||
"administrator": "Administrador",
|
||||
"user-can-invite-other-to-group": "El usuario puede invitar a otros al grupo",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "El usuario puede administrar el grupo",
|
||||
"user-can-organize-group-data": "El usuario puede organizar los datos del grupo",
|
||||
"enable-advanced-features": "Habilitar Características Avanzadas",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Tämä määrittelee, miten käyttäjä todentaa Mealien. Jos et ole varma, valitse 'Mealie'",
|
||||
"permissions": "Käyttöoikeudet",
|
||||
"administrator": "Ylläpitäjä",
|
||||
"user-can-invite-other-to-group": "Käyttäjä voi kutsua muita ryhmään",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Käyttäjä voi hallita ryhmää",
|
||||
"user-can-organize-group-data": "Käyttäjä voi järjestellä ryhmän tietoja",
|
||||
"enable-advanced-features": "Salli edistyneemmät ominaisuudet",
|
||||
|
||||
@@ -810,8 +810,8 @@
|
||||
"items-checked-count": "Aucun élément coché|Un élément coché|{count} éléments cochés",
|
||||
"no-label": "Aucune étiquette",
|
||||
"completed-on": "Terminé le {date}",
|
||||
"you-are-offline": "You are offline",
|
||||
"you-are-offline-description": "Not all features are available while offline. You can still add, modify, and remove items, but you will not be able to sync your changes to the server until you are back online.",
|
||||
"you-are-offline": "Vous êtes hors-ligne",
|
||||
"you-are-offline-description": "Certaines fonctionnalités ne sont pas disponibles lorsque vous êtes hors-ligne. Vous pouvez toujours ajouter, modifier et supprimer des éléments, mais il ne sera pas possible de synchroniser les changements avec le serveur tant que vous ne serez pas en ligne.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Voulez-vous vraiment sélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Voulez-vous vraiment désélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Voulez-vous vraiment supprimer tous les éléments sélectionnés ?"
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Ceci infique comment un utilisateur va s'authentifier sur Mealie. Si vous n'êtes pas sûr, choisissez 'Mealie'",
|
||||
"permissions": "Autorisations",
|
||||
"administrator": "Administrateur",
|
||||
"user-can-invite-other-to-group": "L'utilisateur peut inviter quelqu'un au groupe",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "L'utilisateur peut gérer le groupe",
|
||||
"user-can-organize-group-data": "L'utilisateur peut organiser des données de groupe",
|
||||
"enable-advanced-features": "Activer les fonctions avancées",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Ceci infique comment un utilisateur va s'authentifier sur Mealie. Si vous n'êtes pas sûr, choisissez 'Mealie'",
|
||||
"permissions": "Autorisations",
|
||||
"administrator": "Administrateur",
|
||||
"user-can-invite-other-to-group": "L'utilisateur peut inviter quelqu'un au groupe",
|
||||
"user-can-invite-other-to-group": "L’utilisateur peut inviter d’autres personnes dans le groupe",
|
||||
"user-can-manage-group": "L'utilisateur peut gérer le groupe",
|
||||
"user-can-organize-group-data": "L'utilisateur peut organiser des données de groupe",
|
||||
"enable-advanced-features": "Activer les fonctions avancées",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "זה מציין איך משתמש יתחבר ל״מילי״. אם אתה לא בטוח, בחר מילי",
|
||||
"permissions": "הרשאות",
|
||||
"administrator": "מנהל ראשי",
|
||||
"user-can-invite-other-to-group": "משתמש יכול להזמין אחרים לקבוצה",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "משתמש יכול לנהל קבוצה",
|
||||
"user-can-organize-group-data": "משתמש יכול לשנות מידע של קבוצה",
|
||||
"enable-advanced-features": "אפשר אפשרויות מתקדמות",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Ovo određuje način autentifikacije korisnika u Mealie sustavu. Ako niste sigurni, odaberite 'Mealie",
|
||||
"permissions": "Dozvole",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "Korisnik može pozvati druge u grupu",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Korisnik može upravljati grupom",
|
||||
"user-can-organize-group-data": "Korisnik može organizirati podatke grupe",
|
||||
"enable-advanced-features": "Omogućite napredne značajke",
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"screen-awake": "Képernyő ébren tartása",
|
||||
"remove-image": "Kép etávolítása",
|
||||
"nextStep": "Következő lépés",
|
||||
"recipe-actions": "Recipe Actions",
|
||||
"recipe-actions": "Receptekkel kapcsolatos tevékenységek",
|
||||
"parser": {
|
||||
"experimental-alert-text": "A Mealie természetes nyelvi feldolgozást használ a recept összetevőinek elemzésére, az egységek és az élelmiszerelemek létrehozására. Ez a funkció kísérleti jellegű, és előfordulhat, hogy nem mindig működik az elvárt módon. Ha nem szeretné használni az elemzett eredményeket, válassza a 'Mégse' lehetőséget, és a módosítások nem kerülnek mentésre.",
|
||||
"ingredient-parser": "Hozzávaló elemző",
|
||||
@@ -810,11 +810,11 @@
|
||||
"items-checked-count": "Nincs ellenőrzött tétel|Egy ellenőrzött tétel|{count} ellenőrzött tétel",
|
||||
"no-label": "Nincs címke",
|
||||
"completed-on": "Teljesítve {date}",
|
||||
"you-are-offline": "You are offline",
|
||||
"you-are-offline-description": "Not all features are available while offline. You can still add, modify, and remove items, but you will not be able to sync your changes to the server until you are back online.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Are you sure you want to check all items?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?"
|
||||
"you-are-offline": "Offline vagy",
|
||||
"you-are-offline-description": "Offline állapotban nem minden funkció érhető el. Továbbra is hozzáadhat, módosíthat és eltávolíthat elemeket, de a módosításokat nem tudja szinkronizálni a szerverrel, amíg vissza nem tér az online állapotba.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Biztos, hogy minden elemet be akar jelölni?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Biztos, hogy minden elem kijelölését visszavonja?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Biztosan törölni akarja az összes bejelölt elemet?"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Minden recept",
|
||||
@@ -991,7 +991,7 @@
|
||||
"example-food-singular": "pl. Hagyma",
|
||||
"example-food-plural": "pl. Hagymák",
|
||||
"label-overwrite-warning": "Ez a kiválasztott címkét az összes kiválasztott élelmiszerhez hozzárendeli, és esetleg felülírja a meglévő címkéket.",
|
||||
"on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list."
|
||||
"on-hand-checkbox-label": "Ha ezt a jelzőt beállítja, akkor ez az élelmiszer alapértelmezés szerint nem lesz bejelölve, amikor egy receptet hozzáad egy bevásárlólistához."
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "Töltse be az Ön nyelve szerinti közös mennyiségi egységet tartalmazó adatbázist.",
|
||||
@@ -1044,9 +1044,9 @@
|
||||
"source-unit-will-be-deleted": "A forrás mennyiségi egység törlésre kerül"
|
||||
},
|
||||
"recipe-actions": {
|
||||
"recipe-actions-data": "Recipe Actions Data",
|
||||
"new-recipe-action": "New Recipe Action",
|
||||
"edit-recipe-action": "Edit Recipe Action",
|
||||
"recipe-actions-data": "Receptekkel kapcsolatos tevékenységek adatai",
|
||||
"new-recipe-action": "Új recept tevékenység",
|
||||
"edit-recipe-action": "Recept tevékenység szerkesztése",
|
||||
"action-type": "Művelet típusa"
|
||||
},
|
||||
"create-alias": "Alias készítése",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie'",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -587,7 +587,7 @@
|
||||
"recipe-debugger": "Debugger Ricetta",
|
||||
"recipe-debugger-description": "Prendi l'URL della ricetta che vuoi fare il debug e incollalo qui. L'URL verrà recuperato dallo scraper di ricette e i risultati verranno visualizzati. Se non si vede alcun dato restituito, il sito che si sta cercando di analizzare non è supportato da Mealie o la sua libreria di scraping.",
|
||||
"use-openai": "Usa OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"recipe-debugger-use-openai-description": "Usa OpenAI per analizzare i risultati invece di affidarsi alla libreria scraper. Quando si crea una ricetta tramite URL, questo viene fatto automaticamente se la libreria scraper fallisce, ma è possibile testarlo manualmente qui.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Visualizzazione ad Albero",
|
||||
"recipe-yield": "Resa Ricetta",
|
||||
@@ -596,7 +596,7 @@
|
||||
"screen-awake": "Mantieni lo schermo acceso",
|
||||
"remove-image": "Rimuovi immagine",
|
||||
"nextStep": "Passo successivo",
|
||||
"recipe-actions": "Recipe Actions",
|
||||
"recipe-actions": "Azioni Ricetta",
|
||||
"parser": {
|
||||
"experimental-alert-text": "Mealie utilizza l'elaborazione del linguaggio naturale per analizzare e creare unità e prodotti alimentari per i vostri ingredienti di ricetta. Questa funzione è sperimentale e potrebbe non funzionare sempre come previsto. Se preferisci non usare i risultati analizzati, puoi selezionare 'Annulla' e le tue modifiche non saranno salvate.",
|
||||
"ingredient-parser": "Analizzatore ingredienti",
|
||||
@@ -605,7 +605,7 @@
|
||||
"select-parser": "Seleziona Analizzatore",
|
||||
"natural-language-processor": "Analizzatore di Linguaggio Naturale",
|
||||
"brute-parser": "Analizzatore brutale",
|
||||
"openai-parser": "OpenAI Parser",
|
||||
"openai-parser": "Parser OpenAI",
|
||||
"parse-all": "Analizza tutto",
|
||||
"no-unit": "Nessuna unità",
|
||||
"missing-unit": "Crea unità mancante: {unit}",
|
||||
@@ -640,7 +640,7 @@
|
||||
"backup-created-at-response-export_path": "Backup Creato in {path}",
|
||||
"backup-deleted": "Backup eliminato",
|
||||
"restore-success": "Ripristino riuscito",
|
||||
"restore-fail": "Restore failed. Check your server logs for more details",
|
||||
"restore-fail": "Ripristino non riuscito. Controlla i log del tuo server per maggiori dettagli",
|
||||
"backup-tag": "Tag Backup",
|
||||
"create-heading": "Crea un Backup",
|
||||
"delete-backup": "Elimina Backup",
|
||||
@@ -778,9 +778,9 @@
|
||||
"oidc-ready": "Pronto per OIDC",
|
||||
"oidc-ready-error-text": "I valori OIDC non sono configurati. Questo può essere ignorato se non si utilizza Autenticazione OIDC.",
|
||||
"oidc-ready-success-text": "Le variabili OIDC richieste sono tutte impostate.",
|
||||
"openai-ready": "OpenAI Ready",
|
||||
"openai-ready-error-text": "Not all OpenAI Values are configured. This can be ignored if you are not using OpenAI features.",
|
||||
"openai-ready-success-text": "Required OpenAI variables are all set."
|
||||
"openai-ready": "OpenAI Pronto",
|
||||
"openai-ready-error-text": "Non tutti i valori OpenAI sono configurati. Puoi ignorarlo se non utilizzi le funzioni di OpenAI.",
|
||||
"openai-ready-success-text": "Le variabili OpenAI richieste sono tutte impostate."
|
||||
},
|
||||
"shopping-list": {
|
||||
"all-lists": "Tutte le Liste",
|
||||
@@ -794,7 +794,7 @@
|
||||
"food": "Alimenti",
|
||||
"note": "Nota",
|
||||
"label": "Etichetta",
|
||||
"save-label": "Save Label",
|
||||
"save-label": "Salva Etichetta",
|
||||
"linked-item-warning": "Questo elemento è collegato a una o più ricette. La modifica delle unità o degli alimenti potrebbe dare risultati inattesi quando si aggiunge o si rimuove la ricetta da questo elenco.",
|
||||
"toggle-food": "Attiva/Disattiva Alimento",
|
||||
"manage-labels": "Gestisci Etichette",
|
||||
@@ -810,11 +810,11 @@
|
||||
"items-checked-count": "Nessun elemento selezionato|Un elemento selezionato|{count} elementi selezionati",
|
||||
"no-label": "Nessuna etichetta",
|
||||
"completed-on": "Completato il {date}",
|
||||
"you-are-offline": "You are offline",
|
||||
"you-are-offline-description": "Not all features are available while offline. You can still add, modify, and remove items, but you will not be able to sync your changes to the server until you are back online.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Are you sure you want to check all items?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?"
|
||||
"you-are-offline": "Non sei in linea",
|
||||
"you-are-offline-description": "Non tutte le funzioni sono disponibili quando non sei in linea. Puoi ancora aggiungere, modificare e rimuovere elementi, ma non sarai in grado di sincronizzare le modifiche con il server fino a quando non sarai di nuovo in linea.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Sei sicuro di voler tutti gli elementi?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Sei sicuro di voler deselezionare tutti gli elementi?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Sei sicuro di voler rimuovere tutti gli articoli selezionati?"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Ricette",
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Indica come un utente si autenticherà con Mealie. Se non sei sicuro, scegli 'Mealie",
|
||||
"permissions": "Permessi",
|
||||
"administrator": "Amministratore",
|
||||
"user-can-invite-other-to-group": "L'utente può invitare altri al gruppo",
|
||||
"user-can-invite-other-to-group": "L'utente può invitare altre persone nel gruppo",
|
||||
"user-can-manage-group": "L'utente può gestire il gruppo",
|
||||
"user-can-organize-group-data": "L'utente può organizzare i dati del gruppo",
|
||||
"enable-advanced-features": "Abilita funzionalità avanzate",
|
||||
@@ -990,8 +990,8 @@
|
||||
"food-data": "Dati Alimento",
|
||||
"example-food-singular": "esempio: Cipolla",
|
||||
"example-food-plural": "esempio: Cipolle",
|
||||
"label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels.",
|
||||
"on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list."
|
||||
"label-overwrite-warning": "Questo assegnerà l'etichetta scelta a tutti gli alimenti selezionati e potenzialmente sovrascriverà le etichette esistenti.",
|
||||
"on-hand-checkbox-label": "Abilitando questa impostazione, l'alimento verrà deselezionato di default quando si aggiungerà una ricetta a una lista della spesa."
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "Riempie il database con unità comuni basate sulla lingua.",
|
||||
@@ -1020,7 +1020,7 @@
|
||||
"edit-label": "Modifica Etichetta",
|
||||
"new-label": "Nuova Etichetta",
|
||||
"labels": "Etichette",
|
||||
"assign-label": "Assign Label"
|
||||
"assign-label": "Assegna Etichetta"
|
||||
},
|
||||
"recipes": {
|
||||
"purge-exports": "Elimina Esportazioni",
|
||||
@@ -1044,10 +1044,10 @@
|
||||
"source-unit-will-be-deleted": "L'unità di origine verrà eliminata"
|
||||
},
|
||||
"recipe-actions": {
|
||||
"recipe-actions-data": "Recipe Actions Data",
|
||||
"new-recipe-action": "New Recipe Action",
|
||||
"edit-recipe-action": "Edit Recipe Action",
|
||||
"action-type": "Action Type"
|
||||
"recipe-actions-data": "Dati Azioni Ricetta",
|
||||
"new-recipe-action": "Nuova Azione Ricetta",
|
||||
"edit-recipe-action": "Modifica Azione Ricetta",
|
||||
"action-type": "Tipo Di Azione"
|
||||
},
|
||||
"create-alias": "Crea Alias",
|
||||
"manage-aliases": "Gestisci Alias",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "権限",
|
||||
"administrator": "管理者",
|
||||
"user-can-invite-other-to-group": "ユーザーは他のグループに招待できます",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "ユーザーはグループを管理できます",
|
||||
"user-can-organize-group-data": "ユーザーはグループデータを整理できます",
|
||||
"enable-advanced-features": "高度な機能を有効にする",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Leidimai",
|
||||
"administrator": "Administratorius",
|
||||
"user-can-invite-other-to-group": "Naudotojas gali kviesti kitus į grupę",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Naudotojas gali valdyti grupę",
|
||||
"user-can-organize-group-data": "Naudotojas gali tvarkyti grupės duomenis",
|
||||
"enable-advanced-features": "Įjungti pažangias funkcijas",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Dit bepaalt hoe een gebruiker zich aanmeldt bij Mealie. Als je het niet zeker weet, kies dan voor 'Mealie'",
|
||||
"permissions": "Gebruikersrechten",
|
||||
"administrator": "Beheerder",
|
||||
"user-can-invite-other-to-group": "Gebruiker kan iemand uitnodigen voor de groep",
|
||||
"user-can-invite-other-to-group": "Gebruiker kan anderen uitnodigen voor zijn groep",
|
||||
"user-can-manage-group": "Gebruiker kan de groep beheren",
|
||||
"user-can-organize-group-data": "Gebruiker kan groepsgegevens indelen",
|
||||
"enable-advanced-features": "Geavanceerde functies inschakelen",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Rettigheter",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "Brukeren kan invitere andre til gruppe",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Brukeren kan administrere gruppe",
|
||||
"user-can-organize-group-data": "Brukeren kan organisere gruppedata",
|
||||
"enable-advanced-features": "Aktiver avanserte funksjoner",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Określa jak użytkownik będzie uwierzytelniać się z Mealie. Jeśli nie jesteś pewien, wybierz 'Mealie",
|
||||
"permissions": "Uprawnienia",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "Użytkownik może zaprosić innych do grupy",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Użytkownik może zarządzać grupą",
|
||||
"user-can-organize-group-data": "Użytkownik może organizować dane grupy",
|
||||
"enable-advanced-features": "Włącz zaawansowane funkcje",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Isto especifica como um usuário se autentica com o Mealie. Se não tem certeza, escolha \"Mealie\"",
|
||||
"permissions": "Permissões",
|
||||
"administrator": "Administrador",
|
||||
"user-can-invite-other-to-group": "O usuário pode convidar outro para o grupo",
|
||||
"user-can-invite-other-to-group": "O usuário pode convidar outros para o grupo",
|
||||
"user-can-manage-group": "Usuário pode gerenciar o grupo",
|
||||
"user-can-organize-group-data": "Usuário pode organizar dados do grupo",
|
||||
"enable-advanced-features": "Ativar recursos avançados",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissões",
|
||||
"administrator": "Administrador",
|
||||
"user-can-invite-other-to-group": "O utilizador pode convidar outro para o grupo",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "O utilizador pode gerir o grupo",
|
||||
"user-can-organize-group-data": "O utilizador pode organizar dados do grupo",
|
||||
"enable-advanced-features": "Habilitar recursos avançados",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Права доступа",
|
||||
"administrator": "Администратор",
|
||||
"user-can-invite-other-to-group": "Пользователь может пригласить других в группу",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Пользователь может управлять группой",
|
||||
"user-can-organize-group-data": "Пользователь может менять групповые данные",
|
||||
"enable-advanced-features": "Включить доп. функции",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Toto určuje ako bude overený užívateľ. Ak si nie ste istý, zvoľte 'Mealie'",
|
||||
"permissions": "Povolenia",
|
||||
"administrator": "Administrátor",
|
||||
"user-can-invite-other-to-group": "Užívateľ môže do skupiny pozvať ďalších",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Užívateľ môže spravovať skupinu",
|
||||
"user-can-organize-group-data": "Užívateľ môže spravovať údaje skupiny",
|
||||
"enable-advanced-features": "Povoliť pokročilé funkcie",
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
"clipboard-copy-failure": "Kopiranje na odložišče ni bilo uspešno.",
|
||||
"confirm-delete-generic-items": "Ali ste prepričani, da želite izbrisati izbrane elemente?",
|
||||
"organizers": "Organizatorji",
|
||||
"caution": "Caution"
|
||||
"caution": "Pozor"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Ste prepričani, da želite izbrisati <b>{groupName}<b/>?",
|
||||
@@ -449,8 +449,8 @@
|
||||
"ingredients": "Sestavine",
|
||||
"insert-ingredient": "Dodaj sestavino",
|
||||
"insert-section": "Vstavi odsek",
|
||||
"insert-above": "Insert Above",
|
||||
"insert-below": "Insert Below",
|
||||
"insert-above": "Vstavi zgoraj",
|
||||
"insert-below": "Vstavi spodaj",
|
||||
"instructions": "Navodila",
|
||||
"key-name-required": "Obvezen vnos imena ključa",
|
||||
"landscape-view-coming-soon": "Ležeči pogled",
|
||||
@@ -586,7 +586,7 @@
|
||||
"report-deletion-failed": "Brisanje poročila ni bilo uspešno",
|
||||
"recipe-debugger": "Odpravljanje težav v strganju recepta",
|
||||
"recipe-debugger-description": "Prilepi povezavo do recepta, ki ga želiš postrgati. Strgalnik receptov bo postrgal spletno stran in prikazal rezultate. Če ne vidiš nobenih podatkov, potem spletna stran, ki jo želiš strgati ni podprta v Mealie oz. v strgalniku, ki ga uporablja.",
|
||||
"use-openai": "Use OpenAI",
|
||||
"use-openai": "Uporabi OpenAI",
|
||||
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
|
||||
"debug": "Debug",
|
||||
"tree-view": "Drevesni prikaz",
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Dovoljenja",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "Uporabnik lahko druge povabi v skupino",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Uporabnik lahko upravlja s skupino",
|
||||
"user-can-organize-group-data": "Uporabnik lahko organizira podatke skupine",
|
||||
"enable-advanced-features": "Vključi napredne funkcije",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Ово одређује како ће се корисник аутентификовати са Mили. Ако нисте сигурни, изаберите 'Mealie'",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
"clipboard-copy-failure": "Det gick inte att kopiera till urklipp.",
|
||||
"confirm-delete-generic-items": "Är du säker på att du vill radera följande objekt?",
|
||||
"organizers": "Organisatörer",
|
||||
"caution": "Caution"
|
||||
"caution": "Varning"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Är du säker på att du vill radera <b>{groupName}<b/>?",
|
||||
@@ -449,8 +449,8 @@
|
||||
"ingredients": "Ingredienser",
|
||||
"insert-ingredient": "Infoga ingrediens",
|
||||
"insert-section": "Infoga avdelning",
|
||||
"insert-above": "Insert Above",
|
||||
"insert-below": "Insert Below",
|
||||
"insert-above": "Infoga Ovan",
|
||||
"insert-below": "Infoga Nedan",
|
||||
"instructions": "Instruktioner",
|
||||
"key-name-required": "Nyckelnamn krävs",
|
||||
"landscape-view-coming-soon": "Landskapsvy (kommer snart)",
|
||||
@@ -810,11 +810,11 @@
|
||||
"items-checked-count": "Inga artiklar markerade|En artikel markerad|{count} artiklar markerade",
|
||||
"no-label": "Ingen etikett",
|
||||
"completed-on": "Slutförd på {date}",
|
||||
"you-are-offline": "You are offline",
|
||||
"you-are-offline-description": "Not all features are available while offline. You can still add, modify, and remove items, but you will not be able to sync your changes to the server until you are back online.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Are you sure you want to check all items?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?"
|
||||
"you-are-offline": "Du är offline",
|
||||
"you-are-offline-description": "Alla funktioner är inte tillgängliga när du är offline. Du kan fortfarande lägga till, ändra och ta bort objekt, men du kommer ej kunna synka dina ändringar till servern förrän du är online igen.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Är du säker på att du vill markera alla objekt?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Är du säker på att du vill avmarkera alla objekt?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Är du säker på att du vill ta bort alla markerade objekt?"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Recept",
|
||||
@@ -990,8 +990,8 @@
|
||||
"food-data": "Mat data",
|
||||
"example-food-singular": "ex: Lök",
|
||||
"example-food-plural": "ex: Lökar",
|
||||
"label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels.",
|
||||
"on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list."
|
||||
"label-overwrite-warning": "Detta kommer att tilldela den valda etiketten till alla utvalda måltider och potentiellt skriva över dina befintliga etiketter.",
|
||||
"on-hand-checkbox-label": "Om du ställer in den här flaggan kommer de här livsmedlen att avmarkeras som standard när du lägger till ett recept i en inköpslista."
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "Fyll databasen med vanliga enheter baserade på ditt språk.",
|
||||
@@ -1020,7 +1020,7 @@
|
||||
"edit-label": "Redigera etikett",
|
||||
"new-label": "Ny etikett",
|
||||
"labels": "Etiketter",
|
||||
"assign-label": "Assign Label"
|
||||
"assign-label": "Tilldela etikett"
|
||||
},
|
||||
"recipes": {
|
||||
"purge-exports": "Rensa exporter",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "Bu, bir kullanıcının Mealie ile nasıl kimlik doğrulaması yapacağını belirtir. Emin değilseniz, 'Mealie'yi seçin",
|
||||
"permissions": "İzinler",
|
||||
"administrator": "Yönetici",
|
||||
"user-can-invite-other-to-group": "Kullanıcı başkalarını gruba davet edebilir",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "Kullanıcı grubu yönetebilir",
|
||||
"user-can-organize-group-data": "Kullanıcı grup verilerini düzenleyebilir",
|
||||
"enable-advanced-features": "Gelişmiş özellikleri etkinleştir",
|
||||
|
||||
@@ -804,17 +804,17 @@
|
||||
"delete-checked": "Видалити відмічене",
|
||||
"toggle-label-sort": "Сортування міток",
|
||||
"reorder-labels": "Перевпорядкувати мітки",
|
||||
"uncheck-all-items": "Зняти вибір з усіх елементів",
|
||||
"check-all-items": "Вибрати всі елементи",
|
||||
"uncheck-all-items": "Зняти відмітку з усіх елементів",
|
||||
"check-all-items": "Відмітити всі елементи",
|
||||
"linked-recipes-count": "Нема пов'язаних рецептів|Один пов'язаний рецепт|{count} пов'язаних рецептів",
|
||||
"items-checked-count": "Нема відмічених елементів|Один відмічений елемент|{count} елементів відмічено",
|
||||
"no-label": "Без Мітки",
|
||||
"completed-on": "Завершено {date}",
|
||||
"you-are-offline": "You are offline",
|
||||
"you-are-offline-description": "Not all features are available while offline. You can still add, modify, and remove items, but you will not be able to sync your changes to the server until you are back online.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Are you sure you want to check all items?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?"
|
||||
"you-are-offline": "Ви не в мережі",
|
||||
"you-are-offline-description": "Не всі функції доступні без мережі. Ви все ще можете додавати, змінювати та видаляти елементи, але не зможете синхронізувати зміни на сервер, поки під'єднаєтесь до мережі.",
|
||||
"are-you-sure-you-want-to-check-all-items": "Ви впевнені, що хочете відмітити всі елементи?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Ви впевнені, що хочете зняти відмітку з усіх елементів?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Ви впевнені, що хочете видалити всі відмічені елементи?"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Всі рецепти",
|
||||
@@ -867,7 +867,7 @@
|
||||
"create-a-tool": "Створити новий інструмент",
|
||||
"tool-name": "Назва інструмента",
|
||||
"create-new-tool": "Створити новий інструмент",
|
||||
"on-hand-checkbox-label": "Показувати як в наявності (позначено)",
|
||||
"on-hand-checkbox-label": "Показувати як в наявності (відмічене)",
|
||||
"required-tools": "Необхідні інструменти",
|
||||
"tool": "Інструмент"
|
||||
},
|
||||
@@ -991,7 +991,7 @@
|
||||
"example-food-singular": "приклад: Цибулина",
|
||||
"example-food-plural": "приклад: Цибуля",
|
||||
"label-overwrite-warning": "Це призначить обрану мітку для всіх вибраних продуктів і потенційно перезапише існуючі мітки.",
|
||||
"on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list."
|
||||
"on-hand-checkbox-label": "Встановлення цього параметра дозволить зробити цю їжу невідміченою за замовчуванням при додаванні рецепта до списку покупок."
|
||||
},
|
||||
"units": {
|
||||
"seed-dialog-text": "Заповнити базу даних розповсюдженими одиницями виміру що відповідають мові.",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "权限",
|
||||
"administrator": "管理员",
|
||||
"user-can-invite-other-to-group": "用户可以邀请其他人加入群组",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "用户可以管理群组",
|
||||
"user-can-organize-group-data": "用户可以整理群组数据",
|
||||
"enable-advanced-features": "启用高级功能",
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
|
||||
"permissions": "Permissions",
|
||||
"administrator": "Administrator",
|
||||
"user-can-invite-other-to-group": "User can invite other to group",
|
||||
"user-can-invite-other-to-group": "User can invite others to group",
|
||||
"user-can-manage-group": "User can manage group",
|
||||
"user-can-organize-group-data": "User can organize group data",
|
||||
"enable-advanced-features": "Enable advanced features",
|
||||
|
||||
@@ -6,9 +6,15 @@ const prefix = "/api";
|
||||
const routes = {
|
||||
webhooks: `${prefix}/groups/webhooks`,
|
||||
webhooksId: (id: string | number) => `${prefix}/groups/webhooks/${id}`,
|
||||
webhooksIdTest: (id: string | number) => `${prefix}/groups/webhooks/${id}/test`,
|
||||
};
|
||||
|
||||
export class WebhooksAPI extends BaseCRUDAPI<CreateWebhook, ReadWebhook> {
|
||||
baseRoute = routes.webhooks;
|
||||
itemRoute = routes.webhooksId;
|
||||
itemTestRoute = routes.webhooksIdTest;
|
||||
|
||||
async testOne(itemId: string | number) {
|
||||
return await this.requests.post<null>(`${this.itemTestRoute(itemId)}`, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ import {
|
||||
mdiRotateRight,
|
||||
mdiBookOpenPageVariant,
|
||||
mdiFileCabinet,
|
||||
mdiSilverwareForkKnife
|
||||
} from "@mdi/js";
|
||||
|
||||
export const icons = {
|
||||
@@ -249,6 +250,7 @@ export const icons = {
|
||||
search: mdiMagnify,
|
||||
shareVariant: mdiShareVariant,
|
||||
shuffleVariant: mdiShuffleVariant,
|
||||
silverwareForkKnife: mdiSilverwareForkKnife,
|
||||
sort: mdiSort,
|
||||
sortAscending: mdiSortAscending,
|
||||
sortDescending: mdiSortDescending,
|
||||
|
||||
@@ -351,13 +351,22 @@ export default {
|
||||
},
|
||||
manifest: {
|
||||
start_url: "/",
|
||||
scope: "/",
|
||||
lang: "en",
|
||||
dir: "auto",
|
||||
name: "Mealie",
|
||||
short_name: "Mealie",
|
||||
id: "mealie",
|
||||
description: "Mealie is a recipe management and meal planning app",
|
||||
theme_color: process.env.THEME_LIGHT_PRIMARY || "#E58325",
|
||||
background_color: "#FFFFFF",
|
||||
display: "standalone",
|
||||
display_override: [
|
||||
"standalone",
|
||||
"minimal-ui",
|
||||
"browser",
|
||||
"window-controls-overlay"
|
||||
],
|
||||
share_target: {
|
||||
action: "/r/create/url",
|
||||
method: "GET",
|
||||
@@ -395,6 +404,56 @@ export default {
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
screenshots: [
|
||||
{
|
||||
"src": "/screenshots/home-narrow.png",
|
||||
"sizes": "1600x2420",
|
||||
"form_factor": "narrow",
|
||||
"label": "Home Page"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/recipe-narrow.png",
|
||||
"sizes": "1600x2420",
|
||||
"form_factor": "narrow",
|
||||
"label": "Recipe Page"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/editor-narrow.png",
|
||||
"sizes": "1600x2420",
|
||||
"form_factor": "narrow",
|
||||
"label": "Editor Page"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/parser-narrow.png",
|
||||
"sizes": "1600x2420",
|
||||
"form_factor": "narrow",
|
||||
"label": "Parser Page"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/home-wide.png",
|
||||
"sizes": "2560x1460",
|
||||
"form_factor": "wide",
|
||||
"label": "Home Page"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/recipe-wide.png",
|
||||
"sizes": "2560x1460",
|
||||
"form_factor": "wide",
|
||||
"label": "Recipe Page"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/editor-wide.png",
|
||||
"sizes": "2560x1460",
|
||||
"form_factor": "wide",
|
||||
"label": "Editor Page"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/parser-wide.png",
|
||||
"sizes": "2560x1460",
|
||||
"form_factor": "wide",
|
||||
"label": "Parser Page"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Shopping Lists",
|
||||
@@ -403,8 +462,12 @@ export default {
|
||||
"url": "/shopping-lists",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/svgs/mdiFormatListChecks.svg",
|
||||
"sizes": "256x256",
|
||||
"src": "/icons/mdiFormatListChecks-192x192.png",
|
||||
"sizes": "192x192",
|
||||
},
|
||||
{
|
||||
"src": "/icons/mdiFormatListChecks-96x96.png",
|
||||
"sizes": "96x96",
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -415,12 +478,28 @@ export default {
|
||||
"url": "/group/mealplan/planner/view",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/svgs/mdiCalendarMultiselect.svg",
|
||||
"sizes": "256x256",
|
||||
"src": "/icons/mdiCalendarMultiselect-192x192.png",
|
||||
"sizes": "192x192",
|
||||
},
|
||||
{
|
||||
"src": "/icons/mdiCalendarMultiselect-96x96.png",
|
||||
"sizes": "96x96",
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
prefer_related_applications: false,
|
||||
handle_links: "preferred",
|
||||
orientation: "any",
|
||||
categories: [
|
||||
"food"
|
||||
],
|
||||
launch_handler: {
|
||||
"client_mode": ["focus-existing", "auto"]
|
||||
},
|
||||
edge_side_panel: {
|
||||
"preferred_width": 400
|
||||
}
|
||||
},
|
||||
icon: false, // disables the icon module
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mealie",
|
||||
"version": "1.0.0",
|
||||
"version": "1.10.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
:slug="mealplan.recipe ? mealplan.recipe.slug : mealplan.title"
|
||||
:description="mealplan.recipe ? mealplan.recipe.description : mealplan.text"
|
||||
:name="mealplan.recipe ? mealplan.recipe.name : mealplan.title"
|
||||
:tags="mealplan.recipe ? mealplan.recipe.tags : []"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
</v-card-text>
|
||||
</BasePageTitle>
|
||||
|
||||
<BannerExperimental />
|
||||
|
||||
<BaseButton create @click="actions.createOne()" />
|
||||
<v-expansion-panels class="mt-2">
|
||||
<v-expansion-panel v-for="(webhook, index) in webhooks" :key="index" class="my-2 left-border rounded">
|
||||
@@ -36,6 +34,7 @@
|
||||
:webhook="webhook"
|
||||
@save="actions.updateOne($event)"
|
||||
@delete="actions.deleteOne($event)"
|
||||
@test="actions.testOne($event).then(() => alert.success($tc('events.test-message-sent')))"
|
||||
/>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
@@ -47,6 +46,7 @@
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useGroupWebhooks, timeUTC } from "~/composables/use-group-webhooks";
|
||||
import GroupWebhookEditor from "~/components/Domain/Group/GroupWebhookEditor.vue";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
export default defineComponent({
|
||||
components: { GroupWebhookEditor },
|
||||
@@ -55,6 +55,7 @@ export default defineComponent({
|
||||
const { actions, webhooks } = useGroupWebhooks();
|
||||
|
||||
return {
|
||||
alert,
|
||||
webhooks,
|
||||
actions,
|
||||
timeUTC
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<template #title> {{ shoppingList.name }} </template>
|
||||
</BasePageTitle>
|
||||
<BannerWarning
|
||||
v-if="isOffline"
|
||||
v-if="$nuxt.isOffline"
|
||||
:title="$tc('shopping-list.you-are-offline')"
|
||||
:description="$tc('shopping-list.you-are-offline-description')"
|
||||
/>
|
||||
@@ -46,7 +46,6 @@
|
||||
:units="allUnits || []"
|
||||
:foods="allFoods || []"
|
||||
:recipes="recipeMap"
|
||||
:is-offline="isOffline"
|
||||
@checked="saveListItem"
|
||||
@save="saveListItem"
|
||||
@delete="deleteListItem(item)"
|
||||
@@ -75,7 +74,6 @@
|
||||
:units="allUnits || []"
|
||||
:foods="allFoods || []"
|
||||
:recipes="recipeMap"
|
||||
:is-offline="isOffline"
|
||||
@checked="saveListItem"
|
||||
@save="saveListItem"
|
||||
@delete="deleteListItem(item)"
|
||||
@@ -132,7 +130,6 @@
|
||||
:labels="allLabels || []"
|
||||
:units="allUnits || []"
|
||||
:foods="allFoods || []"
|
||||
:is-offline="isOffline"
|
||||
@delete="createEditorOpen = false"
|
||||
@cancel="createEditorOpen = false"
|
||||
@save="createListItem"
|
||||
@@ -141,7 +138,7 @@
|
||||
<div v-else class="mt-4 d-flex justify-end">
|
||||
<BaseButton
|
||||
v-if="preferences.viewByLabel" edit class="mr-2"
|
||||
:disabled="isOffline"
|
||||
:disabled="$nuxt.isOffline"
|
||||
@click="toggleReorderLabelsDialog">
|
||||
<template #icon> {{ $globals.icons.tags }} </template>
|
||||
{{ $t('shopping-list.reorder-labels') }}
|
||||
@@ -221,7 +218,6 @@
|
||||
:labels="allLabels || []"
|
||||
:units="allUnits || []"
|
||||
:foods="allFoods || []"
|
||||
:is-offline="isOffline"
|
||||
@checked="saveListItem"
|
||||
@save="saveListItem"
|
||||
@delete="deleteListItem(item)"
|
||||
@@ -244,10 +240,10 @@
|
||||
{{ $tc('shopping-list.linked-recipes-count', shoppingList.recipeReferences ? shoppingList.recipeReferences.length : 0) }}
|
||||
</div>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<RecipeList :recipes="Array.from(recipeMap.values())" show-description :disabled="isOffline">
|
||||
<RecipeList :recipes="Array.from(recipeMap.values())" show-description :disabled="$nuxt.isOffline">
|
||||
<template v-for="(recipe, index) in recipeMap.values()" #[`actions-${recipe.id}`]>
|
||||
<v-list-item-action :key="'item-actions-decrease' + recipe.id">
|
||||
<v-btn icon :disabled="isOffline" @click.prevent="removeRecipeReferenceToList(recipe.id)">
|
||||
<v-btn icon :disabled="$nuxt.isOffline" @click.prevent="removeRecipeReferenceToList(recipe.id)">
|
||||
<v-icon color="grey lighten-1">{{ $globals.icons.minus }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
@@ -255,7 +251,7 @@
|
||||
{{ shoppingList.recipeReferences[index].recipeQuantity }}
|
||||
</div>
|
||||
<v-list-item-action :key="'item-actions-increase' + recipe.id">
|
||||
<v-btn icon :disabled="isOffline" @click.prevent="addRecipeReferenceToList(recipe.id)">
|
||||
<v-btn icon :disabled="$nuxt.isOffline" @click.prevent="addRecipeReferenceToList(recipe.id)">
|
||||
<v-icon color="grey lighten-1">{{ $globals.icons.createAlt }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
@@ -268,7 +264,7 @@
|
||||
<div class="d-flex justify-end">
|
||||
<BaseButton
|
||||
edit
|
||||
:disabled="isOffline"
|
||||
:disabled="$nuxt.isOffline"
|
||||
@click="toggleSettingsDialog"
|
||||
>
|
||||
<template #icon> {{ $globals.icons.cog }} </template>
|
||||
@@ -278,7 +274,7 @@
|
||||
</v-lazy>
|
||||
|
||||
<v-lazy>
|
||||
<div v-if="!isOffline" class="d-flex justify-end mt-10">
|
||||
<div v-if="$nuxt.isOnline" class="d-flex justify-end mt-10">
|
||||
<ButtonLink
|
||||
:to="`/group/data/labels`"
|
||||
:text="$tc('shopping-list.manage-labels')"
|
||||
@@ -868,7 +864,6 @@ export default defineComponent({
|
||||
|
||||
// set a temporary updatedAt timestamp prior to refresh so it appears at the top of the checked items
|
||||
item.updateAt = new Date().toISOString();
|
||||
item.updateAt = item.updateAt.substring(0, item.updateAt.length-1);
|
||||
}
|
||||
|
||||
// make updates reflect immediately
|
||||
@@ -1073,7 +1068,6 @@ export default defineComponent({
|
||||
getLabelColor,
|
||||
groupSlug,
|
||||
itemsByLabel,
|
||||
isOffline: shoppingListItemActions.isOffline,
|
||||
listItems,
|
||||
loadingCounter,
|
||||
preferences,
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<v-list-item-title>
|
||||
{{ token.name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle> {{ $t('general.created-on-date', [$d(new Date(token.createdAt+"Z"))]) }} </v-list-item-subtitle>
|
||||
<v-list-item-subtitle> {{ $t('general.created-on-date', [$d(new Date(token.createdAt))]) }} </v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<BaseButton delete small @click="deleteToken(token.id)"></BaseButton>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/static/icons/mdiCalendarMultiselect-192x192.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/static/icons/mdiCalendarMultiselect-96x96.png
Normal file
|
After Width: | Height: | Size: 771 B |
BIN
frontend/static/icons/mdiFormatListChecks-192x192.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/static/icons/mdiFormatListChecks-96x96.png
Normal file
|
After Width: | Height: | Size: 760 B |
BIN
frontend/static/screenshots/editor-narrow.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
frontend/static/screenshots/editor-wide.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
frontend/static/screenshots/home-narrow.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/screenshots/home-wide.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
frontend/static/screenshots/parser-narrow.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
frontend/static/screenshots/parser-wide.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
frontend/static/screenshots/recipe-narrow.png
Normal file
|
After Width: | Height: | Size: 770 KiB |
BIN
frontend/static/screenshots/recipe-wide.png
Normal file
|
After Width: | Height: | Size: 943 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-calendar-multiselect" viewBox="0 0 24 24"><path d="M19,19V8H5V19H19M16,1H18V3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1M7,10H9V12H7V10M15,10H17V12H15V10M11,14H13V16H11V14M15,14H17V16H15V14Z" /></svg>
|
||||
|
Before Width: | Height: | Size: 298 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-format-list-checks" viewBox="0 0 24 24"><path d="M3,5H9V11H3V5M5,7V9H7V7H5M11,7H21V9H11V7M11,15H21V17H11V15M5,20L1.5,16.5L2.91,15.09L5,17.17L9.59,12.59L11,14L5,20Z" /></svg>
|
||||
|
Before Width: | Height: | Size: 222 B |
@@ -17,18 +17,14 @@ from mealie.services.scheduler import SchedulerRegistry, SchedulerService, tasks
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
description = f"""
|
||||
description = """
|
||||
Mealie is a web application for managing your recipes, meal plans, and shopping lists. This is the Restful
|
||||
API interactive documentation that can be used to explore the API. If you're justing getting started with
|
||||
the API and want to get started quickly, you can use the
|
||||
[API Usage | Mealie Docs](https://nightly.mealie.io/documentation/getting-started/api-usage/)
|
||||
[API Usage | Mealie Docs](https://docs.mealie.io/documentation/getting-started/api-usage/)
|
||||
as a reference for how to get started.
|
||||
|
||||
|
||||
As of this release <b>{APP_VERSION}</b>, Mealie is still in rapid development and therefore some of these APIs may
|
||||
change from version to version.
|
||||
|
||||
|
||||
If you have any questions or comments about mealie, please use the discord server to talk to the developers or other
|
||||
community members. If you'd like to file an issue, please use the
|
||||
[GitHub Issue Tracker | Mealie](https://github.com/mealie-recipes/mealie/issues/new/choose)
|
||||
@@ -36,10 +32,9 @@ community members. If you'd like to file an issue, please use the
|
||||
|
||||
## Helpful Links
|
||||
- [Home Page](https://mealie.io)
|
||||
- [Documentation](https://nightly.mealie.io)
|
||||
- [Documentation](https://docs.mealie.io)
|
||||
- [Discord](https://discord.gg/QuStdQGSGK)
|
||||
- [Demo](https://demo.mealie.io)
|
||||
- [Beta](https://demo.mealie.io)
|
||||
"""
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
@@ -32,7 +32,7 @@ def get_latest_version() -> str:
|
||||
|
||||
global _LAST_RESET
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
if not _LAST_RESET or now - _LAST_RESET > datetime.timedelta(days=MAX_DAYS_OLD):
|
||||
_LAST_RESET = now
|
||||
|
||||
@@ -38,7 +38,7 @@ class OpenIDProvider(AuthProvider[OIDCRequest]):
|
||||
user = self.try_get_user(claims.get(settings.OIDC_USER_CLAIM))
|
||||
is_admin = False
|
||||
if settings.OIDC_USER_GROUP or settings.OIDC_ADMIN_GROUP:
|
||||
group_claim = claims.get(settings.OIDC_GROUPS_CLAIM, [])
|
||||
group_claim = claims.get(settings.OIDC_GROUPS_CLAIM, []) or []
|
||||
is_admin = settings.OIDC_ADMIN_GROUP in group_claim if settings.OIDC_ADMIN_GROUP else False
|
||||
is_valid_user = settings.OIDC_USER_GROUP in group_claim if settings.OIDC_USER_GROUP else True
|
||||
|
||||
@@ -82,7 +82,12 @@ class OpenIDProvider(AuthProvider[OIDCRequest]):
|
||||
|
||||
def get_claims(self, settings: AppSettings) -> JWTClaims | None:
|
||||
"""Get the claims from the ID token and check if the required claims are present"""
|
||||
required_claims = {"preferred_username", "name", "email", settings.OIDC_USER_CLAIM}
|
||||
required_claims = {
|
||||
"preferred_username",
|
||||
"name",
|
||||
"email",
|
||||
settings.OIDC_USER_CLAIM,
|
||||
}
|
||||
jwks = OpenIDProvider.get_jwks(self.get_ttl_hash()) # cache the key set for 30 minutes
|
||||
if not jwks:
|
||||
return None
|
||||
|
||||
@@ -72,9 +72,18 @@ class PostgresProvider(AbstractDBProvider, BaseSettings):
|
||||
|
||||
@property
|
||||
def db_url_public(self) -> str:
|
||||
user = self.POSTGRES_USER
|
||||
password = self.POSTGRES_PASSWORD
|
||||
return self.db_url.replace(user, "*****", 1).replace(password, "*****", 1)
|
||||
if self.POSTGRES_URL_OVERRIDE:
|
||||
return "Postgres Url Overridden"
|
||||
|
||||
return str(
|
||||
PostgresDsn.build(
|
||||
scheme="postgresql",
|
||||
username="******",
|
||||
password="******",
|
||||
host=f"{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}",
|
||||
path=f"{self.POSTGRES_DB or ''}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def db_provider_factory(provider_name: str, data_dir: Path, env_file: Path, env_encoding="utf-8") -> AbstractDBProvider:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import logging
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
from dateutil.tz import tzlocal
|
||||
from pydantic import field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
@@ -9,6 +13,11 @@ from mealie.core.settings.themes import Theme
|
||||
from .db_providers import AbstractDBProvider, db_provider_factory
|
||||
|
||||
|
||||
class ScheduleTime(NamedTuple):
|
||||
hour: int
|
||||
minute: int
|
||||
|
||||
|
||||
def determine_secrets(data_dir: Path, production: bool) -> str:
|
||||
if not production:
|
||||
return "shh-secret-test-key"
|
||||
@@ -58,6 +67,44 @@ class AppSettings(BaseSettings):
|
||||
ALLOW_SIGNUP: bool = False
|
||||
|
||||
DAILY_SCHEDULE_TIME: str = "23:45"
|
||||
"""Local server time, in HH:MM format. See `DAILY_SCHEDULE_TIME_UTC` for the parsed UTC equivalent"""
|
||||
|
||||
_logger: logging.Logger | None = None
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
if self._logger is None:
|
||||
# lazy load the logger, since get_logger depends on the settings being loaded
|
||||
from mealie.core.root_logger import get_logger
|
||||
|
||||
self._logger = get_logger()
|
||||
|
||||
return self._logger
|
||||
|
||||
@property
|
||||
def DAILY_SCHEDULE_TIME_UTC(self) -> ScheduleTime:
|
||||
"""The DAILY_SCHEDULE_TIME in UTC, parsed into hours and minutes"""
|
||||
|
||||
# parse DAILY_SCHEDULE_TIME into hours and minutes
|
||||
try:
|
||||
hour_str, minute_str = self.DAILY_SCHEDULE_TIME.split(":")
|
||||
local_hour = int(hour_str)
|
||||
local_minute = int(minute_str)
|
||||
except ValueError:
|
||||
local_hour = 23
|
||||
local_minute = 45
|
||||
self.logger.exception(
|
||||
f"Unable to parse {self.DAILY_SCHEDULE_TIME=} as HH:MM; defaulting to {local_hour}:{local_minute}"
|
||||
)
|
||||
|
||||
# DAILY_SCHEDULE_TIME is in local time, so we convert it to UTC
|
||||
local_tz = tzlocal()
|
||||
now = datetime.now(local_tz)
|
||||
local_time = now.replace(hour=local_hour, minute=local_minute)
|
||||
utc_time = local_time.astimezone(timezone.utc)
|
||||
|
||||
self.logger.debug(f"Local time: {local_hour}:{local_minute} | UTC time: {utc_time.hour}:{utc_time.minute}")
|
||||
return ScheduleTime(utc_time.hour, utc_time.minute)
|
||||
|
||||
# ===============================================
|
||||
# Security Configuration
|
||||
|
||||
@@ -4,11 +4,13 @@ from sqlalchemy import DateTime, Integer
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from text_unidecode import unidecode
|
||||
|
||||
from ._model_utils.datetime import get_utc_now
|
||||
|
||||
|
||||
class SqlAlchemyBase(DeclarativeBase):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now, index=True)
|
||||
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, index=True)
|
||||
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, onupdate=get_utc_now)
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, val: str) -> str:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from .auto_init import auto_init
|
||||
from .guid import GUID
|
||||
|
||||
__all__ = [
|
||||
"auto_init",
|
||||
"GUID",
|
||||
]
|
||||
|
||||