mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge branch 'main' into improve_focus
This commit is contained in:
commit
6e06f9f5aa
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@ -224,7 +224,7 @@ jobs:
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
run: | # zizmor: ignore[template-injection]
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
@ -426,7 +426,7 @@ jobs:
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
run: | # zizmor: ignore[template-injection]
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
@ -535,6 +535,7 @@ jobs:
|
||||
run: exit 1
|
||||
- name: All jobs passed or skipped
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
# zizmor: ignore[template-injection]
|
||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||
|
||||
success-check-ml:
|
||||
@ -549,4 +550,5 @@ jobs:
|
||||
run: exit 1
|
||||
- name: All jobs passed or skipped
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
# zizmor: ignore[template-injection]
|
||||
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
|
||||
on:
|
||||
workflow_run:
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||
workflows: ['Docs build']
|
||||
types:
|
||||
- completed
|
||||
@ -115,22 +115,22 @@ jobs:
|
||||
- name: Load parameters
|
||||
id: parameters
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
env:
|
||||
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
||||
with:
|
||||
script: |
|
||||
const json = `${{ needs.checks.outputs.parameters }}`;
|
||||
const parameters = JSON.parse(json);
|
||||
const parameters = JSON.parse(process.env.PARAM_JSON);
|
||||
core.setOutput("event", parameters.event);
|
||||
core.setOutput("name", parameters.name);
|
||||
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
||||
|
||||
- run: |
|
||||
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
env:
|
||||
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
||||
with:
|
||||
script: |
|
||||
let artifact = ${{ needs.checks.outputs.artifact }};
|
||||
let artifact = JSON.parse(process.env.ARTIFACT_JSON);
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
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
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||
types: [closed]
|
||||
|
||||
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
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||
types: [opened, labeled, unlabeled, synchronize]
|
||||
|
||||
permissions: {}
|
||||
|
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
@ -1,6 +1,6 @@
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
- pull_request_target
|
||||
- pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
id: push-tag
|
||||
@ -61,6 +64,8 @@ jobs:
|
||||
build_mobile:
|
||||
uses: ./.github/workflows/build-mobile.yml
|
||||
needs: bump_version
|
||||
permissions:
|
||||
contents: read
|
||||
secrets:
|
||||
KEY_JKS: ${{ secrets.KEY_JKS }}
|
||||
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
|
||||
run: dart run custom_lint
|
||||
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
|
||||
- name: All jobs passed or skipped
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
# zizmor: ignore[template-injection]
|
||||
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",
|
||||
"version": "2.2.61",
|
||||
"version": "2.2.63",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.61",
|
||||
"version": "2.2.63",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
@ -54,7 +54,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.61",
|
||||
"version": "2.2.63",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
|
8
docs/static/archived-versions.json
vendored
8
docs/static/archived-versions.json
vendored
@ -1,4 +1,12 @@
|
||||
[
|
||||
{
|
||||
"label": "v1.132.1",
|
||||
"url": "https://v1.132.1.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.132.0",
|
||||
"url": "https://v1.132.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.131.3",
|
||||
"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",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
@ -44,7 +44,7 @@
|
||||
},
|
||||
"../cli": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.61",
|
||||
"version": "2.2.63",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
@ -93,7 +93,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"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_reenable": "Para reactivarlo, utiliza un <link>Comando del servidor</link>.",
|
||||
"background_task_job": "Tareas en segundo plano",
|
||||
"backup_database": "Respaldar base de datos",
|
||||
"backup_database_enable_description": "Activar respaldo de base de datos",
|
||||
"backup_keep_last_amount": "Cantidad de respaldos previos a mantener",
|
||||
"backup_settings": "Ajustes de respaldo",
|
||||
"backup_settings_description": "Administrar configuración de respaldo de base de datos",
|
||||
"backup_database": "Crear volcado de base de datos",
|
||||
"backup_database_enable_description": "Activar volcado de base de datos",
|
||||
"backup_keep_last_amount": "Cantidad de volcados previos a mantener",
|
||||
"backup_settings": "Ajustes de volcado 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",
|
||||
"cleanup": "Limpieza",
|
||||
"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_title": "Ajustes de las miniaturas",
|
||||
"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_settings": "Configuración tareas",
|
||||
"job_settings": "Configuración de tareas",
|
||||
"job_settings_description": "Administrar tareas simultáneas",
|
||||
"job_status": "Estado de la tarea",
|
||||
"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",
|
||||
"no_paths_added": "No se han añadido carpetas",
|
||||
"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!",
|
||||
"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>\"",
|
||||
@ -252,12 +252,12 @@
|
||||
"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_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_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_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",
|
||||
"system_settings": "Ajustes del Sistema",
|
||||
"tag_cleanup_job": "Limpieza de etiquetas",
|
||||
@ -345,7 +345,7 @@
|
||||
"trash_settings": "Configuración papelera",
|
||||
"trash_settings_description": "Administrar la configuración de la papelera",
|
||||
"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_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",
|
||||
@ -429,7 +429,7 @@
|
||||
"allow_dark_mode": "Permitir modo oscuro",
|
||||
"allow_edits": "Permitir edición",
|
||||
"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",
|
||||
"anti_clockwise": "En sentido antihorario",
|
||||
"api_key": "Clave API",
|
||||
@ -473,7 +473,7 @@
|
||||
"asset_skipped": "Omitido",
|
||||
"asset_skipped_in_trash": "En la papelera",
|
||||
"asset_uploaded": "Subido",
|
||||
"asset_uploading": "Cargando…",
|
||||
"asset_uploading": "Subiendo…",
|
||||
"asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos",
|
||||
"asset_viewer_settings_title": "Visor de Archivos",
|
||||
"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_count": "{count, plural, one {# activo} other {# activos}}",
|
||||
"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_permanently_deleted_count": "Eliminado permanentemente {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_trashed": "{} elemento(s) eliminado(s)",
|
||||
"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",
|
||||
"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",
|
||||
@ -510,11 +510,11 @@
|
||||
"backup_all": "Todos",
|
||||
"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_current_upload_notification": "Cargando {}",
|
||||
"backup_background_service_current_upload_notification": "Subiendo {}",
|
||||
"backup_background_service_default_notification": "Comprobando nuevos elementos…",
|
||||
"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_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_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",
|
||||
@ -536,7 +536,7 @@
|
||||
"backup_controller_page_backup_selected": "Seleccionado: ",
|
||||
"backup_controller_page_backup_sub": "Fotos y videos respaldados",
|
||||
"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_failed": "Fallidos ({})",
|
||||
"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_turn_off": "Apagar 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_info_card_assets": "elementos",
|
||||
"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_title": "Estado de la subida",
|
||||
"backup_options_page_title": "Opciones de Copia de Seguridad",
|
||||
@ -767,7 +767,7 @@
|
||||
"download_enqueue": "Descarga en cola",
|
||||
"download_error": "Error al descargar",
|
||||
"download_failed": "Descarga fallida",
|
||||
"download_filename": "Archivo: {}",
|
||||
"download_filename": "archivo: {}",
|
||||
"download_finished": "Descarga completada",
|
||||
"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",
|
||||
@ -978,7 +978,7 @@
|
||||
"external": "Externo",
|
||||
"external_libraries": "Bibliotecas Externas",
|
||||
"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",
|
||||
"failed": "Fallido",
|
||||
"failed_to_load_assets": "Error al cargar los activos",
|
||||
@ -1125,7 +1125,7 @@
|
||||
"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",
|
||||
"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_latitude_error": "Introduce una latitud válida",
|
||||
"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",
|
||||
"not_in_any_album": "Sin álbum",
|
||||
"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",
|
||||
"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.",
|
||||
@ -1432,6 +1432,8 @@
|
||||
"recent_searches": "Búsquedas recientes",
|
||||
"recently_added": "Añadidos recientemente",
|
||||
"recently_added_page_title": "Recién Agregadas",
|
||||
"recently_taken": "Recientemente tomado",
|
||||
"recently_taken_page_title": "Recientemente Tomado",
|
||||
"refresh": "Actualizar",
|
||||
"refresh_encoded_videos": "Recargar los vídeos codificados",
|
||||
"refresh_faces": "Actualizar caras",
|
||||
@ -1615,7 +1617,7 @@
|
||||
"settings_saved": "Ajustes guardados",
|
||||
"share": "Compartir",
|
||||
"share_add_photos": "Agregar fotos",
|
||||
"share_assets_selected": "{} seleccionados",
|
||||
"share_assets_selected": "{} seleccionado(s)",
|
||||
"share_dialog_preparing": "Preparando...",
|
||||
"shared": "Compartido",
|
||||
"shared_album_activities_input_disable": "Los comentarios están deshabilitados",
|
||||
@ -1629,7 +1631,7 @@
|
||||
"shared_by_user": "Compartido por {user}",
|
||||
"shared_by_you": "Compartido por ti",
|
||||
"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_clipboard_copied_massage": "Copiado al portapapeles",
|
||||
"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_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_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_restore_all": "Restaurar todos",
|
||||
"trash_page_select_assets_btn": "Seleccionar elementos",
|
||||
@ -1818,22 +1820,22 @@
|
||||
"unstack": "Desapilar",
|
||||
"unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}",
|
||||
"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",
|
||||
"updated_password": "Contraseña actualizada",
|
||||
"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_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_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}",
|
||||
"upload_status_duplicates": "Duplicados",
|
||||
"upload_status_errors": "Errores",
|
||||
"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 ({})",
|
||||
"uploading": "Cargando",
|
||||
"uploading": "Subiendo",
|
||||
"url": "URL",
|
||||
"usage": "Uso",
|
||||
"use_current_connection": "Usar conexión actual",
|
||||
|
@ -915,6 +915,8 @@
|
||||
"hide_unnamed_people": "Sakrij neimenovane osobe",
|
||||
"host": "Domaćin",
|
||||
"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_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}",
|
||||
@ -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_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_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_web_interface": "Immich Web Sučelje",
|
||||
"import_from_json": "Uvoz iz JSON-a",
|
||||
|
@ -1432,6 +1432,8 @@
|
||||
"recent_searches": "Pencarian terkini",
|
||||
"recently_added": "Recently added",
|
||||
"recently_added_page_title": "Baru Ditambahkan",
|
||||
"recently_taken": "Diambil terkini",
|
||||
"recently_taken_page_title": "Diambil Terkini",
|
||||
"refresh": "Segarkan",
|
||||
"refresh_encoded_videos": "Segarkan video terenkode",
|
||||
"refresh_faces": "Segarkan wajah",
|
||||
|
@ -1432,6 +1432,7 @@
|
||||
"recent_searches": "최근 검색",
|
||||
"recently_added": "최근 추가",
|
||||
"recently_added_page_title": "최근 추가",
|
||||
"recently_taken": "최근 촬영됨",
|
||||
"refresh": "새로고침",
|
||||
"refresh_encoded_videos": "동영상 재인코딩",
|
||||
"refresh_faces": "얼굴 새로고침",
|
||||
|
@ -85,7 +85,7 @@
|
||||
"image_quality": "Kvalitet",
|
||||
"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_settings": "Bildeinnstilliinger",
|
||||
"image_settings": "Bildeinnstillinger",
|
||||
"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_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",
|
||||
"administration": "Administrasjon",
|
||||
"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_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",
|
||||
@ -378,6 +380,8 @@
|
||||
"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_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_troubleshooting_subtitle": "Aktiver ekstra funksjoner for feilsøking",
|
||||
"advanced_settings_troubleshooting_title": "Feilsøking",
|
||||
@ -992,6 +996,7 @@
|
||||
"filetype": "Filtype",
|
||||
"filter": "Filter",
|
||||
"filter_people": "Filtrer personer",
|
||||
"filter_places": "Filtrer steder",
|
||||
"find_them_fast": "Finn dem raskt ved søking av navn",
|
||||
"fix_incorrect_match": "Fiks feilaktig match",
|
||||
"folder": "Folder",
|
||||
@ -1282,6 +1287,7 @@
|
||||
"onboarding_welcome_user": "Velkommen, {user}",
|
||||
"online": "Tilkoblet",
|
||||
"only_favorites": "Bare favoritter",
|
||||
"open": "Åpne",
|
||||
"open_in_map_view": "Åpne i kartvisning",
|
||||
"open_in_openstreetmap": "Åpne i OpenStreetMap",
|
||||
"open_the_search_filters": "Åpne søkefiltrene",
|
||||
@ -1426,6 +1432,8 @@
|
||||
"recent_searches": "Nylige søk",
|
||||
"recently_added": "Nylig lagt til",
|
||||
"recently_added_page_title": "Nylig lagt til",
|
||||
"recently_taken": "Nylig tatt",
|
||||
"recently_taken_page_title": "Nylig tatt",
|
||||
"refresh": "Oppdater",
|
||||
"refresh_encoded_videos": "Oppdater kodete videoer",
|
||||
"refresh_faces": "Oppdater ansikter",
|
||||
|
@ -978,7 +978,7 @@
|
||||
"external": "Zunanji",
|
||||
"external_libraries": "Zunanje knjižnice",
|
||||
"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",
|
||||
"failed": "Ni uspelo",
|
||||
"failed_to_load_assets": "Sredstev ni bilo mogoče naložiti",
|
||||
@ -1125,7 +1125,7 @@
|
||||
"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",
|
||||
"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_latitude_error": "Vnesi veljavno zemljepisno širino",
|
||||
"location_picker_latitude_hint": "Tukaj vnesi svojo zemljepisno širino",
|
||||
@ -1432,6 +1432,8 @@
|
||||
"recent_searches": "Nedavna iskanja",
|
||||
"recently_added": "Nedavno dodano",
|
||||
"recently_added_page_title": "Nedavno dodano",
|
||||
"recently_taken": "Nedavno uporabljen",
|
||||
"recently_taken_page_title": "Nedavno Uporabljen",
|
||||
"refresh": "Osveži",
|
||||
"refresh_encoded_videos": "Osveži kodirane videoposnetke",
|
||||
"refresh_faces": "Osveži obraze",
|
||||
|
@ -378,6 +378,7 @@
|
||||
"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_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_troubleshooting_subtitle": "Aktivera funktioner för felsökning",
|
||||
"advanced_settings_troubleshooting_title": "Felsökning",
|
||||
@ -992,6 +993,7 @@
|
||||
"filetype": "Filtyp",
|
||||
"filter": "Filter",
|
||||
"filter_people": "Filtrera personer",
|
||||
"filter_places": "Filtrera platser",
|
||||
"find_them_fast": "Hitta dem snabbt efter namn med sök",
|
||||
"fix_incorrect_match": "Fixa inkorrekt matchning",
|
||||
"folder": "Mapp",
|
||||
|
@ -6,7 +6,6 @@
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
@ -1,40 +1,25 @@
|
||||
package app.alextran.immich
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import java.security.MessageDigest
|
||||
import java.io.FileInputStream
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `BackgroundService` and file trash operations
|
||||
* Android plugin for Dart `BackgroundService`
|
||||
*
|
||||
* Receives messages/method calls from the foreground Dart side to manage
|
||||
* the background service, e.g. start (enqueue), stop (cancel)
|
||||
*/
|
||||
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
|
||||
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private var methodChannel: MethodChannel? = null
|
||||
private var fileTrashChannel: MethodChannel? = null
|
||||
private var context: Context? = null
|
||||
private var pendingResult: Result? = null
|
||||
private val PERMISSION_REQUEST_CODE = 1001
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||
@ -44,10 +29,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
context = ctx
|
||||
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
|
||||
methodChannel?.setMethodCallHandler(this)
|
||||
|
||||
// Add file trash channel
|
||||
fileTrashChannel = MethodChannel(messenger, "file_trash")
|
||||
fileTrashChannel?.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
@ -57,14 +38,11 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private fun onDetachedFromEngine() {
|
||||
methodChannel?.setMethodCallHandler(null)
|
||||
methodChannel = null
|
||||
fileTrashChannel?.setMethodCallHandler(null)
|
||||
fileTrashChannel = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
val ctx = context!!
|
||||
when (call.method) {
|
||||
// Existing BackgroundService methods
|
||||
"enable" -> {
|
||||
val args = call.arguments<ArrayList<*>>()!!
|
||||
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||
@ -136,180 +114,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// File Trash methods moved from MainActivity
|
||||
"moveToTrash" -> {
|
||||
val fileName = call.argument<String>("fileName")
|
||||
if (fileName != null) {
|
||||
if (hasManageStoragePermission()) {
|
||||
val success = moveToTrash(fileName)
|
||||
result.success(success)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Storage permission required", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||
}
|
||||
}
|
||||
|
||||
"restoreFromTrash" -> {
|
||||
val fileName = call.argument<String>("fileName")
|
||||
if (fileName != null) {
|
||||
if (hasManageStoragePermission()) {
|
||||
val success = untrashImage(fileName)
|
||||
result.success(success)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Storage permission required", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||
}
|
||||
}
|
||||
|
||||
"requestManageStoragePermission" -> {
|
||||
if (!hasManageStoragePermission()) {
|
||||
requestManageStoragePermission(result)
|
||||
} else {
|
||||
Log.e("Manage storage permission", "Permission already granted")
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// File Trash methods moved from MainActivity
|
||||
private fun hasManageStoragePermission(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestManageStoragePermission(result: Result) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
pendingResult = result // Store the result callback
|
||||
val activity = activityBinding?.activity ?: return
|
||||
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
intent.data = Uri.parse("package:${activity.packageName}")
|
||||
activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveToTrash(fileName: String): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
val uri = getFileUri(fileName)
|
||||
Log.e("FILE_URI", uri.toString())
|
||||
return uri?.let { moveToTrash(it) } ?: false
|
||||
}
|
||||
|
||||
private fun moveToTrash(contentUri: Uri): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
return try {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.IS_TRASHED, 1) // Move to trash
|
||||
}
|
||||
val updated = contentResolver.update(contentUri, values, null, null)
|
||||
updated > 0
|
||||
} catch (e: Exception) {
|
||||
Log.e("TrashError", "Error moving to trash", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileUri(fileName: String): Uri? {
|
||||
val contentResolver = context?.contentResolver ?: return null
|
||||
val contentUri = MediaStore.Files.getContentUri("external")
|
||||
val projection = arrayOf(MediaStore.Images.Media._ID)
|
||||
val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?"
|
||||
val selectionArgs = arrayOf(fileName)
|
||||
var fileUri: Uri? = null
|
||||
|
||||
contentResolver.query(contentUri, projection, selection, selectionArgs, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
|
||||
fileUri = ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
}
|
||||
return fileUri
|
||||
}
|
||||
|
||||
private fun untrashImage(name: String): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
val uri = getTrashedFileUri(contentResolver, name)
|
||||
Log.e("FILE_URI", uri.toString())
|
||||
return uri?.let { untrashImage(it) } ?: false
|
||||
}
|
||||
|
||||
private fun untrashImage(contentUri: Uri): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
return try {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.IS_TRASHED, 0) // Restore file
|
||||
}
|
||||
val updated = contentResolver.update(contentUri, values, null, null)
|
||||
updated > 0
|
||||
} catch (e: Exception) {
|
||||
Log.e("TrashError", "Error restoring file", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrashedFileUri(contentResolver: ContentResolver, fileName: String): Uri? {
|
||||
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
|
||||
|
||||
val queryArgs = Bundle().apply {
|
||||
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?")
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||
}
|
||||
|
||||
contentResolver.query(contentUri, projection, queryArgs, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
|
||||
return ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ActivityAware implementation
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activityBinding?.removeActivityResultListener(this)
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activityBinding?.removeActivityResultListener(this)
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
// ActivityResultListener implementation
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
val granted = hasManageStoragePermission()
|
||||
pendingResult?.success(granted)
|
||||
pendingResult = null
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
private const val BUFFER_SIZE = 2 * 1024 * 1024
|
||||
private const val BUFFER_SIZE = 2 * 1024 * 1024;
|
||||
|
@ -2,12 +2,14 @@ package app.alextran.immich
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import androidx.annotation.NonNull
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||
// No need to set up method channel here as it's now handled in the plugin
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 193,
|
||||
"android.injected.version.name" => "1.131.3",
|
||||
"android.injected.version.code" => 195,
|
||||
"android.injected.version.name" => "1.132.1",
|
||||
}
|
||||
)
|
||||
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')
|
||||
|
@ -541,7 +541,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@ -685,7 +685,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@ -715,7 +715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@ -748,7 +748,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@ -791,7 +791,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@ -831,7 +831,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.131.3</string>
|
||||
<string>1.132.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@ -93,7 +93,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>201</string>
|
||||
<string>202</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Release"
|
||||
lane :release do
|
||||
increment_version_number(
|
||||
version_number: "1.131.3"
|
||||
version_number: "1.132.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
@ -65,7 +65,6 @@ enum StoreKey<T> {
|
||||
|
||||
// Video settings
|
||||
loadOriginalVideo<bool>._(136),
|
||||
manageLocalMediaAndroid<bool>._(137),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000);
|
||||
|
@ -1,5 +0,0 @@
|
||||
abstract interface class ILocalFilesManager {
|
||||
Future<bool> moveToTrash(String fileName);
|
||||
Future<bool> restoreFromTrash(String fileName);
|
||||
Future<bool> requestManageStoragePermission();
|
||||
}
|
@ -2,11 +2,14 @@ import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
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/painting.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// The local image provider for an asset
|
||||
/// Only viable
|
||||
@ -15,11 +18,16 @@ class ImmichLocalThumbnailProvider
|
||||
final Asset asset;
|
||||
final int height;
|
||||
final int width;
|
||||
final CacheManager? cacheManager;
|
||||
final Logger log = Logger("ImmichLocalThumbnailProvider");
|
||||
final String? userId;
|
||||
|
||||
ImmichLocalThumbnailProvider({
|
||||
required this.asset,
|
||||
this.height = 256,
|
||||
this.width = 256,
|
||||
this.cacheManager,
|
||||
this.userId,
|
||||
}) : assert(asset.local != null, 'Only usable when asset.local is set');
|
||||
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
@ -36,11 +44,10 @@ class ImmichLocalThumbnailProvider
|
||||
ImmichLocalThumbnailProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key.asset, decode, chunkEvents),
|
||||
codec: _codec(key.asset, cache, decode),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription(key.asset.fileName);
|
||||
},
|
||||
@ -50,25 +57,38 @@ class ImmichLocalThumbnailProvider
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
Asset assetData,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async* {
|
||||
final thumbBytes = await assetData.local
|
||||
?.thumbnailDataWithSize(ThumbnailSize(width, height));
|
||||
if (thumbBytes == null) {
|
||||
chunkEvents.close();
|
||||
final cacheKey =
|
||||
'$userId${assetData.localId}${assetData.checksum}$width$height';
|
||||
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
||||
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(
|
||||
"Loading thumb for local photo ${asset.fileName} failed",
|
||||
"Loading thumb for local photo ${assetData.fileName} failed",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
} finally {
|
||||
chunkEvents.close();
|
||||
}
|
||||
await cache.putFile(cacheKey, thumbnailBytes);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -23,7 +23,6 @@ enum PendingAction {
|
||||
assetDelete,
|
||||
assetUploaded,
|
||||
assetHidden,
|
||||
assetTrash,
|
||||
}
|
||||
|
||||
class PendingChange {
|
||||
@ -161,7 +160,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
socket.on('on_asset_delete', _handleOnAssetDelete);
|
||||
socket.on('on_asset_trash', _handleOnAssetTrash);
|
||||
socket.on('on_asset_trash', _handleServerUpdates);
|
||||
socket.on('on_asset_restore', _handleServerUpdates);
|
||||
socket.on('on_asset_update', _handleServerUpdates);
|
||||
socket.on('on_asset_stack_update', _handleServerUpdates);
|
||||
@ -208,26 +207,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
_debounce.run(handlePendingChanges);
|
||||
}
|
||||
|
||||
Future<void> _handlePendingTrashes() async {
|
||||
final trashChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetTrash)
|
||||
.toList();
|
||||
if (trashChanges.isNotEmpty) {
|
||||
List<String> remoteIds = trashChanges
|
||||
.expand((a) => (a.value as List).map((e) => e.toString()))
|
||||
.toList();
|
||||
|
||||
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
await _ref.read(assetProvider.notifier).getAllAsset();
|
||||
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
.whereNot((c) => trashChanges.contains(c))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePendingDeletes() async {
|
||||
final deleteChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetDelete)
|
||||
@ -288,7 +267,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
await _handlePendingUploaded();
|
||||
await _handlePendingDeletes();
|
||||
await _handlingPendingHidden();
|
||||
await _handlePendingTrashes();
|
||||
}
|
||||
|
||||
void _handleOnConfigUpdate(dynamic _) {
|
||||
@ -307,10 +285,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
void _handleOnAssetDelete(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetDelete, data);
|
||||
|
||||
void _handleOnAssetTrash(dynamic data) {
|
||||
addPendingChange(PendingAction.assetTrash, data);
|
||||
}
|
||||
|
||||
void _handleOnAssetHidden(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetHidden, data);
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/utils/local_files_manager.dart';
|
||||
|
||||
final localFilesManagerRepositoryProvider =
|
||||
Provider((ref) => LocalFilesManagerRepository());
|
||||
|
||||
class LocalFilesManagerRepository implements ILocalFilesManager {
|
||||
@override
|
||||
Future<bool> moveToTrash(String fileName) async {
|
||||
return await LocalFilesManager.moveToTrash(fileName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> restoreFromTrash(String fileName) async {
|
||||
return await LocalFilesManager.restoreFromTrash(fileName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestManageStoragePermission() async {
|
||||
return await LocalFilesManager.requestManageStoragePermission();
|
||||
}
|
||||
}
|
@ -61,7 +61,6 @@ enum AppSettingsEnum<T> {
|
||||
0,
|
||||
),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -17,10 +16,8 @@ import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||
@ -28,10 +25,8 @@ import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
@ -53,8 +48,6 @@ final syncServiceProvider = Provider(
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(etagRepositoryProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(localFilesManagerRepositoryProvider),
|
||||
ref.watch(partnerApiRepositoryProvider),
|
||||
ref.watch(userApiRepositoryProvider),
|
||||
),
|
||||
@ -76,8 +69,6 @@ class SyncService {
|
||||
final IUserApiRepository _userApiRepository;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
final AppSettingsService _appSettingsService;
|
||||
final ILocalFilesManager _localFilesManager;
|
||||
|
||||
SyncService(
|
||||
this._hashService,
|
||||
@ -91,8 +82,6 @@ class SyncService {
|
||||
this._userRepository,
|
||||
this._userService,
|
||||
this._eTagRepository,
|
||||
this._appSettingsService,
|
||||
this._localFilesManager,
|
||||
this._partnerApiRepository,
|
||||
this._userApiRepository,
|
||||
);
|
||||
@ -249,19 +238,8 @@ class SyncService {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _moveToTrashMatchedAssets(Iterable<String> idsToDelete) async {
|
||||
final List<Asset> localAssets = await _assetRepository.getAllLocal();
|
||||
final List<Asset> matchedAssets = localAssets
|
||||
.where((asset) => idsToDelete.contains(asset.remoteId))
|
||||
.toList();
|
||||
|
||||
for (var asset in matchedAssets) {
|
||||
_localFilesManager.moveToTrash(asset.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) async {
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
|
||||
return _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteAllByRemoteId(
|
||||
idsToDelete,
|
||||
@ -271,12 +249,6 @@ class SyncService {
|
||||
idsToDelete,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
await _moveToTrashMatchedAssets(idsToDelete);
|
||||
}
|
||||
if (merged.isEmpty) return;
|
||||
for (final Asset asset in merged) {
|
||||
asset.remoteId = null;
|
||||
@ -818,27 +790,10 @@ class SyncService {
|
||||
return (existing, toUpsert);
|
||||
}
|
||||
|
||||
Future<void> _toggleTrashStatusForAssets(List<Asset> assetsList) async {
|
||||
for (var asset in assetsList) {
|
||||
if (asset.isTrashed) {
|
||||
_localFilesManager.moveToTrash(asset.fileName);
|
||||
} else {
|
||||
_localFilesManager.restoreFromTrash(asset.fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
||||
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||
if (assets.isEmpty) return;
|
||||
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
_toggleTrashStatusForAssets(assets);
|
||||
}
|
||||
|
||||
try {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(assets);
|
||||
|
@ -1,39 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class LocalFilesManager {
|
||||
static const MethodChannel _channel = MethodChannel('file_trash');
|
||||
|
||||
static Future<bool> moveToTrash(String fileName) async {
|
||||
try {
|
||||
final bool success =
|
||||
await _channel.invokeMethod('moveToTrash', {'fileName': fileName});
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error moving to trash: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> restoreFromTrash(String fileName) async {
|
||||
try {
|
||||
final bool success = await _channel
|
||||
.invokeMethod('restoreFromTrash', {'fileName': fileName});
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error restoring file: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> requestManageStoragePermission() async {
|
||||
try {
|
||||
final bool success =
|
||||
await _channel.invokeMethod('requestManageStoragePermission');
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error requesting permission: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
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_remote_thumbnail_provider.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/thumbhash_placeholder.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({
|
||||
this.asset,
|
||||
this.width = 250,
|
||||
@ -31,6 +32,7 @@ class ImmichThumbnail extends HookWidget {
|
||||
static ImageProvider imageProvider({
|
||||
Asset? asset,
|
||||
String? assetId,
|
||||
String? userId,
|
||||
int thumbnailSize = 256,
|
||||
}) {
|
||||
if (asset == null && assetId == null) {
|
||||
@ -48,6 +50,7 @@ class ImmichThumbnail extends HookWidget {
|
||||
asset: asset,
|
||||
height: thumbnailSize,
|
||||
width: thumbnailSize,
|
||||
userId: userId,
|
||||
);
|
||||
} else {
|
||||
return ImmichRemoteThumbnailProvider(
|
||||
@ -59,8 +62,10 @@ class ImmichThumbnail extends HookWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Uint8List? blurhash = useBlurHashRef(asset).value;
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
if (asset == null) {
|
||||
return Container(
|
||||
color: Colors.grey,
|
||||
@ -79,6 +84,7 @@ class ImmichThumbnail extends HookWidget {
|
||||
octoSet: blurHashOrPlaceholder(blurhash),
|
||||
image: ImmichThumbnail.imageProvider(
|
||||
asset: asset,
|
||||
userId: userId,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
|
@ -1,13 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
@ -27,8 +25,6 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
|
||||
final advancedTroubleshooting =
|
||||
useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
|
||||
final manageLocalMediaAndroid =
|
||||
useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
||||
final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
|
||||
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
||||
final allowSelfSignedSSLCert =
|
||||
@ -44,16 +40,6 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()),
|
||||
);
|
||||
|
||||
Future<bool> checkAndroidVersion() async {
|
||||
if (Platform.isAndroid) {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
int sdkVersion = androidInfo.version.sdkInt;
|
||||
return sdkVersion >= 30;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final advancedSettings = [
|
||||
SettingsSwitchListTile(
|
||||
enabled: true,
|
||||
@ -61,29 +47,6 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
title: "advanced_settings_troubleshooting_title".tr(),
|
||||
subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
|
||||
),
|
||||
FutureBuilder<bool>(
|
||||
future: checkAndroidVersion(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return SettingsSwitchListTile(
|
||||
enabled: true,
|
||||
valueNotifier: manageLocalMediaAndroid,
|
||||
title: "advanced_settings_sync_remote_deletions_title".tr(),
|
||||
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
|
||||
onChanged: (value) async {
|
||||
if (value) {
|
||||
final result = await ref
|
||||
.read(localFilesManagerRepositoryProvider)
|
||||
.requestManageStoragePermission();
|
||||
manageLocalMediaAndroid.value = result;
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
SettingsSliderListTile(
|
||||
text: "advanced_settings_log_level_title".tr(args: [logLevel]),
|
||||
valueNotifier: levelId,
|
||||
|
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:
|
||||
|
||||
- API version: 1.131.3
|
||||
- API version: 1.132.1
|
||||
- Generator version: 7.8.0
|
||||
- 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 {
|
||||
/// Returns a new [OAuthCallbackDto] instance.
|
||||
OAuthCallbackDto({
|
||||
this.codeVerifier,
|
||||
this.state,
|
||||
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 state;
|
||||
String codeVerifier;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is OAuthCallbackDto &&
|
||||
other.url == url &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is OAuthCallbackDto &&
|
||||
other.codeVerifier == codeVerifier &&
|
||||
other.state == state &&
|
||||
other.codeVerifier == codeVerifier;
|
||||
other.url == url;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(url.hashCode) + (state.hashCode) + (codeVerifier.hashCode);
|
||||
(codeVerifier == null ? 0 : codeVerifier!.hashCode) +
|
||||
(state == null ? 0 : state!.hashCode) +
|
||||
(url.hashCode);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'OAuthCallbackDto[url=$url, state=$state, codeVerifier=$codeVerifier]';
|
||||
String toString() => 'OAuthCallbackDto[codeVerifier=$codeVerifier, state=$state, url=$url]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'url'] = this.url;
|
||||
json[r'state'] = this.state;
|
||||
if (this.codeVerifier != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -56,18 +77,15 @@ class OAuthCallbackDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return OAuthCallbackDto(
|
||||
codeVerifier: mapValueOfType<String>(json, r'codeVerifier'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
url: mapValueOfType<String>(json, r'url')!,
|
||||
state: mapValueOfType<String>(json, r'state')!,
|
||||
codeVerifier: mapValueOfType<String>(json, r'codeVerifier')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<OAuthCallbackDto> listFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static List<OAuthCallbackDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <OAuthCallbackDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
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
|
||||
static Map<String, List<OAuthCallbackDto>> mapListFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static Map<String, List<OAuthCallbackDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<OAuthCallbackDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = OAuthCallbackDto.listFromJson(
|
||||
entry.value,
|
||||
growable: growable,
|
||||
);
|
||||
map[entry.key] = OAuthCallbackDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@ -116,7 +128,6 @@ class OAuthCallbackDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'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 {
|
||||
/// Returns a new [OAuthConfigDto] instance.
|
||||
OAuthConfigDto({
|
||||
this.codeChallenge,
|
||||
required this.redirectUri,
|
||||
required this.state,
|
||||
required this.codeChallenge,
|
||||
this.state,
|
||||
});
|
||||
|
||||
///
|
||||
/// 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 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
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is OAuthConfigDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is OAuthConfigDto &&
|
||||
other.codeChallenge == codeChallenge &&
|
||||
other.redirectUri == redirectUri &&
|
||||
other.state == state &&
|
||||
other.codeChallenge == codeChallenge;
|
||||
other.state == state;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(redirectUri.hashCode) + (state.hashCode) + (codeChallenge.hashCode);
|
||||
(codeChallenge == null ? 0 : codeChallenge!.hashCode) +
|
||||
(redirectUri.hashCode) +
|
||||
(state == null ? 0 : state!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'OAuthConfigDto[redirectUri=$redirectUri, state=$state, codeChallenge=$codeChallenge]';
|
||||
String toString() => 'OAuthConfigDto[codeChallenge=$codeChallenge, redirectUri=$redirectUri, state=$state]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'redirectUri'] = this.redirectUri;
|
||||
json[r'state'] = this.state;
|
||||
if (this.codeChallenge != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -56,18 +77,15 @@ class OAuthConfigDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return OAuthConfigDto(
|
||||
codeChallenge: mapValueOfType<String>(json, r'codeChallenge'),
|
||||
redirectUri: mapValueOfType<String>(json, r'redirectUri')!,
|
||||
state: mapValueOfType<String>(json, r'state')!,
|
||||
codeChallenge: mapValueOfType<String>(json, r'codeChallenge')!,
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<OAuthConfigDto> listFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static List<OAuthConfigDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <OAuthConfigDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
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
|
||||
static Map<String, List<OAuthConfigDto>> mapListFromJson(
|
||||
dynamic json, {
|
||||
bool growable = false,
|
||||
}) {
|
||||
static Map<String, List<OAuthConfigDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<OAuthConfigDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = OAuthConfigDto.listFromJson(
|
||||
entry.value,
|
||||
growable: growable,
|
||||
);
|
||||
map[entry.key] = OAuthConfigDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@ -116,7 +128,6 @@ class OAuthConfigDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'redirectUri',
|
||||
'state',
|
||||
'codeChallenge',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 1.131.3+193
|
||||
version: 1.132.1+195
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
@ -60,9 +60,6 @@ void main() {
|
||||
final MockAlbumMediaRepository albumMediaRepository =
|
||||
MockAlbumMediaRepository();
|
||||
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
|
||||
final MockAppSettingService appSettingService = MockAppSettingService();
|
||||
final MockLocalFilesManagerRepository localFilesManagerRepository =
|
||||
MockLocalFilesManagerRepository();
|
||||
final MockPartnerApiRepository partnerApiRepository =
|
||||
MockPartnerApiRepository();
|
||||
final MockUserApiRepository userApiRepository = MockUserApiRepository();
|
||||
@ -109,8 +106,6 @@ void main() {
|
||||
userRepository,
|
||||
userService,
|
||||
eTagRepository,
|
||||
appSettingService,
|
||||
localFilesManagerRepository,
|
||||
partnerApiRepository,
|
||||
userApiRepository,
|
||||
);
|
||||
|
@ -10,7 +10,6 @@ import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
@ -42,9 +41,6 @@ class MockAuthApiRepository extends Mock implements IAuthApiRepository {}
|
||||
|
||||
class MockAuthRepository extends Mock implements IAuthRepository {}
|
||||
|
||||
class MockPartnerRepository extends Mock implements IPartnerRepository {}
|
||||
|
||||
class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {}
|
||||
|
||||
class MockLocalFilesManagerRepository extends Mock
|
||||
implements ILocalFilesManager {}
|
||||
class MockPartnerRepository extends Mock implements IPartnerRepository {}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
@ -26,6 +25,4 @@ class MockNetworkService extends Mock implements NetworkService {}
|
||||
|
||||
class MockSearchApi extends Mock implements SearchApi {}
|
||||
|
||||
class MockAppSettingService extends Mock implements AppSettingsService {}
|
||||
|
||||
class MockBackgroundService extends Mock implements BackgroundService {}
|
||||
|
@ -7656,7 +7656,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"contact": {}
|
||||
},
|
||||
"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",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.131.3
|
||||
* 1.132.1
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@ -687,17 +687,17 @@ export type TestEmailResponseDto = {
|
||||
messageId: string;
|
||||
};
|
||||
export type OAuthConfigDto = {
|
||||
codeChallenge?: string;
|
||||
redirectUri: string;
|
||||
state?: string;
|
||||
codeChallenge?: string;
|
||||
};
|
||||
export type OAuthAuthorizeResponseDto = {
|
||||
url: string;
|
||||
};
|
||||
export type OAuthCallbackDto = {
|
||||
url: string;
|
||||
state?: string;
|
||||
codeVerifier?: string;
|
||||
state?: string;
|
||||
url: string;
|
||||
};
|
||||
export type PartnerResponseDto = {
|
||||
avatarColor: UserAvatarColor;
|
||||
|
2524
server/package-lock.json
generated
2524
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@ -88,7 +88,7 @@
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.33.5",
|
||||
"sharp": "^0.34.0",
|
||||
"sirv": "^3.0.0",
|
||||
"tailwindcss-preset-email": "^1.3.2",
|
||||
"thumbhash": "^0.1.1",
|
||||
@ -132,7 +132,8 @@
|
||||
"globals": "^16.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-addon-api": "^8.3.1",
|
||||
"node-gyp": "^11.2.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.0.2",
|
||||
@ -152,5 +153,8 @@
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.14.0"
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.34.0"
|
||||
}
|
||||
}
|
||||
|
@ -17,12 +17,12 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
|
||||
import { repositories } from 'src/repositories';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { services } from 'src/services';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { getKyselyConfig } from 'src/utils/database';
|
||||
|
||||
const common = [...repositories, ...services, GlobalExceptionFilter];
|
||||
@ -52,7 +52,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
@Inject(IWorker) private worker: ImmichWorker,
|
||||
logger: LoggingRepository,
|
||||
private eventRepository: EventRepository,
|
||||
private jobRepository: JobRepository,
|
||||
private jobService: JobService,
|
||||
private telemetryRepository: TelemetryRepository,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
@ -62,10 +62,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
this.telemetryRepository.setup({ repositories });
|
||||
|
||||
this.jobRepository.setup({ services });
|
||||
if (this.worker === ImmichWorker.MICROSERVICES) {
|
||||
this.jobRepository.startWorkers();
|
||||
}
|
||||
this.jobService.setServices(services);
|
||||
|
||||
this.eventRepository.setAuthFn(async (client) =>
|
||||
this.authService.authenticate({
|
||||
|
@ -407,6 +407,8 @@ export enum DatabaseExtension {
|
||||
export enum BootstrapEventPriority {
|
||||
// Database service should be initialized before anything else, most other services need database access
|
||||
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
|
||||
SystemConfig = 100,
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export class JobRepository {
|
||||
this.logger.setContext(JobRepository.name);
|
||||
}
|
||||
|
||||
setup({ services }: { services: ClassConstructor<unknown>[] }) {
|
||||
setup(services: ClassConstructor<unknown>[]) {
|
||||
const reflector = this.moduleRef.get(Reflector, { strict: false });
|
||||
|
||||
// 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> {
|
||||
if (!this.buildVersions) {
|
||||
const { nodeVersion, resourcePaths } = this.configRepository.getEnv();
|
||||
|
||||
const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([
|
||||
maybeFirstLine('node --version'),
|
||||
maybeFirstLine('ffmpeg -version'),
|
||||
maybeFirstLine('convert --version'),
|
||||
]);
|
||||
|
||||
const lockfile = await readFile(resourcePaths.lockFile)
|
||||
const lockfile: BuildLockfile | undefined = await readFile(resourcePaths.lockFile)
|
||||
.then((buffer) => JSON.parse(buffer.toString()))
|
||||
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
|
||||
|
||||
return {
|
||||
nodejs: nodejsOutput || nodeVersion || '',
|
||||
exiftool: await exiftool.version(),
|
||||
ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '',
|
||||
libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips,
|
||||
imagemagick:
|
||||
getLockfileVersion('imagemagick', lockfile) || magickOutput.replaceAll('Version: ImageMagick ', '') || '',
|
||||
const [nodejsVersion, ffmpegVersion, magickVersion, exiftoolVersion] = await Promise.all([
|
||||
this.retrieveVersionFallback('node --version', undefined, nodeVersion),
|
||||
this.retrieveVersionFallback(
|
||||
'ffmpeg -version',
|
||||
(output) => output.replaceAll('ffmpeg version ', ''),
|
||||
getLockfileVersion('ffmpeg', lockfile),
|
||||
),
|
||||
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.session.create.mockResolvedValue(factory.session());
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse(user),
|
||||
);
|
||||
await expect(
|
||||
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, {
|
||||
profileImagePath: `upload/profile/${user.id}/${fileId}.jpg`,
|
||||
@ -796,9 +800,13 @@ describe(AuthService.name, () => {
|
||||
mocks.user.update.mockResolvedValue(user);
|
||||
mocks.session.create.mockResolvedValue(factory.session());
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
oauthResponse(user),
|
||||
);
|
||||
await expect(
|
||||
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.oauth.getProfilePicture).not.toHaveBeenCalled();
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { snakeCase } from 'lodash';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||
import {
|
||||
AssetType,
|
||||
BootstrapEventPriority,
|
||||
ImmichWorker,
|
||||
JobCommand,
|
||||
JobName,
|
||||
@ -51,6 +53,8 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
|
||||
|
||||
@Injectable()
|
||||
export class JobService extends BaseService {
|
||||
private services: ClassConstructor<unknown>[] = [];
|
||||
|
||||
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
|
||||
onConfigInit({ newConfig: config }: ArgOf<'config.init'>) {
|
||||
this.logger.debug(`Updating queue concurrency settings`);
|
||||
@ -69,6 +73,18 @@ export class JobService extends BaseService {
|
||||
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> {
|
||||
await this.jobRepository.queue(asJobItem(dto));
|
||||
}
|
||||
|
@ -20,12 +20,6 @@ export default defineConfig({
|
||||
'src/services/index.ts',
|
||||
'src/sql-tools/from-database/index.ts',
|
||||
],
|
||||
thresholds: {
|
||||
lines: 85,
|
||||
statements: 85,
|
||||
branches: 90,
|
||||
functions: 85,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
deps: {
|
||||
|
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
@ -82,7 +82,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.131.3",
|
||||
"version": "1.132.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@ -76,7 +76,7 @@
|
||||
<div class="inline-block" use:resizeObserver={({ width }) => (innerWidth = width)}>
|
||||
{#each memoryStore.memories as memory (memory.id)}
|
||||
<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}"
|
||||
>
|
||||
<img
|
||||
|
Loading…
x
Reference in New Issue
Block a user