Display photos in details panel (#100)

This commit is contained in:
czifumasa 2022-05-13 22:15:40 +02:00 committed by GitHub
parent d30c038406
commit 4ca0025438
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 224 additions and 9 deletions

View File

@ -7,6 +7,7 @@ import {
JsonEvent,
JsonFam,
JsonGedcomData,
JsonImage,
JsonIndi,
} from 'topola';
import {GedcomData, normalizeGedcom, TopolaData} from '../util/gedcom_util';
@ -80,6 +81,7 @@ interface Person {
BirthDate: string;
DeathDate: string;
};
Photo: string;
PhotoData?: {
path: string;
url: string;
@ -348,6 +350,8 @@ export async function loadWikiTree(
>();
// Map from numerical id to human-readable id.
const idToName = new Map<number, string>();
// Map from human-readable person id to fullSizeUrl of person photo.
const fullSizePhotoUrls: Map<string, string> = new Map();
everyone.forEach((person) => {
idToName.set(person.Id, person.Name);
@ -364,6 +368,7 @@ export async function loadWikiTree(
});
const indis: JsonIndi[] = [];
const converted = new Set<number>();
everyone.forEach((person) => {
if (converted.has(person.Id)) {
@ -371,6 +376,12 @@ export async function loadWikiTree(
}
converted.add(person.Id);
const indi = convertPerson(person, intl);
if (person.PhotoData?.path) {
fullSizePhotoUrls.set(
person.Name,
`https://www.wikitree.com${person.PhotoData.path}`,
);
}
if (person.Spouses) {
Object.values(person.Spouses).forEach((spouse) => {
const famId = getFamilyId(person.Id, spouse.Id);
@ -417,7 +428,7 @@ export async function loadWikiTree(
});
const chartData = normalizeGedcom({indis, fams});
const gedcom = buildGedcom(chartData);
const gedcom = buildGedcom(chartData, fullSizePhotoUrls);
return {chartData, gedcom};
}
@ -481,7 +492,12 @@ function convertPerson(person: Person, intl: IntlShape): JsonIndi {
indi.death = Object.assign({}, date, {place: person.DeathLocation});
}
if (person.PhotoData) {
indi.images = [{url: `https://www.wikitree.com${person.PhotoData.url}`}];
indi.images = [
{
url: `https://www.wikitree.com${person.PhotoData.url}`,
title: person.Photo,
},
];
}
return indi;
}
@ -589,7 +605,40 @@ function eventToGedcom(event: JsonEvent): GedcomEntry[] {
return result;
}
function indiToGedcom(indi: JsonIndi): GedcomEntry {
function imageToGedcom(
image: JsonImage,
fullSizePhotoUrl: string | undefined,
): GedcomEntry[] {
return [
{
level: 2,
pointer: '',
tag: 'FILE',
data: fullSizePhotoUrl || image.url,
tree: [
{
level: 3,
pointer: '',
tag: 'FORM',
data: image.title?.split('.').pop() || '',
tree: [],
},
{
level: 3,
pointer: '',
tag: 'TITL',
data: image.title?.split('.')[0] || '',
tree: [],
},
],
},
];
}
function indiToGedcom(
indi: JsonIndi,
fullSizePhotoUrl: Map<string, string>,
): GedcomEntry {
// WikiTree URLs replace spaces with underscores.
const escapedId = indi.id.replace(/ /g, '_');
const record: GedcomEntry = {
@ -652,6 +701,15 @@ function indiToGedcom(indi: JsonIndi): GedcomEntry {
tree: [],
});
}
(indi.images || []).forEach((image) => {
record.tree.push({
level: 1,
pointer: '',
tag: 'OBJE',
data: '',
tree: imageToGedcom(image, fullSizePhotoUrl.get(indi.id)),
});
});
return record;
}
@ -706,11 +764,14 @@ function famToGedcom(fam: JsonFam): GedcomEntry {
* Creates a GEDCOM structure for the purpose of displaying the details
* panel.
*/
function buildGedcom(data: JsonGedcomData): GedcomData {
function buildGedcom(
data: JsonGedcomData,
fullSizePhotoUrls: Map<string, string>,
): GedcomData {
const gedcomIndis: {[key: string]: GedcomEntry} = {};
const gedcomFams: {[key: string]: GedcomEntry} = {};
data.indis.forEach((indi) => {
gedcomIndis[indi.id] = indiToGedcom(indi);
gedcomIndis[indi.id] = indiToGedcom(indi, fullSizePhotoUrls);
});
data.fams.forEach((fam) => {
gedcomFams[fam.id] = famToGedcom(fam);

View File

@ -1,10 +1,17 @@
import flatMap from 'array.prototype.flatmap';
import {dereference, GedcomData, getData} from '../util/gedcom_util';
import {
dereference,
GedcomData,
getData,
getFileName,
isImageFile,
} from '../util/gedcom_util';
import {Events} from './events';
import {GedcomEntry} from 'parse-gedcom';
import {MultilineText} from './multiline-text';
import {TranslatedTag} from './translated-tag';
import {Header, Item} from 'semantic-ui-react';
import {WrappedImage} from './wrapped-image';
const EXCLUDED_TAGS = [
'BIRT',
@ -47,6 +54,24 @@ function dataDetails(entry: GedcomEntry) {
);
}
function fileDetails(objectEntry: GedcomEntry) {
const imageFileEntry = objectEntry.tree.find(
(entry) =>
entry.tag === 'FILE' &&
entry.data.startsWith('http') &&
isImageFile(entry.data),
);
return imageFileEntry ? (
<div className="person-image">
<WrappedImage
url={imageFileEntry.data}
filename={getFileName(imageFileEntry) || ''}
/>
</div>
) : null;
}
function noteDetails(entry: GedcomEntry) {
return (
<MultilineText
@ -128,6 +153,7 @@ export function Details(props: Props) {
<div className="details">
<Item.Group divided>
{getDetails(entries, ['NAME'], nameDetails)}
{getDetails(entriesWithData, ['OBJE'], fileDetails)}
<Events gedcom={props.gedcom} entries={entries} indi={props.indi} />
{getOtherDetails(entriesWithData)}
{getDetails(entriesWithData, ['NOTE'], noteDetails)}

View File

@ -0,0 +1,87 @@
import {
Container,
Icon,
Image,
Label,
Message,
Modal,
Placeholder,
} from 'semantic-ui-react';
import {SyntheticEvent, useState} from 'react';
import {FormattedMessage} from 'react-intl';
interface Props {
url: string;
filename: string;
title?: string;
}
export function WrappedImage(props: Props) {
const [imageOpen, setImageOpen] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageFailed, setImageFailed] = useState(false);
const [imageSrc, setImageSrc] = useState('');
if (imageLoaded && imageSrc !== props.url) {
setImageLoaded(false);
}
return (
<>
<Image
className={imageLoaded ? 'loaded-image-thumbnail' : 'hidden-image'}
onClick={() => setImageOpen(true)}
onLoad={() => {
setImageLoaded(true);
setImageSrc(props.url);
setImageFailed(false);
}}
onError={(e: SyntheticEvent<HTMLImageElement, Event>) => {
setImageLoaded(true);
setImageSrc(props.url);
setImageFailed(true);
e.currentTarget.alt = '';
}}
src={props.url}
alt={props.title || props.filename}
centered={true}
/>
<Placeholder
className={!imageLoaded ? 'image-placeholder' : 'hidden-image'}
>
<Placeholder.Image square />
</Placeholder>
{imageFailed && (
<Container fluid textAlign="center">
<Message negative compact>
<Message.Header>
<FormattedMessage
id="error.failed_to_load_image"
defaultMessage={'Failed to load image file'}
/>
</Message.Header>
</Message>
</Container>
)}
<Modal
basic
size="large"
closeIcon={<Icon name="close" color="red" />}
open={imageOpen}
onClose={() => setImageOpen(false)}
onOpen={() => setImageOpen(true)}
centered={false}
>
<Modal.Header>{props.title}</Modal.Header>
<Modal.Content image>
<Image
className="modal-image"
src={props.url}
alt={props.title || props.filename}
label={<Label attached="bottom" content={props.filename} />}
wrapped
/>
</Modal.Content>
</Modal>
</>
);
}

View File

@ -28,6 +28,7 @@ body, html {
flex: 0 0 320px;
overflow: auto;
border-left: solid #ccc 1px;
overflow-x: hidden;
}
.hidden {
@ -164,6 +165,12 @@ div.zoom {
min-width: 40%;
}
.details .person-image {
max-width: 289px;
width: 289px;
padding: 0 10px;
}
.ui.form .field.no-margin {
margin: 0;
}
@ -176,3 +183,26 @@ div.zoom {
height: 300px;
overflow-y: scroll;
}
.loaded-image-thumbnail {
cursor: zoom-in;
}
.hidden-image {
display: none !important;
}
.modal-image {
display: block;
margin-left: auto;
margin-right: auto;
}
.modal-image .ui.attached.label {
width: auto;
min-width: 100%;
}
.image-placeholder {
height: 100%;
}

View File

@ -78,6 +78,7 @@
"error.WIKITREE_ID_NOT_PROVIDED": "Identyfikator WikiTree nie został podany",
"error.WIKITREE_PROFILE_NOT_ACCESSIBLE": "Profil WikiTree {id} nie jest dostępny",
"error.WIKITREE_PROFILE_NOT_FOUND": "Profil WikiTree {id} nie istnieje",
"error.failed_to_load_image": "Błąd podczas pobierania pliku ze zdjęciem",
"wikitree.private": "Prywatne",
"tab.info": "Info",
"tab.settings": "Ustawienia",

View File

@ -118,7 +118,8 @@ function calcDateDifferenceInYears(
const startYear = firstDateObject.getUTCFullYear();
let yearDiff = secondDateObject.getUTCFullYear() - startYear;
let monthDiff = secondDateObject.getUTCMonth() - firstDateObject.getUTCMonth();
let monthDiff =
secondDateObject.getUTCMonth() - firstDateObject.getUTCMonth();
if (monthDiff < 0) {
yearDiff--;
monthDiff += 12;

View File

@ -192,10 +192,10 @@ export function normalizeGedcom(gedcom: JsonGedcomData): JsonGedcomData {
return sortSpouses(sortChildren(gedcom));
}
const IMAGE_EXTENSIONS = ['.jpg', '.png', '.gif'];
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'];
/** Returns true if the given file name has a known image extension. */
function isImageFile(fileName: string): boolean {
export function isImageFile(fileName: string): boolean {
const lowerName = fileName.toLowerCase();
return IMAGE_EXTENSIONS.some((ext) => lowerName.endsWith(ext));
}
@ -282,3 +282,12 @@ export function getName(person: GedcomEntry): string | undefined {
const name = notMarriedName || names[0];
return name?.data.replace(/\//g, '');
}
export function getFileName(fileEntry: GedcomEntry): string | undefined {
const fileTitle = fileEntry?.tree.find((entry) => entry.tag === 'TITL')?.data;
const fileExtension = fileEntry?.tree.find((entry) => entry.tag === 'FORM')
?.data;
return fileTitle && fileExtension && fileTitle + '.' + fileExtension;
}