mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-03-12 02:23:46 +00:00
Allow uploading images together with the GEDCOM file.
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user