feat: new setting modal (#2834)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Qi
2023-06-21 19:57:59 +08:00
committed by GitHub
parent 9a90ce694c
commit aa86d3a2ee
64 changed files with 1911 additions and 159 deletions

View File

@@ -22,7 +22,7 @@
"@blocksuite/blocks": "0.0.0-20230607055421-9b20fcaf-nightly",
"@blocksuite/editor": "0.0.0-20230607055421-9b20fcaf-nightly",
"@blocksuite/global": "0.0.0-20230607055421-9b20fcaf-nightly",
"@blocksuite/icons": "^2.1.19",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/lit": "0.0.0-20230607055421-9b20fcaf-nightly",
"@blocksuite/store": "0.0.0-20230607055421-9b20fcaf-nightly",
"@dnd-kit/core": "^6.0.8",

View File

@@ -46,4 +46,8 @@ export const buildFlags = {
process.env.ENABLE_PRELOADING === undefined
? true
: process.env.ENABLE_PRELOADING === 'true',
enableNewSettingModal:
process.env.ENABLE_NEW_SETTING_MODAL === undefined
? true
: process.env.ENABLE_PRELOADING === 'true',
};

View File

@@ -44,6 +44,7 @@ import { BlockSuiteWorkspace } from '../../shared';
import { toast } from '../../utils';
import {
BlockSuitePageList,
NewWorkspaceSettingDetail,
PageDetailEditor,
WorkspaceHeader,
WorkspaceSettingDetail,
@@ -364,5 +365,18 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
/>
);
},
NewSettingsDetail: ({
currentWorkspace,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<NewWorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
},
};

View File

@@ -21,6 +21,7 @@ import { nanoid } from '@blocksuite/store';
import {
BlockSuitePageList,
NewWorkspaceSettingDetail,
PageDetailEditor,
WorkspaceHeader,
WorkspaceSettingDetail,
@@ -115,5 +116,18 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
/>
);
},
NewSettingsDetail: ({
currentWorkspace,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<NewWorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
},
};

View File

@@ -1,5 +1,5 @@
import { lazy } from 'react';
// export { WorkspaceSettingDetail as NewWorkspaceSettingDetail } from '../components/affine/new-workspace-setting-detail';
export const WorkspaceSettingDetail = lazy(() =>
import('../components/affine/workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({
@@ -7,6 +7,13 @@ export const WorkspaceSettingDetail = lazy(() =>
})
)
);
export const NewWorkspaceSettingDetail = lazy(() =>
import('../components/affine/new-workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({
default: WorkspaceSettingDetail,
})
)
);
export const BlockSuitePageList = lazy(() =>
import('../components/blocksuite/block-suite-page-list').then(
({ BlockSuitePageList }) => ({

View File

@@ -36,6 +36,7 @@ export const WorkspaceAdapters = {
PageDetail: unimplemented,
PageList: unimplemented,
SettingsDetail: unimplemented,
NewSettingsDetail: unimplemented,
},
},
[WorkspaceFlavour.PUBLIC]: {
@@ -57,6 +58,7 @@ export const WorkspaceAdapters = {
PageDetail: unimplemented,
PageList: unimplemented,
SettingsDetail: unimplemented,
NewSettingsDetail: unimplemented,
},
},
} satisfies {

View File

@@ -80,6 +80,7 @@ export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
export const openQuickSearchModalAtom = atom(false);
export const openOnboardingModalAtom = atom(false);
export const openSettingModalAtom = atom(false);
export const openDisableCloudAlertModalAtom = atom(false);

View File

@@ -0,0 +1,67 @@
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useCallback } from 'react';
export type DateFormats =
| 'MM/dd/YYYY'
| 'dd/MM/YYYY'
| 'YYYY-MM-dd'
| 'YYYY.MM.dd'
| 'YYYY/MM/dd'
| 'dd-MMM-YYYY'
| 'dd MMMM YYYY';
export type AppSetting = {
clientBorder: boolean;
fullWidthLayout: boolean;
windowFrameStyle: 'frameless' | 'NativeTitleBar';
dateFormat: DateFormats;
startWeekOnMonday: boolean;
disableBlurBackground: boolean;
disableNoisyBackground: boolean;
autoCheckUpdate: boolean;
autoDownloadUpdate: boolean;
};
export const windowFrameStyleOptions: AppSetting['windowFrameStyle'][] = [
'frameless',
'NativeTitleBar',
];
export const dateFormatOptions: DateFormats[] = [
'MM/dd/YYYY',
'dd/MM/YYYY',
'YYYY-MM-dd',
'YYYY.MM.dd',
'YYYY/MM/dd',
'dd-MMM-YYYY',
'dd MMMM YYYY',
];
export const AppSettingAtom = atomWithStorage<AppSetting>('AFFiNE settings', {
clientBorder: false,
fullWidthLayout: false,
windowFrameStyle: 'frameless',
dateFormat: dateFormatOptions[0],
startWeekOnMonday: false,
disableBlurBackground: false,
disableNoisyBackground: false,
autoCheckUpdate: true,
autoDownloadUpdate: true,
});
export const useAppSetting = () => {
const [settings, setSettings] = useAtom(AppSettingAtom);
return [
settings,
useCallback(
(patch: Partial<AppSetting>) => {
setSettings((prev: AppSetting) => ({
...prev,
...patch,
}));
},
[setSettings]
),
] as const;
};

View File

@@ -0,0 +1,18 @@
import {
AppContainer as AppContainerWithoutSettings,
type WorkspaceRootProps,
} from '@affine/component/workspace';
import { useAppSetting } from '../../atoms/settings';
export const AppContainer = (props: WorkspaceRootProps) => {
const [appSettings] = useAppSetting();
return (
<AppContainerWithoutSettings
useNoisyBackground={!appSettings.disableNoisyBackground}
useBlurBackground={!appSettings.disableBlurBackground}
{...props}
/>
);
};

View File

@@ -0,0 +1,70 @@
import { Menu, MenuItem, MenuTrigger, styled } from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useI18N } from '@affine/i18n';
import type { FC, ReactElement } from 'react';
import { useCallback } from 'react';
export const StyledListItem = styled(MenuItem)(() => ({
width: '132px',
height: '38px',
textTransform: 'capitalize',
}));
const LanguageMenuContent: FC<{
currentLanguage?: string;
}> = ({ currentLanguage }) => {
const i18n = useI18N();
const changeLanguage = useCallback(
(event: string) => {
return i18n.changeLanguage(event);
},
[i18n]
);
return (
<>
{LOCALES.map(option => {
return (
<StyledListItem
key={option.name}
active={currentLanguage === option.originalName}
title={option.name}
onClick={() => {
changeLanguage(option.tag).catch(err => {
throw new Error('Failed to change language', err);
});
}}
>
{option.originalName}
</StyledListItem>
);
})}
</>
);
};
export const LanguageMenu: FC = () => {
const i18n = useI18N();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
return (
<Menu
content={
(
<LanguageMenuContent
currentLanguage={currentLanguage?.originalName}
/>
) as ReactElement
}
placement="bottom-end"
trigger="click"
disablePortal={true}
>
<MenuTrigger
data-testid="language-menu-button"
style={{ textTransform: 'capitalize' }}
>
{currentLanguage?.originalName}
</MenuTrigger>
</Menu>
);
};

View File

@@ -0,0 +1,36 @@
import type {
WorkspaceFlavour,
WorkspaceRegistry,
} from '@affine/env/workspace';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import type { FC } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared';
export type WorkspaceSettingDetailProps = {
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: () => Promise<void>;
onTransferWorkspace: <
From extends WorkspaceFlavour,
To extends WorkspaceFlavour
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
};
export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
workspace,
}) => {
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace ?? null
);
return (
<div>
<h2>New Workspace Setting Coming Soon!</h2>
{workspaceName}
</div>
);
};

