fix: support spell check for mini app (#7602)

* feat(IpcChannel): add Webview_SetSpellCheckEnabled channel and implement spell check handling for webviews

- Introduced a new IPC channel for enabling/disabling spell check in webviews.
- Updated the registerIpc function to handle spell check settings for all webviews.
- Enhanced WebviewContainer to set spell check state on DOM ready event.
- Refactored context menu setup to accommodate webview context menus.

* refactor(ContextMenu): update methods to use Electron.WebContents instead of BrowserWindow

- Changed method signatures to accept Electron.WebContents for better context handling.
- Updated internal calls to utilize the new WebContents reference for toggling dev tools and managing spell check functionality.

* refactor(WebviewContainer): clean up import order and remove unused code

- Adjusted the import order in WebviewContainer.tsx for better readability.
- Removed redundant import of useSettings to streamline the component.
This commit is contained in:
beyondkmp 2025-06-28 08:36:32 +08:00 committed by GitHub
parent 2d3f5baf72
commit 14e31018f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 59 additions and 37 deletions

View File

@ -38,6 +38,7 @@ export enum IpcChannel {
Notification_OnClick = 'notification:on-click',
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
// Open
Open_Path = 'open:path',

View File

@ -8,7 +8,7 @@ import { handleZoomFactor } from '@main/utils/zoom'
import { UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, dialog, ipcMain, session, shell } from 'electron'
import { BrowserWindow, dialog, ipcMain, session, shell, webContents } from 'electron'
import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification'
@ -93,9 +93,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// spell check
ipcMain.handle(IpcChannel.App_SetEnableSpellCheck, (_, isEnable: boolean) => {
const windows = BrowserWindow.getAllWindows()
windows.forEach((window) => {
window.webContents.session.setSpellCheckerEnabled(isEnable)
// disable spell check for all webviews
const webviews = webContents.getAllWebContents()
webviews.forEach((webview) => {
webview.session.setSpellCheckerEnabled(isEnable)
})
})
@ -494,6 +495,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
setOpenLinkExternal(webviewId, isExternal)
)
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
const webview = webContents.fromId(webviewId)
if (!webview) return
webview.session.setSpellCheckerEnabled(isEnable)
})
// store sync
storeSyncService.registerIpcHandler()

View File

@ -4,8 +4,8 @@ import { locales } from '../utils/locales'
import { configManager } from './ConfigManager'
class ContextMenu {
public contextMenu(w: Electron.BrowserWindow) {
w.webContents.on('context-menu', (_event, properties) => {
public contextMenu(w: Electron.WebContents) {
w.on('context-menu', (_event, properties) => {
const template: MenuItemConstructorOptions[] = this.createEditMenuItems(properties)
const filtered = template.filter((item) => item.visible !== false)
if (filtered.length > 0) {
@ -26,7 +26,7 @@ class ContextMenu {
})
}
private createInspectMenuItems(w: Electron.BrowserWindow): MenuItemConstructorOptions[] {
private createInspectMenuItems(w: Electron.WebContents): MenuItemConstructorOptions[] {
const locale = locales[configManager.getLanguage()]
const { common } = locale.translation
const template: MenuItemConstructorOptions[] = [
@ -34,7 +34,7 @@ class ContextMenu {
id: 'inspect',
label: common.inspect,
click: () => {
w.webContents.toggleDevTools()
w.toggleDevTools()
},
enabled: true
}
@ -86,7 +86,7 @@ class ContextMenu {
private createSpellCheckMenuItem(
properties: Electron.ContextMenuParams,
mainWindow: Electron.BrowserWindow
w: Electron.WebContents
): MenuItemConstructorOptions {
const hasText = properties.selectionText.length > 0
@ -95,14 +95,14 @@ class ContextMenu {
label: '&Learn Spelling',
visible: Boolean(properties.isEditable && hasText && properties.misspelledWord),
click: () => {
mainWindow.webContents.session.addWordToSpellCheckerDictionary(properties.misspelledWord)
w.session.addWordToSpellCheckerDictionary(properties.misspelledWord)
}
}
}
private createDictionarySuggestions(
properties: Electron.ContextMenuParams,
mainWindow: Electron.BrowserWindow
w: Electron.WebContents
): MenuItemConstructorOptions[] {
const hasText = properties.selectionText.length > 0
@ -126,7 +126,7 @@ class ContextMenu {
label: suggestion,
visible: Boolean(properties.isEditable && hasText && properties.misspelledWord),
click: (menuItem: Electron.MenuItem) => {
mainWindow.webContents.replaceMisspelling(menuItem.label)
w.replaceMisspelling(menuItem.label)
}
}))
}

View File

@ -143,9 +143,10 @@ export class WindowService {
}
private setupContextMenu(mainWindow: BrowserWindow) {
contextMenu.contextMenu(mainWindow)
app.on('browser-window-created', (_, win) => {
contextMenu.contextMenu(win)
contextMenu.contextMenu(mainWindow.webContents)
// setup context menu for all webviews like miniapp
app.on('web-contents-created', (_, webContents) => {
contextMenu.contextMenu(webContents)
})
// Dangerous API

View File

@ -229,7 +229,9 @@ const api = {
},
webview: {
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal)
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
setSpellCheckEnabled: (webviewId: number, isEnable: boolean) =>
ipcRenderer.invoke(IpcChannel.Webview_SetSpellCheckEnabled, webviewId, isEnable)
},
storeSync: {
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),

View File

@ -1,3 +1,4 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { WebviewTag } from 'electron'
import { memo, useEffect, useRef } from 'react'
@ -21,6 +22,7 @@ const WebviewContainer = memo(
onNavigateCallback: (appid: string, url: string) => void
}) => {
const webviewRef = useRef<WebviewTag | null>(null)
const { enableSpellCheck } = useSettings()
const setRef = (appid: string) => {
onSetRefCallback(appid, null)
@ -46,6 +48,14 @@ const WebviewContainer = memo(
onNavigateCallback(appid, event.url)
}
const handleDomReady = () => {
const webviewId = webviewRef.current?.getWebContentsId()
if (webviewId) {
window.api?.webview?.setSpellCheckEnabled?.(webviewId, enableSpellCheck)
}
}
webviewRef.current.addEventListener('dom-ready', handleDomReady)
webviewRef.current.addEventListener('did-finish-load', handleLoaded)
webviewRef.current.addEventListener('did-navigate-in-page', handleNavigate)
@ -55,6 +65,7 @@ const WebviewContainer = memo(
return () => {
webviewRef.current?.removeEventListener('did-finish-load', handleLoaded)
webviewRef.current?.removeEventListener('did-navigate-in-page', handleNavigate)
webviewRef.current?.removeEventListener('dom-ready', handleDomReady)
}
// because the appid and url are enough, no need to add onLoadedCallback
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -172,27 +172,6 @@ const GeneralSettings: FC = () => {
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.mode.title')}</SettingRowTitle>
<Selector value={storeProxyMode} onChange={onProxyModeChange} options={proxyModeOptions} />
</SettingRow>
{storeProxyMode === 'custom' && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input
placeholder="socks5://127.0.0.1:6153"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
style={{ width: 180 }}
onBlur={() => onSetProxyUrl()}
type="url"
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.spell_check')}</SettingRowTitle>
<Switch checked={enableSpellCheck} onChange={handleSpellCheckChange} />
@ -223,6 +202,27 @@ const GeneralSettings: FC = () => {
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.mode.title')}</SettingRowTitle>
<Selector value={storeProxyMode} onChange={onProxyModeChange} options={proxyModeOptions} />
</SettingRow>
{storeProxyMode === 'custom' && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input
placeholder="socks5://127.0.0.1:6153"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
style={{ width: 180 }}
onBlur={() => onSetProxyUrl()}
type="url"
/>
</SettingRow>
</>
)}
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.notification.title')}</SettingTitle>