Merge branch 'main' into monorepo
This commit is contained in:
commit
16274fa6e1
42 changed files with 2391 additions and 1377 deletions
|
@ -23,6 +23,10 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
|
|||
module.exports = (phase) =>
|
||||
withSentryConfig(
|
||||
withBundleAnalyzer({
|
||||
sentry: {
|
||||
hideSourceMaps: false,
|
||||
widenClientFileUpload: true,
|
||||
},
|
||||
compiler: {
|
||||
emotion: {
|
||||
importMap: {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"@mui/icons-material": "^5.6.2",
|
||||
"@mui/material": "^5.6.2",
|
||||
"@mui/x-date-pickers": "^5.0.0-alpha.6",
|
||||
"@sentry/nextjs": "^6.7.1",
|
||||
"@sentry/nextjs": "^7.49.0",
|
||||
"@stripe/stripe-js": "^1.13.2",
|
||||
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||
"@tensorflow/tfjs-backend-cpu": "^3.13.0",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "",
|
||||
"UNCATEGORIZED": "",
|
||||
"ARCHIVE": "",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "",
|
||||
"ARCHIVE_SECTION_NAME": "",
|
||||
"ALL_SECTION_NAME": "",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "Upload to album",
|
||||
"UNCATEGORIZED": "Uncategorized",
|
||||
"ARCHIVE": "Archive",
|
||||
"FAVORITES": "Favorites",
|
||||
"ARCHIVE_COLLECTION": "Archive album",
|
||||
"ARCHIVE_SECTION_NAME": "Archive",
|
||||
"ALL_SECTION_NAME": "All",
|
||||
|
@ -488,7 +489,7 @@
|
|||
"COLLECT_PHOTOS": "Collect photos",
|
||||
"PUBLIC_COLLECT_SUBTEXT": "Allow people with the link to also add photos to the shared album.",
|
||||
"STOP_EXPORT": "Stop",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> files exported",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> items synced",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "Export started",
|
||||
"IN_PROGRESS": "Export already in progress",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"HERO_SLIDE_3": "Android, iOS, web, computadora",
|
||||
"LOGIN": "Conectar",
|
||||
"SIGN_UP": "Registro",
|
||||
"NEW_USER": "Nuevo a ente",
|
||||
"NEW_USER": "Nuevo en ente",
|
||||
"EXISTING_USER": "Usuario existente",
|
||||
"ENTER_NAME": "Introducir nombre",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "¡Añade un nombre para que tus amigos sepan a quién dar las gracias por estas fotos geniales!",
|
||||
|
@ -82,9 +82,9 @@
|
|||
"ZOOM_IN_OUT": "Acercar/alejar",
|
||||
"PREVIOUS": "Anterior (←)",
|
||||
"NEXT": "Siguiente (→)",
|
||||
"TITLE_PHOTOS": "",
|
||||
"TITLE_ALBUMS": "",
|
||||
"TITLE_AUTH": "",
|
||||
"TITLE_PHOTOS": "ente Fotos",
|
||||
"TITLE_ALBUMS": "ente Álbumes",
|
||||
"TITLE_AUTH": "ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Carga tu primer archivo",
|
||||
"IMPORT_YOUR_FOLDERS": "Importar tus carpetas",
|
||||
"UPLOAD_DROPZONE_MESSAGE": "Soltar para respaldar tus archivos",
|
||||
|
@ -198,9 +198,9 @@
|
|||
"DOWNLOAD_COLLECTION": "Descargar álbum",
|
||||
"DOWNLOAD_COLLECTION_MESSAGE": "<p>¿Está seguro de que desea descargar el álbum completo?</p><p>Todos los archivos se pondrán en cola para su descarga secuencialmente</p>",
|
||||
"CREATE_ALBUM_FAILED": "Error al crear el álbum, inténtalo de nuevo",
|
||||
"SEARCH": "",
|
||||
"SEARCH": "Buscar",
|
||||
"SEARCH_RESULTS": "Buscar resultados",
|
||||
"NO_RESULTS": "",
|
||||
"NO_RESULTS": "No se han encontrado resultados",
|
||||
"SEARCH_HINT": "Buscar álbumes, fechas...",
|
||||
"SEARCH_TYPE": {
|
||||
"COLLECTION": "Álbum",
|
||||
|
@ -255,7 +255,7 @@
|
|||
"UPDATE_TWO_FACTOR_MESSAGE": "Continuar adelante anulará los autenticadores previamente configurados",
|
||||
"UPDATE": "Actualizar",
|
||||
"DISABLE_TWO_FACTOR": "Desactivar doble factor",
|
||||
"DISABLE_TWO_FACTOR_MESSAGE": "",
|
||||
"DISABLE_TWO_FACTOR_MESSAGE": "¿Estás seguro de que desea deshabilitar la autenticación de doble factor?",
|
||||
"TWO_FACTOR_DISABLE_FAILED": "Error al desactivar dos factores, inténtalo de nuevo",
|
||||
"EXPORT_DATA": "Exportar datos",
|
||||
"SELECT_FOLDER": "Seleccionar carpeta",
|
||||
|
@ -267,16 +267,16 @@
|
|||
"LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Su navegador o un addon está bloqueando a ente de guardar datos en almacenamiento local. Por favor, intente cargar esta página después de cambiar su modo de navegación.",
|
||||
"SEND_OTT": "Enviar OTP",
|
||||
"EMAIl_ALREADY_OWNED": "Email ya tomado",
|
||||
"ETAGS_BLOCKED": "",
|
||||
"SKIPPED_VIDEOS_INFO": "",
|
||||
"LIVE_PHOTOS_DETECTED": "",
|
||||
"ETAGS_BLOCKED": "<p>No hemos podido subir los siguientes archivos debido a la configuración de tu navegador.</p><p>Por favor, deshabilite cualquier complemento que pueda estar impidiendo que ente utilice <code>eTags</code> para subir archivos grandes, o utilice nuestra <a>aplicación de escritorio</a> para una experiencia de importación más fiable.</p>",
|
||||
"SKIPPED_VIDEOS_INFO": "<p>Actualmente no podemos añadir vídeos a través de enlaces públicos.</p><p>Para compartir vídeos, por favor <a>regístrate</a> en ente y comparte con los destinatarios a través de su correo electrónico.</p>",
|
||||
"LIVE_PHOTOS_DETECTED": "Los archivos de foto y vídeo de tus fotos en vivo se han fusionado en un solo archivo",
|
||||
"RETRY_FAILED": "Reintentar subidas fallidas",
|
||||
"FAILED_UPLOADS": "Subidas fallidas ",
|
||||
"SKIPPED_FILES": "Subidas ignoradas",
|
||||
"THUMBNAIL_GENERATION_FAILED_UPLOADS": "Generación de miniaturas fallida",
|
||||
"UNSUPPORTED_FILES": "Archivos no soportados",
|
||||
"SUCCESSFUL_UPLOADS": "Subidas exitosas",
|
||||
"SKIPPED_INFO": "",
|
||||
"SKIPPED_INFO": "Se han omitido ya que hay archivos con nombres coincidentes en el mismo álbum",
|
||||
"UNSUPPORTED_INFO": "ente no soporta estos formatos de archivo aún",
|
||||
"BLOCKED_UPLOADS": "Subidas bloqueadas",
|
||||
"SKIPPED_VIDEOS": "Vídeos saltados",
|
||||
|
@ -284,12 +284,13 @@
|
|||
"INPROGRESS_UPLOADS": "Subidas en progreso",
|
||||
"TOO_LARGE_UPLOADS": "Archivos grandes",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "Espacio insuficiente",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_INFO": "",
|
||||
"TOO_LARGE_INFO": "",
|
||||
"THUMBNAIL_GENERATION_FAILED_INFO": "",
|
||||
"LARGER_THAN_AVAILABLE_STORAGE_INFO": "Estos archivos no se han subido porque exceden el límite de tamaño máximo para tu plan de almacenamiento",
|
||||
"TOO_LARGE_INFO": "Estos archivos no se han subido porque exceden nuestro límite máximo de tamaño de archivo",
|
||||
"THUMBNAIL_GENERATION_FAILED_INFO": "Estos archivos fueron cargados, pero por desgracia no pudimos generar las miniaturas para ellos.",
|
||||
"UPLOAD_TO_COLLECTION": "Subir al álbum",
|
||||
"UNCATEGORIZED": "No clasificado",
|
||||
"ARCHIVE": "Archivo",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "Archivo álbum",
|
||||
"ARCHIVE_SECTION_NAME": "Archivo",
|
||||
"ALL_SECTION_NAME": "Todo",
|
||||
|
@ -316,8 +317,8 @@
|
|||
"LEAVE_SHARED_ALBUM_TITLE": "¿Dejar álbum compartido?",
|
||||
"LEAVE_SHARED_ALBUM_MESSAGE": "Dejará el álbum, y dejará de ser visible para usted.",
|
||||
"NOT_FILE_OWNER": "No puedes eliminar archivos de un álbum compartido",
|
||||
"CONFIRM_SELF_REMOVE_MESSAGE": "",
|
||||
"CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "",
|
||||
"CONFIRM_SELF_REMOVE_MESSAGE": "Los elementos seleccionados serán eliminados de este álbum. Los elementos que estén sólo en este álbum serán movidos a Sin categorizar.",
|
||||
"CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Algunos de los elementos que estás eliminando fueron añadidos por otras personas, y perderás el acceso a ellos.",
|
||||
"SORT_BY_CREATION_TIME_ASCENDING": "Antiguo",
|
||||
"SORT_BY_UPDATION_TIME_DESCENDING": "Última actualización",
|
||||
"SORT_BY_NAME": "Nombre",
|
||||
|
@ -325,7 +326,7 @@
|
|||
"THUMBNAIL_REPLACED": "Miniaturas comprimidas",
|
||||
"FIX_THUMBNAIL": "Comprimir",
|
||||
"FIX_THUMBNAIL_LATER": "Comprimir más tarde",
|
||||
"REPLACE_THUMBNAIL_NOT_STARTED": "",
|
||||
"REPLACE_THUMBNAIL_NOT_STARTED": "Algunas de tus miniaturas de vídeos pueden ser comprimidas para ahorrar espacio. ¿Te gustaría que ente las comprima?",
|
||||
"REPLACE_THUMBNAIL_COMPLETED": "Todas las miniaturas se comprimieron con éxito",
|
||||
"REPLACE_THUMBNAIL_NOOP": "No tienes miniaturas que se puedan comprimir más",
|
||||
"REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "No se pudieron comprimir algunas de tus miniaturas, por favor inténtalo de nuevo",
|
||||
|
@ -334,7 +335,7 @@
|
|||
"CREATION_TIME_UPDATED": "Hora del archivo actualizada",
|
||||
"UPDATE_CREATION_TIME_NOT_STARTED": "Seleccione la cartera que desea utilizar",
|
||||
"UPDATE_CREATION_TIME_COMPLETED": "Todos los archivos se han actualizado correctamente",
|
||||
"UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "",
|
||||
"UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "Fallo en la hora del archivo para algunos archivos, por favor inténtelo de nuevo",
|
||||
"CAPTION_CHARACTER_LIMIT": "Máximo 5000 caracteres",
|
||||
"DATE_TIME_ORIGINAL": "EXIF: Fecha original",
|
||||
"DATE_TIME_DIGITIZED": "EXIF: Fecha Digitalizado",
|
||||
|
@ -356,7 +357,7 @@
|
|||
"LINK_EXPIRY": "Enlace vencio",
|
||||
"NEVER": "Nunca",
|
||||
"DISABLE_FILE_DOWNLOAD": "Deshabilitar descarga",
|
||||
"DISABLE_FILE_DOWNLOAD_MESSAGE": "",
|
||||
"DISABLE_FILE_DOWNLOAD_MESSAGE": "<p>¿Está seguro que desea desactivar el botón de descarga de archivos?</p><p>Los visualizadores todavía pueden tomar capturas de pantalla o guardar una copia de sus fotos usando herramientas externas.</p>",
|
||||
"MALICIOUS_CONTENT": "Contiene contenido malicioso",
|
||||
"COPYRIGHT": "Infracciones sobre los derechos de autor de alguien que estoy autorizado a representar",
|
||||
"SHARED_USING": "Compartido usando ",
|
||||
|
@ -376,9 +377,9 @@
|
|||
"CLUB_BY_CAPTURE_TIME": "Club por tiempo de captura",
|
||||
"FILES": "Archivos",
|
||||
"EACH": "Cada",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"DEDUPLICATE_BASED_ON_SIZE": "Los siguientes archivos fueron organizados en base a sus tamaños, por favor revise y elimine elementos que cree que son duplicados",
|
||||
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "Los siguientes archivos fueron organizados en base a sus tamaños y tiempo de captura, por favor revise y elimine elementos que cree que son duplicados",
|
||||
"STOP_ALL_UPLOADS_MESSAGE": "¿Está seguro que desea detener todas las subidas en curso?",
|
||||
"STOP_UPLOADS_HEADER": "Detener las subidas?",
|
||||
"YES_STOP_UPLOADS": "Sí, detener las subidas",
|
||||
"albums_one": "1 álbum",
|
||||
|
@ -389,7 +390,7 @@
|
|||
"CREATE_ACCOUNT": "Crear cuenta",
|
||||
"COPIED": "Copiado",
|
||||
"CANVAS_BLOCKED_TITLE": "No se puede generar la miniatura",
|
||||
"CANVAS_BLOCKED_MESSAGE": "",
|
||||
"CANVAS_BLOCKED_MESSAGE": "<p>Parece que su navegador ha deshabilitado el acceso al lienzo, que es necesario para generar miniaturas para tus fotos </p> <p> Por favor, activa el acceso al lienzo de tu navegador, o revisa nuestra aplicación de escritorio</p>",
|
||||
"WATCH_FOLDERS": "Ver carpetas",
|
||||
"UPGRADE_NOW": "Mejorar ahora",
|
||||
"RENEW_NOW": "Renovar ahora",
|
||||
|
@ -407,23 +408,23 @@
|
|||
"ADD_FOLDER": "Añadir carpeta",
|
||||
"STOP_WATCHING": "Dejar de ver",
|
||||
"STOP_WATCHING_FOLDER": "Dejar de ver carpeta?",
|
||||
"STOP_WATCHING_DIALOG_MESSAGE": "",
|
||||
"STOP_WATCHING_DIALOG_MESSAGE": "Tus archivos existentes no serán eliminados, pero ente dejará de actualizar automáticamente el álbum enlazado en caso de cambios en esta carpeta.",
|
||||
"YES_STOP": "Sí, detener",
|
||||
"MONTH_SHORT": "mes",
|
||||
"YEAR": "año",
|
||||
"FAMILY_PLAN": "Plan familiar",
|
||||
"DOWNLOAD_LOGS": "Descargar logs",
|
||||
"DOWNLOAD_LOGS_MESSAGE": "",
|
||||
"DOWNLOAD_LOGS_MESSAGE": "<p>Esto descargará los registros de depuración, que puede enviarnos por correo electrónico para ayudarnos a depurar su problema.</p><p> Tenga en cuenta que los nombres de los archivos se incluirán para ayudar al seguimiento de problemas con archivos específicos. </p>",
|
||||
"CHANGE_FOLDER": "Cambiar carpeta",
|
||||
"TWO_MONTHS_FREE": "Obtén 2 meses gratis en planes anuales",
|
||||
"GB": "GB",
|
||||
"POPULAR": "Popular",
|
||||
"FREE_PLAN_OPTION_LABEL": "Continuar con el plan gratuito",
|
||||
"FREE_PLAN_DESCRIPTION": "1 GB por 1 año",
|
||||
"CURRENT_USAGE": "",
|
||||
"WEAK_DEVICE": "",
|
||||
"CURRENT_USAGE": "El uso actual es <strong>{{usage}}</strong>",
|
||||
"WEAK_DEVICE": "El navegador web que está utilizando no es lo suficientemente poderoso para cifrar sus fotos. Por favor, intente iniciar sesión en ente en su computadora, o descargue la aplicación ente para móvil/escritorio.",
|
||||
"DRAG_AND_DROP_HINT": "O arrastre y suelte en la ventana ente",
|
||||
"CONFIRM_ACCOUNT_DELETION_MESSAGE": "",
|
||||
"CONFIRM_ACCOUNT_DELETION_MESSAGE": "Los datos subidos se eliminarán y su cuenta se eliminará de forma permanente.<br/><br/>Esta acción no es reversible.",
|
||||
"AUTHENTICATE": "Autenticado",
|
||||
"UPLOADED_TO_SINGLE_COLLECTION": "Subir a una sola colección",
|
||||
"UPLOADED_TO_SEPARATE_COLLECTIONS": "Subir a colecciones separadas",
|
||||
|
@ -442,15 +443,15 @@
|
|||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "<p>Has arrastrado y soltado una mezcla de archivos y carpetas.</p><p>Por favor proporcione sólo archivos o carpetas cuando seleccione la opción de crear álbumes separados</p>",
|
||||
"CHOSE_THEME": "Elegir tema",
|
||||
"ML_SEARCH": "Buscar ML (beta)",
|
||||
"ENABLE_ML_SEARCH_DESCRIPTION": "",
|
||||
"ENABLE_ML_SEARCH_DESCRIPTION": "<p>Esto permitirá el aprendizaje automático en el dispositivo y la búsqueda facial que comenzará a analizar las fotos subidas localmente.</p><p>Para la primera ejecución después de iniciar sesión o habilitar esta función, se descargarán todas las imágenes en el dispositivo local para analizarlas. Así que por favor actívalo sólo si dispones ancho de banda y el almacenamiento suficiente para el procesamiento local de todas las imágenes en tu biblioteca de fotos.</p><p>Si esta es la primera vez que está habilitando, también le pediremos su permiso para procesar los datos faciales.</p>",
|
||||
"ML_MORE_DETAILS": "Más detalles",
|
||||
"ENABLE_FACE_SEARCH": "Activar búsqueda facial",
|
||||
"ENABLE_FACE_SEARCH_TITLE": "Activar búsqueda facial?",
|
||||
"ENABLE_FACE_SEARCH_DESCRIPTION": "",
|
||||
"ENABLE_FACE_SEARCH_DESCRIPTION": "<p>Si activas la búsqueda facial, ente extraerá la geometría facial de tus fotos. Esto sucederá en su dispositivo y cualquier dato biométrico generado será cifrado de extremo a extremo.<p/><p><a>Haga clic aquí para obtener más detalles sobre esta característica en nuestra política de privacidad</a></p>",
|
||||
"DISABLE_BETA": "Desactivar beta",
|
||||
"DISABLE_FACE_SEARCH": "Desactivar búsqueda facial",
|
||||
"DISABLE_FACE_SEARCH_TITLE": "Desactivar búsqueda facial?",
|
||||
"DISABLE_FACE_SEARCH_DESCRIPTION": "",
|
||||
"DISABLE_FACE_SEARCH_DESCRIPTION": "<p>ente dejará de procesar la geometría facial, y también desactivará la búsqueda ML (beta)</p><p>Puede volver a activar la búsqueda facial si lo desea, ya que esta operación es segura.</p>",
|
||||
"ADVANCED": "Avanzado",
|
||||
"FACE_SEARCH_CONFIRMATION": "Comprendo y deseo permitir que ente procese la geometría de la cara",
|
||||
"LABS": "Labs",
|
||||
|
@ -461,8 +462,8 @@
|
|||
"PREFERENCES": "Preferencias",
|
||||
"LANGUAGE": "Idioma",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST": "Archivo de exportación inválido",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "<p>El directorio de exportación seleccionado no existe.</p><p> Por favor, seleccione un directorio válido.</p>",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "Falló la verificación de la suscripción",
|
||||
"STORAGE_UNITS": {
|
||||
"B": "B",
|
||||
"KB": "KB",
|
||||
|
@ -502,19 +503,19 @@
|
|||
"DELETE_ACCOUNT_REASON_LABEL": "¿Cuál es la razón principal por la que eliminas tu cuenta?",
|
||||
"DELETE_ACCOUNT_REASON_PLACEHOLDER": "Selecciona una razón",
|
||||
"DELETE_REASON": {
|
||||
"MISSING_FEATURE": "",
|
||||
"BROKEN_BEHAVIOR": "",
|
||||
"FOUND_ANOTHER_SERVICE": "",
|
||||
"NOT_LISTED": ""
|
||||
"MISSING_FEATURE": "Falta una característica clave que necesito",
|
||||
"BROKEN_BEHAVIOR": "La aplicación o una característica determinada no se comporta como creo que debería",
|
||||
"FOUND_ANOTHER_SERVICE": "He encontrado otro servicio que me gusta más",
|
||||
"NOT_LISTED": "Mi motivo no se encuentra en la lista"
|
||||
},
|
||||
"DELETE_ACCOUNT_FEEDBACK_LABEL": "",
|
||||
"DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "",
|
||||
"CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "",
|
||||
"DELETE_ACCOUNT_FEEDBACK_LABEL": "Lamentamos que te vayas. Explica por qué te vas para ayudarnos a mejorar.",
|
||||
"DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Sugerencias",
|
||||
"CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Sí, quiero eliminar permanentemente esta cuenta y todos sus datos",
|
||||
"CONFIRM_DELETE_ACCOUNT": "Corfirmar borrado de cuenta",
|
||||
"FEEDBACK_REQUIRED": "Ayúdanos con esta información",
|
||||
"FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "Qué hace mejor el otro servicio?",
|
||||
"RECOVER_TWO_FACTOR": "Recuperar dos factores",
|
||||
"at": "",
|
||||
"AUTH_NEXT": "",
|
||||
"AUTH_DOWNLOAD_MOBILE_APP": ""
|
||||
"at": "a las",
|
||||
"AUTH_NEXT": "siguiente",
|
||||
"AUTH_DOWNLOAD_MOBILE_APP": "Descarga nuestra aplicación móvil para administrar tus secretos"
|
||||
}
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "",
|
||||
"UNCATEGORIZED": "",
|
||||
"ARCHIVE": "",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "",
|
||||
"ARCHIVE_SECTION_NAME": "",
|
||||
"ALL_SECTION_NAME": "",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "上传至相册",
|
||||
"UNCATEGORIZED": "未分类的",
|
||||
"ARCHIVE": "存档",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "存档相册",
|
||||
"ARCHIVE_SECTION_NAME": "存档",
|
||||
"ALL_SECTION_NAME": "全部",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "Charger dans l'album",
|
||||
"UNCATEGORIZED": "Aucune catégorie",
|
||||
"ARCHIVE": "Archivé",
|
||||
"FAVORITES": "Favoris",
|
||||
"ARCHIVE_COLLECTION": "Archiver l'album",
|
||||
"ARCHIVE_SECTION_NAME": "Archiver",
|
||||
"ALL_SECTION_NAME": "Tous",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "",
|
||||
"UNCATEGORIZED": "",
|
||||
"ARCHIVE": "",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "",
|
||||
"ARCHIVE_SECTION_NAME": "",
|
||||
"ALL_SECTION_NAME": "",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "Uploaden naar album",
|
||||
"UNCATEGORIZED": "Ongecategoriseerd",
|
||||
"ARCHIVE": "Archiveren",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "Album archiveren",
|
||||
"ARCHIVE_SECTION_NAME": "Archief",
|
||||
"ALL_SECTION_NAME": "Alle",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "",
|
||||
"UNCATEGORIZED": "",
|
||||
"ARCHIVE": "",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "",
|
||||
"ARCHIVE_SECTION_NAME": "",
|
||||
"ALL_SECTION_NAME": "",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "",
|
||||
"UNCATEGORIZED": "",
|
||||
"ARCHIVE": "",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "",
|
||||
"ARCHIVE_SECTION_NAME": "",
|
||||
"ALL_SECTION_NAME": "",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "",
|
||||
"UNCATEGORIZED": "",
|
||||
"ARCHIVE": "",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "",
|
||||
"ARCHIVE_SECTION_NAME": "",
|
||||
"ALL_SECTION_NAME": "",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"UPLOAD_TO_COLLECTION": "",
|
||||
"UNCATEGORIZED": "",
|
||||
"ARCHIVE": "",
|
||||
"FAVORITES": "",
|
||||
"ARCHIVE_COLLECTION": "",
|
||||
"ARCHIVE_SECTION_NAME": "",
|
||||
"ALL_SECTION_NAME": "",
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import isElectron from 'is-electron';
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import exportService from 'services/exportService';
|
||||
import {
|
||||
ExportProgress,
|
||||
ExportRecord,
|
||||
ExportSettings,
|
||||
FileExportStats,
|
||||
} from 'types/export';
|
||||
import exportService from 'services/export';
|
||||
import { ExportProgress, ExportSettings, FileExportStats } from 'types/export';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
@ -19,7 +14,6 @@ import {
|
|||
Typography,
|
||||
} from '@mui/material';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import { SpaceBetweenFlex, VerticallyCenteredFlex } from './Container';
|
||||
import ExportFinished from './ExportFinished';
|
||||
import ExportInit from './ExportInit';
|
||||
|
@ -74,91 +68,60 @@ export default function ExportModal(props: Props) {
|
|||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
const main = async () => {
|
||||
try {
|
||||
exportService.setUIUpdaters({
|
||||
updateExportStage: updateExportStage,
|
||||
updateExportProgress: setExportProgress,
|
||||
updateFileExportStats: setFileExportStats,
|
||||
updateLastExportTime: updateExportTime,
|
||||
});
|
||||
const exportSettings: ExportSettings = getData(LS_KEYS.EXPORT);
|
||||
setExportFolder(exportSettings?.folder);
|
||||
setContinuousExport(exportSettings?.continuousExport);
|
||||
const exportRecord = await syncExportRecord(exportFolder);
|
||||
if (exportRecord?.stage === ExportStage.INPROGRESS) {
|
||||
startExport();
|
||||
}
|
||||
if (exportSettings?.continuousExport) {
|
||||
exportService.enableContinuousExport();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'export on mount useEffect failed');
|
||||
}
|
||||
};
|
||||
void main();
|
||||
try {
|
||||
exportService.setUIUpdaters({
|
||||
setExportStage,
|
||||
setExportProgress,
|
||||
setFileExportStats,
|
||||
setLastExportTime,
|
||||
});
|
||||
const exportSettings: ExportSettings =
|
||||
exportService.getExportSettings();
|
||||
setExportFolder(exportSettings?.folder);
|
||||
setContinuousExport(exportSettings?.continuousExport);
|
||||
void syncExportRecord(exportSettings?.folder);
|
||||
} catch (e) {
|
||||
logError(e, 'export on mount useEffect failed');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
void syncFileCounts();
|
||||
void syncExportRecord(exportFolder);
|
||||
}, [props.show]);
|
||||
|
||||
// =============
|
||||
// STATE UPDATERS
|
||||
// ==============
|
||||
const updateExportFolder = (newFolder: string) => {
|
||||
const exportSettings: ExportSettings = getData(LS_KEYS.EXPORT);
|
||||
const updatedExportSettings: ExportSettings = {
|
||||
...exportSettings,
|
||||
folder: newFolder,
|
||||
};
|
||||
setData(LS_KEYS.EXPORT, updatedExportSettings);
|
||||
exportService.updateExportSettings({ folder: newFolder });
|
||||
setExportFolder(newFolder);
|
||||
};
|
||||
|
||||
const updateContinuousExport = (updatedContinuousExport: boolean) => {
|
||||
const exportSettings: ExportSettings = getData(LS_KEYS.EXPORT);
|
||||
const updatedExportSettings: ExportSettings = {
|
||||
...exportSettings,
|
||||
exportService.updateExportSettings({
|
||||
continuousExport: updatedContinuousExport,
|
||||
};
|
||||
setData(LS_KEYS.EXPORT, updatedExportSettings);
|
||||
setContinuousExport(updatedContinuousExport);
|
||||
};
|
||||
|
||||
const updateExportStage = async (newStage: ExportStage) => {
|
||||
setExportStage(newStage);
|
||||
await exportService.updateExportRecord({ stage: newStage });
|
||||
};
|
||||
|
||||
const updateExportTime = async (newTime: number) => {
|
||||
setLastExportTime(newTime);
|
||||
await exportService.updateExportRecord({
|
||||
lastAttemptTimestamp: newTime,
|
||||
});
|
||||
setContinuousExport(updatedContinuousExport);
|
||||
};
|
||||
|
||||
// ======================
|
||||
// HELPER FUNCTIONS
|
||||
// =======================
|
||||
|
||||
const onExportFolderChange = async (newFolder: string) => {
|
||||
const onExportFolderChange = (newFolder: string) => {
|
||||
try {
|
||||
updateExportFolder(newFolder);
|
||||
syncExportRecord(newFolder);
|
||||
void syncExportRecord(newFolder);
|
||||
} catch (e) {
|
||||
logError(e, 'onExportChange failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const verifyExportFolderExists = () => {
|
||||
const exportFolder = getData(LS_KEYS.EXPORT)?.folder;
|
||||
const exportFolderExists = exportService.exists(exportFolder);
|
||||
if (!exportFolderExists) {
|
||||
if (!exportFolder || !exportService.exists(exportFolder)) {
|
||||
appContext.setDialogMessage(
|
||||
getExportDirectoryDoesNotExistMessage()
|
||||
);
|
||||
|
@ -166,33 +129,19 @@ export default function ExportModal(props: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
const syncExportRecord = async (
|
||||
exportFolder: string
|
||||
): Promise<ExportRecord> => {
|
||||
const syncExportRecord = async (exportFolder: string): Promise<void> => {
|
||||
try {
|
||||
const exportRecord = await exportService.getExportRecord(
|
||||
exportFolder
|
||||
);
|
||||
if (!exportRecord) {
|
||||
setExportStage(ExportStage.INIT);
|
||||
return null;
|
||||
}
|
||||
setExportStage(exportRecord.stage);
|
||||
setLastExportTime(exportRecord.lastAttemptTimestamp);
|
||||
void syncFileCounts();
|
||||
return exportRecord;
|
||||
} catch (e) {
|
||||
logError(e, 'syncExportRecord failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const syncFileCounts = async () => {
|
||||
try {
|
||||
const fileExportStats = await exportService.getFileExportStats();
|
||||
setExportStage(exportRecord?.stage ?? ExportStage.INIT);
|
||||
setLastExportTime(exportRecord?.lastAttemptTimestamp ?? 0);
|
||||
const fileExportStats = await exportService.getFileExportStats(
|
||||
exportRecord
|
||||
);
|
||||
setFileExportStats(fileExportStats);
|
||||
} catch (e) {
|
||||
logError(e, 'error updating file counts');
|
||||
logError(e, 'syncExportRecord failed');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -225,7 +174,7 @@ export default function ExportModal(props: Props) {
|
|||
const startExport = () => {
|
||||
try {
|
||||
verifyExportFolderExists();
|
||||
exportService.runExport();
|
||||
exportService.scheduleExport();
|
||||
} catch (e) {
|
||||
if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
|
||||
logError(e, 'startExport failed');
|
||||
|
@ -257,7 +206,7 @@ export default function ExportModal(props: Props) {
|
|||
<Typography color="text.muted">
|
||||
{t('TOTAL_ITEMS')}
|
||||
</Typography>
|
||||
<Typography color="text.muted">
|
||||
<Typography>
|
||||
{formatNumber(fileExportStats.totalCount)}
|
||||
</Typography>
|
||||
</SpaceBetweenFlex>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Button, Stack, styled, Typography } from '@mui/material';
|
||||
import { DeduplicateContext } from 'pages/deduplicate';
|
||||
import VerticallyCentered, { FlexWrapper } from './Container';
|
||||
import { Box } from '@mui/material';
|
||||
import uploadManager from 'services/upload/uploadManager';
|
||||
|
@ -21,19 +19,8 @@ const NonDraggableImage = styled('img')`
|
|||
pointer-events: none;
|
||||
`;
|
||||
|
||||
export default function EmptyScreen({ openUploader }) {
|
||||
const deduplicateContext = useContext(DeduplicateContext);
|
||||
return deduplicateContext.isOnDeduplicatePage ? (
|
||||
<VerticallyCentered>
|
||||
<div
|
||||
style={{
|
||||
color: '#a6a6a6',
|
||||
fontSize: '18px',
|
||||
}}>
|
||||
{t('NO_DUPLICATES_FOUND')}
|
||||
</div>
|
||||
</VerticallyCentered>
|
||||
) : (
|
||||
export default function GalleryEmptyState({ openUploader }) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Stack
|
||||
sx={{
|
|
@ -18,7 +18,6 @@ import { MergedSourceURL, SelectedState } from 'types/gallery';
|
|||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { useRouter } from 'next/router';
|
||||
import EmptyScreen from './EmptyScreen';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { DeduplicateContext } from 'pages/deduplicate';
|
||||
import { IsArchived } from 'utils/magicMetadata';
|
||||
|
@ -57,9 +56,6 @@ interface Props {
|
|||
selected: SelectedState | ((selected: SelectedState) => SelectedState)
|
||||
) => void;
|
||||
selected: SelectedState;
|
||||
isFirstLoad?;
|
||||
hasNoPersonalFiles?;
|
||||
openUploader?;
|
||||
isInSearchMode?: boolean;
|
||||
search?: Search;
|
||||
deletedFileIds?: Set<number>;
|
||||
|
@ -79,9 +75,6 @@ const PhotoFrame = ({
|
|||
archivedCollections,
|
||||
setSelected,
|
||||
selected,
|
||||
isFirstLoad,
|
||||
hasNoPersonalFiles,
|
||||
openUploader,
|
||||
isInSearchMode,
|
||||
search,
|
||||
deletedFileIds,
|
||||
|
@ -653,49 +646,40 @@ const PhotoFrame = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isFirstLoad &&
|
||||
hasNoPersonalFiles &&
|
||||
!isInSearchMode &&
|
||||
activeCollection === ALL_SECTION ? (
|
||||
<EmptyScreen openUploader={openUploader} />
|
||||
) : (
|
||||
<Container>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<PhotoList
|
||||
width={width}
|
||||
height={height}
|
||||
getThumbnail={getThumbnail}
|
||||
filteredData={filteredData}
|
||||
activeCollection={activeCollection}
|
||||
showAppDownloadBanner={
|
||||
files.length < 30 &&
|
||||
!isInSearchMode &&
|
||||
!deduplicateContext.isOnDeduplicatePage
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<PhotoViewer
|
||||
isOpen={open}
|
||||
items={filteredData}
|
||||
currentIndex={currentIndex}
|
||||
onClose={handleClose}
|
||||
gettingData={getSlideData}
|
||||
favItemIds={favItemIds}
|
||||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
isIncomingSharedCollection={isIncomingSharedCollection}
|
||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||
enableDownload={enableDownload}
|
||||
isSourceLoaded={isSourceLoaded}
|
||||
fileToCollectionsMap={fileToCollectionsMap}
|
||||
collectionNameMap={collectionNameMap}
|
||||
<Container>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<PhotoList
|
||||
width={width}
|
||||
height={height}
|
||||
getThumbnail={getThumbnail}
|
||||
filteredData={filteredData}
|
||||
activeCollection={activeCollection}
|
||||
showAppDownloadBanner={
|
||||
files.length < 30 &&
|
||||
!isInSearchMode &&
|
||||
!deduplicateContext.isOnDeduplicatePage
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<PhotoViewer
|
||||
isOpen={open}
|
||||
items={filteredData}
|
||||
currentIndex={currentIndex}
|
||||
onClose={handleClose}
|
||||
gettingData={getSlideData}
|
||||
favItemIds={favItemIds}
|
||||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
isIncomingSharedCollection={isIncomingSharedCollection}
|
||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||
enableDownload={enableDownload}
|
||||
isSourceLoaded={isSourceLoaded}
|
||||
fileToCollectionsMap={fileToCollectionsMap}
|
||||
collectionNameMap={collectionNameMap}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -99,7 +99,10 @@ export function FileInfo({
|
|||
|
||||
const location = useMemo(() => {
|
||||
if (file && file.metadata) {
|
||||
if (file.metadata.longitude || file.metadata.longitude === 0) {
|
||||
if (
|
||||
(file.metadata.latitude || file.metadata.latitude === 0) &&
|
||||
!(file.metadata.longitude === 0 && file.metadata.latitude === 0)
|
||||
) {
|
||||
return {
|
||||
latitude: file.metadata.latitude,
|
||||
longitude: file.metadata.longitude,
|
||||
|
@ -108,7 +111,10 @@ export function FileInfo({
|
|||
}
|
||||
if (exif) {
|
||||
const exifLocation = getEXIFLocation(exif);
|
||||
if (exifLocation.latitude || exifLocation.latitude === 0) {
|
||||
if (
|
||||
(exifLocation.latitude || exifLocation.latitude === 0) &&
|
||||
!(exifLocation.longitude === 0 && exifLocation.latitude === 0)
|
||||
) {
|
||||
return exifLocation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import ExportModal from 'components/ExportModal';
|
||||
import exportService from 'services/exportService';
|
||||
import exportService from 'services/export';
|
||||
import isElectron from 'is-electron';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
|
@ -11,21 +10,21 @@ import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte';
|
|||
import { openLink } from 'utils/common';
|
||||
import { EnteMenuItem } from 'components/Menu/EnteMenuItem';
|
||||
import { Typography } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { REDIRECTS, getRedirectURL } from 'constants/redirects';
|
||||
|
||||
export default function HelpSection() {
|
||||
const [exportModalView, setExportModalView] = useState(false);
|
||||
|
||||
const { setDialogMessage } = useContext(AppContext);
|
||||
const { openExportModal } = useContext(GalleryContext);
|
||||
|
||||
async function openRoadmapURL() {
|
||||
const roadmapRedirectURL = getRedirectURL(REDIRECTS.ROADMAP);
|
||||
openLink(roadmapRedirectURL, true);
|
||||
}
|
||||
|
||||
function openExportModal() {
|
||||
function handleExportOpen() {
|
||||
if (isElectron()) {
|
||||
setExportModalView(true);
|
||||
openExportModal();
|
||||
} else {
|
||||
setDialogMessage(getDownloadAppMessage());
|
||||
}
|
||||
|
@ -50,7 +49,7 @@ export default function HelpSection() {
|
|||
variant="secondary"
|
||||
/>
|
||||
<EnteMenuItem
|
||||
onClick={openExportModal}
|
||||
onClick={handleExportOpen}
|
||||
label={t('EXPORT')}
|
||||
endIcon={
|
||||
exportService.isExportInProgress() && (
|
||||
|
@ -59,10 +58,6 @@ export default function HelpSection() {
|
|||
}
|
||||
variant="secondary"
|
||||
/>
|
||||
<ExportModal
|
||||
show={exportModalView}
|
||||
onHide={() => setExportModalView(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ const getLocaleDisplayName = (l: Language) => {
|
|||
return '中文';
|
||||
case Language.nl:
|
||||
return 'Nederlands';
|
||||
case Language.es:
|
||||
return 'Español';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -214,8 +214,50 @@ export default function Uploader(props: Props) {
|
|||
addLogLine(`received file upload request`);
|
||||
setWebFiles(props.webFileSelectorFiles);
|
||||
} else if (props.dragAndDropFiles?.length > 0) {
|
||||
addLogLine(`received drag and drop upload request`);
|
||||
setWebFiles(props.dragAndDropFiles);
|
||||
if (isElectron()) {
|
||||
const main = async () => {
|
||||
try {
|
||||
addLogLine(`uploading dropped files from desktop app`);
|
||||
// check and parse dropped files which are zip files
|
||||
let electronFiles = [] as ElectronFile[];
|
||||
for (const file of props.dragAndDropFiles) {
|
||||
if (file.name.endsWith('.zip')) {
|
||||
const zipFiles =
|
||||
await importService.getElectronFilesFromGoogleZip(
|
||||
(file as any).path
|
||||
);
|
||||
addLogLine(
|
||||
`zip file - ${file.name} contains ${zipFiles.length} files`
|
||||
);
|
||||
electronFiles = [...electronFiles, ...zipFiles];
|
||||
} else {
|
||||
// type cast to ElectronFile as the file is dropped from desktop app
|
||||
// type file and ElectronFile should be interchangeable, but currently they have some differences.
|
||||
// Typescript is giving error
|
||||
// Conversion of type 'File' to type 'ElectronFile' may be a mistake because neither type sufficiently
|
||||
// overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
|
||||
// Type 'File' is missing the following properties from type 'ElectronFile': path, blob
|
||||
// for now patching by type casting first to unknown and then to ElectronFile
|
||||
// TODO: fix types and remove type cast
|
||||
electronFiles.push(
|
||||
file as unknown as ElectronFile
|
||||
);
|
||||
}
|
||||
}
|
||||
addLogLine(
|
||||
`uploading dropped files from desktop app - ${electronFiles.length} files found`
|
||||
);
|
||||
setElectronFiles(electronFiles);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to upload desktop dropped files');
|
||||
setWebFiles(props.dragAndDropFiles);
|
||||
}
|
||||
};
|
||||
main();
|
||||
} else {
|
||||
addLogLine(`uploading dropped files from web app`);
|
||||
setWebFiles(props.dragAndDropFiles);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
props.dragAndDropFiles,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
export const ENTE_METADATA_FOLDER = 'metadata';
|
||||
|
||||
export enum RecordType {
|
||||
SUCCESS = 'success',
|
||||
FAILED = 'failed',
|
||||
}
|
||||
export const ENTE_TRASH_FOLDER = 'Trash';
|
||||
|
||||
export enum ExportStage {
|
||||
INIT = 0,
|
||||
INPROGRESS = 1,
|
||||
|
|
|
@ -4,4 +4,5 @@ export enum Language {
|
|||
fr = 'fr',
|
||||
zh = 'zh',
|
||||
nl = 'nl',
|
||||
es = 'es',
|
||||
}
|
||||
|
|
|
@ -73,6 +73,8 @@ import {
|
|||
CLIENT_PACKAGE_NAMES,
|
||||
getAppNameAndTitle,
|
||||
} from 'constants/apps';
|
||||
import exportService from 'services/export';
|
||||
import { ExportStage } from 'constants/export';
|
||||
import { REDIRECTS } from 'constants/redirects';
|
||||
|
||||
const redirectMap = new Map([
|
||||
|
@ -118,7 +120,6 @@ type AppContextType = {
|
|||
somethingWentWrong: () => void;
|
||||
setDialogBoxAttributesV2: (attributes: DialogBoxAttributesV2) => void;
|
||||
};
|
||||
// trigger build
|
||||
|
||||
export const AppContext = createContext<AppContextType>(null);
|
||||
|
||||
|
@ -248,6 +249,42 @@ export default function App(props) {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
const initExport = async () => {
|
||||
try {
|
||||
addLogLine('init export');
|
||||
const exportSettings = exportService.getExportSettings();
|
||||
const exportRecord = await exportService.getExportRecord(
|
||||
exportSettings?.folder
|
||||
);
|
||||
await exportService.runMigration(
|
||||
exportSettings?.folder,
|
||||
exportRecord
|
||||
);
|
||||
if (exportSettings?.continuousExport) {
|
||||
exportService.enableContinuousExport();
|
||||
}
|
||||
if (exportRecord?.stage === ExportStage.INPROGRESS) {
|
||||
addLogLine('export was in progress, resuming');
|
||||
exportService.scheduleExport();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'init export failed');
|
||||
}
|
||||
};
|
||||
initExport();
|
||||
try {
|
||||
eventBus.on(Events.LOGOUT, () => {
|
||||
exportService.disableContinuousExport();
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e, 'Error while subscribing to logout event');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setUserOnline = () => setOffline(false);
|
||||
const setUserOffline = () => setOffline(true);
|
||||
const resetSharedFiles = () => setSharedFiles(null);
|
||||
|
@ -375,7 +412,7 @@ export default function App(props) {
|
|||
const somethingWentWrong = () =>
|
||||
setDialogMessage({
|
||||
title: t('ERROR'),
|
||||
close: { variant: 'error' },
|
||||
close: { variant: 'critical' },
|
||||
content: t('UNKNOWN_ERROR'),
|
||||
});
|
||||
|
||||
|
|
17
apps/photos/src/pages/_error.tsx
Normal file
17
apps/photos/src/pages/_error.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import NextErrorComponent from 'next/error';
|
||||
|
||||
const CustomErrorComponent = (props) => (
|
||||
<NextErrorComponent statusCode={props.statusCode} />
|
||||
);
|
||||
|
||||
CustomErrorComponent.getInitialProps = async (contextData) => {
|
||||
// In case this is running in a serverless function, await this in order to give Sentry
|
||||
// time to send the error before the lambda exits
|
||||
await Sentry.captureUnderscoreErrorException(contextData);
|
||||
|
||||
// This will contain the status code of the response
|
||||
return NextErrorComponent.getInitialProps(contextData);
|
||||
};
|
||||
|
||||
export default CustomErrorComponent;
|
|
@ -28,6 +28,7 @@ import { syncCollections } from 'services/collectionService';
|
|||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import VerticallyCentered from 'components/Container';
|
||||
import { Collection } from 'types/collection';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export const DeduplicateContext = createContext<DeduplicateContextType>(
|
||||
DefaultDeduplicateContext
|
||||
|
@ -179,15 +180,23 @@ export default function Deduplicate() {
|
|||
})}
|
||||
</Info>
|
||||
)}
|
||||
<PhotoFrame
|
||||
files={duplicateFiles}
|
||||
collections={collections}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
activeCollection={ALL_SECTION}
|
||||
isDeduplicating
|
||||
/>
|
||||
{duplicateFiles.length === 0 ? (
|
||||
<VerticallyCentered>
|
||||
<Typography variant="large" color="text.muted">
|
||||
{t('NO_DUPLICATES_FOUND')}
|
||||
</Typography>
|
||||
</VerticallyCentered>
|
||||
) : (
|
||||
<PhotoFrame
|
||||
files={duplicateFiles}
|
||||
collections={collections}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
activeCollection={ALL_SECTION}
|
||||
isDeduplicating
|
||||
/>
|
||||
)}
|
||||
<DeduplicateOptions
|
||||
deleteFileHelper={deleteFileHelper}
|
||||
count={selected.count}
|
||||
|
|
|
@ -109,6 +109,8 @@ import { SYNC_INTERVAL_IN_MICROSECONDS } from 'constants/gallery';
|
|||
import ElectronService from 'services/electron/common';
|
||||
import uploadManager from 'services/upload/uploadManager';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import ExportModal from 'components/ExportModal';
|
||||
import GalleryEmptyState from 'components/GalleryEmptyState';
|
||||
|
||||
export const DeadCenter = styled('div')`
|
||||
flex: 1;
|
||||
|
@ -127,6 +129,7 @@ const defaultGalleryContext: GalleryContextType = {
|
|||
syncWithRemote: () => null,
|
||||
setBlockingLoad: () => null,
|
||||
photoListHeader: null,
|
||||
openExportModal: () => null,
|
||||
};
|
||||
|
||||
export const GalleryContext = createContext<GalleryContextType>(
|
||||
|
@ -220,6 +223,8 @@ export default function Gallery() {
|
|||
const [photoListHeader, setPhotoListHeader] =
|
||||
useState<TimeStampListItem>(null);
|
||||
|
||||
const [exportModalView, setExportModalView] = useState(false);
|
||||
|
||||
const showSessionExpiredMessage = () =>
|
||||
setDialogMessage({
|
||||
title: t('SESSION_EXPIRED'),
|
||||
|
@ -601,6 +606,14 @@ export default function Gallery() {
|
|||
setCollectionSelectorView(false);
|
||||
};
|
||||
|
||||
const openExportModal = () => {
|
||||
setExportModalView(true);
|
||||
};
|
||||
|
||||
const closeExportModal = () => {
|
||||
setExportModalView(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<GalleryContext.Provider
|
||||
value={{
|
||||
|
@ -609,7 +622,8 @@ export default function Gallery() {
|
|||
setActiveCollection,
|
||||
syncWithRemote,
|
||||
setBlockingLoad,
|
||||
photoListHeader: photoListHeader,
|
||||
photoListHeader,
|
||||
openExportModal,
|
||||
}}>
|
||||
<FullScreenDropZone
|
||||
getDragAndDropRootProps={getDragAndDropRootProps}>
|
||||
|
@ -714,28 +728,32 @@ export default function Gallery() {
|
|||
sidebarView={sidebarView}
|
||||
closeSidebar={closeSidebar}
|
||||
/>
|
||||
<PhotoFrame
|
||||
files={files}
|
||||
collections={collections}
|
||||
syncWithRemote={syncWithRemote}
|
||||
favItemIds={favItemIds}
|
||||
archivedCollections={archivedCollections}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
isFirstLoad={isFirstLoad}
|
||||
hasNoPersonalFiles={hasNoPersonalFiles}
|
||||
openUploader={openUploader}
|
||||
isInSearchMode={isInSearchMode}
|
||||
search={search}
|
||||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
activeCollection={activeCollection}
|
||||
isIncomingSharedCollection={
|
||||
collectionSummaries.get(activeCollection)?.type ===
|
||||
CollectionSummaryType.incomingShare
|
||||
}
|
||||
enableDownload={true}
|
||||
/>
|
||||
{!isInSearchMode &&
|
||||
!isFirstLoad &&
|
||||
hasNoPersonalFiles &&
|
||||
activeCollection === ALL_SECTION ? (
|
||||
<GalleryEmptyState openUploader={openUploader} />
|
||||
) : (
|
||||
<PhotoFrame
|
||||
files={files}
|
||||
collections={collections}
|
||||
syncWithRemote={syncWithRemote}
|
||||
favItemIds={favItemIds}
|
||||
archivedCollections={archivedCollections}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
isInSearchMode={isInSearchMode}
|
||||
search={search}
|
||||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
activeCollection={activeCollection}
|
||||
isIncomingSharedCollection={
|
||||
collectionSummaries.get(activeCollection)?.type ===
|
||||
CollectionSummaryType.incomingShare
|
||||
}
|
||||
enableDownload={true}
|
||||
/>
|
||||
)}
|
||||
{selected.count > 0 &&
|
||||
selected.collectionID === activeCollection && (
|
||||
<SelectedFileOptions
|
||||
|
@ -796,6 +814,7 @@ export default function Gallery() {
|
|||
isInSearchMode={isInSearchMode}
|
||||
/>
|
||||
)}
|
||||
<ExportModal show={exportModalView} onHide={closeExportModal} />
|
||||
</FullScreenDropZone>
|
||||
</GalleryContext.Provider>
|
||||
);
|
||||
|
|
|
@ -400,7 +400,6 @@ export default function PublicCollectionGallery() {
|
|||
syncWithRemote={syncWithRemote}
|
||||
setSelected={() => null}
|
||||
selected={{ count: 0, collectionID: null, ownCount: 0 }}
|
||||
isFirstLoad={true}
|
||||
activeCollection={ALL_SECTION}
|
||||
isIncomingSharedCollection
|
||||
enableDownload={
|
||||
|
|
|
@ -994,7 +994,19 @@ export async function getCollectionSummaries(
|
|||
DUMMY_UNCATEGORIZED_SECTION,
|
||||
getDummyUncategorizedCollectionSummaries()
|
||||
);
|
||||
} else {
|
||||
collectionSummaries.get(uncategorizedCollection.id).name =
|
||||
t('UNCATEGORIZED');
|
||||
}
|
||||
|
||||
const favCollection = await getFavCollection();
|
||||
if (favCollection) {
|
||||
const favoriteEntry = collectionSummaries.get(favCollection.id);
|
||||
if (favoriteEntry) {
|
||||
collectionSummaries.get(favCollection.id).name = t('FAVORITES');
|
||||
}
|
||||
}
|
||||
|
||||
collectionSummaries.set(
|
||||
ALL_SECTION,
|
||||
getAllCollectionSummaries(collectionFilesCount, collectionLatestFiles)
|
||||
|
|
1060
apps/photos/src/services/export/index.ts
Normal file
1060
apps/photos/src/services/export/index.ts
Normal file
File diff suppressed because it is too large
Load diff
376
apps/photos/src/services/export/migration.ts
Normal file
376
apps/photos/src/services/export/migration.ts
Normal file
|
@ -0,0 +1,376 @@
|
|||
import { getLocalCollections } from 'services/collectionService';
|
||||
import { getLocalFiles } from 'services/fileService';
|
||||
import {
|
||||
ExportRecordV1,
|
||||
ExportRecordV2,
|
||||
ExportRecord,
|
||||
FileExportNames,
|
||||
ExportRecordV0,
|
||||
CollectionExportNames,
|
||||
} from 'types/export';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { User } from 'types/user';
|
||||
import { getNonEmptyPersonalCollections } from 'utils/collection';
|
||||
import { getExportRecordFileUID, getLivePhotoExportName } from 'utils/export';
|
||||
import {
|
||||
getIDBasedSortedFiles,
|
||||
getPersonalFiles,
|
||||
mergeMetadata,
|
||||
} from 'utils/file';
|
||||
import { addLocalLog, addLogLine } from 'utils/logging';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import exportService from './index';
|
||||
import { Collection } from 'types/collection';
|
||||
import {
|
||||
getExportedFiles,
|
||||
convertCollectionIDFolderPathObjectToMap,
|
||||
getUniqueFileExportNameForMigration,
|
||||
getOldCollectionFolderPath,
|
||||
getUniqueCollectionFolderPath,
|
||||
getUniqueFileSaveName,
|
||||
getOldFileSavePath,
|
||||
getOldFileMetadataSavePath,
|
||||
getFileMetadataSavePath,
|
||||
getFileSavePath,
|
||||
} from 'utils/export/migration';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { decodeLivePhoto } from 'services/livePhotoService';
|
||||
import downloadManager from 'services/downloadManager';
|
||||
import { retryAsyncFunction } from 'utils/network';
|
||||
import { CustomError } from 'utils/error';
|
||||
|
||||
export async function migrateExportJSON(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecord
|
||||
) {
|
||||
try {
|
||||
if (!exportDir) {
|
||||
return;
|
||||
}
|
||||
await migrateExport(exportDir, exportRecord);
|
||||
} catch (e) {
|
||||
logError(e, 'migrateExportJSON failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
this function migrates the exportRecord file to apply any schema changes.
|
||||
currently we apply only a single migration to update file and collection name to newer format
|
||||
so there is just a if condition check,
|
||||
later this will be converted to a loop which applies the migration one by one
|
||||
till the files reaches the latest version
|
||||
*/
|
||||
async function migrateExport(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecordV1 | ExportRecordV2 | ExportRecord
|
||||
) {
|
||||
try {
|
||||
if (!exportRecord?.version) {
|
||||
exportRecord = {
|
||||
...exportRecord,
|
||||
version: 0,
|
||||
};
|
||||
}
|
||||
addLogLine(`current export version: ${exportRecord.version}`);
|
||||
if (exportRecord.version === 0) {
|
||||
addLogLine('migrating export to version 1');
|
||||
await migrationV0ToV1(exportDir, exportRecord as ExportRecordV0);
|
||||
exportRecord = await exportService.updateExportRecord({
|
||||
version: 1,
|
||||
});
|
||||
addLogLine('migration to version 1 complete');
|
||||
}
|
||||
if (exportRecord.version === 1) {
|
||||
addLogLine('migrating export to version 2');
|
||||
await migrationV1ToV2(exportRecord as ExportRecordV1);
|
||||
exportRecord = await exportService.updateExportRecord({
|
||||
version: 2,
|
||||
});
|
||||
addLogLine('migration to version 2 complete');
|
||||
}
|
||||
if (exportRecord.version === 2) {
|
||||
addLogLine('migrating export to version 3');
|
||||
await migrationV2ToV3(exportDir, exportRecord as ExportRecordV2);
|
||||
exportRecord = await exportService.updateExportRecord({
|
||||
version: 3,
|
||||
});
|
||||
addLogLine('migration to version 3 complete');
|
||||
}
|
||||
addLogLine(`Record at latest version`);
|
||||
} catch (e) {
|
||||
logError(e, 'export record migration failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function migrationV0ToV1(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecordV0
|
||||
) {
|
||||
if (!exportRecord?.exportedFiles) {
|
||||
return;
|
||||
}
|
||||
const collectionIDPathMap = new Map<number, string>();
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
const localFiles = mergeMetadata(await getLocalFiles());
|
||||
const localCollections = await getLocalCollections();
|
||||
const personalFiles = getIDBasedSortedFiles(
|
||||
getPersonalFiles(localFiles, user)
|
||||
);
|
||||
const nonEmptyPersonalCollections = getNonEmptyPersonalCollections(
|
||||
localCollections,
|
||||
personalFiles,
|
||||
user
|
||||
);
|
||||
await migrateCollectionFolders(
|
||||
nonEmptyPersonalCollections,
|
||||
exportDir,
|
||||
collectionIDPathMap
|
||||
);
|
||||
await migrateFiles(
|
||||
getExportedFiles(personalFiles, exportRecord),
|
||||
collectionIDPathMap
|
||||
);
|
||||
}
|
||||
|
||||
async function migrationV1ToV2(exportRecord: ExportRecordV1) {
|
||||
await removeDeprecatedExportRecordProperties(exportRecord);
|
||||
}
|
||||
|
||||
async function migrationV2ToV3(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecordV2
|
||||
) {
|
||||
if (!exportRecord?.exportedFiles) {
|
||||
return;
|
||||
}
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
const localFiles = await getLocalFiles();
|
||||
const personalFiles = getIDBasedSortedFiles(
|
||||
getPersonalFiles(localFiles, user)
|
||||
);
|
||||
|
||||
const collectionExportNames =
|
||||
await getCollectionExportNamesFromExportedCollectionPaths(
|
||||
exportDir,
|
||||
exportRecord
|
||||
);
|
||||
|
||||
const fileExportNames = await getFileExportNamesFromExportedFiles(
|
||||
exportRecord,
|
||||
getExportedFiles(personalFiles, exportRecord)
|
||||
);
|
||||
|
||||
exportRecord.exportedCollectionPaths = undefined;
|
||||
exportRecord.exportedFiles = undefined;
|
||||
const updatedExportRecord: ExportRecord = {
|
||||
...exportRecord,
|
||||
fileExportNames,
|
||||
collectionExportNames,
|
||||
};
|
||||
await exportService.updateExportRecord(updatedExportRecord);
|
||||
}
|
||||
|
||||
/*
|
||||
This updates the folder name of already exported folders from the earlier format of
|
||||
`collectionID_collectionName` to newer `collectionName(numbered)` format
|
||||
*/
|
||||
async function migrateCollectionFolders(
|
||||
collections: Collection[],
|
||||
exportDir: string,
|
||||
collectionIDPathMap: Map<number, string>
|
||||
) {
|
||||
for (const collection of collections) {
|
||||
const oldCollectionExportPath = getOldCollectionFolderPath(
|
||||
exportDir,
|
||||
collection.id,
|
||||
collection.name
|
||||
);
|
||||
const newCollectionExportPath = getUniqueCollectionFolderPath(
|
||||
exportDir,
|
||||
collection.name
|
||||
);
|
||||
collectionIDPathMap.set(collection.id, newCollectionExportPath);
|
||||
if (!exportService.exists(oldCollectionExportPath)) {
|
||||
continue;
|
||||
}
|
||||
await exportService.rename(
|
||||
oldCollectionExportPath,
|
||||
newCollectionExportPath
|
||||
);
|
||||
await addCollectionExportedRecordV1(
|
||||
exportDir,
|
||||
collection.id,
|
||||
newCollectionExportPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This updates the file name of already exported files from the earlier format of
|
||||
`fileID_fileName` to newer `fileName(numbered)` format
|
||||
*/
|
||||
async function migrateFiles(
|
||||
files: EnteFile[],
|
||||
collectionIDPathMap: Map<number, string>
|
||||
) {
|
||||
for (const file of files) {
|
||||
const oldFileSavePath = getOldFileSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
file
|
||||
);
|
||||
const oldFileMetadataSavePath = getOldFileMetadataSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
file
|
||||
);
|
||||
const newFileSaveName = getUniqueFileSaveName(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
file.metadata.title
|
||||
);
|
||||
|
||||
const newFileSavePath = getFileSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
newFileSaveName
|
||||
);
|
||||
|
||||
const newFileMetadataSavePath = getFileMetadataSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
newFileSaveName
|
||||
);
|
||||
if (!exportService.exists(oldFileSavePath)) {
|
||||
continue;
|
||||
}
|
||||
await exportService.rename(oldFileSavePath, newFileSavePath);
|
||||
await exportService.rename(
|
||||
oldFileMetadataSavePath,
|
||||
newFileMetadataSavePath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDeprecatedExportRecordProperties(
|
||||
exportRecord: ExportRecordV1
|
||||
) {
|
||||
if (exportRecord?.queuedFiles) {
|
||||
exportRecord.queuedFiles = undefined;
|
||||
}
|
||||
if (exportRecord?.progress) {
|
||||
exportRecord.progress = undefined;
|
||||
}
|
||||
if (exportRecord?.failedFiles) {
|
||||
exportRecord.failedFiles = undefined;
|
||||
}
|
||||
await exportService.updateExportRecord(exportRecord);
|
||||
}
|
||||
|
||||
async function getCollectionExportNamesFromExportedCollectionPaths(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecordV2
|
||||
): Promise<CollectionExportNames> {
|
||||
if (!exportRecord.exportedCollectionPaths) {
|
||||
return;
|
||||
}
|
||||
const exportedCollectionNames = Object.fromEntries(
|
||||
Object.entries(exportRecord.exportedCollectionPaths).map(
|
||||
([key, value]) => [key, value.replace(exportDir, '').slice(1)]
|
||||
)
|
||||
);
|
||||
return exportedCollectionNames;
|
||||
}
|
||||
|
||||
/*
|
||||
Earlier the file were sorted by id,
|
||||
which we can use to determine which file got which number suffix
|
||||
this can be used to determine the filepaths of the those already exported files
|
||||
and update the exportedFilePaths property of the exportRecord
|
||||
This is based on the assumption new files have higher ids than the older ones
|
||||
*/
|
||||
async function getFileExportNamesFromExportedFiles(
|
||||
exportRecord: ExportRecordV2,
|
||||
exportedFiles: EnteFile[]
|
||||
): Promise<FileExportNames> {
|
||||
if (!exportedFiles.length) {
|
||||
return;
|
||||
}
|
||||
addLogLine(
|
||||
'updating exported files to exported file paths property',
|
||||
`got ${exportedFiles.length} files`
|
||||
);
|
||||
let exportedFileNames: FileExportNames;
|
||||
const usedFilePaths = new Map<string, Set<string>>();
|
||||
const exportedCollectionPaths = convertCollectionIDFolderPathObjectToMap(
|
||||
exportRecord.exportedCollectionPaths
|
||||
);
|
||||
for (const file of exportedFiles) {
|
||||
const collectionPath = exportedCollectionPaths.get(file.collectionID);
|
||||
addLocalLog(
|
||||
() =>
|
||||
`collection path for ${file.collectionID} is ${collectionPath}`
|
||||
);
|
||||
let fileExportName: string;
|
||||
/*
|
||||
For Live Photos we need to download the file to get the image and video name
|
||||
*/
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const fileStream = await retryAsyncFunction(() =>
|
||||
downloadManager.downloadFile(file)
|
||||
);
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const imageExportName = getUniqueFileExportNameForMigration(
|
||||
collectionPath,
|
||||
livePhoto.imageNameTitle,
|
||||
usedFilePaths
|
||||
);
|
||||
const videoExportName = getUniqueFileExportNameForMigration(
|
||||
collectionPath,
|
||||
livePhoto.videoNameTitle,
|
||||
usedFilePaths
|
||||
);
|
||||
fileExportName = getLivePhotoExportName(
|
||||
imageExportName,
|
||||
videoExportName
|
||||
);
|
||||
} else {
|
||||
fileExportName = getUniqueFileExportNameForMigration(
|
||||
collectionPath,
|
||||
file.metadata.title,
|
||||
usedFilePaths
|
||||
);
|
||||
}
|
||||
addLocalLog(
|
||||
() =>
|
||||
`file export name for ${file.metadata.title} is ${fileExportName}`
|
||||
);
|
||||
exportedFileNames = {
|
||||
...exportedFileNames,
|
||||
[getExportRecordFileUID(file)]: fileExportName,
|
||||
};
|
||||
}
|
||||
return exportedFileNames;
|
||||
}
|
||||
|
||||
async function addCollectionExportedRecordV1(
|
||||
folder: string,
|
||||
collectionID: number,
|
||||
collectionExportPath: string
|
||||
) {
|
||||
try {
|
||||
const exportRecord = (await exportService.getExportRecord(
|
||||
folder
|
||||
)) as unknown as ExportRecordV1;
|
||||
if (!exportRecord?.exportedCollectionPaths) {
|
||||
exportRecord.exportedCollectionPaths = {};
|
||||
}
|
||||
exportRecord.exportedCollectionPaths = {
|
||||
...exportRecord.exportedCollectionPaths,
|
||||
[collectionID]: collectionExportPath,
|
||||
};
|
||||
|
||||
await exportService.updateExportRecord(exportRecord, folder);
|
||||
} catch (e) {
|
||||
logError(e, 'addCollectionExportedRecord failed');
|
||||
throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED);
|
||||
}
|
||||
}
|
|
@ -1,747 +0,0 @@
|
|||
import { runningInBrowser } from 'utils/common';
|
||||
import {
|
||||
getUnExportedFiles,
|
||||
dedupe,
|
||||
getGoogleLikeMetadataFile,
|
||||
getExportRecordFileUID,
|
||||
getUniqueCollectionFolderPath,
|
||||
getUniqueFileSaveName,
|
||||
getOldFileSavePath,
|
||||
getOldCollectionFolderPath,
|
||||
getFileMetadataSavePath,
|
||||
getFileSavePath,
|
||||
getOldFileMetadataSavePath,
|
||||
getExportedFiles,
|
||||
getMetadataFolderPath,
|
||||
getCollectionsRenamedAfterLastExport,
|
||||
getCollectionIDPathMapFromExportRecord,
|
||||
} from 'utils/export';
|
||||
import { retryAsyncFunction } from 'utils/network';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import {
|
||||
getLocalCollections,
|
||||
getNonEmptyCollections,
|
||||
} from './collectionService';
|
||||
import downloadManager from './downloadManager';
|
||||
import { getLocalFiles } from './fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
import { decodeLivePhoto } from './livePhotoService';
|
||||
import {
|
||||
generateStreamFromArrayBuffer,
|
||||
getFileExtension,
|
||||
getPersonalFiles,
|
||||
mergeMetadata,
|
||||
} from 'utils/file';
|
||||
|
||||
import { updateFileCreationDateInEXIF } from './upload/exifService';
|
||||
import QueueProcessor from './queueProcessor';
|
||||
import { Collection } from 'types/collection';
|
||||
import {
|
||||
CollectionIDNameMap,
|
||||
CollectionIDPathMap,
|
||||
ExportProgress,
|
||||
ExportRecord,
|
||||
ExportRecordV1,
|
||||
ExportUIUpdaters,
|
||||
FileExportStats,
|
||||
} from 'types/export';
|
||||
import { User } from 'types/user';
|
||||
import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file';
|
||||
import { ExportStage, RecordType } from 'constants/export';
|
||||
import { ElectronAPIs } from 'types/electron';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { t } from 'i18next';
|
||||
import { eventBus, Events } from './events';
|
||||
import { getCollectionNameMap } from 'utils/collection';
|
||||
|
||||
const EXPORT_RECORD_FILE_NAME = 'export_status.json';
|
||||
|
||||
export const ENTE_EXPORT_DIRECTORY = 'ente Photos';
|
||||
|
||||
class ExportService {
|
||||
private electronAPIs: ElectronAPIs;
|
||||
private exportInProgress: boolean = false;
|
||||
private reRunNeeded = false;
|
||||
private exportRecordUpdater = new QueueProcessor<void>(1);
|
||||
private stopExport: boolean = false;
|
||||
private allElectronAPIsExist: boolean = false;
|
||||
private fileReader: FileReader = null;
|
||||
private continuousExportEventHandler: () => void;
|
||||
private uiUpdater: ExportUIUpdaters;
|
||||
private currentExportProgress: ExportProgress = {
|
||||
total: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.electronAPIs = runningInBrowser() && window['ElectronAPIs'];
|
||||
this.allElectronAPIsExist = !!this.electronAPIs?.exists;
|
||||
}
|
||||
|
||||
async setUIUpdaters(uiUpdater: ExportUIUpdaters) {
|
||||
this.uiUpdater = uiUpdater;
|
||||
this.uiUpdater.updateExportProgress(this.currentExportProgress);
|
||||
}
|
||||
|
||||
private updateExportProgress(exportProgress: ExportProgress) {
|
||||
this.currentExportProgress = exportProgress;
|
||||
this.uiUpdater.updateExportProgress(exportProgress);
|
||||
}
|
||||
|
||||
async changeExportDirectory(callback: (newExportDir: string) => void) {
|
||||
try {
|
||||
const newRootDir = await this.electronAPIs.selectRootDirectory();
|
||||
if (!newRootDir) {
|
||||
return;
|
||||
}
|
||||
const newExportDir = `${newRootDir}/${ENTE_EXPORT_DIRECTORY}`;
|
||||
await this.electronAPIs.checkExistsAndCreateDir(newExportDir);
|
||||
callback(newExportDir);
|
||||
} catch (e) {
|
||||
logError(e, 'changeExportDirectory failed');
|
||||
}
|
||||
}
|
||||
|
||||
async openExportDirectory(exportFolder: string) {
|
||||
try {
|
||||
await this.electronAPIs.openDirectory(exportFolder);
|
||||
} catch (e) {
|
||||
logError(e, 'openExportDirectory failed');
|
||||
}
|
||||
}
|
||||
|
||||
enableContinuousExport() {
|
||||
try {
|
||||
if (this.continuousExportEventHandler) {
|
||||
addLogLine('continuous export already enabled');
|
||||
return;
|
||||
}
|
||||
this.continuousExportEventHandler = this.runExport;
|
||||
this.continuousExportEventHandler();
|
||||
eventBus.addListener(
|
||||
Events.LOCAL_FILES_UPDATED,
|
||||
this.continuousExportEventHandler
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'failed to enableContinuousExport ');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
disableContinuousExport() {
|
||||
try {
|
||||
if (!this.continuousExportEventHandler) {
|
||||
addLogLine('continuous export already disabled');
|
||||
return;
|
||||
}
|
||||
eventBus.removeListener(
|
||||
Events.LOCAL_FILES_UPDATED,
|
||||
this.continuousExportEventHandler
|
||||
);
|
||||
this.continuousExportEventHandler = null;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to disableContinuousExport');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
getFileExportStats = async (): Promise<FileExportStats> => {
|
||||
try {
|
||||
const exportRecord = await this.getExportRecord();
|
||||
const userPersonalFiles = await getPersonalFiles();
|
||||
const unExportedFiles = getUnExportedFiles(
|
||||
userPersonalFiles,
|
||||
exportRecord
|
||||
);
|
||||
return {
|
||||
totalCount: userPersonalFiles.length,
|
||||
pendingCount: unExportedFiles.length,
|
||||
};
|
||||
} catch (e) {
|
||||
logError(e, 'getUpdateFileLists failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
async stopRunningExport() {
|
||||
try {
|
||||
this.stopExport = true;
|
||||
this.reRunNeeded = false;
|
||||
await this.postExport();
|
||||
} catch (e) {
|
||||
logError(e, 'stopRunningExport failed');
|
||||
}
|
||||
}
|
||||
|
||||
runExport = async () => {
|
||||
try {
|
||||
if (this.exportInProgress) {
|
||||
addLogLine('export in progress, scheduling re-run');
|
||||
this.electronAPIs.sendNotification(
|
||||
t('EXPORT_NOTIFICATION.IN_PROGRESS')
|
||||
);
|
||||
this.reRunNeeded = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
addLogLine('starting export');
|
||||
this.exportInProgress = true;
|
||||
await this.uiUpdater.updateExportStage(ExportStage.INPROGRESS);
|
||||
this.updateExportProgress({
|
||||
success: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
await this.exportFiles();
|
||||
addLogLine('export completed');
|
||||
} finally {
|
||||
this.exportInProgress = false;
|
||||
if (this.reRunNeeded) {
|
||||
this.reRunNeeded = false;
|
||||
addLogLine('re-running export');
|
||||
setTimeout(this.runExport, 0);
|
||||
}
|
||||
await this.postExport();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
|
||||
logError(e, 'runExport failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async exportFiles() {
|
||||
try {
|
||||
const exportDir = getData(LS_KEYS.EXPORT)?.folder;
|
||||
if (!exportDir) {
|
||||
// no-export folder set
|
||||
return;
|
||||
}
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
const localFiles = await getLocalFiles();
|
||||
const userPersonalFiles = localFiles
|
||||
.filter((file) => file.ownerID === user?.id)
|
||||
.sort((fileA, fileB) => fileA.id - fileB.id);
|
||||
|
||||
const collections = await getLocalCollections();
|
||||
const nonEmptyCollections = getNonEmptyCollections(
|
||||
collections,
|
||||
userPersonalFiles
|
||||
);
|
||||
const userCollections = nonEmptyCollections
|
||||
.filter((collection) => collection.owner.id === user?.id)
|
||||
.sort(
|
||||
(collectionA, collectionB) =>
|
||||
collectionA.id - collectionB.id
|
||||
);
|
||||
if (this.checkAllElectronAPIsExists()) {
|
||||
await this.migrateExport(
|
||||
exportDir,
|
||||
collections,
|
||||
userPersonalFiles
|
||||
);
|
||||
}
|
||||
const exportRecord = await this.getExportRecord(exportDir);
|
||||
|
||||
const filesToExport = getUnExportedFiles(
|
||||
userPersonalFiles,
|
||||
exportRecord
|
||||
);
|
||||
|
||||
addLogLine(
|
||||
`exportFiles: filesToExportCount: ${filesToExport?.length}, userPersonalFileCount: ${userPersonalFiles?.length}`
|
||||
);
|
||||
|
||||
const collectionIDPathMap: CollectionIDPathMap =
|
||||
getCollectionIDPathMapFromExportRecord(exportRecord);
|
||||
const collectionIDNameMap = getCollectionNameMap(collections);
|
||||
const renamedCollections = getCollectionsRenamedAfterLastExport(
|
||||
userCollections,
|
||||
exportRecord
|
||||
);
|
||||
await this.fileExporter(
|
||||
filesToExport,
|
||||
collectionIDNameMap,
|
||||
renamedCollections,
|
||||
collectionIDPathMap,
|
||||
exportDir
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'exportFiles failed');
|
||||
}
|
||||
}
|
||||
|
||||
async fileExporter(
|
||||
files: EnteFile[],
|
||||
collectionIDNameMap: CollectionIDNameMap,
|
||||
renamedCollections: Collection[],
|
||||
collectionIDPathMap: CollectionIDPathMap,
|
||||
exportDir: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (
|
||||
renamedCollections?.length &&
|
||||
this.checkAllElectronAPIsExists()
|
||||
) {
|
||||
await this.renameCollectionFolders(
|
||||
renamedCollections,
|
||||
exportDir,
|
||||
collectionIDPathMap
|
||||
);
|
||||
}
|
||||
if (!files?.length) {
|
||||
this.electronAPIs.sendNotification(
|
||||
t('EXPORT_NOTIFICATION.UP_TO_DATE')
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.stopExport = false;
|
||||
this.electronAPIs.sendNotification(t('EXPORT_NOTIFICATION.START'));
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
this.updateExportProgress({
|
||||
success,
|
||||
failed,
|
||||
total: files.length,
|
||||
});
|
||||
for (const file of files) {
|
||||
if (this.stopExport) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
let collectionPath = collectionIDPathMap.get(
|
||||
file.collectionID
|
||||
);
|
||||
if (!collectionPath || !this.exists(collectionPath)) {
|
||||
collectionPath = await this.createNewCollectionFolder(
|
||||
exportDir,
|
||||
file.collectionID,
|
||||
collectionIDNameMap,
|
||||
collectionIDPathMap
|
||||
);
|
||||
}
|
||||
await this.downloadAndSave(file, collectionPath);
|
||||
await this.addFileExportedRecord(
|
||||
exportDir,
|
||||
file,
|
||||
RecordType.SUCCESS
|
||||
);
|
||||
success++;
|
||||
} catch (e) {
|
||||
failed++;
|
||||
logError(e, 'export failed for a file');
|
||||
if (
|
||||
e.message ===
|
||||
CustomError.ADD_FILE_EXPORTED_RECORD_FAILED
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
await this.addFileExportedRecord(
|
||||
exportDir,
|
||||
file,
|
||||
RecordType.FAILED
|
||||
);
|
||||
}
|
||||
this.updateExportProgress({
|
||||
success,
|
||||
failed,
|
||||
total: files.length,
|
||||
});
|
||||
}
|
||||
if (!this.stopExport) {
|
||||
this.electronAPIs.sendNotification(
|
||||
t('EXPORT_NOTIFICATION.FINISH')
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'fileExporter failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async postExport() {
|
||||
await this.uiUpdater.updateExportStage(ExportStage.FINISHED);
|
||||
await this.uiUpdater.updateLastExportTime(Date.now());
|
||||
this.uiUpdater.updateFileExportStats(await this.getFileExportStats());
|
||||
}
|
||||
|
||||
async addFileExportedRecord(
|
||||
folder: string,
|
||||
file: EnteFile,
|
||||
type: RecordType
|
||||
) {
|
||||
try {
|
||||
const fileUID = getExportRecordFileUID(file);
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
if (type === RecordType.SUCCESS) {
|
||||
if (!exportRecord.exportedFiles) {
|
||||
exportRecord.exportedFiles = [];
|
||||
}
|
||||
exportRecord.exportedFiles.push(fileUID);
|
||||
}
|
||||
exportRecord.exportedFiles = dedupe(exportRecord.exportedFiles);
|
||||
await this.updateExportRecord(exportRecord, folder);
|
||||
} catch (e) {
|
||||
logError(e, 'addFileExportedRecord failed');
|
||||
throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
async addCollectionExportedRecord(
|
||||
folder: string,
|
||||
collectionID: number,
|
||||
collectionFolderPath: string
|
||||
) {
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
if (!exportRecord?.exportedCollectionPaths) {
|
||||
exportRecord.exportedCollectionPaths = {};
|
||||
}
|
||||
exportRecord.exportedCollectionPaths = {
|
||||
...exportRecord.exportedCollectionPaths,
|
||||
[collectionID]: collectionFolderPath,
|
||||
};
|
||||
|
||||
await this.updateExportRecord(exportRecord, folder);
|
||||
}
|
||||
|
||||
async updateExportRecord(newData: Partial<ExportRecord>, folder?: string) {
|
||||
const response = this.exportRecordUpdater.queueUpRequest(() =>
|
||||
this.updateExportRecordHelper(folder, newData)
|
||||
);
|
||||
await response.promise;
|
||||
}
|
||||
|
||||
async updateExportRecordHelper(
|
||||
folder: string,
|
||||
newData: Partial<ExportRecord>
|
||||
) {
|
||||
try {
|
||||
if (!folder) {
|
||||
folder = getData(LS_KEYS.EXPORT)?.folder;
|
||||
}
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
const newRecord = { ...exportRecord, ...newData };
|
||||
await this.electronAPIs.setExportRecord(
|
||||
`${folder}/${EXPORT_RECORD_FILE_NAME}`,
|
||||
JSON.stringify(newRecord, null, 2)
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'error updating Export Record');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async getExportRecord(folder?: string): Promise<ExportRecord> {
|
||||
try {
|
||||
if (!folder) {
|
||||
folder = getData(LS_KEYS.EXPORT)?.folder;
|
||||
}
|
||||
if (!folder) {
|
||||
return null;
|
||||
}
|
||||
const exportFolderExists = this.exists(folder);
|
||||
if (!exportFolderExists) {
|
||||
return null;
|
||||
}
|
||||
const recordFile = await this.electronAPIs.getExportRecord(
|
||||
`${folder}/${EXPORT_RECORD_FILE_NAME}`
|
||||
);
|
||||
return JSON.parse(recordFile);
|
||||
} catch (e) {
|
||||
logError(e, 'export Record JSON parsing failed ');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async createNewCollectionFolder(
|
||||
exportFolder: string,
|
||||
collectionID: number,
|
||||
collectionIDNameMap: CollectionIDNameMap,
|
||||
collectionIDPathMap: CollectionIDPathMap
|
||||
) {
|
||||
const collectionName = collectionIDNameMap.get(collectionID);
|
||||
const collectionFolderPath = getUniqueCollectionFolderPath(
|
||||
exportFolder,
|
||||
collectionID,
|
||||
collectionName
|
||||
);
|
||||
await this.electronAPIs.checkExistsAndCreateDir(collectionFolderPath);
|
||||
await this.electronAPIs.checkExistsAndCreateDir(
|
||||
getMetadataFolderPath(collectionFolderPath)
|
||||
);
|
||||
await this.addCollectionExportedRecord(
|
||||
exportFolder,
|
||||
collectionID,
|
||||
collectionFolderPath
|
||||
);
|
||||
collectionIDPathMap.set(collectionID, collectionFolderPath);
|
||||
return collectionFolderPath;
|
||||
}
|
||||
async renameCollectionFolders(
|
||||
renamedCollections: Collection[],
|
||||
exportFolder: string,
|
||||
collectionIDPathMap: CollectionIDPathMap
|
||||
) {
|
||||
for (const collection of renamedCollections) {
|
||||
const oldCollectionFolderPath = collectionIDPathMap.get(
|
||||
collection.id
|
||||
);
|
||||
|
||||
const newCollectionFolderPath = getUniqueCollectionFolderPath(
|
||||
exportFolder,
|
||||
collection.id,
|
||||
collection.name
|
||||
);
|
||||
await this.electronAPIs.checkExistsAndRename(
|
||||
oldCollectionFolderPath,
|
||||
newCollectionFolderPath
|
||||
);
|
||||
|
||||
await this.addCollectionExportedRecord(
|
||||
exportFolder,
|
||||
collection.id,
|
||||
newCollectionFolderPath
|
||||
);
|
||||
collectionIDPathMap.set(collection.id, newCollectionFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadAndSave(file: EnteFile, collectionPath: string) {
|
||||
try {
|
||||
file.metadata = mergeMetadata([file])[0].metadata;
|
||||
const fileSaveName = getUniqueFileSaveName(
|
||||
collectionPath,
|
||||
file.metadata.title,
|
||||
file.id
|
||||
);
|
||||
let fileStream = await retryAsyncFunction(() =>
|
||||
downloadManager.downloadFile(file)
|
||||
);
|
||||
const fileType = getFileExtension(file.metadata.title);
|
||||
if (
|
||||
file.pubMagicMetadata?.data.editedTime &&
|
||||
(fileType === TYPE_JPEG || fileType === TYPE_JPG)
|
||||
) {
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
if (!this.fileReader) {
|
||||
this.fileReader = new FileReader();
|
||||
}
|
||||
const updatedFileBlob = await updateFileCreationDateInEXIF(
|
||||
this.fileReader,
|
||||
fileBlob,
|
||||
new Date(file.pubMagicMetadata.data.editedTime / 1000)
|
||||
);
|
||||
fileStream = updatedFileBlob.stream();
|
||||
}
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
await this.exportLivePhoto(fileStream, file, collectionPath);
|
||||
} else {
|
||||
await this.saveMediaFile(
|
||||
collectionPath,
|
||||
fileSaveName,
|
||||
fileStream
|
||||
);
|
||||
await this.saveMetadataFile(collectionPath, fileSaveName, file);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'download and save failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async exportLivePhoto(
|
||||
fileStream: ReadableStream<any>,
|
||||
file: EnteFile,
|
||||
collectionPath: string
|
||||
) {
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
|
||||
const imageSaveName = getUniqueFileSaveName(
|
||||
collectionPath,
|
||||
livePhoto.imageNameTitle,
|
||||
file.id
|
||||
);
|
||||
await this.saveMediaFile(collectionPath, imageSaveName, imageStream);
|
||||
await this.saveMetadataFile(collectionPath, imageSaveName, file);
|
||||
|
||||
const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
|
||||
const videoSaveName = getUniqueFileSaveName(
|
||||
collectionPath,
|
||||
livePhoto.videoNameTitle,
|
||||
file.id
|
||||
);
|
||||
await this.saveMediaFile(collectionPath, videoSaveName, videoStream);
|
||||
await this.saveMetadataFile(collectionPath, videoSaveName, file);
|
||||
}
|
||||
|
||||
private async saveMediaFile(
|
||||
collectionFolderPath: string,
|
||||
fileSaveName: string,
|
||||
fileStream: ReadableStream<any>
|
||||
) {
|
||||
await this.electronAPIs.saveStreamToDisk(
|
||||
getFileSavePath(collectionFolderPath, fileSaveName),
|
||||
fileStream
|
||||
);
|
||||
}
|
||||
private async saveMetadataFile(
|
||||
collectionFolderPath: string,
|
||||
fileSaveName: string,
|
||||
file: EnteFile
|
||||
) {
|
||||
await this.electronAPIs.saveFileToDisk(
|
||||
getFileMetadataSavePath(collectionFolderPath, fileSaveName),
|
||||
getGoogleLikeMetadataFile(fileSaveName, file)
|
||||
);
|
||||
}
|
||||
|
||||
isExportInProgress = () => {
|
||||
return this.exportInProgress;
|
||||
};
|
||||
|
||||
exists = (path: string) => {
|
||||
return this.electronAPIs.exists(path);
|
||||
};
|
||||
|
||||
checkAllElectronAPIsExists = () => this.allElectronAPIsExist;
|
||||
|
||||
/*
|
||||
this function migrates the exportRecord file to apply any schema changes.
|
||||
currently we apply only a single migration to update file and collection name to newer format
|
||||
so there is just a if condition check,
|
||||
later this will be converted to a loop which applies the migration one by one
|
||||
till the files reaches the latest version
|
||||
*/
|
||||
private async migrateExport(
|
||||
exportDir: string,
|
||||
collections: Collection[],
|
||||
files: EnteFile[]
|
||||
) {
|
||||
const exportRecord = await this.getExportRecord(exportDir);
|
||||
let currentVersion = exportRecord?.version ?? 0;
|
||||
if (currentVersion === 0) {
|
||||
const collectionIDPathMap = new Map<number, string>();
|
||||
|
||||
await this.migrateCollectionFolders(
|
||||
collections,
|
||||
exportDir,
|
||||
collectionIDPathMap
|
||||
);
|
||||
await this.migrateFiles(
|
||||
getExportedFiles(files, exportRecord),
|
||||
collectionIDPathMap
|
||||
);
|
||||
currentVersion++;
|
||||
await this.updateExportRecord({
|
||||
version: currentVersion,
|
||||
});
|
||||
}
|
||||
if (currentVersion === 1) {
|
||||
await this.removeDeprecatedExportRecordProperties();
|
||||
currentVersion++;
|
||||
await this.updateExportRecord({
|
||||
version: currentVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This updates the folder name of already exported folders from the earlier format of
|
||||
`collectionID_collectionName` to newer `collectionName(numbered)` format
|
||||
*/
|
||||
private async migrateCollectionFolders(
|
||||
collections: Collection[],
|
||||
exportDir: string,
|
||||
collectionIDPathMap: CollectionIDPathMap
|
||||
) {
|
||||
for (const collection of collections) {
|
||||
const oldCollectionFolderPath = getOldCollectionFolderPath(
|
||||
exportDir,
|
||||
collection.id,
|
||||
collection.name
|
||||
);
|
||||
const newCollectionFolderPath = getUniqueCollectionFolderPath(
|
||||
exportDir,
|
||||
collection.id,
|
||||
collection.name
|
||||
);
|
||||
collectionIDPathMap.set(collection.id, newCollectionFolderPath);
|
||||
if (this.electronAPIs.exists(oldCollectionFolderPath)) {
|
||||
await this.electronAPIs.checkExistsAndRename(
|
||||
oldCollectionFolderPath,
|
||||
newCollectionFolderPath
|
||||
);
|
||||
await this.addCollectionExportedRecord(
|
||||
exportDir,
|
||||
collection.id,
|
||||
newCollectionFolderPath
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This updates the file name of already exported files from the earlier format of
|
||||
`fileID_fileName` to newer `fileName(numbered)` format
|
||||
*/
|
||||
private async migrateFiles(
|
||||
files: EnteFile[],
|
||||
collectionIDPathMap: Map<number, string>
|
||||
) {
|
||||
for (let file of files) {
|
||||
const oldFileSavePath = getOldFileSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
file
|
||||
);
|
||||
const oldFileMetadataSavePath = getOldFileMetadataSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
file
|
||||
);
|
||||
file = mergeMetadata([file])[0];
|
||||
const newFileSaveName = getUniqueFileSaveName(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
file.metadata.title,
|
||||
file.id
|
||||
);
|
||||
|
||||
const newFileSavePath = getFileSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
newFileSaveName
|
||||
);
|
||||
|
||||
const newFileMetadataSavePath = getFileMetadataSavePath(
|
||||
collectionIDPathMap.get(file.collectionID),
|
||||
newFileSaveName
|
||||
);
|
||||
await this.electronAPIs.checkExistsAndRename(
|
||||
oldFileSavePath,
|
||||
newFileSavePath
|
||||
);
|
||||
await this.electronAPIs.checkExistsAndRename(
|
||||
oldFileMetadataSavePath,
|
||||
newFileMetadataSavePath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async removeDeprecatedExportRecordProperties() {
|
||||
const exportRecord = (await this.getExportRecord()) as ExportRecordV1;
|
||||
if (exportRecord?.queuedFiles) {
|
||||
exportRecord.queuedFiles = undefined;
|
||||
}
|
||||
if (exportRecord?.progress) {
|
||||
exportRecord.progress = undefined;
|
||||
}
|
||||
if (exportRecord?.failedFiles) {
|
||||
exportRecord.failedFiles = undefined;
|
||||
}
|
||||
await this.updateExportRecord(exportRecord);
|
||||
}
|
||||
}
|
||||
export default new ExportService();
|
|
@ -10,10 +10,6 @@ export interface AppUpdateInfo {
|
|||
export interface ElectronAPIs {
|
||||
exists: (path: string) => boolean;
|
||||
checkExistsAndCreateDir: (dirPath: string) => Promise<void>;
|
||||
checkExistsAndRename: (
|
||||
oldDirPath: string,
|
||||
newDirPath: string
|
||||
) => Promise<void>;
|
||||
saveStreamToDisk: (
|
||||
path: string,
|
||||
fileStream: ReadableStream<any>
|
||||
|
@ -90,4 +86,7 @@ export interface ElectronAPIs {
|
|||
logRendererProcessMemoryUsage: (message: string) => Promise<void>;
|
||||
registerForegroundEventListener: (onForeground: () => void) => void;
|
||||
openDirectory: (dirPath: string) => Promise<void>;
|
||||
moveFile: (oldPath: string, newPath: string) => Promise<void>;
|
||||
deleteFolder: (path: string) => Promise<void>;
|
||||
rename: (oldPath: string, newPath: string) => Promise<void>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { ExportStage } from 'constants/export';
|
||||
|
||||
export type CollectionIDNameMap = Map<number, string>;
|
||||
export type CollectionIDPathMap = Map<number, string>;
|
||||
export interface ExportProgress {
|
||||
success: number;
|
||||
failed: number;
|
||||
|
@ -10,28 +8,54 @@ export interface ExportProgress {
|
|||
export interface ExportedCollectionPaths {
|
||||
[collectionID: number]: string;
|
||||
}
|
||||
|
||||
export interface CollectionExportNames {
|
||||
[ID: number]: string;
|
||||
}
|
||||
|
||||
export interface FileExportNames {
|
||||
[ID: string]: string;
|
||||
}
|
||||
|
||||
export interface FileExportStats {
|
||||
totalCount: number;
|
||||
pendingCount: number;
|
||||
}
|
||||
|
||||
export interface ExportRecordV0 {
|
||||
stage: ExportStage;
|
||||
lastAttemptTimestamp: number;
|
||||
progress: ExportProgress;
|
||||
queuedFiles: string[];
|
||||
exportedFiles: string[];
|
||||
failedFiles: string[];
|
||||
}
|
||||
|
||||
export interface ExportRecordV1 {
|
||||
version?: number;
|
||||
stage?: ExportStage;
|
||||
lastAttemptTimestamp?: number;
|
||||
progress?: ExportProgress;
|
||||
queuedFiles?: string[];
|
||||
exportedFiles?: string[];
|
||||
failedFiles?: string[];
|
||||
exportedCollectionPaths?: ExportedCollectionPaths;
|
||||
version: number;
|
||||
stage: ExportStage;
|
||||
lastAttemptTimestamp: number;
|
||||
progress: ExportProgress;
|
||||
queuedFiles: string[];
|
||||
exportedFiles: string[];
|
||||
failedFiles: string[];
|
||||
exportedCollectionPaths: ExportedCollectionPaths;
|
||||
}
|
||||
|
||||
export interface ExportRecordV2 {
|
||||
version: number;
|
||||
stage: ExportStage;
|
||||
lastAttemptTimestamp: number;
|
||||
exportedFiles: string[];
|
||||
exportedCollectionPaths: ExportedCollectionPaths;
|
||||
}
|
||||
|
||||
export interface ExportRecord {
|
||||
version: number;
|
||||
stage: ExportStage;
|
||||
lastAttemptTimestamp: number;
|
||||
exportedFiles: string[];
|
||||
exportedCollectionPaths: ExportedCollectionPaths;
|
||||
collectionExportNames: CollectionExportNames;
|
||||
fileExportNames: FileExportNames;
|
||||
}
|
||||
|
||||
export interface ExportSettings {
|
||||
|
@ -40,8 +64,8 @@ export interface ExportSettings {
|
|||
}
|
||||
|
||||
export interface ExportUIUpdaters {
|
||||
updateExportStage: (stage: ExportStage) => Promise<void>;
|
||||
updateExportProgress: (progress: ExportProgress) => void;
|
||||
updateFileExportStats: (fileExportStats: FileExportStats) => void;
|
||||
updateLastExportTime: (exportTime: number) => Promise<void>;
|
||||
setExportStage: (stage: ExportStage) => void;
|
||||
setExportProgress: (progress: ExportProgress) => void;
|
||||
setFileExportStats: (fileExportStats: FileExportStats) => void;
|
||||
setLastExportTime: (exportTime: number) => void;
|
||||
}
|
||||
|
|
|
@ -33,4 +33,5 @@ export type GalleryContextType = {
|
|||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
setBlockingLoad: (value: boolean) => void;
|
||||
photoListHeader: TimeStampListItem;
|
||||
openExportModal: () => void;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
addToCollection,
|
||||
getNonEmptyCollections,
|
||||
moveToCollection,
|
||||
removeFromCollection,
|
||||
restoreToCollection,
|
||||
|
@ -272,3 +273,21 @@ export function getCollectionNameMap(
|
|||
collections.map((collection) => [collection.id, collection.name])
|
||||
);
|
||||
}
|
||||
|
||||
export function getNonEmptyPersonalCollections(
|
||||
collections: Collection[],
|
||||
personalFiles: EnteFile[],
|
||||
user: User
|
||||
): Collection[] {
|
||||
if (!user?.id) {
|
||||
throw Error('user missing');
|
||||
}
|
||||
const nonEmptyCollections = getNonEmptyCollections(
|
||||
collections,
|
||||
personalFiles
|
||||
);
|
||||
const personalCollections = nonEmptyCollections.filter(
|
||||
(collection) => collection.owner.id === user?.id
|
||||
);
|
||||
return personalCollections;
|
||||
}
|
||||
|
|
|
@ -1,75 +1,106 @@
|
|||
import { Collection } from 'types/collection';
|
||||
import exportService from 'services/exportService';
|
||||
import { CollectionIDPathMap, ExportRecord } from 'types/export';
|
||||
import exportService from 'services/export';
|
||||
import {
|
||||
ExportRecord,
|
||||
CollectionExportNames,
|
||||
FileExportNames,
|
||||
} from 'types/export';
|
||||
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
import { Metadata } from 'types/upload';
|
||||
import { splitFilenameAndExtension } from 'utils/file';
|
||||
import { ENTE_METADATA_FOLDER } from 'constants/export';
|
||||
import { ENTE_METADATA_FOLDER, ENTE_TRASH_FOLDER } from 'constants/export';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { formatDateTimeShort } from 'utils/time/format';
|
||||
|
||||
export const getExportRecordFileUID = (file: EnteFile) =>
|
||||
`${file.id}_${file.collectionID}_${file.updationTime}`;
|
||||
|
||||
export const getCollectionsCreatedAfterLastExport = (
|
||||
collections: Collection[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const exportedCollections = new Set(
|
||||
Object.keys(exportRecord?.exportedCollectionPaths ?? {}).map((x) =>
|
||||
Number(x)
|
||||
)
|
||||
);
|
||||
const unExportedCollections = collections.filter((collection) => {
|
||||
if (!exportedCollections.has(collection.id)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return unExportedCollections;
|
||||
};
|
||||
export const getCollectionIDPathMapFromExportRecord = (
|
||||
exportRecord: ExportRecord
|
||||
): CollectionIDPathMap => {
|
||||
export const getCollectionIDFromFileUID = (fileUID: string) =>
|
||||
Number(fileUID.split('_')[1]);
|
||||
|
||||
export const convertCollectionIDExportNameObjectToMap = (
|
||||
collectionExportNames: CollectionExportNames
|
||||
): Map<number, string> => {
|
||||
return new Map<number, string>(
|
||||
Object.entries(exportRecord.exportedCollectionPaths ?? {}).map((e) => {
|
||||
Object.entries(collectionExportNames ?? {}).map((e) => {
|
||||
return [Number(e[0]), String(e[1])];
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getCollectionsRenamedAfterLastExport = (
|
||||
export const convertFileIDExportNameObjectToMap = (
|
||||
fileExportNames: FileExportNames
|
||||
): Map<string, string> => {
|
||||
return new Map<string, string>(
|
||||
Object.entries(fileExportNames ?? {}).map((e) => {
|
||||
return [String(e[0]), String(e[1])];
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getRenamedExportedCollections = (
|
||||
collections: Collection[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const collectionIDPathMap =
|
||||
getCollectionIDPathMapFromExportRecord(exportRecord);
|
||||
if (!exportRecord?.collectionExportNames) {
|
||||
return [];
|
||||
}
|
||||
const collectionIDExportNameMap = convertCollectionIDExportNameObjectToMap(
|
||||
exportRecord.collectionExportNames
|
||||
);
|
||||
const renamedCollections = collections.filter((collection) => {
|
||||
if (collectionIDPathMap.has(collection.id)) {
|
||||
const currentFolderName = collectionIDPathMap.get(collection.id);
|
||||
const startIndex = currentFolderName.lastIndexOf('/');
|
||||
const lastIndex = currentFolderName.lastIndexOf('(');
|
||||
const nameRoot = currentFolderName.slice(
|
||||
startIndex + 1,
|
||||
lastIndex !== -1 ? lastIndex : currentFolderName.length
|
||||
if (collectionIDExportNameMap.has(collection.id)) {
|
||||
const currentExportName = collectionIDExportNameMap.get(
|
||||
collection.id
|
||||
);
|
||||
|
||||
if (nameRoot !== sanitizeName(collection.name)) {
|
||||
return true;
|
||||
if (currentExportName === collection.name) {
|
||||
return false;
|
||||
}
|
||||
const hasNumberedSuffix = currentExportName.match(/\(\d+\)$/);
|
||||
const currentExportNameWithoutNumberedSuffix = hasNumberedSuffix
|
||||
? currentExportName.replace(/\(\d+\)$/, '')
|
||||
: currentExportName;
|
||||
|
||||
return collection.name !== currentExportNameWithoutNumberedSuffix;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return renamedCollections;
|
||||
};
|
||||
|
||||
export const getDeletedExportedCollections = (
|
||||
collections: Collection[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
if (!exportRecord?.collectionExportNames) {
|
||||
return [];
|
||||
}
|
||||
const presentCollections = new Set(
|
||||
collections.map((collection) => collection.id)
|
||||
);
|
||||
const deletedExportedCollections = Object.keys(
|
||||
exportRecord?.collectionExportNames
|
||||
)
|
||||
.map(Number)
|
||||
.filter((collectionID) => {
|
||||
if (!presentCollections.has(collectionID)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return deletedExportedCollections;
|
||||
};
|
||||
|
||||
export const getUnExportedFiles = (
|
||||
allFiles: EnteFile[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const exportedFiles = new Set(exportRecord?.exportedFiles);
|
||||
if (!exportRecord?.fileExportNames) {
|
||||
return allFiles;
|
||||
}
|
||||
const exportedFiles = new Set(Object.keys(exportRecord?.fileExportNames));
|
||||
const unExportedFiles = allFiles.filter((file) => {
|
||||
if (!exportedFiles.has(getExportRecordFileUID(file))) {
|
||||
return true;
|
||||
|
@ -79,27 +110,49 @@ export const getUnExportedFiles = (
|
|||
return unExportedFiles;
|
||||
};
|
||||
|
||||
export const getExportedFiles = (
|
||||
export const getDeletedExportedFiles = (
|
||||
allFiles: EnteFile[],
|
||||
exportRecord: ExportRecord
|
||||
) => {
|
||||
const exportedFileIds = new Set(exportRecord?.exportedFiles);
|
||||
const exportedFiles = allFiles.filter((file) => {
|
||||
if (exportedFileIds.has(getExportRecordFileUID(file))) {
|
||||
): string[] => {
|
||||
if (!exportRecord?.fileExportNames) {
|
||||
return [];
|
||||
}
|
||||
const presentFileUIDs = new Set(
|
||||
allFiles?.map((file) => getExportRecordFileUID(file))
|
||||
);
|
||||
const deletedExportedFiles = Object.keys(
|
||||
exportRecord?.fileExportNames
|
||||
).filter((fileUID) => {
|
||||
if (!presentFileUIDs.has(fileUID)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return exportedFiles;
|
||||
return deletedExportedFiles;
|
||||
};
|
||||
|
||||
export const dedupe = (files: string[]) => {
|
||||
const fileSet = new Set(files);
|
||||
return Array.from(fileSet);
|
||||
export const getCollectionExportedFiles = (
|
||||
exportRecord: ExportRecord,
|
||||
collectionID: number
|
||||
): string[] => {
|
||||
if (!exportRecord?.fileExportNames) {
|
||||
return [];
|
||||
}
|
||||
const collectionExportedFiles = Object.keys(
|
||||
exportRecord?.fileExportNames
|
||||
).filter((fileUID) => {
|
||||
const fileCollectionID = Number(fileUID.split('_')[1]);
|
||||
if (fileCollectionID === collectionID) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return collectionExportedFiles;
|
||||
};
|
||||
|
||||
export const getGoogleLikeMetadataFile = (
|
||||
fileSaveName: string,
|
||||
fileExportName: string,
|
||||
file: EnteFile
|
||||
) => {
|
||||
const metadata: Metadata = file.metadata;
|
||||
|
@ -110,7 +163,7 @@ export const getGoogleLikeMetadataFile = (
|
|||
const captionValue: string = file?.pubMagicMetadata?.data?.caption;
|
||||
return JSON.stringify(
|
||||
{
|
||||
title: fileSaveName,
|
||||
title: fileExportName,
|
||||
caption: captionValue,
|
||||
creationTime: {
|
||||
timestamp: creationTime,
|
||||
|
@ -130,89 +183,113 @@ export const getGoogleLikeMetadataFile = (
|
|||
);
|
||||
};
|
||||
|
||||
export const oldSanitizeName = (name: string) =>
|
||||
name.replaceAll('/', '_').replaceAll(' ', '_');
|
||||
|
||||
export const sanitizeName = (name: string) =>
|
||||
sanitize(name, { replacement: '_' });
|
||||
|
||||
export const getUniqueCollectionFolderPath = (
|
||||
export const getUniqueCollectionExportName = (
|
||||
dir: string,
|
||||
collectionID: number,
|
||||
collectionName: string
|
||||
): string => {
|
||||
if (!exportService.checkAllElectronAPIsExists()) {
|
||||
return getOldCollectionFolderPath(dir, collectionID, collectionName);
|
||||
}
|
||||
let collectionFolderPath = `${dir}/${sanitizeName(collectionName)}`;
|
||||
let count = 1;
|
||||
while (exportService.exists(collectionFolderPath)) {
|
||||
collectionFolderPath = `${dir}/${sanitizeName(
|
||||
collectionName
|
||||
)}(${count})`;
|
||||
count++;
|
||||
}
|
||||
return collectionFolderPath;
|
||||
};
|
||||
|
||||
export const getMetadataFolderPath = (collectionFolderPath: string) =>
|
||||
`${collectionFolderPath}/${ENTE_METADATA_FOLDER}`;
|
||||
|
||||
export const getUniqueFileSaveName = (
|
||||
collectionPath: string,
|
||||
filename: string,
|
||||
fileID: number
|
||||
) => {
|
||||
if (!exportService.checkAllElectronAPIsExists()) {
|
||||
return getOldFileSaveName(filename, fileID);
|
||||
}
|
||||
let fileSaveName = sanitizeName(filename);
|
||||
let collectionExportName = sanitizeName(collectionName);
|
||||
let count = 1;
|
||||
while (
|
||||
exportService.exists(getFileSavePath(collectionPath, fileSaveName))
|
||||
exportService.exists(
|
||||
getCollectionExportPath(dir, collectionExportName)
|
||||
) ||
|
||||
collectionExportName === ENTE_TRASH_FOLDER
|
||||
) {
|
||||
collectionExportName = `${sanitizeName(collectionName)}(${count})`;
|
||||
count++;
|
||||
}
|
||||
return collectionExportName;
|
||||
};
|
||||
|
||||
export const getMetadataFolderExportPath = (collectionExportPath: string) =>
|
||||
`${collectionExportPath}/${ENTE_METADATA_FOLDER}`;
|
||||
|
||||
export const getUniqueFileExportName = (
|
||||
collectionExportPath: string,
|
||||
filename: string
|
||||
) => {
|
||||
let fileExportName = sanitizeName(filename);
|
||||
let count = 1;
|
||||
while (
|
||||
exportService.exists(
|
||||
getFileExportPath(collectionExportPath, fileExportName)
|
||||
)
|
||||
) {
|
||||
const filenameParts = splitFilenameAndExtension(sanitizeName(filename));
|
||||
if (filenameParts[1]) {
|
||||
fileSaveName = `${filenameParts[0]}(${count}).${filenameParts[1]}`;
|
||||
fileExportName = `${filenameParts[0]}(${count}).${filenameParts[1]}`;
|
||||
} else {
|
||||
fileSaveName = `${filenameParts[0]}(${count})`;
|
||||
fileExportName = `${filenameParts[0]}(${count})`;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return fileSaveName;
|
||||
return fileExportName;
|
||||
};
|
||||
|
||||
export const getOldFileSaveName = (filename: string, fileID: number) =>
|
||||
`${fileID}_${oldSanitizeName(filename)}`;
|
||||
export const getFileMetadataExportPath = (
|
||||
collectionExportPath: string,
|
||||
fileExportName: string
|
||||
) => `${collectionExportPath}/${ENTE_METADATA_FOLDER}/${fileExportName}.json`;
|
||||
|
||||
export const getFileMetadataSavePath = (
|
||||
collectionFolderPath: string,
|
||||
fileSaveName: string
|
||||
) => `${collectionFolderPath}/${ENTE_METADATA_FOLDER}/${fileSaveName}.json`;
|
||||
export const getCollectionExportPath = (
|
||||
exportFolder: string,
|
||||
collectionExportName: string
|
||||
) => `${exportFolder}/${collectionExportName}`;
|
||||
|
||||
export const getFileSavePath = (
|
||||
collectionFolderPath: string,
|
||||
fileSaveName: string
|
||||
) => `${collectionFolderPath}/${fileSaveName}`;
|
||||
export const getFileExportPath = (
|
||||
collectionExportPath: string,
|
||||
fileExportName: string
|
||||
) => `${collectionExportPath}/${fileExportName}`;
|
||||
|
||||
export const getOldCollectionFolderPath = (
|
||||
dir: string,
|
||||
collectionID: number,
|
||||
collectionName: string
|
||||
) => `${dir}/${collectionID}_${oldSanitizeName(collectionName)}`;
|
||||
export const getTrashedFileExportPath = (exportDir: string, path: string) => {
|
||||
const fileRelativePath = path.replace(`${exportDir}/`, '');
|
||||
let trashedFilePath = `${exportDir}/${ENTE_TRASH_FOLDER}/${fileRelativePath}`;
|
||||
let count = 1;
|
||||
while (exportService.exists(trashedFilePath)) {
|
||||
const trashedFilePathParts = splitFilenameAndExtension(trashedFilePath);
|
||||
if (trashedFilePathParts[1]) {
|
||||
trashedFilePath = `${trashedFilePathParts[0]}(${count}).${trashedFilePathParts[1]}`;
|
||||
} else {
|
||||
trashedFilePath = `${trashedFilePathParts[0]}(${count})`;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return trashedFilePath;
|
||||
};
|
||||
|
||||
export const getOldFileSavePath = (
|
||||
collectionFolderPath: string,
|
||||
file: EnteFile
|
||||
// if filepath is /home/user/Ente/Export/Collection1/1.jpg
|
||||
// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json
|
||||
export const getMetadataFileExportPath = (filePath: string) => {
|
||||
// extract filename and collection folder path
|
||||
const filename = filePath.split('/').pop();
|
||||
const collectionExportPath = filePath.replace(`/${filename}`, '');
|
||||
return `${collectionExportPath}/${ENTE_METADATA_FOLDER}/${filename}.json`;
|
||||
};
|
||||
|
||||
export const getLivePhotoExportName = (
|
||||
imageExportName: string,
|
||||
videoExportName: string
|
||||
) =>
|
||||
`${collectionFolderPath}/${file.id}_${oldSanitizeName(
|
||||
file.metadata.title
|
||||
)}`;
|
||||
JSON.stringify({
|
||||
image: imageExportName,
|
||||
video: videoExportName,
|
||||
});
|
||||
|
||||
export const getOldFileMetadataSavePath = (
|
||||
collectionFolderPath: string,
|
||||
file: EnteFile
|
||||
) =>
|
||||
`${collectionFolderPath}/${ENTE_METADATA_FOLDER}/${
|
||||
file.id
|
||||
}_${oldSanitizeName(file.metadata.title)}.json`;
|
||||
export const isLivePhotoExportName = (exportName: string) => {
|
||||
try {
|
||||
JSON.parse(exportName);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseLivePhotoExportName = (
|
||||
livePhotoExportName: string
|
||||
): { image: string; video: string } => {
|
||||
const { image, video } = JSON.parse(livePhotoExportName);
|
||||
return { image, video };
|
||||
};
|
||||
|
|
144
apps/photos/src/utils/export/migration.ts
Normal file
144
apps/photos/src/utils/export/migration.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
import { ENTE_METADATA_FOLDER } from 'constants/export';
|
||||
import {
|
||||
ExportedCollectionPaths,
|
||||
ExportRecordV0,
|
||||
ExportRecordV1,
|
||||
ExportRecordV2,
|
||||
} from 'types/export';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { splitFilenameAndExtension } from 'utils/ffmpeg';
|
||||
import { getExportRecordFileUID, sanitizeName } from '.';
|
||||
import exportService from 'services/export';
|
||||
|
||||
export const convertCollectionIDFolderPathObjectToMap = (
|
||||
exportedCollectionPaths: ExportedCollectionPaths
|
||||
): Map<number, string> => {
|
||||
return new Map<number, string>(
|
||||
Object.entries(exportedCollectionPaths ?? {}).map((e) => {
|
||||
return [Number(e[0]), String(e[1])];
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getExportedFiles = (
|
||||
allFiles: EnteFile[],
|
||||
exportRecord: ExportRecordV0 | ExportRecordV1 | ExportRecordV2
|
||||
) => {
|
||||
if (!exportRecord?.exportedFiles) {
|
||||
return [];
|
||||
}
|
||||
const exportedFileIds = new Set(exportRecord?.exportedFiles);
|
||||
const exportedFiles = allFiles.filter((file) => {
|
||||
if (exportedFileIds.has(getExportRecordFileUID(file))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return exportedFiles;
|
||||
};
|
||||
|
||||
export const oldSanitizeName = (name: string) =>
|
||||
name.replaceAll('/', '_').replaceAll(' ', '_');
|
||||
|
||||
export const getUniqueCollectionFolderPath = (
|
||||
dir: string,
|
||||
collectionName: string
|
||||
): string => {
|
||||
let collectionFolderPath = `${dir}/${sanitizeName(collectionName)}`;
|
||||
let count = 1;
|
||||
while (exportService.exists(collectionFolderPath)) {
|
||||
collectionFolderPath = `${dir}/${sanitizeName(
|
||||
collectionName
|
||||
)}(${count})`;
|
||||
count++;
|
||||
}
|
||||
return collectionFolderPath;
|
||||
};
|
||||
|
||||
export const getMetadataFolderPath = (collectionFolderPath: string) =>
|
||||
`${collectionFolderPath}/${ENTE_METADATA_FOLDER}`;
|
||||
|
||||
export const getUniqueFileSaveName = (
|
||||
collectionPath: string,
|
||||
filename: string
|
||||
) => {
|
||||
let fileSaveName = sanitizeName(filename);
|
||||
let count = 1;
|
||||
while (
|
||||
exportService.exists(getFileSavePath(collectionPath, fileSaveName))
|
||||
) {
|
||||
const filenameParts = splitFilenameAndExtension(sanitizeName(filename));
|
||||
if (filenameParts[1]) {
|
||||
fileSaveName = `${filenameParts[0]}(${count}).${filenameParts[1]}`;
|
||||
} else {
|
||||
fileSaveName = `${filenameParts[0]}(${count})`;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return fileSaveName;
|
||||
};
|
||||
|
||||
export const getOldFileSaveName = (filename: string, fileID: number) =>
|
||||
`${fileID}_${oldSanitizeName(filename)}`;
|
||||
|
||||
export const getFileMetadataSavePath = (
|
||||
collectionFolderPath: string,
|
||||
fileSaveName: string
|
||||
) => `${collectionFolderPath}/${ENTE_METADATA_FOLDER}/${fileSaveName}.json`;
|
||||
|
||||
export const getFileSavePath = (
|
||||
collectionFolderPath: string,
|
||||
fileSaveName: string
|
||||
) => `${collectionFolderPath}/${fileSaveName}`;
|
||||
|
||||
export const getOldCollectionFolderPath = (
|
||||
dir: string,
|
||||
collectionID: number,
|
||||
collectionName: string
|
||||
) => `${dir}/${collectionID}_${oldSanitizeName(collectionName)}`;
|
||||
|
||||
export const getOldFileSavePath = (
|
||||
collectionFolderPath: string,
|
||||
file: EnteFile
|
||||
) =>
|
||||
`${collectionFolderPath}/${file.id}_${oldSanitizeName(
|
||||
file.metadata.title
|
||||
)}`;
|
||||
|
||||
export const getOldFileMetadataSavePath = (
|
||||
collectionFolderPath: string,
|
||||
file: EnteFile
|
||||
) =>
|
||||
`${collectionFolderPath}/${ENTE_METADATA_FOLDER}/${
|
||||
file.id
|
||||
}_${oldSanitizeName(file.metadata.title)}.json`;
|
||||
|
||||
export const getUniqueFileExportNameForMigration = (
|
||||
collectionPath: string,
|
||||
filename: string,
|
||||
usedFilePaths: Map<string, Set<string>>
|
||||
) => {
|
||||
let fileExportName = sanitizeName(filename);
|
||||
let count = 1;
|
||||
while (
|
||||
usedFilePaths
|
||||
.get(collectionPath)
|
||||
?.has(getFileSavePath(collectionPath, fileExportName))
|
||||
) {
|
||||
const filenameParts = splitFilenameAndExtension(sanitizeName(filename));
|
||||
if (filenameParts[1]) {
|
||||
fileExportName = `${filenameParts[0]}(${count}).${filenameParts[1]}`;
|
||||
} else {
|
||||
fileExportName = `${filenameParts[0]}(${count})`;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
if (!usedFilePaths.has(collectionPath)) {
|
||||
usedFilePaths.set(collectionPath, new Set());
|
||||
}
|
||||
usedFilePaths
|
||||
.get(collectionPath)
|
||||
.add(getFileSavePath(collectionPath, fileExportName));
|
||||
return fileExportName;
|
||||
};
|
|
@ -31,7 +31,6 @@ import { addLogLine } from 'utils/logging';
|
|||
import { CustomError } from 'utils/error';
|
||||
import { convertBytesToHumanReadable } from './size';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import { getLocalFiles } from 'services/fileService';
|
||||
|
||||
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
|
||||
|
||||
|
@ -571,11 +570,13 @@ export function getLatestVersionFiles(files: EnteFile[]) {
|
|||
);
|
||||
}
|
||||
|
||||
export async function getPersonalFiles() {
|
||||
const files = await getLocalFiles();
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
export function getPersonalFiles(files: EnteFile[], user: User) {
|
||||
if (!user?.id) {
|
||||
throw Error('user missing');
|
||||
}
|
||||
return files.filter((file) => file.ownerID === user.id);
|
||||
}
|
||||
|
||||
export function getIDBasedSortedFiles(files: EnteFile[]) {
|
||||
return files.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ export function getBestPossibleUserLocale(): Language {
|
|||
return Language.zh;
|
||||
} else if (lc.startsWith('nl')) {
|
||||
return Language.nl;
|
||||
} else if (lc.startsWith('es')) {
|
||||
return Language.es;
|
||||
}
|
||||
}
|
||||
return Language.en;
|
||||
|
|
|
@ -20,7 +20,7 @@ export const logError = async (
|
|||
);
|
||||
}
|
||||
Sentry.captureException(err, {
|
||||
level: Sentry.Severity.Info,
|
||||
level: 'info',
|
||||
user: { id: await getSentryUserID() },
|
||||
contexts: {
|
||||
...(info && {
|
||||
|
|
475
yarn.lock
475
yarn.lock
|
@ -324,6 +324,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.13":
|
||||
version "1.4.15"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||
|
||||
"@mui/base@5.0.0-alpha.124":
|
||||
version "5.0.0-alpha.124"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.124.tgz#164068642e41ba655fd2b9eaf881526909a41201"
|
||||
|
@ -593,143 +598,157 @@
|
|||
dependencies:
|
||||
dequal "^2.0.2"
|
||||
|
||||
"@rollup/plugin-commonjs@24.0.0":
|
||||
version "24.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c"
|
||||
integrity sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==
|
||||
dependencies:
|
||||
"@rollup/pluginutils" "^5.0.1"
|
||||
commondir "^1.0.1"
|
||||
estree-walker "^2.0.2"
|
||||
glob "^8.0.3"
|
||||
is-reference "1.2.1"
|
||||
magic-string "^0.27.0"
|
||||
|
||||
"@rollup/pluginutils@^5.0.1":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33"
|
||||
integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.0"
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
"@rushstack/eslint-patch@^1.1.3":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
|
||||
integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
|
||||
|
||||
"@sentry/browser@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/browser/-/browser-6.12.0.tgz"
|
||||
integrity sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ==
|
||||
"@sentry-internal/tracing@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.49.0.tgz#f589de565370884b9a13f82c98463de9b2d25dcd"
|
||||
integrity sha512-ESh3+ZneQk/3HESTUmIPNrW5GVPu/HrRJU+eAJJto74vm+6vP7zDn2YV2gJ1w18O/37nc7W/bVCgZJlhZ3cwew==
|
||||
dependencies:
|
||||
"@sentry/core" "6.12.0"
|
||||
"@sentry/types" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
"@sentry/core" "7.49.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/cli@^1.68.0":
|
||||
version "1.68.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/cli/-/cli-1.68.0.tgz"
|
||||
integrity sha512-zc7+cxKDqpHLREGJKRH6KwE8fZW8bnczg3OLibJ0czleXoWPdAuOK1Xm1BTMcOnaXfg3VKAh0rI7S1PTdj+SrQ==
|
||||
"@sentry/browser@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.49.0.tgz#5ce1cdb8d883c129d9a4e313c08a54c5ada4661b"
|
||||
integrity sha512-x2DekKkQoY7/dhBzE4J25mdQ978NtPBTVQb+uZqlF/t5mp4K44TAszmPqy8lC/CmVHkp7qcpRGSCIzeboUL4KA==
|
||||
dependencies:
|
||||
"@sentry-internal/tracing" "7.49.0"
|
||||
"@sentry/core" "7.49.0"
|
||||
"@sentry/replay" "7.49.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/cli@^1.74.6":
|
||||
version "1.75.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.0.tgz#4a5e71b5619cd4e9e6238cc77857c66f6b38d86a"
|
||||
integrity sha512-vT8NurHy00GcN8dNqur4CMIYvFH3PaKdkX3qllVvi4syybKqjwoz+aWRCvprbYv0knweneFkLt1SmBWqazUMfA==
|
||||
dependencies:
|
||||
https-proxy-agent "^5.0.0"
|
||||
mkdirp "^0.5.5"
|
||||
node-fetch "^2.6.0"
|
||||
npmlog "^4.1.2"
|
||||
node-fetch "^2.6.7"
|
||||
progress "^2.0.3"
|
||||
proxy-from-env "^1.1.0"
|
||||
which "^2.0.2"
|
||||
|
||||
"@sentry/core@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/core/-/core-6.12.0.tgz"
|
||||
integrity sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ==
|
||||
"@sentry/core@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.49.0.tgz#340d059f5efeff1a3359fef66d0c8e34e79ac992"
|
||||
integrity sha512-AlSnCYgfEbvK8pkNluUkmdW/cD9UpvOVCa+ERQswXNRkAv5aDGCL6Ihv6fnIajE++BYuwZh0+HwZUBVKTFzoZg==
|
||||
dependencies:
|
||||
"@sentry/hub" "6.12.0"
|
||||
"@sentry/minimal" "6.12.0"
|
||||
"@sentry/types" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/hub@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/hub/-/hub-6.12.0.tgz"
|
||||
integrity sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg==
|
||||
"@sentry/integrations@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.49.0.tgz#e123f687e0abe10d3428027e3879ce231503fc2f"
|
||||
integrity sha512-qsEVkcZjw+toFGnzsVo+Cozz+hMK9LugzkfJyOFL+CyiEx9MfkEmsvRpZe1ETEWKe/VZylYU27NQzl6UNuAUjw==
|
||||
dependencies:
|
||||
"@sentry/types" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/integrations@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.12.0.tgz"
|
||||
integrity sha512-M9gsVdWZp5fAFFpTjK2IBuWzW4SBxGAI3tVbYZvVx16S/BY0GsPC1dYpjJx9OTBS/8CmCWdGxnUmjACo/8w1LA==
|
||||
dependencies:
|
||||
"@sentry/types" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
localforage "^1.8.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/minimal@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.12.0.tgz"
|
||||
integrity sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw==
|
||||
"@sentry/nextjs@^7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.49.0.tgz#4cc1b8a6cf77db2d9f9cb488644f5f8f80505122"
|
||||
integrity sha512-MXcaIe1qgSDlRYIlq4XzjFNIBNmSRb4MnaQg7JKmoSzEh+AXvnRDNG5gYhsKJBKdBZGRsKyevNrbb9Yh9YpsNg==
|
||||
dependencies:
|
||||
"@sentry/hub" "6.12.0"
|
||||
"@sentry/types" "6.12.0"
|
||||
"@rollup/plugin-commonjs" "24.0.0"
|
||||
"@sentry/core" "7.49.0"
|
||||
"@sentry/integrations" "7.49.0"
|
||||
"@sentry/node" "7.49.0"
|
||||
"@sentry/react" "7.49.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
"@sentry/webpack-plugin" "1.20.0"
|
||||
chalk "3.0.0"
|
||||
rollup "2.78.0"
|
||||
stacktrace-parser "^0.1.10"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/nextjs@^6.7.1":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-6.12.0.tgz"
|
||||
integrity sha512-b1UjIGpKcd7ZTtkZXHz84zpsPaEgpq6HkV0rnXg2+AXicdolRYJoHFUrlTJQgrVssYe0InuypV65xvKlIfZamA==
|
||||
"@sentry/node@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.49.0.tgz#95c28848b5c27940b079c08e9987088e2bf9d821"
|
||||
integrity sha512-KLIrqcbKk4yR3g8fjl87Eyv4M9j4YI6b7sqVAZYj3FrX3mC6JQyGdlDfUpSKy604n1iAdr6OuUp5f9x7jPJaeQ==
|
||||
dependencies:
|
||||
"@sentry/core" "6.12.0"
|
||||
"@sentry/integrations" "6.12.0"
|
||||
"@sentry/node" "6.12.0"
|
||||
"@sentry/react" "6.12.0"
|
||||
"@sentry/tracing" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
"@sentry/webpack-plugin" "1.17.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/node/-/node-6.12.0.tgz"
|
||||
integrity sha512-hfAU3cX5sNWgqyDQBCOIQOZj21l0w1z2dG4MjmrMMHKrQ18pfMaaOtEwRXMCdjTUlwsK+L3TOoUB23lbezmu1A==
|
||||
dependencies:
|
||||
"@sentry/core" "6.12.0"
|
||||
"@sentry/hub" "6.12.0"
|
||||
"@sentry/tracing" "6.12.0"
|
||||
"@sentry/types" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
"@sentry-internal/tracing" "7.49.0"
|
||||
"@sentry/core" "7.49.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
cookie "^0.4.1"
|
||||
https-proxy-agent "^5.0.0"
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/react@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/react/-/react-6.12.0.tgz"
|
||||
integrity sha512-E8Nw9PPzP/EyMy64ksr9xcyYYlBmUA5ROnkPQp7o5wF0xf5/J+nMS1tQdyPnLQe2KUgHlN4kVs2HHft1m7mSYQ==
|
||||
"@sentry/react@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.49.0.tgz#9a31808d4232d3010019e09d7c706b3d4fe54960"
|
||||
integrity sha512-s+ROJr1tP9zVBmoOn94JM+fu2TuoJKxkSXTEUOKoQ9P6P5ROzpDqTzHRGk6u4OjZTy5tftRyEqBGM2Iaf9Y+UA==
|
||||
dependencies:
|
||||
"@sentry/browser" "6.12.0"
|
||||
"@sentry/minimal" "6.12.0"
|
||||
"@sentry/types" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
"@sentry/browser" "7.49.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.12.0.tgz"
|
||||
integrity sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA==
|
||||
"@sentry/replay@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.49.0.tgz#c7f16bc3ca0c5911f641738f8894eb596c5da00d"
|
||||
integrity sha512-UY3bHoBDPOu4Dpq3m3oxNjLrq09NiFVYUfrTN4QOq1Am2SA04XbuCj/YZ+jNVy/NrFtoz9cTovK6oQbNw53jog==
|
||||
dependencies:
|
||||
"@sentry/hub" "6.12.0"
|
||||
"@sentry/minimal" "6.12.0"
|
||||
"@sentry/types" "6.12.0"
|
||||
"@sentry/utils" "6.12.0"
|
||||
"@sentry/core" "7.49.0"
|
||||
"@sentry/types" "7.49.0"
|
||||
"@sentry/utils" "7.49.0"
|
||||
|
||||
"@sentry/types@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.49.0.tgz#2c217091e13dc373682f5be2e9b5baed9d2ae695"
|
||||
integrity sha512-9yXXh7iv76+O6h2ONUVx0wsL1auqJFWez62mTjWk4350SgMmWp/zUkBxnVXhmcYqscz/CepC+Loz9vITLXtgxg==
|
||||
|
||||
"@sentry/utils@7.49.0":
|
||||
version "7.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.49.0.tgz#b1b3a2af52067dd27e660c7c3062a31cdf4b94f9"
|
||||
integrity sha512-JdC9yGnOgev4ISJVwmIoFsk8Zx0psDZJAj2DV7x4wMZsO6QK+YjC7G3mUED/S5D5lsrkBZ/3uvQQhr8DQI4UcQ==
|
||||
dependencies:
|
||||
"@sentry/types" "7.49.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/types/-/types-6.12.0.tgz"
|
||||
integrity sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA==
|
||||
|
||||
"@sentry/utils@6.12.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.npmjs.org/@sentry/utils/-/utils-6.12.0.tgz"
|
||||
integrity sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA==
|
||||
"@sentry/webpack-plugin@1.20.0":
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58"
|
||||
integrity sha512-Ssj1mJVFsfU6vMCOM2d+h+KQR7QHSfeIP16t4l20Uq/neqWXZUQ2yvQfe4S3BjdbJXz/X4Rw8Hfy1Sd0ocunYw==
|
||||
dependencies:
|
||||
"@sentry/types" "6.12.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/webpack-plugin@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.17.1.tgz"
|
||||
integrity sha512-L47a0hxano4a+9jbvQSBzHCT1Ph8fYAvGGUvFg8qc69yXS9si5lXRNIH/pavN6mqJjhQjAcEsEp+vxgvT4xZDQ==
|
||||
dependencies:
|
||||
"@sentry/cli" "^1.68.0"
|
||||
"@sentry/cli" "^1.74.6"
|
||||
webpack-sources "^2.0.0 || ^3.0.0"
|
||||
|
||||
"@stripe/stripe-js@^1.13.2":
|
||||
version "1.17.1"
|
||||
|
@ -816,6 +835,11 @@
|
|||
resolved "https://registry.npmjs.org/@types/debounce-promise/-/debounce-promise-3.1.4.tgz"
|
||||
integrity sha512-9SEVY3nsz+uMN2DwDocftB5TAgZe7D0cOzxxRhpotWs6T4QFqRaTXpXbOSzbk31/7iYcfCkJJPwWGzTxyuGhCg==
|
||||
|
||||
"@types/estree@*", "@types/estree@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
|
||||
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
|
||||
|
||||
"@types/http-proxy@^1.17.5":
|
||||
version "1.17.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a"
|
||||
|
@ -1218,16 +1242,6 @@ ansi-escapes@^4.3.0:
|
|||
dependencies:
|
||||
type-fest "^0.21.3"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz"
|
||||
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
|
||||
|
||||
ansi-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz"
|
||||
integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
|
||||
|
||||
ansi-regex@^5.0.0, ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
|
@ -1247,19 +1261,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
|||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
aproba@^1.0.3:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz"
|
||||
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
|
||||
|
||||
are-we-there-yet@~1.1.2:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz"
|
||||
integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==
|
||||
dependencies:
|
||||
delegates "^1.0.0"
|
||||
readable-stream "^2.0.6"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
|
@ -1418,6 +1419,13 @@ brace-expansion@^1.1.7:
|
|||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.1, braces@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
|
||||
|
@ -1455,6 +1463,14 @@ caniuse-lite@^1.0.30001406:
|
|||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001444.tgz#c0a530776eb44d933b493de1d05346f2527b30fc"
|
||||
integrity sha512-ecER9xgJQVMqcrxThKptsW0pPxSae8R2RB87LNa+ivW9ppNWRHEplXcDzkCOP4LYWGj8hunXLqaiC41iBATNyg==
|
||||
|
||||
chalk@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
|
||||
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^2.0.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
|
||||
|
@ -1527,11 +1543,6 @@ clsx@^1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
|
||||
code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz"
|
||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
@ -1571,16 +1582,16 @@ commander@^7.2.0:
|
|||
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz"
|
||||
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
|
||||
|
||||
convert-source-map@^1.5.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
|
||||
|
@ -1755,11 +1766,6 @@ define-properties@^1.1.4:
|
|||
has-property-descriptors "^1.0.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
delegates@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
density-clustering@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/density-clustering/-/density-clustering-1.3.0.tgz#dc9f59c8f0ab97e1624ac64930fd3194817dcac5"
|
||||
|
@ -2308,6 +2314,11 @@ estraverse@^5.3.0:
|
|||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
|
||||
estree-walker@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
|
||||
|
@ -2474,6 +2485,11 @@ fs.realpath@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
|
||||
|
@ -2494,20 +2510,6 @@ functions-have-names@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz"
|
||||
integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
|
||||
dependencies:
|
||||
aproba "^1.0.3"
|
||||
console-control-strings "^1.0.0"
|
||||
has-unicode "^2.0.0"
|
||||
object-assign "^4.1.0"
|
||||
signal-exit "^3.0.0"
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.1"
|
||||
wide-align "^1.1.0"
|
||||
|
||||
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz"
|
||||
|
@ -2583,6 +2585,17 @@ glob@7.1.7, glob@^7.1.3:
|
|||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^8.0.3:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
|
||||
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
globals@^13.19.0:
|
||||
version "13.19.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8"
|
||||
|
@ -2703,11 +2716,6 @@ has-tostringtag@^1.0.0:
|
|||
dependencies:
|
||||
has-symbols "^1.0.2"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz"
|
||||
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
|
||||
|
||||
has@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
|
||||
|
@ -2993,18 +3001,6 @@ is-extglob@^2.1.1:
|
|||
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
|
||||
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
|
||||
|
||||
is-fullwidth-code-point@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"
|
||||
integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
|
||||
dependencies:
|
||||
number-is-nan "^1.0.0"
|
||||
|
||||
is-fullwidth-code-point@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
|
||||
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
|
||||
|
@ -3061,6 +3057,13 @@ is-plain-obj@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
|
||||
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
|
||||
|
||||
is-reference@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
|
||||
integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
is-regex@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz"
|
||||
|
@ -3423,6 +3426,13 @@ lru_map@^0.3.3:
|
|||
resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz"
|
||||
integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=
|
||||
|
||||
magic-string@^0.27.0:
|
||||
version "0.27.0"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
|
||||
integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.13"
|
||||
|
||||
md5.js@^1.3.4:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz"
|
||||
|
@ -3475,6 +3485,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2:
|
|||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^5.0.1:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
|
||||
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.5:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
|
@ -3589,10 +3606,12 @@ node-fetch@2.6.7, node-fetch@~2.6.1:
|
|||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^2.6.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz"
|
||||
integrity sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==
|
||||
node-fetch@^2.6.7:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
|
||||
integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.9"
|
||||
|
@ -3613,21 +3632,6 @@ npm-run-path@^4.0.1:
|
|||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
npmlog@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz"
|
||||
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
||||
dependencies:
|
||||
are-we-there-yet "~1.1.2"
|
||||
console-control-strings "~1.1.0"
|
||||
gauge "~2.7.3"
|
||||
set-blocking "~2.0.0"
|
||||
|
||||
number-is-nan@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz"
|
||||
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
|
||||
|
||||
object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
|
||||
|
@ -4182,19 +4186,6 @@ readable-stream@^2.0.2:
|
|||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@^2.0.6, readable-stream@~2.3.6:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz"
|
||||
|
@ -4214,6 +4205,19 @@ readable-stream@~1.0.17, readable-stream@~1.0.27-1:
|
|||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@~2.3.6:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-web-to-node-stream@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz"
|
||||
|
@ -4314,6 +4318,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rollup@2.78.0:
|
||||
version "2.78.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e"
|
||||
integrity sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
|
||||
|
@ -4388,11 +4399,6 @@ semver@^7.3.7:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
|
||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||
|
||||
set-immediate-shim@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz"
|
||||
|
@ -4427,7 +4433,7 @@ side-channel@^1.0.4:
|
|||
get-intrinsic "^1.0.2"
|
||||
object-inspect "^1.9.0"
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
|
||||
signal-exit@^3.0.2, signal-exit@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz"
|
||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||
|
@ -4484,6 +4490,13 @@ source-map@^0.5.7:
|
|||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
|
||||
|
||||
stacktrace-parser@^0.1.10:
|
||||
version "0.1.10"
|
||||
resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a"
|
||||
integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==
|
||||
dependencies:
|
||||
type-fest "^0.7.1"
|
||||
|
||||
stop-iteration-iterator@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"
|
||||
|
@ -4496,23 +4509,6 @@ string-argv@0.3.1:
|
|||
resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz"
|
||||
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
|
||||
|
||||
string-width@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"
|
||||
integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
|
||||
dependencies:
|
||||
code-point-at "^1.0.0"
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz"
|
||||
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||
dependencies:
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz"
|
||||
|
@ -4598,20 +4594,6 @@ stringify-object@^3.3.0:
|
|||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
|
||||
integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz"
|
||||
integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
|
||||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
strip-ansi@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz"
|
||||
|
@ -4867,6 +4849,11 @@ type-fest@^0.21.3:
|
|||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
type-fest@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48"
|
||||
integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==
|
||||
|
||||
typed-array-length@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
|
||||
|
@ -4965,6 +4952,11 @@ webpack-bundle-analyzer@4.7.0:
|
|||
sirv "^1.0.7"
|
||||
ws "^7.3.1"
|
||||
|
||||
"webpack-sources@^2.0.0 || ^3.0.0":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
|
@ -5006,20 +4998,13 @@ which-typed-array@^1.1.9:
|
|||
has-tostringtag "^1.0.0"
|
||||
is-typed-array "^1.1.10"
|
||||
|
||||
which@^2.0.1:
|
||||
which@^2.0.1, which@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
|
||||
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
wide-align@^1.1.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz"
|
||||
integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
|
||||
dependencies:
|
||||
string-width "^1.0.2 || 2"
|
||||
|
||||
word-wrap@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
|
||||
|
|
Loading…
Reference in a new issue