View File

@@ -0,0 +1,3 @@
export const AccountSetting = () => {
return <div>AccountSetting</div>;
};

View File

@@ -0,0 +1,14 @@
import type { FC } from 'react';
import { settingHeader } from './share.css';
export const SettingHeader: FC<{ title: string; subtitle?: string }> = ({
title,
subtitle,
}) => {
return (
<div className={settingHeader}>
<div className="title">{title}</div>
<div className="subtitle">{subtitle}</div>
</div>
);
};

View File

@@ -0,0 +1,22 @@
import type { CSSProperties, FC, PropsWithChildren, ReactElement } from 'react';
import { settingRow } from './share.css';
export const SettingRow: FC<
PropsWithChildren<{
name: string;
desc: string | ReactElement;
style?: CSSProperties;
onClick?: () => void;
}>
> = ({ name, desc, children, onClick, style }) => {
return (
<div className={settingRow} style={style} onClick={onClick}>
<div className="left-col">
<div className="name">{name}</div>
<div className="desc">{desc}</div>
</div>
<div className="right-col">{children}</div>
</div>
);
};

View File

@@ -0,0 +1,69 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const settingHeader = style({
height: '68px',
borderBottom: '1px solid var(--affine-border-color)',
marginBottom: '24px',
});
globalStyle(`${settingHeader} .title`, {
fontSize: 'var(--affine-font-base)',
fontWeight: 600,
lineHeight: '24px',
marginBottom: '4px',
});
globalStyle(`${settingHeader} .subtitle`, {
fontSize: 'var(--affine-font-xs)',
lineHeight: '16px',
color: 'var(--affine-text-secondary-color)',
});
export const wrapper = style({
borderBottom: '1px solid var(--affine-border-color)',
paddingBottom: '24px',
marginBottom: '24px',
selectors: {
'&:last-of-type': {
borderBottom: 'none',
paddingBottom: '0',
marginBottom: '0',
},
},
});
globalStyle(`${wrapper} .title`, {
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
lineHeight: '18px',
color: 'var(--affine-text-secondary-color)',
marginBottom: '16px',
});
export const settingRow = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '25px',
color: 'var(--affine-text-primary-color)',
});
globalStyle(`${settingRow} .left-col`, {
flexShrink: 0,
maxWidth: '80%',
});
globalStyle(`${settingRow} .name`, {
marginBottom: '2px',
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
});
globalStyle(`${settingRow} .desc`, {
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
});
globalStyle(`${settingRow} .right-col`, {
flexGrow: 1,
display: 'flex',
justifyContent: 'flex-end',
paddingLeft: '15px',
});

View File

@@ -0,0 +1,14 @@
import type { FC, PropsWithChildren } from 'react';
import { wrapper } from './share.css';
export const Wrapper: FC<PropsWithChildren<{ title?: string }>> = ({
title,
children,
}) => {
return (
<div className={wrapper}>
{title ? <div className="title">{title}</div> : null}
{children}
</div>
);
};

View File

@@ -0,0 +1,2 @@
// Some settings are not implemented yet, but need to show in the setting modal when boss is watching.
export const IS_EXHIBITION = false;

View File

@@ -0,0 +1,110 @@
import { Switch } from '@affine/component';
import { relatedLinks } from '@affine/component/contact-modal';
import { env } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons';
import { useCallback } from 'react';
import { type AppSetting, useAppSetting } from '../../../../../atoms/settings';
import { SettingHeader } from '../../common/setting-header';
import { SettingRow } from '../../common/setting-row';
import { Wrapper } from '../../common/wrapper';
import { IS_EXHIBITION } from '../../config';
import { communityItem, communityWrapper, link } from './style.css';
export const AboutAffine = () => {
const t = useAFFiNEI18N();
const [appSettings, setAppSettings] = useAppSetting();
const changeSwitch = useCallback(
(key: keyof AppSetting, checked: boolean) => {
setAppSettings({ [key]: checked });
},
[setAppSettings]
);
return (
<>
<SettingHeader title={t['About AFFiNE']()} subtitle={t['None yet']()} />
{IS_EXHIBITION && env.isDesktop ? (
<Wrapper title={t['Version']()}>
<SettingRow
name={t['Check for updates']()}
desc={t['New version is ready']()}
></SettingRow>
<SettingRow
name={t['Check for updates automatically']()}
desc={t[
'If enabled, it will automatically check for new versions at regular intervals.'
]()}
>
<Switch
checked={appSettings.autoCheckUpdate}
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
/>
</SettingRow>
<SettingRow
name={t['Download updates automatically']()}
desc={t[
'If enabled, new versions will be automatically downloaded to the current device.'
]()}
>
<Switch
checked={appSettings.autoCheckUpdate}
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
/>
</SettingRow>
<SettingRow
name={t[`Discover what's new`]()}
desc={t['View the AFFiNE Changelog.']()}
style={{ cursor: 'pointer' }}
onClick={() => {
window.open(
'https://github.com/toeverything/AFFiNE/releases',
'_blank'
);
}}
>
<ArrowRightSmallIcon />
</SettingRow>
</Wrapper>
) : null}
<Wrapper title={t['Contact with us']()}>
<a className={link} href="https://affine.pro" target="_blank">
{t['Official Website']()}
<OpenInNewIcon className="icon" />
</a>
<a className={link} href="https://community.affine.pro" target="_blank">
{t['AFFiNE Community']()}
<OpenInNewIcon className="icon" />
</a>
</Wrapper>
<Wrapper title={t['Communities']()}>
<div className={communityWrapper}>
{relatedLinks.map(({ icon, title, link }) => {
return (
<div
className={communityItem}
onClick={() => {
window.open(link, '_blank');
}}
key={title}
>
{icon}
<p>{title}</p>
</div>
);
})}
</div>
</Wrapper>
<Wrapper title={t['Info of legal']()}>
<a className={link} href="https://affine.pro/privacy" target="_blank">
{t['Privacy']()}
<OpenInNewIcon className="icon" />
</a>
<a className={link} href="https://affine.pro/terms" target="_blank">
{t['Terms of Use']()}
<OpenInNewIcon className="icon" />
</a>
</Wrapper>
</>
);
};

View File

@@ -0,0 +1,47 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const link = style({
height: '18px',
display: 'flex',
alignItems: 'center',
color: 'var(--affine-text-primary-color)',
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
marginBottom: '12px',
selectors: {
'&:last-of-type': {
marginBottom: '0',
},
},
});
globalStyle(`${link} .icon`, {
color: 'var(--affine-icon-color)',
fontSize: 'var(--affine-font-base)',
marginLeft: '5px',
});
export const communityWrapper = style({
display: 'grid',
justifyContent: 'space-between',
gridTemplateColumns: 'repeat(auto-fill, 84px)',
gridGap: '6px',
});
export const communityItem = style({
width: '84px',
height: '58px',
borderRadius: '8px',
border: '1px solid var(--affine-border-color)',
color: 'var(--affine-text-primary-color)',
cursor: 'pointer',
});
globalStyle(`${communityItem} svg`, {
width: '24px',
height: '24px',
display: 'block',
margin: '8px auto 4px',
});
globalStyle(`${communityItem} p`, {
fontSize: 'var(--affine-font-xs)',
textAlign: 'center',
});

