mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 10:36:22 +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 = {
|
||||
workspace: Workspace;
|
||||
page: Doc;
|
||||
isJournal?: boolean;
|
||||
};
|
||||
|
||||
export const SharePageButton = ({
|
||||
workspace,
|
||||
page,
|
||||
isJournal,
|
||||
}: SharePageModalProps) => {
|
||||
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
return (
|
||||
<ShareMenu
|
||||
isJournal={isJournal}
|
||||
workspaceMetadata={workspace.meta}
|
||||
currentPage={page}
|
||||
onEnableAffineCloud={() =>
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ globalStyle(`${shareLinkStyle} > span`, {
|
||||
globalStyle(`${shareLinkStyle} > div > svg`, {
|
||||
color: cssVar('linkColor'),
|
||||
});
|
||||
export const journalShareButton = style({
|
||||
export const shareButton = style({
|
||||
height: 32,
|
||||
padding: '0px 8px',
|
||||
});
|
||||
|
||||
+22
-30
@@ -1,27 +1,24 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
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 { useI18n } from '@affine/i18n';
|
||||
import { WebIcon } from '@blocksuite/icons/rc';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
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 { ShareExport } from './share-export';
|
||||
import { SharePage } from './share-page';
|
||||
|
||||
export interface ShareMenuProps {
|
||||
export interface ShareMenuProps extends PropsWithChildren {
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
currentPage: Doc;
|
||||
isJournal?: boolean;
|
||||
onEnableAffineCloud: () => void;
|
||||
}
|
||||
|
||||
const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
export const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<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();
|
||||
|
||||
return (
|
||||
<Button ref={ref} className={styles.shareButton} type="primary">
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
return (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
@@ -53,27 +62,14 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
||||
data-testid="local-share-menu-button"
|
||||
type="primary"
|
||||
>
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
<div data-testid="local-share-menu-button">
|
||||
{props.children || <DefaultShareButton />}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
@@ -85,13 +81,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={clsx({ [styles.journalShareButton]: props.isJournal })}
|
||||
data-testid="cloud-share-menu-button"
|
||||
type="primary"
|
||||
>
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
<div data-testid="cloud-share-menu-button">
|
||||
{props.children || <DefaultShareButton />}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,18 @@ import {
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
MenuSub,
|
||||
} from '@affine/component/ui/menu';
|
||||
import { openHistoryTipsModalAtom } from '@affine/core/atoms';
|
||||
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 { 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 { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||
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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -24,7 +28,9 @@ import {
|
||||
HistoryIcon,
|
||||
ImportIcon,
|
||||
PageIcon,
|
||||
ShareIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import {
|
||||
DocService,
|
||||
useLiveData,
|
||||
@@ -40,16 +46,19 @@ import { useFavorite } from '../favorite';
|
||||
|
||||
type PageMenuProps = {
|
||||
rename?: () => void;
|
||||
pageId: string;
|
||||
page: Doc;
|
||||
isJournal?: boolean;
|
||||
};
|
||||
// fixme: refactor this file
|
||||
export const PageHeaderMenuButton = ({
|
||||
rename,
|
||||
pageId,
|
||||
page,
|
||||
isJournal,
|
||||
}: PageMenuProps) => {
|
||||
const pageId = page?.id;
|
||||
const t = useI18n();
|
||||
const { hideShare } = useDetailPageHeaderResponsive();
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
@@ -127,8 +136,46 @@ export const PageHeaderMenuButton = ({
|
||||
}
|
||||
}, [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 = (
|
||||
<>
|
||||
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
||||
{!isJournal && (
|
||||
<MenuItem
|
||||
preFix={
|
||||
|
||||
+17
@@ -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>
|
||||
);
|
||||
};
|
||||
+59
@@ -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
|
||||
? 'transparent'
|
||||
: 'var(--affine-background-secondary-color)',
|
||||
borderRadius: '12px',
|
||||
borderRadius: '8px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
padding: '4px 4px',
|
||||
position: 'relative',
|
||||
@@ -23,7 +23,7 @@ export const StyledEditorModeSwitch = styled('div')<{
|
||||
height: '24px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
borderRadius: '8px',
|
||||
borderRadius: '4px',
|
||||
zIndex: 1,
|
||||
position: 'absolute',
|
||||
transform: `translateX(${switchLeft ? '0' : '32px'})`,
|
||||
|
||||
@@ -1,60 +1,19 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { EdgelessRootService } from '@blocksuite/blocks';
|
||||
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';
|
||||
|
||||
export const PresentButton = () => {
|
||||
const t = useI18n();
|
||||
const [isPresent, setIsPresent] = useState(false);
|
||||
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]);
|
||||
const { isPresent, handlePresent } = usePresent();
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<PresentationIcon />}
|
||||
className={styles.presentButton}
|
||||
onClick={handlePresent}
|
||||
onClick={() => handlePresent()}
|
||||
disabled={isPresent}
|
||||
withoutHoverStyle
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user