mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-08-03 09:31:52 +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;
|
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) => {
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user