View File

@@ -0,0 +1,58 @@
import { Menu, MenuItem, MenuTrigger } from '@affine/component';
import dayjs from 'dayjs';
import { type FC, useCallback } from 'react';
import {
dateFormatOptions,
type DateFormats,
useAppSetting,
} from '../../../../../atoms/settings';
const DateFormatMenuContent: FC<{
currentOption: DateFormats;
onSelect: (option: DateFormats) => void;
}> = ({ onSelect, currentOption }) => {
return (
<>
{dateFormatOptions.map(option => {
return (
<MenuItem
key={option}
active={currentOption === option}
onClick={() => {
onSelect(option);
}}
>
{dayjs(new Date()).format(option)}
</MenuItem>
);
})}
</>
);
};
export const DateFormatSetting = () => {
const [appearanceSettings, setAppSettings] = useAppSetting();
const handleSelect = useCallback(
(option: DateFormats) => {
setAppSettings({ dateFormat: option });
},
[setAppSettings]
);
return (
<Menu
content={
<DateFormatMenuContent
onSelect={handleSelect}
currentOption={appearanceSettings.dateFormat}
/>
}
placement="bottom-end"
trigger="click"
disablePortal={true}
>
<MenuTrigger data-testid="date-format-menu-trigger">
{dayjs(new Date()).format(appearanceSettings.dateFormat)}
</MenuTrigger>
</Menu>
);
};

View File

@@ -0,0 +1,168 @@
import { RadioButton, RadioButtonGroup, Switch } from '@affine/component';
import { env } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useTheme } from 'next-themes';
import { useCallback } from 'react';
import {
type AppSetting,
useAppSetting,
windowFrameStyleOptions,
} from '../../../../../atoms/settings';
import { LanguageMenu } from '../../../language-menu';
import { SettingHeader } from '../../common/setting-header';
import { SettingRow } from '../../common/setting-row';
import { Wrapper } from '../../common/wrapper';
import { IS_EXHIBITION } from '../../config';
import { DateFormatSetting } from './date-format-setting';
import { settingWrapper } from './style.css';
export const ThemeSettings = () => {
const t = useAFFiNEI18N();
const { setTheme, theme } = useTheme();
return (
<RadioButtonGroup
className={settingWrapper}
defaultValue={theme}
onValueChange={useCallback(
(value: string) => {
setTheme(value);
},
[setTheme]
)}
>
<RadioButton value="system">{t['system']()}</RadioButton>
<RadioButton value="light">{t['light']()}</RadioButton>
<RadioButton value="dark">{t['dark']()}</RadioButton>
</RadioButtonGroup>
);
};
export const AppearanceSettings = () => {
const t = useAFFiNEI18N();
const [appSettings, setAppSettings] = useAppSetting();
const changeSwitch = useCallback(
(key: keyof AppSetting, checked: boolean) => {
setAppSettings({ [key]: checked });
},
[setAppSettings]
);
return (
<>
<SettingHeader
title={t['Appearance Settings']()}
subtitle={t['Customize your AFFiNE Appearance']()}
/>
<Wrapper title={t['Theme']()}>
<SettingRow
name={t['Color Scheme']()}
desc={t['Choose your color scheme']()}
>
<ThemeSettings />
</SettingRow>
<SettingRow
name={t['Display Language']()}
desc={t['Select the language for the interface.']()}
>
<div className={settingWrapper}>
<LanguageMenu />
</div>
</SettingRow>
{IS_EXHIBITION && env.isDesktop ? (
<SettingRow
name={t['Client Border Style']()}
desc={t['Customize the appearance of the client.']()}
>
<Switch
checked={appSettings.clientBorder}
onChange={checked => changeSwitch('clientBorder', checked)}
/>
</SettingRow>
) : null}
<SettingRow
name={t['Full width Layout']()}
desc={t['Maximum display of content within a page.']()}
>
<Switch
checked={appSettings.fullWidthLayout}
onChange={checked => changeSwitch('fullWidthLayout', checked)}
/>
</SettingRow>
{IS_EXHIBITION && env.isDesktop ? (
<SettingRow
name={t['Window frame style']()}
desc={t['Customize appearance of Windows Client.']()}
>
<RadioButtonGroup
className={settingWrapper}
defaultValue={appSettings.windowFrameStyle}
onValueChange={(value: AppSetting['windowFrameStyle']) => {
setAppSettings({ windowFrameStyle: value });
}}
>
{windowFrameStyleOptions.map(option => {
return (
<RadioButton value={option} key={option}>
{t[option]()}
</RadioButton>
);
})}
</RadioButtonGroup>
</SettingRow>
) : null}
</Wrapper>
{IS_EXHIBITION ? (
<Wrapper title={t['Date']()}>
<SettingRow
name={t['Date Format']()}
desc={t['Customize your date style.']()}
>
<div className={settingWrapper}>
<DateFormatSetting />
</div>
</SettingRow>
<SettingRow
name={t['Start Week On Monday']()}
desc={t['By default, the week starts on Sunday.']()}
>
<Switch
checked={appSettings.startWeekOnMonday}
onChange={checked => changeSwitch('startWeekOnMonday', checked)}
/>
</SettingRow>
</Wrapper>
) : null}
{env.isDesktop ? (
<Wrapper title={t['Sidebar']()}>
<SettingRow
name={t['Disable the noise background on the sidebar']()}
desc={t['None yet']()}
>
<Switch
checked={appSettings.disableNoisyBackground}
onChange={checked =>
changeSwitch('disableNoisyBackground', checked)
}
/>
</SettingRow>
<SettingRow
name={t['Disable the blur sidebar']()}
desc={t['None yet']()}
>
<Switch
checked={appSettings.disableBlurBackground}
onChange={checked =>
changeSwitch('disableBlurBackground', checked)
}
/>
</SettingRow>
</Wrapper>
) : null}
</>
);
};

View File

@@ -0,0 +1,9 @@
import { style } from '@vanilla-extract/css';
export const settingWrapper = style({
flexGrow: 1,
display: 'flex',
width: '50%',
minWidth: '150px',
maxWidth: '250px',
});

View File

@@ -0,0 +1,53 @@
import {
AppearanceIcon,
InformationIcon,
KeyboardIcon,
} from '@blocksuite/icons';
import type { FC, SVGProps } from 'react';
import { AboutAffine } from './about';
import { AppearanceSettings } from './appearance';
import { Shortcuts } from './shortcuts';
export type GeneralSettingKeys = 'shortcuts' | 'appearance' | 'about';
export type GeneralSettingList = {
key: GeneralSettingKeys;
title: string;
icon: FC<SVGProps<SVGSVGElement>>;
}[];
export const generalSettingList: GeneralSettingList = [
{
key: 'appearance',
title: 'Appearance',
icon: AppearanceIcon,
},
{
key: 'shortcuts',
title: 'Keyboard Shortcuts',
icon: KeyboardIcon,
},
{
key: 'about',
title: 'About AFFiNE',
icon: InformationIcon,
},
];
export const GeneralSetting = ({
generalKey,
}: {
generalKey: GeneralSettingKeys;
}) => {
switch (generalKey) {
case 'shortcuts':
return <Shortcuts />;
case 'appearance':
return <AppearanceSettings />;
case 'about':
return <AboutAffine />;
default:
return null;
}
};

View File

