Allow uploading images together with the GEDCOM file.

This commit is contained in:
Przemek Wiech
2019-03-12 19:30:20 +01:00
parent f75eee4e82
commit 61d4f43357
4 changed files with 99 additions and 20 deletions

View File

@@ -59,6 +59,8 @@ export class App extends React.Component<RouteComponentProps, {}> {
return; return;
} }
const gedcom = this.props.location.state && this.props.location.state.data; 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 search = queryString.parse(this.props.location.search);
const getParam = (name: string) => { const getParam = (name: string) => {
const value = search[name]; const value = search[name];
@@ -76,7 +78,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
this.props.history.replace({pathname: '/'}); this.props.history.replace({pathname: '/'});
} else if (this.isNewData(hash, url)) { } else if (this.isNewData(hash, url)) {
const loadedData = hash const loadedData = hash
? loadGedcom(hash, gedcom) ? loadGedcom(hash, gedcom, images)
: loadFromUrl(url!, handleCors); : loadFromUrl(url!, handleCors);
loadedData.then( loadedData.then(
(data) => { (data) => {

View File

@@ -105,7 +105,15 @@ function sortChildren(gedcom: JsonGedcomData): JsonGedcomData {
* Removes images that are not HTTP links. * Removes images that are not HTTP links.
* Does not modify the input object. * Does not modify the input object.
*/ */
function filterImage(indi: JsonIndi): JsonIndi { function filterImage(indi: JsonIndi, images: Map<string, string>): 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')) { if (!indi.imageUrl || indi.imageUrl.startsWith('http')) {
return indi; return indi;
} }
@@ -118,17 +126,26 @@ function filterImage(indi: JsonIndi): JsonIndi {
* Removes images that are not HTTP links. * Removes images that are not HTTP links.
* Does not modify the input object. * Does not modify the input object.
*/ */
function filterImages(gedcom: JsonGedcomData): JsonGedcomData { function filterImages(
const newIndis = gedcom.indis.map(filterImage); gedcom: JsonGedcomData,
images: Map<string, string>,
): JsonGedcomData {
const newIndis = gedcom.indis.map((indi) => filterImage(indi, images));
return Object.assign({}, gedcom, {indis: newIndis}); return Object.assign({}, gedcom, {indis: newIndis});
} }
/** /**
* Converts GEDCOM file into JSON data performing additional transformations: * Converts GEDCOM file into JSON data performing additional transformations:
* - sort children by birth date * - 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<string, string>,
): TopolaData {
const entries = parseGedcom(gedcom); const entries = parseGedcom(gedcom);
const json = gedcomEntriesToJson(entries); const json = gedcomEntriesToJson(entries);
if ( if (
@@ -142,7 +159,7 @@ export function convertGedcom(gedcom: string): TopolaData {
} }
return { return {
chartData: filterImages(sortChildren(json)), chartData: filterImages(sortChildren(json), images),
gedcom: prepareGedcom(entries), gedcom: prepareGedcom(entries),
}; };
} }

View File

@@ -16,8 +16,12 @@ export function getSelection(
}; };
} }
function prepareData(gedcom: string, cacheId: string): TopolaData { function prepareData(
const data = convertGedcom(gedcom); gedcom: string,
cacheId: string,
images?: Map<string, string>,
): TopolaData {
const data = convertGedcom(gedcom, images || new Map());
const serializedData = JSON.stringify(data); const serializedData = JSON.stringify(data);
try { try {
sessionStorage.setItem(cacheId, serializedData); sessionStorage.setItem(cacheId, serializedData);
@@ -54,7 +58,11 @@ export function loadFromUrl(
} }
/** Loads data from the given GEDCOM file contents. */ /** Loads data from the given GEDCOM file contents. */
function loadGedcomSync(hash: string, gedcom?: string) { function loadGedcomSync(
hash: string,
gedcom?: string,
images?: Map<string, string>,
) {
const cachedData = sessionStorage.getItem(hash); const cachedData = sessionStorage.getItem(hash);
if (cachedData) { if (cachedData) {
return JSON.parse(cachedData); return JSON.parse(cachedData);
@@ -62,13 +70,17 @@ function loadGedcomSync(hash: string, gedcom?: string) {
if (!gedcom) { if (!gedcom) {
throw new Error('Error loading data. Please upload your file again.'); 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. */ /** Loads data from the given GEDCOM file contents. */
export function loadGedcom(hash: string, gedcom?: string): Promise<TopolaData> { export function loadGedcom(
hash: string,
gedcom?: string,
images?: Map<string, string>,
): Promise<TopolaData> {
try { try {
return Promise.resolve(loadGedcomSync(hash, gedcom)); return Promise.resolve(loadGedcomSync(hash, gedcom, images));
} catch (e) { } catch (e) {
return Promise.reject(new Error('Failed to read GEDCOM file')); return Promise.reject(new Error('Failed to read GEDCOM file'));
} }

View File

@@ -29,6 +29,31 @@ interface Props {
onDownloadSvg: () => void; onDownloadSvg: () => void;
} }
function loadFileAsText(file: File): Promise<string> {
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<string> {
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< export class TopBar extends React.Component<
RouteComponentProps & Props, RouteComponentProps & Props,
State State
@@ -42,17 +67,39 @@ export class TopBar extends React.Component<
if (!files || !files.length) { if (!files || !files.length) {
return; return;
} }
const reader = new FileReader(); const filesArray = Array.from(files);
reader.onload = (evt: ProgressEvent) => { const gedcomFile =
const data = (evt.target as FileReader).result; files.length === 1
const hash = md5(data as string); ? 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({ this.props.history.push({
pathname: '/view', pathname: '/view',
search: queryString.stringify({file: hash}), 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. */ /** Opens the "Load from URL" dialog. */
@@ -195,6 +242,7 @@ export class TopBar extends React.Component<
type="file" type="file"
accept=".ged" accept=".ged"
id="fileInput" id="fileInput"
multiple
onChange={(e) => this.handleUpload(e)} onChange={(e) => this.handleUpload(e)}
/> />
<label htmlFor="fileInput"> <label htmlFor="fileInput">