Repository: CherryHQ/cherry-studio
Pull request #5868: refactor: update zoom handling in IPC and settings
Author: login: beyondkmp
Labels: name: UI/UX
Created: 2025-05-11T06:57:33Z | Merged: 2025-05-11T10:38:31Z

* renamed zoom-related IPC channels for clarity
* refactored zoom handling logic in ipc.ts and ShortcutService
* removed unused zoom factor state management from settings
* updated DisplaySettings component to manage zoom via buttons
* localized zoom settings in multiple languages

<!-- Template from https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?-->
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
-->

### What this PR does

Before this PR:
zoom factor数据双向同步问题，显示太大，没有办法恢复。在preload.js里面监听onZoomFactorUpdate不是非常好.

After this PR:

1. zoom factor都是直接从主进程的configManager里面获取，解决数据双向问题。
2. 只有zoom in 和zoom out按键和reset，不让用户手动选择，可以防止误选择
3. 设置最小值为0.5, 最大值为2.0，参考slack的设置，如果再大或者再小也没有多大意义。

![image](https://github.com/user-attachments/assets/3fa51868-ad4f-438b-99a0-f1ab1f05827e)

<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: -->

Fixes #

### Why we need it and why it was done in this way

The following tradeoffs were made:

The following alternatives were considered:

Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... -->

### Breaking changes

<!-- optional -->

If this PR introduces breaking changes, please describe the changes and the impact on users.

### Special notes for your reviewer

<!-- optional -->

### Checklist

This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.

- [x] PR: The PR description is expressive enough and will help future contributors
- [x] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required
- [x] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. You want a user-guide update if it's a user facing feature.

### Release note

<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->

```release-note
update zoom handling in IPC and settings
```

## Summary by Sourcery

Refactor zoom handling by centralizing logic, simplifying user controls, and updating IPC channels and settings management.

Enhancements:
- Renamed and consolidated zoom-related IPC channels for improved clarity.
- Centralized zoom logic in a new utility and removed redundant state management from settings.
- Simplified zoom controls in the UI to use increment, decrement, and reset buttons with defined limits.
- Localized zoom settings labels in multiple languages.

## Summary by Sourcery

Refactor zoom handling by centralizing zoom logic in the main process, updating IPC channels, and simplifying zoom controls in the settings UI.

Enhancements:
- Renamed and consolidated zoom-related IPC channels for improved clarity and maintainability.
- Refactored zoom logic to be managed centrally in the main process and removed redundant state management from settings.
- Simplified zoom controls in the DisplaySettings component to use increment, decrement, and reset buttons with defined limits.

Documentation:
- Localized zoom settings labels in multiple languages.

Chores:
- Removed unused zoom factor state and related code from settings and hooks.


Diff:
diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts
index 38aadf92332..1dd61c6364f 100644
--- a/packages/shared/IpcChannel.ts
+++ b/packages/shared/IpcChannel.ts
@@ -13,8 +13,7 @@ export enum IpcChannel {
   App_RestartTray = 'app:restart-tray',
   App_SetTheme = 'app:set-theme',
   App_SetAutoUpdate = 'app:set-auto-update',
-  App_SetZoomFactor = 'app:set-zoom-factor',
-  ZoomFactorUpdated = 'app:zoom-factor-updated',
+  App_HandleZoomFactor = 'app:handle-zoom-factor',
 
   App_IsBinaryExist = 'app:is-binary-exist',
   App_GetBinaryPath = 'app:get-binary-path',
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index e26b6231986..d628dd45a5f 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -3,6 +3,7 @@ import { arch } from 'node:os'
 
 import { isMac, isWin } from '@main/constant'
 import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
+import { handleZoomFactor } from '@main/utils/zoom'
 import { IpcChannel } from '@shared/IpcChannel'
 import { Shortcut, ThemeMode } from '@types'
 import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
@@ -141,15 +142,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
     notifyThemeChange()
   })
 
