Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45f374610f | |||
| b419a2bfd2 | |||
| ee8d96bee7 | |||
| c65ff2d75d | |||
| ea1c90d8ca | |||
| 3e963e3e6d | |||
| 82769128a1 | |||
| 05f4ce6b2e | |||
| 508984ecfa | |||
| a05507f73d | |||
| a27def119c | |||
| fce94f38ce | |||
| 83bd9c5ce3 | |||
| bfbfade99a | |||
| 16daf7b8d3 | |||
| fcad79d662 | |||
| 2424ce17ec | |||
| 4715905334 | |||
| 7c05aedcfe | |||
| 7347fc7c86 | |||
| ada7d9a666 | |||
| 0cb3fb8e3c | |||
| dcb788b57b | |||
| 73bcb9d598 | |||
| f3e90c3999 | |||
| cb4ed3c9d7 | |||
| f8b0746e73 | |||
| d8ad7a743e | |||
| 0ba445895c | |||
| 3b719ab9ba | |||
| 8887e811d3 | |||
| cb9e994749 | |||
| 1841533d2c | |||
| 7da99f2116 | |||
| b0af5e7f35 | |||
| dd6bb0ed3e | |||
| 1306f0bcfe | |||
| ca4494c09e | |||
| f57db0acc8 | |||
| 342f992556 | |||
| d0cf3974ad | |||
| 0f3eb35c83 | |||
| d15fb37932 | |||
| 051f33bb1e | |||
| 566eacc8fe | |||
| d19023a1ba | |||
| a6e36f02a7 | |||
| c3fd1a89ef | |||
| 8b1ff536b3 | |||
| 6b08b8da1b | |||
| c4ff42f181 | |||
| ee0aa0fe5b | |||
| 09c871fc29 | |||
| 8496959188 | |||
| 77105001e0 | |||
| 2d57d38644 | |||
| c9d13619a6 | |||
| b99ec9bb50 | |||
| b3f26469c8 |
+1
-1
@@ -4,7 +4,7 @@ __pycache__
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
venv
|
||||
.venv
|
||||
**/.venv
|
||||
|
||||
# System-specific files
|
||||
.DS_Store
|
||||
|
||||
@@ -12,6 +12,9 @@ on:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DOCKER_USER: 1001:127
|
||||
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
|
||||
@@ -20,6 +23,8 @@ env:
|
||||
jobs:
|
||||
build-and-push-backend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
@@ -43,12 +48,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# with:
|
||||
# docker-build-args: '--target backend-production -f Dockerfile'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '--target backend-production -f Dockerfile'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -63,6 +68,8 @@ jobs:
|
||||
|
||||
build-and-push-frontend-generic:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
@@ -86,12 +93,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# with:
|
||||
# docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -107,6 +114,8 @@ jobs:
|
||||
|
||||
build-and-push-frontend-dinum:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
@@ -130,12 +139,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# with:
|
||||
# docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -151,6 +160,8 @@ jobs:
|
||||
|
||||
build-and-push-summary:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
@@ -174,13 +185,13 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
|
||||
docker-context: './src/summary'
|
||||
-
|
||||
name: Build and push
|
||||
@@ -197,6 +208,8 @@ jobs:
|
||||
|
||||
build-and-push-agents:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
@@ -220,14 +233,14 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
|
||||
# docker-context: './src/agents'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
|
||||
docker-context: './src/agents'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -242,6 +255,8 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
notify-argocd:
|
||||
permissions:
|
||||
contents: read
|
||||
needs:
|
||||
- build-and-push-frontend-generic
|
||||
- build-and-push-frontend-dinum
|
||||
|
||||
+54
-12
@@ -124,15 +124,17 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
- name: Check code formatting with ruff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
run: uv run ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: ~/.local/bin/ruff check .
|
||||
run: uv run ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: ~/.local/bin/pylint meet demo core
|
||||
run: uv run pylint meet demo core
|
||||
|
||||
lint-agents:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -279,10 +281,10 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the dependencies
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
- name: Install gettext (required to compile messages)
|
||||
run: |
|
||||
@@ -290,10 +292,50 @@ jobs:
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: python manage.py compilemessages
|
||||
run: uv run python manage.py compilemessages
|
||||
|
||||
- name: Run tests
|
||||
run: ~/.local/bin/pytest -n 2
|
||||
run: uv run pytest -n 2
|
||||
|
||||
test-summary:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/summary
|
||||
|
||||
env:
|
||||
APP_API_TOKEN: "test-api-token"
|
||||
AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage"
|
||||
AWS_S3_ENDPOINT_URL: "minio:9000"
|
||||
AWS_S3_ACCESS_KEY_ID: "meet"
|
||||
AWS_S3_SECRET_ACCESS_KEY: "password"
|
||||
WHISPERX_BASE_URL: "https://configure-your-url.com"
|
||||
WHISPERX_ASR_MODEL: "large-v2"
|
||||
WHISPERX_API_KEY: "test-whisperx-secret"
|
||||
WHISPERX_DEFAULT_LANGUAGE: "fr"
|
||||
LLM_BASE_URL: "https://configure-your-url.com"
|
||||
LLM_API_KEY: "test-llm-secret"
|
||||
LLM_MODEL: "test-llm-model"
|
||||
WEBHOOK_API_TOKEN: "test-webhook-secret"
|
||||
WEBHOOK_URL: "https://configure-your-url.com"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
|
||||
- name: Run summary tests
|
||||
run: ~/.local/bin/pytest
|
||||
|
||||
lint-front:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -31,6 +31,7 @@ MANIFEST
|
||||
|
||||
# Translations # Translations
|
||||
*.pot
|
||||
*.mo
|
||||
|
||||
# Environments
|
||||
.env
|
||||
|
||||
+36
-2
@@ -10,11 +10,44 @@ and this project adheres to
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿️(frontend) fix sidepanel accessibility aria-label #1182
|
||||
- ♿️(frontend) fix more tools heading hierarchy #1181
|
||||
|
||||
## [1.11.0] - 2026-03-19
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(helm) support celery with our Django backend #1124
|
||||
- ✨(helm) support ingress for custom background image #1124
|
||||
- ✨(backend) add authenticated user rate throttling on request-entry #1129
|
||||
- ✨(backend) expose `is_active` field for Application in Django admin #1133
|
||||
- ✨(file-upload) disable by default & limit count by user #1141
|
||||
- ✨(frontend) custom background #1067
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿️(frontend) Caption text size setting for accessibility #1062
|
||||
- ♿️(frontend) sync html lang attribute with i18n for screen readers #1111
|
||||
- ♿️(frontend) improve MoreLink a11y and UX on home page #1112
|
||||
- ♿(frontend) improve chat toast a11y for screen readers #1109
|
||||
- ♿(frontend) improve ui and qria labels for help article links #1108
|
||||
- ♿️(frontend) improve chat toast a11y for screen readers #1109
|
||||
- ♿️(frontend) improve ui and aria labels for help article links #1108
|
||||
- 🌐(frontend) improve German translation #1125
|
||||
- 🔨(python-env) migrate meet main app to UV #1120
|
||||
- ♻️(backend) align Application model field with `is_active` convention #1133
|
||||
- 🔐(backend) avoids revealing the inactive status of an application #1135
|
||||
- ⚡️(helm) reduce initialDelaySeconds and add periods seconds #1139
|
||||
- 🔒️(backend) avoid information exposure through exception messages #1144
|
||||
- ⬆️(dependencies) update PyJWT to v2.12.0 [SECURITY] #1151
|
||||
- 📌(agents) unpin OpenSSL and related dependencies #1167
|
||||
- ♿️(frontend) add caption font and background color customization #1122
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix hand icon and queue position alignment and position #1119
|
||||
- 🩹(backend) add page_size to pagination for room endpoints #1131
|
||||
- 🐛(backend) refactor lobby throttling to use participant id #1129
|
||||
- 🩹(backend) ignore non-recording uploads in storage webhook handler #1142
|
||||
- 🐛(frontend) fix dimension mismatch in BackgroundCustomProcessor #1116
|
||||
|
||||
## [1.10.0] - 2026-03-05
|
||||
|
||||
@@ -24,6 +57,7 @@ and this project adheres to
|
||||
- 🦺(backend) strengthen API validation for recording options #1063
|
||||
- ⚡️(frontend) optimize few performance caveats #1073
|
||||
- 🔒️(helm) introduce a dedicated Kubernetes Ingress for webhook-livekit #1066
|
||||
- ⬆️(deps) bump rollup from 4.44.2 to 4.59.0 in /src/frontend #1088
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
+39
-23
@@ -13,14 +13,28 @@ RUN apk update && \
|
||||
# ---- Back-end builder image ----
|
||||
FROM base AS back-builder
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
# Copy required python dependencies
|
||||
COPY ./src/backend /builder
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
RUN mkdir /install && \
|
||||
pip install --prefix=/install .
|
||||
# Disable Python downloads, because we want to use the system interpreter
|
||||
# across both images. If using a managed Python version, it needs to be
|
||||
# copied from the build image into the final image;
|
||||
ENV UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
# install uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=src/backend/uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=src/backend/pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project --no-dev
|
||||
COPY src/backend /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-dev
|
||||
|
||||
# ---- mails ----
|
||||
FROM node:20 AS mail-builder
|
||||
@@ -30,7 +44,7 @@ COPY ./src/mail /mail/app
|
||||
WORKDIR /mail/app
|
||||
|
||||
RUN yarn install --frozen-lockfile && \
|
||||
yarn build
|
||||
yarn build
|
||||
|
||||
|
||||
# ---- static link collector ----
|
||||
@@ -42,17 +56,17 @@ RUN apk add \
|
||||
libmagic \
|
||||
rdfind
|
||||
|
||||
# Copy installed python dependencies
|
||||
COPY --from=back-builder /install /usr/local
|
||||
|
||||
# Copy Meet application (see .dockerignore)
|
||||
COPY ./src/backend /app/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the application from the builder
|
||||
COPY --from=back-builder /app /app
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
|
||||
# collectstatic
|
||||
RUN DJANGO_CONFIGURATION=Build DJANGO_JWT_PRIVATE_SIGNING_KEY=Dummy \
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
# Replace duplicated file by a symlink to decrease the overall size of the
|
||||
# final image
|
||||
@@ -81,14 +95,17 @@ COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
# docker user (see entrypoint).
|
||||
RUN chmod g=u /etc/passwd
|
||||
|
||||
# Copy installed python dependencies
|
||||
COPY --from=back-builder /install /usr/local
|
||||
|
||||
# Copy Meet application (see .dockerignore)
|
||||
COPY ./src/backend /app/
|
||||
# Copy the application from the builder
|
||||
COPY --from=back-builder /app /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Generate compiled translation messages
|
||||
RUN DJANGO_CONFIGURATION=Build \
|
||||
python manage.py compilemessages --ignore=".venv/**/*"
|
||||
|
||||
# We wrap commands run in this container by the following entrypoint that
|
||||
# creates a user on-the-fly with the container user ID (see USER) and root group
|
||||
# ID.
|
||||
@@ -103,10 +120,9 @@ USER root:root
|
||||
# Install psql
|
||||
RUN apk add postgresql-client
|
||||
|
||||
# Uninstall Meet and re-install it in editable mode along with development
|
||||
# dependencies
|
||||
RUN pip uninstall -y meet
|
||||
RUN pip install -e .[dev]
|
||||
# Install development dependencies
|
||||
RUN --mount=from=ghcr.io/astral-sh/uv:0.10.9,source=/uv,target=/bin/uv \
|
||||
uv sync --all-extras --locked
|
||||
|
||||
# Restore the un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
@@ -115,7 +131,7 @@ USER ${DOCKER_USER}
|
||||
# Target database host (e.g. database engine following docker compose services
|
||||
# name) & port
|
||||
ENV DB_HOST=postgresql \
|
||||
DB_PORT=5432
|
||||
DB_PORT=5432
|
||||
|
||||
# Run django development server
|
||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||
|
||||
@@ -191,6 +191,7 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f
|
||||
|
||||
test: ## run project tests
|
||||
@$(MAKE) test-back-parallel
|
||||
@$(MAKE) test-summary
|
||||
.PHONY: test
|
||||
|
||||
test-back: ## run back-end tests
|
||||
@@ -203,6 +204,11 @@ test-back-parallel: ## run all back-end tests in parallel
|
||||
bin/pytest -n auto $${args:-${1}}
|
||||
.PHONY: test-back-parallel
|
||||
|
||||
test-summary: ## run summary tests
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest-summary $${args:-${1}}
|
||||
.PHONY: test-summary
|
||||
|
||||
makemigrations: ## run django makemigrations for the Meet project.
|
||||
@echo "$(BOLD)Running makemigrations$(RESET)"
|
||||
@$(COMPOSE) up -d postgresql
|
||||
@@ -223,7 +229,7 @@ superuser: ## Create an admin superuser with password "admin"
|
||||
.PHONY: superuser
|
||||
|
||||
back-i18n-compile: ## compile the gettext files
|
||||
@$(MANAGE) compilemessages --ignore="venv/**/*"
|
||||
@$(MANAGE) compilemessages --ignore=".venv/**/*"
|
||||
.PHONY: back-i18n-compile
|
||||
|
||||
back-i18n-generate: ## create the .pot files used for i18n
|
||||
@@ -354,13 +360,13 @@ install-external-secrets: ## install the kubernetes secrets from Vaultwarden
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
start-tilt: ## start the kubernetes cluster using kind
|
||||
tilt up -f ./bin/Tiltfile
|
||||
tilt up --namespace=meet -f ./bin/Tiltfile
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
|
||||
DEV_ENV=dev-keycloak tilt up -f ./bin/Tiltfile
|
||||
DEV_ENV=dev-keycloak tilt up --namespace=meet -f ./bin/Tiltfile
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
start-tilt-dinum: ## start the kubernetes cluster using kind, without Pro Connect for authentication, but with DINUM styles
|
||||
DEV_ENV=dev-dinum tilt up -f ./bin/Tiltfile
|
||||
DEV_ENV=dev-dinum tilt up --namespace=meet -f ./bin/Tiltfile
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
+4
-4
@@ -18,7 +18,6 @@ docker_build(
|
||||
'localhost:5001/meet-backend:latest',
|
||||
context='..',
|
||||
dockerfile='../Dockerfile',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
only=['./src/backend', './src/mail', './docker'],
|
||||
target = 'backend-production',
|
||||
live_update=[
|
||||
@@ -34,7 +33,6 @@ clean_old_images('localhost:5001/meet-backend')
|
||||
docker_build(
|
||||
'localhost:5001/meet-frontend-dinum:latest',
|
||||
context='..',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
dockerfile='../docker/dinum-frontend/Dockerfile',
|
||||
only=['./src/frontend', './docker', './.dockerignore'],
|
||||
target = 'frontend-production',
|
||||
@@ -59,7 +57,6 @@ clean_old_images('localhost:5001/meet-frontend-generic')
|
||||
docker_build(
|
||||
'localhost:5001/meet-summary:latest',
|
||||
context='../src/summary',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
dockerfile='../src/summary/Dockerfile',
|
||||
only=['.'],
|
||||
target = 'production',
|
||||
@@ -72,7 +69,6 @@ clean_old_images('localhost:5001/meet-summary')
|
||||
docker_build(
|
||||
'localhost:5001/meet-agents:latest',
|
||||
context='../src/agents',
|
||||
build_args={'DOCKER_USER': '1001:127'},
|
||||
dockerfile='../src/agents/Dockerfile',
|
||||
only=['.'],
|
||||
target = 'production',
|
||||
@@ -103,7 +99,11 @@ k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev} template .
|
||||
|
||||
k8s_resource('minio-bucket', resource_deps=['minio'])
|
||||
k8s_resource('meet-backend', resource_deps=['postgresql', 'minio', 'redis', 'livekit-livekit-server'])
|
||||
k8s_resource('meet-celery-backend', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-summarize', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-transcribe', resource_deps=['redis'])
|
||||
k8s_resource('meet-backend-migrate', resource_deps=['meet-backend'])
|
||||
k8s_resource('livekit-livekit-server', resource_deps=['redis'])
|
||||
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
|
||||
k8s_resource('keycloak', resource_deps=['kc-postgresql'])
|
||||
k8s_resource('meet-backend-createsuperuser', resource_deps=['meet-backend-migrate'])
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_dc_run \
|
||||
app-summary-dev \
|
||||
python -m pytest "$@"
|
||||
+3
-1
@@ -58,7 +58,7 @@ services:
|
||||
/usr/bin/mc admin config set meet notify_webhook:meet-webhook endpoint='http://app-dev:8000/api/v1.0/recordings/storage-hook/' auth_token='Bearer password' &&
|
||||
/usr/bin/mc admin service restart meet --wait --json &&
|
||||
sleep 15 &&
|
||||
/usr/bin/mc event add meet/meet-media-storage arn:minio:sqs::meet-webhook:webhook --event put &&
|
||||
/usr/bin/mc event add meet/meet-media-storage arn:minio:sqs::meet-webhook:webhook --event put --prefix "recordings" &&
|
||||
exit 0;"
|
||||
|
||||
app-dev:
|
||||
@@ -80,6 +80,7 @@ services:
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
- /app/.venv
|
||||
depends_on:
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
@@ -105,6 +106,7 @@ services:
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
- /app/.venv
|
||||
depends_on:
|
||||
- app-dev
|
||||
|
||||
|
||||
@@ -61,11 +61,10 @@ services:
|
||||
`docker compose up -d`
|
||||
```
|
||||
|
||||
Your keycloak instance is now available on https://doc.yourdomain.tld
|
||||
Your keycloak instance is now available on https://id.yourdomain.tld
|
||||
|
||||
> [!CAUTION]
|
||||
> Version of the images are set to latest, you should pin it to the desired version to avoid unwanted upgrades when pulling latest image. You can find available versions on [Keycloak registry](https://quay.io/repository/keycloak/keycloak?tab=tags).
|
||||
```
|
||||
|
||||
## Creating an OIDC Client for Meet Application
|
||||
|
||||
@@ -76,7 +75,7 @@ Your keycloak instance is now available on https://doc.yourdomain.tld
|
||||
3. Enter the name of the realm - `meet`.
|
||||
4. Click "Create".
|
||||
|
||||
#### Step 2: Create a New Client
|
||||
### Step 2: Create a New Client
|
||||
|
||||
1. Navigate to the "Clients" tab.
|
||||
2. Click on the "Create client" button.
|
||||
@@ -86,7 +85,7 @@ Your keycloak instance is now available on https://doc.yourdomain.tld
|
||||
1. Set the "Web Origins" to the URL of your meet application - e.g. `https://meet.example.com`.
|
||||
1. Click "Save".
|
||||
|
||||
#### Step 3: Get Client Credentials
|
||||
### Step 3: Get Client Credentials
|
||||
|
||||
1. Go to the "Credentials" tab.
|
||||
2. Copy the client ID (`meet` in this example) and the client secret.
|
||||
|
||||
@@ -68,13 +68,13 @@ backend:
|
||||
python manage.py createsuperuser --email admin@example.com --password admin
|
||||
restartPolicy: Never
|
||||
|
||||
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumeMounts:
|
||||
- name: certs
|
||||
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
|
||||
mountPath: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
|
||||
subPath: cacert.pem
|
||||
|
||||
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumes:
|
||||
- name: certs
|
||||
configMap:
|
||||
|
||||
@@ -87,12 +87,12 @@ If you are using an external service, you need to set `REDIS_URL` environment va
|
||||
|
||||
Generate a secure key for `LIVEKIT_API_SECRET` in `env.d/common`.
|
||||
|
||||
We provide a minimal recommanded config for production environment in `livekit-server.yaml`. Set the previously generated API secret key in the config file.
|
||||
We provide a minimal recommended config for production environment in `livekit-server.yaml`. Set the previously generated API secret key in the config file.
|
||||
|
||||
To view other customization options, see [config-sample.yaml](https://github.com/livekit/livekit/blob/master/config-sample.yaml)
|
||||
|
||||
> [!NOTE]
|
||||
> In this example, we configured multiplexing on a single UDP port. For better performances, you can configure a range of UDP ports.
|
||||
> In this example, we configured multiplexing on a single UDP port. For better performance, you can configure a range of UDP ports.
|
||||
|
||||
### Meet
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ AWS_S3_DOMAIN_REPLACE=http://localhost:9000
|
||||
AWS_S3_ENDPOINT_URL=http://minio:9000
|
||||
AWS_S3_ACCESS_KEY_ID=meet
|
||||
AWS_S3_SECRET_ACCESS_KEY=password
|
||||
MEDIA_BASE_URL=http://localhost:8083
|
||||
MEDIA_BASE_URL=http://localhost:3000
|
||||
FILE_UPLOAD_ENABLED=True
|
||||
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/certs
|
||||
|
||||
@@ -4,8 +4,6 @@ FROM python:3.13-slim AS base
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libglib2.0-0 \
|
||||
libgobject-2.0-0 \
|
||||
"openssl=3.5.4-1~deb13u2" \
|
||||
"libssl3t64=3.5.4-1~deb13u2" \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.3.10",
|
||||
|
||||
@@ -197,6 +197,38 @@ def resend_notification(modeladmin, request, queryset): # pylint: disable=unuse
|
||||
)
|
||||
|
||||
|
||||
@admin.action(description=_("Mark selected recordings as 'Failed to Stop'"))
|
||||
def mark_as_failed_to_stop(modeladmin, request, queryset):
|
||||
"""Force selected recordings status to failed_to_stop."""
|
||||
|
||||
eligible_statuses = [
|
||||
models.RecordingStatusChoices.ACTIVE,
|
||||
models.RecordingStatusChoices.INITIATED,
|
||||
models.RecordingStatusChoices.STOPPED,
|
||||
]
|
||||
|
||||
eligible = queryset.filter(status__in=eligible_statuses)
|
||||
skipped = queryset.exclude(status__in=eligible_statuses).count()
|
||||
|
||||
updated = eligible.update(status=models.RecordingStatusChoices.FAILED_TO_STOP)
|
||||
|
||||
if updated > 0:
|
||||
modeladmin.message_user(
|
||||
request,
|
||||
_("%(count)s recording(s) successfully marked as 'Failed to Stop'.")
|
||||
% {"count": updated},
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
|
||||
if skipped > 0:
|
||||
modeladmin.message_user(
|
||||
request,
|
||||
_("Skipped %(count)s recording(s) with an ineligible status.")
|
||||
% {"count": skipped},
|
||||
level=messages.WARNING,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(models.Recording)
|
||||
class RecordingAdmin(admin.ModelAdmin):
|
||||
"""Recording admin interface declaration."""
|
||||
@@ -224,7 +256,7 @@ class RecordingAdmin(admin.ModelAdmin):
|
||||
"updated_at",
|
||||
"worker_id",
|
||||
)
|
||||
actions = [resend_notification]
|
||||
actions = [resend_notification, mark_as_failed_to_stop]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queries by prefetching related access and user data to avoid N+1 queries."""
|
||||
@@ -276,7 +308,7 @@ class ApplicationAdmin(admin.ModelAdmin):
|
||||
|
||||
form = ApplicationAdminForm
|
||||
|
||||
list_display = ("id", "name", "client_id", "get_scopes_display")
|
||||
list_display = ("id", "name", "client_id", "get_scopes_display", "is_active")
|
||||
fields = [
|
||||
"name",
|
||||
"id",
|
||||
@@ -285,6 +317,7 @@ class ApplicationAdmin(admin.ModelAdmin):
|
||||
"scopes",
|
||||
"client_id",
|
||||
"client_secret",
|
||||
"is_active",
|
||||
]
|
||||
readonly_fields = ["id", "created_at", "updated_at"]
|
||||
inlines = [ApplicationDomainInline]
|
||||
|
||||
@@ -43,6 +43,21 @@ def get_frontend_configuration(request):
|
||||
"expiration_days": settings.RECORDING_EXPIRATION_DAYS,
|
||||
"max_duration": settings.RECORDING_MAX_DURATION,
|
||||
},
|
||||
"background_image": {
|
||||
"upload_is_enabled": settings.FILE_UPLOAD_ENABLED,
|
||||
"max_count_by_user": settings.FILE_UPLOAD_RESTRICTIONS["background_image"][
|
||||
"max_count_by_user"
|
||||
],
|
||||
"max_size": settings.FILE_UPLOAD_RESTRICTIONS["background_image"][
|
||||
"max_size"
|
||||
],
|
||||
"allowed_extensions": settings.FILE_UPLOAD_RESTRICTIONS["background_image"][
|
||||
"allowed_extensions"
|
||||
],
|
||||
"allowed_mimetypes": settings.FILE_UPLOAD_RESTRICTIONS["background_image"][
|
||||
"allowed_mimetypes"
|
||||
],
|
||||
},
|
||||
"telephony": {
|
||||
"enabled": settings.ROOM_TELEPHONY_ENABLED,
|
||||
"phone_number": settings.ROOM_TELEPHONY_PHONE_NUMBER
|
||||
|
||||
@@ -13,6 +13,7 @@ class FeatureFlag:
|
||||
"recording": "RECORDING_ENABLE",
|
||||
"storage_event": "RECORDING_STORAGE_EVENT_ENABLE",
|
||||
"subtitle": "ROOM_SUBTITLE_ENABLED",
|
||||
"file_upload": "FILE_UPLOAD_ENABLED",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Permission handlers for the Meet core app."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework import permissions
|
||||
@@ -116,6 +117,13 @@ class FilePermission(IsAuthenticated):
|
||||
Handling soft deletions specificities
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Allow access only to authenticated users."""
|
||||
if not settings.FILE_UPLOAD_ENABLED:
|
||||
raise Http404
|
||||
|
||||
return super().has_permission(request, view)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""
|
||||
Return a 404 on deleted files or if the user is not the owner
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Throttling modules for the API."""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from lasuite.drf.throttling import MonitoredThrottleMixin
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
|
||||
@@ -14,11 +16,58 @@ class MonitoredAnonRateThrottle(MonitoredThrottleMixin, AnonRateThrottle):
|
||||
"""Throttle for the monitored scoped rate throttle."""
|
||||
|
||||
|
||||
class MonitoredUserRateThrottle(MonitoredThrottleMixin, UserRateThrottle):
|
||||
"""Throttle for the monitored scoped rate throttle."""
|
||||
|
||||
|
||||
class RequestEntryAuthenticatedUserRateThrottle(MonitoredUserRateThrottle):
|
||||
"""Throttle authenticated user requesting room entry"""
|
||||
|
||||
scope = "request_entry"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
"""Use the authenticated user ID as the throttle cache key."""
|
||||
|
||||
if request.user and not request.user.is_authenticated:
|
||||
return None # Defer to RequestEntryAnonRateThrottle for anonymous users.
|
||||
|
||||
return super().get_cache_key(request, view)
|
||||
|
||||
|
||||
class RequestEntryAnonRateThrottle(MonitoredAnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room entry"""
|
||||
|
||||
scope = "request_entry"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
"""Use the lobby participant cookie ID as the throttle cache key.
|
||||
|
||||
Only throttle if a cookie is already set. If no cookie exists yet,
|
||||
return None to skip throttling — the cookie will be set on the first
|
||||
response, and throttling will apply from the second request onward.
|
||||
|
||||
Keying on the cookie rather than the IP address prevents penalising
|
||||
multiple users behind the same NAT/proxy, and is consistent with how
|
||||
LobbyService identifies participants.
|
||||
|
||||
Note: as per DRF documentation, application-level throttling is not a
|
||||
security measure against brute-force or DoS attacks. This throttle exists
|
||||
solely to guard against accidental hammering from buggy clients.
|
||||
"""
|
||||
|
||||
if request.user and request.user.is_authenticated:
|
||||
return None # Only throttle unauthenticated requests.
|
||||
|
||||
participant_id = request.COOKIES.get(settings.LOBBY_COOKIE_NAME)
|
||||
|
||||
if participant_id is None:
|
||||
return None # No throttling for cookieless requests
|
||||
|
||||
return self.cache_format % {
|
||||
"scope": self.scope,
|
||||
"ident": participant_id,
|
||||
}
|
||||
|
||||
|
||||
class CreationCallbackAnonRateThrottle(MonitoredAnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room generation callback"""
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""API endpoints"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from logging import getLogger
|
||||
from urllib.parse import unquote, urlparse
|
||||
@@ -12,6 +11,7 @@ from django.db.models import Q
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_filters import rest_framework as django_filters
|
||||
from rest_framework import (
|
||||
@@ -33,10 +33,12 @@ from rest_framework import (
|
||||
|
||||
from core import enums, models, utils
|
||||
from core.api.filters import ListFileFilter
|
||||
from core.enums import MEDIA_STORAGE_URL_PATTERN
|
||||
from core.recording.enums import FileExtension
|
||||
from core.recording.event.authentication import StorageEventAuthentication
|
||||
from core.recording.event.exceptions import (
|
||||
InvalidBucketError,
|
||||
InvalidFilepathError,
|
||||
InvalidFileTypeError,
|
||||
ParsingEventDataError,
|
||||
)
|
||||
@@ -78,17 +80,6 @@ from .feature_flag import FeatureFlag
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
FILE_FOLDER = settings.FILE_UPLOAD_PATH
|
||||
UUID_REGEX = (
|
||||
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
|
||||
)
|
||||
FILE_EXT_REGEX = r"[\d\w]+"
|
||||
MEDIA_STORAGE_URL_PATTERN = re.compile(
|
||||
f"{settings.MEDIA_URL:s}"
|
||||
rf"(?P<key>{FILE_FOLDER:s}/(?P<pk>{UUID_REGEX:s})/\.{FILE_EXT_REGEX:s})$"
|
||||
)
|
||||
|
||||
|
||||
class NestedGenericViewSet(viewsets.GenericViewSet):
|
||||
"""
|
||||
A generic Viewset aims to be used in a nested route context.
|
||||
@@ -224,6 +215,7 @@ class RoomViewSet(
|
||||
API endpoints to access and perform actions on rooms.
|
||||
"""
|
||||
|
||||
pagination_class = Pagination
|
||||
permission_classes = [permissions.RoomPermissions]
|
||||
queryset = models.Room.objects.all()
|
||||
serializer_class = serializers.RoomSerializer
|
||||
@@ -392,7 +384,10 @@ class RoomViewSet(
|
||||
methods=["post"],
|
||||
url_path="request-entry",
|
||||
permission_classes=[],
|
||||
throttle_classes=[throttling.RequestEntryAnonRateThrottle],
|
||||
throttle_classes=[
|
||||
throttling.RequestEntryAuthenticatedUserRateThrottle,
|
||||
throttling.RequestEntryAnonRateThrottle,
|
||||
],
|
||||
)
|
||||
def request_entry(self, request, pk=None): # pylint: disable=unused-argument
|
||||
"""Request entry to a room"""
|
||||
@@ -493,9 +488,7 @@ class RoomViewSet(
|
||||
if status_code == drf_status.HTTP_500_INTERNAL_SERVER_ERROR:
|
||||
raise e
|
||||
|
||||
return drf_response.Response(
|
||||
{"status": "error", "message": str(e)}, status=status_code
|
||||
)
|
||||
return drf_response.Response({"status": "error"}, status=status_code)
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
@@ -762,14 +755,19 @@ class RecordingViewSet(
|
||||
recording_id = parser.get_recording_id(request.data)
|
||||
|
||||
except ParsingEventDataError as e:
|
||||
raise drf_exceptions.PermissionDenied(f"Invalid request data: {e}") from e
|
||||
raise drf_exceptions.PermissionDenied("Invalid request data.") from e
|
||||
|
||||
except InvalidBucketError as e:
|
||||
raise drf_exceptions.PermissionDenied("Invalid bucket specified") from e
|
||||
raise drf_exceptions.PermissionDenied("Invalid bucket specified.") from e
|
||||
|
||||
except InvalidFileTypeError as e:
|
||||
except InvalidFilepathError:
|
||||
return drf_response.Response(
|
||||
{"message": f"Ignore this file type, {e}"},
|
||||
{"message": "Notification ignored."},
|
||||
)
|
||||
|
||||
except InvalidFileTypeError:
|
||||
return drf_response.Response(
|
||||
{"message": "Notification ignored."},
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -944,7 +942,7 @@ class FileViewSet(
|
||||
filterset_class = ListFileFilter
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get queryset that defaults to the the current request user."""
|
||||
"""Get queryset that defaults to the current request user."""
|
||||
user = self.request.user
|
||||
queryset = super().get_queryset().select_related("creator")
|
||||
|
||||
@@ -969,6 +967,26 @@ class FileViewSet(
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Set the current user as creator of the newly created file."""
|
||||
|
||||
if settings.FILE_UPLOAD_APPLY_RESTRICTIONS:
|
||||
file_type = serializer.validated_data["type"]
|
||||
config_for_file_type = settings.FILE_UPLOAD_RESTRICTIONS[file_type]
|
||||
|
||||
count = models.File.objects.filter(
|
||||
creator=self.request.user,
|
||||
deleted_at__isnull=True,
|
||||
type=file_type,
|
||||
).count()
|
||||
|
||||
if count >= config_for_file_type["max_count_by_user"]:
|
||||
logger.info(
|
||||
"create_item: user reached max files per user for type %s",
|
||||
file_type,
|
||||
)
|
||||
raise serializers.PermissionDenied(
|
||||
_("You have reached the maximum number of files for this type.")
|
||||
)
|
||||
|
||||
serializer.save(creator=self.request.user)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
@@ -976,6 +994,7 @@ class FileViewSet(
|
||||
instance.soft_delete()
|
||||
|
||||
@decorators.action(detail=True, methods=["post"], url_path="upload-ended")
|
||||
@FeatureFlag.require("file_upload")
|
||||
def upload_ended(self, request, *args, **kwargs):
|
||||
"""
|
||||
Check the actual uploaded file and mark it as ready.
|
||||
@@ -1158,6 +1177,7 @@ class FileViewSet(
|
||||
return url_params, request.user.id, file
|
||||
|
||||
@decorators.action(detail=False, methods=["get"], url_path="media-auth")
|
||||
@FeatureFlag.require("file_upload")
|
||||
def media_auth(self, request, *args, **kwargs):
|
||||
"""
|
||||
This view is used by an Nginx subrequest to control access to an file's
|
||||
|
||||
@@ -14,9 +14,15 @@ FILE_EXT_REGEX = r"[a-zA-Z0-9]{1,10}"
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
RECORDING_STORAGE_URL_PATTERN = re.compile(
|
||||
f"/media/{settings.RECORDING_OUTPUT_FOLDER}/(?P<recording_id>{UUID_REGEX:s}).(?P<extension>{FILE_EXT_REGEX:s})"
|
||||
rf"{settings.MEDIA_URL:s}{settings.RECORDING_OUTPUT_FOLDER}/(?P<recording_id>{UUID_REGEX:s})\.(?P<extension>{FILE_EXT_REGEX:s})"
|
||||
)
|
||||
|
||||
MEDIA_STORAGE_URL_PATTERN = re.compile(
|
||||
f"{settings.MEDIA_URL:s}"
|
||||
rf"(?P<key>{settings.FILE_UPLOAD_PATH:s}/(?P<pk>{UUID_REGEX:s})\.{FILE_EXT_REGEX:s})$"
|
||||
)
|
||||
|
||||
|
||||
# Django sets `LANGUAGES` by default with all supported languages. We can use it for
|
||||
# the choice of languages which should not be limited to the few languages active in
|
||||
# the app.
|
||||
|
||||
@@ -203,7 +203,7 @@ class ApplicationJWTAuthentication(BaseJWTAuthentication):
|
||||
logger.warning("Application not found: %s", client_id)
|
||||
raise exceptions.AuthenticationFailed("Application not found.") from e
|
||||
|
||||
if not application.active:
|
||||
if not application.is_active:
|
||||
logger.warning(
|
||||
"Inactive application attempted authentication: %s", client_id
|
||||
)
|
||||
|
||||
@@ -61,12 +61,12 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
except models.Application.DoesNotExist as e:
|
||||
raise drf_exceptions.AuthenticationFailed("Invalid credentials") from e
|
||||
|
||||
if not application.active:
|
||||
raise drf_exceptions.AuthenticationFailed("Application is inactive")
|
||||
|
||||
if not check_password(client_secret, application.client_secret):
|
||||
raise drf_exceptions.AuthenticationFailed("Invalid credentials")
|
||||
|
||||
if not application.is_active:
|
||||
raise drf_exceptions.AuthenticationFailed("Application is inactive")
|
||||
|
||||
email = serializer.validated_data["scope"]
|
||||
try:
|
||||
validate_email(email)
|
||||
|
||||
@@ -129,7 +129,7 @@ class ApplicationFactory(factory.django.DjangoModelFactory):
|
||||
model = models.Application
|
||||
|
||||
name = factory.Faker("company")
|
||||
active = True
|
||||
is_active = True
|
||||
client_id = factory.LazyFunction(utils.generate_client_id)
|
||||
client_secret = factory.LazyFunction(utils.generate_client_secret)
|
||||
scopes = []
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-11 14:39
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_file'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='application',
|
||||
old_name='active',
|
||||
new_name='is_active',
|
||||
),
|
||||
]
|
||||
@@ -759,7 +759,7 @@ class Application(BaseModel):
|
||||
verbose_name=_("Application name"),
|
||||
help_text=_("Descriptive name for this application."),
|
||||
)
|
||||
active = models.BooleanField(default=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
client_id = models.CharField(
|
||||
max_length=100, unique=True, default=utils.generate_client_id
|
||||
)
|
||||
@@ -952,7 +952,7 @@ class File(BaseModel):
|
||||
_, extension = splitext(self.filename)
|
||||
# We store only the extension in the storage system to avoid
|
||||
# leaking Personal Information in logs, etc.
|
||||
return f"{self.key_base}/{extension!s}"
|
||||
return f"{self.key_base}{extension!s}"
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
|
||||
@@ -9,6 +9,8 @@ from typing import Any, Dict, Optional, Protocol
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from core.enums import FILE_EXT_REGEX, UUID_REGEX
|
||||
|
||||
from .exceptions import (
|
||||
InvalidBucketError,
|
||||
InvalidFilepathError,
|
||||
@@ -86,7 +88,7 @@ class MinioParser:
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
self._filepath_regex = re.compile(
|
||||
r"(?P<url_encoded_folder_path>(?:[^%]+%2F)+)?(?P<recording_id>[0-9a-fA-F\-]{36})\.(?P<extension>[a-zA-Z0-9]+)"
|
||||
rf"(?P<url_encoded_folder_path>(?:[^%]+%2F)+)?{settings.RECORDING_OUTPUT_FOLDER}%2F(?P<recording_id>{UUID_REGEX})\.(?P<extension>{FILE_EXT_REGEX})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -16,7 +16,7 @@ class RecordingEventsError(Exception):
|
||||
|
||||
|
||||
class RecordingEventsService:
|
||||
"""Handles recording-related Livekit webhook events."""
|
||||
"""Handles recording-related LiveKit webhook events."""
|
||||
|
||||
@staticmethod
|
||||
def handle_update(recording, egress_status):
|
||||
|
||||
@@ -129,7 +129,7 @@ class LobbyService:
|
||||
) -> Tuple[LobbyParticipant, Optional[Dict]]:
|
||||
"""Request entry to a room for a participant.
|
||||
|
||||
This usual status transitions is:
|
||||
The usual status transitions are:
|
||||
UNKNOWN -> WAITING -> (ACCEPTED | DENIED)
|
||||
|
||||
Flow:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Custom template tags for the core application of People."""
|
||||
"""Custom template tags for the core application of Meet."""
|
||||
|
||||
import base64
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ def test_api_files_create_file_authenticated_success():
|
||||
|
||||
assert policy_parsed.scheme == "http"
|
||||
assert policy_parsed.netloc == "localhost:9000"
|
||||
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}/.png"
|
||||
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}.png"
|
||||
|
||||
query_params = parse_qs(policy_parsed.query)
|
||||
|
||||
@@ -174,6 +174,26 @@ def test_api_files_create_file_authenticated_extension_case_insensitive():
|
||||
assert file.title == "file"
|
||||
|
||||
|
||||
def test_api_files_create_file_disabled(settings):
|
||||
"""
|
||||
Creating a file is denied if file upload is disabled
|
||||
"""
|
||||
settings.FILE_UPLOAD_ENABLED = False
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
"/api/v1.0/files/",
|
||||
{
|
||||
"type": FileTypeChoices.BACKGROUND_IMAGE,
|
||||
"filename": "file.JPG",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert not File.objects.exists()
|
||||
|
||||
|
||||
def test_api_files_create_file_authenticated_not_checking_extension(settings):
|
||||
"""
|
||||
Creating a file with an extension not allowed should not fail when restrictions are disabled.
|
||||
@@ -239,6 +259,48 @@ def test_api_files_create_file_authenticated_hidden_file_but_checking_extension_
|
||||
assert response.json() == {"filename": ["This file extension is not allowed."]}
|
||||
|
||||
|
||||
def test_api_files_create_file_too_many(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
Creating a file is forbidden if above user limit.
|
||||
"""
|
||||
settings.FILE_UPLOAD_APPLY_RESTRICTIONS = True
|
||||
settings.FILE_UPLOAD_RESTRICTIONS = {
|
||||
"background_image": {
|
||||
**settings.FILE_UPLOAD_RESTRICTIONS["background_image"],
|
||||
"max_count_by_user": 1,
|
||||
},
|
||||
}
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
"/api/v1.0/files/",
|
||||
{
|
||||
"type": FileTypeChoices.BACKGROUND_IMAGE,
|
||||
"filename": "1.png",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/files/",
|
||||
{
|
||||
"type": FileTypeChoices.BACKGROUND_IMAGE,
|
||||
"filename": "2.png",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You have reached the maximum number of files for this type."
|
||||
}
|
||||
assert File.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_files_create_force_id_success():
|
||||
"""It should be possible to force the item ID when creating a item."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -39,7 +39,7 @@ def test_api_files_update_anonymous_forbidden():
|
||||
|
||||
def test_api_files_update_description_and_title():
|
||||
"""
|
||||
Test the description and title of an file can be updated.
|
||||
Test the description and title of a file can be updated.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def valid_minio_event():
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
"object": {
|
||||
"key": "recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
"key": "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
"contentType": "audio/ogg",
|
||||
},
|
||||
}
|
||||
@@ -51,7 +51,7 @@ def test_parse_valid_event(minio_parser, valid_minio_event):
|
||||
"""Test parsing a valid Minio event."""
|
||||
event = minio_parser.parse(valid_minio_event)
|
||||
assert isinstance(event, StorageEvent)
|
||||
assert event.filepath == "recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg"
|
||||
assert event.filepath == "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg"
|
||||
assert event.filetype == "audio/ogg"
|
||||
assert event.bucket_name == "test-bucket"
|
||||
assert event.metadata is None
|
||||
@@ -130,11 +130,13 @@ def test_validate_invalid_filetype(minio_parser):
|
||||
"invalid_filepath",
|
||||
[
|
||||
"invalid_filepath", # totally invalid string
|
||||
"recording/46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
"recording/46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing extension
|
||||
"recordings/46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
"recordings/46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing extension
|
||||
"46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing url_encoded_folder_path and extension
|
||||
"", # empty string
|
||||
"recording%2F46d1a1212426484d8fb309b5d886f7a8.ogg",
|
||||
"46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", # no folder at all
|
||||
"uploads%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", # wrong folder name
|
||||
"folder%2Fuploads%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", # nested but no recordings/
|
||||
],
|
||||
)
|
||||
def test_validate_invalid_filepath(invalid_filepath, minio_parser):
|
||||
@@ -152,7 +154,7 @@ def test_validate_invalid_filepath(invalid_filepath, minio_parser):
|
||||
def test_validate_valid_event(minio_parser):
|
||||
"""Test validation with valid event data."""
|
||||
event = StorageEvent(
|
||||
filepath="recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
filepath="recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
filetype="audio/ogg",
|
||||
bucket_name="test-bucket",
|
||||
metadata=None,
|
||||
@@ -170,7 +172,7 @@ def test_get_recording_id_success(minio_parser, valid_minio_event):
|
||||
def test_validate_filepath_with_folder(minio_parser):
|
||||
"""Test validation of filepath with folder structure."""
|
||||
event = StorageEvent(
|
||||
filepath="parent_folder%2Ffolder%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
filepath="parent_folder%2Frecordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
filetype="audio/ogg",
|
||||
bucket_name="test-bucket",
|
||||
metadata=None,
|
||||
@@ -219,7 +221,7 @@ def test_validate_custom_filetypes():
|
||||
parser = MinioParser(bucket_name="test-bucket", allowed_filetypes={"audio/mp3"})
|
||||
|
||||
event = StorageEvent(
|
||||
filepath="parent_folder%2Ffolder%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
filepath="parent_folder%2Frecordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
filetype="audio/mp3",
|
||||
bucket_name="test-bucket",
|
||||
metadata=None,
|
||||
|
||||
@@ -14,6 +14,7 @@ from ...factories import RecordingFactory
|
||||
from ...models import Recording, RecordingStatusChoices
|
||||
from ...recording.event.exceptions import (
|
||||
InvalidBucketError,
|
||||
InvalidFilepathError,
|
||||
InvalidFileTypeError,
|
||||
ParsingEventDataError,
|
||||
)
|
||||
@@ -94,7 +95,7 @@ def test_save_recording_parsing_error(recording_settings, mock_get_parser, clien
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid request data: Error message"}
|
||||
assert response.json() == {"detail": "Invalid request data."}
|
||||
|
||||
|
||||
def test_save_recording_bucket_error(recording_settings, mock_get_parser, client):
|
||||
@@ -111,7 +112,7 @@ def test_save_recording_bucket_error(recording_settings, mock_get_parser, client
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid bucket specified"}
|
||||
assert response.json() == {"detail": "Invalid bucket specified."}
|
||||
|
||||
|
||||
def test_save_recording_filetype_error(recording_settings, mock_get_parser):
|
||||
@@ -132,7 +133,28 @@ def test_save_recording_filetype_error(recording_settings, mock_get_parser):
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Ignore this file type, unsupported '.json'"}
|
||||
assert response.json() == {"message": "Notification ignored."}
|
||||
|
||||
|
||||
def test_save_recording_filepath_error(recording_settings, mock_get_parser):
|
||||
"""Test handling of unsupported filepath in recording event data."""
|
||||
|
||||
mock_parser = mock.Mock()
|
||||
mock_parser.get_recording_id.side_effect = InvalidFilepathError(
|
||||
"Invalid filepath structure: parent/folder/recording.jpeg"
|
||||
)
|
||||
mock_get_parser.return_value = mock_parser
|
||||
|
||||
client = APIClient()
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/storage-hook/",
|
||||
{"recording_data": "valid-data"},
|
||||
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Notification ignored."}
|
||||
|
||||
|
||||
def test_save_recording_unknown_recording(recording_settings, mock_get_parser, client):
|
||||
|
||||
@@ -120,3 +120,39 @@ def test_api_rooms_list_authenticated_distinct():
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 1
|
||||
assert content["results"][0]["id"] == str(room.id)
|
||||
|
||||
|
||||
def test_api_rooms_list_pagination_page_size():
|
||||
"""Users should be able to customize the number of results per page via page_size param."""
|
||||
user = UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
RoomFactory.create_batch(11, users=[user])
|
||||
|
||||
response = client.get("/api/v1.0/rooms/?page_size=2")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert content["count"] == 11
|
||||
assert len(content["results"]) == 2
|
||||
assert content["next"] == "http://testserver/api/v1.0/rooms/?page=2&page_size=2"
|
||||
assert content["previous"] is None
|
||||
|
||||
response = client.get("/api/v1.0/rooms/?page=6&page_size=2")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert content["count"] == 11
|
||||
assert len(content["results"]) == 1
|
||||
assert content["next"] is None
|
||||
assert content["previous"] == "http://testserver/api/v1.0/rooms/?page=5&page_size=2"
|
||||
|
||||
response = client.get("/api/v1.0/rooms/?page_size=3")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert content["count"] == 11
|
||||
assert len(content["results"]) == 3
|
||||
assert content["next"] == "http://testserver/api/v1.0/rooms/?page=2&page_size=3"
|
||||
assert content["previous"] is None
|
||||
|
||||
@@ -631,3 +631,109 @@ def test_list_waiting_participants_empty(settings):
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"participants": []}
|
||||
|
||||
|
||||
@mock.patch.object(utils, "notify_participants", return_value=None)
|
||||
@mock.patch.object(
|
||||
utils, "generate_livekit_config", return_value={"token": "test-token"}
|
||||
)
|
||||
def test_request_entry_throttling_anonymous_without_cookie(
|
||||
mock_notify_participants, mock_generate_livekit_config, settings
|
||||
):
|
||||
"""Anonymous users without a cookie should not be throttled."""
|
||||
|
||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
client = APIClient()
|
||||
|
||||
settings.LOBBY_COOKIE_NAME = "mocked-cookie"
|
||||
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["request_entry"] = "1/minute"
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.cookies.get("mocked-cookie") is not None
|
||||
|
||||
client.cookies.clear() # Simulate a new cookieless request
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@mock.patch.object(utils, "notify_participants", return_value=None)
|
||||
@mock.patch.object(
|
||||
utils, "generate_livekit_config", return_value={"token": "test-token"}
|
||||
)
|
||||
def test_request_entry_throttling_anonymous_with_cookie(
|
||||
mock_notify_participants, mock_generate_livekit_config, settings
|
||||
):
|
||||
"""Anonymous users with a cookie should be throttled after exceeding the rate limit."""
|
||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
client = APIClient()
|
||||
|
||||
settings.LOBBY_COOKIE_NAME = "mocked-cookie"
|
||||
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["request_entry"] = "2/minute"
|
||||
|
||||
participant_id = str(uuid.uuid4())
|
||||
client.cookies.load({"mocked-cookie": participant_id})
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
|
||||
assert response.status_code == 429
|
||||
|
||||
|
||||
@mock.patch.object(utils, "notify_participants", return_value=None)
|
||||
@mock.patch.object(
|
||||
utils, "generate_livekit_config", return_value={"token": "test-token"}
|
||||
)
|
||||
def test_request_entry_throttling_authenticated_user(
|
||||
mock_notify_participants, mock_generate_livekit_config, settings
|
||||
):
|
||||
"""Authenticated users should be throttled."""
|
||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||
user = UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.LOBBY_COOKIE_NAME = "mocked-cookie"
|
||||
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["request_entry"] = "2/minute"
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/request-entry/",
|
||||
{"username": "test_user"},
|
||||
)
|
||||
|
||||
assert response.status_code == 429
|
||||
|
||||
@@ -77,7 +77,6 @@ def test_missing_auth_header(client, serialized_event_data, mock_livekit_config)
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"status": "error",
|
||||
"message": "Authorization header missing",
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +90,7 @@ def test_invalid_payload(client, auth_token, mock_livekit_config):
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"status": "error", "message": "Invalid webhook payload"}
|
||||
assert response.json() == {"status": "error"}
|
||||
|
||||
|
||||
def test_unknown_event_type(client, mock_livekit_config):
|
||||
@@ -116,7 +115,6 @@ def test_unknown_event_type(client, mock_livekit_config):
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"status": "error",
|
||||
"message": "Unknown webhook type: unknown_event_type",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -904,7 +904,7 @@ def test_api_rooms_token_unknown_application(settings):
|
||||
|
||||
def test_api_rooms_token_inactive_application(settings):
|
||||
"""Token for inactive application should be rejected."""
|
||||
application = ApplicationFactory(active=False)
|
||||
application = ApplicationFactory(is_active=False)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
payload = {
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_api_applications_generate_token_success(settings):
|
||||
"""Valid credentials should return a JWT token."""
|
||||
UserFactory(email="User.Family@example.com")
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
is_active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
)
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_api_applications_generate_token_invalid_client_id():
|
||||
def test_api_applications_generate_token_invalid_client_secret():
|
||||
"""Invalid client_secret should return 401."""
|
||||
user = UserFactory(email="user@example.com")
|
||||
application = ApplicationFactory(active=True)
|
||||
application = ApplicationFactory(is_active=True)
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
@@ -100,7 +100,7 @@ def test_api_applications_generate_token_invalid_client_secret():
|
||||
def test_api_applications_generate_token_inactive_application():
|
||||
"""Inactive application should return 401."""
|
||||
user = UserFactory(email="user@example.com")
|
||||
application = ApplicationFactory(active=False)
|
||||
application = ApplicationFactory(is_active=False)
|
||||
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
@@ -122,9 +122,31 @@ def test_api_applications_generate_token_inactive_application():
|
||||
assert "Application is inactive" in str(response.data)
|
||||
|
||||
|
||||
def test_api_applications_generate_token_inactive_application_wrong_secret():
|
||||
"""An inactive application with a wrong secret should return 401."""
|
||||
user = UserFactory(email="user@example.com")
|
||||
application = ApplicationFactory(is_active=False)
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/",
|
||||
{
|
||||
"client_id": application.client_id,
|
||||
"client_secret": "wrong-secret",
|
||||
"grant_type": "client_credentials",
|
||||
"scope": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Invalid credentials" in str(response.data)
|
||||
assert "inactive" not in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_applications_generate_token_invalid_email_format():
|
||||
"""Invalid email format should return 400."""
|
||||
application = ApplicationFactory(active=True)
|
||||
application = ApplicationFactory(is_active=True)
|
||||
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
@@ -149,7 +171,7 @@ def test_api_applications_generate_token_invalid_email_format():
|
||||
def test_api_applications_generate_token_domain_not_authorized():
|
||||
"""Application without domain authorization should return 403."""
|
||||
user = UserFactory(email="user@denied.com")
|
||||
application = ApplicationFactory(active=True)
|
||||
application = ApplicationFactory(is_active=True)
|
||||
ApplicationDomainFactory(application=application, domain="allowed.com")
|
||||
|
||||
plain_secret = "test-secret-123"
|
||||
@@ -176,7 +198,7 @@ def test_api_applications_generate_token_domain_authorized():
|
||||
"""Application with domain authorization should succeed."""
|
||||
user = UserFactory(email="user@allowed.com")
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
is_active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST],
|
||||
)
|
||||
ApplicationDomainFactory(application=application, domain="allowed.com")
|
||||
@@ -203,7 +225,7 @@ def test_api_applications_generate_token_domain_authorized():
|
||||
|
||||
def test_api_applications_generate_token_user_not_found():
|
||||
"""Non-existent user should return 404."""
|
||||
application = ApplicationFactory(active=True)
|
||||
application = ApplicationFactory(is_active=True)
|
||||
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
@@ -231,7 +253,7 @@ def test_api_applications_token_payload_structure(settings):
|
||||
user = UserFactory(email="user@example.com")
|
||||
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
is_active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
)
|
||||
|
||||
@@ -284,7 +306,7 @@ def test_api_applications_token_new_user(settings):
|
||||
assert len(User.objects.all()) == 0
|
||||
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
is_active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
)
|
||||
|
||||
@@ -342,7 +364,7 @@ def test_api_applications_token_existing_user(settings):
|
||||
assert len(User.objects.all()) == 1
|
||||
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
is_active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_models_application_name_maxlength():
|
||||
def test_models_application_active_default():
|
||||
"""An application should be active by default."""
|
||||
application = Application.objects.create(name="Test App")
|
||||
assert application.active is True
|
||||
assert application.is_active is True
|
||||
|
||||
|
||||
def test_models_application_scopes_default():
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-26 17:34+0000\n"
|
||||
"POT-Creation-Date: 2026-03-12 13:46+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -29,11 +29,11 @@ msgstr "Berechtigungen"
|
||||
msgid "Important dates"
|
||||
msgstr "Wichtige Daten"
|
||||
|
||||
#: core/admin.py:132 core/admin.py:243
|
||||
#: core/admin.py:132 core/admin.py:275
|
||||
msgid "No owner"
|
||||
msgstr "Kein Eigentümer"
|
||||
|
||||
#: core/admin.py:135 core/admin.py:246
|
||||
#: core/admin.py:135 core/admin.py:278
|
||||
msgid "Multiple owners"
|
||||
msgstr "Mehrere Eigentümer"
|
||||
|
||||
@@ -61,117 +61,136 @@ msgstr "Benachrichtigungen für %(count)s Aufnahme(n) erfolgreich gesendet."
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s abgelaufene Aufnahme(n) übersprungen."
|
||||
|
||||
#: core/admin.py:309
|
||||
#: core/admin.py:200
|
||||
msgid "Mark selected recordings as 'Failed to Stop'"
|
||||
msgstr "Ausgewählte Aufnahmen als ‚Fehler beim Stoppen‘ markieren"
|
||||
|
||||
#: core/admin.py:218
|
||||
#, python-format
|
||||
msgid "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
||||
msgstr "%(count)s Aufnahme(n) erfolgreich als ‚Fehler beim Stoppen‘ markiert."
|
||||
|
||||
#: core/admin.py:226
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Skipped %(count)s expired recording(s)."
|
||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||
msgstr "%(count)s abgelaufene Aufnahme(n) übersprungen."
|
||||
|
||||
#: core/admin.py:342
|
||||
msgid "No scopes"
|
||||
msgstr "Keine Scopes"
|
||||
|
||||
#: core/admin.py:311
|
||||
#: core/admin.py:344
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/filters.py:24
|
||||
#: core/api/filters.py:25
|
||||
msgid "Creator is me"
|
||||
msgstr "Ersteller bin ich"
|
||||
|
||||
#: core/api/serializers.py:84
|
||||
#: core/api/serializers.py:88
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
|
||||
"hinzuzufügen."
|
||||
|
||||
#: core/api/serializers.py:443
|
||||
#: core/api/serializers.py:516
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "Diese Dateiendung ist nicht erlaubt."
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/api/serializers.py:533
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "Sie haben die maximale Anzahl an Dateien dieses Typs erreicht."
|
||||
|
||||
#: core/models.py:37
|
||||
msgid "Member"
|
||||
msgstr "Mitglied"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:38
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:37
|
||||
#: core/models.py:39
|
||||
msgid "Owner"
|
||||
msgstr "Eigentümer"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:55
|
||||
msgid "Initiated"
|
||||
msgstr "Gestartet"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:56
|
||||
msgid "Active"
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:57
|
||||
msgid "Stopped"
|
||||
msgstr "Beendet"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:58
|
||||
msgid "Saved"
|
||||
msgstr "Gespeichert"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:59
|
||||
msgid "Aborted"
|
||||
msgstr "Abgebrochen"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:60
|
||||
msgid "Failed to Start"
|
||||
msgstr "Start fehlgeschlagen"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:61
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stopp fehlgeschlagen"
|
||||
|
||||
#: core/models.py:60
|
||||
#: core/models.py:62
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Benachrichtigung erfolgreich"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:89
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "BILDSCHIRMAUFZEICHNUNG"
|
||||
|
||||
#: core/models.py:88
|
||||
#: core/models.py:90
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSKRIPT"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:96
|
||||
msgid "Public Access"
|
||||
msgstr "Öffentlicher Zugriff"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:97
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrauenswürdiger Zugriff"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:98
|
||||
msgid "Restricted Access"
|
||||
msgstr "Eingeschränkter Zugriff"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:110
|
||||
msgid "id"
|
||||
msgstr "ID"
|
||||
|
||||
#: core/models.py:109
|
||||
#: core/models.py:111
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "Primärschlüssel des Eintrags als UUID"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:117
|
||||
msgid "created on"
|
||||
msgstr "erstellt am"
|
||||
|
||||
#: core/models.py:116
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:123
|
||||
msgid "updated on"
|
||||
msgstr "aktualisiert am"
|
||||
|
||||
#: core/models.py:122
|
||||
#: core/models.py:124
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
|
||||
|
||||
#: core/models.py:142
|
||||
#: core/models.py:144
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -179,11 +198,11 @@ msgstr ""
|
||||
"Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen "
|
||||
"und die Zeichen @/./+/-/_ enthalten."
|
||||
|
||||
#: core/models.py:148
|
||||
#: core/models.py:150
|
||||
msgid "sub"
|
||||
msgstr "Sub"
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:152
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -191,55 +210,55 @@ msgstr ""
|
||||
"Optional für ausstehende Benutzer; erforderlich nach Kontoaktivierung. "
|
||||
"Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ Zeichen erlaubt."
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:161
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:164
|
||||
#: core/models.py:166
|
||||
msgid "admin email address"
|
||||
msgstr "Administrator-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:168
|
||||
msgid "full name"
|
||||
msgstr "Vollständiger Name"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:170
|
||||
msgid "short name"
|
||||
msgstr "Kurzname"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:176
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: core/models.py:175
|
||||
#: core/models.py:177
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "Mitarbeiterstatus"
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
|
||||
|
||||
#: core/models.py:194
|
||||
#: core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: core/models.py:197
|
||||
#: core/models.py:199
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -247,66 +266,66 @@ msgstr ""
|
||||
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
|
||||
"anstelle des Löschens des Kontos."
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:212
|
||||
msgid "user"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:211
|
||||
#: core/models.py:213
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:272
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:271
|
||||
#: core/models.py:273
|
||||
msgid "Resources"
|
||||
msgstr "Ressourcen"
|
||||
|
||||
#: core/models.py:329
|
||||
#: core/models.py:331
|
||||
msgid "Resource access"
|
||||
msgstr "Ressourcenzugriff"
|
||||
|
||||
#: core/models.py:330
|
||||
#: core/models.py:332
|
||||
msgid "Resource accesses"
|
||||
msgstr "Ressourcenzugriffe"
|
||||
|
||||
#: core/models.py:336
|
||||
#: core/models.py:338
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
|
||||
"bereits."
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:394
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-Raumkonfiguration"
|
||||
|
||||
#: core/models.py:393
|
||||
#: core/models.py:395
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
|
||||
|
||||
#: core/models.py:400
|
||||
#: core/models.py:402
|
||||
msgid "Room PIN code"
|
||||
msgstr "PIN-Code für den Raum"
|
||||
|
||||
#: core/models.py:401
|
||||
#: core/models.py:403
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
|
||||
|
||||
#: core/models.py:407 core/models.py:561
|
||||
#: core/models.py:409 core/models.py:563
|
||||
msgid "Room"
|
||||
msgstr "Raum"
|
||||
|
||||
#: core/models.py:408
|
||||
#: core/models.py:410
|
||||
msgid "Rooms"
|
||||
msgstr "Räume"
|
||||
|
||||
#: core/models.py:572
|
||||
#: core/models.py:574
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker-ID"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:576
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -315,120 +334,120 @@ msgstr ""
|
||||
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
|
||||
"ermöglicht."
|
||||
|
||||
#: core/models.py:582
|
||||
#: core/models.py:584
|
||||
msgid "Recording mode"
|
||||
msgstr "Aufzeichnungsmodus"
|
||||
|
||||
#: core/models.py:583
|
||||
#: core/models.py:585
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
|
||||
|
||||
#: core/models.py:588 core/models.py:589
|
||||
#: core/models.py:590 core/models.py:591
|
||||
msgid "Recording options"
|
||||
msgstr "Aufnahmeoptionen"
|
||||
|
||||
#: core/models.py:595
|
||||
#: core/models.py:597
|
||||
msgid "Recording"
|
||||
msgstr "Aufzeichnung"
|
||||
|
||||
#: core/models.py:596
|
||||
#: core/models.py:598
|
||||
msgid "Recordings"
|
||||
msgstr "Aufzeichnungen"
|
||||
|
||||
#: core/models.py:704
|
||||
#: core/models.py:706
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Beziehung Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:705
|
||||
#: core/models.py:707
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Beziehungen Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:711
|
||||
#: core/models.py:713
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:717
|
||||
#: core/models.py:719
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:723
|
||||
#: core/models.py:725
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
|
||||
|
||||
#: core/models.py:740
|
||||
#: core/models.py:742
|
||||
msgid "Create rooms"
|
||||
msgstr "Räume erstellen"
|
||||
|
||||
#: core/models.py:741
|
||||
#: core/models.py:743
|
||||
msgid "List rooms"
|
||||
msgstr "Räume auflisten"
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:744
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Raumdetails abrufen"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:745
|
||||
msgid "Update rooms"
|
||||
msgstr "Räume aktualisieren"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:746
|
||||
msgid "Delete rooms"
|
||||
msgstr "Räume löschen"
|
||||
|
||||
#: core/models.py:757
|
||||
#: core/models.py:759
|
||||
msgid "Application name"
|
||||
msgstr "Anwendungsname"
|
||||
|
||||
#: core/models.py:758
|
||||
#: core/models.py:760
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschreibender Name für diese Anwendung."
|
||||
|
||||
#: core/models.py:768
|
||||
#: core/models.py:770
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist."
|
||||
|
||||
#: core/models.py:779
|
||||
#: core/models.py:781
|
||||
msgid "Application"
|
||||
msgstr "Anwendung"
|
||||
|
||||
#: core/models.py:780
|
||||
#: core/models.py:782
|
||||
msgid "Applications"
|
||||
msgstr "Anwendungen"
|
||||
|
||||
#: core/models.py:803
|
||||
#: core/models.py:805
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Geben Sie eine gültige Domain ein"
|
||||
|
||||
#: core/models.py:806
|
||||
#: core/models.py:808
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:807
|
||||
#: core/models.py:809
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann."
|
||||
|
||||
#: core/models.py:819
|
||||
#: core/models.py:821
|
||||
msgid "Application domain"
|
||||
msgstr "Anwendungsdomain"
|
||||
|
||||
#: core/models.py:820
|
||||
#: core/models.py:822
|
||||
msgid "Application domains"
|
||||
msgstr "Anwendungsdomains"
|
||||
|
||||
#: core/models.py:838
|
||||
#: core/models.py:840
|
||||
msgid "Pending"
|
||||
msgstr "Ausstehend"
|
||||
|
||||
#: core/models.py:846
|
||||
#: core/models.py:848
|
||||
msgid "Ready"
|
||||
msgstr "Bereit"
|
||||
|
||||
#: core/models.py:852
|
||||
#: core/models.py:854
|
||||
msgid "Background image"
|
||||
msgstr "Hintergrundbild"
|
||||
|
||||
#: core/models.py:864
|
||||
#: core/models.py:866
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -445,11 +464,11 @@ msgstr "Datei"
|
||||
msgid "Files"
|
||||
msgstr "Dateien"
|
||||
|
||||
#: core/models.py:970
|
||||
#: core/models.py:1000
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "Diese Datei wurde bereits endgültig gelöscht."
|
||||
|
||||
#: core/models.py:980
|
||||
#: core/models.py:1010
|
||||
#, fuzzy
|
||||
#| msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
@@ -585,18 +604,18 @@ msgstr ""
|
||||
" Wenn Sie Fragen haben oder Unterstützung benötigen, wenden Sie sich bitte "
|
||||
"an unser Support-Team unter %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:224
|
||||
#: meet/settings.py:223
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: meet/settings.py:225
|
||||
#: meet/settings.py:224
|
||||
msgid "French"
|
||||
msgstr "Französisch"
|
||||
|
||||
#: meet/settings.py:226
|
||||
#: meet/settings.py:225
|
||||
msgid "Dutch"
|
||||
msgstr "Niederländisch"
|
||||
|
||||
#: meet/settings.py:227
|
||||
#: meet/settings.py:226
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-26 17:26+0000\n"
|
||||
"POT-Creation-Date: 2026-03-12 13:46+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -29,11 +29,11 @@ msgstr "Permissions"
|
||||
msgid "Important dates"
|
||||
msgstr "Important dates"
|
||||
|
||||
#: core/admin.py:132 core/admin.py:243
|
||||
#: core/admin.py:132 core/admin.py:275
|
||||
msgid "No owner"
|
||||
msgstr "No owner"
|
||||
|
||||
#: core/admin.py:135 core/admin.py:246
|
||||
#: core/admin.py:135 core/admin.py:278
|
||||
msgid "Multiple owners"
|
||||
msgstr "Multiple owners"
|
||||
|
||||
@@ -61,115 +61,134 @@ msgstr "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "Skipped %(count)s expired recording(s)."
|
||||
|
||||
#: core/admin.py:309
|
||||
#: core/admin.py:200
|
||||
msgid "Mark selected recordings as 'Failed to Stop'"
|
||||
msgstr "Mark selected recordings as 'Failed to Stop'"
|
||||
|
||||
#: core/admin.py:218
|
||||
#, python-format
|
||||
msgid "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
||||
msgstr "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
||||
|
||||
#: core/admin.py:226
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Skipped %(count)s expired recording(s)."
|
||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||
msgstr "Skipped %(count)s expired recording(s)."
|
||||
|
||||
#: core/admin.py:342
|
||||
msgid "No scopes"
|
||||
msgstr "No scopes"
|
||||
|
||||
#: core/admin.py:311
|
||||
#: core/admin.py:344
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/filters.py:24
|
||||
#: core/api/filters.py:25
|
||||
msgid "Creator is me"
|
||||
msgstr "Creator is me"
|
||||
|
||||
#: core/api/serializers.py:84
|
||||
#: core/api/serializers.py:88
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr "You must be administrator or owner of a room to add accesses to it."
|
||||
|
||||
#: core/api/serializers.py:443
|
||||
#: core/api/serializers.py:516
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "This file extension is not allowed."
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/api/serializers.py:533
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "You have reached the maximum number of files for this type."
|
||||
|
||||
#: core/models.py:37
|
||||
msgid "Member"
|
||||
msgstr "Member"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:38
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:37
|
||||
#: core/models.py:39
|
||||
msgid "Owner"
|
||||
msgstr "Owner"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:55
|
||||
msgid "Initiated"
|
||||
msgstr "Initiated"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:56
|
||||
msgid "Active"
|
||||
msgstr "Active"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:57
|
||||
msgid "Stopped"
|
||||
msgstr "Stopped"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:58
|
||||
msgid "Saved"
|
||||
msgstr "Saved"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:59
|
||||
msgid "Aborted"
|
||||
msgstr "Aborted"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:60
|
||||
msgid "Failed to Start"
|
||||
msgstr "Failed to Start"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:61
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Failed to Stop"
|
||||
|
||||
#: core/models.py:60
|
||||
#: core/models.py:62
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification succeeded"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:89
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCREEN_RECORDING"
|
||||
|
||||
#: core/models.py:88
|
||||
#: core/models.py:90
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:96
|
||||
msgid "Public Access"
|
||||
msgstr "Public Access"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:97
|
||||
msgid "Trusted Access"
|
||||
msgstr "Trusted Access"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:98
|
||||
msgid "Restricted Access"
|
||||
msgstr "Restricted Access"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:110
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:109
|
||||
#: core/models.py:111
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primary key for the record as UUID"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:117
|
||||
msgid "created on"
|
||||
msgstr "created on"
|
||||
|
||||
#: core/models.py:116
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date and time at which a record was created"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:123
|
||||
msgid "updated on"
|
||||
msgstr "updated on"
|
||||
|
||||
#: core/models.py:122
|
||||
#: core/models.py:124
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date and time at which a record was last updated"
|
||||
|
||||
#: core/models.py:142
|
||||
#: core/models.py:144
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -177,11 +196,11 @@ msgstr ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
|
||||
#: core/models.py:148
|
||||
#: core/models.py:150
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:152
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -189,55 +208,55 @@ msgstr ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:161
|
||||
msgid "identity email address"
|
||||
msgstr "identity email address"
|
||||
|
||||
#: core/models.py:164
|
||||
#: core/models.py:166
|
||||
msgid "admin email address"
|
||||
msgstr "admin email address"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:168
|
||||
msgid "full name"
|
||||
msgstr "full name"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:170
|
||||
msgid "short name"
|
||||
msgstr "short name"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:176
|
||||
msgid "language"
|
||||
msgstr "language"
|
||||
|
||||
#: core/models.py:175
|
||||
#: core/models.py:177
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "The language in which the user wants to see the interface."
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "The timezone in which the user wants to see times."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "device"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Whether the user is a device or a real user."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "staff status"
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Whether the user can log into this admin site."
|
||||
|
||||
#: core/models.py:194
|
||||
#: core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "active"
|
||||
|
||||
#: core/models.py:197
|
||||
#: core/models.py:199
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -245,63 +264,63 @@ msgstr ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:212
|
||||
msgid "user"
|
||||
msgstr "user"
|
||||
|
||||
#: core/models.py:211
|
||||
#: core/models.py:213
|
||||
msgid "users"
|
||||
msgstr "users"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:272
|
||||
msgid "Resource"
|
||||
msgstr "Resource"
|
||||
|
||||
#: core/models.py:271
|
||||
#: core/models.py:273
|
||||
msgid "Resources"
|
||||
msgstr "Resources"
|
||||
|
||||
#: core/models.py:329
|
||||
#: core/models.py:331
|
||||
msgid "Resource access"
|
||||
msgstr "Resource access"
|
||||
|
||||
#: core/models.py:330
|
||||
#: core/models.py:332
|
||||
msgid "Resource accesses"
|
||||
msgstr "Resource accesses"
|
||||
|
||||
#: core/models.py:336
|
||||
#: core/models.py:338
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Resource access with this User and Resource already exists."
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:394
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio room configuration"
|
||||
|
||||
#: core/models.py:393
|
||||
#: core/models.py:395
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Values for Visio parameters to configure the room."
|
||||
|
||||
#: core/models.py:400
|
||||
#: core/models.py:402
|
||||
msgid "Room PIN code"
|
||||
msgstr "Room PIN code"
|
||||
|
||||
#: core/models.py:401
|
||||
#: core/models.py:403
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Unique n-digit code that identifies this room in telephony mode."
|
||||
|
||||
#: core/models.py:407 core/models.py:561
|
||||
#: core/models.py:409 core/models.py:563
|
||||
msgid "Room"
|
||||
msgstr "Room"
|
||||
|
||||
#: core/models.py:408
|
||||
#: core/models.py:410
|
||||
msgid "Rooms"
|
||||
msgstr "Rooms"
|
||||
|
||||
#: core/models.py:572
|
||||
#: core/models.py:574
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:576
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -309,125 +328,125 @@ msgstr ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
|
||||
#: core/models.py:582
|
||||
#: core/models.py:584
|
||||
msgid "Recording mode"
|
||||
msgstr "Recording mode"
|
||||
|
||||
#: core/models.py:583
|
||||
#: core/models.py:585
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Defines the mode of recording being called."
|
||||
|
||||
#: core/models.py:588 core/models.py:589
|
||||
#: core/models.py:590 core/models.py:591
|
||||
msgid "Recording options"
|
||||
msgstr "Recording options"
|
||||
|
||||
#: core/models.py:595
|
||||
#: core/models.py:597
|
||||
msgid "Recording"
|
||||
msgstr "Recording"
|
||||
|
||||
#: core/models.py:596
|
||||
#: core/models.py:598
|
||||
msgid "Recordings"
|
||||
msgstr "Recordings"
|
||||
|
||||
#: core/models.py:704
|
||||
#: core/models.py:706
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Recording/user relation"
|
||||
|
||||
#: core/models.py:705
|
||||
#: core/models.py:707
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Recording/user relations"
|
||||
|
||||
#: core/models.py:711
|
||||
#: core/models.py:713
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "This user is already in this recording."
|
||||
|
||||
#: core/models.py:717
|
||||
#: core/models.py:719
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "This team is already in this recording."
|
||||
|
||||
#: core/models.py:723
|
||||
#: core/models.py:725
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Either user or team must be set, not both."
|
||||
|
||||
#: core/models.py:740
|
||||
#: core/models.py:742
|
||||
#, fuzzy
|
||||
#| msgid "created on"
|
||||
msgid "Create rooms"
|
||||
msgstr "Create rooms"
|
||||
|
||||
#: core/models.py:741
|
||||
#: core/models.py:743
|
||||
msgid "List rooms"
|
||||
msgstr "List rooms"
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:744
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Retrieve room details"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:745
|
||||
#, fuzzy
|
||||
#| msgid "updated on"
|
||||
msgid "Update rooms"
|
||||
msgstr "Update rooms"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:746
|
||||
msgid "Delete rooms"
|
||||
msgstr "Delete rooms"
|
||||
|
||||
#: core/models.py:757
|
||||
#: core/models.py:759
|
||||
msgid "Application name"
|
||||
msgstr "Application name"
|
||||
|
||||
#: core/models.py:758
|
||||
#: core/models.py:760
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Descriptive name for this application."
|
||||
|
||||
#: core/models.py:768
|
||||
#: core/models.py:770
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr "Hashed on Save. Copy it now if this is a new secret."
|
||||
|
||||
#: core/models.py:779
|
||||
#: core/models.py:781
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:780
|
||||
#: core/models.py:782
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:803
|
||||
#: core/models.py:805
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Enter a valid domain"
|
||||
|
||||
#: core/models.py:806
|
||||
#: core/models.py:808
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:807
|
||||
#: core/models.py:809
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Email domain this application can act on behalf of."
|
||||
|
||||
#: core/models.py:819
|
||||
#: core/models.py:821
|
||||
msgid "Application domain"
|
||||
msgstr "Application domain"
|
||||
|
||||
#: core/models.py:820
|
||||
#: core/models.py:822
|
||||
msgid "Application domains"
|
||||
msgstr "Application domains"
|
||||
|
||||
#: core/models.py:838
|
||||
#: core/models.py:840
|
||||
#, fuzzy
|
||||
#| msgid "Recording"
|
||||
msgid "Pending"
|
||||
msgstr "Pending"
|
||||
|
||||
#: core/models.py:846
|
||||
#: core/models.py:848
|
||||
msgid "Ready"
|
||||
msgstr "Ready"
|
||||
|
||||
#: core/models.py:852
|
||||
#: core/models.py:854
|
||||
msgid "Background image"
|
||||
msgstr "Background image"
|
||||
|
||||
#: core/models.py:864
|
||||
#: core/models.py:866
|
||||
msgid "title"
|
||||
msgstr "title"
|
||||
|
||||
@@ -443,13 +462,13 @@ msgstr "File"
|
||||
msgid "Files"
|
||||
msgstr "Files"
|
||||
|
||||
#: core/models.py:970
|
||||
#: core/models.py:1000
|
||||
#, fuzzy
|
||||
#| msgid "This user is already in this recording."
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "This file is already hard deleted."
|
||||
|
||||
#: core/models.py:980
|
||||
#: core/models.py:1010
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgstr "To hard delete a file, it must first be soft deleted."
|
||||
|
||||
@@ -581,18 +600,18 @@ msgstr ""
|
||||
" If you have any questions or need assistance, please contact our support "
|
||||
"team at %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:224
|
||||
#: meet/settings.py:223
|
||||
msgid "English"
|
||||
msgstr "English"
|
||||
|
||||
#: meet/settings.py:225
|
||||
#: meet/settings.py:224
|
||||
msgid "French"
|
||||
msgstr "French"
|
||||
|
||||
#: meet/settings.py:226
|
||||
#: meet/settings.py:225
|
||||
msgid "Dutch"
|
||||
msgstr "Dutch"
|
||||
|
||||
#: meet/settings.py:227
|
||||
#: meet/settings.py:226
|
||||
msgid "German"
|
||||
msgstr "German"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-26 17:26+0000\n"
|
||||
"POT-Creation-Date: 2026-03-12 13:46+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -29,11 +29,11 @@ msgstr "Permissions"
|
||||
msgid "Important dates"
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: core/admin.py:132 core/admin.py:243
|
||||
#: core/admin.py:132 core/admin.py:275
|
||||
msgid "No owner"
|
||||
msgstr "Pas de propriétaire"
|
||||
|
||||
#: core/admin.py:135 core/admin.py:246
|
||||
#: core/admin.py:135 core/admin.py:278
|
||||
msgid "Multiple owners"
|
||||
msgstr "Plusieurs propriétaires"
|
||||
|
||||
@@ -61,119 +61,139 @@ msgstr "Notifications envoyées avec succès pour %(count)s enregistrement(s)."
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s enregistrement(s) expiré(s) ignoré(s)."
|
||||
|
||||
#: core/admin.py:309
|
||||
#: core/admin.py:200
|
||||
msgid "Mark selected recordings as 'Failed to Stop'"
|
||||
msgstr "Marquer les enregistrements sélectionnés comme « Échec d’arrêt »"
|
||||
|
||||
#: core/admin.py:218
|
||||
#, python-format
|
||||
msgid "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
||||
msgstr ""
|
||||
"%(count)s enregistrement(s) marqué(s) avec succès comme « Échec d’arrêt »."
|
||||
|
||||
#: core/admin.py:226
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Skipped %(count)s expired recording(s)."
|
||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||
msgstr "%(count)s enregistrement(s) avec un statut inéligible ignoré(s)."
|
||||
|
||||
#: core/admin.py:342
|
||||
msgid "No scopes"
|
||||
msgstr "Aucun scopes"
|
||||
|
||||
#: core/admin.py:311
|
||||
#: core/admin.py:344
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/filters.py:24
|
||||
#: core/api/filters.py:25
|
||||
msgid "Creator is me"
|
||||
msgstr "Je suis le créateur"
|
||||
|
||||
#: core/api/serializers.py:84
|
||||
#: core/api/serializers.py:88
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
|
||||
"des accès."
|
||||
|
||||
#: core/api/serializers.py:443
|
||||
#: core/api/serializers.py:516
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "Cette extension n'est pas autorisée"
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/api/serializers.py:533
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "Vous avez atteint le nombre maximum de fichiers de ce type"
|
||||
|
||||
#: core/models.py:37
|
||||
msgid "Member"
|
||||
msgstr "Membre"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:38
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: core/models.py:37
|
||||
#: core/models.py:39
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:55
|
||||
msgid "Initiated"
|
||||
msgstr "Initié"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:56
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:57
|
||||
msgid "Stopped"
|
||||
msgstr "Arrêté"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:58
|
||||
msgid "Saved"
|
||||
msgstr "Enregistré"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:59
|
||||
msgid "Aborted"
|
||||
msgstr "Abandonné"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:60
|
||||
msgid "Failed to Start"
|
||||
msgstr "Échec au démarrage"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:61
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Échec à l'arrêt"
|
||||
|
||||
#: core/models.py:60
|
||||
#: core/models.py:62
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification réussie"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:89
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "ENREGISTREMENT_ÉCRAN"
|
||||
|
||||
#: core/models.py:88
|
||||
#: core/models.py:90
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPTION"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:96
|
||||
msgid "Public Access"
|
||||
msgstr "Accès public"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:97
|
||||
msgid "Trusted Access"
|
||||
msgstr "Accès de confiance"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:98
|
||||
msgid "Restricted Access"
|
||||
msgstr "Accès restreint"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:110
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:109
|
||||
#: core/models.py:111
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:117
|
||||
msgid "created on"
|
||||
msgstr "créé le"
|
||||
|
||||
#: core/models.py:116
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date et heure auxquelles un enregistrement a été créé"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:123
|
||||
msgid "updated on"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:122
|
||||
#: core/models.py:124
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
|
||||
"fois"
|
||||
|
||||
#: core/models.py:142
|
||||
#: core/models.py:144
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -181,11 +201,11 @@ msgstr ""
|
||||
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des "
|
||||
"chiffres et les caractères @/./+/-/_."
|
||||
|
||||
#: core/models.py:148
|
||||
#: core/models.py:150
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:152
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -193,55 +213,55 @@ msgstr ""
|
||||
"Optionnel pour les utilisateurs en attente ; requis lors de l'activation du "
|
||||
"compte. 255 caractères maximum. Lettres, chiffres et @/./+/-/_ uniquement."
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:161
|
||||
msgid "identity email address"
|
||||
msgstr "adresse e-mail d'identité"
|
||||
|
||||
#: core/models.py:164
|
||||
#: core/models.py:166
|
||||
msgid "admin email address"
|
||||
msgstr "adresse e-mail d'administrateur"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:168
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:170
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:176
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:175
|
||||
#: core/models.py:177
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "statut du personnel"
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: core/models.py:194
|
||||
#: core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: core/models.py:197
|
||||
#: core/models.py:199
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -249,65 +269,65 @@ msgstr ""
|
||||
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
|
||||
"au lieu de supprimer des comptes."
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:212
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:211
|
||||
#: core/models.py:213
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:272
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:271
|
||||
#: core/models.py:273
|
||||
msgid "Resources"
|
||||
msgstr "Ressources"
|
||||
|
||||
#: core/models.py:329
|
||||
#: core/models.py:331
|
||||
msgid "Resource access"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:330
|
||||
#: core/models.py:332
|
||||
msgid "Resource accesses"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:336
|
||||
#: core/models.py:338
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:394
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Configuration de la salle de visioconférence"
|
||||
|
||||
#: core/models.py:393
|
||||
#: core/models.py:395
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
|
||||
|
||||
#: core/models.py:400
|
||||
#: core/models.py:402
|
||||
msgid "Room PIN code"
|
||||
msgstr "Code PIN de la salle"
|
||||
|
||||
#: core/models.py:401
|
||||
#: core/models.py:403
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Code unique à n chiffres qui identifie cette salle en mode téléphonique."
|
||||
|
||||
#: core/models.py:407 core/models.py:561
|
||||
#: core/models.py:409 core/models.py:563
|
||||
msgid "Room"
|
||||
msgstr "Salle"
|
||||
|
||||
#: core/models.py:408
|
||||
#: core/models.py:410
|
||||
msgid "Rooms"
|
||||
msgstr "Salles"
|
||||
|
||||
#: core/models.py:572
|
||||
#: core/models.py:574
|
||||
msgid "Worker ID"
|
||||
msgstr "ID du Worker"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:576
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -315,127 +335,128 @@ msgstr ""
|
||||
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
|
||||
"conservé même lorsque le Worker s'arrête, permettant un suivi facile."
|
||||
|
||||
#: core/models.py:582
|
||||
#: core/models.py:584
|
||||
msgid "Recording mode"
|
||||
msgstr "Mode d'enregistrement"
|
||||
|
||||
#: core/models.py:583
|
||||
#: core/models.py:585
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Définit le mode d'enregistrement appelé."
|
||||
|
||||
#: core/models.py:588 core/models.py:589
|
||||
#: core/models.py:590 core/models.py:591
|
||||
msgid "Recording options"
|
||||
msgstr "Options d'enregistrement"
|
||||
|
||||
#: core/models.py:595
|
||||
#: core/models.py:597
|
||||
msgid "Recording"
|
||||
msgstr "Enregistrement"
|
||||
|
||||
#: core/models.py:596
|
||||
#: core/models.py:598
|
||||
msgid "Recordings"
|
||||
msgstr "Enregistrements"
|
||||
|
||||
#: core/models.py:704
|
||||
#: core/models.py:706
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Relation enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:705
|
||||
#: core/models.py:707
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Relations enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:711
|
||||
#: core/models.py:713
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Cet utilisateur est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:717
|
||||
#: core/models.py:719
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Cette équipe est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:723
|
||||
#: core/models.py:725
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
|
||||
|
||||
#: core/models.py:740
|
||||
#: core/models.py:742
|
||||
msgid "Create rooms"
|
||||
msgstr "Créer des salles"
|
||||
|
||||
#: core/models.py:741
|
||||
#: core/models.py:743
|
||||
msgid "List rooms"
|
||||
msgstr "Lister les salles"
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:744
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Afficher les détails d’une salle"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:745
|
||||
msgid "Update rooms"
|
||||
msgstr "Mettre à jour les salles"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:746
|
||||
msgid "Delete rooms"
|
||||
msgstr "Supprimer les salles"
|
||||
|
||||
#: core/models.py:757
|
||||
#: core/models.py:759
|
||||
msgid "Application name"
|
||||
msgstr "Nom de l’application"
|
||||
|
||||
#: core/models.py:758
|
||||
#: core/models.py:760
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Nom descriptif de cette application."
|
||||
|
||||
#: core/models.py:768
|
||||
#: core/models.py:770
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Haché lors de l’enregistrement. Copiez-le maintenant s’il s’agit d’un "
|
||||
"nouveau secret."
|
||||
|
||||
#: core/models.py:779
|
||||
#: core/models.py:781
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:780
|
||||
#: core/models.py:782
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:803
|
||||
#: core/models.py:805
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Saisissez un domaine valide"
|
||||
|
||||
#: core/models.py:806
|
||||
#: core/models.py:808
|
||||
msgid "Domain"
|
||||
msgstr "Domaine"
|
||||
|
||||
#: core/models.py:807
|
||||
#: core/models.py:809
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Domaine de messagerie au nom duquel cette application peut agir."
|
||||
|
||||
#: core/models.py:819
|
||||
#: core/models.py:821
|
||||
msgid "Application domain"
|
||||
msgstr "Domaine d’application"
|
||||
|
||||
#: core/models.py:820
|
||||
#: core/models.py:822
|
||||
msgid "Application domains"
|
||||
msgstr "Domaines d’application"
|
||||
|
||||
#: core/models.py:838
|
||||
#: core/models.py:840
|
||||
msgid "Pending"
|
||||
msgstr "En attente"
|
||||
|
||||
#: core/models.py:846
|
||||
#: core/models.py:848
|
||||
msgid "Ready"
|
||||
msgstr "Prêt"
|
||||
|
||||
#: core/models.py:852
|
||||
#: core/models.py:854
|
||||
msgid "Background image"
|
||||
msgstr "Image de fond"
|
||||
|
||||
#: core/models.py:864
|
||||
#: core/models.py:866
|
||||
msgid "title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: core/models.py:890
|
||||
msgid "Malware detection info when the analysis status is unsafe."
|
||||
msgstr "Information concernant la détection de Malware cand le statut n'est pas sain"
|
||||
msgstr ""
|
||||
"Information concernant la détection de Malware cand le statut n'est pas sain"
|
||||
|
||||
#: core/models.py:895
|
||||
msgid "File"
|
||||
@@ -445,15 +466,17 @@ msgstr "Fichier"
|
||||
msgid "Files"
|
||||
msgstr "Fichiers"
|
||||
|
||||
#: core/models.py:970
|
||||
#: core/models.py:1000
|
||||
#, fuzzy
|
||||
#| msgid "This user is already in this recording."
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "Ce fichier a été supprimé."
|
||||
|
||||
#: core/models.py:980
|
||||
#: core/models.py:1010
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgstr "Pour supprimer définitivement un fichier il doit d'abord avoir été marqué comme supprimé (soft delete)"
|
||||
msgstr ""
|
||||
"Pour supprimer définitivement un fichier il doit d'abord avoir été marqué "
|
||||
"comme supprimé (soft delete)"
|
||||
|
||||
#: core/recording/event/notification.py:116
|
||||
msgid "Your recording is ready"
|
||||
@@ -583,18 +606,18 @@ msgstr ""
|
||||
" Si vous avez des questions ou besoin d'assistance, veuillez contacter notre "
|
||||
"équipe d'assistance à %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:224
|
||||
#: meet/settings.py:223
|
||||
msgid "English"
|
||||
msgstr "Anglais"
|
||||
|
||||
#: meet/settings.py:225
|
||||
#: meet/settings.py:224
|
||||
msgid "French"
|
||||
msgstr "Français"
|
||||
|
||||
#: meet/settings.py:226
|
||||
#: meet/settings.py:225
|
||||
msgid "Dutch"
|
||||
msgstr "Néerlandais"
|
||||
|
||||
#: meet/settings.py:227
|
||||
#: meet/settings.py:226
|
||||
msgid "German"
|
||||
msgstr "Allemand"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-26 17:34+0000\n"
|
||||
"POT-Creation-Date: 2026-03-12 13:46+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -29,11 +29,11 @@ msgstr "Rechten"
|
||||
msgid "Important dates"
|
||||
msgstr "Belangrijke datums"
|
||||
|
||||
#: core/admin.py:132 core/admin.py:243
|
||||
#: core/admin.py:132 core/admin.py:275
|
||||
msgid "No owner"
|
||||
msgstr "Geen eigenaar"
|
||||
|
||||
#: core/admin.py:135 core/admin.py:246
|
||||
#: core/admin.py:135 core/admin.py:278
|
||||
msgid "Multiple owners"
|
||||
msgstr "Meerdere eigenaren"
|
||||
|
||||
@@ -61,116 +61,135 @@ msgstr "Meldingen succesvol verzonden voor %(count)s opname(n)."
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s verlopen opname(n) overgeslagen."
|
||||
|
||||
#: core/admin.py:309
|
||||
#: core/admin.py:200
|
||||
msgid "Mark selected recordings as 'Failed to Stop'"
|
||||
msgstr "Geselecteerde opnames markeren als 'Mislukt bij stoppen'"
|
||||
|
||||
#: core/admin.py:218
|
||||
#, python-format
|
||||
msgid "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
||||
msgstr "%(count)s opname(s) succesvol gemarkeerd als 'Mislukt bij stoppen'."
|
||||
|
||||
#: core/admin.py:226
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Skipped %(count)s expired recording(s)."
|
||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||
msgstr "%(count)s opname(s) met een niet-toegestane status overgeslagen."
|
||||
|
||||
#: core/admin.py:342
|
||||
msgid "No scopes"
|
||||
msgstr "Geen scopes"
|
||||
|
||||
#: core/admin.py:311
|
||||
#: core/admin.py:344
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/filters.py:24
|
||||
#: core/api/filters.py:25
|
||||
msgid "Creator is me"
|
||||
msgstr "Maker ben ik"
|
||||
|
||||
#: core/api/serializers.py:84
|
||||
#: core/api/serializers.py:88
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
|
||||
|
||||
#: core/api/serializers.py:443
|
||||
#: core/api/serializers.py:516
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "Deze bestandsextensie is niet toegestaan."
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/api/serializers.py:533
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "Het maximale aantal bestanden voor dit type is bereikt."
|
||||
|
||||
#: core/models.py:37
|
||||
msgid "Member"
|
||||
msgstr "Lid"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:38
|
||||
msgid "Administrator"
|
||||
msgstr "Beheerder"
|
||||
|
||||
#: core/models.py:37
|
||||
#: core/models.py:39
|
||||
msgid "Owner"
|
||||
msgstr "Eigenaar"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:55
|
||||
msgid "Initiated"
|
||||
msgstr "Gestart"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:56
|
||||
msgid "Active"
|
||||
msgstr "Actief"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:57
|
||||
msgid "Stopped"
|
||||
msgstr "Gestopt"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:58
|
||||
msgid "Saved"
|
||||
msgstr "Opgeslagen"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:59
|
||||
msgid "Aborted"
|
||||
msgstr "Afgebroken"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:60
|
||||
msgid "Failed to Start"
|
||||
msgstr "Starten mislukt"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:61
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stoppen mislukt"
|
||||
|
||||
#: core/models.py:60
|
||||
#: core/models.py:62
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notificatie geslaagd"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:89
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCHERM_OPNAME"
|
||||
|
||||
#: core/models.py:88
|
||||
#: core/models.py:90
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:96
|
||||
msgid "Public Access"
|
||||
msgstr "Openbare toegang"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:97
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrouwde toegang"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:98
|
||||
msgid "Restricted Access"
|
||||
msgstr "Beperkte toegang"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:110
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:109
|
||||
#: core/models.py:111
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primaire sleutel voor het record als UUID"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:117
|
||||
msgid "created on"
|
||||
msgstr "aangemaakt op"
|
||||
|
||||
#: core/models.py:116
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum en tijd waarop een record werd aangemaakt"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:123
|
||||
msgid "updated on"
|
||||
msgstr "bijgewerkt op"
|
||||
|
||||
#: core/models.py:122
|
||||
#: core/models.py:124
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
|
||||
|
||||
#: core/models.py:142
|
||||
#: core/models.py:144
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -178,11 +197,11 @@ msgstr ""
|
||||
"Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/"
|
||||
"_ tekens bevatten."
|
||||
|
||||
#: core/models.py:148
|
||||
#: core/models.py:150
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:152
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -190,55 +209,55 @@ msgstr ""
|
||||
"Optioneel voor gebruikers in afwachting; vereist bij accountactivering. "
|
||||
"Maximum 255 tekens. Alleen letters, cijfers en @/./+/-/_ toegestaan."
|
||||
|
||||
#: core/models.py:159
|
||||
#: core/models.py:161
|
||||
msgid "identity email address"
|
||||
msgstr "identiteit e-mailadres"
|
||||
|
||||
#: core/models.py:164
|
||||
#: core/models.py:166
|
||||
msgid "admin email address"
|
||||
msgstr "beheerder e-mailadres"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:168
|
||||
msgid "full name"
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:170
|
||||
msgid "short name"
|
||||
msgstr "korte naam"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:176
|
||||
msgid "language"
|
||||
msgstr "taal"
|
||||
|
||||
#: core/models.py:175
|
||||
#: core/models.py:177
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||
|
||||
#: core/models.py:181
|
||||
#: core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "De tijdzone waarin de gebruiker tijden wil zien."
|
||||
|
||||
#: core/models.py:184
|
||||
#: core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "apparaat"
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||
|
||||
#: core/models.py:189
|
||||
#: core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "personeelsstatus"
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
||||
|
||||
#: core/models.py:194
|
||||
#: core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "actief"
|
||||
|
||||
#: core/models.py:197
|
||||
#: core/models.py:199
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -246,64 +265,64 @@ msgstr ""
|
||||
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
|
||||
"plaats van accounts te verwijderen."
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:212
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: core/models.py:211
|
||||
#: core/models.py:213
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:272
|
||||
msgid "Resource"
|
||||
msgstr "Bron"
|
||||
|
||||
#: core/models.py:271
|
||||
#: core/models.py:273
|
||||
msgid "Resources"
|
||||
msgstr "Bronnen"
|
||||
|
||||
#: core/models.py:329
|
||||
#: core/models.py:331
|
||||
msgid "Resource access"
|
||||
msgstr "Brontoegang"
|
||||
|
||||
#: core/models.py:330
|
||||
#: core/models.py:332
|
||||
msgid "Resource accesses"
|
||||
msgstr "Brontoegangsrechten"
|
||||
|
||||
#: core/models.py:336
|
||||
#: core/models.py:338
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Brontoegang met deze gebruiker en bron bestaat al."
|
||||
|
||||
#: core/models.py:392
|
||||
#: core/models.py:394
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-ruimteconfiguratie"
|
||||
|
||||
#: core/models.py:393
|
||||
#: core/models.py:395
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
|
||||
|
||||
#: core/models.py:400
|
||||
#: core/models.py:402
|
||||
msgid "Room PIN code"
|
||||
msgstr "Pincode van de kamer"
|
||||
|
||||
#: core/models.py:401
|
||||
#: core/models.py:403
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
|
||||
|
||||
#: core/models.py:407 core/models.py:561
|
||||
#: core/models.py:409 core/models.py:563
|
||||
msgid "Room"
|
||||
msgstr "Ruimte"
|
||||
|
||||
#: core/models.py:408
|
||||
#: core/models.py:410
|
||||
msgid "Rooms"
|
||||
msgstr "Ruimtes"
|
||||
|
||||
#: core/models.py:572
|
||||
#: core/models.py:574
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:576
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -311,120 +330,120 @@ msgstr ""
|
||||
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
|
||||
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
|
||||
|
||||
#: core/models.py:582
|
||||
#: core/models.py:584
|
||||
msgid "Recording mode"
|
||||
msgstr "Opnamemodus"
|
||||
|
||||
#: core/models.py:583
|
||||
#: core/models.py:585
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definieert de modus van opname die wordt aangeroepen."
|
||||
|
||||
#: core/models.py:588 core/models.py:589
|
||||
#: core/models.py:590 core/models.py:591
|
||||
msgid "Recording options"
|
||||
msgstr "Opnameopties"
|
||||
|
||||
#: core/models.py:595
|
||||
#: core/models.py:597
|
||||
msgid "Recording"
|
||||
msgstr "Opname"
|
||||
|
||||
#: core/models.py:596
|
||||
#: core/models.py:598
|
||||
msgid "Recordings"
|
||||
msgstr "Opnames"
|
||||
|
||||
#: core/models.py:704
|
||||
#: core/models.py:706
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Opname/gebruiker-relatie"
|
||||
|
||||
#: core/models.py:705
|
||||
#: core/models.py:707
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Opname/gebruiker-relaties"
|
||||
|
||||
#: core/models.py:711
|
||||
#: core/models.py:713
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Deze gebruiker is al in deze opname."
|
||||
|
||||
#: core/models.py:717
|
||||
#: core/models.py:719
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dit team is al in deze opname."
|
||||
|
||||
#: core/models.py:723
|
||||
#: core/models.py:725
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
|
||||
|
||||
#: core/models.py:740
|
||||
#: core/models.py:742
|
||||
msgid "Create rooms"
|
||||
msgstr "Ruimtes aanmaken"
|
||||
|
||||
#: core/models.py:741
|
||||
#: core/models.py:743
|
||||
msgid "List rooms"
|
||||
msgstr "Ruimtes weergeven"
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:744
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Details van een ruimte ophalen"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:745
|
||||
msgid "Update rooms"
|
||||
msgstr "Ruimtes bijwerken"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:746
|
||||
msgid "Delete rooms"
|
||||
msgstr "Ruimtes verwijderen"
|
||||
|
||||
#: core/models.py:757
|
||||
#: core/models.py:759
|
||||
msgid "Application name"
|
||||
msgstr "Naam van de applicatie"
|
||||
|
||||
#: core/models.py:758
|
||||
#: core/models.py:760
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschrijvende naam voor deze applicatie."
|
||||
|
||||
#: core/models.py:768
|
||||
#: core/models.py:770
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is."
|
||||
|
||||
#: core/models.py:779
|
||||
#: core/models.py:781
|
||||
msgid "Application"
|
||||
msgstr "Applicatie"
|
||||
|
||||
#: core/models.py:780
|
||||
#: core/models.py:782
|
||||
msgid "Applications"
|
||||
msgstr "Applicaties"
|
||||
|
||||
#: core/models.py:803
|
||||
#: core/models.py:805
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Voer een geldig domein in"
|
||||
|
||||
#: core/models.py:806
|
||||
#: core/models.py:808
|
||||
msgid "Domain"
|
||||
msgstr "Domein"
|
||||
|
||||
#: core/models.py:807
|
||||
#: core/models.py:809
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-maildomein namens welke deze applicatie kan handelen."
|
||||
|
||||
#: core/models.py:819
|
||||
#: core/models.py:821
|
||||
msgid "Application domain"
|
||||
msgstr "Applicatiedomein"
|
||||
|
||||
#: core/models.py:820
|
||||
#: core/models.py:822
|
||||
msgid "Application domains"
|
||||
msgstr "Applicatiedomeinen"
|
||||
|
||||
#: core/models.py:838
|
||||
#: core/models.py:840
|
||||
msgid "Pending"
|
||||
msgstr "In afwachting"
|
||||
|
||||
#: core/models.py:846
|
||||
#: core/models.py:848
|
||||
msgid "Ready"
|
||||
msgstr "Klaar"
|
||||
|
||||
#: core/models.py:852
|
||||
#: core/models.py:854
|
||||
msgid "Background image"
|
||||
msgstr "Achtergrondafbeelding"
|
||||
|
||||
#: core/models.py:864
|
||||
#: core/models.py:866
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -440,11 +459,11 @@ msgstr "Bestand"
|
||||
msgid "Files"
|
||||
msgstr "Bestanden"
|
||||
|
||||
#: core/models.py:970
|
||||
#: core/models.py:1000
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "Dit bestand is al definitief verwijderd."
|
||||
|
||||
#: core/models.py:980
|
||||
#: core/models.py:1010
|
||||
#, fuzzy
|
||||
#| msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
@@ -580,18 +599,18 @@ msgstr ""
|
||||
" Als je vragen hebt of hulp nodig hebt, neem dan contact op met ons support "
|
||||
"team via %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:224
|
||||
#: meet/settings.py:223
|
||||
msgid "English"
|
||||
msgstr "Engels"
|
||||
|
||||
#: meet/settings.py:225
|
||||
#: meet/settings.py:224
|
||||
msgid "French"
|
||||
msgstr "Frans"
|
||||
|
||||
#: meet/settings.py:226
|
||||
#: meet/settings.py:225
|
||||
msgid "Dutch"
|
||||
msgstr "Nederlands"
|
||||
|
||||
#: meet/settings.py:227
|
||||
#: meet/settings.py:226
|
||||
msgid "German"
|
||||
msgstr "Duits"
|
||||
|
||||
@@ -176,6 +176,13 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
FILE_UPLOAD_ENABLED = values.BooleanValue(
|
||||
# False to avoid a breaking change for now
|
||||
default=False,
|
||||
environ_name="FILE_UPLOAD_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
FILE_UPLOAD_PATH = values.Value(
|
||||
"files", environ_name="FILE_UPLOAD_PATH", environ_prefix=None
|
||||
)
|
||||
@@ -188,6 +195,7 @@ class Base(Configuration):
|
||||
{
|
||||
"background_image": {
|
||||
"max_size": 2 * MB,
|
||||
"max_count_by_user": 10,
|
||||
"allowed_extensions": [".jpeg", ".jpg", ".png"],
|
||||
"allowed_mimetypes": ["image/jpeg", "image/png"],
|
||||
},
|
||||
@@ -437,9 +445,11 @@ class Base(Configuration):
|
||||
# Celery
|
||||
# Defaults to False to avoid breaking change, async task will be run
|
||||
# synchronously
|
||||
CELERY_ENABLED = values.BooleanValue(False)
|
||||
CELERY_BROKER_URL = values.Value("redis://redis:6379/0")
|
||||
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({})
|
||||
CELERY_ENABLED = values.BooleanValue(False, environ_prefix=None)
|
||||
CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(False, environ_prefix=None)
|
||||
CELERY_TASK_DEFAULT_QUEUE = values.Value("meet-backend", environ_prefix=None)
|
||||
CELERY_BROKER_URL = values.Value("redis://redis:6379/0", environ_prefix=None)
|
||||
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({}, environ_prefix=None)
|
||||
|
||||
# Session
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
@@ -970,7 +980,8 @@ class Test(Base):
|
||||
APPLICATION_JWT_SECRET_KEY = "devKey" # noqa:S105
|
||||
APPLICATION_JWT_AUDIENCE = "Test inc."
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True)
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
FILE_UPLOAD_ENABLED = True
|
||||
|
||||
def __init__(self):
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
+13
-13
@@ -2,12 +2,12 @@
|
||||
# Meet package
|
||||
#
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["uv_build>=0.10.9,<0.11.0"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -21,8 +21,7 @@ classifiers = [
|
||||
]
|
||||
description = "A simple video and phone conferencing tool, powered by LiveKit"
|
||||
keywords = ["Django", "Contacts", "Templates", "RBAC"]
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"boto3==1.42.49",
|
||||
@@ -53,7 +52,7 @@ dependencies = [
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"psycopg[binary]==3.3.2",
|
||||
"pydantic==2.12.4",
|
||||
"PyJWT==2.11.0",
|
||||
"PyJWT==2.12.0",
|
||||
"python-frontmatter==1.1.0",
|
||||
"python-magic==0.4.27",
|
||||
"requests==2.32.5",
|
||||
@@ -70,7 +69,7 @@ dependencies = [
|
||||
"Homepage" = "https://github.com/suitenumerique/meet"
|
||||
"Repository" = "https://github.com/suitenumerique/meet"
|
||||
|
||||
[project.optional-dependencies]
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"drf-spectacular-sidecar==2026.1.1",
|
||||
@@ -90,12 +89,13 @@ dev = [
|
||||
"types-requests==2.32.4.20260107",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = { find = { where = ["."], exclude = ["tests"] } }
|
||||
zip-safe = true
|
||||
|
||||
[tool.distutils.bdist_wheel]
|
||||
universal = true
|
||||
[tool.uv.build-backend]
|
||||
module-root = ""
|
||||
source-exclude = [
|
||||
"**/tests/**",
|
||||
"**/test_*.py",
|
||||
"**/tests.py",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
|
||||
Generated
+2365
File diff suppressed because it is too large
Load Diff
Generated
+162
-87
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.34",
|
||||
"@fontsource/material-icons-outlined": "5.2.6",
|
||||
@@ -3959,9 +3959,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz",
|
||||
"integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3973,9 +3973,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz",
|
||||
"integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3987,9 +3987,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz",
|
||||
"integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4001,9 +4001,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz",
|
||||
"integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4015,9 +4015,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz",
|
||||
"integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4029,9 +4029,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz",
|
||||
"integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4043,9 +4043,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz",
|
||||
"integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4057,9 +4057,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz",
|
||||
"integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4071,9 +4071,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz",
|
||||
"integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4085,9 +4085,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz",
|
||||
"integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4098,10 +4098,10 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz",
|
||||
"integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==",
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -4112,10 +4112,38 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz",
|
||||
"integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==",
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -4127,9 +4155,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz",
|
||||
"integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4141,9 +4169,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz",
|
||||
"integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4155,9 +4183,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz",
|
||||
"integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -4169,9 +4197,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz",
|
||||
"integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4183,9 +4211,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz",
|
||||
"integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4196,10 +4224,38 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz",
|
||||
"integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4211,9 +4267,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz",
|
||||
"integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -4224,10 +4280,24 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz",
|
||||
"integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -10061,9 +10131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.44.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz",
|
||||
"integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10077,26 +10147,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.44.2",
|
||||
"@rollup/rollup-android-arm64": "4.44.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.44.2",
|
||||
"@rollup/rollup-darwin-x64": "4.44.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.44.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.44.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.44.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.44.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.44.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.44.2",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.44.2",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.44.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.44.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.44.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.44.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.44.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.44.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.44.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.44.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.44.2",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
|
||||
@@ -15,7 +15,19 @@ export const fetchApi = async <T = Record<string, unknown>>(
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
const result = await response.json()
|
||||
|
||||
let result: T
|
||||
if (response.status === 204) {
|
||||
result = undefined as T
|
||||
} else {
|
||||
const contentType = response.headers.get('content-type') ?? ''
|
||||
if (!contentType.includes('application/json')) {
|
||||
result = undefined as T
|
||||
} else {
|
||||
result = (await response.json()) as T
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, result)
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export const keys = {
|
||||
requestEntry: 'requestEntry',
|
||||
waitingParticipants: 'waitingParticipants',
|
||||
roomCreationCallback: 'roomCreationCallback',
|
||||
files: 'files',
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ export interface ApiConfig {
|
||||
expiration_days?: number
|
||||
max_duration?: number
|
||||
}
|
||||
background_image: {
|
||||
upload_is_enabled: boolean
|
||||
max_size: number
|
||||
max_count_by_user: number
|
||||
allowed_extensions: string[]
|
||||
allowed_mimetypes: string[]
|
||||
}
|
||||
subtitle: {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { ApiFileItem } from '@/features/files/api/types.ts'
|
||||
import { keys } from '@/api/queryKeys.ts'
|
||||
|
||||
/**
|
||||
* Upload a file, using XHR so we can report on progress through a handler.
|
||||
*
|
||||
* @param url The URL to PUT the file to.
|
||||
* @param file The file to upload.
|
||||
* @param progressHandler A handler that receives progress updates as a single integer `0 <= x <= 100`.
|
||||
*/
|
||||
export const uploadFile = (
|
||||
url: string,
|
||||
file: File,
|
||||
progressHandler: (progress: number) => void
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('PUT', url)
|
||||
xhr.setRequestHeader('X-amz-acl', 'private')
|
||||
xhr.setRequestHeader('Content-Type', file.type)
|
||||
|
||||
xhr.addEventListener('error', reject)
|
||||
xhr.addEventListener('abort', reject)
|
||||
|
||||
xhr.addEventListener('readystatechange', () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
// Make sure to always set the progress to 100% when the upload is done.
|
||||
// Because 'progress' event listener is not called when the file size is 0.
|
||||
progressHandler(100)
|
||||
return resolve(true)
|
||||
}
|
||||
reject(new Error(`Failed to perform the upload on ${url}.`))
|
||||
}
|
||||
})
|
||||
|
||||
xhr.upload.addEventListener('progress', (progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
progressHandler(
|
||||
Math.floor((progressEvent.loaded / progressEvent.total) * 100)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
xhr.send(file)
|
||||
})
|
||||
|
||||
/**
|
||||
* Asynchronously creates a new file and uploads it to the server.
|
||||
*
|
||||
* @param {object} params - The parameters for the file creation and upload process.
|
||||
* @param {File} params.file - The file object to be uploaded.
|
||||
* @param {function} params.onProgress - A callback function that receives the upload progress as a number (0 to 100).
|
||||
* @returns {Promise<ApiFileItem>} A promise that resolves when the file has been successfully uploaded and the server process is completed.
|
||||
*/
|
||||
export const createFile = async ({
|
||||
file,
|
||||
onProgress,
|
||||
}: {
|
||||
file: File
|
||||
onProgress: (progress: number) => void
|
||||
}): Promise<ApiFileItem> => {
|
||||
const res = await fetchApi<ApiFileItem>(`/files/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ filename: file.name, type: 'background_image' }),
|
||||
})
|
||||
if (res.upload_state !== 'pending') {
|
||||
throw new Error('State should be pending right after creation')
|
||||
}
|
||||
const policy = res.policy
|
||||
await uploadFile(policy, file, onProgress)
|
||||
return await fetchApi<ApiFileItem>(`/files/${res.id}/upload-ended/`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateFile = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createFile,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [keys.files],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { keys } from '@/api/queryKeys.ts'
|
||||
|
||||
/**
|
||||
* Deletes a file specified by its unique identifier.
|
||||
*
|
||||
* @param {Object} params - The parameters required for deleting the file.
|
||||
* @param {string} params.fileId - The unique identifier of the file to be deleted.
|
||||
* @returns {Promise<void>} A promise that resolves when the file is successfully deleted.
|
||||
*/
|
||||
export const deleteFile = async ({
|
||||
fileId,
|
||||
}: {
|
||||
fileId: string
|
||||
}): Promise<void> => {
|
||||
await fetchApi<void>(`/files/${fileId}/`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteFile = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: deleteFile,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [keys.files],
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { keys } from '@/api/queryKeys'
|
||||
import {
|
||||
ApiFileItem,
|
||||
ApiFileType,
|
||||
ApiFileUploadState,
|
||||
} from '@/features/files/api/types.ts'
|
||||
import { useUser } from '@/features/auth'
|
||||
import { useConfig } from '@/api/useConfig.ts'
|
||||
|
||||
type ListFilesResponse = {
|
||||
count: number
|
||||
next: string | null
|
||||
previous: string | null
|
||||
results: ApiFileItem[]
|
||||
}
|
||||
|
||||
type ListFilesFilters = {
|
||||
is_creator_me?: boolean
|
||||
type?: ApiFileType
|
||||
upload_state?: ApiFileUploadState
|
||||
is_deleted?: boolean
|
||||
}
|
||||
|
||||
export type ListFilesParams = {
|
||||
filters?: ListFilesFilters
|
||||
pagination: {
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
}
|
||||
|
||||
export const listMyFiles = async ({
|
||||
filters = {},
|
||||
pagination: { page, pageSize },
|
||||
}: ListFilesParams): Promise<ListFilesResponse> => {
|
||||
const query = new URLSearchParams()
|
||||
query.append('page', page.toString())
|
||||
query.append('page_size', pageSize.toString())
|
||||
if (filters?.is_creator_me ?? true) {
|
||||
query.append('is_creator_me', 'true')
|
||||
}
|
||||
if (filters?.type) {
|
||||
query.append('type', filters.type)
|
||||
}
|
||||
if (filters?.upload_state) {
|
||||
query.append('upload_state', filters.upload_state)
|
||||
}
|
||||
if (typeof filters?.is_deleted === 'boolean') {
|
||||
query.append('is_deleted', filters.is_deleted ? 'true' : 'false')
|
||||
}
|
||||
|
||||
return fetchApi<ListFilesResponse>(`/files?${query.toString()}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export const useListMyFiles = (params: Parameters<typeof listMyFiles>[0]) => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const { data: appConfig } = useConfig()
|
||||
return useQuery({
|
||||
queryKey: [keys.files, params],
|
||||
queryFn: () => listMyFiles(params),
|
||||
refetchOnMount: 'always',
|
||||
placeholderData: keepPreviousData,
|
||||
enabled:
|
||||
isLoggedIn && appConfig?.background_image?.upload_is_enabled === true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export type ApiFileCreator = {
|
||||
id: string // UUID
|
||||
full_name: string | null
|
||||
short_name: string | null
|
||||
}
|
||||
|
||||
export type ApiFileType = 'background_image'
|
||||
export type ApiFileUploadState = 'pending' | 'ready'
|
||||
|
||||
export type ApiFileItem = {
|
||||
id: string // UUID
|
||||
created_at: string // ISO datetime string
|
||||
updated_at: string // ISO datetime string
|
||||
title: string
|
||||
type: ApiFileType
|
||||
creator: ApiFileCreator
|
||||
deleted_at: string | null
|
||||
hard_deleted_at: string | null
|
||||
filename: string
|
||||
upload_state: ApiFileUploadState
|
||||
mimetype: string // e.g. "image/png"
|
||||
size: number // file size in bytes
|
||||
description: string | null
|
||||
} & (
|
||||
| {
|
||||
upload_state: 'ready'
|
||||
url: string
|
||||
}
|
||||
| {
|
||||
upload_state: 'pending'
|
||||
policy: string
|
||||
url: null
|
||||
}
|
||||
)
|
||||
@@ -72,7 +72,8 @@ export const RecordingDownload = () => {
|
||||
|
||||
if (
|
||||
data.status !== RecordingStatus.Saved &&
|
||||
data.status !== RecordingStatus.NotificationSucceed
|
||||
data.status !== RecordingStatus.NotificationSucceed &&
|
||||
data.status !== RecordingStatus.FailedToStop
|
||||
) {
|
||||
return <ErrorScreen title={t('unsaved.title')} body={t('unsaved.body')} />
|
||||
}
|
||||
|
||||
@@ -215,8 +215,8 @@ export const Conference = ({
|
||||
audio={userConfig.audioEnabled}
|
||||
video={
|
||||
userConfig.videoEnabled && {
|
||||
processor: BackgroundProcessorFactory.deserializeProcessor(
|
||||
userConfig.processorSerialized
|
||||
processor: BackgroundProcessorFactory.fromProcessorConfig(
|
||||
userConfig.processorConfig
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ import { css } from '@/styled-system/css'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
createLocalVideoTrack,
|
||||
createLocalAudioTrack,
|
||||
createLocalVideoTrack,
|
||||
LocalAudioTrack,
|
||||
LocalVideoTrack,
|
||||
Track,
|
||||
} from 'livekit-client'
|
||||
import { H } from '@/primitives/H'
|
||||
import { Field } from '@/primitives/Field'
|
||||
import { Button, Dialog, Text, Form } from '@/primitives'
|
||||
import { Button, Dialog, Form, Text } from '@/primitives'
|
||||
import { VStack } from '@/styled-system/jsx'
|
||||
import { Heading } from 'react-aria-components'
|
||||
import { RiImageCircleAiFill } from '@remixicon/react'
|
||||
@@ -44,8 +44,7 @@ const onError = (e: Error) => console.error('ERROR', e)
|
||||
|
||||
const Effects = ({
|
||||
videoTrack,
|
||||
onSubmit,
|
||||
}: Pick<EffectsConfigurationProps, 'videoTrack' | 'onSubmit'>) => {
|
||||
}: Pick<EffectsConfigurationProps, 'videoTrack'>) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join.effects' })
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const openDialog = () => setIsDialogOpen(true)
|
||||
@@ -81,7 +80,7 @@ const Effects = ({
|
||||
>
|
||||
{t('subTitle')}
|
||||
</Text>
|
||||
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
|
||||
<EffectsConfiguration videoTrack={videoTrack} />
|
||||
</Dialog>
|
||||
<Button
|
||||
variant="whiteCircle"
|
||||
@@ -111,7 +110,7 @@ export const Join = ({
|
||||
audioDeviceId,
|
||||
audioOutputDeviceId,
|
||||
videoDeviceId,
|
||||
processorSerialized,
|
||||
processorConfig,
|
||||
username,
|
||||
},
|
||||
saveAudioInputEnabled,
|
||||
@@ -120,7 +119,6 @@ export const Join = ({
|
||||
saveAudioInputDeviceId,
|
||||
saveVideoInputDeviceId,
|
||||
saveUsername,
|
||||
saveProcessorSerialized,
|
||||
} = usePersistentUserChoices()
|
||||
|
||||
const initialUserChoices = useRef<LocalUserChoices | null>(null)
|
||||
@@ -132,7 +130,7 @@ export const Join = ({
|
||||
audioDeviceId,
|
||||
audioOutputDeviceId,
|
||||
videoDeviceId,
|
||||
processorSerialized,
|
||||
processorConfig,
|
||||
username,
|
||||
}
|
||||
}
|
||||
@@ -146,8 +144,8 @@ export const Join = ({
|
||||
video: !!initialUserChoices.current &&
|
||||
initialUserChoices.current?.videoEnabled && {
|
||||
deviceId: initialUserChoices.current.videoDeviceId,
|
||||
processor: BackgroundProcessorFactory.deserializeProcessor(
|
||||
initialUserChoices.current.processorSerialized
|
||||
processor: BackgroundProcessorFactory.fromProcessorConfig(
|
||||
initialUserChoices.current.processorConfig
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -186,9 +184,7 @@ export const Join = ({
|
||||
const track = await createLocalVideoTrack({
|
||||
deviceId: { exact: videoDeviceId },
|
||||
processor:
|
||||
BackgroundProcessorFactory.deserializeProcessor(
|
||||
processorSerialized
|
||||
),
|
||||
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
|
||||
})
|
||||
setDynamicVideoTrack(track)
|
||||
} catch (error) {
|
||||
@@ -207,7 +203,7 @@ export const Join = ({
|
||||
}, [
|
||||
videoEnabled,
|
||||
videoDeviceId,
|
||||
processorSerialized,
|
||||
processorConfig,
|
||||
previewVideoTrack,
|
||||
dynamicVideoTrack,
|
||||
])
|
||||
@@ -690,12 +686,7 @@ export const Join = ({
|
||||
zIndex: '1',
|
||||
})}
|
||||
>
|
||||
<Effects
|
||||
videoTrack={videoTrack}
|
||||
onSubmit={(processor) =>
|
||||
saveProcessorSerialized(processor?.serialize())
|
||||
}
|
||||
/>
|
||||
<Effects videoTrack={videoTrack} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -183,7 +183,13 @@ export const ParticipantTile: (
|
||||
}}
|
||||
>
|
||||
{isHandRaised && !isScreenShare && (
|
||||
<span>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.1rem',
|
||||
}}
|
||||
>
|
||||
<span>{positionInQueue}</span>
|
||||
<RiHand
|
||||
color="black"
|
||||
|
||||
@@ -149,11 +149,12 @@ export const SidePanel = () => {
|
||||
activeSubPanelId,
|
||||
} = useSidePanel()
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
|
||||
const title = t(`heading.${activeSubPanelId || activePanelId}`)
|
||||
|
||||
return (
|
||||
<StyledSidePanel
|
||||
title={t(`heading.${activeSubPanelId || activePanelId}`)}
|
||||
ariaLabel={t('ariaLabel')}
|
||||
title={title}
|
||||
ariaLabel={t('ariaLabel', { title })}
|
||||
onClose={() => {
|
||||
layoutStore.activePanelId = null
|
||||
layoutStore.activeSubPanelId = null
|
||||
|
||||
@@ -65,7 +65,7 @@ const ToolButton = ({
|
||||
<div>
|
||||
<Text
|
||||
margin={false}
|
||||
as="h3"
|
||||
as="h2"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: 0.25,
|
||||
|
||||
+16
-27
@@ -11,11 +11,7 @@ import {
|
||||
TIMEOUT_TICK,
|
||||
timerWorkerScript,
|
||||
} from './TimerWorker'
|
||||
import {
|
||||
BackgroundProcessorInterface,
|
||||
BackgroundOptions,
|
||||
ProcessorType,
|
||||
} from '.'
|
||||
import { BackgroundProcessorInterface, ProcessorConfig, ProcessorType } from '.'
|
||||
|
||||
const PROCESSING_WIDTH = 256
|
||||
const PROCESSING_HEIGHT = 144
|
||||
@@ -33,7 +29,7 @@ const DEFAULT_BLUR = '10'
|
||||
* MediaStreamTrackProcessor.
|
||||
*/
|
||||
export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
options: BackgroundOptions
|
||||
options: ProcessorConfig
|
||||
name: string
|
||||
processedTrack?: MediaStreamTrack | undefined
|
||||
|
||||
@@ -64,15 +60,10 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
type: ProcessorType
|
||||
virtualBackgroundImage?: HTMLImageElement
|
||||
|
||||
constructor(opts: BackgroundOptions) {
|
||||
constructor(opts: ProcessorConfig) {
|
||||
this.name = 'blur'
|
||||
this.options = opts
|
||||
|
||||
if (this.options.blurRadius) {
|
||||
this.type = ProcessorType.BLUR
|
||||
} else {
|
||||
this.type = ProcessorType.VIRTUAL
|
||||
}
|
||||
this.type = opts.type
|
||||
}
|
||||
|
||||
static get isSupported() {
|
||||
@@ -107,6 +98,12 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
}
|
||||
|
||||
_initVirtualBackgroundImage() {
|
||||
if (this.options.type !== 'virtual') {
|
||||
throw new Error(
|
||||
'Virtual background is only supported for virtual background'
|
||||
)
|
||||
}
|
||||
|
||||
const needsUpdate =
|
||||
this.options.imagePath &&
|
||||
this.virtualBackgroundImage &&
|
||||
@@ -118,7 +115,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async update(opts: BackgroundOptions): Promise<void> {
|
||||
async update(opts: ProcessorConfig): Promise<void> {
|
||||
this.options = opts
|
||||
this._initVirtualBackgroundImage()
|
||||
}
|
||||
@@ -187,7 +184,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
0,
|
||||
0,
|
||||
PROCESSING_WIDTH,
|
||||
PROCESSING_WIDTH
|
||||
PROCESSING_HEIGHT
|
||||
)
|
||||
}
|
||||
|
||||
@@ -212,6 +209,9 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
* TODO: future improvement with WebGL.
|
||||
*/
|
||||
async blur() {
|
||||
if (this.options.type !== 'blur') {
|
||||
throw new Error('Blurring is only supported for blur background')
|
||||
}
|
||||
const mask = this.imageSegmenterResult!.categoryMask!.getAsUint8Array()
|
||||
for (let i = 0; i < mask.length; ++i) {
|
||||
this.segmentationMask!.data[i * 4 + 3] = 255 - mask[i]
|
||||
@@ -293,7 +293,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
await this.sizeSource()
|
||||
await this.segment()
|
||||
|
||||
if (this.options.blurRadius) {
|
||||
if (this.options.type === 'blur') {
|
||||
await this.blur()
|
||||
} else {
|
||||
await this.drawVirtualBackground()
|
||||
@@ -354,15 +354,4 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
this.timerWorker?.terminate()
|
||||
this.imageSegmenter?.close()
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new BackgroundCustomProcessor(this.options)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
type: this.type,
|
||||
options: this.options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,15 +332,4 @@ export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
|
||||
this.timerWorker?.terminate()
|
||||
this.faceLandmarker?.close()
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new FaceLandmarksProcessor(this.options)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
type: this.type,
|
||||
options: this.options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-32
@@ -1,28 +1,23 @@
|
||||
import { ProcessorOptions, Track } from 'livekit-client'
|
||||
import {
|
||||
BackgroundBlur,
|
||||
BackgroundTransformer,
|
||||
ProcessorWrapper,
|
||||
VirtualBackground,
|
||||
} from '@livekit/track-processors'
|
||||
import {
|
||||
BackgroundOptions,
|
||||
BackgroundProcessorInterface,
|
||||
ProcessorType,
|
||||
} from '.'
|
||||
import { ProcessorConfig, BackgroundProcessorInterface, ProcessorType } from '.'
|
||||
|
||||
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
|
||||
processor: ProcessorWrapper<BackgroundOptions>
|
||||
opts: BackgroundOptions
|
||||
processor: ProcessorWrapper<{ imagePath?: string; blurRadius?: number }>
|
||||
opts: ProcessorConfig
|
||||
processorType: ProcessorType
|
||||
|
||||
constructor(opts: BackgroundOptions) {
|
||||
constructor(opts: ProcessorConfig) {
|
||||
this.opts = opts
|
||||
|
||||
if (opts.imagePath) {
|
||||
if (opts.type === 'virtual') {
|
||||
this.processorType = ProcessorType.VIRTUAL
|
||||
this.processor = VirtualBackground(opts.imagePath)
|
||||
} else if (opts.blurRadius !== undefined) {
|
||||
} else if (opts.type === 'blur') {
|
||||
this.processorType = ProcessorType.BLUR
|
||||
this.processor = BackgroundBlur(opts.blurRadius)
|
||||
} else {
|
||||
@@ -44,24 +39,23 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
|
||||
return this.processor.destroy()
|
||||
}
|
||||
|
||||
async update(opts: BackgroundOptions): Promise<void> {
|
||||
const newProcessorType = opts.imagePath
|
||||
? ProcessorType.VIRTUAL
|
||||
: ProcessorType.BLUR
|
||||
async update(opts: ProcessorConfig): Promise<void> {
|
||||
this.opts = opts
|
||||
|
||||
const newProcessorType =
|
||||
opts.type === 'virtual' ? ProcessorType.VIRTUAL : ProcessorType.BLUR
|
||||
|
||||
let processedOpts = opts
|
||||
if (newProcessorType !== this.processorType) {
|
||||
this.processorType = newProcessorType
|
||||
if (newProcessorType === ProcessorType.VIRTUAL) {
|
||||
this.processor.name = 'virtual-background'
|
||||
processedOpts = { ...opts, blurRadius: undefined }
|
||||
} else {
|
||||
this.processor.name = 'background-blur'
|
||||
processedOpts = { ...opts, imagePath: undefined }
|
||||
}
|
||||
}
|
||||
await this.processor.updateTransformerOptions(processedOpts)
|
||||
this.opts = processedOpts
|
||||
await this.processor.updateTransformerOptions(
|
||||
opts as { imagePath?: string; blurRadius?: number }
|
||||
)
|
||||
}
|
||||
|
||||
get name() {
|
||||
@@ -73,17 +67,6 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
|
||||
}
|
||||
|
||||
get options() {
|
||||
return (this.processor.transformer as BackgroundTransformer).options
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new UnifiedBackgroundTrackProcessor(this.options || this.opts)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
type: this.processorType,
|
||||
options: this.options,
|
||||
}
|
||||
return this.opts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,7 @@ import { ProcessorWrapper } from '@livekit/track-processors'
|
||||
import { Track, TrackProcessor } from 'livekit-client'
|
||||
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
|
||||
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
|
||||
|
||||
export type BackgroundOptions = {
|
||||
blurRadius?: number
|
||||
imagePath?: string
|
||||
}
|
||||
|
||||
export interface ProcessorSerialized {
|
||||
type: ProcessorType
|
||||
options: BackgroundOptions
|
||||
}
|
||||
|
||||
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
|
||||
update(opts: BackgroundOptions): Promise<void>
|
||||
options: BackgroundOptions
|
||||
clone(): BackgroundProcessorInterface
|
||||
serialize(): ProcessorSerialized
|
||||
}
|
||||
import { FaceLandmarksOptions } from './FaceLandmarksProcessor'
|
||||
|
||||
export enum ProcessorType {
|
||||
BLUR = 'blur',
|
||||
@@ -26,6 +10,16 @@ export enum ProcessorType {
|
||||
FACE_LANDMARKS = 'faceLandmarks',
|
||||
}
|
||||
|
||||
export type ProcessorConfig =
|
||||
| { type: ProcessorType.BLUR; blurRadius: number }
|
||||
| { type: ProcessorType.VIRTUAL; imagePath: string; fileId?: string }
|
||||
| ({ type: ProcessorType.FACE_LANDMARKS } & FaceLandmarksOptions)
|
||||
|
||||
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
|
||||
update(opts: ProcessorConfig): Promise<void>
|
||||
options: ProcessorConfig
|
||||
}
|
||||
|
||||
export class BackgroundProcessorFactory {
|
||||
static hasModernApiSupport() {
|
||||
return ProcessorWrapper.hasModernApiSupport
|
||||
@@ -36,28 +30,27 @@ export class BackgroundProcessorFactory {
|
||||
}
|
||||
|
||||
static getProcessor(
|
||||
type: ProcessorType,
|
||||
opts: BackgroundOptions
|
||||
config: ProcessorConfig
|
||||
): BackgroundProcessorInterface | undefined {
|
||||
const isBlur = type === ProcessorType.BLUR
|
||||
const isVirtual = type === ProcessorType.VIRTUAL
|
||||
const isBlur = config.type === ProcessorType.BLUR
|
||||
const isVirtual = config.type === ProcessorType.VIRTUAL
|
||||
|
||||
if (!isBlur && !isVirtual) return undefined
|
||||
|
||||
if (ProcessorWrapper.isSupported) {
|
||||
return new UnifiedBackgroundTrackProcessor(opts)
|
||||
return new UnifiedBackgroundTrackProcessor(config)
|
||||
}
|
||||
|
||||
if (BackgroundCustomProcessor.isSupported) {
|
||||
return new BackgroundCustomProcessor(opts)
|
||||
return new BackgroundCustomProcessor(config)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
static deserializeProcessor(data?: ProcessorSerialized) {
|
||||
if (data?.type) {
|
||||
return BackgroundProcessorFactory.getProcessor(data?.type, data?.options)
|
||||
static fromProcessorConfig(data?: ProcessorConfig) {
|
||||
if (data) {
|
||||
return BackgroundProcessorFactory.getProcessor(data)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
+2
-2
@@ -82,8 +82,8 @@ export const VideoDeviceControl = ({
|
||||
*
|
||||
* See https://github.com/numerique-gouv/meet/pull/309#issuecomment-2622404121
|
||||
*/
|
||||
const processor = BackgroundProcessorFactory.deserializeProcessor(
|
||||
userChoices.processorSerialized
|
||||
const processor = BackgroundProcessorFactory.fromProcessorConfig(
|
||||
userChoices.processorConfig
|
||||
)
|
||||
|
||||
const toggle = trackProps.toggle as (
|
||||
|
||||
@@ -2,14 +2,12 @@ import { useLocalParticipant } from '@livekit/components-react'
|
||||
import { LocalVideoTrack } from 'livekit-client'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { EffectsConfiguration } from './EffectsConfiguration'
|
||||
import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices'
|
||||
import { useCanPublishTrack } from '@/features/rooms/livekit/hooks/useCanPublishTrack'
|
||||
import { TrackSource } from '@livekit/protocol'
|
||||
|
||||
export const Effects = () => {
|
||||
const { cameraTrack } = useLocalParticipant()
|
||||
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
|
||||
const { saveProcessorSerialized } = usePersistentUserChoices()
|
||||
|
||||
const canPublishCamera = useCanPublishTrack(TrackSource.CAMERA)
|
||||
|
||||
@@ -24,9 +22,6 @@ export const Effects = () => {
|
||||
isDisabled={!canPublishCamera}
|
||||
videoTrack={localCameraTrack}
|
||||
layout="vertical"
|
||||
onSubmit={(processor) =>
|
||||
saveProcessorSerialized(processor?.serialize())
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
+653
-251
@@ -1,16 +1,16 @@
|
||||
import { LocalVideoTrack, Track } from 'livekit-client'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
BackgroundOptions,
|
||||
BackgroundProcessorFactory,
|
||||
BackgroundProcessorInterface,
|
||||
ProcessorConfig,
|
||||
ProcessorType,
|
||||
} from '../blur'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { H, P, Text, ToggleButton } from '@/primitives'
|
||||
import { Button, Dialog, H, P, Text, ToggleButton } from '@/primitives'
|
||||
import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { HStack, styled } from '@/styled-system/jsx'
|
||||
import { BlurOn } from '@/components/icons/BlurOn'
|
||||
import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
|
||||
import { useTrackToggle } from '@livekit/components-react'
|
||||
@@ -19,6 +19,19 @@ import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
|
||||
import { FunnyEffects } from './FunnyEffects'
|
||||
import { useHasFunnyEffectsAccess } from '../../hooks/useHasFunnyEffectsAccess'
|
||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import {
|
||||
ListFilesParams,
|
||||
useListMyFiles,
|
||||
} from '@/features/files/api/listFiles.ts'
|
||||
import { useCreateFile } from '@/features/files/api/createFile.ts'
|
||||
import { FileTrigger } from 'react-aria-components'
|
||||
import { RiDeleteBinLine, RiImageAddFill } from '@remixicon/react'
|
||||
import { useDeleteFile } from '@/features/files/api/deleteFile.ts'
|
||||
import { useUser } from '@/features/auth'
|
||||
import { ApiFileItem } from '@/features/files/api/types.ts'
|
||||
import { useConfig } from '@/api/useConfig.ts'
|
||||
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices.ts'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
|
||||
enum BlurRadius {
|
||||
NONE = 0,
|
||||
@@ -40,14 +53,51 @@ const Information = styled('div', {
|
||||
export type EffectsConfigurationProps = {
|
||||
isDisabled?: boolean
|
||||
videoTrack: LocalVideoTrack
|
||||
onSubmit?: (processor?: BackgroundProcessorInterface) => void
|
||||
layout?: 'vertical' | 'horizontal'
|
||||
}
|
||||
|
||||
const listFilesQueryParams: ListFilesParams = {
|
||||
filters: {
|
||||
type: 'background_image',
|
||||
upload_state: 'ready',
|
||||
is_creator_me: true,
|
||||
is_deleted: false,
|
||||
},
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
},
|
||||
}
|
||||
|
||||
function deriveIdFromProcessorConfig(config: ProcessorConfig) {
|
||||
if (config.type === ProcessorType.BLUR) {
|
||||
return `blur-${config.blurRadius}`
|
||||
} else if (config.type === ProcessorType.VIRTUAL) {
|
||||
// the imagePath is not stable for custom backgrounds
|
||||
// so we try first with the fileId
|
||||
if (config.fileId) {
|
||||
return `virtual-${config.fileId}`
|
||||
}
|
||||
return `virtual-${config.imagePath}`
|
||||
} else if (config.type === ProcessorType.FACE_LANDMARKS) {
|
||||
return 'face-landmarks'
|
||||
}
|
||||
throw new Error(`Unknown config type in config: ${config}`)
|
||||
}
|
||||
|
||||
// We use a valtio store so that the state is persisted between the join room
|
||||
// and the actual room
|
||||
const uploadNotPossibleLocalState = proxy({
|
||||
imageBackgroundConfig: null as null | {
|
||||
type: ProcessorType.VIRTUAL
|
||||
imagePath: string
|
||||
label: string
|
||||
},
|
||||
})
|
||||
|
||||
export const EffectsConfiguration = ({
|
||||
isDisabled,
|
||||
videoTrack,
|
||||
onSubmit,
|
||||
layout = 'horizontal',
|
||||
}: EffectsConfigurationProps) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
@@ -63,6 +113,365 @@ export const EffectsConfiguration = ({
|
||||
> | null>(null)
|
||||
const effectAnnouncementId = useRef(0)
|
||||
|
||||
const {
|
||||
saveProcessorConfig,
|
||||
userChoices: { processorConfig },
|
||||
} = usePersistentUserChoices()
|
||||
|
||||
const selectedId = useMemo(
|
||||
() =>
|
||||
processorConfig ? deriveIdFromProcessorConfig(processorConfig) : 'none',
|
||||
[processorConfig]
|
||||
)
|
||||
|
||||
const uploadNotPossibleSnap = useSnapshot(uploadNotPossibleLocalState)
|
||||
|
||||
const announceEffectStatusMessage = useCallback(
|
||||
(message: string) => {
|
||||
effectAnnouncementId.current += 1
|
||||
const currentId = effectAnnouncementId.current
|
||||
|
||||
if (effectAnnouncementTimeout.current) {
|
||||
clearTimeout(effectAnnouncementTimeout.current)
|
||||
}
|
||||
|
||||
effectAnnouncementTimeout.current = setTimeout(() => {
|
||||
if (currentId !== effectAnnouncementId.current) return
|
||||
announce(message)
|
||||
}, 80)
|
||||
},
|
||||
[announce]
|
||||
)
|
||||
|
||||
const getVirtualBackgroundName = useCallback(
|
||||
(imagePath?: string) => {
|
||||
if (!imagePath) return ''
|
||||
const match = imagePath.match(/\/backgrounds\/(\d+)\.jpg$/)
|
||||
if (!match) return ''
|
||||
const index = Number(match[1]) - 1
|
||||
if (Number.isNaN(index)) return ''
|
||||
return t(`virtual.presets.descriptions.${index}`)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
const updateEffectStatusMessage = useCallback(
|
||||
(config: ProcessorConfig, wasSelectedBeforeToggle: boolean) => {
|
||||
if (wasSelectedBeforeToggle) {
|
||||
announceEffectStatusMessage(t('blur.status.none'))
|
||||
return
|
||||
}
|
||||
|
||||
if (config.type === ProcessorType.BLUR) {
|
||||
const message =
|
||||
config.blurRadius === BlurRadius.LIGHT
|
||||
? t('blur.status.light')
|
||||
: t('blur.status.strong')
|
||||
announceEffectStatusMessage(message)
|
||||
return
|
||||
}
|
||||
|
||||
if (config.type === ProcessorType.VIRTUAL) {
|
||||
const backgroundName = getVirtualBackgroundName(config.imagePath)
|
||||
if (backgroundName) {
|
||||
announceEffectStatusMessage(
|
||||
`${t('virtual.selectedLabel')} ${backgroundName}`
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
[announceEffectStatusMessage, getVirtualBackgroundName, t]
|
||||
)
|
||||
|
||||
const toggleEffect = useCallback(
|
||||
async (config: ProcessorConfig) => {
|
||||
setProcessorPending(true)
|
||||
const wasSelectedBeforeToggle =
|
||||
selectedId === deriveIdFromProcessorConfig(config)
|
||||
|
||||
if (!videoTrack) {
|
||||
/**
|
||||
* Special case: if no video track is available, then we must pass directly the processor into the
|
||||
* toggle call. Otherwise, the rest of the function below would not have a videoTrack to call
|
||||
* setProcessor on.
|
||||
*
|
||||
* We arrive in this condition when we enter the room with the camera already off.
|
||||
*/
|
||||
const newProcessorTmp = BackgroundProcessorFactory.getProcessor(config)!
|
||||
await toggle(true, {
|
||||
processor: newProcessorTmp,
|
||||
})
|
||||
setTimeout(() => setProcessorPending(false))
|
||||
return
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
await toggle(true)
|
||||
}
|
||||
|
||||
const processor =
|
||||
videoTrack?.getProcessor() as BackgroundProcessorInterface
|
||||
try {
|
||||
if (wasSelectedBeforeToggle) {
|
||||
// Stop processor.
|
||||
await videoTrack.stopProcessor()
|
||||
saveProcessorConfig(undefined)
|
||||
} else if (
|
||||
!processor ||
|
||||
(processor.options.type !== config.type &&
|
||||
!BackgroundProcessorFactory.hasModernApiSupport())
|
||||
) {
|
||||
// Change processor.
|
||||
const newProcessor = BackgroundProcessorFactory.getProcessor(config)!
|
||||
// IMPORTANT: Must explicitly stop previous processor before setting a new one
|
||||
// in browsers without modern API support to prevent UI crashes.
|
||||
// This workaround is needed until this issue is resolved:
|
||||
// https://github.com/livekit/track-processors-js/issues/85
|
||||
if (!BackgroundProcessorFactory.hasModernApiSupport()) {
|
||||
await videoTrack.stopProcessor()
|
||||
}
|
||||
await videoTrack.setProcessor(newProcessor)
|
||||
saveProcessorConfig(config)
|
||||
} else {
|
||||
await processor?.update(config)
|
||||
saveProcessorConfig(config)
|
||||
}
|
||||
|
||||
updateEffectStatusMessage(config, wasSelectedBeforeToggle)
|
||||
} catch (error) {
|
||||
console.error('Error applying effect:', error)
|
||||
} finally {
|
||||
// Without setTimeout the DOM is not refreshing when updating the options.
|
||||
setTimeout(() => setProcessorPending(false))
|
||||
}
|
||||
},
|
||||
[
|
||||
enabled,
|
||||
saveProcessorConfig,
|
||||
selectedId,
|
||||
toggle,
|
||||
updateEffectStatusMessage,
|
||||
videoTrack,
|
||||
]
|
||||
)
|
||||
|
||||
const { data: appConfig } = useConfig()
|
||||
const { isLoggedIn } = useUser()
|
||||
const canUploadBackground =
|
||||
isLoggedIn === true &&
|
||||
appConfig?.background_image?.upload_is_enabled === true
|
||||
// We split the error state in 2 parts so that there are no visual glitches when closing the alert
|
||||
const [personalBackgroundHasError, setPersonalBackgroundHasError] =
|
||||
useState<boolean>(false)
|
||||
const [personalBackgroundError, setPersonalBackgroundError] = useState<
|
||||
'file_too_large' | 'invalid_file_type' | null
|
||||
>(null)
|
||||
const createFileMutation = useCreateFile()
|
||||
const deleteFileMutation = useDeleteFile()
|
||||
const filesQ = useListMyFiles(listFilesQueryParams)
|
||||
const hasReachedMaxNbBackgrounds =
|
||||
(canUploadBackground &&
|
||||
appConfig &&
|
||||
filesQ.data &&
|
||||
filesQ.data.count >= appConfig.background_image.max_count_by_user) ??
|
||||
false
|
||||
|
||||
const getHandleSelectChangeFile = useCallback(
|
||||
(file: ApiFileItem) => {
|
||||
return async () => {
|
||||
await toggleEffect({
|
||||
type: ProcessorType.VIRTUAL,
|
||||
imagePath: file.url!,
|
||||
fileId: file.id,
|
||||
})
|
||||
}
|
||||
},
|
||||
[toggleEffect]
|
||||
)
|
||||
|
||||
const handleNewBackgroundFilePicked = useCallback(
|
||||
async (file: File) => {
|
||||
if (
|
||||
!(
|
||||
appConfig?.background_image?.allowed_mimetypes?.includes(file.type) ??
|
||||
false
|
||||
)
|
||||
) {
|
||||
setPersonalBackgroundError('invalid_file_type')
|
||||
setPersonalBackgroundHasError(true)
|
||||
return
|
||||
}
|
||||
if (file.size > (appConfig?.background_image?.max_size ?? 0)) {
|
||||
setPersonalBackgroundError('file_too_large')
|
||||
setPersonalBackgroundHasError(true)
|
||||
return
|
||||
}
|
||||
|
||||
// When the user is not logged in, we fallback to just loading that image
|
||||
if (!canUploadBackground) {
|
||||
// For the preview to work, we somehow need to create a data URL from the raw file.
|
||||
// revoking is handled with userChoicesStore
|
||||
if (uploadNotPossibleSnap.imageBackgroundConfig) {
|
||||
URL.revokeObjectURL(
|
||||
uploadNotPossibleSnap.imageBackgroundConfig.imagePath
|
||||
)
|
||||
}
|
||||
const imagePath = URL.createObjectURL(file)
|
||||
|
||||
const fileId = `local-image`
|
||||
await toggleEffect({
|
||||
type: ProcessorType.VIRTUAL,
|
||||
imagePath,
|
||||
fileId,
|
||||
})
|
||||
uploadNotPossibleLocalState.imageBackgroundConfig = {
|
||||
type: ProcessorType.VIRTUAL,
|
||||
label: file.name.split('.')[0],
|
||||
imagePath,
|
||||
}
|
||||
} else {
|
||||
// Otherwise we create the file in the backend and automatically select it
|
||||
// when it's uploaded.
|
||||
createFileMutation.mutate(
|
||||
{
|
||||
file,
|
||||
onProgress: (progress) => {
|
||||
console.debug('upload-progress', progress)
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (file) => {
|
||||
// We automatically select that created file
|
||||
getHandleSelectChangeFile(file)()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
[
|
||||
appConfig?.background_image?.allowed_mimetypes,
|
||||
appConfig?.background_image?.max_size,
|
||||
canUploadBackground,
|
||||
createFileMutation,
|
||||
getHandleSelectChangeFile,
|
||||
toggleEffect,
|
||||
uploadNotPossibleSnap.imageBackgroundConfig,
|
||||
]
|
||||
)
|
||||
|
||||
const filePickerErrorContext = useMemo(
|
||||
() => ({
|
||||
allowedExtension: appConfig?.background_image?.allowed_extensions ?? [],
|
||||
maxSize: (appConfig?.background_image?.max_size ?? 0) / (1024 * 1024),
|
||||
}),
|
||||
[appConfig?.background_image]
|
||||
)
|
||||
|
||||
const processorOptions = useMemo<{
|
||||
isDisabled: boolean
|
||||
blurBased: {
|
||||
radius: BlurRadius
|
||||
Icon: React.FC
|
||||
ref?: React.Ref<HTMLButtonElement>
|
||||
tooltip: string
|
||||
id: string
|
||||
config: ProcessorConfig
|
||||
isSelected: boolean
|
||||
}[]
|
||||
virtualBackgrounds: {
|
||||
id: string
|
||||
config: ProcessorConfig
|
||||
isSelected: boolean
|
||||
tooltip: string
|
||||
ariaLabel: string
|
||||
thumbnailPath: string
|
||||
index: number
|
||||
}[]
|
||||
remoteCustomVirtualBackgrounds: {
|
||||
id: string
|
||||
config: ProcessorConfig
|
||||
isSelected: boolean
|
||||
tooltip: string
|
||||
file: ApiFileItem
|
||||
}[]
|
||||
}>(() => {
|
||||
return {
|
||||
isDisabled: (processorPendingReveal || isDisabled) ?? false,
|
||||
blurBased: [
|
||||
{
|
||||
key: 'light',
|
||||
radius: BlurRadius.LIGHT,
|
||||
icon: BlurOn,
|
||||
ref: blurLightRef,
|
||||
},
|
||||
{
|
||||
key: 'normal',
|
||||
radius: BlurRadius.NORMAL,
|
||||
icon: BlurOnStrong,
|
||||
ref: undefined,
|
||||
},
|
||||
].map((item) => {
|
||||
const config: ProcessorConfig = {
|
||||
type: ProcessorType.BLUR,
|
||||
blurRadius: item.radius,
|
||||
}
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
return {
|
||||
id,
|
||||
tooltip: t(`blur.light.${selectedId === id ? 'clear' : 'apply'}`),
|
||||
radius: item.radius,
|
||||
isSelected: selectedId === id,
|
||||
Icon: item.icon,
|
||||
ref: item.ref,
|
||||
config,
|
||||
}
|
||||
}),
|
||||
virtualBackgrounds: [...Array(8).keys()].map((index) => {
|
||||
const imagePath = `/assets/backgrounds/${index + 1}.jpg`
|
||||
const thumbnailPath = `/assets/backgrounds/thumbnails/${index + 1}.jpg`
|
||||
const config: ProcessorConfig = {
|
||||
type: ProcessorType.VIRTUAL,
|
||||
imagePath,
|
||||
}
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
const isSelected = selectedId === id
|
||||
const prefix = isSelected ? 'selectedLabel' : 'apply'
|
||||
const backgroundName = t(`virtual.presets.descriptions.${index}`)
|
||||
const ariaLabel = `${t(`virtual.presets.${prefix}`)} ${backgroundName}`
|
||||
|
||||
return {
|
||||
tooltip: backgroundName,
|
||||
id,
|
||||
config,
|
||||
isSelected: selectedId === id,
|
||||
thumbnailPath,
|
||||
ariaLabel,
|
||||
index,
|
||||
}
|
||||
}),
|
||||
remoteCustomVirtualBackgrounds: (filesQ.data?.results ?? [])
|
||||
.filter((file) => file.url)
|
||||
.map((file) => {
|
||||
const config: ProcessorConfig = {
|
||||
type: ProcessorType.VIRTUAL,
|
||||
imagePath: file.url!,
|
||||
fileId: file.id,
|
||||
}
|
||||
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
|
||||
return {
|
||||
tooltip: file.title,
|
||||
id,
|
||||
config,
|
||||
isSelected: selectedId === id,
|
||||
file,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}, [processorPendingReveal, isDisabled, filesQ.data?.results, t, selectedId])
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current
|
||||
if (!videoElement) return
|
||||
@@ -97,171 +506,6 @@ export const EffectsConfiguration = ({
|
||||
[]
|
||||
)
|
||||
|
||||
const announceEffectStatusMessage = (message: string) => {
|
||||
effectAnnouncementId.current += 1
|
||||
const currentId = effectAnnouncementId.current
|
||||
|
||||
if (effectAnnouncementTimeout.current) {
|
||||
clearTimeout(effectAnnouncementTimeout.current)
|
||||
}
|
||||
|
||||
effectAnnouncementTimeout.current = setTimeout(() => {
|
||||
if (currentId !== effectAnnouncementId.current) return
|
||||
announce(message)
|
||||
}, 80)
|
||||
}
|
||||
|
||||
const clearEffect = async () => {
|
||||
await videoTrack.stopProcessor()
|
||||
onSubmit?.(undefined)
|
||||
}
|
||||
|
||||
const getVirtualBackgroundName = (imagePath?: string) => {
|
||||
if (!imagePath) return ''
|
||||
const match = imagePath.match(/\/backgrounds\/(\d+)\.jpg$/)
|
||||
if (!match) return ''
|
||||
const index = Number(match[1]) - 1
|
||||
if (Number.isNaN(index)) return ''
|
||||
return t(`virtual.descriptions.${index}`)
|
||||
}
|
||||
|
||||
const updateEffectStatusMessage = (
|
||||
type: ProcessorType,
|
||||
options: BackgroundOptions,
|
||||
wasSelectedBeforeToggle: boolean
|
||||
) => {
|
||||
if (wasSelectedBeforeToggle) {
|
||||
announceEffectStatusMessage(t('blur.status.none'))
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ProcessorType.BLUR) {
|
||||
const message =
|
||||
options.blurRadius === BlurRadius.LIGHT
|
||||
? t('blur.status.light')
|
||||
: t('blur.status.strong')
|
||||
announceEffectStatusMessage(message)
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ProcessorType.VIRTUAL) {
|
||||
const backgroundName = getVirtualBackgroundName(options.imagePath)
|
||||
if (backgroundName) {
|
||||
announceEffectStatusMessage(
|
||||
`${t('virtual.selectedLabel')} ${backgroundName}`
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEffect = async (
|
||||
type: ProcessorType,
|
||||
options: BackgroundOptions
|
||||
) => {
|
||||
setProcessorPending(true)
|
||||
const wasSelectedBeforeToggle = isSelected(type, options)
|
||||
|
||||
if (!videoTrack) {
|
||||
/**
|
||||
* Special case: if no video track is available, then we must pass directly the processor into the
|
||||
* toggle call. Otherwise, the rest of the function below would not have a videoTrack to call
|
||||
* setProcessor on.
|
||||
*
|
||||
* We arrive in this condition when we enter the room with the camera already off.
|
||||
*/
|
||||
const newProcessorTmp = BackgroundProcessorFactory.getProcessor(
|
||||
type,
|
||||
options
|
||||
)!
|
||||
await toggle(true, {
|
||||
processor: newProcessorTmp,
|
||||
})
|
||||
setTimeout(() => setProcessorPending(false))
|
||||
return
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
await toggle(true)
|
||||
}
|
||||
|
||||
const processor = getProcessor()
|
||||
try {
|
||||
if (wasSelectedBeforeToggle) {
|
||||
// Stop processor.
|
||||
await clearEffect()
|
||||
} else if (
|
||||
!processor ||
|
||||
(processor.serialize().type !== type &&
|
||||
!BackgroundProcessorFactory.hasModernApiSupport())
|
||||
) {
|
||||
// Change processor.
|
||||
const newProcessor = BackgroundProcessorFactory.getProcessor(
|
||||
type,
|
||||
options
|
||||
)!
|
||||
// IMPORTANT: Must explicitly stop previous processor before setting a new one
|
||||
// in browsers without modern API support to prevent UI crashes.
|
||||
// This workaround is needed until this issue is resolved:
|
||||
// https://github.com/livekit/track-processors-js/issues/85
|
||||
if (!BackgroundProcessorFactory.hasModernApiSupport()) {
|
||||
await videoTrack.stopProcessor()
|
||||
}
|
||||
await videoTrack.setProcessor(newProcessor)
|
||||
onSubmit?.(newProcessor)
|
||||
} else {
|
||||
await processor?.update(options)
|
||||
// We want to trigger onSubmit when options changes so the parent component is aware of it.
|
||||
onSubmit?.(processor)
|
||||
}
|
||||
|
||||
updateEffectStatusMessage(type, options, wasSelectedBeforeToggle)
|
||||
} catch (error) {
|
||||
console.error('Error applying effect:', error)
|
||||
} finally {
|
||||
// Without setTimeout the DOM is not refreshing when updating the options.
|
||||
setTimeout(() => setProcessorPending(false))
|
||||
}
|
||||
}
|
||||
|
||||
const getProcessor = () => {
|
||||
return videoTrack?.getProcessor() as BackgroundProcessorInterface
|
||||
}
|
||||
|
||||
const isSelected = (type: ProcessorType, options: BackgroundOptions) => {
|
||||
const processor = getProcessor()
|
||||
const processorSerialized = processor?.serialize()
|
||||
return (
|
||||
!!processor &&
|
||||
processorSerialized.type === type &&
|
||||
JSON.stringify(processorSerialized.options) === JSON.stringify(options)
|
||||
)
|
||||
}
|
||||
|
||||
const tooltipBlur = (type: ProcessorType, options: BackgroundOptions) => {
|
||||
const strength =
|
||||
options.blurRadius === BlurRadius.LIGHT ? 'light' : 'normal'
|
||||
const action = isSelected(type, options) ? 'clear' : 'apply'
|
||||
|
||||
return t(`${type}.${strength}.${action}`)
|
||||
}
|
||||
|
||||
const ariaLabelVirtualBackground = (
|
||||
index: number,
|
||||
imagePath: string
|
||||
): string => {
|
||||
const isSelectedBackground = isSelected(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})
|
||||
const prefix = isSelectedBackground ? 'selectedLabel' : 'apply'
|
||||
const backgroundName = t(`virtual.descriptions.${index}`)
|
||||
return `${t(`virtual.${prefix}`)} ${backgroundName}`
|
||||
}
|
||||
|
||||
const tooltipVirtualBackground = (index: number): string => {
|
||||
return t(`virtual.descriptions.${index}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css(
|
||||
@@ -362,7 +606,7 @@ export const EffectsConfiguration = ({
|
||||
<div>
|
||||
<div>
|
||||
<H
|
||||
lvl={3}
|
||||
lvl={2}
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
@@ -377,65 +621,208 @@ export const EffectsConfiguration = ({
|
||||
gap: '1.25rem',
|
||||
})}
|
||||
>
|
||||
<ToggleButton
|
||||
ref={blurLightRef}
|
||||
variant="bigSquare"
|
||||
aria-label={tooltipBlur(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.LIGHT,
|
||||
})}
|
||||
tooltip={tooltipBlur(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.LIGHT,
|
||||
})}
|
||||
isDisabled={processorPendingReveal || isDisabled}
|
||||
onChange={async () =>
|
||||
await toggleEffect(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.LIGHT,
|
||||
})
|
||||
}
|
||||
isSelected={isSelected(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.LIGHT,
|
||||
})}
|
||||
data-attr="toggle-blur-light"
|
||||
>
|
||||
<BlurOn />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={tooltipBlur(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.NORMAL,
|
||||
})}
|
||||
tooltip={tooltipBlur(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.NORMAL,
|
||||
})}
|
||||
isDisabled={processorPendingReveal || isDisabled}
|
||||
onChange={async () =>
|
||||
await toggleEffect(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.NORMAL,
|
||||
})
|
||||
}
|
||||
isSelected={isSelected(ProcessorType.BLUR, {
|
||||
blurRadius: BlurRadius.NORMAL,
|
||||
})}
|
||||
data-attr="toggle-blur-normal"
|
||||
>
|
||||
<BlurOnStrong />
|
||||
</ToggleButton>
|
||||
{processorOptions.blurBased.map(({ Icon, ...option }) => (
|
||||
<ToggleButton
|
||||
key={option.id}
|
||||
ref={option.ref}
|
||||
variant="bigSquare"
|
||||
aria-label={option.tooltip}
|
||||
tooltip={option.tooltip}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={() => toggleEffect(option.config)}
|
||||
isSelected={option.isSelected}
|
||||
data-attr={`toggle-${option.id}`}
|
||||
>
|
||||
<Icon />
|
||||
</ToggleButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
marginTop: '1.5rem',
|
||||
})}
|
||||
>
|
||||
|
||||
<div className={css({ marginTop: '1.5rem' })}>
|
||||
<H
|
||||
lvl={3}
|
||||
lvl={2}
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
marginBottom: '0.6rem',
|
||||
}}
|
||||
variant="bodyXsBold"
|
||||
>
|
||||
{t('virtual.title')}
|
||||
</H>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
marginBottom: '1rem',
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={2}
|
||||
style={{
|
||||
marginBottom: '0.4rem',
|
||||
}}
|
||||
variant="bodyXsMedium"
|
||||
>
|
||||
{t('virtual.personal.title')}
|
||||
</H>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1.25rem',
|
||||
paddingBottom: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
{canUploadBackground &&
|
||||
processorOptions.remoteCustomVirtualBackgrounds.map(
|
||||
(option) => (
|
||||
<div
|
||||
key={option.id}
|
||||
className={
|
||||
'hoverGroup ' + css({ position: 'relative' })
|
||||
}
|
||||
>
|
||||
<VisualOnlyTooltip tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.tooltip}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={getHandleSelectChangeFile(option.file)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.file.url!})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${option.file.id}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
<Button
|
||||
className={
|
||||
'hoverGroupChild ' +
|
||||
css({
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
})
|
||||
}
|
||||
size={'xs'}
|
||||
variant={'tertiary'}
|
||||
onClick={() => {
|
||||
if (option.isSelected) {
|
||||
// we remove the current effect
|
||||
toggleEffect(option.config)
|
||||
}
|
||||
deleteFileMutation.mutate({
|
||||
fileId: option.file.id,
|
||||
})
|
||||
}}
|
||||
isDisabled={deleteFileMutation.isPending}
|
||||
>
|
||||
<RiDeleteBinLine size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{!canUploadBackground &&
|
||||
uploadNotPossibleSnap.imageBackgroundConfig && (
|
||||
<VisualOnlyTooltip
|
||||
tooltip={
|
||||
uploadNotPossibleSnap.imageBackgroundConfig.label
|
||||
}
|
||||
>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={
|
||||
uploadNotPossibleSnap.imageBackgroundConfig.label
|
||||
}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={() => {
|
||||
toggleEffect(
|
||||
uploadNotPossibleSnap.imageBackgroundConfig!
|
||||
)
|
||||
}}
|
||||
isSelected={
|
||||
deriveIdFromProcessorConfig(
|
||||
uploadNotPossibleSnap.imageBackgroundConfig
|
||||
) === selectedId
|
||||
}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${uploadNotPossibleSnap.imageBackgroundConfig.imagePath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-local`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
)}
|
||||
<FileTrigger
|
||||
acceptedFileTypes={
|
||||
appConfig?.background_image?.allowed_mimetypes ?? [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
]
|
||||
}
|
||||
onSelect={(e) => {
|
||||
if (e && e.item(0)) {
|
||||
const file = e.item(0) as File
|
||||
handleNewBackgroundFilePicked(file)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="bigSquare"
|
||||
aria-label={t('virtual.personal.selectFileTooltip')}
|
||||
tooltip={t('virtual.personal.selectFileTooltip')}
|
||||
isDisabled={
|
||||
(canUploadBackground &&
|
||||
filesQ.data &&
|
||||
filesQ.data.count >=
|
||||
(appConfig?.background_image?.max_count_by_user ??
|
||||
0)) ||
|
||||
processorOptions.isDisabled ||
|
||||
createFileMutation.isPending
|
||||
}
|
||||
data-attr="input-file-select-personal-background"
|
||||
>
|
||||
<RiImageAddFill />
|
||||
</Button>
|
||||
</FileTrigger>
|
||||
</div>
|
||||
{!isLoggedIn && (
|
||||
<Text variant="xsNote">
|
||||
{t('virtual.personal.notLoggedInWarning')}
|
||||
</Text>
|
||||
)}
|
||||
{!canUploadBackground && isLoggedIn && (
|
||||
<Text variant="xsNote">
|
||||
{t('virtual.personal.warningUploadDisabled')}
|
||||
</Text>
|
||||
)}
|
||||
{hasReachedMaxNbBackgrounds && (
|
||||
<Text variant="xsNote">
|
||||
{t('virtual.personal.uploadLimitReached')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
marginTop: '0.4rem',
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={2}
|
||||
style={{
|
||||
marginBottom: '0.4rem',
|
||||
}}
|
||||
variant="bodyXsMedium"
|
||||
>
|
||||
{t('virtual.presets.title')}
|
||||
</H>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
@@ -444,35 +831,24 @@ export const EffectsConfiguration = ({
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
{[...Array(8).keys()].map((i) => {
|
||||
const imagePath = `/assets/backgrounds/${i + 1}.jpg`
|
||||
const thumbnailPath = `/assets/backgrounds/thumbnails/${i + 1}.jpg`
|
||||
const tooltipText = tooltipVirtualBackground(i)
|
||||
return (
|
||||
<VisualOnlyTooltip key={i} tooltip={tooltipText}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={ariaLabelVirtualBackground(i, imagePath)}
|
||||
isDisabled={processorPendingReveal || isDisabled}
|
||||
onChange={async () =>
|
||||
await toggleEffect(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})
|
||||
}
|
||||
isSelected={isSelected(ProcessorType.VIRTUAL, {
|
||||
imagePath,
|
||||
})}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${i}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
)
|
||||
})}
|
||||
{processorOptions.virtualBackgrounds.map((option) => (
|
||||
<VisualOnlyTooltip key={option.id} tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.ariaLabel}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={() => toggleEffect(option.config)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-preset-${option.index}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -483,6 +859,32 @@ export const EffectsConfiguration = ({
|
||||
</Information>
|
||||
)}
|
||||
</div>
|
||||
<Dialog
|
||||
isOpen={personalBackgroundHasError}
|
||||
type="alert"
|
||||
title={t(`virtual.personal.errors.${personalBackgroundError}.title`)}
|
||||
aria-label={t(
|
||||
`virtual.personal.errors.${personalBackgroundError}.title`
|
||||
)}
|
||||
onClose={() => setPersonalBackgroundHasError(false)}
|
||||
onOpenChange={() => setPersonalBackgroundHasError(false)}
|
||||
>
|
||||
<P>
|
||||
{t(
|
||||
`virtual.personal.errors.${personalBackgroundError}.description`,
|
||||
filePickerErrorContext
|
||||
)}
|
||||
</P>
|
||||
<HStack justifyContent="end" direction="row">
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
onPress={() => setPersonalBackgroundHasError(false)}
|
||||
>
|
||||
{t('virtual.personal.errors.close')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const FunnyEffects = ({
|
||||
showFrench: false,
|
||||
}
|
||||
}
|
||||
return processor.serialize().options
|
||||
return { ...processor.options }
|
||||
}
|
||||
|
||||
const options = getOptions()
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
||||
export function useGridLayout(
|
||||
/** HTML element that contains the grid. */
|
||||
gridElement: React.RefObject<HTMLDivElement>,
|
||||
/** Count of tracks that should get layed out */
|
||||
/** Count of tracks that should get laid out */
|
||||
trackCount: number,
|
||||
options: {
|
||||
gridLayouts?: GridLayoutDefinition[]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { userChoicesStore } from '@/stores/userChoices'
|
||||
import type { VideoResolution } from '@/stores/userChoices'
|
||||
import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur'
|
||||
import { ProcessorConfig } from '@/features/rooms/livekit/components/blur'
|
||||
import type { VideoQuality } from 'livekit-client'
|
||||
|
||||
export function usePersistentUserChoices() {
|
||||
@@ -36,10 +36,8 @@ export function usePersistentUserChoices() {
|
||||
saveNoiseReductionEnabled: (enabled: boolean) => {
|
||||
userChoicesStore.noiseReductionEnabled = enabled
|
||||
},
|
||||
saveProcessorSerialized: (
|
||||
processorSerialized: ProcessorSerialized | undefined
|
||||
) => {
|
||||
userChoicesStore.processorSerialized = processorSerialized
|
||||
saveProcessorConfig: (processorConfig: ProcessorConfig | undefined) => {
|
||||
userChoicesStore.processorConfig = processorConfig
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
const {
|
||||
userChoices: {
|
||||
videoDeviceId,
|
||||
processorSerialized,
|
||||
processorConfig,
|
||||
videoPublishResolution,
|
||||
videoSubscribeQuality,
|
||||
},
|
||||
@@ -78,7 +78,7 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
resolution: VideoPresets[key].resolution,
|
||||
deviceId: { exact: videoDeviceId },
|
||||
processor:
|
||||
BackgroundProcessorFactory.deserializeProcessor(processorSerialized),
|
||||
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import { useSnapshot } from 'valtio'
|
||||
import {
|
||||
accessibilityStore,
|
||||
type CaptionTextSize,
|
||||
type CaptionColor,
|
||||
CAPTION_TEXT_SIZE_OPTIONS,
|
||||
CAPTION_COLOR_OPTIONS,
|
||||
} from '@/stores/accessibility'
|
||||
import { useAreSubtitlesAvailable } from '../hooks/useAreSubtitlesAvailable'
|
||||
|
||||
export const CaptionsSettings = () => {
|
||||
const { t } = useTranslation('settings', {
|
||||
@@ -24,6 +27,27 @@ export const CaptionsSettings = () => {
|
||||
[t]
|
||||
)
|
||||
|
||||
const captionFontColorItems = useMemo(
|
||||
() =>
|
||||
CAPTION_COLOR_OPTIONS.map((color) => ({
|
||||
value: color,
|
||||
label: t(`fontColor.options.${color}`),
|
||||
})),
|
||||
[t]
|
||||
)
|
||||
|
||||
const captionBackgroundColorItems = useMemo(
|
||||
() =>
|
||||
CAPTION_COLOR_OPTIONS.map((color) => ({
|
||||
value: color,
|
||||
label: t(`backgroundColor.options.${color}`),
|
||||
})),
|
||||
[t]
|
||||
)
|
||||
|
||||
const areSubtitlesAvailable = useAreSubtitlesAvailable()
|
||||
if (!areSubtitlesAvailable) return null
|
||||
|
||||
return (
|
||||
<li>
|
||||
<H
|
||||
@@ -34,16 +58,44 @@ export const CaptionsSettings = () => {
|
||||
>
|
||||
{t('heading')}
|
||||
</H>
|
||||
<Field
|
||||
type="select"
|
||||
label={t('textSize.label')}
|
||||
items={captionTextSizeItems}
|
||||
selectedKey={snap.captionTextSize}
|
||||
onSelectionChange={(key) => {
|
||||
accessibilityStore.captionTextSize = key as CaptionTextSize
|
||||
}}
|
||||
wrapperProps={{ noMargin: true, fullWidth: true }}
|
||||
/>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.75rem',
|
||||
})}
|
||||
>
|
||||
<Field
|
||||
type="select"
|
||||
label={t('textSize.label')}
|
||||
items={captionTextSizeItems}
|
||||
selectedKey={snap.captionTextSize}
|
||||
onSelectionChange={(key) => {
|
||||
accessibilityStore.captionTextSize = key as CaptionTextSize
|
||||
}}
|
||||
wrapperProps={{ noMargin: true, fullWidth: true }}
|
||||
/>
|
||||
<Field
|
||||
type="select"
|
||||
label={t('fontColor.label')}
|
||||
items={captionFontColorItems}
|
||||
selectedKey={snap.captionFontColor}
|
||||
onSelectionChange={(key) => {
|
||||
accessibilityStore.captionFontColor = key as CaptionColor
|
||||
}}
|
||||
wrapperProps={{ noMargin: true, fullWidth: true }}
|
||||
/>
|
||||
<Field
|
||||
type="select"
|
||||
label={t('backgroundColor.label')}
|
||||
items={captionBackgroundColorItems}
|
||||
selectedKey={snap.captionBackgroundColor}
|
||||
onSelectionChange={(key) => {
|
||||
accessibilityStore.captionBackgroundColor = key as CaptionColor
|
||||
}}
|
||||
wrapperProps={{ noMargin: true, fullWidth: true }}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useSnapshot } from 'valtio'
|
||||
import {
|
||||
accessibilityStore,
|
||||
CAPTION_TEXT_SIZE_OPTIONS,
|
||||
CAPTION_FONT_COLOR_VALUES,
|
||||
CAPTION_BACKGROUND_COLOR_VALUES,
|
||||
type CaptionTextSize,
|
||||
} from '@/stores/accessibility'
|
||||
|
||||
@@ -92,10 +94,14 @@ const useTranscriptionState = () => {
|
||||
}
|
||||
|
||||
const Transcription = ({ row }: { row: TranscriptionRow }) => {
|
||||
const { captionTextSize } = useSnapshot(accessibilityStore)
|
||||
const { captionTextSize, captionFontColor, captionBackgroundColor } =
|
||||
useSnapshot(accessibilityStore)
|
||||
const participantColor = getParticipantColor(row.participant)
|
||||
const participantName = getParticipantName(row.participant)
|
||||
const { fontSize, lineHeight } = CAPTION_FONT_SIZES[captionTextSize]
|
||||
const fontColor = CAPTION_FONT_COLOR_VALUES[captionFontColor]
|
||||
const backgroundColor =
|
||||
CAPTION_BACKGROUND_COLOR_VALUES[captionBackgroundColor]
|
||||
|
||||
const getDisplayText = (row: TranscriptionRow): string => {
|
||||
return row.segments
|
||||
@@ -128,9 +134,9 @@ const Transcription = ({ row }: { row: TranscriptionRow }) => {
|
||||
/>
|
||||
<div
|
||||
className={css({
|
||||
color: 'white',
|
||||
width: '100%',
|
||||
})}
|
||||
style={{ color: fontColor }}
|
||||
>
|
||||
<Text variant="h3" margin={false}>
|
||||
{participantName}
|
||||
@@ -138,8 +144,10 @@ const Transcription = ({ row }: { row: TranscriptionRow }) => {
|
||||
<p
|
||||
className={css({
|
||||
fontWeight: '400',
|
||||
borderRadius: '4px',
|
||||
padding: '0.125rem 0.25rem',
|
||||
})}
|
||||
style={{ fontSize, lineHeight }}
|
||||
style={{ fontSize, lineHeight, backgroundColor }}
|
||||
>
|
||||
{displayText}
|
||||
</p>
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
},
|
||||
"scope": "Diese Erklärung zur Barrierefreiheit gilt für die Website visio.numerique.gouv.fr",
|
||||
"complianceStatus": {
|
||||
"title": "Stand der Vereinbarkeit",
|
||||
"title": "Stand der Gesetzeskonformität",
|
||||
"body": "visio.numerique.gouv.fr ist nicht konform mit RGAA 4.1. Die Website wurde noch nicht geprüft. Das Team bemüht sich jedoch, eine für alle zugängliche Seite gemäß den Empfehlungen des RGAA zu erstellen."
|
||||
},
|
||||
"improvement": {
|
||||
"title": "Verbesserung und Kontakt",
|
||||
"body": "Wenn Sie auf Inhalte oder Dienste nicht zugreifen können, können Sie die verantwortliche Person von lasuite.numerique.gouv.fr kontaktieren, um eine barrierefreie Alternative zu erhalten oder die Inhalte in einer anderen Form zu bekommen.",
|
||||
"body": "Wenn du auf Inhalte oder Dienste nicht zugreifen können, kannst du die verantwortliche Person von lasuite.numerique.gouv.fr kontaktieren, um eine barrierefreie Alternative zu erhalten oder die Inhalte in einer anderen Form zu bekommen.",
|
||||
"contact": {
|
||||
"email": "E-Mail: visio@numerique.gouv.fr",
|
||||
"address": "Adresse: DINUM, 20 avenue de Ségur, 75007 Paris"
|
||||
@@ -22,11 +22,11 @@
|
||||
},
|
||||
"recourse": {
|
||||
"title": "Rechtsbehelfe",
|
||||
"introduction": "Dieses Verfahren ist in folgendem Fall zu verwenden: Sie haben der für die Website zuständigen Stelle ein Problem mit der Barrierefreiheit gemeldet, das Sie daran hindert, auf Inhalte oder Dienste des Portals zuzugreifen, und keine zufriedenstellende Antwort erhalten.",
|
||||
"introduction": "Dieses Verfahren ist in folgendem Fall zu verwenden: Du hast der für die Website zuständigen Stelle ein Problem mit der Barrierefreiheit gemeldet, das dich daran hindert, auf Inhalte oder Dienste des Portals zuzugreifen, und keine zufriedenstellende Antwort erhalten.",
|
||||
"options": {
|
||||
"intro": "Sie können:",
|
||||
"option1": "Dem*der Beauftragten für Rechte eine Nachricht schreiben",
|
||||
"option2": "Den*die Vertreter*in des*der Beauftragten für Rechte in Ihrer Region kontaktieren",
|
||||
"intro": "Du kannst:",
|
||||
"option1": "Dem*der Rechtsbeauftragten eine Nachricht schreiben",
|
||||
"option2": "Den*die Vertreter*in des*der Rechtsbeauftragten in Ihrer Region kontaktieren",
|
||||
"option3": "Einen Brief per Post senden (kostenfrei, ohne Briefmarke): </br> Défenseur des droits, Libre réponse 71120, 75342 Paris CEDEX 07"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"heading": "Beim Laden der Seite ist ein Fehler aufgetreten"
|
||||
},
|
||||
"feedback": {
|
||||
"context": "Ihr Feedback prägt das Produkt!",
|
||||
"cta": "Teilen Sie uns Ihre Meinung mit - neues Fenster"
|
||||
"context": "Dein Feedback gestaltet das Produkt!",
|
||||
"cta": "Teile uns deine Meinung mit – neuer Tab"
|
||||
},
|
||||
"forbidden": {
|
||||
"heading": "Zugriff verweigert"
|
||||
@@ -17,13 +17,13 @@
|
||||
"login": {
|
||||
"buttonLabel": "Anmelden",
|
||||
"proconnectButtonLabel": "Mit ProConnect anmelden",
|
||||
"proconnectLinkLabel": "Was ist ProConnect? – neues Fenster",
|
||||
"proconnectLinkLabel": "Was ist ProConnect? – neuer Tab",
|
||||
"proconnectLink": "Was ist ProConnect?"
|
||||
},
|
||||
"logout": "Abmelden",
|
||||
"notFound": {
|
||||
"heading": "Überprüfen Sie Ihren Meeting-Code",
|
||||
"body": "Stellen Sie sicher, dass Sie den richtigen Meeting-Code in der URL eingegeben haben. Beispiel:"
|
||||
"heading": "Überprüfe deinen Meeting-Code",
|
||||
"body": "Stelle sicher, dass du den richtigen Meeting-Code in der URL eingegeben haben. Beispiel:"
|
||||
},
|
||||
"selected": "ausgewählt",
|
||||
"submit": "OK",
|
||||
@@ -38,16 +38,16 @@
|
||||
"accessibility": "Barrierefreiheit: nicht konform",
|
||||
"ariaLabel": "neues Fenster",
|
||||
"codeAnnotation": "Unser Code ist offen und verfügbar in diesem",
|
||||
"code": "Open-Source-Code-Repository",
|
||||
"technicalDetails": "Technisches Datenblatt",
|
||||
"code": "Open-Source-Repository",
|
||||
"technicalDetails": "Technische Angaben",
|
||||
"termsOfService": "Nutzungsbedingungen"
|
||||
},
|
||||
"mentions": "Sofern nicht anders angegeben, sind die Inhalte dieser Website verfügbar unter der",
|
||||
"license": "Etalab 2.0 Lizenz"
|
||||
},
|
||||
"loginHint": {
|
||||
"title": "Melden Sie sich mit Ihrem Konto an",
|
||||
"body": "Statt zu warten, melden Sie sich mit Ihrem ProConnect-Konto an.",
|
||||
"title": "Melde dich mit deinem Konto an",
|
||||
"body": "Melde dich mit deinem Konto an, statt zu warten.",
|
||||
"button": {
|
||||
"ariaLabel": "Hinweis schließen",
|
||||
"label": "OK"
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"skipLink": "Zum Hauptinhalt springen",
|
||||
"clipboardContent": {
|
||||
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
|
||||
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
|
||||
"url": "Um an der Videokonferenz teilzunehmen, folge diesem Link: {{roomUrl}}",
|
||||
"numberAndPin": "Um per Telefon teilzunehmen, wähle {{phoneNumber}} und gib diesen Code ein: {{pinCode}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"createMeeting": "Meeting erstellen",
|
||||
"heading": "Einfache und sichere Videokonferenzen",
|
||||
"intro": "Kommunizieren und arbeiten Sie mühelos, ohne Ihre digitale Souveränität zu gefährden",
|
||||
"joinInputError": "Verwenden Sie einen Meeting-Link oder -Code. Beispiele:",
|
||||
"intro": "Kommuniziere und arbeite mühelos, ohne deine digitale Souveränität zu gefährden",
|
||||
"joinInputError": "Verwende einen Meeting-Link oder -Code. Beispiele:",
|
||||
"joinInputExample": "URL oder 10-stelliger Code",
|
||||
"joinInputLabel": "Meeting-Link",
|
||||
"joinInputSubmit": "Meeting beitreten",
|
||||
"joinMeeting": "Meeting beitreten",
|
||||
"joinMeetingTipContent": "Sie können einem Meeting beitreten, indem Sie den vollständigen Link in die Adressleiste Ihres Browsers einfügen.",
|
||||
"joinMeetingTipHeading": "Wussten Sie schon?",
|
||||
"loginToCreateMeeting": "Melden Sie sich an, um ein Meeting zu erstellen",
|
||||
"moreLinkLabel": "Mehr erfahren über {{appTitle}} – neues Tab",
|
||||
"joinMeetingTipContent": "Du kannst einem Meeting beitreten, indem du den vollständigen Link in die Adressleiste deines Browsers einfügst.",
|
||||
"joinMeetingTipHeading": "Wusstest du schon?",
|
||||
"loginToCreateMeeting": "Melde dich an, um ein Meeting zu erstellen",
|
||||
"moreLinkLabel": "Mehr über {{appTitle}} erfahren – neuer Tab",
|
||||
"moreLink": "Mehr erfahren",
|
||||
"moreAbout": "über {{appTitle}}",
|
||||
"createMenu": {
|
||||
"laterOption": "Meeting für später planen",
|
||||
"instantOption": "Sofort-Meeting starten"
|
||||
"instantOption": "Meeting sofort starten"
|
||||
},
|
||||
"laterMeetingDialog": {
|
||||
"heading": "Ihre Zugangsdaten",
|
||||
"description": "Teilen Sie diese Informationen mit den Gästen. Sie können dem Meeting beitreten, ohne sich anmelden zu müssen. Dieses Meeting ist dauerhaft und kann wiederverwendet werden.",
|
||||
"heading": "Zugangsdaten",
|
||||
"description": "Teile diese Informationen mit den Gästen. Sie können dem Meeting beitreten, ohne sich anmelden zu müssen. Dieses Meeting ist dauerhaft und kann wiederverwendet werden.",
|
||||
"permissions": "Personen mit diesem Link benötigen keine Genehmigung, um diesem Meeting beizutreten.",
|
||||
"copy": "Informationen kopieren",
|
||||
"copied": "Informationen kopiert",
|
||||
"copyUrl": "Meeting-Link kopieren",
|
||||
"phone": {
|
||||
"call": "Rufen Sie an:",
|
||||
"call": "Ruf an:",
|
||||
"pinCode": "Code:"
|
||||
}
|
||||
},
|
||||
@@ -45,16 +45,16 @@
|
||||
"tooltip": "Formular ausfüllen"
|
||||
},
|
||||
"slide1": {
|
||||
"title": "Wechseln Sie zur Einfachheit. Testen Sie uns jetzt!",
|
||||
"body": "Entdecken Sie eine intuitive und zugängliche Lösung, entwickelt für alle Mitarbeitenden im öffentlichen Dienst, ihre Partner und viele weitere."
|
||||
"title": "Mach Schluss mit kompliziert – probier uns jetzt aus!",
|
||||
"body": "Entdecke eine intuitive und leicht zugängliche Lösung, entwickelt für alle Mitarbeitenden im öffentlichen Dienst, ihre Partner und viele weitere."
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Gruppenanrufe ohne Einschränkungen",
|
||||
"body": "Unbegrenzte Meeting-Dauer mit flüssiger, hochwertiger Kommunikation – unabhängig von der Gruppengröße."
|
||||
"title": "Gruppenmeetings ohne Einschränkungen",
|
||||
"body": "Unbegrenzte Meeting-Dauer mit flüssiger Kommunikation in hoher Qualität – unabhängig von der Gruppengröße."
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Verwandeln Sie Ihre Meetings mit KI",
|
||||
"body": "Erhalten Sie präzise und verwertbare Transkripte zur Steigerung Ihrer Produktivität. Funktion in der Beta – jetzt testen!"
|
||||
"title": "Verwandele deine Meetings mit KI",
|
||||
"body": "Erhalte präzise und verwertbare Transkripte zur Steigerung deiner Produktivität. Beta-Funktion – jetzt testen!"
|
||||
},
|
||||
"carouselLabel": "Einführungs-Diashow",
|
||||
"slidePosition": "Folie {{current}} von {{total}}"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"title": "Rechtliche Hinweise",
|
||||
"creator": {
|
||||
"title": "Herausgeber",
|
||||
"body": "Der Visio-Dienst wird herausgegeben von der Interministeriellen Digitaldirektion des Staates (DINUM) mit Sitz:",
|
||||
"title": "Herausgebende",
|
||||
"body": "Der Visio-Dienst wird herausgegeben von der Interministeriellen Digitaldirektion Frankreichs (DINUM) mit Sitz:",
|
||||
"contact": {
|
||||
"title": "Kontaktdaten",
|
||||
"address": "20 avenue de Ségur",
|
||||
@@ -31,7 +31,7 @@
|
||||
"title": "Wiederverwendung von Inhalten und Links",
|
||||
"body1": "Sofern nicht ausdrücklich anders angegeben und geistige Eigentumsrechte Dritter betroffen sind, werden die Inhalte dieser Website unter der ",
|
||||
"license": "offenen Lizenz Etalab 2.0",
|
||||
"body2": "bereitgestellt. Insbesondere dürfen Sie diese Inhalte frei vervielfältigen, kopieren, ändern, extrahieren, umwandeln, verbreiten, weiterleiten, veröffentlichen, übertragen und nutzen – unter der Bedingung, dass die Quelle und das Datum der letzten Aktualisierung genannt werden und Dritte nicht durch die Informationen in die Irre geführt werden.",
|
||||
"body3": "Jede öffentliche oder private Website ist berechtigt, ohne vorherige Genehmigung, einen Link (einschließlich eines Deep Links) zu den auf dieser Website veröffentlichten Informationen zu setzen."
|
||||
"body2": "bereitgestellt. Insbesondere dürfen diese Inhalte frei vervielfältigt, kopiert, geändert, extrahiert, umgewandelt, verbreitet, weitergeleitet, veröffentlicht, übertragen und genutzt werden – unter der Bedingung, dass die Quelle und das Datum der letzten Aktualisierung genannt werden und Dritte nicht durch die Informationen in die Irre geführt werden.",
|
||||
"body3": "Jede öffentliche oder private Website ist berechtigt, ohne vorherige Genehmigung, Links (einschließlich Deep Links) zu den auf dieser Website veröffentlichten Informationen zu setzen."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultName": "Ein Mitwirkender",
|
||||
"defaultName": "Eine Person",
|
||||
"joined": {
|
||||
"description": "{{name}} ist dem Raum beigetreten"
|
||||
},
|
||||
@@ -7,47 +7,47 @@
|
||||
"description": "{{name}} hat die Hand gehoben.",
|
||||
"cta": "Warteliste öffnen"
|
||||
},
|
||||
"muted": "{{name}} hat dein Mikrofon stummgeschaltet. Kein Teilnehmer kann dich hören.",
|
||||
"muted": "{{name}} hat dein Mikrofon stummgeschaltet.",
|
||||
"openChat": "Chat öffnen",
|
||||
"chatMessageReceived": "{{name}} sagt: {{message}}",
|
||||
"lowerHand": {
|
||||
"auto": "Es scheint, dass du angefangen hast zu sprechen, daher wird deine Hand gesenkt.",
|
||||
"auto": "Du hast begonnen zu sprechen – deine Hand wird daher automatisch gesenkt.",
|
||||
"dismiss": "Hand oben lassen"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} hat mit {{emoji}} reagiert"
|
||||
},
|
||||
"permissionsRemoved": {
|
||||
"camera": "Der Organisator hat das Video für alle Teilnehmer deaktiviert.",
|
||||
"microphone": "Der Organisator hat das Mikrofon für alle Teilnehmer deaktiviert.",
|
||||
"screen_share": "Der Organisator hat die Bildschirmfreigabe für alle Teilnehmer deaktiviert."
|
||||
"camera": "Die Moderation hat das Video für alle Teilnehmenden deaktiviert.",
|
||||
"microphone": "Die Moderation hat das Mikrofon für alle Teilnehmenden deaktiviert.",
|
||||
"screen_share": "Die Moderation hat die Bildschirmfreigabe für alle Teilnehmenden deaktiviert."
|
||||
},
|
||||
"waitingParticipants": {
|
||||
"one": "Eine Person möchte diesem Anruf beitreten.",
|
||||
"several": "Mehrere Personen möchten diesem Anruf beitreten.",
|
||||
"one": "Eine Person möchte diesem Meeting beitreten.",
|
||||
"several": "Mehrere Personen möchten diesem Meeting beitreten.",
|
||||
"open": "Öffnen",
|
||||
"accept": "Annehmen"
|
||||
"accept": "Hereinlassen"
|
||||
},
|
||||
"transcript": {
|
||||
"started": "{{name}} hat die Transkription des Treffens gestartet.",
|
||||
"stopped": "{{name}} hat die Transkription des Treffens gestoppt.",
|
||||
"started": "{{name}} hat die Meeting-Transkription gestartet.",
|
||||
"stopped": "{{name}} hat die Meeting-Transkription gestoppt.",
|
||||
"limitReached": "Die Transkription hat die maximal zulässige Dauer überschritten und wird automatisch gespeichert.",
|
||||
"requested": "{{name}} will die Transkription starten."
|
||||
"requested": "{{name}} möchte die Meeting-Transkription starten."
|
||||
},
|
||||
"screenRecording": {
|
||||
"started": "{{name}} hat die Aufzeichnung des Treffens gestartet.",
|
||||
"stopped": "{{name}} hat die Aufzeichnung des Treffens gestoppt.",
|
||||
"started": "{{name}} hat die Meeting-Aufzeichnung gestartet.",
|
||||
"stopped": "{{name}} hat die Meeting-Aufzeichnung gestoppt.",
|
||||
"limitReached": "Die Aufzeichnung hat die maximal zulässige Dauer überschritten und wird automatisch gespeichert.",
|
||||
"requested": "{{name}} will die Aufnahme starten."
|
||||
"requested": "{{name}} möchte die Meeting-Aufzeichnung starten."
|
||||
},
|
||||
"recordingSave": {
|
||||
"transcript": {
|
||||
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald die Transkription fertig ist.",
|
||||
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald die Transkription fertig ist."
|
||||
"message": "Deine Transkription wird gespeichert! Du erhältst eine E-Mail auf <strong>{{email}}</strong>, sobald die Transkription bereit ist.",
|
||||
"default": "Deine Transkription wird gespeichert! Du erhältst das Transkript per E-Mail, sobald es bereit ist."
|
||||
},
|
||||
"screenRecording": {
|
||||
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald sie fertig ist.",
|
||||
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald sie fertig ist."
|
||||
"message": "Deine Aufzeichnung wird gespeichert! Du erhältst eine E-Mail auf <strong>{{email}}</strong>, sobald sie bereit ist.",
|
||||
"default": "Deine Aufzeichnung wird gespeichert! Du erhältst eine E-Mail, sobald sie bereit ist."
|
||||
}
|
||||
},
|
||||
"openMenu": "Menü öffnen"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"error": {
|
||||
"title": "Aufzeichnung nicht verfügbar",
|
||||
"body": "Die Aufzeichnung ist nicht verfügbar oder wurde möglicherweise gelöscht. Nur der Organisator der Besprechung hat Zugriff darauf. Wenden Sie sich bei Bedarf gerne an ihn."
|
||||
"body": "Die Aufzeichnung ist nicht verfügbar oder wurde möglicherweise gelöscht. Nur die*der Organisator:in der Besprechung hat Zugriff darauf. Wende dich bei Bedarf gerne an die Person."
|
||||
},
|
||||
"expired": {
|
||||
"title": "Aufzeichnung abgelaufen",
|
||||
@@ -9,20 +9,20 @@
|
||||
},
|
||||
"authentication": {
|
||||
"title": "Authentifizierung erforderlich",
|
||||
"body": "Bitte melden Sie sich an, um auf diese Aufzeichnung zuzugreifen."
|
||||
"body": "Bitte melde dich sich an, um auf diese Aufzeichnung zuzugreifen."
|
||||
},
|
||||
"unsaved": {
|
||||
"title": "Download nicht verfügbar",
|
||||
"body": "Diese Aufzeichnung ist noch nicht zum Herunterladen bereit. Bitte versuchen Sie es später erneut."
|
||||
"body": "Diese Aufzeichnung ist noch nicht zum Herunterladen bereit. Bitte versuche es später erneut."
|
||||
},
|
||||
"success": {
|
||||
"title": "Ihre Aufzeichnung ist bereit!",
|
||||
"body": "Aufzeichnung des Treffens {{room}} vom {{created_at}}.",
|
||||
"title": "Deine Aufzeichnung ist bereit!",
|
||||
"body": "Aufzeichnung des Meetings {{room}} vom {{created_at}}.",
|
||||
"expiration": "Achtung, diese Aufzeichnung wird nach {{expiration_days}} Tag(en) gelöscht.",
|
||||
"button": "Herunterladen",
|
||||
"warning": {
|
||||
"title": "Linkfreigabe deaktiviert",
|
||||
"body": "Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Organisatoren können sie herunterladen."
|
||||
"body": "Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Organisator:innen können sie herunterladen."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"feedback": {
|
||||
"heading": {
|
||||
"normal": "Sie haben das Meeting verlassen",
|
||||
"duplicateIdentity": "Sie haben dem Meeting von einem anderen Gerät aus beigetreten",
|
||||
"participantRemoved": "Sie wurden von einem Administrator aus dem Anruf entfernt"
|
||||
"normal": "Du hast das Meeting verlassen",
|
||||
"duplicateIdentity": "Du bist dem Meeting von einem anderen Gerät aus beigetreten",
|
||||
"participantRemoved": "Du wurdest von der*dem Organisator:in aus dem Meeting entfernt"
|
||||
},
|
||||
"home": "Zur Startseite zurückkehren",
|
||||
"back": "Dem Meeting erneut beitreten"
|
||||
},
|
||||
"selectDevice": {
|
||||
"loading": "Laden…",
|
||||
"select": "Wählen Sie einen Wert",
|
||||
"permissionsNeeded": "Genehmigung erforderlich",
|
||||
"select": "Wähle einen Wert",
|
||||
"permissionsNeeded": "Berechtigung erforderlich",
|
||||
"settings": {
|
||||
"audio": "Audioeinstellungen",
|
||||
"video": "Videoeinstellungen"
|
||||
@@ -19,7 +19,7 @@
|
||||
"effects": "Effekte anwenden",
|
||||
"videoinput": {
|
||||
"choose": "Kamera auswählen",
|
||||
"permissionsNeeded": "Kamera auswählen - genehmigung erforderlich",
|
||||
"permissionsNeeded": "Kamera auswählen – Berechtigung erforderlich",
|
||||
"disable": "Kamera deaktivieren",
|
||||
"enable": "Kamera aktivieren",
|
||||
"turnedOff": "Kamera deaktiviert",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"audioinput": {
|
||||
"choose": "Mikrofon auswählen",
|
||||
"permissionsNeeded": "Mikrofon auswählen - genehmigung erforderlich",
|
||||
"permissionsNeeded": "Mikrofon auswählen – Berechtigung erforderlich",
|
||||
"disable": "Mikrofon deaktivieren",
|
||||
"enable": "Mikrofon aktivieren",
|
||||
"turnedOff": "Mikrofon deaktiviert",
|
||||
@@ -37,30 +37,30 @@
|
||||
"label": "Mikrofon"
|
||||
},
|
||||
"audiooutput": {
|
||||
"choose": "Lautsprecher auswählen",
|
||||
"permissionsNeeded": "Lautsprecher auswählen - genehmigung erforderlich"
|
||||
"choose": "Audioausgabe auswählen",
|
||||
"permissionsNeeded": "Audioausgabe auswählen – Berechtigung erforderlich"
|
||||
}
|
||||
},
|
||||
"join": {
|
||||
"effects": {
|
||||
"description": "Effekte anwenden",
|
||||
"title": "Effekte",
|
||||
"subTitle": "Konfigurieren Sie die Effekte Ihrer Kamera."
|
||||
"subTitle": "Konfiguriere die Effekte deiner Kamera."
|
||||
},
|
||||
"heading": "Dem Meeting beitreten?",
|
||||
"joinLabel": "Beitreten",
|
||||
"joinMeeting": "Meeting beitreten",
|
||||
"toggleOff": "Klicken, um auszuschalten",
|
||||
"toggleOn": "Klicken, um einzuschalten",
|
||||
"usernameHint": "Wird anderen Teilnehmern angezeigt",
|
||||
"usernameLabel": "Ihr Name",
|
||||
"usernameHint": "Wird anderen Teilnehmenden angezeigt",
|
||||
"usernameLabel": "Dein Name",
|
||||
"errors": {
|
||||
"usernameEmpty": "Ihr Name darf nicht leer sein"
|
||||
"usernameEmpty": "Dein Name darf nicht leer sein"
|
||||
},
|
||||
"cameraDisabled": "Kamera ist deaktiviert.",
|
||||
"cameraStarting": "Kamera wird gestartet…",
|
||||
"cameraNotGranted": "Möchten Sie, dass andere Sie während der Besprechung sehen können?",
|
||||
"cameraAndMicNotGranted": "Möchten Sie, dass andere Sie während der Besprechung sehen und hören können?",
|
||||
"cameraNotGranted": "Möchtest du, dass andere dich während des Meetings sehen können?",
|
||||
"cameraAndMicNotGranted": "Möchtest du, dass andere dich während des Meetings sehen und hören können?",
|
||||
"permissionsButton": {
|
||||
"cameraAndMicNotGranted": "Zugriff auf Kamera und Mikrofon erlauben",
|
||||
"cameraNotGranted": "Zugriff auf Kamera erlauben"
|
||||
@@ -71,28 +71,28 @@
|
||||
},
|
||||
"waiting": {
|
||||
"title": "Beitrittsanfrage wird gesendet...",
|
||||
"body": "Sie können diesem Anruf beitreten, sobald jemand Sie autorisiert"
|
||||
"body": "Du kannst diesem Meeting beitreten, sobald deine Anfrage angenommen wird"
|
||||
},
|
||||
"denied": {
|
||||
"title": "Sie können diesem Anruf nicht beitreten",
|
||||
"body": "Ihre Beitrittsanfrage wurde abgelehnt."
|
||||
"title": "Du kannst diesem Meeting nicht beitreten",
|
||||
"body": "Deine Beitrittsanfrage wurde abgelehnt."
|
||||
},
|
||||
"timeoutInvite": {
|
||||
"title": "Sie können diesem Anruf nicht beitreten",
|
||||
"body": "Niemand hat auf Ihre Anfrage reagiert"
|
||||
"title": "Du kannst diesem Meeting nicht beitreten",
|
||||
"body": "Niemand hat auf deine Anfrage reagiert"
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Dadurch verlassen Sie das Meeting.",
|
||||
"leaveRoomPrompt": "Hiermit verlässt du das Meeting.",
|
||||
"shareDialog": {
|
||||
"copy": "Informationen kopieren",
|
||||
"copy": "Infos kopieren",
|
||||
"copyUrl": "Link kopieren",
|
||||
"copied": "Informationen kopiert",
|
||||
"heading": "Ihr Meeting ist bereit",
|
||||
"description": "Teilen Sie diesen Link mit Personen, die Sie zum Meeting einladen möchten.",
|
||||
"copied": "Infos kopiert",
|
||||
"heading": "Dein Meeting ist bereit",
|
||||
"description": "Teile diesen Link mit Personen, die du zum Meeting einladen möchten.",
|
||||
"permissions": "Personen mit diesem Link benötigen keine Erlaubnis, um diesem Meeting beizutreten.",
|
||||
"closeDialog": "Dialogfenster schließen",
|
||||
"phone": {
|
||||
"call": "Rufen Sie an:",
|
||||
"call": "Ruf an:",
|
||||
"pinCode": "Code:"
|
||||
}
|
||||
},
|
||||
@@ -108,62 +108,62 @@
|
||||
"videoinput": "Kamera konnte nicht aktiviert werden"
|
||||
},
|
||||
"body": {
|
||||
"audioinput": "Ihr Mikrofon wird bereits in einem anderen Tab oder von einer anderen Anwendung verwendet, was die Aktivierung in diesem Videoanruf verhindert. Wenn Sie möchten, schließen Sie den anderen Tab oder die Anwendung, um es hier zu verwenden.",
|
||||
"videoinput": "Ihre Kamera wird bereits in einem anderen Tab oder von einer anderen Anwendung verwendet, was die Aktivierung in diesem Videoanruf verhindert. Wenn Sie möchten, schließen Sie den anderen Tab oder die Anwendung, um sie hier zu verwenden."
|
||||
"audioinput": "Dein Mikrofon wird bereits in einem anderen Tab oder von einer anderen App verwendet. Dies verhindert die Aktivierung in diesem Meeting. Wenn du dein Mikrofon hier verwenden möchtest, schließe den anderen Tab oder die App.",
|
||||
"videoinput": "Deine Kamera wird bereits in einem anderen Tab oder von einer anderen App verwendet. Dies verhindert die Aktivierung in diesem Meeting. Wenn du deine Kamera hier verwenden möchtest, schließe den anderen Tab oder die App."
|
||||
}
|
||||
},
|
||||
"close": "OK"
|
||||
},
|
||||
"permissionErrorDialog": {
|
||||
"heading": {
|
||||
"camera": "{{appTitle}} darf Ihre Kamera nicht verwenden",
|
||||
"microphone": "{{appTitle}} darf Ihr Mikrofon nicht verwenden",
|
||||
"cameraAndMicrophone": "{{appTitle}} darf weder Ihr Mikrofon noch Ihre Kamera verwenden",
|
||||
"camera": "{{appTitle}} hat keine Berechtigung, um deine Kamera zu verwenden",
|
||||
"microphone": "{{appTitle}} hat keine Berechtigung, um dein Mikrofon zu verwenden",
|
||||
"cameraAndMicrophone": "{{appTitle}} hat keine Berechtigung, um dein Mikrofon und deine Kamera zu verwenden",
|
||||
"default": "{{appTitle}} hat keine Berechtigung für bestimmte Zugriffe"
|
||||
},
|
||||
"body": {
|
||||
"openMenu": {
|
||||
"others": "Klicken Sie auf das Einstellungen-Symbol ICON_PLACEHOLDER in der Adressleiste Ihres Browsers",
|
||||
"safari": "Klicken Sie auf das Menü 'Safari' und öffnen Sie 'Einstellungen für {{appDomain}}'."
|
||||
"others": "Klicke das Einstellungs-Symbol ICON_PLACEHOLDER in der Adressleiste deines Browsers",
|
||||
"safari": "Klicke auf das Menü 'Safari' und öffne 'Einstellungen für {{appDomain}}'."
|
||||
},
|
||||
"details": {
|
||||
"camera": "Zugriff auf die Kamera erlauben",
|
||||
"microphone": "Zugriff auf das Mikrofon erlauben",
|
||||
"cameraAndMicrophone": "Zugriff auf Kamera und Mikrofon erlauben",
|
||||
"default": "Aktivieren Sie die erforderlichen Berechtigungen."
|
||||
"default": "Erlaube die erforderlichen Berechtigungen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissionsButton": {
|
||||
"tooltip": "Mehr Infos",
|
||||
"ariaLabel": "Berechtigungsproblem. Mehr Infos anzeigen"
|
||||
"ariaLabel": "Problem mit Berechtigungen. Mehr Infos anzeigen"
|
||||
},
|
||||
"error": {
|
||||
"createRoom": {
|
||||
"heading": "Authentifizierung erforderlich",
|
||||
"body": "Dieser Raum wurde noch nicht erstellt. Bitte authentifizieren Sie sich, um ihn zu erstellen, oder warten Sie, bis ein authentifizierter Benutzer dies tut."
|
||||
"body": "Dieser Raum wurde noch nicht erstellt. Bitte authentifiziere dich, um ihn zu erstellen, oder warte, bis eine authentifizierte Person dies tut."
|
||||
},
|
||||
"screenShare": {
|
||||
"title": "Bildschirmfreigabe nicht möglich",
|
||||
"ariaLabel": "Bildschirmfreigabe nicht möglich",
|
||||
"message": "Ihr Browser darf möglicherweise den Bildschirm Ihres Computers nicht aufzeichnen.",
|
||||
"macInstructions": "Gehen Sie zu Ihren",
|
||||
"message": "Deinem Browser fehlt möglicherweise die Berechtigung, den Bildschirm deines Geräts abzugreifen.",
|
||||
"macInstructions": "Gehe zu den",
|
||||
"macSystemPreferences": "Systemeinstellungen",
|
||||
"helpLinkText": "Weitere Informationen finden Sie unter",
|
||||
"helpLinkLabel": "Präsentationsproblem",
|
||||
"helpLinkText": "Weitere Informationen findest du unter",
|
||||
"helpLinkLabel": "Bildschirmfreigabeproblem",
|
||||
"closeButton": "Schließen",
|
||||
"newTab": "Neues Fenster"
|
||||
"newTab": "Neuer Tab"
|
||||
}
|
||||
},
|
||||
"isIdleDisconnectModal": {
|
||||
"title": "Bist du noch da?",
|
||||
"body": "Du bist der einzige Teilnehmer. Dieses Gespräch endet in {{duration}}. Möchtest du das Gespräch fortsetzen?",
|
||||
"body": "Du bist die*der einzige Teilnehmende. Diese Meeting endet in {{duration}}. Möchtest du das Meeting fortsetzen?",
|
||||
"settingsPrefix": "Um diese Nachricht nicht mehr zu sehen, gehe zu",
|
||||
"settingsLink": "Einstellungen > Allgemein",
|
||||
"settingsSuffix": ".",
|
||||
"stayButton": "Gespräch fortsetzen",
|
||||
"stayButton": "Meeting fortsetzen",
|
||||
"leaveButton": "Jetzt verlassen",
|
||||
"countdownAnnouncement": "Das Gespräch endet in {{duration}}."
|
||||
"countdownAnnouncement": "Das Meeting endet in {{duration}}."
|
||||
},
|
||||
"controls": {
|
||||
"microphone": "Mikrofon",
|
||||
@@ -201,8 +201,8 @@
|
||||
"participants": {
|
||||
"open": "Alle ausblenden",
|
||||
"closed": "Alle anzeigen",
|
||||
"count_one": "{{count}} Teilnehmer",
|
||||
"count_other": "{{count}} Teilnehmer"
|
||||
"count_one": "{{count}} Teilnehmende",
|
||||
"count_other": "{{count}} Teilnehmende"
|
||||
},
|
||||
"tools": {
|
||||
"open": "Weitere Tools ausblenden",
|
||||
@@ -217,16 +217,16 @@
|
||||
"button": "Reaktion senden",
|
||||
"send": "Reaktion {{emoji}} senden",
|
||||
"announce": "{{name}} : {{emoji}}",
|
||||
"you": "Sie",
|
||||
"you": "du",
|
||||
"emojis": {
|
||||
"thumbs-up": "Daumen hoch",
|
||||
"thumbs-down": "Daumen runter",
|
||||
"clapping-hands": "Klatschende Hände",
|
||||
"red-heart": "rotes Herz",
|
||||
"red-heart": "Rotes Herz",
|
||||
"face-with-tears-of-joy": "Gesicht mit Freudentränen",
|
||||
"face-with-open-mouth": "überraschter Gesichtsausdruck",
|
||||
"party-popper": "Party-Popper",
|
||||
"folded-hands": "gefaltete Hände"
|
||||
"face-with-open-mouth": "Überraschter Gesichtsausdruck",
|
||||
"party-popper": "Konfettikanone",
|
||||
"folded-hands": "Gefaltete Hände"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -237,7 +237,7 @@
|
||||
"settings": "Einstellungen",
|
||||
"screenRecording": "Aufzeichnung",
|
||||
"transcript": "Transkription",
|
||||
"username": "Ihren Namen aktualisieren",
|
||||
"username": "Deinen Namen aktualisieren",
|
||||
"effects": "Effekte anwenden",
|
||||
"switchCamera": "Kamera wechseln",
|
||||
"fullscreen": {
|
||||
@@ -248,9 +248,9 @@
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
"activateCamera": "Ihre Kamera ist deaktiviert. Wählen Sie eine Option, um sie zu aktivieren.",
|
||||
"cameraDisabled": "Ihre Kamera ist deaktiviert.",
|
||||
"notAvailable": "Videoeffekte werden bald in Ihrem Browser verfügbar sein. Wir arbeiten daran! In der Zwischenzeit können Sie Google Chrome für die beste Leistung oder Firefox verwenden :(",
|
||||
"activateCamera": "Deine Kamera ist deaktiviert. Wähle eine Option, um sie zu aktivieren.",
|
||||
"cameraDisabled": "Deine Kamera ist deaktiviert.",
|
||||
"notAvailable": "Videoeffekte werden bald in deinem Browser verfügbar sein. Wir arbeiten daran! In der Zwischenzeit kannst du Google Chrome für die beste Leistung oder Firefox verwenden :(",
|
||||
"heading": "Unschärfe",
|
||||
"clear": "Effekt deaktivieren",
|
||||
"blur": {
|
||||
@@ -273,17 +273,39 @@
|
||||
"title": "Virtueller Hintergrund",
|
||||
"selectedLabel": "Hintergrund angewendet:",
|
||||
"apply": "Ersetze deinen Hintergrund:",
|
||||
"descriptions": {
|
||||
"0": "Gerilltes Holzmöbel",
|
||||
"1": "Besprechungsraum",
|
||||
"2": "Loft mit schwarzer Glaswand",
|
||||
"3": "Esszimmer",
|
||||
"4": "Holzregale",
|
||||
"5": "Holztreppe",
|
||||
"6": "Graue Bibliothek",
|
||||
"7": "Kaffeetheke"
|
||||
"personal": {
|
||||
"title": "Meine Hintergründe",
|
||||
"selectFileTooltip": "Wähle ein Bild aus, das als persönlicher Hintergrund verwendet werden soll",
|
||||
"notLoggedInWarning": "Du bist nicht angemeldet, der persönliche Hintergrund wird nicht von einem Meeting zum anderen gespeichert.",
|
||||
"warningUploadDisabled": "Persönliche Hintergründe werden derzeit nicht von einem Meeting zum anderen gespeichert.",
|
||||
"uploadLimitReached": "Du kannst keine weiteren persönlichen Hintergründe hinzufügen.",
|
||||
"errors": {
|
||||
"close": "Schließen",
|
||||
"file_too_large": {
|
||||
"title": "Datei zu groß",
|
||||
"description": "Die Datei ist zu groß. Bitte wähle eine Datei mit weniger als {{maxSize, number}} MB."
|
||||
},
|
||||
"invalid_file_type": {
|
||||
"title": "Ungültiger Dateityp",
|
||||
"description": "Der Dateityp wird nicht unterstützt. Bitte wähle eine {{allowedExtension}}-Datei."
|
||||
}
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Vorschläge",
|
||||
"descriptions": {
|
||||
"0": "Gerilltes Holzmöbel",
|
||||
"1": "Besprechungsraum",
|
||||
"2": "Loft mit schwarzer Glaswand",
|
||||
"3": "Esszimmer",
|
||||
"4": "Holzregale",
|
||||
"5": "Holztreppe",
|
||||
"6": "Graue Bibliothek",
|
||||
"7": "Kaffeetheke"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"faceLandmarks": {
|
||||
"title": "Visuelle Effekte",
|
||||
"glasses": {
|
||||
@@ -297,11 +319,11 @@
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"ariaLabel": "Seitenleiste",
|
||||
"ariaLabel": "Seitenleiste - {{title}}",
|
||||
"backToTools": "Zurück zu den Meeting-Tools",
|
||||
"heading": {
|
||||
"participants": "Teilnehmer",
|
||||
"effects": "Effekte",
|
||||
"participants": "Teilnehmende",
|
||||
"effects": "Hintergrund & Effekte",
|
||||
"chat": "Nachrichten im Chat",
|
||||
"transcript": "Transkribieren",
|
||||
"screenRecording": "Aufzeichnen",
|
||||
@@ -310,8 +332,8 @@
|
||||
"info": "Meeting-Informationen"
|
||||
},
|
||||
"content": {
|
||||
"participants": "Teilnehmer",
|
||||
"effects": "Effekte",
|
||||
"participants": "Teilnehmende",
|
||||
"effects": "Hintergrund & Effekte",
|
||||
"chat": "Nachrichten",
|
||||
"transcript": "transkribieren",
|
||||
"screenRecording": "aufzeichnen",
|
||||
@@ -322,12 +344,12 @@
|
||||
"closeButton": "{{content}} ausblenden"
|
||||
},
|
||||
"chat": {
|
||||
"disclaimer": "Die Nachrichten sind nur für Teilnehmer zum Zeitpunkt des Sendens sichtbar. Alle Nachrichten werden am Ende des Anrufs gelöscht.",
|
||||
"disclaimer": "Die Nachrichten sind nur für Teilnehmende zum Zeitpunkt des Sendens sichtbar. Alle Nachrichten werden am Ende des Meetings gelöscht.",
|
||||
"messagesLog": "Chat-Nachrichten"
|
||||
},
|
||||
"moreTools": {
|
||||
"body": "Greifen Sie auf weitere Tools zu, um Ihre Meetings zu verbessern.",
|
||||
"linkAriaLabel": "Dokumentation zu den Tools öffnen - öffnet in neuem Fenster",
|
||||
"body": "Nutze weitere Tools, um deine Meetings zu verbessern.",
|
||||
"linkAriaLabel": "Dokumentation zu den Tools öffnen – öffnet in neuem Tab",
|
||||
"moreLink": "Dokumentation öffnen",
|
||||
"tools": {
|
||||
"transcript": {
|
||||
@@ -344,77 +366,77 @@
|
||||
"roomInformation": {
|
||||
"title": "Verbindungsinformationen",
|
||||
"button": {
|
||||
"ariaLabel": "Kopieren Sie die Informationen aus Ihrer Besprechung",
|
||||
"ariaLabel": "Kopiere die Informationen aus dem Meeting",
|
||||
"copy": "Informationen kopieren",
|
||||
"copied": "Informationen kopiert"
|
||||
},
|
||||
"phone": {
|
||||
"call": "Rufen Sie an:",
|
||||
"call": "Ruf an:",
|
||||
"pinCode": "Code:"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transcript": {
|
||||
"heading": "Transkribieren Sie Ihr Meeting mit dem KI-Assistenten",
|
||||
"body": "Transkribieren Sie bis zu {{max_duration}} Meetingdauer.",
|
||||
"bodyWithoutMaxDuration": "Transkribieren Sie Ihr Meeting ohne Begrenzung.",
|
||||
"heading": "Transkribiere dein Meeting mithilfe von KI",
|
||||
"body": "Transkribiere bis zu {{max_duration}} Meetingdauer.",
|
||||
"bodyWithoutMaxDuration": "Transkribieren dein Meeting ohne Zeitbegrenzung.",
|
||||
"details": {
|
||||
"receiver": "Das Transkript wird an den Organisator und die Mitorganisatoren gesendet.",
|
||||
"receiver": "Das Transkript wird an die*den Organisator:in und die Mitorganisierenden gesendet.",
|
||||
"destination": "Ein neues Dokument wird erstellt auf",
|
||||
"destinationUnknown": "Ein neues Dokument wird erstellt",
|
||||
"language": "Meeting-Sprache:",
|
||||
"recording": "Auch eine Aufzeichnung starten"
|
||||
},
|
||||
"button": {
|
||||
"start": "Meeting transkribieren starten",
|
||||
"stop": "Meeting transkribieren beenden",
|
||||
"start": "Meeting-Transkription starten",
|
||||
"stop": "Meeting-Transkription beenden",
|
||||
"saving": "Speichern…",
|
||||
"starting": "Wird gestartet…",
|
||||
"anotherModeStarted": "Eine Aufnahme läuft. <br/> Wechseln Sie das Menü und stoppen Sie sie."
|
||||
"anotherModeStarted": "Eine Aufnahme läuft. <br/> Wechsele das Menü und stoppe sie."
|
||||
},
|
||||
"linkMore": "Dokumentation öffnen",
|
||||
"linkAriaLabel": "Dokumentation zur Transkription öffnen - öffnet in neuem Fenster",
|
||||
"linkAriaLabel": "Dokumentation zur Transkription öffnen – öffnet in neuem Tab",
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Zugriff eingeschränkt",
|
||||
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine Transkription (Beta) starten.",
|
||||
"body": "Aus Sicherheitsgründen kann nur die*der Ersteller:in bzw. Admin des Meetings eine Transkription (Beta) starten.",
|
||||
"linkMore": "Dokumentation öffnen",
|
||||
"linkAriaLabel": "Dokumentation zum Transkriptionszugang öffnen - öffnet in neuem Fenster",
|
||||
"linkAriaLabel": "Dokumentation zum Transkriptionszugang öffnen – öffnet in neuem Tab",
|
||||
"dividerLabel": "ODER",
|
||||
"login": {
|
||||
"heading": "Anmeldung erforderlich",
|
||||
"body": "Nur der Ersteller des Meetings oder ein Administrator kann die Transkription starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."
|
||||
"body": "Nur die*der Ersteller:in bzw. Admin kann die Transkription starten. Melde dich an, um deine Berechtigungen zu überprüfen."
|
||||
},
|
||||
"request": {
|
||||
"heading": "Anfrage an Moderator",
|
||||
"body": "Der Moderator wird benachrichtigt und kann die Transkription starten.",
|
||||
"buttonLabel": "Anfordern"
|
||||
"heading": "Transkription anfragen",
|
||||
"body": "Die Moderation wird benachrichtigt und kann die Transkription starten.",
|
||||
"buttonLabel": "Anfragen"
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Premium-Funktion",
|
||||
"body": "Diese Funktion ist öffentlichen Bediensteten vorbehalten. Wenn Ihre E-Mail-Adresse nicht autorisiert ist, kontaktieren Sie bitte den Support, um Zugriff zu erhalten.",
|
||||
"body": "Diese Funktion ist öffentlichen Bediensteten vorbehalten. Wenn deine E-Mail-Adresse nicht autorisiert ist, kontaktiere bitte den Support, um Zugriff zu erhalten.",
|
||||
"linkMore": "Dokumentation öffnen",
|
||||
"linkAriaLabel": "Dokumentation zum Premium-Zugang öffnen - öffnet in neuem Fenster",
|
||||
"linkAriaLabel": "Dokumentation zum Premium-Zugang öffnen – öffnet in neuem Tab",
|
||||
"dividerLabel": "ODER",
|
||||
"login": {
|
||||
"heading": "Anmeldung erforderlich",
|
||||
"body": "Nur der Ersteller des Meetings oder ein Administrator kann die Transkription starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."
|
||||
"body": "Nur die*der Ersteller:in bzw. Admin kann die Transkription starten. Melde dich an, um deine Berechtigungen zu überprüfen."
|
||||
},
|
||||
"request": {
|
||||
"heading": "Anfrage an Moderator",
|
||||
"body": "Der Moderator wird benachrichtigt und kann die Transkription starten.",
|
||||
"heading": "Transkription anfragen",
|
||||
"body": "Die Moderation wird benachrichtigt und kann die Transkription starten.",
|
||||
"buttonLabel": "Anfordern"
|
||||
}
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
"heading": "Diesen Anruf für später aufzeichnen",
|
||||
"body": "Zeichnen Sie bis zu {{max_duration}} des Meetings auf.",
|
||||
"bodyWithoutMaxDuration": "Zeichnen Sie Ihr Meeting unbegrenzt auf.",
|
||||
"heading": "Dieses Meeting für später aufzeichnen",
|
||||
"body": "Zeichne bis zu {{max_duration}} des Meetings auf.",
|
||||
"bodyWithoutMaxDuration": "Zeichne dein Meeting ohne Zeitbegrenzung auf.",
|
||||
"linkMore": "Dokumentation öffnen",
|
||||
"linkAriaLabel": "Dokumentation zur Aufzeichnung öffnen - öffnet in neuem Fenster",
|
||||
"linkAriaLabel": "Dokumentation zur Aufzeichnung öffnen – öffnet in neuem Tab",
|
||||
"details": {
|
||||
"receiver": "Die Aufzeichnung wird an den Organisator und die Mitorganisatoren gesendet.",
|
||||
"receiver": "Die Aufzeichnung wird an die*den Organisator:in und die Mitorganisierenden gesendet.",
|
||||
"destination": "Diese Aufzeichnung wird vorübergehend auf unseren Servern gespeichert",
|
||||
"loading": "Aufzeichnung wird gestartet",
|
||||
"linkMore": "Mehr erfahren",
|
||||
@@ -425,21 +447,21 @@
|
||||
"stop": "Meeting-Aufzeichnung beenden",
|
||||
"saving": "Wird gespeichert…",
|
||||
"starting": "Wird gestartet…",
|
||||
"anotherModeStarted": "Eine Transkription läuft. <br/> Wechseln Sie das Menü und stoppen Sie sie."
|
||||
"anotherModeStarted": "Eine Transkription läuft. <br/> Wechsele das Menü und stoppen sie."
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Zugriff eingeschränkt",
|
||||
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine Videoaufnahme (Beta) starten.",
|
||||
"body": "Aus Sicherheitsgründen kann nur die*der Ersteller:in bzw. Admin des Meetings eine Videoaufnahme (Beta) starten.",
|
||||
"linkMore": "Dokumentation öffnen",
|
||||
"linkAriaLabel": "Dokumentation zum Aufzeichnungszugang öffnen - öffnet in neuem Fenster",
|
||||
"linkAriaLabel": "Dokumentation zum Aufzeichnungszugang öffnen – öffnet in neuem Tab",
|
||||
"dividerLabel": "ODER",
|
||||
"login": {
|
||||
"heading": "Anmeldung erforderlich",
|
||||
"body": "Nur der Ersteller der Besprechung oder ein Administrator kann die Aufzeichnung starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."
|
||||
"body": "Nur die*der Ersteller:in bzw. Admin kann die Aufzeichnung starten. Melde dich an, um deine Berechtigungen zu überprüfen."
|
||||
},
|
||||
"request": {
|
||||
"heading": "Aufnahme anfordern",
|
||||
"body": "Der Moderator wird benachrichtigt und kann die Aufnahme für Sie starten.",
|
||||
"heading": "Aufnahme anfragen",
|
||||
"body": "Die Moderation wird benachrichtigt und kann die Aufnahme starten.",
|
||||
"buttonLabel": "Anfrage senden"
|
||||
}
|
||||
},
|
||||
@@ -454,10 +476,10 @@
|
||||
"button": "OK"
|
||||
},
|
||||
"admin": {
|
||||
"description": "Diese Einstellungen für Organisatoren ermöglichen dir die Kontrolle über dein Meeting. Nur Organisatoren haben Zugriff auf diese Optionen.",
|
||||
"description": "Diese Einstellungen für Organisierende ermöglichen dir die Kontrolle über dein Meeting. Nur Organisierende haben Zugriff auf diese Optionen.",
|
||||
"access": {
|
||||
"title": "Raumzugang",
|
||||
"description": "Diese Einstellungen gelten auch für zukünftige Ausgaben dieses Meetings.",
|
||||
"description": "Diese Einstellungen gelten auch für zukünftige Meetings in diesem Raum.",
|
||||
"type": "Zugangsarten für Meetings",
|
||||
"levels": {
|
||||
"public": {
|
||||
@@ -475,28 +497,28 @@
|
||||
}
|
||||
},
|
||||
"moderation": {
|
||||
"title": "Moderation des Meetings",
|
||||
"description": "Diese Einstellungen beschränken die Aktionen, die Mitwirkende während des Meetings durchführen können.",
|
||||
"title": "Meeting-Moderation",
|
||||
"description": "Diese Einstellungen beschränken die Aktionen, die Teilnehmende während des Meetings durchführen können.",
|
||||
"microphone": {
|
||||
"label": "Mikrofon einschalten",
|
||||
"description": ""
|
||||
},
|
||||
"camera": {
|
||||
"label": "Video aktivieren",
|
||||
"label": "Video einschalten",
|
||||
"description": ""
|
||||
},
|
||||
"screenshare": {
|
||||
"label": "Ihren Bildschirm teilen",
|
||||
"description": "Wenn Sie diese Option deaktivieren, können Teilnehmer ihren Bildschirm nicht mehr teilen, und laufende Bildschirmfreigaben werden sofort beendet."
|
||||
"label": "Bildschirm teilen",
|
||||
"description": "Wenn du diese Option deaktivierst, können Teilnehmende ihren Bildschirm nicht mehr teilen. Laufende Bildschirmfreigaben werden sofort beendet."
|
||||
}
|
||||
}
|
||||
},
|
||||
"rating": {
|
||||
"submit": "Absenden",
|
||||
"question": "Wie bewertest du die Qualität deines Gesprächs?",
|
||||
"question": "Wie bewertest du die Qualität dieses Meetings?",
|
||||
"levels": {
|
||||
"min": "sehr schlecht",
|
||||
"max": "ausgezeichnet"
|
||||
"max": "hervorragend"
|
||||
}
|
||||
},
|
||||
"openFeedback": {
|
||||
@@ -506,7 +528,7 @@
|
||||
"skip": "Überspringen"
|
||||
},
|
||||
"confirmationMessage": {
|
||||
"heading": "Vielen Dank für deine Rückmeldung",
|
||||
"heading": "Vielen Dank für dein Feedback",
|
||||
"body": "Unser Produktteam nimmt sich Zeit, dein Feedback sorgfältig zu prüfen. Wir melden uns so bald wie möglich bei dir."
|
||||
},
|
||||
"authenticationMessage": {
|
||||
@@ -518,9 +540,9 @@
|
||||
"participants": {
|
||||
"subheading": "Im Raum",
|
||||
"you": "Du",
|
||||
"unknown": "Unbekannter Teilnehmer",
|
||||
"unknown": "Unbekannte Person",
|
||||
"host": "Host",
|
||||
"contributors": "Mitwirkende",
|
||||
"contributors": "Teilnehmende",
|
||||
"collapsable": {
|
||||
"open": "{{name}}-Liste öffnen",
|
||||
"close": "{{name}}-Liste schließen"
|
||||
@@ -529,7 +551,7 @@
|
||||
"muteParticipant": "{{name}}’s Mikrofon ausschalten",
|
||||
"muteParticipantAlert": {
|
||||
"heading": "{{name}} stummschalten",
|
||||
"description": "{{name}} für alle Teilnehmer stummschalten? {{name}} kann sich nur selbst wieder einschalten.",
|
||||
"description": "{{name}} für alle Teilnehmenden stummschalten? {{name}} kann sich nur selbst wieder einschalten.",
|
||||
"confirm": "Stummschalten",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
@@ -554,8 +576,8 @@
|
||||
},
|
||||
"participantMenu": {
|
||||
"remove": {
|
||||
"label": "Vom Anruf ausschließen",
|
||||
"ariaLabel": "{{name}} vom Anruf ausschließen"
|
||||
"label": "Vom Meeting ausschließen",
|
||||
"ariaLabel": "{{name}} vom Meeting ausschließen"
|
||||
},
|
||||
"unpin": {
|
||||
"label": "Lösen",
|
||||
@@ -589,8 +611,8 @@
|
||||
"started": "Aufzeichnung läuft"
|
||||
},
|
||||
"limitReachedAlert": {
|
||||
"title": "Aufnahmelimit überschritten",
|
||||
"description": "Die Aufnahme hat die zulässige Höchstdauer{{duration_message}} überschritten. Sie wird jetzt automatisch gespeichert.",
|
||||
"title": "Aufzeichnungslimit überschritten",
|
||||
"description": "Die Aufzeichnung hat die zulässige Höchstdauer{{duration_message}} überschritten. Sie wird jetzt automatisch gespeichert.",
|
||||
"durationMessage": " von {{duration}}",
|
||||
"button": "OK"
|
||||
}
|
||||
@@ -602,12 +624,12 @@
|
||||
"enable": "Anheften",
|
||||
"disable": "Lösen"
|
||||
},
|
||||
"effects": "Visuelle Effekte anwenden",
|
||||
"effects": "Hintergründe und Effekte anwenden",
|
||||
"muteParticipant": "{{name}} stummschalten",
|
||||
"fullScreen": "Vollbild"
|
||||
},
|
||||
"shortcutsPanel": {
|
||||
"title": "Tastenkombinationen",
|
||||
"title": "Tastenkürzel",
|
||||
"categories": {
|
||||
"navigation": "Navigation",
|
||||
"media": "Medien",
|
||||
@@ -618,13 +640,13 @@
|
||||
"focus-toolbar": "Fokus auf die untere Symbolleiste",
|
||||
"toggle-microphone": "Mikrofon umschalten",
|
||||
"toggle-camera": "Kamera umschalten",
|
||||
"push-to-talk": "Push-to-talk (gedrückt halten zum Einschalten)",
|
||||
"push-to-talk": "Push-to-talk",
|
||||
"reaction": "Reaktionspanel",
|
||||
"fullscreen": "Vollbild umschalten",
|
||||
"recording": "Aufnahmepanel umschalten",
|
||||
"raise-hand": "Hand heben oder senken",
|
||||
"toggle-chat": "Chat anzeigen/ausblenden",
|
||||
"toggle-participants": "Teilnehmer anzeigen/ausblenden",
|
||||
"toggle-participants": "Teilnehmende anzeigen/ausblenden",
|
||||
"open-shortcuts-settings": "Tastenkürzel-Einstellungen öffnen"
|
||||
},
|
||||
"sr": {
|
||||
@@ -642,8 +664,8 @@
|
||||
}
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "Um eine Endlosschleife zu vermeiden, teile nicht deinen gesamten Bildschirm. Teile stattdessen einen Tab oder ein anderes Fenster.",
|
||||
"stop": "Präsentation beenden",
|
||||
"message": "Damit dein Bildschirm nicht unendlich gespiegelt wird, teile nicht deinen gesamten Bildschirm. Teile stattdessen einen Tab oder ein anderes Fenster.",
|
||||
"stop": "Teilen beenden",
|
||||
"ignore": "Ignorieren"
|
||||
},
|
||||
"participantTile": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"joinButton": "Mit Visio beitreten",
|
||||
"copyLinkTooltip": "Link kopieren",
|
||||
"resetLabel": "Zurücksetzen",
|
||||
"participantLimit": "Bis zu 150 Teilnehmer.",
|
||||
"popupBlocked": "Popup wurde blockiert. Bitte erlauben Sie Popups für diese Website."
|
||||
"participantLimit": "Bis zu 150 Teilnehmende.",
|
||||
"popupBlocked": "Popup wurde blockiert. Bitte erlaube Popups für diese Website."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
{
|
||||
"account": {
|
||||
"currentlyLoggedAs": "Sie sind derzeit angemeldet als <0>{{user}}</0>",
|
||||
"currentlyLoggedAs": "Du bist derzeit angemeldet als <0>{{user}}</0>",
|
||||
"heading": "Konto",
|
||||
"youAreNotLoggedIn": "Sie sind nicht angemeldet.",
|
||||
"nameLabel": "Ihr Name",
|
||||
"youAreNotLoggedIn": "Du bist nicht angemeldet.",
|
||||
"nameLabel": "Dein Name",
|
||||
"authentication": "Authentifizierung",
|
||||
"nameError": "Ihr Name darf nicht leer sein"
|
||||
"nameError": "Dein Name darf nicht leer sein"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Einstellungen",
|
||||
"idleDisconnectModal": {
|
||||
"label": "Anrufe ohne Teilnehmer verlassen",
|
||||
"description": "Verlässt automatisch einen Anruf nach einigen Minuten, wenn kein anderer Teilnehmer beitritt"
|
||||
"label": "Meetings ohne Teilnehmende verlassen",
|
||||
"description": "Meetings nach einigen Minuten automatisch verlassen, wenn keine andere Person beitritt"
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"microphone": {
|
||||
"heading": "Mikrofon",
|
||||
"label": "Wählen Sie Ihre Audioeingabe",
|
||||
"label": "Audioeingabe wählen",
|
||||
"disabled": "Mikrofon deaktiviert"
|
||||
},
|
||||
"noiseReduction": {
|
||||
"label": "Rauschunterdrückung",
|
||||
"heading": "Rauschunterdrückung",
|
||||
"label": "Geräuschunterdrückung",
|
||||
"heading": "Geräuschunterdrückung",
|
||||
"ariaLabel": {
|
||||
"enable": "Rauschunterdrückung aktivieren",
|
||||
"disable": "Rauschunterdrückung deaktivieren"
|
||||
"enable": "Geräuschunterdrückung aktivieren",
|
||||
"disable": "Geräuschunterdrückung deaktivieren"
|
||||
}
|
||||
},
|
||||
"speakers": {
|
||||
"heading": "Lautsprecher",
|
||||
"label": "Wählen Sie Ihre Audioausgabe",
|
||||
"label": "Audioausgabe wählen",
|
||||
"test": "Testen",
|
||||
"ongoingTest": "Soundtest läuft…",
|
||||
"safariWarning": "Die Lautsprecherauswahl ist auf Safari aufgrund von Browser-Einschränkungen noch nicht verfügbar."
|
||||
"safariWarning": "Die Audioausgabeauswahl ist auf Safari aufgrund von Browser-Einschränkungen noch nicht verfügbar."
|
||||
},
|
||||
"permissionsRequired": "Berechtigungen erforderlich"
|
||||
},
|
||||
"video": {
|
||||
"camera": {
|
||||
"heading": "Kamera",
|
||||
"label": "Wählen Sie Ihre Videoeingabe aus",
|
||||
"label": "Videoeingabe wählen",
|
||||
"disabled": "Kamera deaktiviert",
|
||||
"previewAriaLabel": {
|
||||
"enabled": "Videovorschau aktiviert",
|
||||
@@ -50,18 +50,18 @@
|
||||
"resolution": {
|
||||
"heading": "Auflösung",
|
||||
"publish": {
|
||||
"label": "Wählen Sie Ihre maximale Sendeauflösung (max.)",
|
||||
"label": "Wähle die maximale Auflösung beim Senden",
|
||||
"items": {
|
||||
"high": "Hohe Auflösung",
|
||||
"medium": "Standardauflösung",
|
||||
"medium": "Mittlere Auflösung",
|
||||
"low": "Niedrige Auflösung"
|
||||
}
|
||||
},
|
||||
"subscribe": {
|
||||
"label": "Wählen Sie Ihre Empfangsauflösung (max.)",
|
||||
"label": "Wähle die maximale Auflösung beim Empfangen",
|
||||
"items": {
|
||||
"high": "Hohe Auflösung (automatisch)",
|
||||
"medium": "Standardauflösung",
|
||||
"medium": "Mittlere Auflösung",
|
||||
"low": "Niedrige Auflösung"
|
||||
}
|
||||
}
|
||||
@@ -82,15 +82,15 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Tonbenachrichtigungen",
|
||||
"label": "Tonbenachrichtigungen für",
|
||||
"heading": "Benachrichtigungen mit Ton",
|
||||
"label": "Benachrichtigungen mit Ton für",
|
||||
"actions": {
|
||||
"disable": "Deaktivieren",
|
||||
"enable": "Aktivieren"
|
||||
},
|
||||
"items": {
|
||||
"participantJoined": {
|
||||
"label": "Teilnehmer beigetreten"
|
||||
"label": "Person beigetreten"
|
||||
},
|
||||
"handRaised": {
|
||||
"label": "Hand gehoben"
|
||||
@@ -126,6 +126,34 @@
|
||||
"medium": "Mittel",
|
||||
"large": "Groß"
|
||||
}
|
||||
},
|
||||
"fontColor": {
|
||||
"label": "Schriftfarbe",
|
||||
"options": {
|
||||
"default": "Standard",
|
||||
"white": "Weiß",
|
||||
"black": "Schwarz",
|
||||
"blue": "Blau",
|
||||
"green": "Grün",
|
||||
"red": "Rot",
|
||||
"yellow": "Gelb",
|
||||
"cyan": "Cyan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
},
|
||||
"backgroundColor": {
|
||||
"label": "Hintergrundfarbe",
|
||||
"options": {
|
||||
"default": "Standard",
|
||||
"white": "Weiß",
|
||||
"black": "Schwarz",
|
||||
"blue": "Blau",
|
||||
"green": "Grün",
|
||||
"red": "Rot",
|
||||
"yellow": "Gelb",
|
||||
"cyan": "Cyan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"loginHint": {
|
||||
"title": "Log in with your account",
|
||||
"body": "Instead of waiting, log in with your ProConnect account.",
|
||||
"body": "Instead of waiting, log in with your account.",
|
||||
"button": {
|
||||
"ariaLabel": "Close the suggestion",
|
||||
"label": "OK"
|
||||
|
||||
@@ -270,18 +270,39 @@
|
||||
}
|
||||
},
|
||||
"virtual": {
|
||||
"title": "Virtual background",
|
||||
"title": "Virtual backgrounds",
|
||||
"selectedLabel": "Background applied:",
|
||||
"apply": "Replace your background:",
|
||||
"descriptions": {
|
||||
"0": "Fluted wooden furniture",
|
||||
"1": "Meeting room",
|
||||
"2": "Loft with black glass partition",
|
||||
"3": "Dining room",
|
||||
"4": "Wooden shelves",
|
||||
"5": "Wooden staircase",
|
||||
"6": "Gray library",
|
||||
"7": "Coffee counter"
|
||||
"personal": {
|
||||
"title": "My backgrounds",
|
||||
"selectFileTooltip": "Select an image file to use as a personal background",
|
||||
"notLoggedInWarning": "You are not logged-in, personal backgrounds won't be saved from one meeting to the other.",
|
||||
"warningUploadDisabled": "Personal backgrounds are currently not saved from one meeting to the other.",
|
||||
"uploadLimitReached": "You cannot upload more personal backgrounds.",
|
||||
"errors": {
|
||||
"close": "Close",
|
||||
"file_too_large": {
|
||||
"title": "File too large",
|
||||
"description": "The file is too large. Please choose a file smaller than {{maxSize, number}} MB."
|
||||
},
|
||||
"invalid_file_type": {
|
||||
"title": "Invalid file type",
|
||||
"description": "The file type is not supported. Please choose a {{allowedExtension, list(type: 'disjunction')}} file."
|
||||
}
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Suggestions",
|
||||
"descriptions": {
|
||||
"0": "Fluted wooden furniture",
|
||||
"1": "Meeting room",
|
||||
"2": "Loft with black glass partition",
|
||||
"3": "Dining room",
|
||||
"4": "Wooden shelves",
|
||||
"5": "Wooden staircase",
|
||||
"6": "Gray library",
|
||||
"7": "Coffee counter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"faceLandmarks": {
|
||||
@@ -297,7 +318,7 @@
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"ariaLabel": "Sidepanel",
|
||||
"ariaLabel": "Sidepanel - {{title}}",
|
||||
"backToTools": "Back to meeting tools",
|
||||
"heading": {
|
||||
"participants": "Participants",
|
||||
|
||||
@@ -126,6 +126,34 @@
|
||||
"medium": "Medium",
|
||||
"large": "Large"
|
||||
}
|
||||
},
|
||||
"fontColor": {
|
||||
"label": "Font color",
|
||||
"options": {
|
||||
"default": "Default",
|
||||
"white": "White",
|
||||
"black": "Black",
|
||||
"blue": "Blue",
|
||||
"green": "Green",
|
||||
"red": "Red",
|
||||
"yellow": "Yellow",
|
||||
"cyan": "Cyan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
},
|
||||
"backgroundColor": {
|
||||
"label": "Background color",
|
||||
"options": {
|
||||
"default": "Default",
|
||||
"white": "White",
|
||||
"black": "Black",
|
||||
"blue": "Blue",
|
||||
"green": "Green",
|
||||
"red": "Red",
|
||||
"yellow": "Yellow",
|
||||
"cyan": "Cyan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"loginHint": {
|
||||
"title": "Connectez-vous avec votre compte",
|
||||
"body": "Au lieu de patienter, connectez-vous avec votre compte ProConnect.",
|
||||
"body": "Au lieu de patienter, connectez-vous avec votre compte.",
|
||||
"button": {
|
||||
"ariaLabel": "Fermer la suggestion",
|
||||
"label": "OK"
|
||||
|
||||
@@ -270,18 +270,39 @@
|
||||
}
|
||||
},
|
||||
"virtual": {
|
||||
"title": "Arrière-plan virtuel",
|
||||
"title": "Arrière-plans virtuels",
|
||||
"selectedLabel": "Arrière-plan appliqué :",
|
||||
"apply": "Remplacer votre arrière plan :",
|
||||
"descriptions": {
|
||||
"0": "Meuble cannelé en bois",
|
||||
"1": "Salle de réunion",
|
||||
"2": "Loft avec verrière noire",
|
||||
"3": "Salle à manger",
|
||||
"4": "Étagères en bois",
|
||||
"5": "Escalier en bois",
|
||||
"6": "Bibliothèque grise",
|
||||
"7": "Comptoir de café"
|
||||
"personal": {
|
||||
"title": "Mes arrière-plans",
|
||||
"selectFileTooltip": "Sélectionnez une image à utiliser comme arrière-plan",
|
||||
"notLoggedInWarning": "Vous n'êtes pas connecté, l'arrière-plan personnel ne sera pas sauvegardé d'une réunion à l'autre.",
|
||||
"warningUploadDisabled": "Les arrière-plans personnels ne sont actuellement pas sauvegardés d'une réunion à l'autre.",
|
||||
"uploadLimitReached": "Vous ne pouvez pas ajouter plus d'arrière-plans personnels.",
|
||||
"errors": {
|
||||
"close": "Fermer",
|
||||
"file_too_large": {
|
||||
"title": "Fichier trop volumineux",
|
||||
"description": "Le fichier est trop volumineux. Veuillez choisir un fichier de moins de {{maxSize, number}} Mo."
|
||||
},
|
||||
"invalid_file_type": {
|
||||
"title": "Type de fichier non valide",
|
||||
"description": "Le type de fichier n'est pas pris en charge. Veuillez choisir un fichier {{allowedExtension}}."
|
||||
}
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Suggestions",
|
||||
"descriptions": {
|
||||
"0": "Meuble cannelé en bois",
|
||||
"1": "Salle de réunion",
|
||||
"2": "Loft avec verrière noire",
|
||||
"3": "Salle à manger",
|
||||
"4": "Étagères en bois",
|
||||
"5": "Escalier en bois",
|
||||
"6": "Bibliothèque grise",
|
||||
"7": "Comptoir de café"
|
||||
}
|
||||
}
|
||||
},
|
||||
"faceLandmarks": {
|
||||
@@ -297,7 +318,7 @@
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"ariaLabel": "Panneau latéral",
|
||||
"ariaLabel": "Panneau latéral - {{title}}",
|
||||
"backToTools": "Retour aux outils de réunion",
|
||||
"heading": {
|
||||
"participants": "Participants",
|
||||
|
||||
@@ -126,6 +126,34 @@
|
||||
"medium": "Moyen",
|
||||
"large": "Grand"
|
||||
}
|
||||
},
|
||||
"fontColor": {
|
||||
"label": "Couleur du texte",
|
||||
"options": {
|
||||
"default": "Par défaut",
|
||||
"white": "Blanc",
|
||||
"black": "Noir",
|
||||
"blue": "Bleu",
|
||||
"green": "Vert",
|
||||
"red": "Rouge",
|
||||
"yellow": "Jaune",
|
||||
"cyan": "Cyan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
},
|
||||
"backgroundColor": {
|
||||
"label": "Couleur de fond",
|
||||
"options": {
|
||||
"default": "Par défaut",
|
||||
"white": "Blanc",
|
||||
"black": "Noir",
|
||||
"blue": "Bleu",
|
||||
"green": "Vert",
|
||||
"red": "Rouge",
|
||||
"yellow": "Jaune",
|
||||
"cyan": "Cyan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"loginHint": {
|
||||
"title": "Log in met je account",
|
||||
"body": "In plaats van te wachten, log in met je ProConnect-account.",
|
||||
"body": "In plaats van te wachten, log in met je account.",
|
||||
"button": {
|
||||
"ariaLabel": "Sluit de suggestie",
|
||||
"label": "OK"
|
||||
|
||||
@@ -273,15 +273,36 @@
|
||||
"title": "Virtuele achtergrond",
|
||||
"selectedLabel": "Achtergrond toegepast:",
|
||||
"apply": "Vervang je achtergrond:",
|
||||
"descriptions": {
|
||||
"0": "Geprofileerd houten meubel",
|
||||
"1": "Vergaderruimte",
|
||||
"2": "Loft met zwarte glaswand",
|
||||
"3": "Eetkamer",
|
||||
"4": "Houten planken",
|
||||
"5": "Houten trap",
|
||||
"6": "Grijze bibliotheek",
|
||||
"7": "Koffiebar"
|
||||
"personal": {
|
||||
"title": "Mijn achtergronden",
|
||||
"selectFileTooltip": "Selecteer een afbeeldingsbestand om te gebruiken als persoonlijke achtergrond",
|
||||
"notLoggedInWarning": "U bent niet ingelogd, persoonlijke achtergronden worden niet opgeslagen van de ene vergadering naar de andere.",
|
||||
"warningUploadDisabled": "Persoonlijke achtergronden worden momenteel niet opgeslagen van de ene vergadering naar de andere.",
|
||||
"uploadLimitReached": "U kunt geen persoonlijke achtergronden meer uploaden.",
|
||||
"errors": {
|
||||
"close": "Sluiten",
|
||||
"file_too_large": {
|
||||
"title": "Bestand te groot",
|
||||
"description": "Het bestand is te groot. Kies een bestand kleiner dan {{maxSize, number}} MB."
|
||||
},
|
||||
"invalid_file_type": {
|
||||
"title": "Ongeldig bestandstype",
|
||||
"description": "Het bestandstype wordt niet ondersteund. Kies een {{allowedExtension, list(type: 'disjunction')}} bestand."
|
||||
}
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Suggesties",
|
||||
"descriptions": {
|
||||
"0": "Geprofileerd houten meubel",
|
||||
"1": "Vergaderruimte",
|
||||
"2": "Loft met zwarte glaswand",
|
||||
"3": "Eetkamer",
|
||||
"4": "Houten planken",
|
||||
"5": "Houten trap",
|
||||
"6": "Grijze bibliotheek",
|
||||
"7": "Koffiebar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"faceLandmarks": {
|
||||
@@ -297,7 +318,7 @@
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"ariaLabel": "Zijbalk",
|
||||
"ariaLabel": "Zijbalk - {{title}}",
|
||||
"backToTools": "Terug naar vergadertools",
|
||||
"heading": {
|
||||
"participants": "Deelnemers",
|
||||
|
||||
@@ -126,6 +126,34 @@
|
||||
"medium": "Gemiddeld",
|
||||
"large": "Groot"
|
||||
}
|
||||
},
|
||||
"fontColor": {
|
||||
"label": "Tekstkleur",
|
||||
"options": {
|
||||
"default": "Standaard",
|
||||
"white": "Wit",
|
||||
"black": "Zwart",
|
||||
"blue": "Blauw",
|
||||
"green": "Groen",
|
||||
"red": "Rood",
|
||||
"yellow": "Geel",
|
||||
"cyan": "Cyaan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
},
|
||||
"backgroundColor": {
|
||||
"label": "Achtergrondkleur",
|
||||
"options": {
|
||||
"default": "Standaard",
|
||||
"white": "Wit",
|
||||
"black": "Zwart",
|
||||
"blue": "Blauw",
|
||||
"green": "Groen",
|
||||
"red": "Rood",
|
||||
"yellow": "Geel",
|
||||
"cyan": "Cyaan",
|
||||
"magenta": "Magenta"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -36,6 +36,10 @@ export const text = cva({
|
||||
textStyle: 'body',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
bodyXsMedium: {
|
||||
textStyle: 'body',
|
||||
fontWeight: 'medium',
|
||||
},
|
||||
body: {
|
||||
textStyle: 'body',
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user