feat: add zoom factor setting and localization support (#5665)

* feat: add zoom factor setting and localization support

- Introduced App_SetZoomFactor IPC channel for managing zoom levels.
- Implemented zoom factor functionality in the main IPC handler.
- Added setZoomFactor method in the settings store and corresponding UI in DisplaySettings.
- Included localization for zoom settings in English and Chinese.

* add i18n

* recover file

* delete code

* fix: update zoom factor handling to apply to all windows

- Modified the IPC handler for App_SetZoomFactor to set the zoom factor for all non-destroyed windows instead of just the main window.

* add getzoomfactor api

* feat: synchronize zoom factor with Redux state on app initialization

- Added functionality to fetch the zoom factor from the main process and dispatch it to the Redux store during app initialization.
- Removed redundant zoom factor fetching logic from DisplaySettings component.

* feat: enhance zoom factor management with IPC updates

- Added a new IPC channel for zoom factor updates to notify all renderer processes.
- Introduced a constant for predefined zoom levels to streamline zoom adjustments.
- Updated the zoom handling logic to utilize the new zoom levels and ensure smooth transitions.
- Implemented a listener in the preload script to handle zoom factor updates from the main process.
- Refactored the app initialization to include real-time updates for the zoom factor in the Redux state.

* feat: integrate zoom options into DisplaySettings component

- Added ZOOM_OPTIONS constant to generate structured options for Ant Design Select from predefined zoom levels.
- Refactored DisplaySettings to utilize ZOOM_OPTIONS, removing redundant zoom option definitions.
- Simplified the zoom factor fetching logic in useAppInit for better readability and efficiency.

* refactor: streamline zoom factor handling and remove unused IPC channel

- Removed the App_GetZoomFactor IPC channel as it was no longer needed.
- Updated zoom factor handling to directly set and notify the main window of changes.
- Simplified the logic for setting the zoom factor in the WindowService and ShortcutService.
- Adjusted the useAppInit hook to utilize the new zoom factor management approach.

* refactor: improve zoom factor handling in WindowService and useAppInit hook

- Simplified the zoom factor setting in WindowService by directly using the config manager.
- Updated useAppInit to ensure the zoom factor is set correctly on initialization, enhancing responsiveness to changes.
This commit is contained in:
beyondkmp 2025-05-08 07:18:43 +08:00 committed by GitHub
parent 94ef903b46
commit a33a234f36
14 changed files with 122 additions and 11 deletions

View File

@ -13,6 +13,8 @@ export enum IpcChannel {
App_RestartTray = 'app:restart-tray', App_RestartTray = 'app:restart-tray',
App_SetTheme = 'app:set-theme', App_SetTheme = 'app:set-theme',
App_SetAutoUpdate = 'app:set-auto-update', App_SetAutoUpdate = 'app:set-auto-update',
App_SetZoomFactor = 'app:set-zoom-factor',
ZoomFactorUpdated = 'app:zoom-factor-updated',
App_IsBinaryExist = 'app:is-binary-exist', App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path', App_GetBinaryPath = 'app:get-binary-path',

View File

@ -134,6 +134,14 @@ export const textExts = [
'.tf' // Technology File '.tf' // Technology File
] ]
export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]
// 从 ZOOM_LEVELS 生成 Ant Design Select 所需的 options 结构
export const ZOOM_OPTIONS = ZOOM_LEVELS.map((level) => ({
value: level,
label: `${Math.round(level * 100)}%`
}))
export const ZOOM_SHORTCUTS = [ export const ZOOM_SHORTCUTS = [
{ {
key: 'zoom_in', key: 'zoom_in',

View File

@ -141,6 +141,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
notifyThemeChange() notifyThemeChange()
}) })
// zoom factor
ipcMain.handle(IpcChannel.App_SetZoomFactor, (_, factor: number) => {
configManager.setZoomFactor(factor)
const windows = BrowserWindow.getAllWindows()
windows.forEach((win) => {
if (!win.isDestroyed()) {
win.webContents.setZoomFactor(factor)
}
})
})
// clear cache // clear cache
ipcMain.handle(IpcChannel.App_ClearCache, async () => { ipcMain.handle(IpcChannel.App_ClearCache, async () => {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')] const sessions = [session.defaultSession, session.fromPartition('persist:webview')]

View File

@ -1,3 +1,5 @@
import { ZOOM_LEVELS } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut } from '@types' import { Shortcut } from '@types'
import { BrowserWindow, globalShortcut } from 'electron' import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
@ -14,9 +16,9 @@ const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; on
function getShortcutHandler(shortcut: Shortcut) { function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) { switch (shortcut.key) {
case 'zoom_in': case 'zoom_in':
return (window: BrowserWindow) => handleZoom(0.1)(window) return (window: BrowserWindow) => handleZoom(1)(window)
case 'zoom_out': case 'zoom_out':
return (window: BrowserWindow) => handleZoom(-0.1)(window) return (window: BrowserWindow) => handleZoom(-1)(window)
case 'zoom_reset': case 'zoom_reset':
return (window: BrowserWindow) => { return (window: BrowserWindow) => {
window.webContents.setZoomFactor(1) window.webContents.setZoomFactor(1)
@ -42,10 +44,39 @@ function formatShortcutKey(shortcut: string[]): string {
function handleZoom(delta: number) { function handleZoom(delta: number) {
return (window: BrowserWindow) => { return (window: BrowserWindow) => {
const currentZoom = configManager.getZoomFactor() const currentZoom = configManager.getZoomFactor()
const newZoom = Number((currentZoom + delta).toFixed(1)) let currentIndex = ZOOM_LEVELS.indexOf(currentZoom)
if (newZoom >= 0.1 && newZoom <= 5.0) {
window.webContents.setZoomFactor(newZoom) // 如果当前缩放比例不在预设列表中,找到最接近的
if (currentIndex === -1) {
let closestIndex = 0
let minDiff = Math.abs(ZOOM_LEVELS[0] - currentZoom)
for (let i = 1; i < ZOOM_LEVELS.length; i++) {
const diff = Math.abs(ZOOM_LEVELS[i] - currentZoom)
if (diff < minDiff) {
minDiff = diff
closestIndex = i
}
}
currentIndex = closestIndex
}
let nextIndex = currentIndex + delta
// 边界检查
if (nextIndex < 0) {
nextIndex = 0 // 已经是最小值
} else if (nextIndex >= ZOOM_LEVELS.length) {
nextIndex = ZOOM_LEVELS.length - 1 // 已经是最大值
}
const newZoom = ZOOM_LEVELS[nextIndex]
if (newZoom !== currentZoom) {
// 只有在实际改变时才更新
configManager.setZoomFactor(newZoom) configManager.setZoomFactor(newZoom)
// 通知所有渲染进程更新 zoomFactor
window.webContents.setZoomFactor(newZoom)
window.webContents.send(IpcChannel.ZoomFactorUpdated, newZoom)
} }
} }
} }

View File

@ -19,6 +19,7 @@ const api = {
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive), setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray), restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
setTheme: (theme: 'light' | 'dark' | 'auto') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme), setTheme: (theme: 'light' | 'dark' | 'auto') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
setZoomFactor: (factor: number) => ipcRenderer.invoke(IpcChannel.App_SetZoomFactor, factor),
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive), setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url), openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
@ -190,6 +191,17 @@ const api = {
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe), subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
unsubscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Unsubscribe), unsubscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Unsubscribe),
onUpdate: (action: any) => ipcRenderer.invoke(IpcChannel.StoreSync_OnUpdate, action) onUpdate: (action: any) => ipcRenderer.invoke(IpcChannel.StoreSync_OnUpdate, action)
},
// 新增:监听主进程的 zoom factor 更新
onZoomFactorUpdate: (callback: (factor: number) => void) => {
const listener = (_event: Electron.IpcRendererEvent, factor: number) => {
callback(factor)
}
ipcRenderer.on(IpcChannel.ZoomFactorUpdated, listener)
// 返回一个移除监听器的函数
return () => {
ipcRenderer.removeListener(IpcChannel.ZoomFactorUpdated, listener)
}
} }
} }