@@ -0,0 +1,69 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
useEdgelessShortcuts,
useGeneralShortcuts,
useMarkdownShortcuts,
usePageShortcuts,
} from '../../../../../hooks/affine/use-shortcuts';
import { SettingHeader } from '../../common/setting-header';
import { Wrapper } from '../../common/wrapper';
import { shortcutRow } from './style.css';
export const Shortcuts = () => {
const t = useAFFiNEI18N();
const markdownShortcuts = useMarkdownShortcuts();
const pageShortcuts = usePageShortcuts();
const edgelessShortcuts = useEdgelessShortcuts();
const generalShortcuts = useGeneralShortcuts();
return (
<>
<SettingHeader
title={t['Keyboard Shortcuts']()}
subtitle={t['Check Keyboard Shortcuts quickly']()}
/>
<Wrapper title={t['General']()}>
{Object.entries(generalShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
<span>{title}</span>
<span className="shortcut">{shortcuts}</span>
</div>
);
})}
</Wrapper>
<Wrapper title={t['Page']()}>
{Object.entries(pageShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
<span>{title}</span>
<span className="shortcut">{shortcuts}</span>
</div>
);
})}
</Wrapper>
<Wrapper title={t['Edgeless']()}>
{Object.entries(edgelessShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
<span>{title}</span>
<span className="shortcut">{shortcuts}</span>
</div>
);
})}
</Wrapper>
<Wrapper title={t['Markdown Syntax']()}>
{Object.entries(markdownShortcuts).map(([title, shortcuts]) => {
return (
<div key={title} className={shortcutRow}>
<span>{title}</span>
<span className="shortcut">{shortcuts}</span>
</div>
);
})}
</Wrapper>
</>
);
};

View File

@@ -0,0 +1,21 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const shortcutRow = style({
height: '32px',
marginBottom: '12px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontSize: 'var(--affine-font-base)',
selectors: {
'&:last-of-type': {
marginBottom: '0',
},
},
});
globalStyle(`${shortcutRow} .shortcut`, {
border: '1px solid var(--affine-border-color)',
borderRadius: '8px',
padding: '4px 18px',
});

View File

@@ -0,0 +1,140 @@
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContactWithUsIcon } from '@blocksuite/icons';
import type { NextRouter } from 'next/router';
import type React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useWorkspaces } from '../../../hooks/use-workspaces';
import type { BlockSuiteWorkspace } from '../../../shared';
import { AccountSetting } from './account-setting';
import {
GeneralSetting,
type GeneralSettingKeys,
generalSettingList,
} from './general-setting';
import { SettingSidebar } from './setting-sidebar';
import { settingContent } from './style.css';
import type { Workspace } from './type';
import { WorkSpaceSetting } from './workspace-setting';
export type QuickSearchModalProps = {
currentWorkspace?: BlockSuiteWorkspace;
workspaceList?: BlockSuiteWorkspace[];
open: boolean;
setOpen: (value: boolean) => void;
router: NextRouter;
};
export const SettingModal: React.FC<QuickSearchModalProps> = ({
open,
setOpen,
}) => {
const t = useAFFiNEI18N();
const workspaces = useWorkspaces();
const [currentWorkspace] = useCurrentWorkspace();
const workspaceList = useMemo(() => {
return workspaces.filter(
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
) as Workspace[];
}, [workspaces]);
const [currentRef, setCurrentRef] = useState<{
workspace: Workspace | null;
generalKey: GeneralSettingKeys | null;
isAccount: boolean;
}>({
workspace: null,
generalKey: generalSettingList[0].key,
isAccount: false,
});
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
const onGeneralSettingClick = useCallback((key: GeneralSettingKeys) => {
setCurrentRef({
workspace: null,
generalKey: key,
isAccount: false,
});
}, []);
const onWorkspaceSettingClick = useCallback((workspace: Workspace) => {
setCurrentRef({
workspace: workspace,
generalKey: null,
isAccount: false,
});
}, []);
const onAccountSettingClick = useCallback(() => {
setCurrentRef({
workspace: null,
generalKey: null,
isAccount: true,
});
}, []);
return (
<Modal
open={open}
onClose={handleClose}
wrapperPosition={['center', 'center']}
data-testid="setting-modal"
>
<ModalWrapper
width={1080}
height={760}
style={{
maxHeight: '85vh',
maxWidth: '70vw',
overflow: 'hidden',
display: 'flex',
}}
>
<ModalCloseButton top={16} right={20} onClick={handleClose} />
<SettingSidebar
generalSettingList={generalSettingList}
onGeneralSettingClick={onGeneralSettingClick}
currentWorkspace={
currentWorkspace as AffineLegacyCloudWorkspace | LocalWorkspace
}
workspaceList={workspaceList}
onWorkspaceSettingClick={onWorkspaceSettingClick}
selectedGeneralKey={currentRef.generalKey}
selectedWorkspace={currentRef.workspace}
onAccountSettingClick={onAccountSettingClick}
/>
<div className={settingContent}>
<div className="wrapper">
<div className="content">
{currentRef.workspace ? (
<WorkSpaceSetting workspace={currentRef.workspace} />
) : null}
{currentRef.generalKey ? (
<GeneralSetting generalKey={currentRef.generalKey} />
) : null}
{currentRef.isAccount ? <AccountSetting /> : null}
</div>
<div className="footer">
<ContactWithUsIcon />
<a href="https://community.affine.pro/home" target="_blank">
{t[
'Need more customization options? You can suggest them to us in the community.'
]()}
</a>
</div>
</div>
</div>
</ModalWrapper>
</Modal>
);
};

View File

@@ -0,0 +1,126 @@
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import type {
GeneralSettingKeys,
GeneralSettingList,
} from '../general-setting';
import type { Workspace } from '../type';
import {
accountButton,
settingSlideBar,
sidebarItemsWrapper,
sidebarSelectItem,
sidebarSubtitle,
sidebarTitle,
} from './style.css';
export const SettingSidebar = ({
generalSettingList,
onGeneralSettingClick,
currentWorkspace,
workspaceList,
onWorkspaceSettingClick,
selectedWorkspace,
selectedGeneralKey,
onAccountSettingClick,
}: {
generalSettingList: GeneralSettingList;
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
currentWorkspace: Workspace;
workspaceList: Workspace[];
onWorkspaceSettingClick: (
workspace: AffineLegacyCloudWorkspace | LocalWorkspace
) => void;
selectedWorkspace: Workspace | null;
selectedGeneralKey: string | null;
onAccountSettingClick: () => void;
}) => {
return (
<div className={settingSlideBar}>
<div className={sidebarTitle}>Settings</div>
<div className={sidebarSubtitle}>General</div>
<div className={sidebarItemsWrapper}>
{generalSettingList.map(({ title, icon, key }) => {
return (
<div
className={clsx(sidebarSelectItem, {
active: key === selectedGeneralKey,
})}
key={key}
title={title}
onClick={() => {
onGeneralSettingClick(key);
}}
>
{icon({ className: 'icon' })}
<span className="setting-name">{title}</span>
</div>
);
})}
</div>
<div className={sidebarSubtitle}>Workspace</div>
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
{workspaceList.map(workspace => {
return (
<WorkspaceListItem
key={workspace.id}
workspace={workspace}
onClick={() => {
onWorkspaceSettingClick(workspace);
}}
isCurrent={workspace.id === currentWorkspace.id}
isActive={workspace.id === selectedWorkspace?.id}
/>
);
})}
</div>
<div className={accountButton} onClick={onAccountSettingClick}>
<div className="avatar"></div>
<div className="content">
<div className="name" title="xxx">
Account NameAccount Name
</div>
<div className="email" title="xxx">
xxxxxxxx@gmail.comxxxxxxxx@gmail.com
</div>
</div>
</div>
</div>
);
};
const WorkspaceListItem = ({
workspace,
onClick,
isCurrent,
isActive,
}: {
workspace: AffineLegacyCloudWorkspace | LocalWorkspace;
onClick: () => void;
isCurrent: boolean;
isActive: boolean;
}) => {
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace ?? null
);
return (
<div
className={clsx(sidebarSelectItem, { active: isActive })}
title={workspaceName}
onClick={onClick}
>
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
<span className="setting-name">{workspaceName}</span>
{isCurrent ? <div className="current-label">Current</div> : null}
</div>
);
};

