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
@@ -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',
});