Skip to content

Commit

Permalink
feat(electron): spellcheck setting
Browse files Browse the repository at this point in the history
  • Loading branch information
pengx17 committed Nov 11, 2024
1 parent b374924 commit b0024a1
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 65 deletions.
6 changes: 5 additions & 1 deletion packages/frontend/apps/electron/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
DesktopApiService,
} from '@affine/core/modules/desktop-api';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import {
configureSpellCheckSettingModule,
EditorSettingService,
} from '@affine/core/modules/editor-setting';
import { configureFindInPageModule } from '@affine/core/modules/find-in-page';
import { I18nProvider } from '@affine/core/modules/i18n';
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
Expand Down Expand Up @@ -78,6 +81,7 @@ configureDesktopWorkbenchModule(framework);
configureAppTabsHeaderModule(framework);
configureFindInPageModule(framework);
configureDesktopApiModule(framework);
configureSpellCheckSettingModule(framework);

framework.impl(PopupWindowProvider, p => {
const apis = p.get(DesktopApiService).api;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ export type TabViewsMetaSchema = z.infer<typeof tabViewsMetaSchema>;
export type WorkbenchMeta = z.infer<typeof workbenchMetaSchema>;
export type WorkbenchViewMeta = z.infer<typeof workbenchViewMetaSchema>;
export type WorkbenchViewModule = z.infer<typeof workbenchViewIconNameSchema>;

export const SpellCheckStateSchema = z.object({
enabled: z.boolean().optional(),
});

export const SpellCheckStateKey = 'spellCheckState';
export type SpellCheckStateKey = typeof SpellCheckStateKey;
export type SpellCheckStateSchema = z.infer<typeof SpellCheckStateSchema>;
6 changes: 5 additions & 1 deletion packages/frontend/apps/electron/src/main/ui/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getLinkPreview } from 'link-preview-js';

import { persistentConfig } from '../config-storage/persist';
import { logger } from '../logger';
import type { WorkbenchViewMeta } from '../shared-state-schema';
import type { NamespaceHandlers } from '../type';
import {
activateView,
Expand All @@ -27,7 +28,6 @@ import {
} from '../windows-manager';
import { showTabContextMenu } from '../windows-manager/context-menu';
import { getOrCreateCustomThemeWindow } from '../windows-manager/custom-theme-window';
import type { WorkbenchViewMeta } from '../windows-manager/tab-views-meta-schema';
import { getChallengeResponse } from './challenge';
import { uiSubjects } from './subject';

Expand Down Expand Up @@ -216,4 +216,8 @@ export const uiHandlers = {
win.show();
win.focus();
},
restartApp: async () => {
app.relaunch();
app.quit();
},
} satisfies NamespaceHandlers;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { join } from 'node:path';

import {
app,
Menu,
MenuItem,
session,
type View,
type WebContents,
Expand All @@ -26,16 +28,18 @@ import { CLOUD_BASE_URL, isDev } from '../config';
import { mainWindowOrigin, shellViewUrl } from '../constants';
import { ensureHelperProcess } from '../helper-process';
import { logger } from '../logger';
import { globalStateStorage } from '../shared-storage/storage';
import { getCustomThemeWindow } from './custom-theme-window';
import { getMainWindow, MainWindowManager } from './main-window';
import {
SpellCheckStateKey,
SpellCheckStateSchema,
TabViewsMetaKey,
type TabViewsMetaSchema,
tabViewsMetaSchema,
type WorkbenchMeta,
type WorkbenchViewMeta,
} from './tab-views-meta-schema';
} from '../shared-state-schema';
import { globalStateStorage } from '../shared-storage/storage';
import { getCustomThemeWindow } from './custom-theme-window';
import { getMainWindow, MainWindowManager } from './main-window';

async function getAdditionalArguments() {
const { getExposedMeta } = await import('../exposed');
Expand Down Expand Up @@ -74,6 +78,10 @@ const TabViewsMetaState = {
},
};

const spellCheckSettings = SpellCheckStateSchema.parse(
globalStateStorage.get(SpellCheckStateKey) ?? {}
);

type AddTabAction = {
type: 'add-tab';
payload: WorkbenchMeta;
Expand Down Expand Up @@ -816,13 +824,44 @@ export class WebContentViewsManager {
transparent: true,
contextIsolation: true,
sandbox: false,
spellcheck: false, // TODO(@pengx17): enable?
spellcheck: spellCheckSettings.enabled,
preload: join(__dirname, './preload.js'), // this points to the bundled preload module
// serialize exposed meta that to be used in preload
additionalArguments: additionalArguments,
},
});