View File

@@ -0,0 +1,138 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const settingSlideBar = style({
width: '25%',
maxWidth: '242px',
// TODO: use color variable
// background: 'var(--affine-background-secondary-color)',
backgroundColor: '#F4F4F5',
padding: '20px 16px',
height: '100%',
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
});
export const sidebarTitle = style({
fontSize: 'var(--affine-font-h-6)',
fontWeight: '600',
lineHeight: 'var(--affine-line-height)',
paddingLeft: '8px',
});
export const sidebarSubtitle = style({
fontSize: 'var(--affine-font-sm)',
lineHeight: 'var(--affine-line-height)',
color: 'var(--affine-text-secondary-color)',
paddingLeft: '8px',
marginTop: '20px',
marginBottom: '4px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
export const sidebarItemsWrapper = style({
selectors: {
'&.scroll': {
flexGrow: 1,
},
},
});
export const sidebarSelectItem = style({
display: 'flex',
alignItems: 'center',
padding: '0 8px',
height: '30px',
marginBottom: '4px',
fontSize: 'var(--affine-font-sm)',
borderRadius: '8px',
cursor: 'pointer',
userSelect: 'none',
':hover': {
background: 'var(--affine-hover-color)',
},
selectors: {
'&.active': {
background: 'var(--affine-hover-color)',
},
[`${sidebarItemsWrapper} &:last-of-type`]: {
marginBottom: 0,
},
},
});
globalStyle(`${settingSlideBar} .icon`, {
width: '16px',
height: '16px',
marginRight: '10px',
flexShrink: 0,
});
globalStyle(`${settingSlideBar} .setting-name`, {
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flexGrow: 1,
});
globalStyle(`${settingSlideBar} .current-label`, {
height: '20px',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '0 5px',
// TODO: use color variable
background: '#1E96EB',
fontSize: 'var(--affine-font-xs)',
fontWeight: '600',
color: 'var(--affine-white)',
marginLeft: '10px',
flexShrink: 0,
});
export const accountButton = style({
height: '42px',
padding: '4px 8px',
borderRadius: '8px',
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
':hover': {
background: 'var(--affine-hover-color)',
},
});
globalStyle(`${accountButton} .avatar`, {
width: '28px',
height: '28px',
border: '1px solid',
borderColor: 'var(--affine-white)',
borderRadius: '14px',
flexShrink: '0',
marginRight: '10px',
background: 'red',
});
globalStyle(`${accountButton} .content`, {
flexGrow: '1',
minWidth: 0,
});
globalStyle(`${accountButton} .name`, {
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flexGrow: 1,
});
globalStyle(`${accountButton} .email`, {
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flexGrow: 1,
});

View File

@@ -0,0 +1,38 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const settingContent = style({
flexGrow: '1',
height: '100%',
padding: '40px 0',
});
globalStyle(`${settingContent} .wrapper`, {
width: '66%',
minWidth: '450px',
height: '100%',
maxWidth: '560px',
margin: '0 auto',
overflowY: 'auto',
});
globalStyle(`${settingContent} .content`, {
minHeight: '100%',
paddingBottom: '80px',
});
globalStyle(`${settingContent} .footer`, {
cursor: 'pointer',
paddingTop: '40px',
marginTop: '-80px',
fontSize: 'var(--affine-font-sm)',
display: 'flex',
});
globalStyle(`${settingContent} .footer a`, {
color: 'var(--affine-text-primary-color)',
});
globalStyle(`${settingContent} .footer > svg`, {
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-icon-color)',
marginRight: '12px',
marginTop: '2px',
});

View File

@@ -0,0 +1,6 @@
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
export type Workspace = AffineLegacyCloudWorkspace | LocalWorkspace;

View File

@@ -0,0 +1,29 @@
import { assertExists } from '@blocksuite/store';
import React, { Suspense, useCallback } from 'react';
import { getUIAdapter } from '../../../../adapters/workspace';
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
import { useAppHelper } from '../../../../hooks/use-workspaces';
import type { Workspace } from '../type';
export const WorkSpaceSetting = ({ workspace }: { workspace: Workspace }) => {
const helper = useAppHelper();
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
const onDeleteWorkspace = useCallback(async () => {
assertExists(currentWorkspace);
const workspaceId = currentWorkspace.id;
return helper.deleteWorkspace(workspaceId);
}, [helper]);
const onTransformWorkspace = useOnTransformWorkspace();
return (
<Suspense fallback={<div>loading</div>}>
<NewSettingsDetail
onTransformWorkspace={onTransformWorkspace}
onDeleteWorkspace={onDeleteWorkspace}
currentWorkspace={workspace}
/>
</Suspense>
);
};

View File

@@ -1,5 +1,5 @@
import { Menu, MenuItem } from '@affine/component';
import { AffineIcon, SignOutIcon } from '@blocksuite/icons';
import { AffineLogoSBlue2_1Icon, SignOutIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
@@ -90,7 +90,7 @@ export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
{props.name ? (
props.name.substring(0, 1)
) : (
<AffineIcon fontSize={24} color={'#5438FF'} />
<AffineLogoSBlue2_1Icon fontSize={24} color={'#5438FF'} />
)}
</div>
)}

View File

@@ -4,3 +4,16 @@ export const pluginContainer = style({
height: '100%',
width: '100%',
});
export const editor = style({
height: 'calc(100% - 52px)',
selectors: {
'&.full-screen': {
padding: '0 5%',
vars: {
'--affine-editor-width': '100%',
},
},
},
});

View File

