feat: photo/video capture-date pipeline + premium UI/UX overhaul

Backend — Photos timeline now groups by real capture date instead of upload time. New MediaMetadataService (FileLifecycleHook) extracts EXIF DateTimeOriginal from images and container creation_time from videos (mov/mp4/mkv) via nom-exif, timezone-correct (OffsetTimeOriginal), persisting captured_at so the existing media_sort_date trigger takes over. Adds POST /admin/photos/metadata/reextract to backfill existing media. Falls back to upload date when no embedded date exists.

Frontend — premium grid cards: combined metadata line (relative date · size, owner avatar when shared), custom selection checkbox with a clear checked state, uniform full-width 4:3 thumbnail tiles independent of filename length, centered file-type icons, and a hit-test fix so checkbox/star/kebab clicks reach the controls (the decorative thumbnail no longer captures pointer events). Notification messages internationalised across all 16 locales. Broader polish: design tokens, a11y/focus-visible states, brand + PWA assets.

Chore — bump semver-compatible dependencies (cargo upgrade); add nom-exif 3.6.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-15 00:17:17 +02:00
parent dac299fea6
commit 81a93a489b
129 changed files with 8194 additions and 2473 deletions
+98 -3
View File
@@ -151,7 +151,22 @@
"shareCreated": "Enlace compartido creado correctamente",
"shareUpdated": "Configuración de compartido actualizada",
"shareRemoved": "Compartido eliminado correctamente",
"inviteByEmail": "Invitar por correo — se enviará una invitación"
"inviteByEmail": "Invitar por correo — se enviará una invitación",
"directoryUnavailable": "Directorio de usuarios no disponible",
"linkNamePlaceholder": "Nombre del enlace (opcional)",
"newLink": "Nuevo enlace",
"noExpiry": "Sin caducidad",
"pending": "Pendiente",
"people": "Personas",
"publicLinks": "Enlaces públicos",
"role": {
"canEdit": "Puede editar",
"canManage": "Puede gestionar",
"canView": "Puede ver"
},
"searchPlaceholder": "Buscar personas…",
"shareOf": "Compartir:",
"sharedLink": "Enlace compartido"
},
"share_dialogTitle": "Compartir Enlace",
"share_linkLabel": "Enlace compartido:",
@@ -277,7 +292,11 @@
"close": "Cerrar",
"delete_permanently": "Eliminar permanentemente",
"empty_trash": "Vaciar papelera",
"open_parent_folder": "Ir a la carpeta padre"
"open_parent_folder": "Ir a la carpeta padre",
"add": "Añadir",
"apply": "Aplicar",
"clear": "Limpiar",
"remove": "Quitar"
},
"user_menu": {
"appearance": "Apariencia",
@@ -460,7 +479,10 @@
"magicLinkSubmit": "Enviar enlace de inicio de sesión",
"magicLinkSent": "Si existe una cuenta para ese correo, se ha enviado un enlace de inicio de sesión. Revisa tu bandeja de entrada.",
"magicLinkUnavailable": "El inicio de sesión por correo electrónico no está disponible en este servidor.",
"magicLinkNetworkError": "No se pudo conectar con el servidor: {{message}}"
"magicLinkNetworkError": "No se pudo conectar con el servidor: {{message}}",
"magicLinkToggle": "¿Sin contraseña? Recíbelo por correo",
"passwordsMatch": "Las contraseñas coinciden",
"capsLock": "Bloq Mayús activado"
},
"storage": {
"title": "Almacenamiento",
@@ -881,5 +903,78 @@
"notifyRateLimited": "Demasiadas notificaciones para este destinatario — inténtalo más tarde.",
"removeAccess": "Quitar acceso",
"resendInvitation": "Reenviar correo de invitación"
},
"sort": {
"asc": "ascendente",
"desc": "descendente"
},
"notif": {
"errorTitle": "Error",
"searchError": "Error al realizar la búsqueda",
"cleanupCompleted": "Limpieza completada",
"cleanupCompletedBody": "Se ha borrado el historial de archivos recientes",
"batchCopy": "Copia en lote",
"batchCopyBody": "{{success}} copiados, {{errors}} fallidos",
"itemsCopied": "Elementos copiados",
"itemsCopiedBody": "{{count}} elementos copiados correctamente",
"batchMove": "Movimiento en lote",
"batchMoveBody": "{{success}} movidos, {{errors}} fallidos",
"itemsMoved": "Elementos movidos",
"itemsMovedBody": "{{count}} elementos movidos correctamente",
"batchDelete": "Borrado en lote",
"batchDeleteBody": "{{success}} movidos a la papelera, {{errors}} fallidos",
"movedToTrash": "Movido a la papelera",
"movedToTrashBody": "{{count}} elementos movidos a la papelera",
"trashItemsError": "No se pudieron mover los elementos a la papelera",
"preparingDownload": "Preparando descarga",
"preparingDownloadBody": "Preparando la descarga…",
"downloadItemsError": "No se pudieron descargar los elementos seleccionados",
"favoritesAddError": "No se pudieron añadir los elementos a favoritos",
"invalidEmail": "Introduce una dirección de correo válida",
"notificationSendError": "No se pudo enviar la notificación",
"folderCreated": "Carpeta creada",
"folderCreatedBody": "«{{name}}» creada correctamente",
"fileMoved": "Archivo movido",
"fileMovedBody": "Archivo movido correctamente",
"fileMoveError": "Error al mover el archivo: {{error}}",
"fileMoveErrorGeneric": "Error al mover el archivo",
"folderMoved": "Carpeta movida",
"folderMovedBody": "Carpeta movida correctamente",
"folderMoveError": "Error al mover la carpeta: {{error}}",
"folderMoveErrorGeneric": "Error al mover la carpeta",
"fileCopied": "Archivo copiado",
"fileCopiedBody": "Archivo copiado correctamente",
"fileCopyError": "Error al copiar el archivo: {{error}}",
"fileCopyErrorGeneric": "Error al copiar el archivo",
"folderRenamed": "Carpeta renombrada",
"folderRenamedBody": "Carpeta renombrada a «{{name}}»",
"fileTrashed": "Archivo movido a la papelera",
"fileTrashedBody": "«{{name}}» movido a la papelera",
"fileDeleted": "Archivo eliminado",
"fileDeletedBody": "«{{name}}» eliminado correctamente",
"fileDeleteError": "Error al eliminar el archivo",
"folderTrashed": "Carpeta movida a la papelera",
"folderTrashedBody": "«{{name}}» movida a la papelera",
"folderDeleted": "Carpeta eliminada",
"folderDeletedBody": "«{{name}}» eliminada correctamente",
"folderDeleteError": "Error al eliminar la carpeta",
"itemRestored": "Elemento restaurado",
"itemRestoredBody": "Elemento restaurado correctamente",
"itemRestoreError": "Error al restaurar el elemento",
"itemDeleted": "Elemento eliminado",
"itemDeletedBody": "Elemento eliminado permanentemente",
"itemDeleteError": "Error al eliminar el elemento",
"trashEmptied": "Papelera vaciada",
"trashEmptiedBody": "La papelera se ha vaciado correctamente",
"trashEmptyError": "Error al vaciar la papelera",
"cacheCleared": "Caché limpiada",
"cacheClearedBody": "Caché de búsqueda limpiada correctamente",
"cacheClearError": "Error al limpiar la caché de búsqueda",
"wopiOpenError": "No se pudo abrir el editor de documentos.",
"linkCopied": "Enlace copiado",
"linkCopiedBody": "Enlace copiado al portapapeles",
"linkCopyError": "No se pudo copiar el enlace",
"notificationSent": "Notificación enviada",
"notificationSentBody": "Notificación enviada a {{email}}"
}
}