View File

@ -18,7 +18,17 @@ import useUpdateHandler from './useUpdateHandler'
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings() const {
proxyUrl,
language,
windowStyle,
autoCheckUpdate,
proxyMode,
customCss,
enableDataCollection,
setZoomFactor,
zoomFactor
} = useSettings()
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel() const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar')) const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
@ -31,6 +41,19 @@ export function useAppInit() {
avatar?.value && dispatch(setAvatar(avatar.value)) avatar?.value && dispatch(setAvatar(avatar.value))
}, [avatar, dispatch]) }, [avatar, dispatch])
useEffect(() => {
const removeZoomListener = window.api.onZoomFactorUpdate((factor) => {
setZoomFactor(factor)
})
return () => {
removeZoomListener()
}
}, [setZoomFactor])
useEffect(() => {
setZoomFactor(zoomFactor)
}, [setZoomFactor, zoomFactor])
useEffect(() => { useEffect(() => {
document.getElementById('spinner')?.remove() document.getElementById('spinner')?.remove()
runAsyncFunction(async () => { runAsyncFunction(async () => {

View File

@ -14,7 +14,8 @@ import {
setTopicPosition, setTopicPosition,
setTray as _setTray, setTray as _setTray,
setTrayOnClose, setTrayOnClose,
setWindowStyle setWindowStyle,
setZoomFactor
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types' import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
@ -79,6 +80,10 @@ export function useSettings() {
}, },
setAssistantIconType(assistantIconType: AssistantIconType) { setAssistantIconType(assistantIconType: AssistantIconType) {
dispatch(setAssistantIconType(assistantIconType)) dispatch(setAssistantIconType(assistantIconType))
},
setZoomFactor(factor: number) {
dispatch(setZoomFactor(factor))
window.api.setZoomFactor(factor)
} }
} }
} }

View File

@ -1475,6 +1475,7 @@
"theme.window.style.opaque": "Opaque Window", "theme.window.style.opaque": "Opaque Window",
"theme.window.style.title": "Window Style", "theme.window.style.title": "Window Style",
"theme.window.style.transparent": "Transparent Window", "theme.window.style.transparent": "Transparent Window",
"zoom.title": "Page Zoom",
"title": "Settings", "title": "Settings",
"topic.position": "Topic position", "topic.position": "Topic position",
"topic.position.left": "Left", "topic.position.left": "Left",

View File

@ -1473,6 +1473,7 @@
"theme.window.style.opaque": "不透明ウィンドウ", "theme.window.style.opaque": "不透明ウィンドウ",
"theme.window.style.title": "ウィンドウスタイル", "theme.window.style.title": "ウィンドウスタイル",
"theme.window.style.transparent": "透明ウィンドウ", "theme.window.style.transparent": "透明ウィンドウ",
"zoom.title": "ページズーム",
"title": "設定", "title": "設定",
"topic.position": "トピックの位置", "topic.position": "トピックの位置",
"topic.position.left": "左", "topic.position.left": "左",

View File

@ -1473,6 +1473,7 @@
"theme.window.style.opaque": "Непрозрачное окно", "theme.window.style.opaque": "Непрозрачное окно",
"theme.window.style.title": "Стиль окна", "theme.window.style.title": "Стиль окна",
"theme.window.style.transparent": "Прозрачное окно", "theme.window.style.transparent": "Прозрачное окно",
"zoom.title": "Масштаб страницы",
"title": "Настройки", "title": "Настройки",
"topic.position": "Позиция топиков", "topic.position": "Позиция топиков",
"topic.position.left": "Слева", "topic.position.left": "Слева",

View File

@ -1475,6 +1475,7 @@
"theme.window.style.opaque": "不透明窗口", "theme.window.style.opaque": "不透明窗口",
"theme.window.style.title": "窗口样式", "theme.window.style.title": "窗口样式",
"theme.window.style.transparent": "透明窗口", "theme.window.style.transparent": "透明窗口",
"zoom.title": "页面缩放",
"title": "设置", "title": "设置",
"topic.position": "话题位置", "topic.position": "话题位置",
"topic.position.left": "左侧", "topic.position.left": "左侧",

View File

@ -1474,6 +1474,7 @@
"theme.window.style.opaque": "不透明視窗", "theme.window.style.opaque": "不透明視窗",
"theme.window.style.title": "視窗樣式", "theme.window.style.title": "視窗樣式",
"theme.window.style.transparent": "透明視窗", "theme.window.style.transparent": "透明視窗",
"zoom.title": "頁面縮放",
"title": "設定", "title": "設定",
"topic.position": "話題位置", "topic.position": "話題位置",
"topic.position.left": "左側", "topic.position.left": "左側",

View File

@ -13,7 +13,8 @@ import {
setSidebarIcons setSidebarIcons
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import { Button, Input, Segmented, Switch } from 'antd' import { ZOOM_OPTIONS } from '@shared/config/constant'
import { Button, Input, Segmented, Select, Switch } from 'antd'
import { FC, useCallback, useMemo, useState } from 'react' import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -33,7 +34,9 @@ const DisplaySettings: FC = () => {
showTopicTime, showTopicTime,
customCss, customCss,
sidebarIcons, sidebarIcons,
assistantIconType assistantIconType,
zoomFactor,
setZoomFactor
} = useSettings() } = useSettings()
const { theme: themeMode } = useTheme() const { theme: themeMode } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
@ -106,6 +109,11 @@ const DisplaySettings: FC = () => {
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
<Segmented value={theme} shape="round" onChange={setTheme} options={themeOptions} /> <Segmented value={theme} shape="round" onChange={setTheme} options={themeOptions} />
</SettingRow> </SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.zoom.title')}</SettingRowTitle>
<Select style={{ width: 120 }} value={zoomFactor} onChange={setZoomFactor} options={ZOOM_OPTIONS} />
</SettingRow>
{isMac && ( {isMac && (
<> <>
<SettingDivider /> <SettingDivider />

View File

@ -129,6 +129,7 @@ export interface SettingsState {
siyuan: boolean siyuan: boolean
docx: boolean docx: boolean
} }
zoomFactor: number
} }
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid' export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
@ -233,7 +234,8 @@ export const initialState: SettingsState = {
obsidian: true, obsidian: true,
siyuan: true, siyuan: true,
docx: true docx: true
} },
zoomFactor: 1
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -508,6 +510,9 @@ const settingsSlice = createSlice({
}, },
setEnableBackspaceDeleteModel: (state, action: PayloadAction<boolean>) => { setEnableBackspaceDeleteModel: (state, action: PayloadAction<boolean>) => {
state.enableBackspaceDeleteModel = action.payload state.enableBackspaceDeleteModel = action.payload
},
setZoomFactor: (state, action: PayloadAction<number>) => {
state.zoomFactor = action.payload
} }
} }
}) })
@ -600,7 +605,8 @@ export const {
setEnableDataCollection, setEnableDataCollection,
setEnableQuickPanelTriggers, setEnableQuickPanelTriggers,
setExportMenuOptions, setExportMenuOptions,
setEnableBackspaceDeleteModel setEnableBackspaceDeleteModel,
setZoomFactor
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer