mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 19:02:23 +08:00
feat(core): responsive detail page header (#7263)
This commit is contained in:
@@ -7,19 +7,13 @@ import { ShareMenu } from './share-menu';
|
|||||||
type SharePageModalProps = {
|
type SharePageModalProps = {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
page: Doc;
|
page: Doc;
|
||||||
isJournal?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SharePageButton = ({
|
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
||||||
workspace,
|
|
||||||
page,
|
|
||||||
isJournal,
|
|
||||||
}: SharePageModalProps) => {
|
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
isJournal={isJournal}
|
|
||||||
workspaceMetadata={workspace.meta}
|
workspaceMetadata={workspace.meta}
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
onEnableAffineCloud={() =>
|
onEnableAffineCloud={() =>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ globalStyle(`${shareLinkStyle} > span`, {
|
|||||||
globalStyle(`${shareLinkStyle} > div > svg`, {
|
globalStyle(`${shareLinkStyle} > div > svg`, {
|
||||||
color: cssVar('linkColor'),
|
color: cssVar('linkColor'),
|
||||||
});
|
});
|
||||||
export const journalShareButton = style({
|
export const shareButton = style({
|
||||||
height: 32,
|
height: 32,
|
||||||
padding: '0px 8px',
|
padding: '0px 8px',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
|
|
||||||
import { useIsActiveView } from '@affine/core/modules/workbench';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { WebIcon } from '@blocksuite/icons/rc';
|
import { WebIcon } from '@blocksuite/icons/rc';
|
||||||
import type { Doc } from '@blocksuite/store';
|
import type { Doc } from '@blocksuite/store';
|
||||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import { forwardRef, type PropsWithChildren, type Ref } from 'react';
|
||||||
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import { ShareExport } from './share-export';
|
import { ShareExport } from './share-export';
|
||||||
import { SharePage } from './share-page';
|
import { SharePage } from './share-page';
|
||||||
|
|
||||||
export interface ShareMenuProps {
|
export interface ShareMenuProps extends PropsWithChildren {
|
||||||
workspaceMetadata: WorkspaceMetadata;
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
currentPage: Doc;
|
currentPage: Doc;
|
||||||
isJournal?: boolean;
|
|
||||||
onEnableAffineCloud: () => void;
|
onEnableAffineCloud: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShareMenuContent = (props: ShareMenuProps) => {
|
export const ShareMenuContent = (props: ShareMenuProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
return (
|
return (
|
||||||
<div className={styles.containerStyle}>
|
<div className={styles.containerStyle}>
|
||||||
@@ -40,8 +37,20 @@ const ShareMenuContent = (props: ShareMenuProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LocalShareMenu = (props: ShareMenuProps) => {
|
const DefaultShareButton = forwardRef(function DefaultShareButton(
|
||||||
|
_,
|
||||||
|
ref: Ref<HTMLButtonElement>
|
||||||
|
) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button ref={ref} className={styles.shareButton} type="primary">
|
||||||
|
{t['com.affine.share-menu.shareButton']()}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const LocalShareMenu = (props: ShareMenuProps) => {
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
items={<ShareMenuContent {...props} />}
|
items={<ShareMenuContent {...props} />}
|
||||||
@@ -53,27 +62,14 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
|||||||
modal: false,
|
modal: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<div data-testid="local-share-menu-button">
|
||||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
{props.children || <DefaultShareButton />}
|
||||||
data-testid="local-share-menu-button"
|
</div>
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
{t['com.affine.share-menu.shareButton']()}
|
|
||||||
</Button>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
// only enable copy link commands when the view is active and the workspace is cloud
|
|
||||||
const isActiveView = useIsActiveView();
|
|
||||||
useRegisterCopyLinkCommands({
|
|
||||||
workspaceId: props.workspaceMetadata.id,
|
|
||||||
docId: props.currentPage.id,
|
|
||||||
isActiveView,
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
items={<ShareMenuContent {...props} />}
|
items={<ShareMenuContent {...props} />}
|
||||||
@@ -85,13 +81,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
|||||||
modal: false,
|
modal: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<div data-testid="cloud-share-menu-button">
|
||||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
{props.children || <DefaultShareButton />}
|
||||||
data-testid="cloud-share-menu-button"
|
</div>
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
{t['com.affine.share-menu.shareButton']()}
|
|
||||||
</Button>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ import {
|
|||||||
MenuIcon,
|
MenuIcon,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuSeparator,
|
MenuSeparator,
|
||||||
|
MenuSub,
|
||||||
} from '@affine/component/ui/menu';
|
} from '@affine/component/ui/menu';
|
||||||
import { openHistoryTipsModalAtom } from '@affine/core/atoms';
|
import { openHistoryTipsModalAtom } from '@affine/core/atoms';
|
||||||
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
||||||
|
import { ShareMenuContent } from '@affine/core/components/affine/share-page-modal/share-menu';
|
||||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||||
|
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
|
||||||
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
||||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive';
|
||||||
import { mixpanel } from '@affine/core/utils';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -24,7 +28,9 @@ import {
|
|||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
ImportIcon,
|
ImportIcon,
|
||||||
PageIcon,
|
PageIcon,
|
||||||
|
ShareIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
|
import type { Doc } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
DocService,
|
DocService,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
@@ -40,16 +46,19 @@ import { useFavorite } from '../favorite';
|
|||||||
|
|
||||||
type PageMenuProps = {
|
type PageMenuProps = {
|
||||||
rename?: () => void;
|
rename?: () => void;
|
||||||
pageId: string;
|
page: Doc;
|
||||||
isJournal?: boolean;
|
isJournal?: boolean;
|
||||||
};
|
};
|
||||||
// fixme: refactor this file
|
// fixme: refactor this file
|
||||||
export const PageHeaderMenuButton = ({
|
export const PageHeaderMenuButton = ({
|
||||||
rename,
|
rename,
|
||||||
pageId,
|
page,
|
||||||
isJournal,
|
isJournal,
|
||||||
}: PageMenuProps) => {
|
}: PageMenuProps) => {
|
||||||
|
const pageId = page?.id;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const { hideShare } = useDetailPageHeaderResponsive();
|
||||||
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
@@ -127,8 +136,46 @@ export const PageHeaderMenuButton = ({
|
|||||||
}
|
}
|
||||||
}, [importFile]);
|
}, [importFile]);
|
||||||
|
|
||||||
|
const showResponsiveMenu = hideShare;
|
||||||
|
const ResponsiveMenuItems = (
|
||||||
|
<>
|
||||||
|
{hideShare ? (
|
||||||
|
<MenuSub
|
||||||
|
subContentOptions={{
|
||||||
|
sideOffset: 12,
|
||||||
|
alignOffset: -8,
|
||||||
|
}}
|
||||||
|
items={
|
||||||
|
<div style={{ padding: 4 }}>
|
||||||
|
<ShareMenuContent
|
||||||
|
workspaceMetadata={workspace.meta}
|
||||||
|
currentPage={page}
|
||||||
|
onEnableAffineCloud={() =>
|
||||||
|
confirmEnableCloud(workspace, {
|
||||||
|
openPageId: page.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
triggerOptions={{
|
||||||
|
preFix: (
|
||||||
|
<MenuIcon>
|
||||||
|
<ShareIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t['com.affine.share-menu.shareButton']()}
|
||||||
|
</MenuSub>
|
||||||
|
) : null}
|
||||||
|
<MenuSeparator />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const EditMenu = (
|
const EditMenu = (
|
||||||
<>
|
<>
|
||||||
|
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
||||||
{!isJournal && (
|
{!isJournal && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
preFix={
|
preFix={
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IconButton } from '@affine/component';
|
||||||
|
import { PresentationIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
|
import { usePresent } from './use-present';
|
||||||
|
|
||||||
|
export const DetailPageHeaderPresentButton = () => {
|
||||||
|
const { isPresent, handlePresent } = usePresent();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
size={'large'}
|
||||||
|
icon={<PresentationIcon />}
|
||||||
|
onClick={() => handlePresent(!isPresent)}
|
||||||
|
></IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||||
|
import type { EdgelessRootService } from '@blocksuite/blocks';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const usePresent = () => {
|
||||||
|
const [isPresent, setIsPresent] = useState(false);
|
||||||
|
const [editor] = useActiveBlocksuiteEditor();
|
||||||
|
|
||||||
|
const handlePresent = useCallback(
|
||||||
|
(enable = true) => {
|
||||||
|
isPresent;
|
||||||
|
const editorHost = editor?.host;
|
||||||
|
if (!editorHost) return;
|
||||||
|
|
||||||
|
// TODO: use surfaceService subAtom
|
||||||
|
const enterOrLeavePresentationMode = () => {
|
||||||
|
const edgelessRootService = editorHost.spec.getService(
|
||||||
|
'affine:page'
|
||||||
|
) as EdgelessRootService;
|
||||||
|
|
||||||
|
if (!edgelessRootService) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTool = edgelessRootService.tool.edgelessTool.type;
|
||||||
|
const isFrameNavigator = activeTool === 'frameNavigator';
|
||||||
|
if ((enable && isFrameNavigator) || (!enable && !isFrameNavigator))
|
||||||
|
return;
|
||||||
|
|
||||||
|
edgelessRootService.tool.setEdgelessTool({
|
||||||
|
type: enable ? 'frameNavigator' : 'default',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
enterOrLeavePresentationMode();
|
||||||
|
setIsPresent(enable);
|
||||||
|
},
|
||||||
|
[editor?.host, isPresent]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPresent) return;
|
||||||
|
|
||||||
|
const editorHost = editor?.host;
|
||||||
|
if (!editorHost) return;
|
||||||
|
|
||||||
|
const edgelessPage = editorHost?.querySelector('affine-edgeless-root');
|
||||||
|
if (!edgelessPage) return;
|
||||||
|
|
||||||
|
return edgelessPage.slots.edgelessToolUpdated.on(() => {
|
||||||
|
setIsPresent(edgelessPage.edgelessTool.type === 'frameNavigator');
|
||||||
|
}).dispose;
|
||||||
|
}, [editor?.host, isPresent]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPresent,
|
||||||
|
handlePresent,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@ export const StyledEditorModeSwitch = styled('div')<{
|
|||||||
background: showAlone
|
background: showAlone
|
||||||
? 'transparent'
|
? 'transparent'
|
||||||
: 'var(--affine-background-secondary-color)',
|
: 'var(--affine-background-secondary-color)',
|
||||||
borderRadius: '12px',
|
borderRadius: '8px',
|
||||||
...displayFlex('space-between', 'center'),
|
...displayFlex('space-between', 'center'),
|
||||||
padding: '4px 4px',
|
padding: '4px 4px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -23,7 +23,7 @@ export const StyledEditorModeSwitch = styled('div')<{
|
|||||||
height: '24px',
|
height: '24px',
|
||||||
background: 'var(--affine-background-primary-color)',
|
background: 'var(--affine-background-primary-color)',
|
||||||
boxShadow: 'var(--affine-shadow-1)',
|
boxShadow: 'var(--affine-shadow-1)',
|
||||||
borderRadius: '8px',
|
borderRadius: '4px',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
transform: `translateX(${switchLeft ? '0' : '32px'})`,
|
transform: `translateX(${switchLeft ? '0' : '32px'})`,
|
||||||
|
|||||||
@@ -1,60 +1,19 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { EdgelessRootService } from '@blocksuite/blocks';
|
|
||||||
import { PresentationIcon } from '@blocksuite/icons/rc';
|
import { PresentationIcon } from '@blocksuite/icons/rc';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
|
import { usePresent } from '../../blocksuite/block-suite-header/present/use-present';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const PresentButton = () => {
|
export const PresentButton = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [isPresent, setIsPresent] = useState(false);
|
const { isPresent, handlePresent } = usePresent();
|
||||||
const [editor] = useActiveBlocksuiteEditor();
|
|
||||||
|
|
||||||
const handlePresent = useCallback(() => {
|
|
||||||
const editorHost = editor?.host;
|
|
||||||
if (!editorHost || isPresent) return;
|
|
||||||
|
|
||||||
// TODO: use surfaceService subAtom
|
|
||||||
const enterPresentationMode = () => {
|
|
||||||
const edgelessRootService = editorHost.spec.getService(
|
|
||||||
'affine:page'
|
|
||||||
) as EdgelessRootService;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!edgelessRootService ||
|
|
||||||
edgelessRootService.tool.edgelessTool.type === 'frameNavigator'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
edgelessRootService.tool.setEdgelessTool({ type: 'frameNavigator' });
|
|
||||||
};
|
|
||||||
|
|
||||||
enterPresentationMode();
|
|
||||||
setIsPresent(true);
|
|
||||||
}, [editor?.host, isPresent]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPresent) return;
|
|
||||||
|
|
||||||
const editorHost = editor?.host;
|
|
||||||
if (!editorHost) return;
|
|
||||||
|
|
||||||
const edgelessPage = editorHost?.querySelector('affine-edgeless-root');
|
|
||||||
if (!edgelessPage) return;
|
|
||||||
|
|
||||||
return edgelessPage.slots.edgelessToolUpdated.on(() => {
|
|
||||||
setIsPresent(edgelessPage.edgelessTool.type === 'frameNavigator');
|
|
||||||
}).dispose;
|
|
||||||
}, [editor?.host, isPresent]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
icon={<PresentationIcon />}
|
icon={<PresentationIcon />}
|
||||||
className={styles.presentButton}
|
className={styles.presentButton}
|
||||||
onClick={handlePresent}
|
onClick={() => handlePresent()}
|
||||||
disabled={isPresent}
|
disabled={isPresent}
|
||||||
withoutHoverStyle
|
withoutHoverStyle
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { registerAffineCommand } from '@affine/core/commands';
|
import { registerAffineCommand } from '@affine/core/commands';
|
||||||
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
|
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
|
||||||
|
import { useIsActiveView } from '@affine/core/modules/workbench';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export function useRegisterCopyLinkCommands({
|
export function useRegisterCopyLinkCommands({
|
||||||
workspaceId,
|
workspaceMeta,
|
||||||
docId,
|
docId,
|
||||||
isActiveView,
|
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceMeta: WorkspaceMetadata;
|
||||||
docId: string;
|
docId: string;
|
||||||
isActiveView: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
|
const isActiveView = useIsActiveView();
|
||||||
|
const workspaceId = workspaceMeta.id;
|
||||||
|
const isCloud = workspaceMeta.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||||
const { onClickCopyLink } = useSharingUrl({
|
const { onClickCopyLink } = useSharingUrl({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
pageId: docId,
|
pageId: docId,
|
||||||
@@ -31,12 +35,12 @@ export function useRegisterCopyLinkCommands({
|
|||||||
label: '',
|
label: '',
|
||||||
icon: null,
|
icon: null,
|
||||||
run() {
|
run() {
|
||||||
isActiveView && onClickCopyLink();
|
isActiveView && isCloud && onClickCopyLink();
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
unsubs.forEach(unsub => unsub());
|
unsubs.forEach(unsub => unsub());
|
||||||
};
|
};
|
||||||
}, [docId, isActiveView, onClickCopyLink]);
|
}, [docId, isActiveView, isCloud, onClickCopyLink]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export class View extends Entity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
size$ = new LiveData(100);
|
size$ = new LiveData(100);
|
||||||
|
/** Width of header content in px (excludes sidebar-toggle/windows button/...) */
|
||||||
|
headerContentWidth$ = new LiveData(1920);
|
||||||
|
|
||||||
header = createIsland();
|
header = createIsland();
|
||||||
body = createIsland();
|
body = createIsland();
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const rightSidebarButton = style({
|
|||||||
export const viewHeaderContainer = style({
|
export const viewHeaderContainer = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
width: 0,
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
minWidth: 12,
|
minWidth: 12,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { IconButton, observeResize } from '@affine/component';
|
||||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||||
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Suspense, useCallback } from 'react';
|
import { Suspense, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||||
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
||||||
@@ -41,6 +41,7 @@ const ToggleButton = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RouteContainer = ({ route }: Props) => {
|
export const RouteContainer = ({ route }: Props) => {
|
||||||
|
const viewHeaderContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const view = useService(ViewService).view;
|
const view = useService(ViewService).view;
|
||||||
const viewPosition = useViewPosition();
|
const viewPosition = useViewPosition();
|
||||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||||
@@ -51,6 +52,14 @@ export const RouteContainer = ({ route }: Props) => {
|
|||||||
rightSidebar.toggle();
|
rightSidebar.toggle();
|
||||||
}, [rightSidebar]);
|
}, [rightSidebar]);
|
||||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = viewHeaderContainerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
return observeResize(container, entry => {
|
||||||
|
view.headerContentWidth$.next(entry.contentRect.width);
|
||||||
|
});
|
||||||
|
}, [view.headerContentWidth$]);
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -60,7 +69,10 @@ export const RouteContainer = ({ route }: Props) => {
|
|||||||
className={styles.leftSidebarButton}
|
className={styles.leftSidebarButton}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<view.header.Target className={styles.viewHeaderContainer} />
|
<view.header.Target
|
||||||
|
ref={viewHeaderContainerRef}
|
||||||
|
className={styles.viewHeaderContainer}
|
||||||
|
/>
|
||||||
{viewPosition.isLast && (
|
{viewPosition.isLast && (
|
||||||
<>
|
<>
|
||||||
{rightSidebarHasViews && (
|
{rightSidebarHasViews && (
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { InlineEditHandle } from '@affine/component';
|
import { Divider, type InlineEditHandle } from '@affine/component';
|
||||||
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
||||||
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
||||||
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
||||||
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-suite-header/menu';
|
||||||
|
import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuite/block-suite-header/present/detail-header-present-button';
|
||||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||||
|
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
|
||||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||||
import type { Doc } from '@blocksuite/store';
|
import type { Doc } from '@blocksuite/store';
|
||||||
import type { Workspace } from '@toeverything/infra';
|
import { type Workspace } from '@toeverything/infra';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ import { appSidebarFloatingAtom } from '../../../components/app-sidebar';
|
|||||||
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
|
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
|
||||||
import { HeaderDivider } from '../../../components/pure/header';
|
import { HeaderDivider } from '../../../components/pure/header';
|
||||||
import * as styles from './detail-page-header.css';
|
import * as styles from './detail-page-header.css';
|
||||||
|
import { useDetailPageHeaderResponsive } from './use-header-responsive';
|
||||||
|
|
||||||
function Header({
|
function Header({
|
||||||
children,
|
children,
|
||||||
@@ -43,6 +46,7 @@ interface PageHeaderProps {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}
|
}
|
||||||
export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||||
|
const { hideShare, hideToday } = useDetailPageHeaderResponsive();
|
||||||
return (
|
return (
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
<EditorModeSwitch
|
<EditorModeSwitch
|
||||||
@@ -55,11 +59,13 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
page={page}
|
page={page}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<JournalTodayButton docCollection={workspace.docCollection} />
|
{hideToday ? null : (
|
||||||
|
<JournalTodayButton docCollection={workspace.docCollection} />
|
||||||
|
)}
|
||||||
<HeaderDivider />
|
<HeaderDivider />
|
||||||
<PageHeaderMenuButton isJournal pageId={page?.id} />
|
<PageHeaderMenuButton isJournal page={page} />
|
||||||
{page ? (
|
{page && !hideShare ? (
|
||||||
<SharePageButton isJournal workspace={workspace} page={page} />
|
<SharePageButton workspace={workspace} page={page} />
|
||||||
) : null}
|
) : null}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
@@ -67,6 +73,8 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
|
|
||||||
export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||||
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
||||||
|
const { hideCollect, hideShare, hidePresent, showDivider } =
|
||||||
|
useDetailPageHeaderResponsive();
|
||||||
|
|
||||||
const onRename = useCallback(() => {
|
const onRename = useCallback(() => {
|
||||||
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
||||||
@@ -82,19 +90,33 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
pageId={page?.id}
|
pageId={page?.id}
|
||||||
docCollection={workspace.docCollection}
|
docCollection={workspace.docCollection}
|
||||||
/>
|
/>
|
||||||
<PageHeaderMenuButton rename={onRename} pageId={page?.id} />
|
{hideCollect ? null : <FavoriteButton pageId={page?.id} />}
|
||||||
<FavoriteButton pageId={page?.id} />
|
<PageHeaderMenuButton rename={onRename} page={page} />
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
{page ? <SharePageButton workspace={workspace} page={page} /> : null}
|
|
||||||
|
{!hidePresent ? <DetailPageHeaderPresentButton /> : null}
|
||||||
|
|
||||||
|
{page && !hideShare ? (
|
||||||
|
<SharePageButton workspace={workspace} page={page} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showDivider ? (
|
||||||
|
<Divider orientation="vertical" style={{ height: 20, marginLeft: 4 }} />
|
||||||
|
) : null}
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailPageHeader(props: PageHeaderProps) {
|
export function DetailPageHeader(props: PageHeaderProps) {
|
||||||
const { page } = props;
|
const { page, workspace } = props;
|
||||||
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
|
const { isJournal } = useJournalInfoHelper(page.collection, page.id);
|
||||||
const isInTrash = page.meta?.trash;
|
const isInTrash = page.meta?.trash;
|
||||||
|
|
||||||
|
useRegisterCopyLinkCommands({
|
||||||
|
workspaceMeta: workspace.meta,
|
||||||
|
docId: page.id,
|
||||||
|
});
|
||||||
|
|
||||||
return isJournal && !isInTrash ? (
|
return isJournal && !isInTrash ? (
|
||||||
<JournalPageHeader {...props} />
|
<JournalPageHeader {...props} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { RightSidebarService } from '@affine/core/modules/right-sidebar';
|
||||||
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
|
import { useViewPosition } from '@affine/core/modules/workbench/view/use-view-position';
|
||||||
|
import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export const useDetailPageHeaderResponsive = () => {
|
||||||
|
const mode = useLiveData(useService(DocService).doc.mode$);
|
||||||
|
|
||||||
|
const view = useService(ViewService).view;
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const availableWidth = useLiveData(view.headerContentWidth$);
|
||||||
|
const viewPosition = useViewPosition();
|
||||||
|
const workbenchViewsCount = useLiveData(
|
||||||
|
workbench.views$.map(views => views.length)
|
||||||
|
);
|
||||||
|
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
||||||
|
const rightSidebarOpen = useLiveData(rightSidebar.isOpen$);
|
||||||
|
|
||||||
|
// share button should be hidden once split-view is enabled
|
||||||
|
const hideShare = availableWidth < 500 || workbenchViewsCount > 1;
|
||||||
|
const hidePresent = availableWidth < 400 || mode !== 'edgeless';
|
||||||
|
const hideCollect = availableWidth < 300;
|
||||||
|
const hideToday = availableWidth < 300;
|
||||||
|
|
||||||
|
const showDivider =
|
||||||
|
viewPosition.isLast && !rightSidebarOpen && !(hidePresent && hideShare);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hideShare,
|
||||||
|
hidePresent,
|
||||||
|
hideCollect,
|
||||||
|
hideToday,
|
||||||
|
showDivider,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { LiveData, useLiveData } from '@toeverything/infra';
|
import { LiveData, useLiveData } from '@toeverything/infra';
|
||||||
import { useEffect, useRef } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
type Ref,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
export const createIsland = () => {
|
export const createIsland = () => {
|
||||||
@@ -7,8 +13,13 @@ export const createIsland = () => {
|
|||||||
let mounted = false;
|
let mounted = false;
|
||||||
let provided = false;
|
let provided = false;
|
||||||
return {
|
return {
|
||||||
Target: ({ ...other }: React.HTMLProps<HTMLDivElement>) => {
|
Target: forwardRef(function IslandTarget(
|
||||||
|
{ ...other }: React.HTMLProps<HTMLDivElement>,
|
||||||
|
ref: Ref<HTMLDivElement>
|
||||||
|
) {
|
||||||
const target = useRef<HTMLDivElement | null>(null);
|
const target = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => target.current as HTMLDivElement, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mounted === true) {
|
if (mounted === true) {
|
||||||
throw new Error('Island should not be mounted more than once');
|
throw new Error('Island should not be mounted more than once');
|
||||||
@@ -21,7 +32,7 @@ export const createIsland = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return <div {...other} ref={target}></div>;
|
return <div {...other} ref={target}></div>;
|
||||||
},
|
}),
|
||||||
Provider: ({ children }: React.PropsWithChildren) => {
|
Provider: ({ children }: React.PropsWithChildren) => {
|
||||||
const target = useLiveData(targetLiveData$);
|
const target = useLiveData(targetLiveData$);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user