mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-08-04 01:51:49 +00:00
Use async/await for async functions.
This commit is contained in:
77
src/app.tsx
77
src/app.tsx
@@ -54,7 +54,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
this.componentDidUpdate();
|
this.componentDidUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
async componentDidUpdate() {
|
||||||
if (this.props.location.pathname !== '/view') {
|
if (this.props.location.pathname !== '/view') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,45 +77,42 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
if (!url && !hash) {
|
if (!url && !hash) {
|
||||||
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
|
try {
|
||||||
? loadGedcom(hash, gedcom, images)
|
// Set loading state.
|
||||||
: loadFromUrl(url!, handleCors);
|
this.setState(
|
||||||
loadedData.then(
|
Object.assign({}, this.state, {
|
||||||
(data) => {
|
data: undefined,
|
||||||
// Set state with data.
|
selection: undefined,
|
||||||
this.setState(
|
hash,
|
||||||
Object.assign({}, this.state, {
|
error: undefined,
|
||||||
data,
|
loading: true,
|
||||||
hash,
|
url,
|
||||||
selection: getSelection(data.chartData, indi, generation),
|
}),
|
||||||
error: undefined,
|
);
|
||||||
loading: false,
|
const data = hash
|
||||||
url,
|
? await loadGedcom(hash, gedcom, images)
|
||||||
showSidePanel,
|
: await loadFromUrl(url!, handleCors);
|
||||||
}),
|
// Set state with data.
|
||||||
);
|
this.setState(
|
||||||
},
|
Object.assign({}, this.state, {
|
||||||
(error) => {
|
data,
|
||||||
// Set error state.
|
hash,
|
||||||
this.setState(
|
selection: getSelection(data.chartData, indi, generation),
|
||||||
Object.assign({}, this.state, {
|
error: undefined,
|
||||||
error: error.message,
|
loading: false,
|
||||||
loading: false,
|
url,
|
||||||
}),
|
showSidePanel,
|
||||||
);
|
}),
|
||||||
},
|
);
|
||||||
);
|
} catch (error) {
|
||||||
// Set loading state.
|
// Set error state.
|
||||||
this.setState(
|
this.setState(
|
||||||
Object.assign({}, this.state, {
|
Object.assign({}, this.state, {
|
||||||
data: undefined,
|
error: error.message,
|
||||||
selection: undefined,
|
loading: false,
|
||||||
hash,
|
}),
|
||||||
error: undefined,
|
);
|
||||||
loading: true,
|
}
|
||||||
url,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (this.state.data && this.state.selection) {
|
} else if (this.state.data && this.state.selection) {
|
||||||
// Update selection if it has changed in the URL.
|
// Update selection if it has changed in the URL.
|
||||||
const selection = getSelection(
|
const selection = getSelection(
|
||||||
|
|||||||
@@ -38,28 +38,29 @@ function loadAsDataUrl(blob: Blob): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function inlineImage(image: SVGImageElement) {
|
||||||
|
const href = image.href.baseVal;
|
||||||
|
if (!href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(href);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const dataUrl = await loadAsDataUrl(blob);
|
||||||
|
image.href.baseVal = dataUrl;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load image:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all images in the SVG and replaces them with inlined images as data
|
* Fetches all images in the SVG and replaces them with inlined images as data
|
||||||
* URLs. Images are replaced in place. The replacement is done, the returned
|
* URLs. Images are replaced in place. The replacement is done, the returned
|
||||||
* promise is resolved.
|
* promise is resolved.
|
||||||
*/
|
*/
|
||||||
function inlineImages(svg: Element): Promise<void[]> {
|
async function inlineImages(svg: Element): Promise<void> {
|
||||||
const images = Array.from(svg.getElementsByTagName('image'));
|
const images = Array.from(svg.getElementsByTagName('image'));
|
||||||
const promises = images.map((image) => {
|
await Promise.all(images.map(inlineImage));
|
||||||
const href = image.href && image.href.baseVal;
|
|
||||||
if (!href) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return fetch(href)
|
|
||||||
.then((response) => response.blob())
|
|
||||||
.then(loadAsDataUrl)
|
|
||||||
.then((dataUrl) => {
|
|
||||||
image.href.baseVal = dataUrl;
|
|
||||||
})
|
|
||||||
// Log and ignore errors.
|
|
||||||
.catch((e) => console.warn('Failed to load image:', e));
|
|
||||||
});
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads a blob into an image object. */
|
/** Loads a blob into an image object. */
|
||||||
@@ -67,9 +68,7 @@ function loadImage(blob: Blob): Promise<HTMLImageElement> {
|
|||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.src = URL.createObjectURL(blob);
|
image.src = URL.createObjectURL(blob);
|
||||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
image.addEventListener('load', () => {
|
image.addEventListener('load', () => resolve(image));
|
||||||
resolve(image);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,12 +214,11 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
|
|||||||
return new XMLSerializer().serializeToString(svg);
|
return new XMLSerializer().serializeToString(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSvgContentsWithInlinedImages() {
|
private async getSvgContentsWithInlinedImages() {
|
||||||
const svg = document.getElementById('chart')!.cloneNode(true) as Element;
|
const svg = document.getElementById('chart')!.cloneNode(true) as Element;
|
||||||
svg.removeAttribute('transform');
|
svg.removeAttribute('transform');
|
||||||
return inlineImages(svg).then(() =>
|
await inlineImages(svg);
|
||||||
new XMLSerializer().serializeToString(svg),
|
return new XMLSerializer().serializeToString(svg);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shows the print dialog to print the currently displayed chart. */
|
/** Shows the print dialog to print the currently displayed chart. */
|
||||||
@@ -243,35 +241,32 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
|
|||||||
document.body.appendChild(printWindow);
|
document.body.appendChild(printWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadSvg() {
|
async downloadSvg() {
|
||||||
this.getSvgContentsWithInlinedImages().then((contents) => {
|
const contents = await this.getSvgContentsWithInlinedImages();
|
||||||
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
||||||
saveAs(blob, 'topola.svg');
|
saveAs(blob, 'topola.svg');
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
private async drawOnCanvas(): Promise<HTMLCanvasElement> {
|
||||||
drawOnCanvas(): Promise<HTMLCanvasElement> {
|
const contents = await this.getSvgContentsWithInlinedImages();
|
||||||
return this.getSvgContentsWithInlinedImages()
|
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
||||||
.then((contents) => new Blob([contents], {type: 'image/svg+xml'}))
|
return await drawOnCanvas(await loadImage(blob));
|
||||||
.then(loadImage)
|
}
|
||||||
.then(drawOnCanvas);
|
|
||||||
}
|
async downloadPng() {
|
||||||
|
const canvas = await this.drawOnCanvas();
|
||||||
downloadPng() {
|
const blob = await canvasToBlob(canvas, 'image/png');
|
||||||
this.drawOnCanvas()
|
saveAs(blob, 'topola.png');
|
||||||
.then((canvas) => canvasToBlob(canvas, 'image/png'))
|
}
|
||||||
.then((blob) => saveAs(blob, 'topola.png'));
|
|
||||||
}
|
async downloadPdf() {
|
||||||
|
const canvas = await this.drawOnCanvas();
|
||||||
downloadPdf() {
|
const doc = new jsPDF({
|
||||||
this.drawOnCanvas().then((canvas) => {
|
orientation: canvas.width > canvas.height ? 'l' : 'p',
|
||||||
const doc = new jsPDF({
|
unit: 'pt',
|
||||||
orientation: canvas.width > canvas.height ? 'l' : 'p',
|
format: [canvas.width, canvas.height],
|
||||||
unit: 'pt',
|
|
||||||
format: [canvas.width, canvas.height],
|
|
||||||
});
|
|
||||||
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
|
|
||||||
doc.save('topola.pdf');
|
|
||||||
});
|
});
|
||||||
|
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
|
||||||
|
doc.save('topola.pdf');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,37 +32,32 @@ function prepareData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */
|
/** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */
|
||||||
export function loadFromUrl(
|
export async function loadFromUrl(
|
||||||
url: string,
|
url: string,
|
||||||
handleCors: boolean,
|
handleCors: boolean,
|
||||||
): Promise<TopolaData> {
|
): Promise<TopolaData> {
|
||||||
const cachedData = sessionStorage.getItem(url);
|
const cachedData = sessionStorage.getItem(url);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
return Promise.resolve(JSON.parse(cachedData));
|
return JSON.parse(cachedData);
|
||||||
}
|
}
|
||||||
const urlToFetch = handleCors
|
const urlToFetch = handleCors
|
||||||
? 'https://cors-anywhere.herokuapp.com/' + url
|
? 'https://cors-anywhere.herokuapp.com/' + url
|
||||||
: url;
|
: url;
|
||||||
|
|
||||||
return window
|
const response = await window.fetch(urlToFetch);
|
||||||
.fetch(urlToFetch)
|
if (response.status !== 200) {
|
||||||
.then((response) => {
|
throw new Error(response.statusText);
|
||||||
if (response.status !== 200) {
|
}
|
||||||
return Promise.reject(new Error(response.statusText));
|
const gedcom = await response.text();
|
||||||
}
|
return prepareData(gedcom, url);
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then((gedcom) => {
|
|
||||||
return prepareData(gedcom, url);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads data from the given GEDCOM file contents. */
|
/** Loads data from the given GEDCOM file contents. */
|
||||||
function loadGedcomSync(
|
export async function loadGedcom(
|
||||||
hash: string,
|
hash: string,
|
||||||
gedcom?: string,
|
gedcom?: string,
|
||||||
images?: Map<string, string>,
|
images?: Map<string, string>,
|
||||||
) {
|
): Promise<TopolaData> {
|
||||||
const cachedData = sessionStorage.getItem(hash);
|
const cachedData = sessionStorage.getItem(hash);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
return JSON.parse(cachedData);
|
return JSON.parse(cachedData);
|
||||||
@@ -72,16 +67,3 @@ function loadGedcomSync(
|
|||||||
}
|
}
|
||||||
return prepareData(gedcom, hash, images);
|
return prepareData(gedcom, hash, images);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads data from the given GEDCOM file contents. */
|
|
||||||
export function loadGedcom(
|
|
||||||
hash: string,
|
|
||||||
gedcom?: string,
|
|
||||||
images?: Map<string, string>,
|
|
||||||
): Promise<TopolaData> {
|
|
||||||
try {
|
|
||||||
return Promise.resolve(loadGedcomSync(hash, gedcom, images));
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject(new Error('Failed to read GEDCOM file'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,16 +39,6 @@ function loadFileAsText(file: File): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function isImageFileName(fileName: string) {
|
||||||
const lower = fileName.toLowerCase();
|
const lower = fileName.toLowerCase();
|
||||||
return lower.endsWith('.jpg') || lower.endsWith('.png');
|
return lower.endsWith('.jpg') || lower.endsWith('.png');
|
||||||
@@ -62,12 +52,14 @@ export class TopBar extends React.Component<
|
|||||||
inputRef?: Input;
|
inputRef?: Input;
|
||||||
|
|
||||||
/** Handles the "Upload file" button. */
|
/** Handles the "Upload file" button. */
|
||||||
handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||||
const files = (event.target as HTMLInputElement).files;
|
const files = (event.target as HTMLInputElement).files;
|
||||||
if (!files || !files.length) {
|
if (!files || !files.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filesArray = Array.from(files);
|
const filesArray = Array.from(files);
|
||||||
|
(event.target as HTMLInputElement).value = ''; // Reset the file input.
|
||||||
|
|
||||||
const gedcomFile =
|
const gedcomFile =
|
||||||
files.length === 1
|
files.length === 1
|
||||||
? files[0]
|
? files[0]
|
||||||
@@ -86,20 +78,19 @@ export class TopBar extends React.Component<
|
|||||||
const imageMap = new Map(
|
const imageMap = new Map(
|
||||||
images.map((entry) => [entry.name, entry.url] as [string, string]),
|
images.map((entry) => [entry.name, entry.url] as [string, string]),
|
||||||
);
|
);
|
||||||
loadFileAsText(gedcomFile).then((data) => {
|
|
||||||
const imageFileNames = images
|
const data = await loadFileAsText(gedcomFile);
|
||||||
.map((image) => image.name)
|
const imageFileNames = images
|
||||||
.sort()
|
.map((image) => image.name)
|
||||||
.join('|');
|
.sort()
|
||||||
// Hash GEDCOM contents with uploaded image file names.
|
.join('|');
|
||||||
const hash = md5(md5(data) + imageFileNames);
|
// Hash GEDCOM contents with uploaded image file names.
|
||||||
this.props.history.push({
|
const hash = md5(md5(data) + imageFileNames);
|
||||||
pathname: '/view',
|
this.props.history.push({
|
||||||
search: queryString.stringify({file: hash}),
|
pathname: '/view',
|
||||||
state: {data, images: imageMap},
|
search: queryString.stringify({file: hash}),
|
||||||
});
|
state: {data, images: imageMap},
|
||||||
});
|
});
|
||||||
(event.target as HTMLInputElement).value = ''; // Reset the file input.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Opens the "Load from URL" dialog. */
|
/** Opens the "Load from URL" dialog. */
|
||||||
|
|||||||
Reference in New Issue
Block a user