@@ -18,6 +18,7 @@ import type {
PluginUIAdapter,
} from '@toeverything/plugin-infra/type';
import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import type { FC, ReactElement } from 'react';
@@ -32,8 +33,10 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { pageSettingFamily } from '../atoms';
import { contentLayoutAtom } from '../atoms/layout';
import { useAppSetting } from '../atoms/settings';
import type { AffineOfficialWorkspace } from '../shared';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { editor } from './page-detail-editor.css';
import { pluginContainer } from './page-detail-editor.css';
export type PageDetailEditorProps = {
@@ -71,12 +74,15 @@ const EditorWrapper = memo(function EditorWrapper({
(DEFAULT_HELLO_WORLD_PAGE_ID === pageId ? 'edgeless' : 'page');
const setEditor = useSetAtom(rootCurrentEditorAtom);
const [appSettings] = useAppSetting();
assertExists(meta);
return (
<Editor
style={{
height: 'calc(100% - 52px)',
}}
className={clsx(editor, {
'full-screen': appSettings?.fullWidthLayout,
})}
key={`${workspace.flavour}-${workspace.id}-${pageId}`}
mode={isPublic ? 'page' : currentMode}
page={page}

View File

@@ -1,91 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
interface ShortcutTip {
[x: string]: string;
}
export const useMacKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return {
[t['Undo']()]: '⌘+Z',
[t['Redo']()]: '⌘+⇧+Z',
[t['Bold']()]: '⌘+B',
[t['Italic']()]: '⌘+I',
[t['Underline']()]: '⌘+U',
[t['Strikethrough']()]: '⌘+⇧+S',
[t['Inline code']()]: ' ⌘+E',
[t['Code block']()]: '⌘+⌥+C',
[t['Link']()]: '⌘+K',
[t['Quick search']()]: '⌘+K',
[t['Body text']()]: '⌘+⌥+0',
[t['Heading']({ number: '1' })]: '⌘+⌥+1',
[t['Heading']({ number: '2' })]: '⌘+⌥+2',
[t['Heading']({ number: '3' })]: '⌘+⌥+3',
[t['Heading']({ number: '4' })]: '⌘+⌥+4',
[t['Heading']({ number: '5' })]: '⌘+⌥+5',
[t['Heading']({ number: '6' })]: '⌘+⌥+6',
[t['Increase indent']()]: 'Tab',
[t['Reduce indent']()]: '⇧+Tab',
};
};
export const useMacMarkdownShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return {
[t['Bold']()]: '**Text** ',
[t['Italic']()]: '*Text* ',
[t['Underline']()]: '~Text~ ',
[t['Strikethrough']()]: '~~Text~~ ',
[t['Divider']()]: '***',
[t['Inline code']()]: '`Text` ',
[t['Code block']()]: '``` Space',
[t['Heading']({ number: '1' })]: '# Text',
[t['Heading']({ number: '2' })]: '## Text',
[t['Heading']({ number: '3' })]: '### Text',
[t['Heading']({ number: '4' })]: '#### Text',
[t['Heading']({ number: '5' })]: '##### Text',
[t['Heading']({ number: '6' })]: '###### Text',
};
};
export const useWindowsKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return {
[t['Undo']()]: 'Ctrl+Z',
[t['Redo']()]: 'Ctrl+Y',
[t['Bold']()]: 'Ctrl+B',
[t['Italic']()]: 'Ctrl+I',
[t['Underline']()]: 'Ctrl+U',
[t['Strikethrough']()]: 'Ctrl+Shift+S',
[t['Inline code']()]: ' Ctrl+E',
[t['Code block']()]: 'Ctrl+Alt+C',
[t['Link']()]: 'Ctrl+K',
[t['Quick search']()]: 'Ctrl+K',
[t['Body text']()]: 'Ctrl+Shift+0',
[t['Heading']({ number: '1' })]: 'Ctrl+Shift+1',
[t['Heading']({ number: '2' })]: 'Ctrl+Shift+2',
[t['Heading']({ number: '3' })]: 'Ctrl+Shift+3',
[t['Heading']({ number: '4' })]: 'Ctrl+Shift+4',
[t['Heading']({ number: '5' })]: 'Ctrl+Shift+5',
[t['Heading']({ number: '6' })]: 'Ctrl+Shift+6',
[t['Increase indent']()]: 'Tab',
[t['Reduce indent']()]: 'Shift+Tab',
};
};
export const useWinMarkdownShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return {
[t['Bold']()]: '**Text** ',
[t['Italic']()]: '*Text* ',
[t['Underline']()]: '~Text~ ',
[t['Strikethrough']()]: '~~Text~~ ',
[t['Divider']()]: '***',
[t['Inline code']()]: '`Text` ',
[t['Code block']()]: '``` Text',
[t['Heading']({ number: '1' })]: '# Text',
[t['Heading']({ number: '2' })]: '## Text',
[t['Heading']({ number: '3' })]: '### Text',
[t['Heading']({ number: '4' })]: '#### Text',
[t['Heading']({ number: '5' })]: '##### Text',
[t['Heading']({ number: '6' })]: '###### Text',
};
};

View File

@@ -3,16 +3,14 @@ import {
MuiClickAwayListener,
MuiSlide,
} from '@affine/component';
import { env } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useEffect, useState } from 'react';
import {
useMacKeyboardShortcuts,
useMacMarkdownShortcuts,
useWindowsKeyboardShortcuts,
useWinMarkdownShortcuts,
} from './config';
useEdgelessShortcuts,
useGeneralShortcuts,
useMarkdownShortcuts,
usePageShortcuts,
} from '../../../hooks/affine/use-shortcuts';
import { KeyboardIcon } from './icons';
import {
StyledListItem,
@@ -26,25 +24,13 @@ type ModalProps = {
onClose: () => void;
};
const checkIsMac = () => {
return env.isBrowser && env.isMacOs;
};
export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
const t = useAFFiNEI18N();
const macMarkdownShortcuts = useMacMarkdownShortcuts();
const winMarkdownShortcuts = useWinMarkdownShortcuts();
const macKeyboardShortcuts = useMacKeyboardShortcuts();
const windowsKeyboardShortcuts = useWindowsKeyboardShortcuts();
const [isMac, setIsMac] = useState(false);
const markdownShortcuts = isMac ? macMarkdownShortcuts : winMarkdownShortcuts;
const keyboardShortcuts = isMac
? macKeyboardShortcuts
: windowsKeyboardShortcuts;
useEffect(() => {
setIsMac(checkIsMac());
}, []);
const markdownShortcuts = useMarkdownShortcuts();
const pageShortcuts = usePageShortcuts();
const edgelessShortcuts = useEdgelessShortcuts();
const generalShortcuts = useGeneralShortcuts();
return (
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
@@ -72,9 +58,27 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
/>
</StyledModalHeader>
<StyledSubTitle style={{ marginTop: 0 }}>
{t['Keyboard Shortcuts']()}
{t['General']()}
</StyledSubTitle>
{Object.entries(keyboardShortcuts).map(([title, shortcuts]) => {
{Object.entries(generalShortcuts).map(([title, shortcuts]) => {
return (
<StyledListItem key={title}>
<span>{title}</span>
<span>{shortcuts}</span>
</StyledListItem>
);
})}
<StyledSubTitle>{t['Page']()}</StyledSubTitle>
{Object.entries(pageShortcuts).map(([title, shortcuts]) => {
return (
<StyledListItem key={title}>
<span>{title}</span>
<span>{shortcuts}</span>
</StyledListItem>
);
})}
<StyledSubTitle>{t['Edgeless']()}</StyledSubTitle>
{Object.entries(edgelessShortcuts).map(([title, shortcuts]) => {
return (
<StyledListItem key={title}>
<span>{title}</span>

View File

@@ -4,6 +4,7 @@ import {
appSidebarOpenAtom,
AppUpdaterButton,
CategoryDivider,
MenuItem,
MenuLinkItem,
QuickSearchInput,
SidebarContainer,
@@ -25,6 +26,7 @@ import type { ReactElement } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useHistoryAtom } from '../../atoms/history';
import { useAppSetting } from '../../atoms/settings';
import type { AllWorkspace } from '../../shared';
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
@@ -32,6 +34,7 @@ import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelecto
export type RootAppSidebarProps = {
isPublicWorkspace: boolean;
onOpenQuickSearchModal: () => void;
onOpenSettingModal: () => void;
onOpenWorkspaceListModal: () => void;
currentWorkspace: AllWorkspace | null;
openPage: (pageId: string) => void;
@@ -88,8 +91,11 @@ export const RootAppSidebar = ({
paths,
onOpenQuickSearchModal,
onOpenWorkspaceListModal,
onOpenSettingModal,
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace?.id || null;
const [appSettings] = useAppSetting();
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const onClickNewPage = useCallback(async () => {
@@ -143,7 +149,10 @@ export const RootAppSidebar = ({
return (
<>
<AppSidebar router={router}>
<AppSidebar
router={router}
hasBackground={!appSettings.disableBlurBackground}
>
<SidebarContainer>
<WorkspaceSelector
currentWorkspace={currentWorkspace}
@@ -168,6 +177,25 @@ export const RootAppSidebar = ({
>
<span data-testid="settings">{t['Settings']()}</span>
</RouteMenuLinkItem>
{config.enableNewSettingModal ? (
<MenuItem icon={<SettingsIcon />} onClick={onOpenSettingModal}>
<span data-testid="new-settings">
{t['Settings']()}
<i
style={{
background: 'var(--affine-palette-line-blue)',
borderRadius: '2px',
fontSize: '8px',
padding: '0 5px',
color: 'var(--affine-white)',
marginLeft: '15px',
}}
>
NEW
</i>
</span>
</MenuItem>
) : null}
</SidebarContainer>
<SidebarScrollableContainer>

View File

@@ -0,0 +1,237 @@
import { env } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMemo } from 'react';
interface ShortcutTip {
[x: string]: string;
}
export const useWinGeneralKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Cancel']()]: 'ESC',
[t['Quick Search']()]: 'Ctrl + K',
[t['New Page']()]: 'Ctrl + N',
// not implement yet
// [t['Append to Daily Note']()]: 'Ctrl + Alt + A',
[t['Expand/Collapse Sidebar']()]: 'Ctrl + /',
// not implement yet
// [t['Go Back']()]: 'Ctrl + [',
// [t['Go Forward']()]: 'Ctrl + ]',
}),
[t]
);
};
export const useMacGeneralKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Cancel']()]: 'ESC',
[t['Quick Search']()]: '⌘ + K',
[t['New Page']()]: '⌘ + N',
// not implement yet
// [t['Append to Daily Note']()]: '⌘ + ⌥ + A',
[t['Expand/Collapse Sidebar']()]: '⌘ + /',
// not implement yet
// [t['Go Back']()]: '⌘ + [',
// [t['Go Forward']()]: '⌘ + ]',
}),
[t]
);
};
export const useMacEdgelessKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Select All']()]: '⌘ + A',
[t['Undo']()]: '⌘ + Z',
[t['Redo']()]: '⌘ + ⇧ + Z',
[t['Zoom in']()]: '⌘ + +',
[t['Zoom out']()]: '⌘ + -',
[t['Zoom to 100%']()]: '⌘ + 0',
[t['Zoom to fit']()]: '⌘ + 1',
[t['Select']()]: 'V',
[t['Text']()]: 'T',
[t['Shape']()]: 'S',
[t['Image']()]: 'I',
[t['Straight Connector']()]: 'L',
[t['Elbowed Connector']()]: 'X',
// not implement yet
// [t['Curve Connector']()]: 'C',
[t['Pen']()]: 'P',
[t['Hand']()]: 'H',
[t['Note']()]: 'N',
// not implement yet
// [t['Group']()]: '⌘ + G',
// [t['Ungroup']()]: '⌘ + ⇧ + G',
}),
[t]
);
};
export const useWinEdgelessKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Select All']()]: 'Ctrl + A',
[t['Undo']()]: 'Ctrl + Z',
[t['Redo']()]: 'Ctrl + Y/Ctrl + Shift + Z',
[t['Zoom in']()]: 'Ctrl + +',
[t['Zoom out']()]: 'Ctrl + -',
[t['Zoom to 100%']()]: 'Ctrl + 0',
[t['Zoom to fit']()]: 'Ctrl + 1',
[t['Select']()]: 'V',
[t['Text']()]: 'T',
[t['Shape']()]: 'S',
[t['Image']()]: 'I',
[t['Straight Connector']()]: 'L',
[t['Elbowed Connector']()]: 'X',
// not implement yet
// [t['Curve Connector']()]: 'C',
[t['Pen']()]: 'P',
[t['Hand']()]: 'H',
[t['Note']()]: 'N',
// not implement yet
// [t['Group']()]: 'Ctrl + G',
// [t['Ungroup']()]: 'Ctrl + Shift + G',
}),
[t]
);
};
export const useMacPageKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Undo']()]: '⌘+Z',
[t['Redo']()]: '⌘+⇧+Z',
[t['Bold']()]: '⌘+B',
[t['Italic']()]: '⌘+I',
[t['Underline']()]: '⌘+U',
[t['Strikethrough']()]: '⌘+⇧+S',
[t['Inline code']()]: ' ⌘+E',
[t['Code block']()]: '⌘+⌥+C',
[t['Link']()]: '⌘+K',
[t['Quick search']()]: '⌘+K',
[t['Body text']()]: '⌘+⌥+0',
[t['Heading']({ number: '1' })]: '⌘+⌥+1',
[t['Heading']({ number: '2' })]: '⌘+⌥+2',
[t['Heading']({ number: '3' })]: '⌘+⌥+3',
[t['Heading']({ number: '4' })]: '⌘+⌥+4',
[t['Heading']({ number: '5' })]: '⌘+⌥+5',
[t['Heading']({ number: '6' })]: '⌘+⌥+6',
[t['Increase indent']()]: 'Tab',
[t['Reduce indent']()]: '⇧+Tab',
[t['Group as Database']()]: '⌘ + G',
// not implement yet
// [t['Move Up']()]: '⌘ + ⌥ + ↑',
// [t['Move Down']()]: '⌘ + ⌥ + ↓',
}),
[t]
);
};
export const useMacMarkdownShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Bold']()]: '**Text** ',
[t['Italic']()]: '*Text* ',
[t['Underline']()]: '~Text~ ',
[t['Strikethrough']()]: '~~Text~~ ',
[t['Divider']()]: '***',
[t['Inline code']()]: '`Text` ',
[t['Code block']()]: '``` Space',
[t['Heading']({ number: '1' })]: '# Text',
[t['Heading']({ number: '2' })]: '## Text',
[t['Heading']({ number: '3' })]: '### Text',
[t['Heading']({ number: '4' })]: '#### Text',
[t['Heading']({ number: '5' })]: '##### Text',
[t['Heading']({ number: '6' })]: '###### Text',
}),
[t]
);
};
export const useWinPageKeyboardShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Undo']()]: 'Ctrl+Z',
[t['Redo']()]: 'Ctrl+Y',
[t['Bold']()]: 'Ctrl+B',
[t['Italic']()]: 'Ctrl+I',
[t['Underline']()]: 'Ctrl+U',
[t['Strikethrough']()]: 'Ctrl+Shift+S',
[t['Inline code']()]: ' Ctrl+E',
[t['Code block']()]: 'Ctrl+Alt+C',
[t['Link']()]: 'Ctrl+K',
[t['Quick search']()]: 'Ctrl+K',
[t['Body text']()]: 'Ctrl+Shift+0',
[t['Heading']({ number: '1' })]: 'Ctrl+Shift+1',
[t['Heading']({ number: '2' })]: 'Ctrl+Shift+2',
[t['Heading']({ number: '3' })]: 'Ctrl+Shift+3',
[t['Heading']({ number: '4' })]: 'Ctrl+Shift+4',
[t['Heading']({ number: '5' })]: 'Ctrl+Shift+5',
[t['Heading']({ number: '6' })]: 'Ctrl+Shift+6',
[t['Increase indent']()]: 'Tab',
[t['Reduce indent']()]: 'Shift+Tab',
[t['Group as Database']()]: 'Ctrl + G',
// not implement yet
// [t['Move Up']()]: 'Ctrl + Alt + ↑',
// [t['Move Down']()]: 'Ctrl + Alt + ↓',
}),
[t]
);
};
export const useWinMarkdownShortcuts = (): ShortcutTip => {
const t = useAFFiNEI18N();
return useMemo(
() => ({
[t['Bold']()]: '**Text** ',
[t['Italic']()]: '*Text* ',
[t['Underline']()]: '~Text~ ',
[t['Strikethrough']()]: '~~Text~~ ',
[t['Divider']()]: '***',
[t['Inline code']()]: '`Text` ',
[t['Code block']()]: '``` Text',
[t['Heading']({ number: '1' })]: '# Text',
[t['Heading']({ number: '2' })]: '## Text',
[t['Heading']({ number: '3' })]: '### Text',
[t['Heading']({ number: '4' })]: '#### Text',
[t['Heading']({ number: '5' })]: '##### Text',
[t['Heading']({ number: '6' })]: '###### Text',
}),
[t]
);
};
export const useMarkdownShortcuts = (): ShortcutTip => {
const macMarkdownShortcuts = useMacMarkdownShortcuts();
const winMarkdownShortcuts = useWinMarkdownShortcuts();
const isMac = env.isBrowser && env.isMacOs;
return isMac ? macMarkdownShortcuts : winMarkdownShortcuts;
};
export const usePageShortcuts = (): ShortcutTip => {
const macPageShortcuts = useMacPageKeyboardShortcuts();
const winPageShortcuts = useWinPageKeyboardShortcuts();
const isMac = env.isBrowser && env.isMacOs;
return isMac ? macPageShortcuts : winPageShortcuts;
};
export const useEdgelessShortcuts = (): ShortcutTip => {
const macEdgelessShortcuts = useMacEdgelessKeyboardShortcuts();
const winEdgelessShortcuts = useWinEdgelessKeyboardShortcuts();
const isMac = env.isBrowser && env.isMacOs;
return isMac ? macEdgelessShortcuts : winEdgelessShortcuts;
};
export const useGeneralShortcuts = (): ShortcutTip => {
const macGeneralShortcuts = useMacGeneralKeyboardShortcuts();
const winGeneralShortcuts = useWinGeneralKeyboardShortcuts();
const isMac = env.isBrowser && env.isMacOs;
return isMac ? macGeneralShortcuts : winGeneralShortcuts;
};

