mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat: new setting modal (#2834)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 }) => ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
67
apps/web/src/atoms/settings.ts
Normal file
67
apps/web/src/atoms/settings.ts
Normal 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;
|
||||
};
|
||||
18
apps/web/src/components/affine/app-container.tsx
Normal file
18
apps/web/src/components/affine/app-container.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
70
apps/web/src/components/affine/language-menu/index.tsx
Normal file
70
apps/web/src/components/affine/language-menu/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const AccountSetting = () => {
|
||||
return <div>AccountSetting</div>;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
2
apps/web/src/components/affine/setting-modal/config.ts
Normal file
2
apps/web/src/components/affine/setting-modal/config.ts
Normal 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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingWrapper = style({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
width: '50%',
|
||||
minWidth: '150px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
140
apps/web/src/components/affine/setting-modal/index.tsx
Normal file
140
apps/web/src/components/affine/setting-modal/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
38
apps/web/src/components/affine/setting-modal/style.css.ts
Normal file
38
apps/web/src/components/affine/setting-modal/style.css.ts
Normal 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',
|
||||
});
|
||||
6
apps/web/src/components/affine/setting-modal/type.ts
Normal file
6
apps/web/src/components/affine/setting-modal/type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
export type Workspace = AffineLegacyCloudWorkspace | LocalWorkspace;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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%',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
237
apps/web/src/hooks/affine/use-shortcuts.ts
Normal file
237
apps/web/src/hooks/affine/use-shortcuts.ts
Normal 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;
|
||||
};
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user