Compare commits

...

6 Commits

Author SHA1 Message Date
Alex
1eb9ac8217 fix(server) Cannot change first time password due to null in first and last name payload (#1205)
* fix(server) Cannot change first time password due to null in first and last name payload

* Added error message for form on the web
2022-12-28 21:07:04 -06:00
Kuljit Uppal
7810dd1942 chore(docs) Add link to releases (#1195)
Include link to Github releases page in updating instructions.
2022-12-28 09:49:34 -06:00
Damián
eeb0456356 Fixed translations and added missing (#1201)
Fixed some wrong translations and finished adding the missing ones.
2022-12-28 08:47:10 -06:00
Jason Rasmussen
c032cfd99e chore(server): fix unit test (#1194) 2022-12-27 20:29:58 -06:00
Alex Tran
4545249fa3 Update docs and readme 2022-12-27 14:28:25 -06:00
Jason Rasmussen
380f719fd8 feat(server,web): update email address (#1186)
* feat: change email

* test: change email
2022-12-27 10:36:31 -06:00
15 changed files with 258 additions and 157 deletions

View File

@@ -30,6 +30,7 @@
## Content ## Content
- [Official Documentation](https://immich.app/docs) - [Official Documentation](https://immich.app/docs)
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
- [Demo](#demo) - [Demo](#demo)
- [Features](#features) - [Features](#features)
- [Introduction](https://immich.app/docs/overview/introduction) - [Introduction](https://immich.app/docs/overview/introduction)

View File

@@ -120,7 +120,7 @@ For more information on how to use the application, please refer to the [Post In
### Step 4 - Upgrading ### Step 4 - Upgrading
When a new version of Immich is (released)[], the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file: When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
```bash title="Upgrade Immich" ```bash title="Upgrade Immich"
docker-compose pull && docker-compose up -d # Or `docker compose` docker-compose pull && docker-compose up -d # Or `docker compose`

View File

@@ -105,16 +105,16 @@ const config = {
position: "right", position: "right",
label: "API", label: "API",
}, },
{
to: "/blog",
position: "right",
label: "Blog",
},
{ {
href: "https://github.com/immich-app/immich", href: "https://github.com/immich-app/immich",
label: "GitHub", label: "GitHub",
position: "right", position: "right",
}, },
{
href: "https://github.com/orgs/immich-app/projects/1",
label: "Roadmap",
position: "right",
},
], ],
}, },
footer: { footer: {
@@ -143,16 +143,20 @@ const config = {
], ],
}, },
{ {
title: "More", title: "Links",
items: [ items: [
{ // {
label: "Blog", // label: "Blog",
to: "/blog", // to: "/blog",
}, // },
{ {
label: "GitHub", label: "GitHub",
href: "https://github.com/immich-app/immich", href: "https://github.com/immich-app/immich",
}, },
{
label: "Roadmap",
href: "https://github.com/orgs/immich-app/projects/1",
},
], ],
}, },
], ],

View File

@@ -1,96 +1,96 @@
{ {
"album_info_card_backup_album_excluded": "EXCLUIDOS", "album_info_card_backup_album_excluded": "EXCLUIDOS",
"album_info_card_backup_album_included": "INCLUIDOS", "album_info_card_backup_album_included": "INCLUIDOS",
"album_thumbnail_card_item": "1 item", "album_thumbnail_card_item": "1 elemento",
"album_thumbnail_card_items": "{} items", "album_thumbnail_card_items": "{} elementos",
"album_thumbnail_card_shared": " · Shared", "album_thumbnail_card_shared": " · Compartido",
"album_viewer_appbar_share_delete": "Eliminar álbum ", "album_viewer_appbar_share_delete": "Eliminar álbum",
"album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum", "album_viewer_appbar_share_err_delete": "No se ha podido eliminar el álbum",
"album_viewer_appbar_share_err_leave": "No ha podido dejar el álbum", "album_viewer_appbar_share_err_leave": "No se ha podido dejar el álbum",
"album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los activos del álbum", "album_viewer_appbar_share_err_remove": "Hay problemas para eliminar contenidos del álbum",
"album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ", "album_viewer_appbar_share_err_title": "No se ha podido cambiar el título del álbum",
"album_viewer_appbar_share_leave": "Abandonar álbum ", "album_viewer_appbar_share_leave": "Abandonar álbum",
"album_viewer_appbar_share_remove": "Eliminar del álbum ", "album_viewer_appbar_share_remove": "Eliminar del álbum",
"album_viewer_page_share_add_users": "Añadir usuarios", "album_viewer_page_share_add_users": "Añadir usuarios",
"asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_subtitle": "Configuración de la galería",
"asset_list_settings_title": "Photo Grid", "asset_list_settings_title": "Galería",
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
"backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
"backup_album_selection_page_assets_scatter": "Los activos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", "backup_album_selection_page_assets_scatter": "Los elementos pueden estar en varios álbumes. Por eso, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
"backup_album_selection_page_select_albums": "Seleccionar Álbumes", "backup_album_selection_page_select_albums": "Seleccionar álbumes",
"backup_album_selection_page_selection_info": "Información sobre la Selección", "backup_album_selection_page_selection_info": "Información sobre la selección",
"backup_album_selection_page_total_assets": "Total de activos únicos", "backup_album_selection_page_total_assets": "Total de elementos únicos",
"backup_all": "Todos", "backup_all": "Todos",
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…", "backup_background_service_backup_failed_message": "No se ha podido realizar la copia de seguridad. Reintentando…",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…", "backup_background_service_connection_failed_message": "No se ha podido conectar con el servidor. Reintentando…",
"backup_background_service_current_upload_notification": "Uploading {}", "backup_background_service_current_upload_notification": "Subidendo {}",
"backup_background_service_default_notification": "Checking for new assets…", "backup_background_service_default_notification": "Buscando nuevos elementos…",
"backup_background_service_error_title": "Backup error", "backup_background_service_error_title": "Error de copia de seguridad",
"backup_background_service_in_progress_notification": "Backing up your assets…", "backup_background_service_in_progress_notification": "Subiendo elementos…",
"backup_background_service_upload_failure_notification": "Failed to upload {}", "backup_background_service_upload_failure_notification": "Error al subir {}",
"backup_controller_page_albums": "Álbumes de copia de seguridad", "backup_controller_page_albums": "Álbumes de copia de seguridad",
"backup_controller_page_background_battery_info_link": "Show me how", "backup_controller_page_background_battery_info_link": "Muéstrame cómo",
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", "backup_controller_page_background_battery_info_message": "Para disfrutar de la mejor experiencia de copia de seguridad en segundo plano, desactive cualquier optimización de la batería que restrinja la actividad en segundo plano para Immich.\nDado que esto es específico de cada dispositivo, busque la información necesaria para el fabricante de su dispositivo.",
"backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Battery optimizations", "backup_controller_page_background_battery_info_title": "Optimización de batería",
"backup_controller_page_background_charging": "Only while charging", "backup_controller_page_background_charging": "Sólo mientras carga",
"backup_controller_page_background_configure_error": "Failed to configure the background service", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano",
"backup_controller_page_background_delay": "Delay new assets backup: {}", "backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", "backup_controller_page_background_description": "Activar el servicio en segundo plano para realizar copias de seguridad automáticas de los nuevos elementos sin tener que abrir la aplicación",
"backup_controller_page_background_is_off": "Automatic background backup is off", "backup_controller_page_background_is_off": "Copia de seguridad automática apagada",
"backup_controller_page_background_is_on": "Automatic background backup is on", "backup_controller_page_background_is_on": "Copia de seguridad automática encendida",
"backup_controller_page_background_turn_off": "Turn off background service", "backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano",
"backup_controller_page_background_turn_on": "Turn on background service", "backup_controller_page_background_turn_on": "Activar el servicio en segundo plano",
"backup_controller_page_background_wifi": "Only on WiFi", "backup_controller_page_background_wifi": "Sólo con WiFi",
"backup_controller_page_backup": "Copia de Seguridad", "backup_controller_page_backup": "Copia de Seguridad",
"backup_controller_page_backup_selected": "Seleccionado:", "backup_controller_page_backup_selected": "Seleccionado:",
"backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos", "backup_controller_page_backup_sub": "Copia de seguridad de fotos y vídeos",
"backup_controller_page_cancel": "Cancelar", "backup_controller_page_cancel": "Cancelar",
"backup_controller_page_created": "Created on: {}", "backup_controller_page_created": "Creado el: {}",
"backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.", "backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos elementos al servidor.",
"backup_controller_page_excluded": "Excluido:", "backup_controller_page_excluded": "Excluido:",
"backup_controller_page_failed": "Failed ({})", "backup_controller_page_failed": "Fallido ({})",
"backup_controller_page_filename": "File name: {} [{}]", "backup_controller_page_filename": "Nombre de archivo: {} [{}]",
"backup_controller_page_id": "ID: {}", "backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Información de la Copia de Seguridad", "backup_controller_page_info": "Información de la copia de seguridad",
"backup_controller_page_none_selected": "Ninguno seleccionado", "backup_controller_page_none_selected": "Ninguno seleccionado",
"backup_controller_page_remainder": "Remanente", "backup_controller_page_remainder": "Restantes",
"backup_controller_page_remainder_sub": "Fotos y álbumes restantes para hacer una copia de seguridad de la selección", "backup_controller_page_remainder_sub": "Fotos y álbumes restantes para hacer una copia de seguridad de la selección",
"backup_controller_page_select": "Seleccionar", "backup_controller_page_select": "Seleccionar",
"backup_controller_page_server_storage": "Almacenamiento en el servidor", "backup_controller_page_server_storage": "Almacenamiento en el servidor",
"backup_controller_page_start_backup": "Iniciar copia de seguridad", "backup_controller_page_start_backup": "Iniciar copia de seguridad",
"backup_controller_page_status_off": "La copia de seguridad está desactivada", "backup_controller_page_status_off": "La copia de seguridad está desactivada",
"backup_controller_page_status_on": "La copia de seguridad está activada", "backup_controller_page_status_on": "La copia de seguridad está activada",
"backup_controller_page_storage_format": "{} de {} usadas", "backup_controller_page_storage_format": "{} de {} usados",
"backup_controller_page_to_backup": "Álbumes a respaldar", "backup_controller_page_to_backup": "Álbumes a respaldar",
"backup_controller_page_total": "Total", "backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados", "backup_controller_page_total_sub": "Todas las fotos y vídeos únicos de los álbumes seleccionados",
"backup_controller_page_turn_off": "Apagar la copia de seguridad", "backup_controller_page_turn_off": "Apagar la copia de seguridad",
"backup_controller_page_turn_on": "Activar la copia de seguridad", "backup_controller_page_turn_on": "Activar la copia de seguridad",
"backup_controller_page_uploading_file_info": "Uploading file info", "backup_controller_page_uploading_file_info": "SUbiendo información de archivos",
"backup_err_only_album": "No se puede eliminar el único álbum", "backup_err_only_album": "No se puede eliminar el único álbum",
"backup_info_card_assets": "activos", "backup_info_card_assets": "elementos",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cach_settings_album_thumbnails": "Miniaturas del álbum ({} elementos)",
"cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button": "Limpiar caché",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", "cache_settings_clear_cache_button_title": "Limpia el caché de la app. Esto afectará significativamente al rendimiento de la aplicación hasta que la caché se haya reconstruido.",
"cache_settings_image_cache_size": "Image cache size ({} assets)", "cache_settings_image_cache_size": "Miniaturas de imágenes ({} elementos)",
"cache_settings_statistics_album": "Library thumbnails", "cache_settings_statistics_album": "Miniaturas del álbum",
"cache_settings_statistics_assets": "{} assets ({})", "cache_settings_statistics_assets": "{} elementos ({})",
"cache_settings_statistics_full": "Full images", "cache_settings_statistics_full": "Imágenes completas",
"cache_settings_statistics_shared": "Shared album thumbnails", "cache_settings_statistics_shared": "Miniaturas del álbum compartido",
"cache_settings_statistics_thumbnail": "Thumbnails", "cache_settings_statistics_thumbnail": "Miniaturas",
"cache_settings_statistics_title": "Cache usage", "cache_settings_statistics_title": "Uso de caché",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_subtitle": "Controla el comportamiento de almacenamiento en caché de la aplicación de Immich",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", "cache_settings_thumbnail_size": "Tamaño del caché de miniaturas ({} elementos)",
"cache_settings_title": "Caching Settings", "cache_settings_title": "Ajustes del caché",
"control_bottom_app_bar_add_to_album": "Add to album", "control_bottom_app_bar_add_to_album": "Añadir al álbum",
"control_bottom_app_bar_album_info": "{} items", "control_bottom_app_bar_album_info": "{} elementos",
"control_bottom_app_bar_album_info_shared": "{} items · Shared", "control_bottom_app_bar_album_info_shared": "{} elementos · Compartido",
"control_bottom_app_bar_create_new_album": "Create new album", "control_bottom_app_bar_create_new_album": "Crear nuevo álbum",
"control_bottom_app_bar_delete": "Eliminar", "control_bottom_app_bar_delete": "Eliminar",
"control_bottom_app_bar_share": "Share", "control_bottom_app_bar_share": "Compartir",
"create_album_page_untitled": "Untitled", "create_album_page_untitled": "Sin título",
"create_shared_album_page_create": "Create", "create_shared_album_page_create": "Crear",
"create_shared_album_page_share": "Compartir", "create_shared_album_page_share": "Compartir",
"create_shared_album_page_share_add_assets": "AÑADIR ACTIVOS", "create_shared_album_page_share_add_assets": "AÑADIR ACTIVOS",
"create_shared_album_page_share_select_photos": "Seleccionar Fotos", "create_shared_album_page_share_select_photos": "Seleccionar Fotos",
@@ -101,17 +101,17 @@
"delete_dialog_cancel": "Cancelar", "delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Eliminar", "delete_dialog_ok": "Eliminar",
"delete_dialog_title": "Eliminar Permanentemente", "delete_dialog_title": "Eliminar Permanentemente",
"exif_bottom_sheet_description": "Añadir Descripción...", "exif_bottom_sheet_description": "Añadir descripción...",
"exif_bottom_sheet_details": "DETALLES", "exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "LOCALZACIÓN", "exif_bottom_sheet_location": "LOCALZACIÓN",
"experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_subtitle": "En desarrollo",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_new_asset_list_title": "Habilitar galería experimental",
"experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_subtitle": "¡Úsalo bajo tu responsabilidad!",
"experimental_settings_title": "Experimental", "experimental_settings_title": "Experimental",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_conflicts": "Añadidos {added} elementos al álbum {album}. {failed} elementos ya estaban añadidos.",
"home_page_add_to_album_success": "Added {added} assets to album {album}.", "home_page_add_to_album_success": "Añadidos {added} elementos al álbum {album}.",
"library_page_albums": "Albums", "library_page_albums": "Álbumes",
"library_page_new_album": "New album", "library_page_new_album": "Nuevo álbum",
"login_form_button_text": "Iniciar Sesión", "login_form_button_text": "Iniciar Sesión",
"login_form_email_hint": "tucorreo@correo.com", "login_form_email_hint": "tucorreo@correo.com",
"login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api", "login_form_endpoint_hint": "http://tu-ip-de-servidor:puerto/api",
@@ -120,75 +120,75 @@
"login_form_err_invalid_email": "Correo electrónico no válido", "login_form_err_invalid_email": "Correo electrónico no válido",
"login_form_err_leading_whitespace": "Espacio en blanco inicial", "login_form_err_leading_whitespace": "Espacio en blanco inicial",
"login_form_err_trailing_whitespace": "Espacio en blanco al final", "login_form_err_trailing_whitespace": "Espacio en blanco al final",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", "login_form_failed_get_oauth_server_config": "Fallo al iniciar sesión con OAuth. Comprueba la URL del servidor.",
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", "login_form_failed_get_oauth_server_disable": "OAuth no está disponible en este servidor",
"login_form_failed_login": "Error logging you in, check server URL, email and password", "login_form_failed_login": "Fallo al iniciar sesión, comprueba URL, correo y contraseña",
"login_form_label_email": "Correo", "login_form_label_email": "Correo",
"login_form_label_password": "Contraseña", "login_form_label_password": "Contraseña",
"login_form_password_hint": "contraseña", "login_form_password_hint": "contraseña",
"login_form_save_login": "Mantener la sesión iniciada", "login_form_save_login": "Mantener la sesión iniciada",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"profile_drawer_app_logs": "Logs", "profile_drawer_app_logs": "Registros",
"profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados", "profile_drawer_client_server_up_to_date": "El cliente y el servidor están actualizados",
"profile_drawer_settings": "Settings", "profile_drawer_settings": "Ajustes",
"profile_drawer_sign_out": "Cerrar Sesión", "profile_drawer_sign_out": "Cerrar Sesión",
"search_bar_hint": "Busca tus fotos", "search_bar_hint": "Buscar fotos",
"search_page_no_objects": "No Objects Info Available", "search_page_no_objects": "No hay información sobre objetos",
"search_page_no_places": "No hay información de lugares disponibles", "search_page_no_places": "No hay información sobre lugares",
"search_page_places": "Lugares", "search_page_places": "Lugares",
"search_page_things": "Cosas", "search_page_things": "Objetos",
"search_result_page_new_search_hint": "Nueva Busqueda", "search_result_page_new_search_hint": "Nueva squeda",
"select_additional_user_for_sharing_page_suggestions": "Sugerencias", "select_additional_user_for_sharing_page_suggestions": "Sugerencias",
"select_user_for_sharing_page_err_album": "Fallo al crear el álbum", "select_user_for_sharing_page_err_album": "Fallo al crear el álbum",
"select_user_for_sharing_page_share_suggestions": "Suggestions", "select_user_for_sharing_page_share_suggestions": "Sugerencias",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_help": "El visor carga primero la miniatura pequeña, luego carga la de tamaño medio (si está activada) y por último la original (si está activada).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", "setting_image_viewer_original_subtitle": "Activar para cargar la imagen original (¡grande!). Desactivar para reducir el uso de datos (tanto de red como de caché).",
"setting_image_viewer_original_title": "Load original image", "setting_image_viewer_original_title": "Cargar imágenes originales",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Desactivar para cargar directamente el original o utilizar sólo la miniatura.",
"setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_preview_title": "Cargar miniaturas",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_failures_grace_period": "Notificar fallos de carga en segundo plano: {}",
"setting_notifications_notify_hours": "{} hours", "setting_notifications_notify_hours": "{} horas",
"setting_notifications_notify_immediately": "immediately", "setting_notifications_notify_immediately": "inmediatamente",
"setting_notifications_notify_minutes": "{} minutes", "setting_notifications_notify_minutes": "{} minutos",
"setting_notifications_notify_never": "never", "setting_notifications_notify_never": "nunca",
"setting_notifications_notify_seconds": "{} seconds", "setting_notifications_notify_seconds": "{} segundos",
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", "setting_notifications_single_progress_subtitle": "Información detallada por cada elemento en copia",
"setting_notifications_single_progress_title": "Show background backup detail progress", "setting_notifications_single_progress_title": "Mostrar detalles de la subida",
"setting_notifications_subtitle": "Adjust your notification preferences", "setting_notifications_subtitle": "Ajusta tus preferencias de notificación",
"setting_notifications_title": "Notifications", "setting_notifications_title": "Notificaciones",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_subtitle": "Progreso general de la subida (subidos/total elementos)",
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Mostrar progreso de la subida",
"setting_pages_app_bar_settings": "Settings", "setting_pages_app_bar_settings": "Ajustes",
"settings_require_restart": "Please restart Immich to apply this setting", "settings_require_restart": "Por favor, reinica Immich para aplicar estos cambios",
"share_add": "Añadir", "share_add": "Añadir",
"share_add_photos": "Añadir fotos", "share_add_photos": "Añadir fotos",
"share_add_title": "Añadir un título", "share_add_title": "Añadir un título",
"share_create_album": "Crear álbum", "share_create_album": "Crear álbum",
"share_dialog_preparing": "Preparing...", "share_dialog_preparing": "Preparando...",
"share_invite": "Invitar al álbum", "share_invite": "Invitar al álbum",
"sharing_page_album": "Álbumes compartidos", "sharing_page_album": "Álbumes compartidos",
"sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con las personas de tu red.", "sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con otros usuarios.",
"sharing_page_empty_list": "LISTA VACIA", "sharing_page_empty_list": "LISTA VACÍA",
"sharing_silver_appbar_create_shared_album": "Crear un álbum compartido", "sharing_silver_appbar_create_shared_album": "Crear un álbum compartido",
"sharing_silver_appbar_share_partner": "Compartir con el compañero", "sharing_silver_appbar_share_partner": "Compartir con pareja",
"tab_controller_nav_library": "Library", "tab_controller_nav_library": "Álbumes",
"tab_controller_nav_photos": "Fotos", "tab_controller_nav_photos": "Galería",
"tab_controller_nav_search": "Buscar", "tab_controller_nav_search": "Buscar",
"tab_controller_nav_sharing": "Compartiendo", "tab_controller_nav_sharing": "Compartido",
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_storage_indicator_title": "Mostrar estado de subida en cada elemento",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})",
"theme_setting_dark_mode_switch": "Dark mode", "theme_setting_dark_mode_switch": "Modo oscuro",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_subtitle": "Ajusta la calidad del visor",
"theme_setting_image_viewer_quality_title": "Image viewer quality", "theme_setting_image_viewer_quality_title": "Calidad del visor",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_system_theme_switch": "Automático (Según ajustes del sistema)",
"theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_subtitle": "Elige el tema de la app",
"theme_setting_theme_title": "Theme", "theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_subtitle": "La carga en tres fases podría aumentar el rendimiento pero provoca una mayor carga de red",
"theme_setting_three_stage_loading_title": "Enable three-stage loading", "theme_setting_three_stage_loading_title": "Activar carga en tres fases",
"version_announcement_overlay_ack": "Reconocer", "version_announcement_overlay_ack": "Reconocer",
"version_announcement_overlay_release_notes": "notas de versión", "version_announcement_overlay_release_notes": "Notas de la versión",
"version_announcement_overlay_text_1": "Hola amigo, hay una nueva versión de", "version_announcement_overlay_text_1": "¡Hola! Hay una nueva versión de",
"version_announcement_overlay_text_2": "tómese su tiempo para visitar la ", "version_announcement_overlay_text_2": "tómate tu tiempo para visitar la ",
"version_announcement_overlay_text_3": "y asegurate de que tu configuración de docker-compose y .env está actualizada para evitar cualquier desconfiguración, especialmente si utiliza WatchTower o cualquier mecanismo que se encargue de actualizar su aplicación de servidor automáticamente.", "version_announcement_overlay_text_3": "y asegúrate de que tu docker-compose y .env están actualizados para prevenir cualquier configuración errónea, especialmente si usas WatchTower o cualquier otro mecanismo que automatice la actualización.",
"version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89" "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89"
} }

View File

@@ -9,6 +9,7 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**id** | **String** | | **id** | **String** | |
**email** | **String** | | [optional]
**password** | **String** | | [optional] **password** | **String** | | [optional]
**firstName** | **String** | | [optional] **firstName** | **String** | | [optional]
**lastName** | **String** | | [optional] **lastName** | **String** | | [optional]

View File

@@ -14,6 +14,7 @@ class UpdateUserDto {
/// Returns a new [UpdateUserDto] instance. /// Returns a new [UpdateUserDto] instance.
UpdateUserDto({ UpdateUserDto({
required this.id, required this.id,
this.email,
this.password, this.password,
this.firstName, this.firstName,
this.lastName, this.lastName,
@@ -24,6 +25,14 @@ class UpdateUserDto {
String id; String id;
///
/// 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? email;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// 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 /// does not include a default value (using the "default:" property), however, the generated
@@ -75,6 +84,7 @@ class UpdateUserDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto && bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
other.id == id && other.id == id &&
other.email == email &&
other.password == password && other.password == password &&
other.firstName == firstName && other.firstName == firstName &&
other.lastName == lastName && other.lastName == lastName &&
@@ -86,6 +96,7 @@ class UpdateUserDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(id.hashCode) + (id.hashCode) +
(email == null ? 0 : email!.hashCode) +
(password == null ? 0 : password!.hashCode) + (password == null ? 0 : password!.hashCode) +
(firstName == null ? 0 : firstName!.hashCode) + (firstName == null ? 0 : firstName!.hashCode) +
(lastName == null ? 0 : lastName!.hashCode) + (lastName == null ? 0 : lastName!.hashCode) +
@@ -94,11 +105,16 @@ class UpdateUserDto {
(profileImagePath == null ? 0 : profileImagePath!.hashCode); (profileImagePath == null ? 0 : profileImagePath!.hashCode);
@override @override
String toString() => 'UpdateUserDto[id=$id, password=$password, firstName=$firstName, lastName=$lastName, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword, profileImagePath=$profileImagePath]'; String toString() => 'UpdateUserDto[id=$id, email=$email, password=$password, firstName=$firstName, lastName=$lastName, isAdmin=$isAdmin, shouldChangePassword=$shouldChangePassword, profileImagePath=$profileImagePath]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
_json[r'id'] = id; _json[r'id'] = id;
if (email != null) {
_json[r'email'] = email;
} else {
_json[r'email'] = null;
}
if (password != null) { if (password != null) {
_json[r'password'] = password; _json[r'password'] = password;
} else { } else {
@@ -152,6 +168,7 @@ class UpdateUserDto {
return UpdateUserDto( return UpdateUserDto(
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
email: mapValueOfType<String>(json, r'email'),
password: mapValueOfType<String>(json, r'password'), password: mapValueOfType<String>(json, r'password'),
firstName: mapValueOfType<String>(json, r'firstName'), firstName: mapValueOfType<String>(json, r'firstName'),
lastName: mapValueOfType<String>(json, r'lastName'), lastName: mapValueOfType<String>(json, r'lastName'),

View File

@@ -21,6 +21,11 @@ void main() {
// TODO // TODO
}); });
// String email
test('to test the property `email`', () async {
// TODO
});
// String password // String password
test('to test the property `password`', () async { test('to test the property `password`', () async {
// TODO // TODO

View File

@@ -1,9 +1,13 @@
import { IsNotEmpty, IsOptional } from 'class-validator'; import { IsEmail, IsNotEmpty, IsOptional } from 'class-validator';
export class UpdateUserDto { export class UpdateUserDto {
@IsNotEmpty() @IsNotEmpty()
id!: string; id!: string;
@IsEmail()
@IsOptional()
email?: string;
@IsOptional() @IsOptional()
password?: string; password?: string;

View File

@@ -28,11 +28,32 @@ export class UserCore {
throw new BadRequestException('Admin user exists'); throw new BadRequestException('Admin user exists');
} }
if (dto.email) {
const duplicate = await this.userRepository.getByEmail(dto.email);
if (duplicate && duplicate.id !== id) {
throw new BadRequestException('Email already in user by another account');
}
}
const user = await this.userRepository.get(id);
if (!user) {
throw new NotFoundException('User not found');
}
try { try {
if (dto.password) { if (dto.password) {
dto.password = await hash(dto.password, SALT_ROUNDS); dto.password = await hash(dto.password, SALT_ROUNDS);
} }
return this.userRepository.update(id, dto);
user.password = dto.password ?? user.password;
user.email = dto.email ?? user.email;
user.firstName = dto.firstName ?? user.firstName;
user.lastName = dto.lastName ?? user.lastName;
user.isAdmin = dto.isAdmin ?? user.isAdmin;
user.shouldChangePassword = dto.shouldChangePassword ?? user.shouldChangePassword;
user.profileImagePath = dto.profileImagePath ?? user.profileImagePath;
return this.userRepository.update(id, user);
} catch (e) { } catch (e) {
Logger.error(e, 'Failed to update user info'); Logger.error(e, 'Failed to update user info');
throw new InternalServerErrorException('Failed to update user info'); throw new InternalServerErrorException('Failed to update user info');

View File

@@ -102,6 +102,31 @@ describe('UserService', () => {
await expect(result).rejects.toBeInstanceOf(ForbiddenException); await expect(result).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('should let a user change their email', async () => {
const dto = { id: immichUser.id, email: 'updated@test.com' };
userRepositoryMock.get.mockResolvedValue(immichUser);
userRepositoryMock.update.mockResolvedValue(immichUser);
await sut.updateUser(immichUser, dto);
expect(userRepositoryMock.update).toHaveBeenCalledWith(immichUser.id, {
id: 'immich_id',
email: 'updated@test.com',
});
});
it('should not let a user change their email to one already in use', async () => {
const dto = { id: immichUser.id, email: 'updated@test.com' };
userRepositoryMock.get.mockResolvedValue(immichUser);
userRepositoryMock.getByEmail.mockResolvedValue(adminUser);
await expect(sut.updateUser(immichUser, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(userRepositoryMock.update).not.toHaveBeenCalled();
});
it('admin can update any user information', async () => { it('admin can update any user information', async () => {
const update: UpdateUserDto = { const update: UpdateUserDto = {
id: immichUser.id, id: immichUser.id,

View File

@@ -2400,6 +2400,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"email": {
"type": "string"
},
"password": { "password": {
"type": "string" "type": "string"
}, },

View File

@@ -1779,6 +1779,12 @@ export interface UpdateUserDto {
* @memberof UpdateUserDto * @memberof UpdateUserDto
*/ */
'id': string; 'id': string;
/**
*
* @type {string}
* @memberof UpdateUserDto
*/
'email'?: string;
/** /**
* *
* @type {string} * @type {string}

View File

@@ -1,5 +1,6 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export enum SettingInputFieldType { export enum SettingInputFieldType {
EMAIL = 'email',
TEXT = 'text', TEXT = 'text',
NUMBER = 'number', NUMBER = 'number',
PASSWORD = 'password' PASSWORD = 'password'

View File

@@ -1,6 +1,10 @@
<script lang="ts"> <script lang="ts">
import { api } from '@api'; import { api } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
let error: string; let error: string;
let success: string; let success: string;
@@ -38,23 +42,35 @@
const firstName = form.get('firstName'); const firstName = form.get('firstName');
const lastName = form.get('lastName'); const lastName = form.get('lastName');
const { status } = await api.userApi.createUser({ try {
email: String(email), const { status } = await api.userApi.createUser({
password: String(password), email: String(email),
firstName: String(firstName), password: String(password),
lastName: String(lastName) firstName: String(firstName),
}); lastName: String(lastName)
});
if (status === 201) { if (status === 201) {
success = 'New user created'; success = 'New user created';
dispatch('user-created'); dispatch('user-created');
isCreatingUser = false; isCreatingUser = false;
return; return;
} else { } else {
error = 'Error create user account';
isCreatingUser = false;
}
} catch (e) {
error = 'Error create user account'; error = 'Error create user account';
isCreatingUser = false; isCreatingUser = false;
console.log('[ERROR] registerUser', e);
notificationController.show({
message: `Error create new user, check console for more detail`,
type: NotificationType.Error
});
} }
} }
} }

View File

@@ -5,6 +5,7 @@
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { api, UserResponseDto } from '@api'; import { api, UserResponseDto } from '@api';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error';
import SettingInputField, { import SettingInputField, {
SettingInputFieldType SettingInputFieldType
} from '../admin-page/settings/setting-input-field.svelte'; } from '../admin-page/settings/setting-input-field.svelte';
@@ -15,6 +16,7 @@
try { try {
const { data } = await api.userApi.updateUser({ const { data } = await api.userApi.updateUser({
id: user.id, id: user.id,
email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName lastName: user.lastName
}); });
@@ -26,11 +28,7 @@
type: NotificationType.Info type: NotificationType.Info
}); });
} catch (error) { } catch (error) {
console.error('Error [user-profile] [updateProfile]', error); handleError(error, 'Unable to save profile');
notificationController.show({
message: 'Unable to save profile',
type: NotificationType.Error
});
} }
}; };
</script> </script>
@@ -47,10 +45,9 @@
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.EMAIL}
label="Email" label="Email"
bind:value={user.email} bind:value={user.email}
disabled={true}
/> />
<SettingInputField <SettingInputField