View File

@@ -1,4 +1,4 @@
import { AppContainer, MainContainer } from '@affine/component/workspace';
import { MainContainer } from '@affine/component/workspace';
import type { AffinePublicWorkspace } from '@affine/env/workspace';
import { useAtom } from 'jotai';
import Head from 'next/head';
@@ -7,6 +7,7 @@ import type React from 'react';
import { lazy, Suspense } from 'react';
import { openQuickSearchModalAtom } from '../atoms';
import { AppContainer } from '../components/affine/app-container';
import { useRouterTitle } from '../hooks/use-router-title';
const QuickSearchModal = lazy(() =>

View File

@@ -4,7 +4,6 @@ import { appSidebarResizingAtom } from '@affine/component/app-sidebar';
import type { DraggableTitleCellData } from '@affine/component/page-list';
import { StyledTitleLink } from '@affine/component/page-list';
import {
AppContainer,
MainContainer,
ToolContainer,
WorkspaceFallback,
@@ -42,12 +41,17 @@ import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { WorkspaceAdapters } from '../adapters/workspace';
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
import {
openQuickSearchModalAtom,
openSettingModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { useTrackRouterHistoryEffect } from '../atoms/history';
import {
publicWorkspaceAtom,
publicWorkspaceIdAtom,
} from '../atoms/public-workspace';
import { AppContainer } from '../components/affine/app-container';
import type { IslandItemNames } from '../components/pure/help-island';
import { HelpIsland } from '../components/pure/help-island';
import {
@@ -71,6 +75,11 @@ const QuickSearchModal = lazy(() =>
default: module.QuickSearchModal,
}))
);
const SettingModal = lazy(() =>
import('../components/affine/setting-modal').then(module => ({
default: module.SettingModal,
}))
);
export const PublicQuickSearch: FC = () => {
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
@@ -119,6 +128,25 @@ export const QuickSearch: FC = () => {
/>
);
};
export const Setting: FC = () => {
const [currentWorkspace] = useCurrentWorkspace();
const router = useRouter();
const [openSettingModal, setOpenSettingModalAtom] =
useAtom(openSettingModalAtom);
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const isPublicWorkspace =
router.pathname.split('/')[1] === 'public-workspace';
if (!blockSuiteWorkspace || isPublicWorkspace) {
return null;
}
return (
<SettingModal
open={openSettingModal}
setOpen={setOpenSettingModalAtom}
router={router}
/>
);
};
const logger = new DebugLogger('workspace-layout');
@@ -390,6 +418,12 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
setOpenQuickSearchModalAtom(true);
}, [setOpenQuickSearchModalAtom]);
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
const handleOpenSettingModal = useCallback(() => {
setOpenSettingModalAtom(true);
}, [setOpenSettingModalAtom]);
const resizing = useAtomValue(appSidebarResizingAtom);
const sensors = useSensors(
@@ -444,6 +478,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
<RootAppSidebar
isPublicWorkspace={isPublicWorkspace}
onOpenQuickSearchModal={handleOpenQuickSearchModal}
onOpenSettingModal={handleOpenSettingModal}
currentWorkspace={currentWorkspace}
onOpenWorkspaceListModal={handleOpenWorkspaceListModal}
openPage={useCallback(
@@ -476,6 +511,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
<PageListTitleCellDragOverlay />
</DndContext>
<QuickSearch />
<Setting />
</>
);
};

View File

@@ -1,5 +1,5 @@
import { Button } from '@affine/component';
import { AppContainer, MainContainer } from '@affine/component/workspace';
import { MainContainer } from '@affine/component/workspace';
import { DebugLogger } from '@affine/debug';
import type { BroadCastChannelProvider } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
@@ -10,6 +10,7 @@ import { Typography } from '@mui/material';
import type React from 'react';
import { useEffect, useMemo, useState } from 'react';
import { AppContainer } from '../../components/affine/app-container';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { toast } from '../../utils';

View File

@@ -1,7 +1,8 @@
import { AppContainer, MainContainer } from '@affine/component/workspace';
import { MainContainer } from '@affine/component/workspace';
import { useRouter } from 'next/router';
import { lazy, Suspense } from 'react';
import { AppContainer } from '../../components/affine/app-container';
import type { NextPageWithLayout } from '../../shared';
const Editor = lazy(() =>

View File

@@ -1,5 +1,5 @@
import { Button } from '@affine/component';
import { AppContainer, MainContainer } from '@affine/component/workspace';
import { MainContainer } from '@affine/component/workspace';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
clearLoginStorage,
@@ -14,6 +14,7 @@ import { useAtom } from 'jotai';
import type { NextPage } from 'next';
import { lazy, Suspense, useMemo } from 'react';
import { AppContainer } from '../../components/affine/app-container';
import { toast } from '../../utils';
const Viewer = lazy(() =>

View File

@@ -1,4 +1,4 @@
import { AppContainer, MainContainer } from '@affine/component/workspace';
import { MainContainer } from '@affine/component/workspace';
import { config } from '@affine/env';
import { NoSsr } from '@mui/material';
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
@@ -6,6 +6,8 @@ import { useAtomValue } from 'jotai';
import type { ReactElement } from 'react';
import { Suspense } from 'react';
import { AppContainer } from '../components/affine/app-container';
const Plugins = () => {
const plugins = useAtomValue(affinePluginsAtom);
return (