mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-21 07:40:11 +08:00
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:
parent
94ef903b46
commit
a33a234f36
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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')]
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": "左",
|
||||||
|
|||||||
@ -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": "Слева",
|
||||||
|
|||||||
@ -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": "左侧",
|
||||||
|
|||||||
@ -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": "左側",
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user