-  // zoom factor
-  ipcMain.handle(IpcChannel.App_SetZoomFactor, (_, factor: number) => {
-    configManager.setZoomFactor(factor)
+  ipcMain.handle(IpcChannel.App_HandleZoomFactor, (_, delta: number, reset: boolean = false) => {
     const windows = BrowserWindow.getAllWindows()
-    windows.forEach((win) => {
-      if (!win.isDestroyed()) {
-        win.webContents.setZoomFactor(factor)
-      }
-    })
+    handleZoomFactor(windows, delta, reset)
+    return configManager.getZoomFactor()
   })
 
   // clear cache
diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts
index c384f57fa97..d69c80b3251 100644
--- a/src/main/services/ShortcutService.ts
+++ b/src/main/services/ShortcutService.ts
@@ -1,5 +1,4 @@
-import { ZOOM_LEVELS } from '@shared/config/constant'
-import { IpcChannel } from '@shared/IpcChannel'
+import { handleZoomFactor } from '@main/utils/zoom'
 import { Shortcut } from '@types'
 import { BrowserWindow, globalShortcut } from 'electron'
 import Logger from 'electron-log'
@@ -16,14 +15,11 @@ const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; on
 function getShortcutHandler(shortcut: Shortcut) {
   switch (shortcut.key) {
     case 'zoom_in':
-      return (window: BrowserWindow) => handleZoom(1)(window)
+      return (window: BrowserWindow) => handleZoomFactor([window], 0.1)
     case 'zoom_out':
-      return (window: BrowserWindow) => handleZoom(-1)(window)
+      return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
     case 'zoom_reset':
-      return (window: BrowserWindow) => {
-        window.webContents.setZoomFactor(1)
-        configManager.setZoomFactor(1)
-      }
+      return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
     case 'show_app':
       return () => {
         windowService.toggleMainWindow()
@@ -41,46 +37,6 @@ function formatShortcutKey(shortcut: string[]): string {
   return shortcut.join('+')
 }
 
-function handleZoom(delta: number) {
-  return (window: BrowserWindow) => {
-    const currentZoom = configManager.getZoomFactor()
-    let currentIndex = ZOOM_LEVELS.indexOf(currentZoom)
-
-    // 如果当前缩放比例不在预设列表中，找到最接近的
-    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)
-      // 通知所有渲染进程更新 zoomFactor
-      window.webContents.setZoomFactor(newZoom)
-      window.webContents.send(IpcChannel.ZoomFactorUpdated, newZoom)
-    }
-  }
-}
-
 const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
   shortcut: string | string[]
 ): string => {
diff --git a/src/main/utils/zoom.ts b/src/main/utils/zoom.ts
new file mode 100644
index 00000000000..8e7145313cf
--- /dev/null
+++ b/src/main/utils/zoom.ts
@@ -0,0 +1,26 @@
+import { BrowserWindow } from 'electron'
+
+import { configManager } from '../services/ConfigManager'
+
+export function handleZoomFactor(wins: BrowserWindow[], delta: number, reset: boolean = false) {
+  if (reset) {
+    wins.forEach((win) => {
+      win.webContents.setZoomFactor(1)
+    })
+    configManager.setZoomFactor(1)
+    return
+  }
+
+  if (delta === 0) {
+    return
+  }
+
+  const currentZoom = configManager.getZoomFactor()
+  const newZoom = Number((currentZoom + delta).toFixed(1))
+  if (newZoom >= 0.5 && newZoom <= 2.0) {
+    wins.forEach((win) => {
+      win.webContents.setZoomFactor(newZoom)
+    })
+    configManager.setZoomFactor(newZoom)
+  }
+}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 425a052c426..eeea6ec3de4 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -19,7 +19,8 @@ const api = {
   setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
   restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
   setTheme: (theme: 'light' | 'dark' | 'auto') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
-  setZoomFactor: (factor: number) => ipcRenderer.invoke(IpcChannel.App_SetZoomFactor, factor),
+  handleZoomFactor: (delta: number, reset: boolean = false) =>
+    ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset),
   setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
   openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
   clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
@@ -192,17 +193,6 @@ const api = {
     subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
     unsubscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Unsubscribe),
     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)
-    }
   }
 }
 
diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts
index 29108a42c29..b31c325400c 100644
--- a/src/renderer/src/hooks/useAppInit.ts
+++ b/src/renderer/src/hooks/useAppInit.ts
@@ -18,17 +18,7 @@ import useUpdateHandler from './useUpdateHandler'
 
 export function useAppInit() {
   const dispatch = useAppDispatch()
-  const {
-    proxyUrl,
-    language,
-    windowStyle,
-    autoCheckUpdate,
-    proxyMode,
-    customCss,
-    enableDataCollection,
-    setZoomFactor,
-    zoomFactor
-  } = useSettings()
+  const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings()
   const { minappShow } = useRuntime()
   const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
   const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
@@ -41,19 +31,6 @@ export function useAppInit() {
     avatar?.value && dispatch(setAvatar(avatar.value))
   }, [avatar, dispatch])
 
-  useEffect(() => {
-    const removeZoomListener = window.api.onZoomFactorUpdate((factor) => {
-      setZoomFactor(factor)
-    })
-    return () => {
-      removeZoomListener()
-    }
-  }, [setZoomFactor])
-
-  useEffect(() => {
-    setZoomFactor(zoomFactor)
-  }, [setZoomFactor, zoomFactor])
-
   useEffect(() => {
     document.getElementById('spinner')?.remove()
     runAsyncFunction(async () => {
diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts
index 4e1e9e9599f..ba6185aba90 100644
--- a/src/renderer/src/hooks/useSettings.ts
+++ b/src/renderer/src/hooks/useSettings.ts
@@ -14,8 +14,7 @@ import {
   setTopicPosition,
   setTray as _setTray,
   setTrayOnClose,
-  setWindowStyle,
-  setZoomFactor
+  setWindowStyle
 } from '@renderer/store/settings'
 import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
 
@@ -80,10 +79,6 @@ export function useSettings() {
     },
     setAssistantIconType(assistantIconType: AssistantIconType) {
       dispatch(setAssistantIconType(assistantIconType))
-    },
-    setZoomFactor(factor: number) {
-      dispatch(setZoomFactor(factor))
-      window.api.setZoomFactor(factor)
     }
   }
 }
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json
index 3d8ab74b966..d15f42d825b 100644
--- a/src/renderer/src/i18n/locales/en-us.json
+++ b/src/renderer/src/i18n/locales/en-us.json
@@ -1527,7 +1527,6 @@
       "theme.window.style.opaque": "Opaque Window",
       "theme.window.style.title": "Window Style",
       "theme.window.style.transparent": "Transparent Window",
-      "zoom.title": "Page Zoom",
       "title": "Settings",
       "topic.position": "Topic position",
       "topic.position.left": "Left",
diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json
index f1b12358d8f..437a72568da 100644
--- a/src/renderer/src/i18n/locales/ja-jp.json
+++ b/src/renderer/src/i18n/locales/ja-jp.json
@@ -1525,7 +1525,6 @@
       "theme.window.style.opaque": "不透明ウィンドウ",
       "theme.window.style.title": "ウィンドウスタイル",
       "theme.window.style.transparent": "透明ウィンドウ",
-      "zoom.title": "ページズーム",
       "title": "設定",
       "topic.position": "トピックの位置",
       "topic.position.left": "左",
@@ -1636,6 +1635,10 @@
       "quit": "終了",
       "show_window": "ウィンドウを表示",
       "visualization": "可視化"
+    },
+    "zoom": {
+      "title": "ズーム",
+      "reset": "リセット"
     }
   }
 }
diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json
index 2692e1c2709..fac4bf50559 100644
--- a/src/renderer/src/i18n/locales/ru-ru.json
+++ b/src/renderer/src/i18n/locales/ru-ru.json
@@ -1525,7 +1525,6 @@
       "theme.window.style.opaque": "Непрозрачное окно",
       "theme.window.style.title": "Стиль окна",
       "theme.window.style.transparent": "Прозрачное окно",
-      "zoom.title": "Масштаб страницы",
       "title": "Настройки",
       "topic.position": "Позиция топиков",
       "topic.position.left": "Слева",
