mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-03-10 09:33:47 +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();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
async componentDidUpdate() {
|
||||
if (this.props.location.pathname !== '/view') {
|
||||
return;
|
||||
}
|
||||
@@ -77,45 +77,42 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
||||
if (!url && !hash) {
|
||||
this.props.history.replace({pathname: '/'});
|
||||
} else if (this.isNewData(hash, url)) {
|
||||
const loadedData = hash
|
||||
? loadGedcom(hash, gedcom, images)
|
||||
: loadFromUrl(url!, handleCors);
|
||||
loadedData.then(
|
||||
(data) => {
|
||||
// Set state with data.
|
||||
this.setState(
|
||||
Object.assign({}, this.state, {
|
||||
data,
|
||||
hash,
|
||||
selection: getSelection(data.chartData, indi, generation),
|
||||
error: undefined,
|
||||
loading: false,
|
||||
url,
|
||||
showSidePanel,
|
||||
}),
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
// Set error state.
|
||||
this.setState(
|
||||
Object.assign({}, this.state, {
|
||||
error: error.message,
|
||||
loading: false,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
// Set loading state.
|
||||
this.setState(
|
||||
Object.assign({}, this.state, {
|
||||
data: undefined,
|
||||
selection: undefined,
|
||||
hash,
|
||||
error: undefined,
|
||||
loading: true,
|
||||
url,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
// Set loading state.
|
||||
this.setState(
|
||||
Object.assign({}, this.state, {
|
||||
data: undefined,
|
||||
selection: undefined,
|
||||
hash,
|
||||
error: undefined,
|
||||
loading: true,
|
||||
url,
|
||||
}),
|
||||
);
|
||||
const data = hash
|
||||
? await loadGedcom(hash, gedcom, images)
|
||||
: await loadFromUrl(url!, handleCors);
|
||||
// Set state with data.
|
||||
this.setState(
|
||||
Object.assign({}, this.state, {
|
||||
data,
|
||||
hash,
|
||||
selection: getSelection(data.chartData, indi, generation),
|
||||
error: undefined,
|
||||
loading: false,
|
||||
url,
|
||||
showSidePanel,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
// Set error state.
|
||||
this.setState(
|
||||
Object.assign({}, this.state, {
|
||||
error: error.message,
|
||||
loading: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (this.state.data && this.state.selection) {
|
||||
// Update selection if it has changed in the URL.
|
||||
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
|
||||
* URLs. Images are replaced in place. The replacement is done, the returned
|
||||
* promise is resolved.
|
||||
*/
|
||||
function inlineImages(svg: Element): Promise<void[]> {
|
||||
async function inlineImages(svg: Element): Promise<void> {
|
||||
const images = Array.from(svg.getElementsByTagName('image'));
|
||||
const promises = images.map((image) => {
|
||||
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);
|
||||
await Promise.all(images.map(inlineImage));
|
||||
}
|
||||
|
||||
/** Loads a blob into an image object. */
|
||||
@@ -67,9 +68,7 @@ function loadImage(blob: Blob): Promise<HTMLImageElement> {
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(blob);
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
image.addEventListener('load', () => {
|
||||
resolve(image);
|
||||
});
|
||||
image.addEventListener('load', () => resolve(image));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,12 +214,11 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
|
||||
return new XMLSerializer().serializeToString(svg);
|
||||
}
|
||||
|
||||
private getSvgContentsWithInlinedImages() {
|
||||
private async getSvgContentsWithInlinedImages() {
|
||||
const svg = document.getElementById('chart')!.cloneNode(true) as Element;
|
||||
svg.removeAttribute('transform');
|
||||
return inlineImages(svg).then(() =>
|
||||
new XMLSerializer().serializeToString(svg),
|
||||
);
|
||||
await inlineImages(svg);
|
||||
return new XMLSerializer().serializeToString(svg);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
downloadSvg() {
|
||||
this.getSvgContentsWithInlinedImages().then((contents) => {
|
||||
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
||||
saveAs(blob, 'topola.svg');
|
||||
});
|
||||
}
|
||||
|
||||
drawOnCanvas(): Promise<HTMLCanvasElement> {
|
||||
return this.getSvgContentsWithInlinedImages()
|
||||
.then((contents) => new Blob([contents], {type: 'image/svg+xml'}))
|
||||
.then(loadImage)
|
||||
.then(drawOnCanvas);
|
||||
}
|
||||
|
||||
downloadPng() {
|
||||
this.drawOnCanvas()
|
||||
.then((canvas) => canvasToBlob(canvas, 'image/png'))
|
||||
.then((blob) => saveAs(blob, 'topola.png'));
|
||||
}
|
||||
|
||||
downloadPdf() {
|
||||
this.drawOnCanvas().then((canvas) => {
|
||||
const doc = new jsPDF({
|
||||
orientation: canvas.width > canvas.height ? 'l' : 'p',
|
||||
unit: 'pt',
|
||||
format: [canvas.width, canvas.height],
|
||||
});
|
||||
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
|
||||
doc.save('topola.pdf');
|
||||
async downloadSvg() {
|
||||
const contents = await this.getSvgContentsWithInlinedImages();
|
||||
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
||||
saveAs(blob, 'topola.svg');
|
||||
}
|
||||
|
||||
private async drawOnCanvas(): Promise<HTMLCanvasElement> {
|
||||
const contents = await this.getSvgContentsWithInlinedImages();
|
||||
const blob = new Blob([contents], {type: 'image/svg+xml'});
|
||||
return await drawOnCanvas(await loadImage(blob));
|
||||
}
|
||||
|
||||
async downloadPng() {
|
||||
const canvas = await this.drawOnCanvas();
|
||||
const blob = await canvasToBlob(canvas, 'image/png');
|
||||
saveAs(blob, 'topola.png');
|
||||
}
|
||||
|
||||
async downloadPdf() {
|
||||
const canvas = await this.drawOnCanvas();
|
||||
const doc = new jsPDF({
|
||||
orientation: canvas.width > canvas.height ? 'l' : 'p',
|
||||
unit: 'pt',
|
||||
format: [canvas.width, canvas.height],
|
||||
});
|
||||
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. */
|
||||
export function loadFromUrl(
|
||||
export async function loadFromUrl(
|
||||
url: string,
|
||||
handleCors: boolean,
|
||||
): Promise<TopolaData> {
|
||||
const cachedData = sessionStorage.getItem(url);
|
||||
if (cachedData) {
|
||||
return Promise.resolve(JSON.parse(cachedData));
|
||||
return JSON.parse(cachedData);
|
||||
}
|
||||
const urlToFetch = handleCors
|
||||
? 'https://cors-anywhere.herokuapp.com/' + url
|
||||
: url;
|
||||
|
||||
return window
|
||||
.fetch(urlToFetch)
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((gedcom) => {
|
||||
return prepareData(gedcom, url);
|
||||
});
|
||||
const response = await window.fetch(urlToFetch);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const gedcom = await response.text();
|
||||
return prepareData(gedcom, url);
|
||||
}
|
||||
|
||||
/** Loads data from the given GEDCOM file contents. */
|
||||
function loadGedcomSync(
|
||||
export async function loadGedcom(
|
||||
hash: string,
|
||||
gedcom?: string,
|
||||
images?: Map<string, string>,
|
||||
) {
|
||||
): Promise<TopolaData> {
|
||||
const cachedData = sessionStorage.getItem(hash);
|
||||
if (cachedData) {
|
||||
return JSON.parse(cachedData);
|
||||
@@ -72,16 +67,3 @@ function loadGedcomSync(
|
||||
}
|
||||
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) {
|
||||
const lower = fileName.toLowerCase();
|
||||
return lower.endsWith('.jpg') || lower.endsWith('.png');
|
||||
@@ -62,12 +52,14 @@ export class TopBar extends React.Component<
|
||||
inputRef?: Input;
|
||||
|
||||
/** Handles the "Upload file" button. */
|
||||
handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
if (!files || !files.length) {
|
||||
return;
|
||||
}
|
||||
const filesArray = Array.from(files);
|
||||
(event.target as HTMLInputElement).value = ''; // Reset the file input.
|
||||
|
||||
const gedcomFile =
|
||||
files.length === 1
|
||||
? files[0]
|
||||
@@ -86,20 +78,19 @@ export class TopBar extends React.Component<
|
||||
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, images: imageMap},
|
||||
});
|
||||
|
||||
const data = await loadFileAsText(gedcomFile);
|
||||
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, images: imageMap},
|
||||
});
|
||||
(event.target as HTMLInputElement).value = ''; // Reset the file input.
|
||||
}
|
||||
|
||||
/** Opens the "Load from URL" dialog. */
|
||||
|
||||
Reference in New Issue
Block a user