import { BrowserWarning } from '@affine/component/affine-banner'; import { appSidebarFloatingAtom, appSidebarOpenAtom, } from '@affine/component/app-sidebar'; import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header'; import { isDesktop } from '@affine/env/constant'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons'; import type { Page } from '@blocksuite/store'; import { affinePluginsAtom } from '@toeverything/plugin-infra/manager'; import type { PluginUIAdapter } from '@toeverything/plugin-infra/type'; import { useAtom, useAtomValue } from 'jotai'; import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; import { forwardRef, memo, useCallback, useEffect, useMemo, useState, } from 'react'; import { guideDownloadClientTipAtom } from '../../../atoms/guide'; import { contentLayoutAtom } from '../../../atoms/layout'; import { useCurrentMode } from '../../../hooks/current/use-current-mode'; import type { AffineOfficialWorkspace } from '../../../shared'; import { DownloadClientTip } from './download-tips'; import EditPage from './header-right-items/edit-page'; import { EditorOptionMenu } from './header-right-items/editor-option-menu'; import { HeaderShareMenu } from './header-right-items/share-menu'; import TrashButtonGroup from './header-right-items/trash-button-group'; import UserAvatar from './header-right-items/user-avatar'; import * as styles from './styles.css'; import { OSWarningMessage, shouldShowWarning } from './utils'; export type BaseHeaderProps< Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace, > = { workspace: Workspace; currentPage: Page | null; isPublic: boolean; leftSlot?: ReactNode; }; export enum HeaderRightItemName { EditorOptionMenu = 'editorOptionMenu', TrashButtonGroup = 'trashButtonGroup', ShareMenu = 'shareMenu', EditPage = 'editPage', UserAvatar = 'userAvatar', // some windows only items WindowsAppControls = 'windowsAppControls', } type HeaderItem = { Component: FC; // todo: public workspace should be one of the flavour availableWhen: ( workspace: AffineOfficialWorkspace, currentPage: Page | null, status: { isPublic: boolean; } ) => boolean; }; const HeaderRightItems: Record = { [HeaderRightItemName.TrashButtonGroup]: { Component: TrashButtonGroup, availableWhen: (_, currentPage) => { return currentPage?.meta.trash === true; }, }, [HeaderRightItemName.ShareMenu]: { Component: HeaderShareMenu, availableWhen: (workspace, currentPage) => { return workspace.flavour !== WorkspaceFlavour.PUBLIC && !!currentPage; }, }, [HeaderRightItemName.EditPage]: { Component: EditPage, availableWhen: (workspace, currentPage, { isPublic }) => { return isPublic; }, }, [HeaderRightItemName.UserAvatar]: { Component: UserAvatar, availableWhen: (workspace, currentPage, { isPublic }) => { return isPublic; }, }, [HeaderRightItemName.EditorOptionMenu]: { Component: EditorOptionMenu, availableWhen: (_, currentPage, { isPublic }) => { return !isPublic; }, }, [HeaderRightItemName.WindowsAppControls]: { Component: () => { const handleMinimizeApp = useCallback(() => { window.apis?.ui.handleMinimizeApp().catch(err => { console.error(err); }); }, []); const handleMaximizeApp = useCallback(() => { window.apis?.ui.handleMaximizeApp().catch(err => { console.error(err); }); }, []); const handleCloseApp = useCallback(() => { window.apis?.ui.handleCloseApp().catch(err => { console.error(err); }); }, []); return (
); }, availableWhen: () => { return isDesktop && globalThis.platform === 'win32'; }, }, }; export type HeaderProps = BaseHeaderProps; const PluginHeaderItemAdapter = memo<{ headerItem: PluginUIAdapter['headerItem']; }>(function PluginHeaderItemAdapter({ headerItem }) { return (
{headerItem({ contentLayoutAtom, })}
); }); const PluginHeader = () => { const affinePluginsMap = useAtomValue(affinePluginsAtom); const plugins = useMemo( () => Object.values(affinePluginsMap), [affinePluginsMap] ); return (
{plugins .filter(plugin => plugin.uiAdapter.headerItem != null) .map(plugin => { const headerItem = plugin.uiAdapter .headerItem as PluginUIAdapter['headerItem']; return ( ); })}
); }; export const Header = forwardRef< HTMLDivElement, PropsWithChildren & HTMLAttributes >((props, ref) => { const [showWarning, setShowWarning] = useState(false); const [showGuideDownloadClientTip, setShowGuideDownloadClientTip] = useState(false); const [shouldShowGuideDownloadClientTip] = useAtom( guideDownloadClientTipAtom ); useEffect(() => { setShowWarning(shouldShowWarning()); setShowGuideDownloadClientTip(shouldShowGuideDownloadClientTip); }, [shouldShowGuideDownloadClientTip]); const open = useAtomValue(appSidebarOpenAtom); const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); const mode = useCurrentMode(); return (
{showGuideDownloadClientTip ? ( ) : ( } onClose={() => { setShowWarning(false); }} /> )}
{!open && } {props.leftSlot}
{props.children}
{useMemo(() => { return Object.entries(HeaderRightItems).map( ([name, { availableWhen, Component }]) => { if ( availableWhen(props.workspace, props.currentPage, { isPublic: props.isPublic, }) ) { return ( ); } return null; } ); }, [props])}
); }); Header.displayName = 'Header';