if (spellCheckSettings.enabled) {
view.webContents.on('context-menu', (_event, params) => {
const menu = new Menu();

// Add each spelling suggestion
for (const suggestion of params.dictionarySuggestions) {
menu.append(
new MenuItem({
label: suggestion,
click: () => view.webContents.replaceMisspelling(suggestion),
})
);
}

// Allow users to add the misspelled word to the dictionary
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: 'Add to dictionary', // TODO: i18n
click: () =>
view.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
),
})
);
}

menu.popup();
});
}

this.webViewsMap$.next(this.tabViewsMap.set(viewId, view));
let unsub = () => {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,26 @@ import {
SettingRow,
SettingWrapper,
} from '@affine/component/setting-components';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { ServerConfigService } from '@affine/core/modules/cloud';
import { DesktopApiService } from '@affine/core/modules/desktop-api';
import {
EditorSettingService,
type FontFamily,
fontStyleOptions,
} from '@affine/core/modules/editor-setting';
import { SpellCheckSettingService } from '@affine/core/modules/editor-setting/services/spell-check-setting';
import {
type FontData,
SystemFontFamilyService,
} from '@affine/core/modules/system-font-family';
import { useI18n } from '@affine/i18n';
import { Trans, useI18n } from '@affine/i18n';
import type { DocMode } from '@blocksuite/affine/blocks';
import { DoneIcon, SearchIcon } from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useLiveData,
useService,
useServices,
} from '@toeverything/infra';
import clsx from 'clsx';
Expand All @@ -41,10 +45,10 @@ import {
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Virtuoso } from 'react-virtuoso';

import { DropdownMenu } from './menu';
import * as styles from './style.css';

const getLabel = (fontKey: FontFamily, t: ReturnType<typeof useI18n>) => {
Expand Down Expand Up @@ -351,55 +355,6 @@ const NewDocDefaultModeSettings = () => {
);
};

export const DeFaultCodeBlockSettings = () => {
const t = useI18n();
return (
<>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.default-code-block.language.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.default-code-block.language.description'
]()}
>
<DropdownMenu
items={<MenuItem>Plain Text</MenuItem>}
trigger={
<MenuTrigger className={styles.menuTrigger} disabled>
Plain Text
</MenuTrigger>
}
/>
</SettingRow>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.default-code-block.wrap.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.default-code-block.wrap.description'
]()}
>
<Switch />
</SettingRow>
</>
);
};

export const SpellCheckSettings = () => {
const t = useI18n();
return (
<SettingRow
name={t['com.affine.settings.editorSettings.general.spell-check.title']()}
desc={t[
'com.affine.settings.editorSettings.general.spell-check.description'
]()}
>
<Switch />
</SettingRow>
);
};

const AISettings = () => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
Expand Down Expand Up @@ -460,6 +415,56 @@ const AISettings = () => {
);
};

