Skip to content

Commit

Permalink
Display photos in details panel (PeWu#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
czifumasa authored May 13, 2022
1 parent d30c038 commit 4ca0025
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 9 deletions.
71 changes: 66 additions & 5 deletions src/datasource/wikitree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
JsonEvent,
JsonFam,
JsonGedcomData,
JsonImage,
JsonIndi,
} from 'topola';
import {GedcomData, normalizeGedcom, TopolaData} from '../util/gedcom_util';
Expand Down Expand Up @@ -80,6 +81,7 @@ interface Person {
BirthDate: string;
DeathDate: string;
};
Photo: string;
PhotoData?: {
path: string;
url: string;
Expand Down Expand Up @@ -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);
Expand All @@ -364,13 +368,20 @@ export async function loadWikiTree(
});

const indis: JsonIndi[] = [];

const converted = new Set<number>();
everyone.forEach((person) => {
if (converted.has(person.Id)) {
return;
}
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);
Expand Down Expand Up @@ -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};
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down
28 changes: 27 additions & 1 deletion src/details/details.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)}
Expand Down
87 changes: 87 additions & 0 deletions src/details/wrapped-image.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
}
30 changes: 30 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ body, html {
flex: 0 0 320px;
overflow: auto;
border-left: solid #ccc 1px;
overflow-x: hidden;
}

.hidden {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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%;
}
1 change: 1 addition & 0 deletions src/translations/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/util/age_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 11 additions & 2 deletions src/util/gedcom_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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;
}

0 comments on commit 4ca0025

Please sign in to comment.