Display images in details panel fromm gedzip files

This commit is contained in:
Przemek Więch
2026-05-17 22:09:10 +02:00
parent 708d26d561
commit a9cd34684c
20 changed files with 610 additions and 98 deletions

View File

@@ -10,6 +10,7 @@ import {
getImageFileEntry,
getNonImageFileEntry,
mapToSource,
resolveFileUrl,
} from '../../util/gedcom_util';
import {Config, Ids} from '../config/config';
import {AdditionalFiles, FileEntry} from './additional-files';
@@ -91,7 +92,11 @@ function attributeDetails(entry: GedcomEntry) {
}
}
function imageDetails(objectEntryReference: GedcomEntry, gedcom: GedcomData) {
function imageDetails(
objectEntryReference: GedcomEntry,
gedcom: GedcomData,
images?: Map<string, string>,
) {
const imageEntry = dereference(
objectEntryReference,
gedcom,
@@ -107,7 +112,7 @@ function imageDetails(objectEntryReference: GedcomEntry, gedcom: GedcomData) {
return (
<div className="person-image">
<WrappedImage
url={imageFileEntry.data}
url={resolveFileUrl(imageFileEntry.data, images)}
filename={getFileName(imageFileEntry) || ''}
/>
</div>
@@ -138,7 +143,11 @@ function sourceDetails(
);
}
function fileDetails(objectEntries: GedcomEntry[], gedcom: GedcomData) {
function fileDetails(
objectEntries: GedcomEntry[],
gedcom: GedcomData,
images?: Map<string, string>,
) {
const files: FileEntry[] = [];
objectEntries
.map((objectEntry) =>
@@ -148,7 +157,7 @@ function fileDetails(objectEntries: GedcomEntry[], gedcom: GedcomData) {
const fileEntry = getNonImageFileEntry(objectEntry);
if (fileEntry) {
files.push({
url: fileEntry.data,
url: resolveFileUrl(fileEntry.data, images),
filename: getFileName(fileEntry),
titl: objectEntry.tree.find((entry) => entry.tag === 'TITL')?.data,
});
@@ -249,12 +258,14 @@ function getSectionForEachMatchingEntry(
detailsFunction: (
entry: GedcomEntry,
gedcom: GedcomData,
images?: Map<string, string>,
) => React.ReactNode | null,
images?: Map<string, string>,
): React.ReactNode[] {
return flatMap(tags, (tag) =>
entries
.filter((entry) => entry.tag === tag)
.map((entry) => detailsFunction(entry, gedcom)),
.map((entry) => detailsFunction(entry, gedcom, images)),
)
.filter((element) => element !== null)
.map((element, index) => (
@@ -271,14 +282,16 @@ function combineAllMatchingEntriesIntoSingleSection(
detailsFunction: (
entries: GedcomEntry[],
gedcom: GedcomData,
images?: Map<string, string>,
) => React.ReactNode | null,
images?: Map<string, string>,
): React.ReactNode {
const entriesWithMatchingTag = flatMap(tags, (tag) =>
entries.filter((entry) => entry.tag === tag),
).filter((element) => element !== null);
const sectionWithDetails = entriesWithMatchingTag.length
? detailsFunction(entriesWithMatchingTag, gedcom)
? detailsFunction(entriesWithMatchingTag, gedcom, images)
: null;
if (!sectionWithDetails) {
@@ -334,6 +347,7 @@ interface Props {
gedcom: GedcomData;
indi: string;
config: Config;
images?: Map<string, string>;
}
export function Details(props: Props) {
@@ -353,9 +367,15 @@ export function Details(props: Props) {
props.gedcom,
['OBJE'],
imageDetails,
props.images,
)}
<ImmediateFamily gedcom={props.gedcom} indi={props.indi} />
<Events gedcom={props.gedcom} entries={entries} indi={props.indi} />
<Events
gedcom={props.gedcom}
entries={entries}
indi={props.indi}
images={props.images}
/>
{props.config.id === Ids.SHOW ? getSectionForId(props.indi) : null}
{getSectionForEachMatchingEntry(
entries,
@@ -375,6 +395,7 @@ export function Details(props: Props) {
props.gedcom,
['OBJE'],
fileDetails,
props.images,
)}
{combineAllMatchingEntriesIntoSingleSection(
entries,

View File

@@ -138,7 +138,7 @@ export function EventExtras(props: Props) {
content={
<FormattedMessage
id="extras.files"
defaultMessage="Additonal files"
defaultMessage="Additional files"
/>
}
size="mini"

View File

@@ -14,6 +14,7 @@ import {
getNonImageFileEntry,
mapToSource,
resolveDate,
resolveFileUrl,
resolveType,
Source,
} from '../../util/gedcom_util';
@@ -26,6 +27,7 @@ interface Props {
gedcom: GedcomData;
indi: string;
entries: GedcomEntry[];
images?: Map<string, string>;
}
interface EventData {
@@ -164,42 +166,55 @@ function eventPlace(entry: GedcomEntry) {
return place?.data ? getData(place) : undefined;
}
function eventImages(entry: GedcomEntry, gedcom: GedcomData): Image[] {
return entry.tree
function eventImages(
entry: GedcomEntry,
gedcom: GedcomData,
images?: Map<string, string>,
): Image[] {
const result: Image[] = [];
entry.tree
.filter((subEntry) => 'OBJE' === subEntry.tag)
.map((objectEntry) =>
dereference(objectEntry, gedcom, (gedcom) => gedcom.other),
)
.map((objectEntry) => getImageFileEntry(objectEntry))
.flatMap((imageFileEntry) =>
imageFileEntry
? [
{
url: imageFileEntry?.data || '',
filename: getFileName(imageFileEntry) || '',
},
]
: [],
);
.forEach((objectEntryReference) => {
const objectEntry = dereference(
objectEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
const fileEntry = getImageFileEntry(objectEntry);
if (fileEntry) {
result.push({
url: resolveFileUrl(fileEntry.data, images),
filename: getFileName(fileEntry) || '',
});
}
});
return result;
}
function eventFiles(entry: GedcomEntry, gedcom: GedcomData): Image[] {
return entry.tree
function eventFiles(
entry: GedcomEntry,
gedcom: GedcomData,
images?: Map<string, string>,
): FileEntry[] {
const result: FileEntry[] = [];
entry.tree
.filter((subEntry) => 'OBJE' === subEntry.tag)
.map((objectEntry) =>
dereference(objectEntry, gedcom, (gedcom) => gedcom.other),
)
.map((objectEntry) => getNonImageFileEntry(objectEntry))
.flatMap((fileEntry) =>
fileEntry
? [
{
url: fileEntry?.data || '',
filename: getFileName(fileEntry) || '',
},
]
: [],
);
.forEach((objectEntryReference) => {
const objectEntry = dereference(
objectEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
const fileEntry = getNonImageFileEntry(objectEntry);
if (fileEntry) {
result.push({
url: resolveFileUrl(fileEntry.data, images),
filename: getFileName(fileEntry),
titl: objectEntry.tree.find((entry) => entry.tag === 'TITL')?.data,
});
}
});
return result;
}
function eventSources(entry: GedcomEntry, gedcom: GedcomData): Source[] {
@@ -235,11 +250,12 @@ function toEvent(
gedcom: GedcomData,
indi: string,
intl: IntlShape,
images?: Map<string, string>,
): EventData[] {
if (entry.tag === 'FAMS') {
return toFamilyEvents(entry, gedcom, indi);
return toFamilyEvents(entry, gedcom, indi, images);
}
return toIndiEvent(entry, gedcom, indi, intl);
return toIndiEvent(entry, gedcom, indi, intl, images);
}
function toIndiEvent(
@@ -247,6 +263,7 @@ function toIndiEvent(
gedcom: GedcomData,
indi: string,
intl: IntlShape,
images?: Map<string, string>,
): EventData[] {
const date = resolveDate(entry) || null;
return [
@@ -256,8 +273,8 @@ function toIndiEvent(
type: resolveType(entry),
age: getAge(entry, indi, gedcom, intl),
place: eventPlace(entry),
images: eventImages(entry, gedcom),
files: eventFiles(entry, gedcom),
images: eventImages(entry, gedcom, images),
files: eventFiles(entry, gedcom, images),
notes: eventNotes(entry, gedcom),
sources: eventSources(entry, gedcom),
indi: indi,
@@ -269,6 +286,7 @@ function toFamilyEvents(
entry: GedcomEntry,
gedcom: GedcomData,
indi: string,
images?: Map<string, string>,
): EventData[] {
const family = dereference(entry, gedcom, (gedcom) => gedcom.fams);
return flatMap(FAMILY_EVENT_TAGS, (tag) =>
@@ -281,8 +299,8 @@ function toFamilyEvents(
type: resolveType(familyEvent),
personLink: getSpouse(indi, family, gedcom),
place: eventPlace(familyEvent),
images: eventImages(familyEvent, gedcom),
files: eventFiles(familyEvent, gedcom),
images: eventImages(familyEvent, gedcom, images),
files: eventFiles(familyEvent, gedcom, images),
notes: eventNotes(familyEvent, gedcom),
sources: eventSources(familyEvent, gedcom),
indi: indi,
@@ -320,7 +338,9 @@ export function Events(props: Props) {
const events = flatMap(SORTED_EVENT_TYPE_GROUPS, (eventTypeGroup) =>
props.entries
.filter((entry) => eventTypeGroup.includes(entry.tag))
.map((eventEntry) => toEvent(eventEntry, props.gedcom, props.indi, intl))
.map((eventEntry) =>
toEvent(eventEntry, props.gedcom, props.indi, intl, props.images),
)
.flatMap((events) => events)
.sort((event1, event2) => compareDates(event1.date, event2.date)),
);

View File

@@ -1,6 +1,7 @@
import {SyntheticEvent, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {
Card,
Container,
Icon,
Image,
@@ -9,6 +10,7 @@ import {
Modal,
Placeholder,
} from 'semantic-ui-react';
import {isBrowserLoadable} from '../../util/gedcom_util';
interface Props {
url: string;
@@ -22,6 +24,33 @@ export function WrappedImage(props: Props) {
const [imageFailed, setImageFailed] = useState(false);
const [imageSrc, setImageSrc] = useState('');
if (!isBrowserLoadable(props.url)) {
return (
<Card
centered
style={{width: '100%', maxWidth: '290px', margin: '0 auto 14px'}}
>
<Card.Content textAlign="center" style={{backgroundColor: '#f9f9f9'}}>
<Icon
name="image"
size="huge"
color="grey"
style={{marginBottom: '10px', opacity: 0.6}}
/>
<Card.Header style={{fontSize: '14px', wordBreak: 'break-all'}}>
{props.title || props.filename}
</Card.Header>
<Card.Meta style={{marginTop: '5px', fontStyle: 'italic'}}>
<FormattedMessage
id="media.not_uploaded"
defaultMessage="File not uploaded"
/>
</Card.Meta>
</Card.Content>
</Card>
);
}
if (imageLoaded && imageSrc !== props.url) {
setImageLoaded(false);
}

View File

@@ -31,7 +31,12 @@ export function SidePanel({
defaultMessage: 'Info',
}),
render: () => (
<Details gedcom={data.gedcom} indi={selectedIndiId} config={config} />
<Details
gedcom={data.gedcom}
indi={selectedIndiId}
config={config}
images={data.images}
/>
),
},
{