mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge remote-tracking branch 'origin/main' into keynav_timeline
This commit is contained in:
commit
a332622730
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@ -224,7 +224,7 @@ jobs:
|
|||||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: | # zizmor: ignore[template-injection]
|
||||||
mkdir -p ${{ runner.temp }}/digests
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
@ -426,7 +426,7 @@ jobs:
|
|||||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
run: |
|
run: | # zizmor: ignore[template-injection]
|
||||||
mkdir -p ${{ runner.temp }}/digests
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
@ -535,6 +535,7 @@ jobs:
|
|||||||
run: exit 1
|
run: exit 1
|
||||||
- name: All jobs passed or skipped
|
- name: All jobs passed or skipped
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
# zizmor: ignore[template-injection]
|
||||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
|
||||||
success-check-ml:
|
success-check-ml:
|
||||||
@ -549,4 +550,5 @@ jobs:
|
|||||||
run: exit 1
|
run: exit 1
|
||||||
- name: All jobs passed or skipped
|
- name: All jobs passed or skipped
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
# zizmor: ignore[template-injection]
|
||||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
14
.github/workflows/docs-deploy.yml
vendored
14
.github/workflows/docs-deploy.yml
vendored
@ -1,6 +1,6 @@
|
|||||||
name: Docs deploy
|
name: Docs deploy
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||||
workflows: ['Docs build']
|
workflows: ['Docs build']
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
@ -115,22 +115,22 @@ jobs:
|
|||||||
- name: Load parameters
|
- name: Load parameters
|
||||||
id: parameters
|
id: parameters
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
|
env:
|
||||||
|
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const json = `${{ needs.checks.outputs.parameters }}`;
|
const parameters = JSON.parse(process.env.PARAM_JSON);
|
||||||
const parameters = JSON.parse(json);
|
|
||||||
core.setOutput("event", parameters.event);
|
core.setOutput("event", parameters.event);
|
||||||
core.setOutput("name", parameters.name);
|
core.setOutput("name", parameters.name);
|
||||||
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
||||||
|
|
||||||
- run: |
|
|
||||||
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
|
|
||||||
|
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
|
env:
|
||||||
|
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
let artifact = ${{ needs.checks.outputs.artifact }};
|
let artifact = JSON.parse(process.env.ARTIFACT_JSON);
|
||||||
let download = await github.rest.actions.downloadArtifact({
|
let download = await github.rest.actions.downloadArtifact({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
|
2
.github/workflows/docs-destroy.yml
vendored
2
.github/workflows/docs-destroy.yml
vendored
@ -1,6 +1,6 @@
|
|||||||
name: Docs destroy
|
name: Docs destroy
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||||
types: [closed]
|
types: [closed]
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
2
.github/workflows/pr-label-validation.yml
vendored
2
.github/workflows/pr-label-validation.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: PR Label Validation
|
name: PR Label Validation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||||
types: [opened, labeled, unlabeled, synchronize]
|
types: [opened, labeled, unlabeled, synchronize]
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
@ -1,6 +1,6 @@
|
|||||||
name: 'Pull Request Labeler'
|
name: 'Pull Request Labeler'
|
||||||
on:
|
on:
|
||||||
- pull_request_target
|
- pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
7
.github/workflows/prepare-release.yml
vendored
7
.github/workflows/prepare-release.yml
vendored
@ -47,7 +47,10 @@ jobs:
|
|||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
|
env:
|
||||||
|
SERVER_BUMP: ${{ inputs.serverBump }}
|
||||||
|
MOBILE_BUMP: ${{ inputs.mobileBump }}
|
||||||
|
run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
|
||||||
|
|
||||||
- name: Commit and tag
|
- name: Commit and tag
|
||||||
id: push-tag
|
id: push-tag
|
||||||
@ -61,6 +64,8 @@ jobs:
|
|||||||
build_mobile:
|
build_mobile:
|
||||||
uses: ./.github/workflows/build-mobile.yml
|
uses: ./.github/workflows/build-mobile.yml
|
||||||
needs: bump_version
|
needs: bump_version
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
secrets:
|
secrets:
|
||||||
KEY_JKS: ${{ secrets.KEY_JKS }}
|
KEY_JKS: ${{ secrets.KEY_JKS }}
|
||||||
ALIAS: ${{ secrets.ALIAS }}
|
ALIAS: ${{ secrets.ALIAS }}
|
||||||
|
27
.github/workflows/static_analysis.yml
vendored
27
.github/workflows/static_analysis.yml
vendored
@ -95,3 +95,30 @@ jobs:
|
|||||||
- name: Run dart custom_lint
|
- name: Run dart custom_lint
|
||||||
run: dart run custom_lint
|
run: dart run custom_lint
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
|
zizmor:
|
||||||
|
name: zizmor
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install the latest version of uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
|
||||||
|
- name: Run zizmor 🌈
|
||||||
|
run: uvx zizmor --format=sarif . > results.sarif
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload SARIF file
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
|
category: zizmor
|
||||||
|
1
.github/workflows/weblate-lock.yml
vendored
1
.github/workflows/weblate-lock.yml
vendored
@ -57,4 +57,5 @@ jobs:
|
|||||||
run: exit 1
|
run: exit 1
|
||||||
- name: All jobs passed or skipped
|
- name: All jobs passed or skipped
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
# zizmor: ignore[template-injection]
|
||||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.61",
|
"version": "2.2.62",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.61",
|
"version": "2.2.62",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.61",
|
"version": "2.2.62",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.132.0",
|
||||||
|
"url": "https://v1.132.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.131.3",
|
"label": "v1.131.3",
|
||||||
"url": "https://v1.131.3.archive.immich.app"
|
"url": "https://v1.131.3.archive.immich.app"
|
||||||
|
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.61",
|
"version": "2.2.62",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -93,7 +93,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
66
i18n/es.json
66
i18n/es.json
@ -39,11 +39,11 @@
|
|||||||
"authentication_settings_disable_all": "¿Estás seguro de que deseas desactivar todos los métodos de inicio de sesión? Esto desactivará por completo el inicio de sesión.",
|
"authentication_settings_disable_all": "¿Estás seguro de que deseas desactivar todos los métodos de inicio de sesión? Esto desactivará por completo el inicio de sesión.",
|
||||||
"authentication_settings_reenable": "Para reactivarlo, utiliza un <link>Comando del servidor</link>.",
|
"authentication_settings_reenable": "Para reactivarlo, utiliza un <link>Comando del servidor</link>.",
|
||||||
"background_task_job": "Tareas en segundo plano",
|
"background_task_job": "Tareas en segundo plano",
|
||||||
"backup_database": "Respaldar base de datos",
|
"backup_database": "Crear volcado de base de datos",
|
||||||
"backup_database_enable_description": "Activar respaldo de base de datos",
|
"backup_database_enable_description": "Activar volcado de base de datos",
|
||||||
"backup_keep_last_amount": "Cantidad de respaldos previos a mantener",
|
"backup_keep_last_amount": "Cantidad de volcados previos a mantener",
|
||||||
"backup_settings": "Ajustes de respaldo",
|
"backup_settings": "Ajustes de volcado de base de datos",
|
||||||
"backup_settings_description": "Administrar configuración de respaldo de base de datos",
|
"backup_settings_description": "Administrar configuración de volcado de base de datos. Nota: estas tareas no están monitorizadas y no se notificarán los fallos.",
|
||||||
"check_all": "Verificar todo",
|
"check_all": "Verificar todo",
|
||||||
"cleanup": "Limpieza",
|
"cleanup": "Limpieza",
|
||||||
"cleared_jobs": "Trabajos borrados para: {job}",
|
"cleared_jobs": "Trabajos borrados para: {job}",
|
||||||
@ -91,9 +91,9 @@
|
|||||||
"image_thumbnail_quality_description": "Calidad de miniatura de 1 a 100. Es mejor cuanto más alto es el valor pero genera archivos más grandes y puede reducir la capacidad de respuesta de la aplicación.",
|
"image_thumbnail_quality_description": "Calidad de miniatura de 1 a 100. Es mejor cuanto más alto es el valor pero genera archivos más grandes y puede reducir la capacidad de respuesta de la aplicación.",
|
||||||
"image_thumbnail_title": "Ajustes de las miniaturas",
|
"image_thumbnail_title": "Ajustes de las miniaturas",
|
||||||
"job_concurrency": "{job}: Procesos simultáneos",
|
"job_concurrency": "{job}: Procesos simultáneos",
|
||||||
"job_created": "Trabajo creado",
|
"job_created": "Tarea creada",
|
||||||
"job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.",
|
"job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.",
|
||||||
"job_settings": "Configuración tareas",
|
"job_settings": "Configuración de tareas",
|
||||||
"job_settings_description": "Administrar tareas simultáneas",
|
"job_settings_description": "Administrar tareas simultáneas",
|
||||||
"job_status": "Estado de la tarea",
|
"job_status": "Estado de la tarea",
|
||||||
"jobs_delayed": "{jobCount, plural, one {# retrasado} other {# retrasados}}",
|
"jobs_delayed": "{jobCount, plural, one {# retrasado} other {# retrasados}}",
|
||||||
@ -169,7 +169,7 @@
|
|||||||
"migration_job_description": "Migrar miniaturas de archivos y caras a la estructura de carpetas más reciente",
|
"migration_job_description": "Migrar miniaturas de archivos y caras a la estructura de carpetas más reciente",
|
||||||
"no_paths_added": "No se han añadido carpetas",
|
"no_paths_added": "No se han añadido carpetas",
|
||||||
"no_pattern_added": "No se han añadido patrones",
|
"no_pattern_added": "No se han añadido patrones",
|
||||||
"note_apply_storage_label_previous_assets": "Nota: para aplicar una Etiqueta de Almacenamient a un elemento anteriormente cargado, lanza el",
|
"note_apply_storage_label_previous_assets": "Nota: para aplicar una Etiqueta de Almacenamiento a un elemento anteriormente cargado, lanza el",
|
||||||
"note_cannot_be_changed_later": "NOTA: ¡No se puede cambiar posteriormente!",
|
"note_cannot_be_changed_later": "NOTA: ¡No se puede cambiar posteriormente!",
|
||||||
"notification_email_from_address": "Desde",
|
"notification_email_from_address": "Desde",
|
||||||
"notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server <noreply@example.com>\"",
|
"notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server <noreply@example.com>\"",
|
||||||
@ -252,12 +252,12 @@
|
|||||||
"storage_template_migration": "Migración de plantillas de almacenamiento",
|
"storage_template_migration": "Migración de plantillas de almacenamiento",
|
||||||
"storage_template_migration_description": "Aplicar la <link>{template}</link> actual a los elementos subidos previamente",
|
"storage_template_migration_description": "Aplicar la <link>{template}</link> actual a los elementos subidos previamente",
|
||||||
"storage_template_migration_info": "La plantilla de almacenamiento convertirá todas las extensiones a minúscula. Los cambios en las plantillas solo se aplican a los elementos nuevos. Para aplicarlos retroactivamente a los elementos subidos previamente ejecute la <link>{job}</link>.",
|
"storage_template_migration_info": "La plantilla de almacenamiento convertirá todas las extensiones a minúscula. Los cambios en las plantillas solo se aplican a los elementos nuevos. Para aplicarlos retroactivamente a los elementos subidos previamente ejecute la <link>{job}</link>.",
|
||||||
"storage_template_migration_job": "Migración de la plantilla de almacenamiento",
|
"storage_template_migration_job": "Tarea de migración de la plantilla de almacenamiento",
|
||||||
"storage_template_more_details": "Para obtener más detalles sobre esta función, consulte la <template-link>Plantilla de almacenamiento</template-link> y sus <implications-link>implicaciones</implications-link>",
|
"storage_template_more_details": "Para obtener más detalles sobre esta función, consulte la <template-link>Plantilla de almacenamiento</template-link> y sus <implications-link>implicaciones</implications-link>",
|
||||||
"storage_template_onboarding_description": "Cuando está habilitada, esta función organizará automáticamente los archivos según una plantilla definida por el usuario. Debido a problemas de estabilidad, la función se ha desactivado de forma predeterminada. Para obtener más información, consulte la <link>documentación</link>.",
|
"storage_template_onboarding_description": "Cuando está habilitada, esta función organizará automáticamente los archivos según una plantilla definida por el usuario. Debido a problemas de estabilidad, la función se ha desactivado de forma predeterminada. Para obtener más información, consulte la <link>documentación</link>.",
|
||||||
"storage_template_path_length": "Límite aproximado de la longitud de la ruta: <b>{length, number}</b>/{limit, number}",
|
"storage_template_path_length": "Límite aproximado de la longitud de la ruta: <b>{length, number}</b>/{limit, number}",
|
||||||
"storage_template_settings": "Plantilla de almacenamiento",
|
"storage_template_settings": "Plantilla de almacenamiento",
|
||||||
"storage_template_settings_description": "Administre la estructura de carpetas y el nombre de archivo del recurso cargado",
|
"storage_template_settings_description": "Administrar la estructura de carpetas y el nombre de archivo del recurso cargado",
|
||||||
"storage_template_user_label": "<code>{label}</code> es la etiqueta de almacenamiento del usuario",
|
"storage_template_user_label": "<code>{label}</code> es la etiqueta de almacenamiento del usuario",
|
||||||
"system_settings": "Ajustes del Sistema",
|
"system_settings": "Ajustes del Sistema",
|
||||||
"tag_cleanup_job": "Limpieza de etiquetas",
|
"tag_cleanup_job": "Limpieza de etiquetas",
|
||||||
@ -345,7 +345,7 @@
|
|||||||
"trash_settings": "Configuración papelera",
|
"trash_settings": "Configuración papelera",
|
||||||
"trash_settings_description": "Administrar la configuración de la papelera",
|
"trash_settings_description": "Administrar la configuración de la papelera",
|
||||||
"untracked_files": "Archivos sin seguimiento",
|
"untracked_files": "Archivos sin seguimiento",
|
||||||
"untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, cargas interrumpidas o sin procesar debido a un error",
|
"untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, subidas interrumpidas o sin procesar debido a un error",
|
||||||
"user_cleanup_job": "Limpieza de usuarios",
|
"user_cleanup_job": "Limpieza de usuarios",
|
||||||
"user_delete_delay": "La cuenta <b>{user}</b> y los archivos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.",
|
"user_delete_delay": "La cuenta <b>{user}</b> y los archivos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.",
|
||||||
"user_delete_delay_settings": "Eliminar retardo",
|
"user_delete_delay_settings": "Eliminar retardo",
|
||||||
@ -429,7 +429,7 @@
|
|||||||
"allow_dark_mode": "Permitir modo oscuro",
|
"allow_dark_mode": "Permitir modo oscuro",
|
||||||
"allow_edits": "Permitir edición",
|
"allow_edits": "Permitir edición",
|
||||||
"allow_public_user_to_download": "Permitir descargar al usuario público",
|
"allow_public_user_to_download": "Permitir descargar al usuario público",
|
||||||
"allow_public_user_to_upload": "Permitir cargar al usuario publico",
|
"allow_public_user_to_upload": "Permitir subir al usuario publico",
|
||||||
"alt_text_qr_code": "Código QR",
|
"alt_text_qr_code": "Código QR",
|
||||||
"anti_clockwise": "En sentido antihorario",
|
"anti_clockwise": "En sentido antihorario",
|
||||||
"api_key": "Clave API",
|
"api_key": "Clave API",
|
||||||
@ -473,7 +473,7 @@
|
|||||||
"asset_skipped": "Omitido",
|
"asset_skipped": "Omitido",
|
||||||
"asset_skipped_in_trash": "En la papelera",
|
"asset_skipped_in_trash": "En la papelera",
|
||||||
"asset_uploaded": "Subido",
|
"asset_uploaded": "Subido",
|
||||||
"asset_uploading": "Cargando…",
|
"asset_uploading": "Subiendo…",
|
||||||
"asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos",
|
"asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos",
|
||||||
"asset_viewer_settings_title": "Visor de Archivos",
|
"asset_viewer_settings_title": "Visor de Archivos",
|
||||||
"assets": "elementos",
|
"assets": "elementos",
|
||||||
@ -482,7 +482,7 @@
|
|||||||
"assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
|
"assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {<b>{name}</b>} other {new album}}",
|
||||||
"assets_count": "{count, plural, one {# activo} other {# activos}}",
|
"assets_count": "{count, plural, one {# activo} other {# activos}}",
|
||||||
"assets_deleted_permanently": "{} elementos(s) eliminado(s) permanentemente",
|
"assets_deleted_permanently": "{} elementos(s) eliminado(s) permanentemente",
|
||||||
"assets_deleted_permanently_from_server": "{} recurso(s) eliminados de forma permanente del servidor de Immich",
|
"assets_deleted_permanently_from_server": "{} recurso(s) eliminado(s) de forma permanente del servidor de Immich",
|
||||||
"assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera",
|
"assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera",
|
||||||
"assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}",
|
"assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}",
|
||||||
"assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}",
|
"assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}",
|
||||||
@ -492,7 +492,7 @@
|
|||||||
"assets_restored_successfully": "{} elemento(s) restaurado(s) exitosamente",
|
"assets_restored_successfully": "{} elemento(s) restaurado(s) exitosamente",
|
||||||
"assets_trashed": "{} elemento(s) eliminado(s)",
|
"assets_trashed": "{} elemento(s) eliminado(s)",
|
||||||
"assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}",
|
"assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}",
|
||||||
"assets_trashed_from_server": "{} recurso(s) enviados a la papelera desde el servidor de Immich",
|
"assets_trashed_from_server": "{} recurso(s) enviado(s) a la papelera desde el servidor de Immich",
|
||||||
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum",
|
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum",
|
||||||
"authorized_devices": "Dispositivos Autorizados",
|
"authorized_devices": "Dispositivos Autorizados",
|
||||||
"automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares",
|
"automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares",
|
||||||
@ -510,11 +510,11 @@
|
|||||||
"backup_all": "Todos",
|
"backup_all": "Todos",
|
||||||
"backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando…",
|
"backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando…",
|
||||||
"backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…",
|
"backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…",
|
||||||
"backup_background_service_current_upload_notification": "Cargando {}",
|
"backup_background_service_current_upload_notification": "Subiendo {}",
|
||||||
"backup_background_service_default_notification": "Comprobando nuevos elementos…",
|
"backup_background_service_default_notification": "Comprobando nuevos elementos…",
|
||||||
"backup_background_service_error_title": "Error de copia de seguridad",
|
"backup_background_service_error_title": "Error de copia de seguridad",
|
||||||
"backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos…",
|
"backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos…",
|
||||||
"backup_background_service_upload_failure_notification": "Error al cargar {}",
|
"backup_background_service_upload_failure_notification": "Error al subir {}",
|
||||||
"backup_controller_page_albums": "Álbumes de copia de seguridad",
|
"backup_controller_page_albums": "Álbumes de copia de seguridad",
|
||||||
"backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.",
|
"backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.",
|
||||||
"backup_controller_page_background_app_refresh_disabled_title": "Actualización en segundo plano desactivada",
|
"backup_controller_page_background_app_refresh_disabled_title": "Actualización en segundo plano desactivada",
|
||||||
@ -536,7 +536,7 @@
|
|||||||
"backup_controller_page_backup_selected": "Seleccionado: ",
|
"backup_controller_page_backup_selected": "Seleccionado: ",
|
||||||
"backup_controller_page_backup_sub": "Fotos y videos respaldados",
|
"backup_controller_page_backup_sub": "Fotos y videos respaldados",
|
||||||
"backup_controller_page_created": "Creado el: {}",
|
"backup_controller_page_created": "Creado el: {}",
|
||||||
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos elementos al servidor.",
|
"backup_controller_page_desc_backup": "Active la copia de seguridad para subir automáticamente los nuevos elementos al servidor cuando se abre la aplicación.",
|
||||||
"backup_controller_page_excluded": "Excluido: ",
|
"backup_controller_page_excluded": "Excluido: ",
|
||||||
"backup_controller_page_failed": "Fallidos ({})",
|
"backup_controller_page_failed": "Fallidos ({})",
|
||||||
"backup_controller_page_filename": "Nombre del archivo: {} [{}]",
|
"backup_controller_page_filename": "Nombre del archivo: {} [{}]",
|
||||||
@ -554,11 +554,11 @@
|
|||||||
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
|
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
|
||||||
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
|
"backup_controller_page_turn_off": "Apagar la copia de seguridad",
|
||||||
"backup_controller_page_turn_on": "Activar la copia de seguridad",
|
"backup_controller_page_turn_on": "Activar la copia de seguridad",
|
||||||
"backup_controller_page_uploading_file_info": "Cargando información del archivo",
|
"backup_controller_page_uploading_file_info": "Subiendo información del archivo",
|
||||||
"backup_err_only_album": "No se puede eliminar el único álbum",
|
"backup_err_only_album": "No se puede eliminar el único álbum",
|
||||||
"backup_info_card_assets": "elementos",
|
"backup_info_card_assets": "elementos",
|
||||||
"backup_manual_cancelled": "Cancelado",
|
"backup_manual_cancelled": "Cancelado",
|
||||||
"backup_manual_in_progress": "Subida en progreso. Espere",
|
"backup_manual_in_progress": "Subida ya en progreso. Vuelve a intentarlo más tarde",
|
||||||
"backup_manual_success": "Éxito",
|
"backup_manual_success": "Éxito",
|
||||||
"backup_manual_title": "Estado de la subida",
|
"backup_manual_title": "Estado de la subida",
|
||||||
"backup_options_page_title": "Opciones de Copia de Seguridad",
|
"backup_options_page_title": "Opciones de Copia de Seguridad",
|
||||||
@ -767,7 +767,7 @@
|
|||||||
"download_enqueue": "Descarga en cola",
|
"download_enqueue": "Descarga en cola",
|
||||||
"download_error": "Error al descargar",
|
"download_error": "Error al descargar",
|
||||||
"download_failed": "Descarga fallida",
|
"download_failed": "Descarga fallida",
|
||||||
"download_filename": "Archivo: {}",
|
"download_filename": "archivo: {}",
|
||||||
"download_finished": "Descarga completada",
|
"download_finished": "Descarga completada",
|
||||||
"download_include_embedded_motion_videos": "Vídeos incrustados",
|
"download_include_embedded_motion_videos": "Vídeos incrustados",
|
||||||
"download_include_embedded_motion_videos_description": "Incluir vídeos incrustados en fotografías en movimiento como un archivo separado",
|
"download_include_embedded_motion_videos_description": "Incluir vídeos incrustados en fotografías en movimiento como un archivo separado",
|
||||||
@ -978,7 +978,7 @@
|
|||||||
"external": "Externo",
|
"external": "Externo",
|
||||||
"external_libraries": "Bibliotecas Externas",
|
"external_libraries": "Bibliotecas Externas",
|
||||||
"external_network": "Red externa",
|
"external_network": "Red externa",
|
||||||
"external_network_sheet_info": "Cuando no estés conectado a la red WiFi preferida, la aplicación se conectará al servidor utilizando la primera de las siguientes URLs a la que pueda acceder, comenzando desde la parte superior de la lista hacia abajo",
|
"external_network_sheet_info": "Cuando no estés conectado a la red Wi-Fi preferida, la aplicación se conectará al servidor utilizando la primera de las siguientes URLs a la que pueda acceder, comenzando desde la parte superior de la lista hacia abajo",
|
||||||
"face_unassigned": "Sin asignar",
|
"face_unassigned": "Sin asignar",
|
||||||
"failed": "Fallido",
|
"failed": "Fallido",
|
||||||
"failed_to_load_assets": "Error al cargar los activos",
|
"failed_to_load_assets": "Error al cargar los activos",
|
||||||
@ -1125,7 +1125,7 @@
|
|||||||
"local_network": "Local network",
|
"local_network": "Local network",
|
||||||
"local_network_sheet_info": "La aplicación se conectará al servidor a través de esta URL cuando utilice la red Wi-Fi especificada",
|
"local_network_sheet_info": "La aplicación se conectará al servidor a través de esta URL cuando utilice la red Wi-Fi especificada",
|
||||||
"location_permission": "Permiso de ubicación",
|
"location_permission": "Permiso de ubicación",
|
||||||
"location_permission_content": "Para usar la función de cambio automático, Immich necesita permiso de ubicación precisa para poder leer el nombre de la red WiFi actual",
|
"location_permission_content": "Para usar la función de cambio automático, Immich necesita permiso de ubicación precisa para poder leer el nombre de la red Wi-Fi actual",
|
||||||
"location_picker_choose_on_map": "Elegir en el mapa",
|
"location_picker_choose_on_map": "Elegir en el mapa",
|
||||||
"location_picker_latitude_error": "Introduce una latitud válida",
|
"location_picker_latitude_error": "Introduce una latitud válida",
|
||||||
"location_picker_latitude_hint": "Introduce tu latitud aquí",
|
"location_picker_latitude_hint": "Introduce tu latitud aquí",
|
||||||
@ -1263,7 +1263,7 @@
|
|||||||
"no_shared_albums_message": "Crea un álbum para compartir fotos y vídeos con personas de tu red",
|
"no_shared_albums_message": "Crea un álbum para compartir fotos y vídeos con personas de tu red",
|
||||||
"not_in_any_album": "Sin álbum",
|
"not_in_any_album": "Sin álbum",
|
||||||
"not_selected": "No seleccionado",
|
"not_selected": "No seleccionado",
|
||||||
"note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos cargados previamente, ejecute el",
|
"note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos subidos previamente, ejecute el",
|
||||||
"notes": "Notas",
|
"notes": "Notas",
|
||||||
"notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.",
|
"notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.",
|
||||||
"notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.",
|
"notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.",
|
||||||
@ -1432,6 +1432,8 @@
|
|||||||
"recent_searches": "Búsquedas recientes",
|
"recent_searches": "Búsquedas recientes",
|
||||||
"recently_added": "Añadidos recientemente",
|
"recently_added": "Añadidos recientemente",
|
||||||
"recently_added_page_title": "Recién Agregadas",
|
"recently_added_page_title": "Recién Agregadas",
|
||||||
|
"recently_taken": "Recientemente tomado",
|
||||||
|
"recently_taken_page_title": "Recientemente Tomado",
|
||||||
"refresh": "Actualizar",
|
"refresh": "Actualizar",
|
||||||
"refresh_encoded_videos": "Recargar los vídeos codificados",
|
"refresh_encoded_videos": "Recargar los vídeos codificados",
|
||||||
"refresh_faces": "Actualizar caras",
|
"refresh_faces": "Actualizar caras",
|
||||||
@ -1615,7 +1617,7 @@
|
|||||||
"settings_saved": "Ajustes guardados",
|
"settings_saved": "Ajustes guardados",
|
||||||
"share": "Compartir",
|
"share": "Compartir",
|
||||||
"share_add_photos": "Agregar fotos",
|
"share_add_photos": "Agregar fotos",
|
||||||
"share_assets_selected": "{} seleccionados",
|
"share_assets_selected": "{} seleccionado(s)",
|
||||||
"share_dialog_preparing": "Preparando...",
|
"share_dialog_preparing": "Preparando...",
|
||||||
"shared": "Compartido",
|
"shared": "Compartido",
|
||||||
"shared_album_activities_input_disable": "Los comentarios están deshabilitados",
|
"shared_album_activities_input_disable": "Los comentarios están deshabilitados",
|
||||||
@ -1629,7 +1631,7 @@
|
|||||||
"shared_by_user": "Compartido por {user}",
|
"shared_by_user": "Compartido por {user}",
|
||||||
"shared_by_you": "Compartido por ti",
|
"shared_by_you": "Compartido por ti",
|
||||||
"shared_from_partner": "Fotos de {partner}",
|
"shared_from_partner": "Fotos de {partner}",
|
||||||
"shared_intent_upload_button_progress_text": "{} / {} Cargados",
|
"shared_intent_upload_button_progress_text": "{} / {} Cargado(s)",
|
||||||
"shared_link_app_bar_title": "Enlaces compartidos",
|
"shared_link_app_bar_title": "Enlaces compartidos",
|
||||||
"shared_link_clipboard_copied_massage": "Copiado al portapapeles",
|
"shared_link_clipboard_copied_massage": "Copiado al portapapeles",
|
||||||
"shared_link_clipboard_text": "Enlace: {}\nContraseña: {}",
|
"shared_link_clipboard_text": "Enlace: {}\nContraseña: {}",
|
||||||
@ -1790,7 +1792,7 @@
|
|||||||
"trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerán aquí.",
|
"trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerán aquí.",
|
||||||
"trash_page_delete_all": "Eliminar todos",
|
"trash_page_delete_all": "Eliminar todos",
|
||||||
"trash_page_empty_trash_dialog_content": "¿Está seguro que quiere eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente",
|
"trash_page_empty_trash_dialog_content": "¿Está seguro que quiere eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente",
|
||||||
"trash_page_info": "Los archivos en la papelera serán eliminados automáticamente después de {} días",
|
"trash_page_info": "Los archivos en la papelera serán eliminados automáticamente de forma permanente después de {} días",
|
||||||
"trash_page_no_assets": "No hay elementos en la papelera",
|
"trash_page_no_assets": "No hay elementos en la papelera",
|
||||||
"trash_page_restore_all": "Restaurar todos",
|
"trash_page_restore_all": "Restaurar todos",
|
||||||
"trash_page_select_assets_btn": "Seleccionar elementos",
|
"trash_page_select_assets_btn": "Seleccionar elementos",
|
||||||
@ -1818,22 +1820,22 @@
|
|||||||
"unstack": "Desapilar",
|
"unstack": "Desapilar",
|
||||||
"unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}",
|
"unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}",
|
||||||
"untracked_files": "Archivos no monitorizados",
|
"untracked_files": "Archivos no monitorizados",
|
||||||
"untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, cargas interrumpidas o por un fallo de la aplicación",
|
"untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, subidas interrumpidas o por un fallo de la aplicación",
|
||||||
"up_next": "A continuación",
|
"up_next": "A continuación",
|
||||||
"updated_password": "Contraseña actualizada",
|
"updated_password": "Contraseña actualizada",
|
||||||
"upload": "Subir",
|
"upload": "Subir",
|
||||||
"upload_concurrency": "Cargas simultáneas",
|
"upload_concurrency": "Subidas simultáneas",
|
||||||
"upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
|
"upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?",
|
||||||
"upload_dialog_title": "Subir elementos",
|
"upload_dialog_title": "Subir elementos",
|
||||||
"upload_errors": "Carga completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de carga.",
|
"upload_errors": "Subida completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de la subida.",
|
||||||
"upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}",
|
"upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}",
|
||||||
"upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}",
|
"upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}",
|
||||||
"upload_status_duplicates": "Duplicados",
|
"upload_status_duplicates": "Duplicados",
|
||||||
"upload_status_errors": "Errores",
|
"upload_status_errors": "Errores",
|
||||||
"upload_status_uploaded": "Subido",
|
"upload_status_uploaded": "Subido",
|
||||||
"upload_success": "Carga realizada correctamente, actualice la página para ver los nuevos recursos de carga.",
|
"upload_success": "Subida realizada correctamente, actualice la página para ver los nuevos recursos de subida.",
|
||||||
"upload_to_immich": "Subir a Immich ({})",
|
"upload_to_immich": "Subir a Immich ({})",
|
||||||
"uploading": "Cargando",
|
"uploading": "Subiendo",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Uso",
|
"usage": "Uso",
|
||||||
"use_current_connection": "Usar conexión actual",
|
"use_current_connection": "Usar conexión actual",
|
||||||
|
@ -915,6 +915,8 @@
|
|||||||
"hide_unnamed_people": "Sakrij neimenovane osobe",
|
"hide_unnamed_people": "Sakrij neimenovane osobe",
|
||||||
"host": "Domaćin",
|
"host": "Domaćin",
|
||||||
"hour": "Sat",
|
"hour": "Sat",
|
||||||
|
"ignore_icloud_photos": "Ignoriraj iCloud fotografije",
|
||||||
|
"ignore_icloud_photos_description": "Fotografije pohranjene na iCloudu neće biti učitane na Immich poslužitelj",
|
||||||
"image": "Slika",
|
"image": "Slika",
|
||||||
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}",
|
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}",
|
||||||
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} {date}",
|
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} {date}",
|
||||||
@ -926,6 +928,10 @@
|
|||||||
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} i {person2} {date}",
|
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} i {person2} {date}",
|
||||||
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {person3} {date}",
|
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {person3} {date}",
|
||||||
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {additionalCount, number} drugih {date}",
|
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {additionalCount, number} drugih {date}",
|
||||||
|
"image_saved_successfully": "Slika je spremljena",
|
||||||
|
"image_viewer_page_state_provider_download_started": "Preuzimanje započelo",
|
||||||
|
"image_viewer_page_state_provider_download_success": "Uspješno Preuzimanje",
|
||||||
|
"image_viewer_page_state_provider_share_error": "Greška pri dijeljenju",
|
||||||
"immich_logo": "Immich Logo",
|
"immich_logo": "Immich Logo",
|
||||||
"immich_web_interface": "Immich Web Sučelje",
|
"immich_web_interface": "Immich Web Sučelje",
|
||||||
"import_from_json": "Uvoz iz JSON-a",
|
"import_from_json": "Uvoz iz JSON-a",
|
||||||
|
@ -1432,6 +1432,8 @@
|
|||||||
"recent_searches": "Pencarian terkini",
|
"recent_searches": "Pencarian terkini",
|
||||||
"recently_added": "Recently added",
|
"recently_added": "Recently added",
|
||||||
"recently_added_page_title": "Baru Ditambahkan",
|
"recently_added_page_title": "Baru Ditambahkan",
|
||||||
|
"recently_taken": "Diambil terkini",
|
||||||
|
"recently_taken_page_title": "Diambil Terkini",
|
||||||
"refresh": "Segarkan",
|
"refresh": "Segarkan",
|
||||||
"refresh_encoded_videos": "Segarkan video terenkode",
|
"refresh_encoded_videos": "Segarkan video terenkode",
|
||||||
"refresh_faces": "Segarkan wajah",
|
"refresh_faces": "Segarkan wajah",
|
||||||
|
@ -1432,6 +1432,7 @@
|
|||||||
"recent_searches": "최근 검색",
|
"recent_searches": "최근 검색",
|
||||||
"recently_added": "최근 추가",
|
"recently_added": "최근 추가",
|
||||||
"recently_added_page_title": "최근 추가",
|
"recently_added_page_title": "최근 추가",
|
||||||
|
"recently_taken": "최근 촬영됨",
|
||||||
"refresh": "새로고침",
|
"refresh": "새로고침",
|
||||||
"refresh_encoded_videos": "동영상 재인코딩",
|
"refresh_encoded_videos": "동영상 재인코딩",
|
||||||
"refresh_faces": "얼굴 새로고침",
|
"refresh_faces": "얼굴 새로고침",
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
"image_quality": "Kvalitet",
|
"image_quality": "Kvalitet",
|
||||||
"image_resolution": "Oppløsning",
|
"image_resolution": "Oppløsning",
|
||||||
"image_resolution_description": "Høyere oppløsninger kan bevare flere detaljer, men det tar lengre tid å kode, har større filstørrelser og kan redusere appresponsen.",
|
"image_resolution_description": "Høyere oppløsninger kan bevare flere detaljer, men det tar lengre tid å kode, har større filstørrelser og kan redusere appresponsen.",
|
||||||
"image_settings": "Bildeinnstilliinger",
|
"image_settings": "Bildeinnstillinger",
|
||||||
"image_settings_description": "Administrer kvalitet og oppløsning på genererte bilder",
|
"image_settings_description": "Administrer kvalitet og oppløsning på genererte bilder",
|
||||||
"image_thumbnail_description": "Små miniatyrbilder med strippet metadata, brukt når du ser på grupper av bilder som hovedtidslinjen",
|
"image_thumbnail_description": "Små miniatyrbilder med strippet metadata, brukt når du ser på grupper av bilder som hovedtidslinjen",
|
||||||
"image_thumbnail_quality_description": "Miniatyrbildekvalitet fra 1-100. Høyere er bedre, men produserer større filer og kan redusere appens respons.",
|
"image_thumbnail_quality_description": "Miniatyrbildekvalitet fra 1-100. Høyere er bedre, men produserer større filer og kan redusere appens respons.",
|
||||||
@ -371,6 +371,8 @@
|
|||||||
"admin_password": "Administrator Passord",
|
"admin_password": "Administrator Passord",
|
||||||
"administration": "Administrasjon",
|
"administration": "Administrasjon",
|
||||||
"advanced": "Avansert",
|
"advanced": "Avansert",
|
||||||
|
"advanced_settings_enable_alternate_media_filter_subtitle": "Bruk denne innstillingen for å filtrere mediefiler under synkronisering basert på alternative kriterier. Bruk kun denne innstillingen dersom man opplever problemer med at applikasjonen ikke oppdager alle album.",
|
||||||
|
"advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTELT] Bruk alternativ enhet album synk filter",
|
||||||
"advanced_settings_log_level_title": "Loggnivå: {}",
|
"advanced_settings_log_level_title": "Loggnivå: {}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "Noen enheter er veldige trege til å hente mikrobilder fra enheten. Aktiver denne innstillingen for å hente de eksternt istedenfor.",
|
"advanced_settings_prefer_remote_subtitle": "Noen enheter er veldige trege til å hente mikrobilder fra enheten. Aktiver denne innstillingen for å hente de eksternt istedenfor.",
|
||||||
"advanced_settings_prefer_remote_title": "Foretrekk eksterne bilder",
|
"advanced_settings_prefer_remote_title": "Foretrekk eksterne bilder",
|
||||||
@ -378,6 +380,8 @@
|
|||||||
"advanced_settings_proxy_headers_title": "Proxy headere",
|
"advanced_settings_proxy_headers_title": "Proxy headere",
|
||||||
"advanced_settings_self_signed_ssl_subtitle": "Hopper over SSL sertifikatverifikasjon for server-endepunkt. Påkrevet for selvsignerte sertifikater.",
|
"advanced_settings_self_signed_ssl_subtitle": "Hopper over SSL sertifikatverifikasjon for server-endepunkt. Påkrevet for selvsignerte sertifikater.",
|
||||||
"advanced_settings_self_signed_ssl_title": "Tillat selvsignerte SSL sertifikater",
|
"advanced_settings_self_signed_ssl_title": "Tillat selvsignerte SSL sertifikater",
|
||||||
|
"advanced_settings_sync_remote_deletions_subtitle": "Automatisk slette eller gjenopprette filer på denne enheten hvis den handlingen har blitt gjort på nettsiden",
|
||||||
|
"advanced_settings_sync_remote_deletions_title": "Synk sletting fra nettsiden [EKSPERIMENTELT]",
|
||||||
"advanced_settings_tile_subtitle": "Avanserte brukerinnstillinger",
|
"advanced_settings_tile_subtitle": "Avanserte brukerinnstillinger",
|
||||||
"advanced_settings_troubleshooting_subtitle": "Aktiver ekstra funksjoner for feilsøking",
|
"advanced_settings_troubleshooting_subtitle": "Aktiver ekstra funksjoner for feilsøking",
|
||||||
"advanced_settings_troubleshooting_title": "Feilsøking",
|
"advanced_settings_troubleshooting_title": "Feilsøking",
|
||||||
@ -992,6 +996,7 @@
|
|||||||
"filetype": "Filtype",
|
"filetype": "Filtype",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"filter_people": "Filtrer personer",
|
"filter_people": "Filtrer personer",
|
||||||
|
"filter_places": "Filtrer steder",
|
||||||
"find_them_fast": "Finn dem raskt ved søking av navn",
|
"find_them_fast": "Finn dem raskt ved søking av navn",
|
||||||
"fix_incorrect_match": "Fiks feilaktig match",
|
"fix_incorrect_match": "Fiks feilaktig match",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
@ -1282,6 +1287,7 @@
|
|||||||
"onboarding_welcome_user": "Velkommen, {user}",
|
"onboarding_welcome_user": "Velkommen, {user}",
|
||||||
"online": "Tilkoblet",
|
"online": "Tilkoblet",
|
||||||
"only_favorites": "Bare favoritter",
|
"only_favorites": "Bare favoritter",
|
||||||
|
"open": "Åpne",
|
||||||
"open_in_map_view": "Åpne i kartvisning",
|
"open_in_map_view": "Åpne i kartvisning",
|
||||||
"open_in_openstreetmap": "Åpne i OpenStreetMap",
|
"open_in_openstreetmap": "Åpne i OpenStreetMap",
|
||||||
"open_the_search_filters": "Åpne søkefiltrene",
|
"open_the_search_filters": "Åpne søkefiltrene",
|
||||||
@ -1426,6 +1432,8 @@
|
|||||||
"recent_searches": "Nylige søk",
|
"recent_searches": "Nylige søk",
|
||||||
"recently_added": "Nylig lagt til",
|
"recently_added": "Nylig lagt til",
|
||||||
"recently_added_page_title": "Nylig lagt til",
|
"recently_added_page_title": "Nylig lagt til",
|
||||||
|
"recently_taken": "Nylig tatt",
|
||||||
|
"recently_taken_page_title": "Nylig tatt",
|
||||||
"refresh": "Oppdater",
|
"refresh": "Oppdater",
|
||||||
"refresh_encoded_videos": "Oppdater kodete videoer",
|
"refresh_encoded_videos": "Oppdater kodete videoer",
|
||||||
"refresh_faces": "Oppdater ansikter",
|
"refresh_faces": "Oppdater ansikter",
|
||||||
|
@ -978,7 +978,7 @@
|
|||||||
"external": "Zunanji",
|
"external": "Zunanji",
|
||||||
"external_libraries": "Zunanje knjižnice",
|
"external_libraries": "Zunanje knjižnice",
|
||||||
"external_network": "Zunanje omrežje",
|
"external_network": "Zunanje omrežje",
|
||||||
"external_network_sheet_info": "Ko aplikacija ni v želenem omrežju WiFi, se bo povezala s strežnikom prek prvega od spodnjih URL-jev, ki jih lahko doseže, začenši od zgoraj navzdol",
|
"external_network_sheet_info": "Ko aplikacija ni v želenem omrežju Wi-Fi, se bo povezala s strežnikom prek prvega od spodnjih URL-jev, ki jih lahko doseže, začenši od zgoraj navzdol",
|
||||||
"face_unassigned": "Nedodeljen",
|
"face_unassigned": "Nedodeljen",
|
||||||
"failed": "Ni uspelo",
|
"failed": "Ni uspelo",
|
||||||
"failed_to_load_assets": "Sredstev ni bilo mogoče naložiti",
|
"failed_to_load_assets": "Sredstev ni bilo mogoče naložiti",
|
||||||
@ -1125,7 +1125,7 @@
|
|||||||
"local_network": "Lokalno omrežje",
|
"local_network": "Lokalno omrežje",
|
||||||
"local_network_sheet_info": "Aplikacija se bo povezala s strežnikom prek tega URL-ja, ko bo uporabljala navedeno omrežje Wi-Fi",
|
"local_network_sheet_info": "Aplikacija se bo povezala s strežnikom prek tega URL-ja, ko bo uporabljala navedeno omrežje Wi-Fi",
|
||||||
"location_permission": "Dovoljenje za lokacijo",
|
"location_permission": "Dovoljenje za lokacijo",
|
||||||
"location_permission_content": "Za uporabo funkcije samodejnega preklapljanja potrebuje Immich dovoljenje za natančno lokacijo, da lahko prebere ime trenutnega omrežja WiFi",
|
"location_permission_content": "Za uporabo funkcije samodejnega preklapljanja potrebuje Immich dovoljenje za natančno lokacijo, da lahko prebere ime trenutnega omrežja Wi-Fi",
|
||||||
"location_picker_choose_on_map": "Izberi na zemljevidu",
|
"location_picker_choose_on_map": "Izberi na zemljevidu",
|
||||||
"location_picker_latitude_error": "Vnesi veljavno zemljepisno širino",
|
"location_picker_latitude_error": "Vnesi veljavno zemljepisno širino",
|
||||||
"location_picker_latitude_hint": "Tukaj vnesi svojo zemljepisno širino",
|
"location_picker_latitude_hint": "Tukaj vnesi svojo zemljepisno širino",
|
||||||
@ -1432,6 +1432,8 @@
|
|||||||
"recent_searches": "Nedavna iskanja",
|
"recent_searches": "Nedavna iskanja",
|
||||||
"recently_added": "Nedavno dodano",
|
"recently_added": "Nedavno dodano",
|
||||||
"recently_added_page_title": "Nedavno dodano",
|
"recently_added_page_title": "Nedavno dodano",
|
||||||
|
"recently_taken": "Nedavno uporabljen",
|
||||||
|
"recently_taken_page_title": "Nedavno Uporabljen",
|
||||||
"refresh": "Osveži",
|
"refresh": "Osveži",
|
||||||
"refresh_encoded_videos": "Osveži kodirane videoposnetke",
|
"refresh_encoded_videos": "Osveži kodirane videoposnetke",
|
||||||
"refresh_faces": "Osveži obraze",
|
"refresh_faces": "Osveži obraze",
|
||||||
|
@ -378,6 +378,7 @@
|
|||||||
"advanced_settings_proxy_headers_title": "Proxy-headers",
|
"advanced_settings_proxy_headers_title": "Proxy-headers",
|
||||||
"advanced_settings_self_signed_ssl_subtitle": "Hoppar över SSL-certifikatverifiering för serverändpunkten. Krävs för självsignerade certifikat.",
|
"advanced_settings_self_signed_ssl_subtitle": "Hoppar över SSL-certifikatverifiering för serverändpunkten. Krävs för självsignerade certifikat.",
|
||||||
"advanced_settings_self_signed_ssl_title": "Tillåt självsignerade SSL-certifikat",
|
"advanced_settings_self_signed_ssl_title": "Tillåt självsignerade SSL-certifikat",
|
||||||
|
"advanced_settings_sync_remote_deletions_title": "Synkonisera fjärradering [EXPERIMENTELL]",
|
||||||
"advanced_settings_tile_subtitle": "Avancerade användarinställningar",
|
"advanced_settings_tile_subtitle": "Avancerade användarinställningar",
|
||||||
"advanced_settings_troubleshooting_subtitle": "Aktivera funktioner för felsökning",
|
"advanced_settings_troubleshooting_subtitle": "Aktivera funktioner för felsökning",
|
||||||
"advanced_settings_troubleshooting_title": "Felsökning",
|
"advanced_settings_troubleshooting_title": "Felsökning",
|
||||||
@ -992,6 +993,7 @@
|
|||||||
"filetype": "Filtyp",
|
"filetype": "Filtyp",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"filter_people": "Filtrera personer",
|
"filter_people": "Filtrera personer",
|
||||||
|
"filter_places": "Filtrera platser",
|
||||||
"find_them_fast": "Hitta dem snabbt efter namn med sök",
|
"find_them_fast": "Hitta dem snabbt efter namn med sök",
|
||||||
"fix_incorrect_match": "Fixa inkorrekt matchning",
|
"fix_incorrect_match": "Fixa inkorrekt matchning",
|
||||||
"folder": "Mapp",
|
"folder": "Mapp",
|
||||||
|
@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 193,
|
"android.injected.version.code" => 194,
|
||||||
"android.injected.version.name" => "1.131.3",
|
"android.injected.version.name" => "1.132.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Release"
|
desc "iOS Release"
|
||||||
lane :release do
|
lane :release do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.131.3"
|
version_number: "1.132.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
@ -2,11 +2,14 @@ import 'dart:async';
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
|
import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
/// The local image provider for an asset
|
/// The local image provider for an asset
|
||||||
/// Only viable
|
/// Only viable
|
||||||
@ -15,11 +18,16 @@ class ImmichLocalThumbnailProvider
|
|||||||
final Asset asset;
|
final Asset asset;
|
||||||
final int height;
|
final int height;
|
||||||
final int width;
|
final int width;
|
||||||
|
final CacheManager? cacheManager;
|
||||||
|
final Logger log = Logger("ImmichLocalThumbnailProvider");
|
||||||
|
final String? userId;
|
||||||
|
|
||||||
ImmichLocalThumbnailProvider({
|
ImmichLocalThumbnailProvider({
|
||||||
required this.asset,
|
required this.asset,
|
||||||
this.height = 256,
|
this.height = 256,
|
||||||
this.width = 256,
|
this.width = 256,
|
||||||
|
this.cacheManager,
|
||||||
|
this.userId,
|
||||||
}) : assert(asset.local != null, 'Only usable when asset.local is set');
|
}) : assert(asset.local != null, 'Only usable when asset.local is set');
|
||||||
|
|
||||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||||
@ -36,11 +44,10 @@ class ImmichLocalThumbnailProvider
|
|||||||
ImmichLocalThumbnailProvider key,
|
ImmichLocalThumbnailProvider key,
|
||||||
ImageDecoderCallback decode,
|
ImageDecoderCallback decode,
|
||||||
) {
|
) {
|
||||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||||
return MultiImageStreamCompleter(
|
return MultiImageStreamCompleter(
|
||||||
codec: _codec(key.asset, decode, chunkEvents),
|
codec: _codec(key.asset, cache, decode),
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
chunkEvents: chunkEvents.stream,
|
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription(key.asset.fileName);
|
yield ErrorDescription(key.asset.fileName);
|
||||||
},
|
},
|
||||||
@ -50,25 +57,38 @@ class ImmichLocalThumbnailProvider
|
|||||||
// Streams in each stage of the image as we ask for it
|
// Streams in each stage of the image as we ask for it
|
||||||
Stream<ui.Codec> _codec(
|
Stream<ui.Codec> _codec(
|
||||||
Asset assetData,
|
Asset assetData,
|
||||||
|
CacheManager cache,
|
||||||
ImageDecoderCallback decode,
|
ImageDecoderCallback decode,
|
||||||
StreamController<ImageChunkEvent> chunkEvents,
|
|
||||||
) async* {
|
) async* {
|
||||||
final thumbBytes = await assetData.local
|
final cacheKey =
|
||||||
?.thumbnailDataWithSize(ThumbnailSize(width, height));
|
'$userId${assetData.localId}${assetData.checksum}$width$height';
|
||||||
if (thumbBytes == null) {
|
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
||||||
chunkEvents.close();
|
if (fileFromCache != null) {
|
||||||
|
try {
|
||||||
|
final buffer =
|
||||||
|
await ui.ImmutableBuffer.fromFilePath(fileFromCache.file.path);
|
||||||
|
final codec = await decode(buffer);
|
||||||
|
yield codec;
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
log.severe('Found thumbnail in cache, but loading it failed', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final thumbnailBytes = await assetData.local?.thumbnailDataWithSize(
|
||||||
|
ThumbnailSize(width, height),
|
||||||
|
quality: 80,
|
||||||
|
);
|
||||||
|
if (thumbnailBytes == null) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
"Loading thumb for local photo ${asset.fileName} failed",
|
"Loading thumb for local photo ${assetData.fileName} failed",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
|
||||||
final codec = await decode(buffer);
|
final codec = await decode(buffer);
|
||||||
yield codec;
|
yield codec;
|
||||||
} finally {
|
await cache.putFile(cacheKey, thumbnailBytes);
|
||||||
chunkEvents.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
|
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
|
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
@ -9,8 +9,9 @@ import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||||
import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart';
|
import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart';
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
class ImmichThumbnail extends HookWidget {
|
class ImmichThumbnail extends HookConsumerWidget {
|
||||||
const ImmichThumbnail({
|
const ImmichThumbnail({
|
||||||
this.asset,
|
this.asset,
|
||||||
this.width = 250,
|
this.width = 250,
|
||||||
@ -31,6 +32,7 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
static ImageProvider imageProvider({
|
static ImageProvider imageProvider({
|
||||||
Asset? asset,
|
Asset? asset,
|
||||||
String? assetId,
|
String? assetId,
|
||||||
|
String? userId,
|
||||||
int thumbnailSize = 256,
|
int thumbnailSize = 256,
|
||||||
}) {
|
}) {
|
||||||
if (asset == null && assetId == null) {
|
if (asset == null && assetId == null) {
|
||||||
@ -48,6 +50,7 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
asset: asset,
|
asset: asset,
|
||||||
height: thumbnailSize,
|
height: thumbnailSize,
|
||||||
width: thumbnailSize,
|
width: thumbnailSize,
|
||||||
|
userId: userId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ImmichRemoteThumbnailProvider(
|
return ImmichRemoteThumbnailProvider(
|
||||||
@ -59,8 +62,10 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
Uint8List? blurhash = useBlurHashRef(asset).value;
|
Uint8List? blurhash = useBlurHashRef(asset).value;
|
||||||
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
|
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
@ -79,6 +84,7 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
octoSet: blurHashOrPlaceholder(blurhash),
|
octoSet: blurHashOrPlaceholder(blurhash),
|
||||||
image: ImmichThumbnail.imageProvider(
|
image: ImmichThumbnail.imageProvider(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
|
userId: userId,
|
||||||
),
|
),
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.131.3
|
- API version: 1.132.0
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
71
mobile/openapi/lib/model/o_auth_callback_dto.dart
generated
71
mobile/openapi/lib/model/o_auth_callback_dto.dart
generated
@ -13,37 +13,58 @@ part of openapi.api;
|
|||||||
class OAuthCallbackDto {
|
class OAuthCallbackDto {
|
||||||
/// Returns a new [OAuthCallbackDto] instance.
|
/// Returns a new [OAuthCallbackDto] instance.
|
||||||
OAuthCallbackDto({
|
OAuthCallbackDto({
|
||||||
|
this.codeVerifier,
|
||||||
|
this.state,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.state,
|
|
||||||
required this.codeVerifier,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? codeVerifier;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? state;
|
||||||
|
|
||||||
String url;
|
String url;
|
||||||
String state;
|
|
||||||
String codeVerifier;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) => identical(this, other) || other is OAuthCallbackDto &&
|
||||||
identical(this, other) ||
|
other.codeVerifier == codeVerifier &&
|
||||||
other is OAuthCallbackDto &&
|
|
||||||
other.url == url &&
|
|
||||||
other.state == state &&
|
other.state == state &&
|
||||||
other.codeVerifier == codeVerifier;
|
other.url == url;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(url.hashCode) + (state.hashCode) + (codeVerifier.hashCode);
|
(codeVerifier == null ? 0 : codeVerifier!.hashCode) +
|
||||||
|
(state == null ? 0 : state!.hashCode) +
|
||||||
|
(url.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() => 'OAuthCallbackDto[codeVerifier=$codeVerifier, state=$state, url=$url]';
|
||||||
'OAuthCallbackDto[url=$url, state=$state, codeVerifier=$codeVerifier]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'url'] = this.url;
|
if (this.codeVerifier != null) {
|
||||||
json[r'state'] = this.state;
|
|
||||||
json[r'codeVerifier'] = this.codeVerifier;
|
json[r'codeVerifier'] = this.codeVerifier;
|
||||||
|
} else {
|
||||||
|
// json[r'codeVerifier'] = null;
|
||||||
|
}
|
||||||
|
if (this.state != null) {
|
||||||
|
json[r'state'] = this.state;
|
||||||
|
} else {
|
||||||
|
// json[r'state'] = null;
|
||||||
|
}
|
||||||
|
json[r'url'] = this.url;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,18 +77,15 @@ class OAuthCallbackDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return OAuthCallbackDto(
|
return OAuthCallbackDto(
|
||||||
|
codeVerifier: mapValueOfType<String>(json, r'codeVerifier'),
|
||||||
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
url: mapValueOfType<String>(json, r'url')!,
|
url: mapValueOfType<String>(json, r'url')!,
|
||||||
state: mapValueOfType<String>(json, r'state')!,
|
|
||||||
codeVerifier: mapValueOfType<String>(json, r'codeVerifier')!,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<OAuthCallbackDto> listFromJson(
|
static List<OAuthCallbackDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final result = <OAuthCallbackDto>[];
|
final result = <OAuthCallbackDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
@ -95,19 +113,13 @@ class OAuthCallbackDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of OAuthCallbackDto-objects as value to a dart map
|
// maps a json object with a list of OAuthCallbackDto-objects as value to a dart map
|
||||||
static Map<String, List<OAuthCallbackDto>> mapListFromJson(
|
static Map<String, List<OAuthCallbackDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final map = <String, List<OAuthCallbackDto>>{};
|
final map = <String, List<OAuthCallbackDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
json = json.cast<String, dynamic>();
|
json = json.cast<String, dynamic>();
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
map[entry.key] = OAuthCallbackDto.listFromJson(
|
map[entry.key] = OAuthCallbackDto.listFromJson(entry.value, growable: growable,);
|
||||||
entry.value,
|
|
||||||
growable: growable,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@ -116,7 +128,6 @@ class OAuthCallbackDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'url',
|
'url',
|
||||||
'state',
|
|
||||||
'codeVerifier',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
71
mobile/openapi/lib/model/o_auth_config_dto.dart
generated
71
mobile/openapi/lib/model/o_auth_config_dto.dart
generated
@ -13,37 +13,58 @@ part of openapi.api;
|
|||||||
class OAuthConfigDto {
|
class OAuthConfigDto {
|
||||||
/// Returns a new [OAuthConfigDto] instance.
|
/// Returns a new [OAuthConfigDto] instance.
|
||||||
OAuthConfigDto({
|
OAuthConfigDto({
|
||||||
|
this.codeChallenge,
|
||||||
required this.redirectUri,
|
required this.redirectUri,
|
||||||
required this.state,
|
this.state,
|
||||||
required this.codeChallenge,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? codeChallenge;
|
||||||
|
|
||||||
String redirectUri;
|
String redirectUri;
|
||||||
String state;
|
|
||||||
String codeChallenge;
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) => identical(this, other) || other is OAuthConfigDto &&
|
||||||
identical(this, other) ||
|
other.codeChallenge == codeChallenge &&
|
||||||
other is OAuthConfigDto &&
|
|
||||||
other.redirectUri == redirectUri &&
|
other.redirectUri == redirectUri &&
|
||||||
other.state == state &&
|
other.state == state;
|
||||||
other.codeChallenge == codeChallenge;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(redirectUri.hashCode) + (state.hashCode) + (codeChallenge.hashCode);
|
(codeChallenge == null ? 0 : codeChallenge!.hashCode) +
|
||||||
|
(redirectUri.hashCode) +
|
||||||
|
(state == null ? 0 : state!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() => 'OAuthConfigDto[codeChallenge=$codeChallenge, redirectUri=$redirectUri, state=$state]';
|
||||||
'OAuthConfigDto[redirectUri=$redirectUri, state=$state, codeChallenge=$codeChallenge]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'redirectUri'] = this.redirectUri;
|
if (this.codeChallenge != null) {
|
||||||
json[r'state'] = this.state;
|
|
||||||
json[r'codeChallenge'] = this.codeChallenge;
|
json[r'codeChallenge'] = this.codeChallenge;
|
||||||
|
} else {
|
||||||
|
// json[r'codeChallenge'] = null;
|
||||||
|
}
|
||||||
|
json[r'redirectUri'] = this.redirectUri;
|
||||||
|
if (this.state != null) {
|
||||||
|
json[r'state'] = this.state;
|
||||||
|
} else {
|
||||||
|
// json[r'state'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,18 +77,15 @@ class OAuthConfigDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return OAuthConfigDto(
|
return OAuthConfigDto(
|
||||||
|
codeChallenge: mapValueOfType<String>(json, r'codeChallenge'),
|
||||||
redirectUri: mapValueOfType<String>(json, r'redirectUri')!,
|
redirectUri: mapValueOfType<String>(json, r'redirectUri')!,
|
||||||
state: mapValueOfType<String>(json, r'state')!,
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
codeChallenge: mapValueOfType<String>(json, r'codeChallenge')!,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<OAuthConfigDto> listFromJson(
|
static List<OAuthConfigDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final result = <OAuthConfigDto>[];
|
final result = <OAuthConfigDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
@ -95,19 +113,13 @@ class OAuthConfigDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of OAuthConfigDto-objects as value to a dart map
|
// maps a json object with a list of OAuthConfigDto-objects as value to a dart map
|
||||||
static Map<String, List<OAuthConfigDto>> mapListFromJson(
|
static Map<String, List<OAuthConfigDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final map = <String, List<OAuthConfigDto>>{};
|
final map = <String, List<OAuthConfigDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
json = json.cast<String, dynamic>();
|
json = json.cast<String, dynamic>();
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
map[entry.key] = OAuthConfigDto.listFromJson(
|
map[entry.key] = OAuthConfigDto.listFromJson(entry.value, growable: growable,);
|
||||||
entry.value,
|
|
||||||
growable: growable,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@ -116,7 +128,6 @@ class OAuthConfigDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'redirectUri',
|
'redirectUri',
|
||||||
'state',
|
|
||||||
'codeChallenge',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.131.3+193
|
version: 1.132.0+194
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
@ -7656,7 +7656,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.131.3
|
* 1.132.0
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@ -687,17 +687,17 @@ export type TestEmailResponseDto = {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
};
|
};
|
||||||
export type OAuthConfigDto = {
|
export type OAuthConfigDto = {
|
||||||
|
codeChallenge?: string;
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
codeChallenge?: string;
|
|
||||||
};
|
};
|
||||||
export type OAuthAuthorizeResponseDto = {
|
export type OAuthAuthorizeResponseDto = {
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
export type OAuthCallbackDto = {
|
export type OAuthCallbackDto = {
|
||||||
url: string;
|
|
||||||
state?: string;
|
|
||||||
codeVerifier?: string;
|
codeVerifier?: string;
|
||||||
|
state?: string;
|
||||||
|
url: string;
|
||||||
};
|
};
|
||||||
export type PartnerResponseDto = {
|
export type PartnerResponseDto = {
|
||||||
avatarColor: UserAvatarColor;
|
avatarColor: UserAvatarColor;
|
||||||
|
1949
server/package-lock.json
generated
1949
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -17,12 +17,12 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
|
|||||||
import { repositories } from 'src/repositories';
|
import { repositories } from 'src/repositories';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { JobRepository } from 'src/repositories/job.repository';
|
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||||
import { services } from 'src/services';
|
import { services } from 'src/services';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { CliService } from 'src/services/cli.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
|
import { JobService } from 'src/services/job.service';
|
||||||
import { getKyselyConfig } from 'src/utils/database';
|
import { getKyselyConfig } from 'src/utils/database';
|
||||||
|
|
||||||
const common = [...repositories, ...services, GlobalExceptionFilter];
|
const common = [...repositories, ...services, GlobalExceptionFilter];
|
||||||
@ -52,7 +52,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
|||||||
@Inject(IWorker) private worker: ImmichWorker,
|
@Inject(IWorker) private worker: ImmichWorker,
|
||||||
logger: LoggingRepository,
|
logger: LoggingRepository,
|
||||||
private eventRepository: EventRepository,
|
private eventRepository: EventRepository,
|
||||||
private jobRepository: JobRepository,
|
private jobService: JobService,
|
||||||
private telemetryRepository: TelemetryRepository,
|
private telemetryRepository: TelemetryRepository,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
) {
|
) {
|
||||||
@ -62,10 +62,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
|||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
this.telemetryRepository.setup({ repositories });
|
this.telemetryRepository.setup({ repositories });
|
||||||
|
|
||||||
this.jobRepository.setup({ services });
|
this.jobService.setServices(services);
|
||||||
if (this.worker === ImmichWorker.MICROSERVICES) {
|
|
||||||
this.jobRepository.startWorkers();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.eventRepository.setAuthFn(async (client) =>
|
this.eventRepository.setAuthFn(async (client) =>
|
||||||
this.authService.authenticate({
|
this.authService.authenticate({
|
||||||
|
@ -407,6 +407,8 @@ export enum DatabaseExtension {
|
|||||||
export enum BootstrapEventPriority {
|
export enum BootstrapEventPriority {
|
||||||
// Database service should be initialized before anything else, most other services need database access
|
// Database service should be initialized before anything else, most other services need database access
|
||||||
DatabaseService = -200,
|
DatabaseService = -200,
|
||||||
|
// Other services may need to queue jobs on bootstrap.
|
||||||
|
JobService = -190,
|
||||||
// Initialise config after other bootstrap services, stop other services from using config on bootstrap
|
// Initialise config after other bootstrap services, stop other services from using config on bootstrap
|
||||||
SystemConfig = 100,
|
SystemConfig = 100,
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ export class JobRepository {
|
|||||||
this.logger.setContext(JobRepository.name);
|
this.logger.setContext(JobRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup({ services }: { services: ClassConstructor<unknown>[] }) {
|
setup(services: ClassConstructor<unknown>[]) {
|
||||||
const reflector = this.moduleRef.get(Reflector, { strict: false });
|
const reflector = this.moduleRef.get(Reflector, { strict: false });
|
||||||
|
|
||||||
// discovery
|
// discovery
|
||||||
|
@ -73,26 +73,54 @@ export class ServerInfoRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildVersions?: ServerBuildVersions;
|
||||||
|
|
||||||
|
private async retrieveVersionFallback(
|
||||||
|
command: string,
|
||||||
|
commandTransform?: (output: string) => string,
|
||||||
|
version?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (!version) {
|
||||||
|
const output = await maybeFirstLine(command);
|
||||||
|
version = commandTransform ? commandTransform(output) : output;
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
async getBuildVersions(): Promise<ServerBuildVersions> {
|
async getBuildVersions(): Promise<ServerBuildVersions> {
|
||||||
|
if (!this.buildVersions) {
|
||||||
const { nodeVersion, resourcePaths } = this.configRepository.getEnv();
|
const { nodeVersion, resourcePaths } = this.configRepository.getEnv();
|
||||||
|
|
||||||
const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([
|
const lockfile: BuildLockfile | undefined = await readFile(resourcePaths.lockFile)
|
||||||
maybeFirstLine('node --version'),
|
|
||||||
maybeFirstLine('ffmpeg -version'),
|
|
||||||
maybeFirstLine('convert --version'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const lockfile = await readFile(resourcePaths.lockFile)
|
|
||||||
.then((buffer) => JSON.parse(buffer.toString()))
|
.then((buffer) => JSON.parse(buffer.toString()))
|
||||||
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
|
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
|
||||||
|
|
||||||
return {
|
const [nodejsVersion, ffmpegVersion, magickVersion, exiftoolVersion] = await Promise.all([
|
||||||
nodejs: nodejsOutput || nodeVersion || '',
|
this.retrieveVersionFallback('node --version', undefined, nodeVersion),
|
||||||
exiftool: await exiftool.version(),
|
this.retrieveVersionFallback(
|
||||||
ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '',
|
'ffmpeg -version',
|
||||||
libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips,
|
(output) => output.replaceAll('ffmpeg version ', ''),
|
||||||
imagemagick:
|
getLockfileVersion('ffmpeg', lockfile),
|
||||||
getLockfileVersion('imagemagick', lockfile) || magickOutput.replaceAll('Version: ImageMagick ', '') || '',
|
),
|
||||||
|
this.retrieveVersionFallback(
|
||||||
|
'magick --version',
|
||||||
|
(output) => output.replaceAll('Version: ImageMagick ', ''),
|
||||||
|
getLockfileVersion('imagemagick', lockfile),
|
||||||
|
),
|
||||||
|
exiftool.version(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const libvipsVersion = getLockfileVersion('libvips', lockfile) || sharp.versions.vips;
|
||||||
|
|
||||||
|
this.buildVersions = {
|
||||||
|
nodejs: nodejsVersion,
|
||||||
|
exiftool: exiftoolVersion,
|
||||||
|
ffmpeg: ffmpegVersion,
|
||||||
|
libvips: libvipsVersion,
|
||||||
|
imagemagick: magickVersion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.buildVersions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -772,9 +772,13 @@ describe(AuthService.name, () => {
|
|||||||
mocks.user.update.mockResolvedValue(user);
|
mocks.user.update.mockResolvedValue(user);
|
||||||
mocks.session.create.mockResolvedValue(factory.session());
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(
|
||||||
oauthResponse(user),
|
sut.callback(
|
||||||
);
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
expect(mocks.user.update).toHaveBeenCalledWith(user.id, {
|
expect(mocks.user.update).toHaveBeenCalledWith(user.id, {
|
||||||
profileImagePath: `upload/profile/${user.id}/${fileId}.jpg`,
|
profileImagePath: `upload/profile/${user.id}/${fileId}.jpg`,
|
||||||
@ -796,9 +800,13 @@ describe(AuthService.name, () => {
|
|||||||
mocks.user.update.mockResolvedValue(user);
|
mocks.user.update.mockResolvedValue(user);
|
||||||
mocks.session.create.mockResolvedValue(factory.session());
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(
|
||||||
oauthResponse(user),
|
sut.callback(
|
||||||
);
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||||
expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled();
|
expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled();
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { ClassConstructor } from 'class-transformer';
|
||||||
import { snakeCase } from 'lodash';
|
import { snakeCase } from 'lodash';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||||
import {
|
import {
|
||||||
AssetType,
|
AssetType,
|
||||||
|
BootstrapEventPriority,
|
||||||
ImmichWorker,
|
ImmichWorker,
|
||||||
JobCommand,
|
JobCommand,
|
||||||
JobName,
|
JobName,
|
||||||
@ -51,6 +53,8 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JobService extends BaseService {
|
export class JobService extends BaseService {
|
||||||
|
private services: ClassConstructor<unknown>[] = [];
|
||||||
|
|
||||||
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
|
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
|
||||||
onConfigInit({ newConfig: config }: ArgOf<'config.init'>) {
|
onConfigInit({ newConfig: config }: ArgOf<'config.init'>) {
|
||||||
this.logger.debug(`Updating queue concurrency settings`);
|
this.logger.debug(`Updating queue concurrency settings`);
|
||||||
@ -69,6 +73,18 @@ export class JobService extends BaseService {
|
|||||||
this.onConfigInit({ newConfig: config });
|
this.onConfigInit({ newConfig: config });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService })
|
||||||
|
onBootstrap() {
|
||||||
|
this.jobRepository.setup(this.services);
|
||||||
|
if (this.worker === ImmichWorker.MICROSERVICES) {
|
||||||
|
this.jobRepository.startWorkers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setServices(services: ClassConstructor<unknown>[]) {
|
||||||
|
this.services = services;
|
||||||
|
}
|
||||||
|
|
||||||
async create(dto: JobCreateDto): Promise<void> {
|
async create(dto: JobCreateDto): Promise<void> {
|
||||||
await this.jobRepository.queue(asJobItem(dto));
|
await this.jobRepository.queue(asJobItem(dto));
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,6 @@ export default defineConfig({
|
|||||||
'src/services/index.ts',
|
'src/services/index.ts',
|
||||||
'src/sql-tools/from-database/index.ts',
|
'src/sql-tools/from-database/index.ts',
|
||||||
],
|
],
|
||||||
thresholds: {
|
|
||||||
lines: 85,
|
|
||||||
statements: 85,
|
|
||||||
branches: 90,
|
|
||||||
functions: 85,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
deps: {
|
deps: {
|
||||||
|
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
@ -82,7 +82,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.131.3",
|
"version": "1.132.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
<div class="inline-block" use:resizeObserver={({ width }) => (innerWidth = width)}>
|
<div class="inline-block" use:resizeObserver={({ width }) => (innerWidth = width)}>
|
||||||
{#each memoryStore.memories as memory (memory.id)}
|
{#each memoryStore.memories as memory (memory.id)}
|
||||||
<a
|
<a
|
||||||
class="memory-card relative mr-8 last:mr-0 inline-block aspect-[3/4] md:aspect-[4/3] max-md:h-[150px] xl:aspect-video h-[215px] rounded-xl"
|
class="memory-card relative mr-2 md:mr-4 last:mr-0 inline-block aspect-[3/4] md:aspect-[4/3] max-md:h-[150px] xl:aspect-video h-[215px] rounded-xl"
|
||||||
href="{AppRoute.MEMORY}?{QueryParameter.ID}={memory.assets[0].id}"
|
href="{AppRoute.MEMORY}?{QueryParameter.ID}={memory.assets[0].id}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
Loading…
x
Reference in New Issue
Block a user