From 61d4f433570221196833cc26a01a2e5372767a6c Mon Sep 17 00:00:00 2001 From: Przemek Wiech Date: Tue, 12 Mar 2019 19:30:20 +0100 Subject: [PATCH] Allow uploading images together with the GEDCOM file. --- src/app.tsx | 4 ++- src/gedcom_util.ts | 29 +++++++++++++++++----- src/load_data.ts | 24 +++++++++++++----- src/top_bar.tsx | 62 ++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 99 insertions(+), 20 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index 27df419..828972b 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -59,6 +59,8 @@ export class App extends React.Component { return; } const gedcom = this.props.location.state && this.props.location.state.data; + const images = + this.props.location.state && this.props.location.state.images; const search = queryString.parse(this.props.location.search); const getParam = (name: string) => { const value = search[name]; @@ -76,7 +78,7 @@ export class App extends React.Component { this.props.history.replace({pathname: '/'}); } else if (this.isNewData(hash, url)) { const loadedData = hash - ? loadGedcom(hash, gedcom) + ? loadGedcom(hash, gedcom, images) : loadFromUrl(url!, handleCors); loadedData.then( (data) => { diff --git a/src/gedcom_util.ts b/src/gedcom_util.ts index cd1540f..e3ccaa6 100644 --- a/src/gedcom_util.ts +++ b/src/gedcom_util.ts @@ -105,7 +105,15 @@ function sortChildren(gedcom: JsonGedcomData): JsonGedcomData { * Removes images that are not HTTP links. * Does not modify the input object. */ -function filterImage(indi: JsonIndi): JsonIndi { +function filterImage(indi: JsonIndi, images: Map): JsonIndi { + if (indi.imageUrl) { + const fileName = indi.imageUrl.match(/[^/\\]*$/)![0]; + if (images.has(fileName)) { + const newIndi = Object.assign({}, indi); + newIndi.imageUrl = images.get(fileName); + return newIndi; + } + } if (!indi.imageUrl || indi.imageUrl.startsWith('http')) { return indi; } @@ -118,17 +126,26 @@ function filterImage(indi: JsonIndi): JsonIndi { * Removes images that are not HTTP links. * Does not modify the input object. */ -function filterImages(gedcom: JsonGedcomData): JsonGedcomData { - const newIndis = gedcom.indis.map(filterImage); +function filterImages( + gedcom: JsonGedcomData, + images: Map, +): JsonGedcomData { + const newIndis = gedcom.indis.map((indi) => filterImage(indi, images)); return Object.assign({}, gedcom, {indis: newIndis}); } /** * Converts GEDCOM file into JSON data performing additional transformations: * - sort children by birth date - * - remove images that are not HTTP links. + * - remove images that are not HTTP links and aren't mapped in `images`. + * + * @param images Map from file name to image URL. This is used to pass in + * uploaded images. */ -export function convertGedcom(gedcom: string): TopolaData { +export function convertGedcom( + gedcom: string, + images: Map, +): TopolaData { const entries = parseGedcom(gedcom); const json = gedcomEntriesToJson(entries); if ( @@ -142,7 +159,7 @@ export function convertGedcom(gedcom: string): TopolaData { } return { - chartData: filterImages(sortChildren(json)), + chartData: filterImages(sortChildren(json), images), gedcom: prepareGedcom(entries), }; } diff --git a/src/load_data.ts b/src/load_data.ts index f086de5..08fa6cc 100644 --- a/src/load_data.ts +++ b/src/load_data.ts @@ -16,8 +16,12 @@ export function getSelection( }; } -function prepareData(gedcom: string, cacheId: string): TopolaData { - const data = convertGedcom(gedcom); +function prepareData( + gedcom: string, + cacheId: string, + images?: Map, +): TopolaData { + const data = convertGedcom(gedcom, images || new Map()); const serializedData = JSON.stringify(data); try { sessionStorage.setItem(cacheId, serializedData); @@ -54,7 +58,11 @@ export function loadFromUrl( } /** Loads data from the given GEDCOM file contents. */ -function loadGedcomSync(hash: string, gedcom?: string) { +function loadGedcomSync( + hash: string, + gedcom?: string, + images?: Map, +) { const cachedData = sessionStorage.getItem(hash); if (cachedData) { return JSON.parse(cachedData); @@ -62,13 +70,17 @@ function loadGedcomSync(hash: string, gedcom?: string) { if (!gedcom) { throw new Error('Error loading data. Please upload your file again.'); } - return prepareData(gedcom, hash); + return prepareData(gedcom, hash, images); } /** Loads data from the given GEDCOM file contents. */ -export function loadGedcom(hash: string, gedcom?: string): Promise { +export function loadGedcom( + hash: string, + gedcom?: string, + images?: Map, +): Promise { try { - return Promise.resolve(loadGedcomSync(hash, gedcom)); + return Promise.resolve(loadGedcomSync(hash, gedcom, images)); } catch (e) { return Promise.reject(new Error('Failed to read GEDCOM file')); } diff --git a/src/top_bar.tsx b/src/top_bar.tsx index 1e8f938..7842186 100644 --- a/src/top_bar.tsx +++ b/src/top_bar.tsx @@ -29,6 +29,31 @@ interface Props { onDownloadSvg: () => void; } +function loadFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (evt: ProgressEvent) => { + resolve((evt.target as FileReader).result as string); + }; + reader.readAsText(file); + }); +} + +function loadFileAsDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (evt: ProgressEvent) => { + resolve((evt.target as FileReader).result as string); + }; + reader.readAsDataURL(file); + }); +} + +function isImageFileName(fileName: string) { + const lower = fileName.toLowerCase(); + return lower.endsWith('.jpg') || lower.endsWith('.png'); +} + export class TopBar extends React.Component< RouteComponentProps & Props, State @@ -42,17 +67,39 @@ export class TopBar extends React.Component< if (!files || !files.length) { return; } - const reader = new FileReader(); - reader.onload = (evt: ProgressEvent) => { - const data = (evt.target as FileReader).result; - const hash = md5(data as string); + const filesArray = Array.from(files); + const gedcomFile = + files.length === 1 + ? files[0] + : filesArray.find((file) => file.name.toLowerCase().endsWith('.ged')) || + files[0]; + + // Convert uploaded images to object URLs. + const images = filesArray + .filter( + (file) => file.name !== gedcomFile.name && isImageFileName(file.name), + ) + .map((file) => ({ + name: file.name, + url: URL.createObjectURL(file), + })); + const imageMap = new Map( + images.map((entry) => [entry.name, entry.url] as [string, string]), + ); + loadFileAsText(gedcomFile).then((data) => { + const imageFileNames = images + .map((image) => image.name) + .sort() + .join('|'); + // Hash GEDCOM contents with uploaded image file names. + const hash = md5(md5(data) + imageFileNames); this.props.history.push({ pathname: '/view', search: queryString.stringify({file: hash}), - state: {data}, + state: {data, images: imageMap}, }); - }; - reader.readAsText(files[0]); + }); + (event.target as HTMLInputElement).value = ''; // Reset the file input. } /** Opens the "Load from URL" dialog. */ @@ -195,6 +242,7 @@ export class TopBar extends React.Component< type="file" accept=".ged" id="fileInput" + multiple onChange={(e) => this.handleUpload(e)} />