feat(core): add actions to editor settings (#8030)

# What Changed?
- Add actions of following edgeless-elements editor settings:
  - note
  - connector
  - edgeless text
  - pen
This commit is contained in:
akumatus
2024-09-02 14:23:04 +00:00
parent e1310b65cd
commit 2e37ee0e33
14 changed files with 619 additions and 353 deletions

View File

@@ -4,8 +4,11 @@ import type { FrameworkProvider } from '../provider';
// eslint-disable-next-line @typescript-eslint/ban-types
export class Component<Props = {}> {
readonly framework: FrameworkProvider;
readonly props: Props;
protected readonly disposables: (() => void)[] = [];
get eventBus() {
return this.framework.eventBus;
}
@@ -19,7 +22,9 @@ export class Component<Props = {}> {
CONSTRUCTOR_CONTEXT.current = {};
}
dispose() {}
dispose() {
this.disposables.forEach(dispose => dispose());
}
[Symbol.dispose]() {
this.dispose();

View File

@@ -1,183 +0,0 @@
import {
Menu,
MenuItem,
MenuTrigger,
RadioGroup,
type RadioItem,
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const ConnecterSettings = () => {
const t = useI18n();
const [connecterStyle, setConnecterStyle] = useState<'general' | 'scribbled'>(
'general'
);
const [connectorShape, setConnectorShape] = useState<
'elbowed' | 'curve' | 'straight'
>('elbowed');
const [borderStyle, setBorderStyle] = useState<'solid' | 'dash'>('solid');
const [borderThickness, setBorderThickness] = useState<number[]>([3]);
const connecterStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'general',
label: t['com.affine.settings.editorSettings.edgeless.style.general'](),
},
{
value: 'scribbled',
label:
t['com.affine.settings.editorSettings.edgeless.style.scribbled'](),
},
],
[t]
);
const connecterShapeItems = useMemo<RadioItem[]>(
() => [
{
value: 'elbowed',
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.elbowed'
](),
},
{
value: 'curve',
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.curve'
](),
},
{
value: 'straight',
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.straight'
](),
},
],
[t]
);
const borderStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'solid',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
},
{
value: 'dash',
label:
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
},
],
[t]
);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.connecter']()}
option={['mock-option']}
type="mock-type"
/>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.color'
]()}
desc={''}
>
<Menu items={<MenuItem>Grey</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Grey
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.style']()}
desc={''}
>
<RadioGroup
items={connecterStyleItems}
value={connecterStyle}
width={250}
className={settingWrapper}
onChange={setConnecterStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape'
]()}
desc={''}
>
<RadioGroup
items={connecterShapeItems}
value={connectorShape}
width={250}
className={settingWrapper}
onChange={setConnectorShape}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.border-style'
]()}
desc={''}
>
<RadioGroup
items={borderStyleItems}
value={borderStyle}
width={250}
className={settingWrapper}
onChange={setBorderStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.border-thickness'
]()}
desc={''}
>
<Slider
value={borderThickness}
onValueChange={setBorderThickness}
min={1}
max={5}
step={1}
nodes={[1, 2, 3, 4, 5]}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.start-endpoint'
]()}
desc={''}
>
<Menu items={<MenuItem>None</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
None
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.end-endpoint'
]()}
desc={''}
>
<Menu items={<MenuItem>Arrow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Arrow
</MenuTrigger>
</Menu>
</SettingRow>
</>
);
};

View File

