Merge branch 'main' into monorepo

This commit is contained in:
Abhinav 2023-05-09 22:48:34 +05:30
commit 16274fa6e1
42 changed files with 2391 additions and 1377 deletions

View file

@ -23,6 +23,10 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
module.exports = (phase) =>
withSentryConfig(
withBundleAnalyzer({
sentry: {
hideSourceMaps: false,
widenClientFileUpload: true,
},
compiler: {
emotion: {
importMap: {

View file

@ -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",

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "",
"UNCATEGORIZED": "",
"ARCHIVE": "",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "",
"ARCHIVE_SECTION_NAME": "",
"ALL_SECTION_NAME": "",

View file

@ -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",

View file

@ -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"
}

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "",
"UNCATEGORIZED": "",
"ARCHIVE": "",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "",
"ARCHIVE_SECTION_NAME": "",
"ALL_SECTION_NAME": "",

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "上传至相册",
"UNCATEGORIZED": "未分类的",
"ARCHIVE": "存档",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "存档相册",
"ARCHIVE_SECTION_NAME": "存档",
"ALL_SECTION_NAME": "全部",

View file

@ -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",

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "",
"UNCATEGORIZED": "",
"ARCHIVE": "",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "",
"ARCHIVE_SECTION_NAME": "",
"ALL_SECTION_NAME": "",

View file

@ -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",

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "",
"UNCATEGORIZED": "",
"ARCHIVE": "",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "",
"ARCHIVE_SECTION_NAME": "",
"ALL_SECTION_NAME": "",

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "",
"UNCATEGORIZED": "",
"ARCHIVE": "",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "",
"ARCHIVE_SECTION_NAME": "",
"ALL_SECTION_NAME": "",

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "",
"UNCATEGORIZED": "",
"ARCHIVE": "",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "",
"ARCHIVE_SECTION_NAME": "",
"ALL_SECTION_NAME": "",

View file

@ -290,6 +290,7 @@
"UPLOAD_TO_COLLECTION": "",
"UNCATEGORIZED": "",
"ARCHIVE": "",
"FAVORITES": "",
"ARCHIVE_COLLECTION": "",
"ARCHIVE_SECTION_NAME": "",
"ALL_SECTION_NAME": "",

View file

@ -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>

View file

@ -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={{

View file

@ -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>
);
};

View file

@ -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;
}
}

View file

@ -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)}
/>
</>
);
}

View file

@ -16,6 +16,8 @@ const getLocaleDisplayName = (l: Language) => {
return '中文';
case Language.nl:
return 'Nederlands';
case Language.es:
return 'Español';
}
};

View file

@ -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,

View file

@ -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,

View file

@ -4,4 +4,5 @@ export enum Language {
fr = 'fr',
zh = 'zh',
nl = 'nl',
es = 'es',
}

View file

@ -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'),
});

View 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;

View file

@ -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}

View file

@ -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>
);

View file

@ -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={

View file

@ -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)

File diff suppressed because it is too large Load diff

View 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);
}
}

View file

@ -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();

View file

@ -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>;
}

View file

@ -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;
}

View file

@ -33,4 +33,5 @@ export type GalleryContextType = {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
setBlockingLoad: (value: boolean) => void;
photoListHeader: TimeStampListItem;
openExportModal: () => void;
};

View file

@ -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;
}

View file

@ -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 };
};

View 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;
};

View file

@ -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);
}

View file

@ -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;

View file

@ -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
View file

@ -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"