mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-04-19 04:56:14 +00:00
Add support for loading gedzip files
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2023-08-25
|
||||||
|
|
||||||
|
- Added support for loading gedzip files
|
||||||
|
|
||||||
## 2023-07-21
|
## 2023-07-21
|
||||||
|
|
||||||
- Display images, notes, sources for events as collapsible tabs (by czifumasa)
|
- Display images, notes, sources for events as collapsible tabs (by czifumasa)
|
||||||
|
|||||||
33
package-lock.json
generated
33
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@artsy/fresnel": "^1.3.1",
|
"@artsy/fresnel": "^1.3.1",
|
||||||
|
"adm-zip": "^0.5.10",
|
||||||
"array.prototype.flatmap": "^1.2.4",
|
"array.prototype.flatmap": "^1.2.4",
|
||||||
"canvas-toBlob": "^1.0.0",
|
"canvas-toBlob": "^1.0.0",
|
||||||
"d3-array": "^2.12.1",
|
"d3-array": "^2.12.1",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"wikitree-js": "^0.4.0"
|
"wikitree-js": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/adm-zip": "^0.5.0",
|
||||||
"@types/array.prototype.flatmap": "^1.2.2",
|
"@types/array.prototype.flatmap": "^1.2.2",
|
||||||
"@types/d3-array": "^2.9.0",
|
"@types/d3-array": "^2.9.0",
|
||||||
"@types/d3-interpolate": "^2.0.0",
|
"@types/d3-interpolate": "^2.0.0",
|
||||||
@@ -3084,6 +3086,15 @@
|
|||||||
"url": "https://github.com/sponsors/gregberge"
|
"url": "https://github.com/sponsors/gregberge"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/adm-zip": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/anymatch": {
|
"node_modules/@types/anymatch": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
||||||
@@ -3989,6 +4000,14 @@
|
|||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adm-zip": {
|
||||||
|
"version": "0.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
|
||||||
|
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aggregate-error": {
|
"node_modules/aggregate-error": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
@@ -28236,6 +28255,15 @@
|
|||||||
"loader-utils": "^2.0.0"
|
"loader-utils": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/adm-zip": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/anymatch": {
|
"@types/anymatch": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
||||||
@@ -29044,6 +29072,11 @@
|
|||||||
"regex-parser": "^2.2.11"
|
"regex-parser": "^2.2.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"adm-zip": {
|
||||||
|
"version": "0.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
|
||||||
|
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ=="
|
||||||
|
},
|
||||||
"aggregate-error": {
|
"aggregate-error": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"main": "src/index.tsx",
|
"main": "src/index.tsx",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@artsy/fresnel": "^1.3.1",
|
"@artsy/fresnel": "^1.3.1",
|
||||||
|
"adm-zip": "^0.5.10",
|
||||||
"array.prototype.flatmap": "^1.2.4",
|
"array.prototype.flatmap": "^1.2.4",
|
||||||
"canvas-toBlob": "^1.0.0",
|
"canvas-toBlob": "^1.0.0",
|
||||||
"d3-array": "^2.12.1",
|
"d3-array": "^2.12.1",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"wikitree-js": "^0.4.0"
|
"wikitree-js": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/adm-zip": "^0.5.0",
|
||||||
"@types/array.prototype.flatmap": "^1.2.2",
|
"@types/array.prototype.flatmap": "^1.2.2",
|
||||||
"@types/d3-array": "^2.9.0",
|
"@types/d3-array": "^2.9.0",
|
||||||
"@types/d3-interpolate": "^2.0.0",
|
"@types/d3-interpolate": "^2.0.0",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {convertGedcom, getSoftware, TopolaData} from '../util/gedcom_util';
|
|||||||
import {DataSource, DataSourceEnum, SourceSelection} from './data_source';
|
import {DataSource, DataSourceEnum, SourceSelection} from './data_source';
|
||||||
import {IndiInfo, JsonGedcomData} from 'topola';
|
import {IndiInfo, JsonGedcomData} from 'topola';
|
||||||
import {TopolaError} from '../util/error';
|
import {TopolaError} from '../util/error';
|
||||||
|
import AdmZip from 'adm-zip';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a valid IndiInfo object, either with the given indi and generation
|
* Returns a valid IndiInfo object, either with the given indi and generation
|
||||||
@@ -36,6 +37,45 @@ function prepareData(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadGedzip(
|
||||||
|
blob: Blob,
|
||||||
|
): Promise<{gedcom: string; images: Map<string, string>}> {
|
||||||
|
const zip = new AdmZip(Buffer.from(await blob.arrayBuffer()));
|
||||||
|
const entries = zip.getEntries();
|
||||||
|
|
||||||
|
let gedcom = undefined;
|
||||||
|
const images = new Map<string, string>();
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.entryName.endsWith('.ged')) {
|
||||||
|
if (gedcom) {
|
||||||
|
console.warn('Multiple GEDCOM files found in zip archive.');
|
||||||
|
} else {
|
||||||
|
gedcom = entry.getData().toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Save image for later.
|
||||||
|
images.set(
|
||||||
|
entry.entryName,
|
||||||
|
URL.createObjectURL(new Blob([entry.getData()])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!gedcom) {
|
||||||
|
throw new Error('GEDCOM file not found in zip archive.');
|
||||||
|
}
|
||||||
|
return {gedcom, images};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadFile(
|
||||||
|
blob: Blob,
|
||||||
|
): Promise<{gedcom: string; images: Map<string, string>}> {
|
||||||
|
const fileHeader = await blob.slice(0, 2).text();
|
||||||
|
if (fileHeader === 'PK') {
|
||||||
|
return loadGedzip(blob);
|
||||||
|
}
|
||||||
|
return {gedcom: await blob.text(), images: new Map()};
|
||||||
|
}
|
||||||
|
|
||||||
/** 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 async function loadFromUrl(
|
export async function loadFromUrl(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -63,16 +103,15 @@ export async function loadFromUrl(
|
|||||||
url = `https://drive.google.com/uc?id=${driveUrlMatch2[1]}&export=download`;
|
url = `https://drive.google.com/uc?id=${driveUrlMatch2[1]}&export=download`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlToFetch = handleCors
|
const urlToFetch = handleCors ? 'https://topolaproxy.bieda.it/' + url : url;
|
||||||
? 'https://topolaproxy.bieda.it/' + url
|
|
||||||
: url;
|
|
||||||
|
|
||||||
const response = await window.fetch(urlToFetch);
|
const response = await window.fetch(urlToFetch);
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
const gedcom = await response.text();
|
|
||||||
return prepareData(gedcom, url);
|
const {gedcom, images} = await loadFile(await response.blob());
|
||||||
|
return prepareData(gedcom, url, images);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads data from the given GEDCOM file contents. */
|
/** Loads data from the given GEDCOM file contents. */
|
||||||
|
|||||||
@@ -6,16 +6,7 @@ import {FormattedMessage} from 'react-intl';
|
|||||||
import {MenuType} from './menu_item';
|
import {MenuType} from './menu_item';
|
||||||
import {SyntheticEvent} from 'react';
|
import {SyntheticEvent} from 'react';
|
||||||
import {useHistory, useLocation} from 'react-router';
|
import {useHistory, useLocation} from 'react-router';
|
||||||
|
import {loadFile} from '../datasource/load_data';
|
||||||
function loadFileAsText(file: File): Promise<string> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (evt: ProgressEvent) => {
|
|
||||||
resolve((evt.target as FileReader).result as string);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isImageFileName(fileName: string) {
|
function isImageFileName(fileName: string) {
|
||||||
const lower = fileName.toLowerCase();
|
const lower = fileName.toLowerCase();
|
||||||
@@ -47,27 +38,18 @@ export function UploadMenu(props: Props) {
|
|||||||
? filesArray[0]
|
? filesArray[0]
|
||||||
: filesArray.find((file) => file.name.toLowerCase().endsWith('.ged')) ||
|
: filesArray.find((file) => file.name.toLowerCase().endsWith('.ged')) ||
|
||||||
filesArray[0];
|
filesArray[0];
|
||||||
|
const {gedcom, images} = await loadFile(gedcomFile);
|
||||||
|
|
||||||
// Convert uploaded images to object URLs.
|
// Convert uploaded images to object URLs.
|
||||||
const images = filesArray
|
filesArray
|
||||||
.filter(
|
.filter(
|
||||||
(file) => file.name !== gedcomFile.name && isImageFileName(file.name),
|
(file) => file.name !== gedcomFile.name && isImageFileName(file.name),
|
||||||
)
|
)
|
||||||
.map((file) => ({
|
.forEach((file) => images.set(file.name, URL.createObjectURL(file)));
|
||||||
name: file.name,
|
|
||||||
url: URL.createObjectURL(file),
|
|
||||||
}));
|
|
||||||
const imageMap = new Map(
|
|
||||||
images.map((entry) => [entry.name, entry.url] as [string, string]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await loadFileAsText(gedcomFile);
|
|
||||||
const imageFileNames = images
|
|
||||||
.map((image) => image.name)
|
|
||||||
.sort()
|
|
||||||
.join('|');
|
|
||||||
// Hash GEDCOM contents with uploaded image file names.
|
// Hash GEDCOM contents with uploaded image file names.
|
||||||
const hash = md5(md5(data) + imageFileNames);
|
const imageFileNames = Array.from(images.keys()).sort().join('|');
|
||||||
|
const hash = md5(md5(gedcom) + imageFileNames);
|
||||||
|
|
||||||
// Use history.replace() when reuploading the same file and history.push() when loading
|
// Use history.replace() when reuploading the same file and history.push() when loading
|
||||||
// a new file.
|
// a new file.
|
||||||
@@ -77,7 +59,7 @@ export function UploadMenu(props: Props) {
|
|||||||
historyPush({
|
historyPush({
|
||||||
pathname: '/view',
|
pathname: '/view',
|
||||||
search: queryString.stringify({file: hash}),
|
search: queryString.stringify({file: hash}),
|
||||||
state: {data, images: imageMap},
|
state: {data: gedcom, images},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +83,7 @@ export function UploadMenu(props: Props) {
|
|||||||
<input
|
<input
|
||||||
className="hidden"
|
className="hidden"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".ged,image/*"
|
accept=".ged,.gdz,.gedzip,.zip,image/*"
|
||||||
id="fileInput"
|
id="fileInput"
|
||||||
multiple
|
multiple
|
||||||
onChange={handleUpload}
|
onChange={handleUpload}
|
||||||
|
|||||||
@@ -210,9 +210,12 @@ function filterImage(indi: JsonIndi, images: Map<string, string>): JsonIndi {
|
|||||||
}
|
}
|
||||||
const newImages: JsonImage[] = [];
|
const newImages: JsonImage[] = [];
|
||||||
indi.images.forEach((image) => {
|
indi.images.forEach((image) => {
|
||||||
const fileName = image.url.match(/[^/\\]*$/)![0];
|
const filePath = image.url.replaceAll('\\', '/');
|
||||||
|
const fileName = filePath.match(/[^/]*$/)![0];
|
||||||
// If the image file has been loaded into memory, use it.
|
// If the image file has been loaded into memory, use it.
|
||||||
if (images.has(fileName)) {
|
if (images.has(filePath)) {
|
||||||
|
newImages.push({url: images.get(filePath)!, title: image.title});
|
||||||
|
} else if (images.has(fileName)) {
|
||||||
newImages.push({url: images.get(fileName)!, title: image.title});
|
newImages.push({url: images.get(fileName)!, title: image.title});
|
||||||
} else if (image.url.startsWith('http') && isImageFile(image.url)) {
|
} else if (image.url.startsWith('http') && isImageFile(image.url)) {
|
||||||
newImages.push(image);
|
newImages.push(image);
|
||||||
|
|||||||
Reference in New Issue
Block a user