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:
+98
-3
@@ -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}}"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user