@@ -0,0 +1,278 @@
import {
Menu,
MenuItem,
MenuTrigger,
RadioGroup,
type RadioItem,
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { useI18n } from '@affine/i18n';
import {
ConnectorMode,
LineColor,
PointStyle,
StrokeStyle,
} from '@blocksuite/blocks';
import { useFramework, useLiveData } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
enum ConnecterStyle {
General = 'general',
Scribbled = 'scribbled',
}
export const ConnectorSettings = () => {
const t = useI18n();
const framework = useFramework();
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const connecterStyleItems = useMemo<RadioItem[]>(
() => [
{
value: ConnecterStyle.General,
label: t['com.affine.settings.editorSettings.edgeless.style.general'](),
},
{
value: ConnecterStyle.Scribbled,
label:
t['com.affine.settings.editorSettings.edgeless.style.scribbled'](),
},
],
[t]
);
const connecterStyle: ConnecterStyle = settings.connector.rough
? ConnecterStyle.Scribbled
: ConnecterStyle.General;
const setConnecterStyle = useCallback(
(value: ConnecterStyle) => {
const isRough = value === ConnecterStyle.Scribbled;
editorSetting.set('connector', {
rough: isRough,
});
},
[editorSetting]
);
const connectorShapeItems = useMemo<RadioItem[]>(
() => [
{
value: ConnectorMode.Orthogonal as any,
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.elbowed'
](),
},
{
value: ConnectorMode.Curve as any,
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.curve'
](),
},
{
value: ConnectorMode.Straight as any,
label:
t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape.straight'
](),
},
],
[t]
);
const connectorShape: ConnectorMode = settings.connector.mode;
const setConnectorShape = useCallback(
(value: ConnectorMode) => {
editorSetting.set('connector', {
mode: value,
});
},
[editorSetting]
);
const borderStyleItems = useMemo<RadioItem[]>(
() => [
{
value: StrokeStyle.Solid,
label:
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
},
{
value: StrokeStyle.Dash,
label:
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
},
],
[t]
);
const borderStyle: StrokeStyle = settings.connector.strokeStyle;
const setBorderStyle = useCallback(
(value: StrokeStyle) => {
editorSetting.set('connector', {
strokeStyle: value,
});
},
[editorSetting]
);
const borderThickness = settings.connector.strokeWidth;
const setBorderThickness = useCallback(
(value: number[]) => {
editorSetting.set('connector', {
strokeWidth: value[0],
});
},
[editorSetting]
);
const colorItems = useMemo(() => {
const { stroke } = settings.connector;
return Object.entries(LineColor).map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', { stroke: value });
};
const isSelected = stroke === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const startEndPointItems = useMemo(() => {
const { frontEndpointStyle } = settings.connector;
return Object.entries(PointStyle).map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', { frontEndpointStyle: value });
};
const isSelected = frontEndpointStyle === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const endEndPointItems = useMemo(() => {
const { rearEndpointStyle } = settings.connector;
return Object.entries(PointStyle).map(([name, value]) => {
const handler = () => {
editorSetting.set('connector', { rearEndpointStyle: value });
};
const isSelected = rearEndpointStyle === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
return (
<>
<EdgelessSnapshot
title={t['com.affine.settings.editorSettings.edgeless.connecter']()}
option={['mock-option']}
type="mock-type"
/>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.color'
]()}
desc={''}
>
<Menu items={colorItems}>
<MenuTrigger className={menuTrigger}>
{String(settings.connector.stroke)}
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.style']()}
desc={''}
>
<RadioGroup
items={connecterStyleItems}
value={connecterStyle}
width={250}
className={settingWrapper}
onChange={setConnecterStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.connector-shape'
]()}
desc={''}
>
<RadioGroup
items={connectorShapeItems}
value={connectorShape}
width={250}
className={settingWrapper}
onChange={setConnectorShape}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.border-style'
]()}
desc={''}
>
<RadioGroup
items={borderStyleItems}
value={borderStyle}
width={250}
className={settingWrapper}
onChange={setBorderStyle}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.border-thickness'
]()}
desc={''}
>
<Slider
value={[borderThickness]}
onValueChange={setBorderThickness}
min={2}
max={12}
step={2}
nodes={[2, 4, 6, 8, 10, 12]}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.start-endpoint'
]()}
desc={''}
>
<Menu items={startEndPointItems}>
<MenuTrigger className={menuTrigger}>
{String(settings.connector.frontEndpointStyle)}
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.edgeless.connecter.end-endpoint'
]()}
desc={''}
>
<Menu items={endEndPointItems}>
<MenuTrigger className={menuTrigger}>
{String(settings.connector.rearEndpointStyle)}
</MenuTrigger>
</Menu>
</SettingRow>
</>
);
};

View File

