Display sources and links to non-image files in details panel (#210)

This commit is contained in:
czifumasa 2025-08-03 23:59:44 +02:00 committed by GitHub
parent 91a8bd0115
commit 3438c29ab8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 377 additions and 118 deletions

View File

@ -0,0 +1,29 @@
import {List} from 'semantic-ui-react';
export interface FileEntry {
url: string;
filename?: string;
}
interface Props {
files?: FileEntry[];
}
export function AdditionalFiles({files}: Props) {
if (!files?.length) return null;
return (
<List>
{files.map((file, index) => (
<List.Item key={index}>
<List.Icon verticalAlign="middle" name="circle" size="tiny" />
<List.Content>
<a target="_blank" href={file.url} rel="noopener noreferrer">
{file.filename || file.url.split('/').pop() || file.url}
</a>
</List.Content>
</List.Item>
))}
</List>
);
}

View File

@ -8,9 +8,13 @@ import {
getData, getData,
getFileName, getFileName,
getImageFileEntry, getImageFileEntry,
getNonImageFileEntry,
mapToSource,
} from '../util/gedcom_util'; } from '../util/gedcom_util';
import {AdditionalFiles} from './additional-files';
import {Events} from './events'; import {Events} from './events';
import {MultilineText} from './multiline-text'; import {MultilineText} from './multiline-text';
import {Sources} from './sources';
import {TranslatedTag} from './translated-tag'; import {TranslatedTag} from './translated-tag';
import {WrappedImage} from './wrapped-image'; import {WrappedImage} from './wrapped-image';
@ -55,23 +59,95 @@ function dataDetails(entry: GedcomEntry) {
); );
} }
function fileDetails(objectEntry: GedcomEntry) { function imageDetails(objectEntryReference: GedcomEntry, gedcom: GedcomData) {
const imageFileEntry = getImageFileEntry(objectEntry); const imageEntry = dereference(
objectEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
return imageFileEntry ? ( const imageFileEntry = getImageFileEntry(imageEntry);
if (!imageFileEntry || !hasData(imageEntry)) {
return null;
}
return (
<div className="person-image"> <div className="person-image">
<WrappedImage <WrappedImage
url={imageFileEntry.data} url={imageFileEntry.data}
filename={getFileName(imageFileEntry) || ''} filename={getFileName(imageFileEntry) || ''}
/> />
</div> </div>
) : null; );
} }
function noteDetails(entry: GedcomEntry) { function sourceDetails(
sourceReferenceEntries: GedcomEntry[],
gedcom: GedcomData,
) {
const sources = sourceReferenceEntries.map((sourceEntryReference) =>
mapToSource(sourceEntryReference, gedcom),
);
if (!sources.length) {
return null;
}
return (
<>
<div className="item-header">
<Header as="span" size="small">
<TranslatedTag tag="SOUR" />
</Header>
</div>
<Sources sources={sources} />
</>
);
}
function fileDetails(objectEntries: GedcomEntry[], gedcom: GedcomData) {
const files = objectEntries
.map((objectEntry) =>
dereference(objectEntry, gedcom, (gedcom) => gedcom.other),
)
.map((objectEntry) => getNonImageFileEntry(objectEntry))
.filter((objectEntry): objectEntry is GedcomEntry => !!objectEntry)
.map((fileEntry) => ({
url: fileEntry.data,
filename: getFileName(fileEntry),
}));
if (!files.length) {
return null;
}
return (
<>
<div className="item-header">
<Header as="span" size="small">
<TranslatedTag tag="OBJE" />
</Header>
</div>
<AdditionalFiles files={files} />
</>
);
}
function noteDetails(noteEntryReference: GedcomEntry, gedcom: GedcomData) {
const noteEntry = dereference(
noteEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
if (!noteEntry || !hasData(noteEntry)) {
return null;
}
return ( return (
<MultilineText <MultilineText
lines={getData(entry).map((line, index) => ( lines={getData(noteEntry).map((line, index) => (
<i key={index}>{line}</i> <i key={index}>{line}</i>
))} ))}
/> />
@ -103,15 +179,19 @@ function nameDetails(entry: GedcomEntry) {
); );
} }
function getDetails( function getSectionForEachMatchingEntry(
entries: GedcomEntry[], entries: GedcomEntry[],
gedcom: GedcomData,
tags: string[], tags: string[],
detailsFunction: (entry: GedcomEntry) => React.ReactNode | null, detailsFunction: (
entry: GedcomEntry,
gedcom: GedcomData,
) => React.ReactNode | null,
): React.ReactNode[] { ): React.ReactNode[] {
return flatMap(tags, (tag) => return flatMap(tags, (tag) =>
entries entries
.filter((entry) => entry.tag === tag) .filter((entry) => entry.tag === tag)
.map((entry) => detailsFunction(entry)), .map((entry) => detailsFunction(entry, gedcom)),
) )
.filter((element) => element !== null) .filter((element) => element !== null)
.map((element, index) => ( .map((element, index) => (
@ -121,6 +201,34 @@ function getDetails(
)); ));
} }
function combineAllMatchingEntriesIntoSingleSection(
entries: GedcomEntry[],
gedcom: GedcomData,
tags: string[],
detailsFunction: (
entries: GedcomEntry[],
gedcom: GedcomData,
) => React.ReactNode | null,
): React.ReactNode {
const entriesWithMatchingTag = flatMap(tags, (tag) =>
entries.filter((entry) => entry.tag === tag),
).filter((element) => element !== null);
const sectionWithDetails = entriesWithMatchingTag.length
? detailsFunction(entriesWithMatchingTag, gedcom)
: null;
if (!sectionWithDetails) {
return null;
}
return (
<Item>
<Item.Content>{sectionWithDetails}</Item.Content>
</Item>
);
}
/** /**
* Returns true if there is displayable information in this entry. * Returns true if there is displayable information in this entry.
* Returns false if there is no data in this entry or this is only a reference * Returns false if there is no data in this entry or this is only a reference
@ -130,9 +238,10 @@ function hasData(entry: GedcomEntry) {
return entry.tree.length > 0 || (entry.data && !entry.data.startsWith('@')); return entry.tree.length > 0 || (entry.data && !entry.data.startsWith('@'));
} }
function getOtherDetails(entries: GedcomEntry[]) { function getOtherSections(entries: GedcomEntry[], gedcom: GedcomData) {
return entries return entries
.filter((entry) => !EXCLUDED_TAGS.includes(entry.tag)) .filter((entry) => !EXCLUDED_TAGS.includes(entry.tag))
.map((entry) => dereference(entry, gedcom, (gedcom) => gedcom.other))
.filter(hasData) .filter(hasData)
.map((entry) => dataDetails(entry)) .map((entry) => dataDetails(entry))
.filter((element) => element !== null) .filter((element) => element !== null)
@ -150,18 +259,42 @@ interface Props {
export function Details(props: Props) { export function Details(props: Props) {
const entries = props.gedcom.indis[props.indi].tree; const entries = props.gedcom.indis[props.indi].tree;
const entriesWithData = entries
.map((entry) => dereference(entry, props.gedcom, (gedcom) => gedcom.other))
.filter(hasData);
return ( return (
<div className="details"> <div className="details">
<Item.Group divided> <Item.Group divided>
{getDetails(entries, ['NAME'], nameDetails)} {getSectionForEachMatchingEntry(
{getDetails(entriesWithData, ['OBJE'], fileDetails)} entries,
props.gedcom,
['NAME'],
nameDetails,
)}
{getSectionForEachMatchingEntry(
entries,
props.gedcom,
['OBJE'],
imageDetails,
)}
<Events gedcom={props.gedcom} entries={entries} indi={props.indi} /> <Events gedcom={props.gedcom} entries={entries} indi={props.indi} />
{getOtherDetails(entriesWithData)} {getOtherSections(entries, props.gedcom)}
{getDetails(entriesWithData, ['NOTE'], noteDetails)} {getSectionForEachMatchingEntry(
entries,
props.gedcom,
['NOTE'],
noteDetails,
)}
{combineAllMatchingEntriesIntoSingleSection(
entries,
props.gedcom,
['OBJE'],
fileDetails,
)}
{combineAllMatchingEntriesIntoSingleSection(
entries,
props.gedcom,
['SOUR'],
sourceDetails,
)}
</Item.Group> </Item.Group>
</div> </div>
); );

View File

@ -1,7 +1,5 @@
import * as React from 'react';
import {useState} from 'react'; import {useState} from 'react';
import {FormattedMessage, IntlShape, useIntl} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import Linkify from 'react-linkify';
import { import {
Icon, Icon,
Item, Item,
@ -11,9 +9,10 @@ import {
Popup, Popup,
Tab, Tab,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import {DateOrRange} from 'topola'; import {Source} from '../util/gedcom_util';
import {formatDateOrRange} from '../util/date_util'; import {AdditionalFiles, FileEntry} from './additional-files';
import {MultilineText} from './multiline-text'; import {MultilineText} from './multiline-text';
import {Sources} from './sources';
import {WrappedImage} from './wrapped-image'; import {WrappedImage} from './wrapped-image';
export interface Image { export interface Image {
@ -22,19 +21,12 @@ export interface Image {
title?: string; title?: string;
} }
export interface Source {
title?: string;
author?: string;
page?: string;
date?: DateOrRange;
publicationInfo?: string;
}
interface Props { interface Props {
images?: Image[]; images?: Image[];
notes?: string[][]; notes?: string[][];
sources?: Source[]; sources?: Source[];
indi: string; indi: string;
files?: FileEntry[];
} }
function eventImages(images: Image[] | undefined) { function eventImages(images: Image[] | undefined) {
@ -69,37 +61,7 @@ function eventNotes(notes: string[][] | undefined) {
); );
} }
function eventSources(sources: Source[] | undefined, intl: IntlShape) {
return (
!!sources?.length && (
<List>
{sources.map((source, index) => (
<List.Item key={index}>
<List.Icon verticalAlign="middle" name="circle" size="tiny" />
<List.Content>
<List.Header>
<Linkify properties={{target: '_blank'}}>
{[source.author, source.title, source.publicationInfo]
.filter((sourceElement) => sourceElement)
.join(', ')}
</Linkify>
</List.Header>
<List.Description>
<Linkify properties={{target: '_blank'}}>{source.page}</Linkify>
{source.date
? ' [' + formatDateOrRange(source.date, intl) + ']'
: null}
</List.Description>
</List.Content>
</List.Item>
))}
</List>
)
);
}
export function EventExtras(props: Props) { export function EventExtras(props: Props) {
const intl = useIntl();
const [activeIndex, setActiveIndex] = useState(-1); const [activeIndex, setActiveIndex] = useState(-1);
const [indi, setIndi] = useState(''); const [indi, setIndi] = useState('');
@ -109,7 +71,7 @@ export function EventExtras(props: Props) {
} }
function handleTabOnClick( function handleTabOnClick(
event: React.MouseEvent<HTMLAnchorElement>, _event: React.MouseEvent<HTMLAnchorElement>,
menuItemProps: MenuItemProps, menuItemProps: MenuItemProps,
) { ) {
menuItemProps.index !== undefined && activeIndex !== menuItemProps.index menuItemProps.index !== undefined && activeIndex !== menuItemProps.index
@ -162,10 +124,37 @@ export function EventExtras(props: Props) {
/> />
</Menu.Item> </Menu.Item>
), ),
render: () => <Tab.Pane>{eventSources(props.sources, intl)}</Tab.Pane>, render: () => (
<Tab.Pane>
<Sources sources={props.sources} />
</Tab.Pane>
),
}; };
const panes = [imageTab, noteTab, sourceTab].flatMap((tab) => const filesTab = props.files?.length && {
menuItem: (
<Menu.Item fitted key="files" onClick={handleTabOnClick}>
<Popup
content={
<FormattedMessage
id="extras.files"
defaultMessage="Additonal files"
/>
}
size="mini"
position="bottom center"
trigger={<Icon circular name="file alternate outline" />}
/>
</Menu.Item>
),
render: () => (
<Tab.Pane>
<AdditionalFiles files={props.files} />
</Tab.Pane>
),
};
const panes = [imageTab, noteTab, sourceTab, filesTab].flatMap((tab) =>
tab ? [tab] : [], tab ? [tab] : [],
); );

View File

@ -14,9 +14,14 @@ import {
getFileName, getFileName,
getImageFileEntry, getImageFileEntry,
getName, getName,
getNonImageFileEntry,
mapToSource,
pointerToId, pointerToId,
resolveDate,
Source,
} from '../util/gedcom_util'; } from '../util/gedcom_util';
import {EventExtras, Image, Source} from './event-extras'; import {FileEntry} from './additional-files';
import {EventExtras, Image} from './event-extras';
import {TranslatedTag} from './translated-tag'; import {TranslatedTag} from './translated-tag';
function PersonLink(props: {person: GedcomEntry}) { function PersonLink(props: {person: GedcomEntry}) {
@ -53,6 +58,7 @@ interface EventData {
personLink?: GedcomEntry; personLink?: GedcomEntry;
place?: string[]; place?: string[];
images?: Image[]; images?: Image[];
files?: FileEntry[];
notes?: string[][]; notes?: string[][];
sources?: Source[]; sources?: Source[];
indi: string; indi: string;
@ -74,7 +80,7 @@ const FAMILY_EVENT_TAGS = ['MARR', 'DIV'];
function EventHeader(props: {event: EventData}) { function EventHeader(props: {event: EventData}) {
const intl = useIntl(); const intl = useIntl();
return ( return (
<div className="event-header"> <div className="item-header">
<Header as="span" size="small"> <Header as="span" size="small">
<TranslatedTag tag={props.event.type} /> <TranslatedTag tag={props.event.type} />
</Header> </Header>
@ -144,48 +150,29 @@ function eventImages(entry: GedcomEntry, gedcom: GedcomData): Image[] {
); );
} }
function eventFiles(entry: GedcomEntry, gedcom: GedcomData): Image[] {
return 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) || '',
},
]
: [],
);
}
function eventSources(entry: GedcomEntry, gedcom: GedcomData): Source[] { function eventSources(entry: GedcomEntry, gedcom: GedcomData): Source[] {
return entry.tree return entry.tree
.filter((subEntry) => 'SOUR' === subEntry.tag) .filter((subEntry) => 'SOUR' === subEntry.tag)
.map((sourceEntryReference) => { .map((sourceEntryReference) => mapToSource(sourceEntryReference, gedcom));
const sourceEntry = dereference(
sourceEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
const title = sourceEntry.tree.find(
(subEntry) => 'TITL' === subEntry.tag,
);
const abbr = sourceEntry.tree.find((subEntry) => 'ABBR' === subEntry.tag);
const author = sourceEntry.tree.find(
(subEntry) => 'AUTH' === subEntry.tag,
);
const publicationInfo = sourceEntry.tree.find(
(subEntry) => 'PUBL' === subEntry.tag,
);
const page = sourceEntryReference.tree.find(
(subEntry) => 'PAGE' === subEntry.tag,
);
const sourceData = sourceEntryReference.tree.find(
(subEntry) => 'DATA' === subEntry.tag,
);
const date = sourceData ? resolveDate(sourceData) : undefined;
return {
title: title?.data || abbr?.data,
author: author?.data,
page: page?.data,
date: date ? getDate(date.data) : undefined,
publicationInfo: publicationInfo?.data,
};
});
} }
function eventNotes(entry: GedcomEntry, gedcom: GedcomData): string[][] { function eventNotes(entry: GedcomEntry, gedcom: GedcomData): string[][] {
@ -221,6 +208,7 @@ function toIndiEvent(
age: getAge(entry, indi, gedcom, intl), age: getAge(entry, indi, gedcom, intl),
place: eventPlace(entry), place: eventPlace(entry),
images: eventImages(entry, gedcom), images: eventImages(entry, gedcom),
files: eventFiles(entry, gedcom),
notes: eventNotes(entry, gedcom), notes: eventNotes(entry, gedcom),
sources: eventSources(entry, gedcom), sources: eventSources(entry, gedcom),
indi: indi, indi: indi,
@ -228,10 +216,6 @@ function toIndiEvent(
]; ];
} }
function resolveDate(entry: GedcomEntry) {
return entry.tree.find((subEntry) => subEntry.tag === 'DATE');
}
function toFamilyEvents( function toFamilyEvents(
entry: GedcomEntry, entry: GedcomEntry,
gedcom: GedcomData, gedcom: GedcomData,
@ -248,6 +232,7 @@ function toFamilyEvents(
personLink: getSpouse(indi, family, gedcom), personLink: getSpouse(indi, family, gedcom),
place: eventPlace(familyMarriageEvent), place: eventPlace(familyMarriageEvent),
images: eventImages(familyMarriageEvent, gedcom), images: eventImages(familyMarriageEvent, gedcom),
files: eventFiles(familyMarriageEvent, gedcom),
notes: eventNotes(familyMarriageEvent, gedcom), notes: eventNotes(familyMarriageEvent, gedcom),
sources: eventSources(familyMarriageEvent, gedcom), sources: eventSources(familyMarriageEvent, gedcom),
indi: indi, indi: indi,
@ -272,6 +257,7 @@ function Event(props: {event: EventData}) {
notes={props.event.notes} notes={props.event.notes}
sources={props.event.sources} sources={props.event.sources}
indi={props.event.indi} indi={props.event.indi}
files={props.event.files}
/> />
</Item.Content> </Item.Content>
</Item> </Item>

38
src/details/sources.tsx Normal file
View File

@ -0,0 +1,38 @@
import {useIntl} from 'react-intl';
import Linkify from 'react-linkify';
import {List} from 'semantic-ui-react';
import {formatDateOrRange} from '../util/date_util';
import {Source} from '../util/gedcom_util';
interface Props {
sources?: Source[];
}
export function Sources({sources}: Props) {
const intl = useIntl();
if (!sources?.length) return null;
return (
<List>
{sources.map((source, index) => (
<List.Item key={index}>
<List.Icon verticalAlign="middle" name="circle" size="tiny" />
<List.Content>
<List.Header>
<Linkify properties={{target: '_blank'}}>
{[source.author, source.title, source.publicationInfo]
.filter((sourceElement) => !!sourceElement)
.join(', ')}
</Linkify>
</List.Header>
<List.Description>
<Linkify properties={{target: '_blank'}}>{source.page}</Linkify>
{source.date && ` [${formatDateOrRange(source.date, intl)}]`}
</List.Description>
</List.Content>
</List.Item>
))}
</List>
);
}

View File

@ -22,6 +22,8 @@ const TAG_DESCRIPTIONS = new Map([
['OCCU', 'Occupation'], ['OCCU', 'Occupation'],
['TITL', 'Title'], ['TITL', 'Title'],
['WWW', 'WWW'], ['WWW', 'WWW'],
['OBJE', 'Additional files'],
['SOUR', 'Sources'],
['birth', 'Birth name'], ['birth', 'Birth name'],
['married', 'Married name'], ['married', 'Married name'],
['maiden', 'Maiden name'], ['maiden', 'Maiden name'],

View File

@ -153,13 +153,13 @@ div.zoom {
padding: 0 15px; padding: 0 15px;
} }
.details .event-header { .details .item-header {
justify-content: space-between; justify-content: space-between;
display: flex; display: flex;
word-break: break-word; word-break: break-word;
} }
.details .event-header .header { .details .item-header .header {
text-transform: uppercase; text-transform: uppercase;
margin: 0; margin: 0;
min-width: 40%; min-width: 40%;

View File

@ -62,6 +62,8 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Обръщение", "gedcom.TITL": "Обръщение",
"gedcom.WWW": "Линк", "gedcom.WWW": "Линк",
"gedcom.OBJE": "Допълнителни файлове",
"gedcom.SOUR": "Източници",
"gedcom._UPD": "Последно обновление", "gedcom._UPD": "Последно обновление",
"gedcom.birth": "Рождено име", "gedcom.birth": "Рождено име",
"gedcom.married": "Име след брак", "gedcom.married": "Име след брак",
@ -106,5 +108,6 @@
"name.unknown_name": "Неизвестно име", "name.unknown_name": "Неизвестно име",
"extras.images": "Изображение", "extras.images": "Изображение",
"extras.notes": "Бележки", "extras.notes": "Бележки",
"extras.sources": "Източници" "extras.sources": "Източници",
"extras.files": "Допълнителни файлове"
} }

View File

@ -63,6 +63,8 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Titul", "gedcom.TITL": "Titul",
"gedcom.WWW": "Stránka WWW", "gedcom.WWW": "Stránka WWW",
"gedcom.OBJE": "Další soubory",
"gedcom.SOUR": "Zdroje",
"gedcom.RELI": "Vyznání", "gedcom.RELI": "Vyznání",
"gedcom._UPD": "Poslední aktualizace", "gedcom._UPD": "Poslední aktualizace",
"gedcom.birth": "Rodné jméno", "gedcom.birth": "Rodné jméno",
@ -107,5 +109,6 @@
"name.unknown_name": "N.N.", "name.unknown_name": "N.N.",
"extras.images": "Obrázky", "extras.images": "Obrázky",
"extras.notes": "Poznámky", "extras.notes": "Poznámky",
"extras.sources": "Zdroje" "extras.sources": "Zdroje",
"extras.files": "Další soubory"
} }

View File

@ -50,6 +50,8 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Titel", "gedcom.TITL": "Titel",
"gedcom.WWW": "Website", "gedcom.WWW": "Website",
"gedcom.OBJE": "Zusätzliche Dateien",
"gedcom.SOUR": "Quellen",
"gedcom._UPD": "Zuletzt aktualisiert", "gedcom._UPD": "Zuletzt aktualisiert",
"gedcom.birth": "Geburtsname", "gedcom.birth": "Geburtsname",
"gedcom.married": "Ehenamen", "gedcom.married": "Ehenamen",

View File

@ -60,6 +60,8 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Titre", "gedcom.TITL": "Titre",
"gedcom.WWW": "Site Web", "gedcom.WWW": "Site Web",
"gedcom.OBJE": "Fichiers supplémentaires",
"gedcom.SOUR": "Sources",
"gedcom._UPD": "Dernière mise à jour", "gedcom._UPD": "Dernière mise à jour",
"gedcom.MARR": "Mariage", "gedcom.MARR": "Mariage",
"gedcom.DIV": "Divorce", "gedcom.DIV": "Divorce",

View File

@ -52,6 +52,8 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Titolo", "gedcom.TITL": "Titolo",
"gedcom.WWW": "Sito web", "gedcom.WWW": "Sito web",
"gedcom.OBJE": "File aggiuntivi",
"gedcom.SOUR": "Fonti",
"gedcom._UPD": "Ultimo aggiornamento", "gedcom._UPD": "Ultimo aggiornamento",
"gedcom.birth": "Nome alla nascita", "gedcom.birth": "Nome alla nascita",
"gedcom.married": "Nome da coniugato/a", "gedcom.married": "Nome da coniugato/a",

View File

@ -55,6 +55,8 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Tytuł", "gedcom.TITL": "Tytuł",
"gedcom.WWW": "Strona WWW", "gedcom.WWW": "Strona WWW",
"gedcom.OBJE": "Dodatkowe pliki",
"gedcom.SOUR": "Źródła",
"gedcom._UPD": "Ostatnia aktualizacja", "gedcom._UPD": "Ostatnia aktualizacja",
"gedcom.MARR": "Małżeństwo", "gedcom.MARR": "Małżeństwo",
"gedcom.DIV": "Rozwód", "gedcom.DIV": "Rozwód",
@ -94,5 +96,6 @@
"name.unknown_name": "N.N.", "name.unknown_name": "N.N.",
"extras.images": "Zdjęcia", "extras.images": "Zdjęcia",
"extras.notes": "Notatki", "extras.notes": "Notatki",
"extras.sources": "Źródła" "extras.sources": "Źródła",
"extras.files": "Dodatkowe pliki"
} }

View File

@ -60,6 +60,8 @@
"gedcom.RIN": "ID", "gedcom.RIN": "ID",
"gedcom.TITL": "Титул", "gedcom.TITL": "Титул",
"gedcom.WWW": "Веб-сайт WWW", "gedcom.WWW": "Веб-сайт WWW",
"gedcom.OBJE": "Дополнительные файлы",
"gedcom.SOUR": "Источники",
"gedcom._UPD": "Последнее обновление", "gedcom._UPD": "Последнее обновление",
"gedcom.birth": "Имя при рождении", "gedcom.birth": "Имя при рождении",
"gedcom.married": "Имя в браке", "gedcom.married": "Имя в браке",
@ -97,5 +99,6 @@
"name.unknown_name": "Н.И.", "name.unknown_name": "Н.И.",
"extras.images": "Картинки", "extras.images": "Картинки",
"extras.notes": "Примечание", "extras.notes": "Примечание",
"extras.sources": "Источники" "extras.sources": "Источники",
"extras.files": "Дополнительные файлы"
} }

View File

@ -1,6 +1,8 @@
import {GedcomEntry, parse as parseGedcom} from 'parse-gedcom'; import {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';
import { import {
DateOrRange,
gedcomEntriesToJson, gedcomEntriesToJson,
getDate,
JsonFam, JsonFam,
JsonGedcomData, JsonGedcomData,
JsonImage, JsonImage,
@ -25,6 +27,14 @@ export interface TopolaData {
gedcom: GedcomData; gedcom: GedcomData;
} }
export interface Source {
title?: string;
author?: string;
page?: string;
date?: DateOrRange;
publicationInfo?: string;
}
/** /**
* Returns the identifier extracted from a pointer string. * Returns the identifier extracted from a pointer string.
* E.g. '@I123@' -> 'I123' * E.g. '@I123@' -> 'I123'
@ -296,13 +306,67 @@ export function getFileName(fileEntry: GedcomEntry): string | undefined {
return fileTitle && fileExtension && fileTitle + '.' + fileExtension; return fileTitle && fileExtension && fileTitle + '.' + fileExtension;
} }
export function getImageFileEntry( function findFileEntry(
objectEntry: GedcomEntry, objectEntry: GedcomEntry,
predicate: (entry: GedcomEntry) => boolean,
): GedcomEntry | undefined { ): GedcomEntry | undefined {
return objectEntry.tree.find( return objectEntry.tree.find(
(entry) => (entry) =>
entry.tag === 'FILE' && entry.tag === 'FILE' && entry.data.startsWith('http') && predicate(entry),
entry.data.startsWith('http') &&
isImageFile(entry.data),
); );
} }
export function getNonImageFileEntry(
objectEntry: GedcomEntry,
): GedcomEntry | undefined {
return findFileEntry(objectEntry, (entry) => !isImageFile(entry.data));
}
export function getImageFileEntry(
objectEntry: GedcomEntry,
): GedcomEntry | undefined {
return findFileEntry(objectEntry, (entry) => isImageFile(entry.data));
}
export function resolveDate(entry: GedcomEntry) {
return entry.tree.find((subEntry) => subEntry.tag === 'DATE');
}
export function mapToSource(
sourceEntryReference: GedcomEntry,
gedcom: GedcomData,
) {
const sourceEntry = dereference(
sourceEntryReference,
gedcom,
(gedcom) => gedcom.other,
);
const title = sourceEntry.tree.find((subEntry) => 'TITL' === subEntry.tag);
const abbr = sourceEntry.tree.find((subEntry) => 'ABBR' === subEntry.tag);
const author = sourceEntry.tree.find((subEntry) => 'AUTH' === subEntry.tag);
const publicationInfo = sourceEntry.tree.find(
(subEntry) => 'PUBL' === subEntry.tag,
);
const page = sourceEntryReference.tree.find(
(subEntry) => 'PAGE' === subEntry.tag,
);
const sourceData = sourceEntryReference.tree.find(
(subEntry) => 'DATA' === subEntry.tag,
);
const date = sourceData ? resolveDate(sourceData) : undefined;
return {
title: title?.data || abbr?.data,
author: author?.data,
page: page?.data,
date: date ? getDate(date.data) : undefined,
publicationInfo: publicationInfo?.data,
};
}