feat: move theme switch and language switch to editor option menu (#2025)

Co-authored-by: himself65 <himself65@outlook.com>
This commit is contained in:
JimmFly
2023-04-28 17:28:51 +08:00
committed by GitHub
parent 903b6eaf30
commit 2ff5ef9d5d
12 changed files with 347 additions and 193 deletions

View File

@@ -14,6 +14,7 @@ import {
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtom } from 'jotai';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
@@ -22,10 +23,41 @@ import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id'
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { toast } from '../../../../utils';
import { Export, MoveToTrash } from '../../../affine/operation-menu-items';
export const EditorOptionMenu = () => {
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
import {
StyledHorizontalDivider,
StyledHorizontalDividerContainer,
} from '../styles';
import { LanguageMenu } from './LanguageMenu';
const CommonMenu = () => {
const content = (
<div
onClick={e => {
e.stopPropagation();
}}
>
<MenuThemeModeSwitch />
<LanguageMenu />
</div>
);
return (
<FlexWrapper alignItems="center" justifyContent="center">
<Menu
width={276}
content={content}
// placement="bottom-end"
disablePortal={true}
trigger="click"
>
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<MoreVerticalIcon />
</IconButton>
</Menu>
</FlexWrapper>
);
};
const PageMenu = () => {
const { t } = useTranslation();
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
const [pageId] = useCurrentPageId();
@@ -35,55 +67,70 @@ export const EditorOptionMenu = () => {
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
assertExists(pageMeta);
const [record, set] = useAtom(workspacePreferredModeAtom);
const mode = record[pageId] ?? 'page';
assertExists(pageMeta);
const { favorite } = pageMeta;
const favorite = pageMeta.favorite ?? false;
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [openConfirm, setOpenConfirm] = useState(false);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const EditMenu = (
<>
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={() => {
setPageMeta(pageId, { favorite: !favorite });
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
{favorite ? t('Remove from favorites') : t('Add to Favorites')}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={() => {
set(record => ({
...record,
[pageId]: mode === 'page' ? 'edgeless' : 'page',
}));
}}
>
{t('Convert to ')}
{mode === 'page' ? t('Edgeless') : t('Page')}
</MenuItem>
<Export />
{!pageMeta.isRootPinboard && (
<MoveToTrash
testId="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
<>
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={() => {
setPageMeta(pageId, { favorite: !favorite });
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
/>
)}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
{favorite ? t('Remove from favorites') : t('Add to Favorites')}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={() => {
set(record => ({
...record,
[pageId]: mode === 'page' ? 'edgeless' : 'page',
}));
}}
>
{t('Convert to ')}
{mode === 'page' ? t('Edgeless') : t('Page')}
</MenuItem>
<Export />
{!pageMeta.isRootPinboard && (
<MoveToTrash
testId="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
)}
<StyledHorizontalDividerContainer>
<StyledHorizontalDivider />
</StyledHorizontalDividerContainer>
</>
<div
onClick={e => {
e.stopPropagation();
}}
>
<MenuThemeModeSwitch />
<LanguageMenu />
</div>
</>
);
@@ -116,3 +163,7 @@ export const EditorOptionMenu = () => {
</>
);
};
export const EditorOptionMenu = () => {
const router = useRouter();
return router.query.pageId ? <PageMenu /> : <CommonMenu />;
};

View File

@@ -0,0 +1,133 @@
import { Button, displayFlex, Menu, MenuItem, styled } from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useTranslation } from '@affine/i18n';
import { ArrowDownSmallIcon, PublishIcon } from '@blocksuite/icons';
import type { FC, ReactElement } from 'react';
import { useCallback } from 'react';
const LanguageMenuContent: FC = () => {
const { i18n } = useTranslation();
const changeLanguage = useCallback(
(event: string) => {
i18n.changeLanguage(event);
},
[i18n]
);
return (
<>
{LOCALES.map(option => {
return (
<StyledListItem
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
}}
>
{option.originalName}
</StyledListItem>
);
})}
</>
);
};
export const LanguageMenu: React.FC = () => {
const { i18n } = useTranslation();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
return (
<StyledContainer>
<StyledIconContainer>
<PublishIcon />
</StyledIconContainer>
<StyledButtonContainer>
<Menu
content={(<LanguageMenuContent />) as ReactElement}
placement="bottom"
trigger="click"
disablePortal={true}
>
<StyledButton
icon={
<StyledArrowDownContainer>
<ArrowDownSmallIcon />
</StyledArrowDownContainer>
}
iconPosition="end"
noBorder={true}
data-testid="language-menu-button"
>
<StyledCurrentLanguage>
{currentLanguage?.originalName}
</StyledCurrentLanguage>
</StyledButton>
</Menu>
</StyledButtonContainer>
</StyledContainer>
);
};
const StyledListItem = styled(MenuItem)(() => ({
width: '132px',
height: '38px',
fontSize: 'var(--affine-font-base)',
textTransform: 'capitalize',
}));
const StyledContainer = styled('div')(() => {
return {
width: '100%',
height: '48px',
backgroundColor: 'transparent',
...displayFlex('flex-start', 'center'),
padding: '0 14px',
};
});
const StyledIconContainer = styled('div')(() => {
return {
width: '20px',
height: '20px',
color: 'var(--affine-icon-color)',
fontSize: '20px',
...displayFlex('flex-start', 'center'),
};
});
const StyledButtonContainer = styled('div')(() => {
return {
width: '100%',
height: '32px',
borderRadius: '4px',
border: `1px solid var(--affine-border-color)`,
backgroundColor: 'transparent',
...displayFlex('flex-start', 'center'),
marginLeft: '12px',
};
});
const StyledButton = styled(Button)(() => {
return {
width: '100%',
height: '32px',
borderRadius: '4px',
backgroundColor: 'transparent',
...displayFlex('space-between', 'center'),
textTransform: 'capitalize',
padding: '0',
};
});
const StyledArrowDownContainer = styled('div')(() => {
return {
height: '32px',
borderLeft: `1px solid var(--affine-border-color)`,
backgroundColor: 'transparent',
...displayFlex('flex-start', 'center'),
padding: '4px 6px',
fontSize: '24px',
};
});
const StyledCurrentLanguage = styled('div')(() => {
return {
marginLeft: '12px',
color: 'var(--affine-text-color)',
};
});

View File

@@ -1,50 +1,65 @@
import { DarkModeIcon, LightModeIcon } from '@blocksuite/icons';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { StyledSwitchItem, StyledThemeModeSwitch } from './style';
export const ThemeModeSwitch = () => {
const { setTheme, resolvedTheme } = useTheme();
import {
StyledSwitchItem,
StyledThemeButton,
StyledThemeButtonContainer,
StyledThemeModeContainer,
StyledThemeModeSwitch,
StyledVerticalDivider,
} from './style';
export const MenuThemeModeSwitch = () => {
const { setTheme, resolvedTheme, theme } = useTheme();
useEffect(() => {
if (environment.isDesktop) {
window.apis?.onThemeChange(resolvedTheme === 'dark' ? 'dark' : 'light');
}
}, [resolvedTheme]);
const [isHover, setIsHover] = useState(false);
return (
<StyledThemeModeSwitch
data-testid="change-theme-container"
onMouseEnter={() => {
setIsHover(true);
}}
onMouseLeave={() => {
setIsHover(false);
}}
>
<StyledSwitchItem
data-testid="change-theme-light"
active={resolvedTheme === 'light'}
isHover={isHover}
onClick={() => {
setTheme('light');
}}
>
<LightModeIcon />
</StyledSwitchItem>
<StyledSwitchItem
data-testid="change-theme-dark"
active={resolvedTheme === 'dark'}
isHover={isHover}
onClick={() => {
setTheme('dark');
}}
>
<DarkModeIcon />
</StyledSwitchItem>
</StyledThemeModeSwitch>
<StyledThemeModeContainer>
<StyledThemeModeSwitch data-testid="change-theme-container" inMenu={true}>
<StyledSwitchItem active={resolvedTheme === 'light'} inMenu={true}>
<LightModeIcon />
</StyledSwitchItem>
<StyledSwitchItem active={resolvedTheme === 'dark'} inMenu={true}>
<DarkModeIcon />
</StyledSwitchItem>
</StyledThemeModeSwitch>
<StyledThemeButtonContainer>
<StyledThemeButton
data-testid="change-theme-light"
active={theme === 'light'}
onClick={() => {
setTheme('light');
}}
>
light
</StyledThemeButton>
<StyledVerticalDivider />
<StyledThemeButton
data-testid="change-theme-dark"
active={theme === 'dark'}
onClick={() => {
setTheme('dark');
}}
>
dark
</StyledThemeButton>
<StyledVerticalDivider />
<StyledThemeButton
active={theme === 'system'}
onClick={() => {
setTheme('system');
}}
>
system
</StyledThemeButton>
</StyledThemeButtonContainer>
</StyledThemeModeContainer>
);
};
export default ThemeModeSwitch;
export default MenuThemeModeSwitch;

View File

@@ -3,24 +3,63 @@ import { css, displayFlex, keyframes, styled } from '@affine/component';
import spring, { toString } from 'css-spring';
const ANIMATE_DURATION = 400;
export const StyledThemeModeSwitch = styled('button')(() => {
export const StyledThemeModeContainer = styled('div')(() => {
return {
width: '32px',
width: '100%',
height: '48px',
borderRadius: '6px',
backgroundColor: 'transparent',
color: 'var(--affine-icon-color)',
fontSize: '16px',
...displayFlex('flex-start', 'center'),
padding: '0 14px',
};
});
export const StyledThemeButtonContainer = styled('div')(() => {
return {
border: `1px solid var(--affine-border-color)`,
borderRadius: '4px',
cursor: 'pointer',
...displayFlex('space-evenly', 'center'),
flexGrow: 1,
marginLeft: '12px',
};
});
export const StyledThemeButton = styled('button')<{
active: boolean;
}>(({ active }) => {
return {
cursor: 'pointer',
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
};
});
export const StyledVerticalDivider = styled('div')(() => {
return {
width: '1px',
height: '32px',
borderLeft: `1px solid var(--affine-border-color)`,
};
});
export const StyledThemeModeSwitch = styled('button')<{
inMenu?: boolean;
}>(({ inMenu }) => {
return {
width: inMenu ? '20px' : '32px',
height: inMenu ? '20px' : '32px',
borderRadius: '6px',
overflow: 'hidden',
WebkitAppRegion: 'no-drag',
backgroundColor: 'transparent',
position: 'relative',
color: 'var(--affine-icon-color)',
fontSize: '24px',
fontSize: inMenu ? '20px' : '24px',
};
});
export const StyledSwitchItem = styled('div')<{
active: boolean;
isHover: boolean;
}>(({ active, isHover }) => {
isHover?: boolean;
inMenu?: boolean;
}>(({ active, isHover, inMenu }) => {
const activeRaiseAnimate = toString(
spring({ top: '0' }, { top: '-100%' }, { preset: 'gentle' })
);
@@ -58,8 +97,8 @@ export const StyledSwitchItem = styled('div')<{
};
return css`
${css(displayFlex('center', 'center'))}
width: 32px;
height: 32px;
width:${inMenu ? '20px' : '32px'} ;
height: ${inMenu ? '20px' : '32px'} ;
position: absolute;
left: 0;
cursor: pointer;

View File

@@ -19,7 +19,6 @@ import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
import EditPage from './header-right-items/EditPage';
import { HeaderShareMenu } from './header-right-items/ShareMenu';
import SyncUser from './header-right-items/SyncUser';
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import UserAvatar from './header-right-items/UserAvatar';
import {
@@ -66,7 +65,6 @@ export type BaseHeaderProps<
export const enum HeaderRightItemName {
EditorOptionMenu = 'editorOptionMenu',
TrashButtonGroup = 'trashButtonGroup',
ThemeModeSwitch = 'themeModeSwitch',
SyncUser = 'syncUser',
ShareMenu = 'shareMenu',
EditPage = 'editPage',
@@ -98,12 +96,6 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
return !isPublic && !isPreview;
},
},
[HeaderRightItemName.ThemeModeSwitch]: {
Component: ThemeModeSwitch,
availableWhen: (_, currentPage) => {
return currentPage?.meta.trash !== true;
},
},
[HeaderRightItemName.ShareMenu]: {
Component: HeaderShareMenu,
availableWhen: (workspace, currentPage) => {
@@ -125,7 +117,7 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
[HeaderRightItemName.EditorOptionMenu]: {
Component: EditorOptionMenu,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return !!currentPage && !isPublic && !isPreview;
return !isPublic && !isPreview;
},
},
};

View File

@@ -167,3 +167,16 @@ export const StyledQuickSearchTipContent = styled('div')(() => {
flexDirection: 'column',
};
});
export const StyledHorizontalDivider = styled('div')(() => {
return {
width: '100%',
borderTop: `1px solid var(--affine-border-color)`,
};
});
export const StyledHorizontalDividerContainer = styled('div')(() => {
return {
width: '100%',
padding: '14px',
};
});

View File

@@ -15,7 +15,6 @@ import { useCallback } from 'react';
import type { AllWorkspace } from '../../../shared';
import { Footer } from '../footer';
import { LanguageMenu } from './language-menu';
import {
StyledCreateWorkspaceCard,
StyledHelperContainer,
@@ -24,7 +23,6 @@ import {
StyledModalHeaderLeft,
StyledModalTitle,
StyledOperationWrapper,
StyledSplitLine,
StyleWorkspaceAdd,
StyleWorkspaceInfo,
StyleWorkspaceTitle,
@@ -86,8 +84,6 @@ export const WorkspaceListModal = ({
</StyledModalHeaderLeft>
<StyledOperationWrapper>
<LanguageMenu />
<StyledSplitLine />
<ModalCloseButton
data-testid="close-workspace-modal"
onClick={() => {

View File

@@ -1,64 +0,0 @@
import { Button, Menu, MenuItem, styled } from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useTranslation } from '@affine/i18n';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import type { FC, ReactElement } from 'react';
import { useCallback } from 'react';
const LanguageMenuContent: FC = () => {
const { i18n } = useTranslation();
const changeLanguage = useCallback(
(event: string) => {
i18n.changeLanguage(event);
},
[i18n]
);
return (
<>
{LOCALES.map(option => {
return (
<ListItem
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
}}
>
{option.originalName}
</ListItem>
);
})}
</>
);
};
export const LanguageMenu: React.FC = () => {
const { i18n } = useTranslation();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
return (
<Menu
content={(<LanguageMenuContent />) as ReactElement}
placement="bottom"
trigger="click"
disablePortal={true}
>
<Button
icon={<ArrowDownSmallIcon />}
iconPosition="end"
noBorder={true}
style={{ textTransform: 'capitalize', padding: '0 12px' }}
data-testid="language-menu-button"
>
{currentLanguage?.originalName}
</Button>
</Menu>
);
};
const ListItem = styled(MenuItem)(() => ({
height: '38px',
fontSize: 'var(--affine-font-base)',
textTransform: 'capitalize',
padding: '0 24px',
}));