const SpellCheckSettings = () => {
const t = useI18n();
const spellCheckSetting = useService(SpellCheckSettingService);

const desktopApiService = useService(DesktopApiService);

const enabled = useLiveData(spellCheckSetting.enabled$)?.enabled;

const [requireRestart, setRequireRestart] = useState(false);

const onToggleSpellCheck = useCallback(
(checked: boolean) => {
spellCheckSetting.setEnabled(checked);
setRequireRestart(true);
},
[spellCheckSetting]
);

const onRestart = useAsyncCallback(async () => {
await desktopApiService.handler.ui.restartApp();
}, [desktopApiService]);

return (
<SettingRow
name={t['com.affine.settings.editorSettings.general.spell-check.title']()}
desc={
requireRestart ? (
<div className={styles.spellCheckSettingDescription}>
<Trans i18nKey="com.affine.settings.editorSettings.general.spell-check.restart-hint">
Settings changed; please restart the app.
<button
onClick={onRestart}
className={styles.spellCheckSettingDescriptionButton}
>
Restart
</button>
</Trans>
</div>
) : (
t[
'com.affine.settings.editorSettings.general.spell-check.description'
]()
)
}
>
<Switch checked={enabled} onChange={onToggleSpellCheck} />
</SettingRow>
);
};

export const General = () => {
const t = useI18n();

Expand All @@ -469,9 +474,10 @@ export const General = () => {
<FontFamilySettings />
<CustomFontFamilySettings />
<NewDocDefaultModeSettings />
{BUILD_CONFIG.isElectron && <SpellCheckSettings />}
{/* // TODO(@akumatus): implement these settings
<DeFaultCodeBlockSettings />
<SpellCheckSettings /> */}
<DeFaultCodeBlockSettings />
*/}
</SettingWrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,12 @@ export const notFound = style({
fontSize: cssVar('fontXs'),
padding: '4px',
});

export const spellCheckSettingDescription = style({
color: cssVarV2('toast/iconState/error'),
});

export const spellCheckSettingDescriptionButton = style({
color: cssVarV2('text/link'),
fontSize: 'inherit',
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type SettingItem<T> = {
readonly value: T;
set: (value: T) => void;
// eslint-disable-next-line rxjs/finnish
$: T;
$: LiveData<T>;
};

export class EditorSetting extends Entity {
Expand Down
11 changes: 10 additions & 1 deletion packages/frontend/core/src/modules/editor-setting/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { type Framework, GlobalState } from '@toeverything/infra';
import {
type Framework,
GlobalState,
GlobalStateService,
} from '@toeverything/infra';

import { UserDBService } from '../userspace';
import { EditorSetting } from './entities/editor-setting';
import { CurrentUserDBEditorSettingProvider } from './impls/user-db';
import { EditorSettingProvider } from './provider/editor-setting-provider';
import { EditorSettingService } from './services/editor-setting';
import { SpellCheckSettingService } from './services/spell-check-setting';
export type { FontFamily } from './schema';
export { EditorSettingSchema, fontStyleOptions } from './schema';
export { EditorSettingService } from './services/editor-setting';
Expand All @@ -18,3 +23,7 @@ export function configureEditorSettingModule(framework: Framework) {
GlobalState,
]);
}

export function configureSpellCheckSettingModule(framework: Framework) {
framework.service(SpellCheckSettingService, [GlobalStateService]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const AffineEditorSettingSchema = z.object({
fontFamily: z.enum(['Sans', 'Serif', 'Mono', 'Custom']).default('Sans'),
customFontFamily: z.string().default(''),
newDocDefaultMode: z.enum(['edgeless', 'page']).default('page'),
spellCheck: z.boolean().default(false),
fullWidthLayout: z.boolean().default(false),
displayDocInfo: z.boolean().default(true),
displayBiDirectionalLink: z.boolean().default(true),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type {
SpellCheckStateKey,
SpellCheckStateSchema,
} from '@affine/electron/main/shared-state-schema';
import type { GlobalStateService } from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';

const SPELL_CHECK_SETTING_KEY: SpellCheckStateKey = 'spellCheckState';

export class SpellCheckSettingService extends Service {
constructor(private readonly globalStateService: GlobalStateService) {
super();
}

enabled$ = LiveData.from(
this.globalStateService.globalState.watch<
SpellCheckStateSchema | undefined
>(SPELL_CHECK_SETTING_KEY),
{ enabled: false }
);

setEnabled(enabled: boolean) {
this.globalStateService.globalState.set(SPELL_CHECK_SETTING_KEY, {
enabled,
});
}
}
Loading

0 comments on commit b0024a1

Please sign in to comment.