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;
}
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<RouteComponentProps, {}> {
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) => {

View File

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

View File

@@ -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<string, string>,
): 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<string, string>,
) {
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<TopolaData> {
export function loadGedcom(
hash: string,
gedcom?: string,
images?: Map<string, string>,
): Promise<TopolaData> {
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'));
}

View File

@@ -29,6 +29,31 @@ interface Props {
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<
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)}
/>
<label htmlFor="fileInput">