Merge remote-tracking branch 'origin/main' into keynav_timeline

This commit is contained in:
Min Idzelis 2025-04-23 20:08:56 +00:00
commit a332622730
44 changed files with 425 additions and 2073 deletions

View File

@ -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) }}"

View File

@ -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,

View File

@ -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: {}

View File

@ -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: {}

View File

@ -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: {}

View File

@ -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 }}

View File

@ -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

View File

@ -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
View File

@ -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": {

View File

@ -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",

View File

@ -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
View File

@ -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": {

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "얼굴 새로고침",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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')

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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',
}; };
} }

View File

@ -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',
}; };
} }

View File

@ -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'

View File

@ -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": [],

View File

@ -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"

View File

@ -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",

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.131.3", "version": "1.132.0",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@ -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({

View File

@ -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,
} }

View File

@ -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

View File

@ -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;
}
} }

View File

@ -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();

View File

@ -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));
} }

View File

@ -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
View File

@ -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"

View File

@ -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": {

View File

@ -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