@@ -1,7 +1,7 @@
import { SettingWrapper } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n';
import { ConnecterSettings } from './connecter';
import { ConnectorSettings } from './connector';
import { MindMapSettings } from './mind-map';
import { NoteSettings } from './note';
import { PenSettings } from './pen';
@@ -15,7 +15,7 @@ export const Edgeless = () => {
<NoteSettings />
<TextSettings />
<ShapeSettings />
<ConnecterSettings />
<ConnectorSettings />
<PenSettings />
<MindMapSettings />
</SettingWrapper>

View File

@@ -7,39 +7,139 @@ import {
Slider,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import {
NoteBackgroundColor,
NoteShadow,
StrokeStyle,
} from '@blocksuite/blocks';
import { useFramework, useLiveData } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
const CORNER_SIZE = [
{ name: 'None', value: 0 },
{ name: 'Small', value: 8 },
{ name: 'Medium', value: 16 },
{ name: 'Large', value: 24 },
{ name: 'Huge', value: 32 },
] as const;
export const NoteSettings = () => {
const t = useI18n();
const [borderStyle, setBorderStyle] = useState<'solid' | 'dash' | 'none'>(
'solid'
);
const [borderThickness, setBorderThickness] = useState<number[]>([3]);
const framework = useFramework();
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const borderStyleItems = useMemo<RadioItem[]>(
() => [
{
value: 'solid',
value: StrokeStyle.Solid,
label:
t['com.affine.settings.editorSettings.edgeless.note.border.solid'](),
},
{
value: 'dash',
value: StrokeStyle.Dash,
label:
t['com.affine.settings.editorSettings.edgeless.note.border.dash'](),
},
{
value: 'none',
value: StrokeStyle.None,
label:
t['com.affine.settings.editorSettings.edgeless.note.border.none'](),
},
],
[t]
);
const { borderStyle } = settings['affine:note'].edgeless.style;
const setBorderStyle = useCallback(
(value: StrokeStyle) => {
editorSetting.set('affine:note', {
edgeless: {
style: {
borderStyle: value,
},
},
});
},
[editorSetting]
);
const { borderSize } = settings['affine:note'].edgeless.style;
const setBorderSize = useCallback(
(value: number[]) => {
editorSetting.set('affine:note', {
edgeless: {
style: {
borderSize: value[0],
},
},
});
},
[editorSetting]
);
const backgroundItems = useMemo(() => {
const { background } = settings['affine:note'];
return Object.entries(NoteBackgroundColor).map(([name, value]) => {
const handler = () => {
editorSetting.set('affine:note', { background: value });
};
const isSelected = background === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const cornerItems = useMemo(() => {
const { borderRadius } = settings['affine:note'].edgeless.style;
return CORNER_SIZE.map(({ name, value }) => {
const handler = () => {
editorSetting.set('affine:note', {
edgeless: {
style: {
borderRadius: value,
},
},
});
};
const isSelected = borderRadius === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const shadowItems = useMemo(() => {
const { shadowType } = settings['affine:note'].edgeless.style;
return Object.entries(NoteShadow).map(([name, value]) => {
const handler = () => {
editorSetting.set('affine:note', {
edgeless: {
style: {
shadowType: value,
},
},
});
};
const isSelected = shadowType === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
return (
<>
<EdgelessSnapshot
@@ -53,9 +153,9 @@ export const NoteSettings = () => {
]()}
desc={''}
>
<Menu items={<MenuItem>Yellow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Yellow
<Menu items={backgroundItems}>
<MenuTrigger className={menuTrigger}>
{String(settings['affine:note'].background)}
</MenuTrigger>
</Menu>
</SettingRow>
@@ -63,9 +163,9 @@ export const NoteSettings = () => {
name={t['com.affine.settings.editorSettings.edgeless.note.corners']()}
desc={''}
>
<Menu items={<MenuItem>Small</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Small
<Menu items={cornerItems}>
<MenuTrigger className={menuTrigger}>
{String(settings['affine:note'].edgeless.style.borderRadius)}
</MenuTrigger>
</Menu>
</SettingRow>
@@ -73,9 +173,9 @@ export const NoteSettings = () => {
name={t['com.affine.settings.editorSettings.edgeless.note.shadow']()}
desc={''}
>
<Menu items={<MenuItem>Box shadow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Box shadow
<Menu items={shadowItems}>
<MenuTrigger className={menuTrigger}>
{String(settings['affine:note'].edgeless.style.shadowType)}
</MenuTrigger>
</Menu>
</SettingRow>
@@ -98,12 +198,12 @@ export const NoteSettings = () => {
desc={''}
>
<Slider
value={borderThickness}
onValueChange={setBorderThickness}
min={1}
max={5}
step={1}
nodes={[1, 2, 3, 4, 5]}
value={[borderSize]}
onValueChange={setBorderSize}
min={2}
max={12}
step={2}
nodes={[2, 4, 6, 8, 10, 12]}
/>
</SettingRow>
</>

View File

@@ -1,14 +1,45 @@
import { Menu, MenuItem, MenuTrigger, Slider } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { useI18n } from '@affine/i18n';
import { useState } from 'react';
import { LineColor } from '@blocksuite/blocks';
import { useFramework, useLiveData } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { menuTrigger } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const PenSettings = () => {
const t = useI18n();
const [borderThickness, setBorderThickness] = useState<number[]>([3]);
const framework = useFramework();
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const colorItems = useMemo(() => {
const { color } = settings.brush;
return Object.entries(LineColor).map(([name, value]) => {
const handler = () => {
editorSetting.set('brush', { color: value });
};
const isSelected = color === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const borderThickness = settings.brush.lineWidth;
const setBorderThickness = useCallback(
(value: number[]) => {
editorSetting.set('brush', {
lineWidth: value[0],
});
},
[editorSetting]
);
return (
<>
<EdgelessSnapshot
@@ -20,9 +51,9 @@ export const PenSettings = () => {
name={t['com.affine.settings.editorSettings.edgeless.pen.color']()}
desc={''}
>
<Menu items={<MenuItem>Yellow</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Yellow
<Menu items={colorItems}>
<MenuTrigger className={menuTrigger}>
{String(settings.brush.color)}
</MenuTrigger>
</Menu>
</SettingRow>
@@ -31,12 +62,12 @@ export const PenSettings = () => {
desc={''}
>
<Slider
value={borderThickness}
value={[borderThickness]}
onValueChange={setBorderThickness}
min={1}
max={5}
step={1}
nodes={[1, 2, 3, 4, 5]}
min={2}
max={12}
step={2}
nodes={[2, 4, 6, 8, 10, 12]}
/>
</SettingRow>
</>

View File

@@ -6,37 +6,46 @@ import {
type RadioItem,
} from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import { useI18n } from '@affine/i18n';
import { useMemo, useState } from 'react';
import {
FontFamily,
FontFamilyMap,
FontStyle,
FontWeight,
LineColor,
TextAlign,
} from '@blocksuite/blocks';
import { useFramework, useLiveData } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
export const TextSettings = () => {
const t = useI18n();
const [textAlignment, setTextAlignment] = useState<
'left' | 'center' | 'right'
>('left');
const framework = useFramework();
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const alignItems = useMemo<RadioItem[]>(
() => [
{
value: 'left',
value: TextAlign.Left,
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.left'
](),
},
{
value: 'center',
value: TextAlign.Center,
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.center'
](),
},
{
value: 'right',
value: TextAlign.Right,
label:
t[
'com.affine.settings.editorSettings.edgeless.text.alignment.right'
@@ -46,6 +55,76 @@ export const TextSettings = () => {
[t]
);
const { textAlign } = settings['affine:edgeless-text'];
const setTextAlign = useCallback(
(value: TextAlign) => {
editorSetting.set('affine:edgeless-text', {
textAlign: value,
});
},
[editorSetting]
);
const colorItems = useMemo(() => {
const { color } = settings['affine:edgeless-text'];
return Object.entries(LineColor).map(([name, value]) => {
const handler = () => {
editorSetting.set('affine:edgeless-text', { color: value });
};
const isSelected = color === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const fontFamilyItems = useMemo(() => {
const { fontFamily } = settings['affine:edgeless-text'];
return Object.entries(FontFamily).map(([name, value]) => {
const handler = () => {
editorSetting.set('affine:edgeless-text', { fontFamily: value });
};
const isSelected = fontFamily === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const fontStyleItems = useMemo(() => {
const { fontStyle } = settings['affine:edgeless-text'];
return Object.entries(FontStyle).map(([name, value]) => {
const handler = () => {
editorSetting.set('affine:edgeless-text', { fontStyle: value });
};
const isSelected = fontStyle === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
const fontWeightItems = useMemo(() => {
const { fontWeight } = settings['affine:edgeless-text'];
return Object.entries(FontWeight).map(([name, value]) => {
const handler = () => {
editorSetting.set('affine:edgeless-text', { fontWeight: value });
};
const isSelected = fontWeight === value;
return (
<MenuItem key={name} onSelect={handler} selected={isSelected}>
{name}
</MenuItem>
);
});
}, [editorSetting, settings]);
return (
<>
<EdgelessSnapshot
@@ -57,29 +136,33 @@ export const TextSettings = () => {
name={t['com.affine.settings.editorSettings.edgeless.text.color']()}
desc={''}
>
<Menu items={<MenuItem>Blue</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Blue
<Menu items={colorItems}>
<MenuTrigger className={menuTrigger}>
{String(settings['affine:edgeless-text'].color)}
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.text.font']()}
name={t[
'com.affine.settings.editorSettings.edgeless.text.font-family'
]()}
desc={''}
>
<Menu items={<MenuItem>Inter</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Inter
<Menu items={fontFamilyItems}>
<MenuTrigger className={menuTrigger}>
{FontFamilyMap[settings['affine:edgeless-text'].fontFamily]}
</MenuTrigger>
</Menu>
</SettingRow>
<SettingRow
name={t['com.affine.settings.editorSettings.edgeless.text.font-size']()}
name={t[
'com.affine.settings.editorSettings.edgeless.text.font-style'
]()}
desc={''}
>
<Menu items={<MenuItem>15px</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
15px
<Menu items={fontStyleItems}>
<MenuTrigger className={menuTrigger}>
{String(settings['affine:edgeless-text'].fontStyle)}
</MenuTrigger>
</Menu>
</SettingRow>
@@ -89,9 +172,9 @@ export const TextSettings = () => {
]()}
desc={''}
>
<Menu items={<MenuItem>Regular</MenuItem>}>
<MenuTrigger className={menuTrigger} disabled>
Regular
<Menu items={fontWeightItems}>
<MenuTrigger className={menuTrigger}>
{settings['affine:edgeless-text'].fontWeight}
</MenuTrigger>
</Menu>
</SettingRow>
@@ -101,10 +184,10 @@ export const TextSettings = () => {
>
<RadioGroup
items={alignItems}
value={textAlignment}
value={textAlign}
width={250}
className={settingWrapper}
onChange={setTextAlignment}
onChange={setTextAlign}
/>
</SettingRow>
</>

View File

@@ -3,6 +3,7 @@ import {
AIPageRootBlockSpec,
} from '@affine/core/blocksuite/presets/ai';
import { mixpanel } from '@affine/core/mixpanel';
import { EditorSettingService } from '@affine/core/modules/editor-settting';
import {
BlockFlavourIdentifier,
BlockServiceIdentifier,
@@ -54,6 +55,7 @@ function withAffineRootService(Service: typeof PageRootService) {
export function createPageRootBlockSpec(
framework: FrameworkProvider
): ExtensionType[] {
const editorSettingService = framework.get(EditorSettingService);
return [
...AIPageRootBlockSpec,
{
@@ -67,6 +69,7 @@ export function createPageRootBlockSpec(
},
ConfigExtension('affine:page', {
linkedWidget: createLinkedWidgetConfig(framework),
editorSetting: editorSettingService.editorSetting.settingSignal,
}),
];
}
@@ -74,6 +77,7 @@ export function createPageRootBlockSpec(
export function createEdgelessRootBlockSpec(
framework: FrameworkProvider
): ExtensionType[] {
const editorSettingService = framework.get(EditorSettingService);
return [
...AIEdgelessRootBlockSpec,
{
@@ -87,6 +91,7 @@ export function createEdgelessRootBlockSpec(
},
ConfigExtension('affine:page', {
linkedWidget: createLinkedWidgetConfig(framework),
editorSetting: editorSettingService.editorSetting.settingSignal,
}),
];
}

View File

@@ -1,7 +1,6 @@
import { Framework, GlobalState, MemoryMemento } from '@toeverything/infra';
import { expect, test } from 'vitest';
import { unflattenObject } from '../../../utils/unflatten-object';
import { EditorSetting } from '../entities/editor-setting';
import { GlobalStateEditorSettingProvider } from '../impls/global-state';
import { EditorSettingProvider } from '../provider/editor-setting-provider';
@@ -23,27 +22,23 @@ test('editor setting service', () => {
const editorSettingService = provider.get(EditorSettingService);
// default value
expect(editorSettingService.editorSetting.settings$.value).toMatchObject({
fontFamily: 'Sans',
'connector.stroke': '#000000',
});
expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Sans');
// set plain object
editorSettingService.editorSetting.set('fontFamily', 'Serif');
expect(editorSettingService.editorSetting.settings$.value).toMatchObject({
fontFamily: 'Serif',
});
expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Serif');
// nested object, should be serialized
editorSettingService.editorSetting.set('connector.stroke', {
// set nested object
editorSettingService.editorSetting.set('connector', {
stroke: {
dark: '#000000',
light: '#ffffff',
},
});
expect(editorSettingService.editorSetting.get('connector').stroke).toEqual({
dark: '#000000',
light: '#ffffff',
});
expect(
(
editorSettingService.editorSetting
.provider as GlobalStateEditorSettingProvider
).get('connector.stroke')
).toBe('{"dark":"#000000","light":"#ffffff"}');
// invalid font family
editorSettingService.editorSetting.provider.set(
@@ -51,22 +46,6 @@ test('editor setting service', () => {
JSON.stringify('abc')
);
// should fallback to default value
expect(editorSettingService.editorSetting.settings$.value['fontFamily']).toBe(
'Sans'
);
// expend demo
const expended = unflattenObject(
editorSettingService.editorSetting.settings$.value
);
expect(expended).toMatchObject({
fontFamily: 'Sans',
connector: {
stroke: {
dark: '#000000',
light: '#ffffff',
},
},
});
// fallback to default value
expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Sans');
});

View File

@@ -1,5 +1,12 @@
import {
createSignalFromObservable,
type Signal,
} from '@blocksuite/affine-shared/utils';
import type { DeepPartial } from '@blocksuite/global/utils';
import { Entity, LiveData } from '@toeverything/infra';
import { map, type Observable } from 'rxjs';
import { isObject, merge } from 'lodash-es';
import type { Observable } from 'rxjs';
import { map } from 'rxjs';
import type { EditorSettingProvider } from '../provider/editor-setting-provider';
import { EditorSettingSchema } from '../schema';
@@ -7,17 +14,30 @@ import { EditorSettingSchema } from '../schema';
export class EditorSetting extends Entity {
constructor(public readonly provider: EditorSettingProvider) {
super();
const { signal, cleanup } = createSignalFromObservable<
Partial<EditorSettingSchema>
>(this.settings$, {});
this.settingSignal = signal;
this.disposables.push(cleanup);
}
settings$ = LiveData.from<EditorSettingSchema>(this.watchAll(), null as any);
settingSignal: Signal<Partial<EditorSettingSchema>>;
get<K extends keyof EditorSettingSchema>(key: K) {
return this.settings$.value[key];
}
set<K extends keyof EditorSettingSchema>(
key: K,
value: EditorSettingSchema[K]
value: DeepPartial<EditorSettingSchema[K]>
) {
const schema = EditorSettingSchema.shape[key];
this.provider.set(key, JSON.stringify(schema.parse(value)));
const curValue = this.get(key);
const nextValue = isObject(curValue) ? merge(curValue, value) : value;
this.provider.set(key, JSON.stringify(schema.parse(nextValue)));
}
private watchAll(): Observable<EditorSettingSchema> {

View File

@@ -1,5 +1,21 @@
import {
BrushSchema,
ConnectorSchema,
EdgelessTextSchema,
NoteSchema,
ShapeSchema,
} from '@blocksuite/affine-shared/utils';
import { z } from 'zod';
// TODO import from BlockSuite
export const BSEditorSettingSchema = z.object({
connector: ConnectorSchema,
brush: BrushSchema,
shape: ShapeSchema,
'affine:edgeless-text': EdgelessTextSchema,
'affine:note': NoteSchema,
});
export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom';
export const fontStyleOptions = [
@@ -12,20 +28,6 @@ export const fontStyleOptions = [
value: string;
}[];
const BSEditorSettingSchema = z.object({
// TODO: import from bs
connector: z.object({
stroke: z
.union([
z.string(),
z.object({
dark: z.string(),
light: z.string(),
}),
])
.default('#000000'),
}),
});
const AffineEditorSettingSchema = z.object({
fontFamily: z.enum(['Sans', 'Serif', 'Mono', 'Custom']).default('Sans'),
customFontFamily: z.string().default(''),
@@ -36,44 +38,8 @@ const AffineEditorSettingSchema = z.object({
displayBiDirectionalLink: z.boolean().default(true),
});
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
x: infer I
) => void
? I
: never;
type FlattenZodObject<O, Prefix extends string = ''> =
O extends z.ZodObject<infer T>
? {
[A in keyof T]: T[A] extends z.ZodObject<any>
? A extends string
? FlattenZodObject<T[A], `${Prefix}${A}.`>
: never
: A extends string
? { [key in `${Prefix}${A}`]: T[A] }
: never;
}[keyof T]
: never;
function flattenZodObject<S extends z.ZodObject<any>>(
schema: S,
target: z.ZodObject<any> = z.object({}),
prefix = ''
) {
for (const key in schema.shape) {
const value = schema.shape[key];
if (value instanceof z.ZodObject) {
flattenZodObject(value, target, prefix + key + '.');
} else {
target.shape[prefix + key] = value;
}
}
type Result = UnionToIntersection<FlattenZodObject<S>>;
return target as Result extends z.ZodRawShape ? z.ZodObject<Result> : never;
}
export const EditorSettingSchema = flattenZodObject(
BSEditorSettingSchema.merge(AffineEditorSettingSchema)
export const EditorSettingSchema = BSEditorSettingSchema.merge(
AffineEditorSettingSchema
);
export type EditorSettingSchema = z.infer<typeof EditorSettingSchema>;

View File

@@ -19,7 +19,6 @@ import { UserQuotaChanged } from '../../cloud/services/user-quota';
export class TelemetryService extends Service {
private prevQuota: NonNullable<QuotaQuery['currentUser']>['quota'] | null =
null;
private readonly disposables: (() => void)[] = [];
constructor(
private readonly auth: AuthService,

View File

@@ -1,19 +0,0 @@
import { expect, test } from 'vitest';
import { unflattenObject } from '../unflatten-object';
test('unflattenObject', () => {
const ob = {
'a.b.c': 1,
d: 2,
};
const result = unflattenObject(ob);
expect(result).toEqual({
a: {
b: {
c: 1,
},
},
d: 2,
});
});

View File

@@ -1270,6 +1270,8 @@
"com.affine.settings.editorSettings.edgeless.text": "Text",
"com.affine.settings.editorSettings.edgeless.text.color": "Text color",
"com.affine.settings.editorSettings.edgeless.text.font": "Font",
"com.affine.settings.editorSettings.edgeless.text.font-family": "Font Family",
"com.affine.settings.editorSettings.edgeless.text.font-style": "Font Style",
"com.affine.settings.editorSettings.edgeless.text.font-size": "Font size",
"com.affine.settings.editorSettings.edgeless.text.font-weight": "Font weight",
"com.affine.settings.editorSettings.edgeless.text.alignment": "Alignment",