Files
topola-viewer/src/menu/top_bar.tsx
2026-05-31 09:59:12 +02:00

411 lines
12 KiB
TypeScript

import queryString from 'query-string';
import {FormattedMessage} from 'react-intl';
import {Link, useLocation, useNavigate} from 'react-router';
import {Dropdown, Icon, Menu} from 'semantic-ui-react';
import {IndiInfo, JsonGedcomData} from 'topola';
import {isGoogleDriveConfigured} from '../datasource/google_drive_service';
import {Media} from '../util/media';
import {GoogleDriveMenu} from './google_drive_menu';
import {MenuItem, MenuType} from './menu_item';
import {SearchBar} from './search';
import {UploadMenu} from './upload_menu';
import {UrlMenu} from './url_menu';
import {WikiTreeLoginMenu, WikiTreeMenu} from './wikitree_menu';
enum ScreenSize {
LARGE,
SMALL,
}
interface EventHandlers {
onSelection: (indiInfo: IndiInfo) => void;
onPrint: () => void;
onDownloadPdf: () => void;
onDownloadPng: () => void;
onDownloadSvg: () => void;
}
interface Props {
/** True if the application is currently showing a chart. */
showingChart: boolean;
/** Data used for the search index. */
data?: JsonGedcomData;
standalone: boolean;
/** Whether to show the "All relatives" chart type in the menu. */
allowAllRelativesChart: boolean;
allowPrintAndDownload: boolean;
eventHandlers: EventHandlers;
/** Whether to show additional WikiTree menus. */
showWikiTreeMenus: boolean;
/** Whether the user has authorized Google Drive and has an active token. */
hasGoogleToken: boolean;
/** Callback to sign out of Google Drive. */
onGoogleSignOut?: () => void;
/** Callback triggered when a new Google Drive token is acquired. */
onGoogleTokenAcquired?: () => void;
}
export function TopBar(props: Props) {
const navigate = useNavigate();
const location = useLocation();
function changeView(view: string) {
const search = queryString.parse(location.search);
if (search.view !== view) {
search.view = view;
location.search = queryString.stringify(search);
navigate(location);
}
}
function chartMenus(screenSize: ScreenSize) {
if (!props.showingChart || !props.data) {
return null;
}
const chartTypeItems = (
<>
<Dropdown.Item onClick={() => changeView('hourglass')}>
<Icon name="hourglass" />
<FormattedMessage
id="menu.hourglass"
defaultMessage="Hourglass chart"
/>
</Dropdown.Item>
{props.allowAllRelativesChart ? (
<Dropdown.Item onClick={() => changeView('relatives')}>
<Icon name="users" />
<FormattedMessage
id="menu.relatives"
defaultMessage="All relatives"
/>
</Dropdown.Item>
) : null}
<Dropdown.Item onClick={() => changeView('donatso')}>
<Icon name="users" />
<FormattedMessage
id="menu.donatso"
defaultMessage="Donatso family chart"
/>
</Dropdown.Item>
<Dropdown.Item onClick={() => changeView('fancy')}>
<Icon name="users" />
<FormattedMessage
id="menu.fancy"
defaultMessage="Fancy tree (experimental)"
/>
</Dropdown.Item>
</>
);
switch (screenSize) {
case ScreenSize.LARGE:
return (
<>
<Menu.Item
onClick={props.eventHandlers.onPrint}
disabled={!props.allowPrintAndDownload}
>
<Icon name="print" />
<FormattedMessage id="menu.print" defaultMessage="Print" />
</Menu.Item>
<Dropdown
trigger={
<div>
<Icon name="download" />
<FormattedMessage
id="menu.download"
defaultMessage="Download"
/>
</div>
}
className="item"
disabled={!props.allowPrintAndDownload}
>
<Dropdown.Menu>
<Dropdown.Item onClick={props.eventHandlers.onDownloadPdf}>
<FormattedMessage
id="menu.pdf_file"
defaultMessage="PDF file"
/>
</Dropdown.Item>
<Dropdown.Item onClick={props.eventHandlers.onDownloadPng}>
<FormattedMessage
id="menu.png_file"
defaultMessage="PNG file"
/>
</Dropdown.Item>
<Dropdown.Item onClick={props.eventHandlers.onDownloadSvg}>
<FormattedMessage
id="menu.svg_file"
defaultMessage="SVG file"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Dropdown
trigger={
<div>
<Icon name="eye" />
<FormattedMessage id="menu.view" defaultMessage="View" />
</div>
}
className="item"
>
<Dropdown.Menu>{chartTypeItems}</Dropdown.Menu>
</Dropdown>
<SearchBar
data={props.data}
onSelection={props.eventHandlers.onSelection}
{...props}
/>
</>
);
case ScreenSize.SMALL:
return (
<>
<Dropdown.Item onClick={props.eventHandlers.onPrint}>
<Icon name="print" />
<FormattedMessage id="menu.print" defaultMessage="Print" />
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item onClick={props.eventHandlers.onDownloadPdf}>
<Icon name="download" />
<FormattedMessage
id="menu.download_pdf"
defaultMessage="Download PDF"
/>
</Dropdown.Item>
<Dropdown.Item onClick={props.eventHandlers.onDownloadPng}>
<Icon name="download" />
<FormattedMessage
id="menu.download_png"
defaultMessage="Download PNG"
/>
</Dropdown.Item>
<Dropdown.Item onClick={props.eventHandlers.onDownloadSvg}>
<Icon name="download" />
<FormattedMessage
id="menu.download_svg"
defaultMessage="Download SVG"
/>
</Dropdown.Item>
<Dropdown.Divider />
{chartTypeItems}
<Dropdown.Divider />
</>
);
}
}
function title() {
return (
<Menu.Item>
<b>Topola Genealogy</b>
</Menu.Item>
);
}
function googleDriveDisconnectMenu(screenSize: ScreenSize) {
if (!props.hasGoogleToken || !isGoogleDriveConfigured()) {
return null;
}
return (
<>
<MenuItem
menuType={
screenSize === ScreenSize.SMALL ? MenuType.Dropdown : MenuType.Menu
}
onClick={props.onGoogleSignOut}
>
<Icon name="sign out" />
<FormattedMessage
id="menu.google_sign_out"
defaultMessage="Disconnect Google Drive"
/>
</MenuItem>
{screenSize === ScreenSize.SMALL ? <Dropdown.Divider /> : null}
</>
);
}
function fileMenuItems(menuType: MenuType) {
return (
<>
<UploadMenu menuType={menuType} {...props} />
<UrlMenu menuType={menuType} {...props} />
<WikiTreeMenu menuType={menuType} {...props} />
<GoogleDriveMenu
menuType={menuType}
onTokenAcquired={props.onGoogleTokenAcquired}
/>
</>
);
}
function fileMenus(screenSize: ScreenSize) {
// In standalone WikiTree mode, show only the "Select WikiTree ID" menu.
if (!props.standalone && props.showWikiTreeMenus) {
switch (screenSize) {
case ScreenSize.LARGE:
return <WikiTreeMenu menuType={MenuType.Menu} {...props} />;
case ScreenSize.SMALL:
return (
<>
<WikiTreeMenu menuType={MenuType.Dropdown} {...props} />
<Dropdown.Divider />
</>
);
}
}
// Don't show "open" menus in non-standalone mode.
if (!props.standalone) {
return null;
}
switch (screenSize) {
case ScreenSize.LARGE:
// Show dropdown if chart is shown, otherwise show individual menu
// items.
return props.showingChart ? (
<Dropdown
trigger={
<div>
<Icon name="folder open" />
<FormattedMessage id="menu.open" defaultMessage="Open" />
</div>
}
className="item"
>
<Dropdown.Menu>{fileMenuItems(MenuType.Dropdown)}</Dropdown.Menu>
</Dropdown>
) : (
fileMenuItems(MenuType.Menu)
);
case ScreenSize.SMALL:
return (
<>
{fileMenuItems(MenuType.Dropdown)}
<Dropdown.Divider />
</>
);
}
}
function wikiTreeLoginMenu(screenSize: ScreenSize) {
if (!props.showWikiTreeMenus) {
return null;
}
return (
<>
<WikiTreeLoginMenu
menuType={
screenSize === ScreenSize.SMALL ? MenuType.Dropdown : MenuType.Menu
}
{...props}
/>
{screenSize === ScreenSize.SMALL ? <Dropdown.Divider /> : null}
</>
);
}
function mobileMenus() {
return (
<>
<Dropdown
trigger={
<div>
<Icon name="sidebar" />
</div>
}
className="item"
icon={null}
>
<Dropdown.Menu>
{fileMenus(ScreenSize.SMALL)}
{chartMenus(ScreenSize.SMALL)}
{googleDriveDisconnectMenu(ScreenSize.SMALL)}
{wikiTreeLoginMenu(ScreenSize.SMALL)}
<Dropdown.Item
href="https://github.com/PeWu/topola-viewer"
target="_blank"
rel="noopener noreferrer"
>
<FormattedMessage
id="menu.github"
defaultMessage="GitHub project"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<div className="topbar--title">
{props.standalone ? <Link to="/">{title()}</Link> : title()}
</div>
{props.showingChart && props.data && (
<SearchBar
data={props.data}
onSelection={props.eventHandlers.onSelection}
{...props}
/>
)}
</>
);
}
function desktopMenus() {
return (
<>
{props.standalone ? <Link to="/">{title()}</Link> : null}
{fileMenus(ScreenSize.LARGE)}
{chartMenus(ScreenSize.LARGE)}
<Menu.Menu position="right">
{googleDriveDisconnectMenu(ScreenSize.LARGE)}
{wikiTreeLoginMenu(ScreenSize.LARGE)}
<Menu.Item
href="https://github.com/PeWu/topola-viewer"
target="_blank"
rel="noopener noreferrer"
>
<FormattedMessage
id="menu.github"
defaultMessage="GitHub project"
/>
</Menu.Item>
</Menu.Menu>
</>
);
}
return (
<>
<Menu
as={Media}
greaterThanOrEqual="large"
attached="top"
inverted
color="blue"
size="large"
>
{desktopMenus()}
</Menu>
<Menu
as={Media}
at="small"
attached="top"
inverted
color="blue"
size="large"
>
{mobileMenus()}
</Menu>
</>
);
}