diff --git a/packages/frontend/apps/electron/renderer/app.tsx b/packages/frontend/apps/electron/renderer/app.tsx index a9afa1d32ae5f..75a5d3d061b9d 100644 --- a/packages/frontend/apps/electron/renderer/app.tsx +++ b/packages/frontend/apps/electron/renderer/app.tsx @@ -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'; @@ -78,6 +81,7 @@ configureDesktopWorkbenchModule(framework); configureAppTabsHeaderModule(framework); configureFindInPageModule(framework); configureDesktopApiModule(framework); +configureSpellCheckSettingModule(framework); framework.impl(PopupWindowProvider, p => { const apis = p.get(DesktopApiService).api; diff --git a/packages/frontend/apps/electron/src/main/windows-manager/tab-views-meta-schema.ts b/packages/frontend/apps/electron/src/main/shared-state-schema.ts similarity index 82% rename from packages/frontend/apps/electron/src/main/windows-manager/tab-views-meta-schema.ts rename to packages/frontend/apps/electron/src/main/shared-state-schema.ts index 18a77dba12438..8d78745ca7197 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/tab-views-meta-schema.ts +++ b/packages/frontend/apps/electron/src/main/shared-state-schema.ts @@ -43,3 +43,11 @@ export type TabViewsMetaSchema = z.infer; export type WorkbenchMeta = z.infer; export type WorkbenchViewMeta = z.infer; export type WorkbenchViewModule = z.infer; + +export const SpellCheckStateSchema = z.object({ + enabled: z.boolean().optional(), +}); + +export const SpellCheckStateKey = 'spellCheckState'; +export type SpellCheckStateKey = typeof SpellCheckStateKey; +export type SpellCheckStateSchema = z.infer; diff --git a/packages/frontend/apps/electron/src/main/ui/handlers.ts b/packages/frontend/apps/electron/src/main/ui/handlers.ts index 903546584de38..0480c23a3410c 100644 --- a/packages/frontend/apps/electron/src/main/ui/handlers.ts +++ b/packages/frontend/apps/electron/src/main/ui/handlers.ts @@ -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, @@ -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'; @@ -216,4 +216,8 @@ export const uiHandlers = { win.show(); win.focus(); }, + restartApp: async () => { + app.relaunch(); + app.quit(); + }, } satisfies NamespaceHandlers; diff --git a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts index 7c5b02f7ada28..a47b89a1072de 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts @@ -2,6 +2,8 @@ import { join } from 'node:path'; import { app, + Menu, + MenuItem, session, type View, type WebContents, @@ -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'); @@ -74,6 +78,10 @@ const TabViewsMetaState = { }, }; +const spellCheckSettings = SpellCheckStateSchema.parse( + globalStateStorage.get(SpellCheckStateKey) ?? {} +); + type AddTabAction = { type: 'add-tab'; payload: WorkbenchMeta; @@ -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 = () => {}; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx index 47964c6a3d28b..acd66b242d09b 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx @@ -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'; @@ -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) => { @@ -351,55 +355,6 @@ const NewDocDefaultModeSettings = () => { ); }; -export const DeFaultCodeBlockSettings = () => { - const t = useI18n(); - return ( - <> - - Plain Text} - trigger={ - - Plain Text - - } - /> - - - - - - ); -}; - -export const SpellCheckSettings = () => { - const t = useI18n(); - return ( - - - - ); -}; - const AISettings = () => { const t = useI18n(); const { openConfirmModal } = useConfirmModal(); @@ -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 ( + + + Settings changed; please restart the app. + + + + ) : ( + t[ + 'com.affine.settings.editorSettings.general.spell-check.description' + ]() + ) + } + > + + + ); +}; + export const General = () => { const t = useI18n(); @@ -469,9 +474,10 @@ export const General = () => { + {BUILD_CONFIG.isElectron && } {/* // TODO(@akumatus): implement these settings - - */} + + */} ); }; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/style.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/style.css.ts index 8fb0177d37055..e2b47528b7d4e 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/style.css.ts +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/style.css.ts @@ -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', +}); diff --git a/packages/frontend/core/src/modules/editor-setting/entities/editor-setting.ts b/packages/frontend/core/src/modules/editor-setting/entities/editor-setting.ts index dd2535784b318..bd8104ee5270b 100644 --- a/packages/frontend/core/src/modules/editor-setting/entities/editor-setting.ts +++ b/packages/frontend/core/src/modules/editor-setting/entities/editor-setting.ts @@ -15,7 +15,7 @@ type SettingItem = { readonly value: T; set: (value: T) => void; // eslint-disable-next-line rxjs/finnish - $: T; + $: LiveData; }; export class EditorSetting extends Entity { diff --git a/packages/frontend/core/src/modules/editor-setting/index.ts b/packages/frontend/core/src/modules/editor-setting/index.ts index 68a36e55bbf30..6ce61f611f062 100644 --- a/packages/frontend/core/src/modules/editor-setting/index.ts +++ b/packages/frontend/core/src/modules/editor-setting/index.ts @@ -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'; @@ -18,3 +23,7 @@ export function configureEditorSettingModule(framework: Framework) { GlobalState, ]); } + +export function configureSpellCheckSettingModule(framework: Framework) { + framework.service(SpellCheckSettingService, [GlobalStateService]); +} diff --git a/packages/frontend/core/src/modules/editor-setting/schema.ts b/packages/frontend/core/src/modules/editor-setting/schema.ts index 1bbfd807981e5..c714e450e0884 100644 --- a/packages/frontend/core/src/modules/editor-setting/schema.ts +++ b/packages/frontend/core/src/modules/editor-setting/schema.ts @@ -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), diff --git a/packages/frontend/core/src/modules/editor-setting/services/spell-check-setting.ts b/packages/frontend/core/src/modules/editor-setting/services/spell-check-setting.ts new file mode 100644 index 0000000000000..406b184f2899c --- /dev/null +++ b/packages/frontend/core/src/modules/editor-setting/services/spell-check-setting.ts @@ -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, + }); + } +} diff --git a/packages/frontend/electron-api/src/index.ts b/packages/frontend/electron-api/src/index.ts index 9d2e448a0a865..33a8f9d2be0ce 100644 --- a/packages/frontend/electron-api/src/index.ts +++ b/packages/frontend/electron-api/src/index.ts @@ -41,10 +41,11 @@ export const sharedStorage = (globalThis as any).__sharedStorage as export type { SharedStorage }; -export type { UpdateMeta } from '@affine/electron/main/updater/event'; export { + type SpellCheckStateSchema, type TabViewsMetaSchema, type WorkbenchMeta, type WorkbenchViewMeta, type WorkbenchViewModule, -} from '@affine/electron/main/windows-manager/tab-views-meta-schema'; +} from '@affine/electron/main/shared-state-schema'; +export type { UpdateMeta } from '@affine/electron/main/updater/event'; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 082961a318c98..f6afd8c7f2edb 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1123,6 +1123,7 @@ "com.affine.settings.editorSettings.general.font-family.title": "Font family", "com.affine.settings.editorSettings.general.spell-check.description": "Automatically detect and correct spelling errors.", "com.affine.settings.editorSettings.general.spell-check.title": "Spell check", + "com.affine.settings.editorSettings.general.spell-check.restart-hint": "Settings changed; please restart the app. <1>Restart", "com.affine.settings.editorSettings.page": "Page", "com.affine.settings.editorSettings.page.display-bi-link.description": "Display bi-directional links on the doc.", "com.affine.settings.editorSettings.page.display-bi-link.title": "Display bi-directional links",