@@ -1636,6 +1635,10 @@
       "quit": "Выйти",
       "show_window": "Показать окно",
       "visualization": "Визуализация"
+    },
+    "zoom": {
+      "title": "Масштаб",
+      "reset": "Сбросить"
     }
   }
 }
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index e68409a6428..2ca3a3333d2 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -1527,7 +1527,6 @@
       "theme.window.style.opaque": "不透明窗口",
       "theme.window.style.title": "窗口样式",
       "theme.window.style.transparent": "透明窗口",
-      "zoom.title": "页面缩放",
       "title": "设置",
       "topic.position": "话题位置",
       "topic.position.left": "左侧",
@@ -1596,6 +1595,10 @@
       "privacy": {
         "title": "隐私设置",
         "enable_privacy_mode": "匿名发送错误报告和数据统计"
+      },
+      "zoom": {
+        "title": "缩放",
+        "reset": "重置"
       }
     },
     "translate": {
diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json
index 893a7b75a1b..f3ffe8444cd 100644
--- a/src/renderer/src/i18n/locales/zh-tw.json
+++ b/src/renderer/src/i18n/locales/zh-tw.json
@@ -1526,7 +1526,6 @@
       "theme.window.style.opaque": "不透明視窗",
       "theme.window.style.title": "視窗樣式",
       "theme.window.style.transparent": "透明視窗",
-      "zoom.title": "頁面縮放",
       "title": "設定",
       "topic.position": "話題位置",
       "topic.position.left": "左側",
@@ -1596,6 +1595,10 @@
       "privacy": {
         "title": "隱私設定",
         "enable_privacy_mode": "匿名發送錯誤報告和資料統計"
+      },
+      "zoom": {
+        "title": "縮放",
+        "reset": "重置"
       }
     },
     "translate": {
diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
index 7db2ec0889f..48f6799ce6e 100644
--- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
+++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
@@ -13,9 +13,8 @@ import {
   setSidebarIcons
 } from '@renderer/store/settings'
 import { ThemeMode } from '@renderer/types'
-import { ZOOM_OPTIONS } from '@shared/config/constant'
-import { Button, Input, Segmented, Select, Switch } from 'antd'
-import { FC, useCallback, useMemo, useState } from 'react'
+import { Button, Input, Segmented, Switch } from 'antd'
+import { FC, useCallback, useEffect, useMemo, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import styled from 'styled-components'
 
@@ -34,13 +33,12 @@ const DisplaySettings: FC = () => {
     showTopicTime,
     customCss,
     sidebarIcons,
-    assistantIconType,
-    zoomFactor,
-    setZoomFactor
+    assistantIconType
   } = useSettings()
   const { theme: themeMode } = useTheme()
   const { t } = useTranslation()
   const dispatch = useAppDispatch()
+  const [currentZoom, setCurrentZoom] = useState(1.0)
 
   const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
   const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
@@ -91,6 +89,31 @@ const DisplaySettings: FC = () => {
     [t]
   )
 
+  useEffect(() => {
+    // 初始化获取当前缩放值
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+
+    const handleResize = () => {
+      window.api.handleZoomFactor(0).then((factor) => {
+        setCurrentZoom(factor)
+      })
+    }
+    // 添加resize事件监听
+    window.addEventListener('resize', handleResize)
+
+    // 清理事件监听，防止内存泄漏
+    return () => {
+      window.removeEventListener('resize', handleResize)
+    }
+  }, [])
+
+  const handleZoomFactor = async (delta: number, reset: boolean = false) => {
+    const zoomFactor = await window.api.handleZoomFactor(delta, reset)
+    setCurrentZoom(zoomFactor)
+  }
+
   const assistantIconTypeOptions = useMemo(
     () => [
       { value: 'model', label: t('settings.assistant.icon.type.model') },
@@ -112,7 +135,18 @@ const DisplaySettings: FC = () => {
         <SettingDivider />
         <SettingRow>
           <SettingRowTitle>{t('settings.zoom.title')}</SettingRowTitle>
-          <Select style={{ width: 120 }} value={zoomFactor} onChange={setZoomFactor} options={ZOOM_OPTIONS} />
+          <ZoomButtonGroup>
+            <Button onClick={() => handleZoomFactor(-0.1)}>-</Button>
+            <ZoomValue>{Math.round(currentZoom * 100)}%</ZoomValue>
+            <Button onClick={() => handleZoomFactor(0.1)}>+</Button>
+            <Button
+              onClick={() => {
+                handleZoomFactor(0, true)
+              }}
+              style={{ marginLeft: 8 }}>
+              {t('settings.zoom.reset')}
+            </Button>
+          </ZoomButtonGroup>
         </SettingRow>
         {isMac && (
           <>
@@ -221,5 +255,14 @@ const ResetButtonWrapper = styled.div`
   align-items: center;
   justify-content: center;
 `
+const ZoomButtonGroup = styled.div`
+  display: flex;
+  align-items: center;
+  width: 210px;
+`
+const ZoomValue = styled.span`
+  width: 40px;
+  text-align: center;
+`
 
 export default DisplaySettings
diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts
index 166ed81d6eb..81db1454a29 100644
--- a/src/renderer/src/store/settings.ts
+++ b/src/renderer/src/store/settings.ts
@@ -129,7 +129,6 @@ export interface SettingsState {
     siyuan: boolean
     docx: boolean
   }
-  zoomFactor: number
 }
 
 export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
@@ -234,8 +233,7 @@ export const initialState: SettingsState = {
     obsidian: true,
     siyuan: true,
     docx: true
-  },
-  zoomFactor: 1
+  }
 }
 
 const settingsSlice = createSlice({
@@ -510,9 +508,6 @@ const settingsSlice = createSlice({
     },
     setEnableBackspaceDeleteModel: (state, action: PayloadAction<boolean>) => {
       state.enableBackspaceDeleteModel = action.payload
-    },
-    setZoomFactor: (state, action: PayloadAction<number>) => {
-      state.zoomFactor = action.payload
     }
   }
 })
@@ -605,8 +600,7 @@ export const {
   setEnableDataCollection,
   setEnableQuickPanelTriggers,
   setExportMenuOptions,
-  setEnableBackspaceDeleteModel,
-  setZoomFactor
+  setEnableBackspaceDeleteModel
 } = settingsSlice.actions
 
 export default settingsSlice.reducer


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the zoom handling by removing the unused zoom state from the settings store, centralizing zoom updates via the main process, and updating the DisplaySettings UI to use dedicated zoom buttons. Key changes include:
- Removing the zoomFactor state and associated actions from the settings store.
- Introducing a new handleZoomFactor API and utility to centrally manage zoom changes.
- Updating the DisplaySettings component to use buttons for zoom in, zoom out, and reset, along with localized text.

### Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/renderer/src/store/settings.ts | Removed unused zoomFactor state and corresponding actions. |
| src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx | Replaced the zoom Select input with button controls and added logic to update the current zoom value. |
| src/renderer/src/i18n/locales/* | Removed old zoom keys and added new localized zoom strings. |
| src/renderer/src/hooks/useSettings.ts & useAppInit.ts | Removed obsolete zoom state handling and actions. |
| src/preload/index.ts, src/main/ipc.ts, and related files | Replaced old setZoomFactor API with the new handleZoomFactor implementation. |
| src/main/utils/zoom.ts | Introduced a new helper to update zoom factors based on delta and reset flag. |
| src/main/services/ShortcutService.ts | Updated zoom shortcut handlers to use the new zoom API. |
| packages/shared/IpcChannel.ts | Updated IPC channels for zoom handling. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx:97**
* The resize event listener is added without a corresponding cleanup; consider returning a cleanup function in the useEffect hook to remove the listener on component unmount.
```
window.addEventListener('resize', () => { window.api.handleZoomFactor(0).then((factor) => { setCurrentZoom(factor) }) })
```
**src/main/utils/zoom.ts:13**
* [nitpick] Consider returning the current zoom factor consistently from handleZoomFactor, even when delta is 0, to ensure a clear and predictable API behavior.
```
if (delta === 0) { return }
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T06:57:57Z
user: login: sourcery-ai[bot]
body: Hey @beyondkmp - I've reviewed your changes - here's some feedback:

- Consider debouncing the resize event listener in DisplaySettings to avoid excessive API calls when the window is resized rapidly.
- The handleZoomFactor utility in main/utils/zoom.ts does not return the updated zoom factor when called with delta or reset; consider returning the new value for consistency with the IPC handler's return value.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T07:02:25Z
user: login: beyondkmp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-11T07:04:30Z
user: login: sourcery-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T07:04:39Z
user: login: beyondkmp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-11T07:04:39Z
user: login: beyondkmp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-11T07:08:02Z
user: login: beyondkmp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-11T07:18:54Z
user: login: beyondkmp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-11T07:22:24Z
user: login: sourcery-ai[bot]
body: Hey @beyondkmp - I've reviewed your changes - here's some feedback:

- Consider debouncing the zoom factor update in DisplaySettings to avoid excessive IPC calls when users click rapidly.
- The new handleZoomFactor utility in main/utils/zoom.ts could benefit from unit tests to ensure correct boundary handling and rounding.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T09:03:50Z
user: login: beyondkmp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-11T09:08:17Z
user: login: kangfenmao
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-11T10:38:16Z

Review comments:
diff_hunk: @@ -91,6 +89,23 @@ const DisplaySettings: FC = () => {
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** Missing cleanup for resize event listener

Wrap the resize handler in a named function and return a cleanup from useEffect to call removeEventListener and prevent memory leaks.
created_at: 2025-05-11T07:02:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +89,23 @@
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** Using window resize to detect zoom changes is unreliable

Resize only tracks viewport size, not zoom. Use a dedicated IPC event or another reliable mechanism for zoom changes.
created_at: 2025-05-11T07:02:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +89,23 @@
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: beyondkmp
body: 不太对，zoom in/zoom out也是会触发resize事件的。所以不需要再专门加一个ipc了。
created_at: 2025-05-11T07:04:30Z
author_association: COLLABORATOR
diff_hunk: @@ -91,6 +89,23 @@
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: sourcery-ai[bot]
body: 你说得没错，zoom in/zoom out 的确会触发 resize 事件，所以用 resize 事件可以同步大部分缩放相关的 UI 更新。不过需要注意的是：

- resize 事件也会因为窗口大小变化等其他操作触发，可能会导致不必要的 setState 或副作用，建议在事件处理函数里加一些判断，避免重复或无效的更新。
- 如果后续有需求，比如主进程直接修改 zoom（不是通过快捷键或 UI），那还是建议用专门的 IPC 事件来同步，这样更健壮、可维护。

目前这种实现方式在现有场景下是可行的，只是建议后续如果 zoom 逻辑变复杂，可以考虑更专门的同步机制。你可以在代码注释里简单说明下用 resize 的原因，方便后续维护者理解。
created_at: 2025-05-11T07:04:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +89,23 @@ const DisplaySettings: FC = () => {
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: beyondkmp
body: 不错的建议。
created_at: 2025-05-11T07:04:39Z
author_association: COLLABORATOR
diff_hunk: @@ -91,6 +89,23 @@ const DisplaySettings: FC = () => {
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: beyondkmp
body: Fixed
created_at: 2025-05-11T07:08:01Z
author_association: COLLABORATOR
diff_hunk: @@ -91,6 +89,23 @@
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: beyondkmp
body: 是的，目前zoom的逻辑非常简单，只是zoom in/zoom out，快捷键和界面的设置都是通过handleZoomFactor函数。所以只需要再resize加一些判断就行了。
created_at: 2025-05-11T07:18:54Z
author_association: COLLABORATOR
diff_hunk: @@ -91,6 +89,23 @@
     [t]
   )
 
+  useEffect(() => {
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+    // 添加一个监听resize事件, 同步快捷键导致的缩放
+    window.addEventListener('resize', () => {
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: beyondkmp
body: Fixed
created_at: 2025-05-11T07:22:23Z
author_association: COLLABORATOR
diff_hunk: @@ -91,6 +89,31 @@ const DisplaySettings: FC = () => {
     [t]
   )
 
+  useEffect(() => {
+    // 初始化获取当前缩放值
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+
+    const handleResize = () => {
+      window.api.handleZoomFactor(0).then((factor) => {
+        setCurrentZoom(factor)
+      })
+    }
+    // 添加resize事件监听
+    window.addEventListener('resize', handleResize)
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: sourcery-ai[bot]
body: **suggestion (performance):** Debounce the resize listener

Debounce handleResize to avoid flooding window.api.handleZoomFactor with calls during rapid resizes.
created_at: 2025-05-11T09:03:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,6 +89,31 @@ const DisplaySettings: FC = () => {
     [t]
   )
 
+  useEffect(() => {
+    // 初始化获取当前缩放值
+    window.api.handleZoomFactor(0).then((factor) => {
+      setCurrentZoom(factor)
+    })
+
+    const handleResize = () => {
+      window.api.handleZoomFactor(0).then((factor) => {
+        setCurrentZoom(factor)
+      })
+    }
+    // 添加resize事件监听
+    window.addEventListener('resize', handleResize)
path: src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
user: login: beyondkmp
body: 测试了下，应该不需要。没有rapid resizes的情况。
created_at: 2025-05-11T09:08:17Z
author_association: COLLABORATOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-11T06:57:39Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This PR refactors zoom handling by centralizing zoom logic in a new utility, simplifying UI controls, renaming IPC channels for clarity, removing redundant zoom state from settings, and localizing zoom settings. Zoom factor is now managed exclusively via the main process, with UI controls limited to increment, decrement, and reset within defined bounds (0.5–2.0).

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Centralized and refactored zoom handling logic across main and renderer processes. | <ul><li>Introduced a new zoom utility (main/utils/zoom.ts) to handle zoom factor changes centrally.</li><li>Replaced previous zoom logic in ShortcutService and ipc.ts to use the new utility.</li><li>Zoom factor is now always retrieved and set via the main process configManager.</li></ul> | `src/main/utils/zoom.ts`<br/>`src/main/services/ShortcutService.ts`<br/>`src/main/ipc.ts` |
| Renamed and consolidated zoom-related IPC channels for clarity. | <ul><li>Replaced App_SetZoomFactor and ZoomFactorUpdated with App_HandleZoomFactor channel.</li><li>Updated preload and renderer to use the new IPC channel.</li></ul> | `packages/shared/IpcChannel.ts`<br/>`src/preload/index.ts` |
| Simplified and updated UI zoom controls in DisplaySettings component. | <ul><li>Replaced zoom dropdown with increment, decrement, and reset buttons.</li><li>Displayed current zoom as a percentage and enforced min/max limits (0.5–2.0).</li><li>Removed direct zoom factor state management from settings.</li></ul> | `src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx` |
| Removed redundant zoom factor state and actions from settings store and hooks. | <ul><li>Eliminated zoomFactor from settings state and related actions.</li><li>Removed zoom factor synchronization logic from useAppInit and useSettings hooks.</li></ul> | `src/renderer/src/store/settings.ts`<br/>`src/renderer/src/hooks/useAppInit.ts`<br/>`src/renderer/src/hooks/useSettings.ts` |
| Localized zoom settings labels in multiple languages. | <ul><li>Added/updated zoom-related translations in all supported locale files.</li></ul> | `src/renderer/src/i18n/locales/en-us.json`<br/>`src/renderer/src/i18n/locales/ja-jp.json`<br/>`src/renderer/src/i18n/locales/ru-ru.json`<br/>`src/renderer/src/i18n/locales/zh-cn.json`<br/>`src/renderer/src/i18n/locales/zh-tw.json` |

### Possibly linked issues

- **#0**: The PR refactors/adds UI zoom controls and logic, addressing the issue's scaling request.

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: 0xfullex
created_at: 2025-05-11T09:01:27Z
body: 以main为主挺好，不需要store了
author_association: MEMBER
user: login: beyondkmp
created_at: 2025-05-11T09:03:57Z
body: 这个确实要以main的为主。这样单向同步比较简单。其它窗口的话也可以直接这样单向获取。
author_association: COLLABORATOR