From 61700473b99ebfcfe72c9685733651dae9390796 Mon Sep 17 00:00:00 2001 From: alger Date: Wed, 28 May 2025 22:08:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=BB=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E8=87=AA=E9=80=82=E5=BA=94=E5=A4=A7=E5=B0=8F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E9=A1=B5=E9=9D=A2=E7=BC=A9=E6=94=BE=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E7=BC=A9=E6=94=BE=E5=9B=A0?= =?UTF-8?q?=E5=AD=90=E7=9A=84=E8=B0=83=E6=95=B4=E5=92=8C=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=9C=A8=E6=90=9C=E7=B4=A2=E6=A0=8F=E4=B8=AD?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E7=BC=A9=E6=94=BE=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/i18n/lang/en-US/comp.ts | 6 +- src/i18n/lang/zh-CN/comp.ts | 6 +- src/main/modules/window.ts | 259 ++++++++++++++++--- src/main/set.json | 3 +- src/renderer/hooks/useZoom.ts | 95 +++++++ src/renderer/layout/components/SearchBar.vue | 49 +++- 7 files changed, 379 insertions(+), 40 deletions(-) create mode 100644 src/renderer/hooks/useZoom.ts diff --git a/package.json b/package.json index c481722..7799e11 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "cors": "^2.8.5", "electron-store": "^8.1.0", "electron-updater": "^6.6.2", + "electron-window-state": "^5.0.3", "express": "^4.18.2", "font-list": "^1.5.1", "husky": "^9.1.7", diff --git a/src/i18n/lang/en-US/comp.ts b/src/i18n/lang/en-US/comp.ts index 1d5981e..6434031 100644 --- a/src/i18n/lang/en-US/comp.ts +++ b/src/i18n/lang/en-US/comp.ts @@ -90,7 +90,11 @@ export default { restart: 'Restart', refresh: 'Refresh', currentVersion: 'Current Version', - searchPlaceholder: 'Search for something...' + searchPlaceholder: 'Search for something...', + zoom: 'Zoom', + zoom100: 'Zoom 100%', + resetZoom: 'Reset Zoom', + zoomDefault: 'Default Zoom' }, titleBar: { closeTitle: 'Choose how to close', diff --git a/src/i18n/lang/zh-CN/comp.ts b/src/i18n/lang/zh-CN/comp.ts index 3d12294..f494fc6 100644 --- a/src/i18n/lang/zh-CN/comp.ts +++ b/src/i18n/lang/zh-CN/comp.ts @@ -88,7 +88,11 @@ export default { restart: '重启', refresh: '刷新', currentVersion: '当前版本', - searchPlaceholder: '搜索点什么吧...' + searchPlaceholder: '搜索点什么吧...', + zoom: '页面缩放', + zoom100: '标准缩放100%', + resetZoom: '点击重置缩放', + zoomDefault: '标准缩放' }, titleBar: { closeTitle: '请选择关闭方式', diff --git a/src/main/modules/window.ts b/src/main/modules/window.ts index 41bb934..ed84026 100644 --- a/src/main/modules/window.ts +++ b/src/main/modules/window.ts @@ -1,21 +1,80 @@ import { is } from '@electron-toolkit/utils'; import { app, BrowserWindow, globalShortcut, ipcMain, screen, session, shell } from 'electron'; import Store from 'electron-store'; +import windowStateKeeper from 'electron-window-state'; import { join } from 'path'; const store = new Store(); -// 保存主窗口的大小和位置 -let mainWindowState = { - width: 1200, - height: 780, +// 默认窗口尺寸 +const DEFAULT_MAIN_WIDTH = 1200; +const DEFAULT_MAIN_HEIGHT = 780; +const DEFAULT_MINI_WIDTH = 340; +const DEFAULT_MINI_HEIGHT = 64; +const DEFAULT_MINI_EXPANDED_HEIGHT = 400; + +// 保存主窗口引用,以便在 activate 事件中使用 +let mainWindowInstance: BrowserWindow | null = null; + +// 保存迷你模式前的窗口状态 +let preMiniModeState = { + width: DEFAULT_MAIN_WIDTH, + height: DEFAULT_MAIN_HEIGHT, x: undefined as number | undefined, y: undefined as number | undefined, isMaximized: false }; -// 保存主窗口引用,以便在 activate 事件中使用 -let mainWindowInstance: BrowserWindow | null = null; +/** + * 计算适合当前缩放比的缩放因子 + * @returns 适合当前系统的页面缩放因子 + */ +function calculateContentZoomFactor(): number { + // 获取系统的缩放因子 + const { scaleFactor } = screen.getPrimaryDisplay(); + + // 缩放因子默认为1 + let zoomFactor = 1; + + // 只在高DPI情况下调整 + if (scaleFactor > 1) { + // 自定义逻辑来根据不同的缩放比例进行调整 + if (scaleFactor >= 2.5) { + // 极高缩放比,例如4K屏幕用200%+缩放 + zoomFactor = 0.7; + } else if (scaleFactor >= 2) { + // 高缩放比,例如200% + zoomFactor = 0.8; + } else if (scaleFactor >= 1.5) { + // 中等缩放比,例如150% + zoomFactor = 0.85; + } else if (scaleFactor > 1.25) { + // 略高缩放比,例如125%-149% + zoomFactor = 0.9; + } else { + // 低缩放比,不做调整 + zoomFactor = 1; + } + } + + // 获取用户的自定义缩放设置(如果有) + const userZoomFactor = store.get('set.contentZoomFactor') as number | undefined; + if (userZoomFactor) { + zoomFactor = userZoomFactor; + } + + return zoomFactor; +} + +/** + * 应用页面内容缩放 + * @param window 目标窗口 + */ +function applyContentZoom(window: BrowserWindow): void { + const zoomFactor = calculateContentZoomFactor(); + window.webContents.setZoomFactor(zoomFactor); + console.log(`应用页面缩放因子: ${zoomFactor}, 系统缩放比: ${screen.getPrimaryDisplay().scaleFactor}`); +} /** * 初始化代理设置 @@ -86,10 +145,10 @@ export function initializeWindowManager() { ipcMain.on('mini-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { - // 保存当前窗口状态 + // 保存当前窗口状态,以便之后恢复 const [width, height] = win.getSize(); const [x, y] = win.getPosition(); - mainWindowState = { + preMiniModeState = { width, height, x, @@ -97,15 +156,17 @@ export function initializeWindowManager() { isMaximized: win.isMaximized() }; - // 获取屏幕尺寸 - const { width: screenWidth } = screen.getPrimaryDisplay().workAreaSize; - + // 获取屏幕工作区尺寸 + const display = screen.getDisplayMatching(win.getBounds()); + const { width: screenWidth, x: screenX } = display.workArea; + // 设置迷你窗口的大小和位置 win.unmaximize(); - win.setMinimumSize(340, 64); - win.setMaximumSize(340, 64); - win.setSize(340, 64); - win.setPosition(screenWidth - 340, 20); + win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); + win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); + win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); + // 将迷你窗口放在工作区的右上角,留出一些边距 + win.setPosition(screenX + screenWidth - DEFAULT_MINI_WIDTH - 20, display.workArea.y + 20); win.setAlwaysOnTop(true); win.setSkipTaskbar(false); win.setResizable(false); @@ -115,6 +176,9 @@ export function initializeWindowManager() { // 发送事件到渲染进程,通知切换到迷你模式 win.webContents.send('mini-mode', true); + + // 迷你窗口使用默认的缩放比 + win.webContents.setZoomFactor(1); } }); @@ -124,18 +188,57 @@ export function initializeWindowManager() { if (win) { // 恢复窗口的大小调整功能 win.setResizable(true); - win.setMaximumSize(0, 0); - - // 恢复窗口的最小尺寸限制 - win.setMinimumSize(1200, 780); + win.setMaximumSize(0, 0); // 取消最大尺寸限制 + + // 根据屏幕尺寸计算合适的最小尺寸 + const workArea = screen.getDisplayMatching(win.getBounds()).workArea; + const minWidth = Math.min(DEFAULT_MAIN_WIDTH, workArea.width * 0.6); + const minHeight = Math.min(DEFAULT_MAIN_HEIGHT, workArea.height * 0.6); + + win.setMinimumSize(Math.round(minWidth), Math.round(minHeight)); // 恢复窗口状态 - if (mainWindowState.isMaximized) { + if (preMiniModeState.isMaximized) { win.maximize(); } else { - win.setSize(mainWindowState.width, mainWindowState.height); - if (mainWindowState.x !== undefined && mainWindowState.y !== undefined) { - win.setPosition(mainWindowState.x, mainWindowState.y); + // 确保窗口尺寸不超过当前屏幕 + const { width, height } = screen.getDisplayMatching({ + x: preMiniModeState.x || 0, + y: preMiniModeState.y || 0, + width: 1, + height: 1 + }).workArea; + + win.setSize( + Math.min(preMiniModeState.width, Math.round(width * 0.9)), + Math.min(preMiniModeState.height, Math.round(height * 0.9)) + ); + + if (preMiniModeState.x !== undefined && preMiniModeState.y !== undefined) { + // 确保窗口位于屏幕内 + const displays = screen.getAllDisplays(); + let isVisible = false; + + for (const display of displays) { + const { x, y, width, height } = display.workArea; + if ( + preMiniModeState.x >= x && + preMiniModeState.x < x + width && + preMiniModeState.y >= y && + preMiniModeState.y < y + height + ) { + isVisible = true; + break; + } + } + + if (isVisible) { + win.setPosition(preMiniModeState.x, preMiniModeState.y); + } else { + win.center(); // 如果位置不可见,则居中 + } + } else { + win.center(); } } @@ -147,6 +250,9 @@ export function initializeWindowManager() { // 发送事件到渲染进程,通知退出迷你模式 win.webContents.send('mini-mode', false); + + // 应用页面内容缩放 + applyContentZoom(win); } }); @@ -154,6 +260,13 @@ export function initializeWindowManager() { store.onDidChange('set.proxyConfig', () => { initializeProxy(); }); + + // 监听自定义内容缩放设置变化 + store.onDidChange('set.contentZoomFactor', () => { + if (mainWindowInstance && !mainWindowInstance.isDestroyed()) { + applyContentZoom(mainWindowInstance); + } + }); // 监听窗口大小调整事件 ipcMain.on('resize-window', (event, width, height) => { @@ -170,19 +283,61 @@ export function initializeWindowManager() { const win = BrowserWindow.fromWebContents(event.sender); if (win) { if (showPlaylist) { - console.log('主进程: 扩大迷你窗口至 340 x 400'); + console.log(`主进程: 扩大迷你窗口至 ${DEFAULT_MINI_WIDTH} x ${DEFAULT_MINI_EXPANDED_HEIGHT}`); // 调整最大尺寸限制,允许窗口变大 - win.setMinimumSize(340, 64); - win.setMaximumSize(340, 400); + win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); + win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_EXPANDED_HEIGHT); // 调整窗口尺寸 - win.setSize(340, 400); + win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_EXPANDED_HEIGHT); } else { - console.log('主进程: 缩小迷你窗口至 340 x 64'); + console.log(`主进程: 缩小迷你窗口至 ${DEFAULT_MINI_WIDTH} x ${DEFAULT_MINI_HEIGHT}`); // 强制重置尺寸限制,确保窗口可以缩小 - win.setMaximumSize(340, 64); - win.setMinimumSize(340, 64); + win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); + win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); // 调整窗口尺寸 - win.setSize(340, 64); + win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); + } + } + }); + + // 允许用户通过IPC调整页面缩放比例 + ipcMain.on('set-content-zoom', (event, zoomFactor) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + win.webContents.setZoomFactor(zoomFactor); + store.set('set.contentZoomFactor', zoomFactor); + } + }); + + // 获取当前缩放比例 + ipcMain.handle('get-content-zoom', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + return win.webContents.getZoomFactor(); + } + return 1; // 默认缩放比例 + }); + + // 获取系统缩放因子 + ipcMain.handle('get-system-scale-factor', () => { + return screen.getPrimaryDisplay().scaleFactor; + }); + + // 重置页面缩放到基于系统缩放比的默认值 + ipcMain.on('reset-content-zoom', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + store.delete('set.contentZoomFactor'); + applyContentZoom(win); + } + }); + + // 监听显示器变化事件 + screen.on('display-metrics-changed', (_event, _display, changedMetrics) => { + if (mainWindowInstance && !mainWindowInstance.isDestroyed()) { + // 当缩放因子变化时,重新应用页面缩放 + if (changedMetrics.includes('scaleFactor')) { + applyContentZoom(mainWindowInstance); } } }); @@ -203,9 +358,27 @@ export function initializeWindowManager() { * 创建主窗口 */ export function createMainWindow(icon: Electron.NativeImage): BrowserWindow { + // 使用 electron-window-state 管理窗口状态 + const mainWindowState = windowStateKeeper({ + defaultWidth: DEFAULT_MAIN_WIDTH, + defaultHeight: DEFAULT_MAIN_HEIGHT + }); + + // 计算适当的最小尺寸(基于工作区大小) + const primaryDisplay = screen.getPrimaryDisplay(); + const { width: workAreaWidth, height: workAreaHeight } = primaryDisplay.workArea; + + // 根据缩放因子和工作区大小调整最小尺寸 + const minWidth = Math.min(Math.round(DEFAULT_MAIN_WIDTH * 0.6), Math.round(workAreaWidth * 0.5)); + const minHeight = Math.min(Math.round(DEFAULT_MAIN_HEIGHT * 0.6), Math.round(workAreaHeight * 0.5)); + const mainWindow = new BrowserWindow({ - width: 1200, - height: 780, + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, + minWidth, + minHeight, show: false, frame: false, autoHideMenuBar: true, @@ -218,11 +391,27 @@ export function createMainWindow(icon: Electron.NativeImage): BrowserWindow { } }); - mainWindow.setMinimumSize(1200, 780); + // 确保最小尺寸设置正确 + mainWindow.setMinimumSize(minWidth, minHeight); mainWindow.removeMenu(); + + // 让 windowStateKeeper 管理窗口状态 + mainWindowState.manage(mainWindow); + + // 初始化时保存到 preMiniModeState,以便从迷你模式恢复 + preMiniModeState = { + width: mainWindowState.width, + height: mainWindowState.height, + x: mainWindowState.x, + y: mainWindowState.y, + isMaximized: mainWindowState.isMaximized + }; mainWindow.on('ready-to-show', () => { mainWindow.show(); + + // 应用页面内容缩放 + applyContentZoom(mainWindow); }); mainWindow.webContents.setWindowOpenHandler((details) => { @@ -247,7 +436,5 @@ export function createMainWindow(icon: Electron.NativeImage): BrowserWindow { // 保存主窗口引用 mainWindowInstance = mainWindow; - mainWindow.on('blur', () => mainWindow && mainWindow.setMaximizable(false)) - return mainWindow; } diff --git a/src/main/set.json b/src/main/set.json index 4750155..fd2a2c8 100644 --- a/src/main/set.json +++ b/src/main/set.json @@ -24,5 +24,6 @@ "unlimitedDownload": false, "enableMusicUnblock": true, "enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili", "kuwo"], - "showTopAction": false + "showTopAction": false, + "contentZoomFactor": 1 } diff --git a/src/renderer/hooks/useZoom.ts b/src/renderer/hooks/useZoom.ts new file mode 100644 index 0000000..6c57616 --- /dev/null +++ b/src/renderer/hooks/useZoom.ts @@ -0,0 +1,95 @@ +import { ref } from 'vue'; + +/** + * 页面缩放功能的组合式API + * 提供页面缩放相关的状态和方法 + */ +export function useZoom() { + // 缩放相关常量 + const MIN_ZOOM = 0.5; + const MAX_ZOOM = 1.5; + const ZOOM_STEP = 0.05; // 5%的步长 + + // 当前缩放因子 + const zoomFactor = ref(1); + + // 初始化获取当前缩放比例 + const initZoomFactor = async () => { + try { + const currentZoom = await window.ipcRenderer.invoke('get-content-zoom'); + zoomFactor.value = currentZoom; + } catch (error) { + console.error('获取缩放比例失败:', error); + } + }; + + // 增加缩放比例,保证100%为节点 + const increaseZoom = () => { + let newZoom; + + // 如果当前缩放低于100%并且增加后会超过100%,则直接设为100% + if (zoomFactor.value < 1.0 && zoomFactor.value + ZOOM_STEP > 1.0) { + newZoom = 1.0; // 精确设置为100% + } else { + newZoom = Math.min(MAX_ZOOM, Math.round((zoomFactor.value + ZOOM_STEP) * 20) / 20); + } + + setZoomFactor(newZoom); + }; + + // 减少缩放比例,保证100%为节点 + const decreaseZoom = () => { + let newZoom; + + // 如果当前缩放大于100%并且减少后会低于100%,则直接设为100% + if (zoomFactor.value > 1.0 && zoomFactor.value - ZOOM_STEP < 1.0) { + newZoom = 1.0; // 精确设置为100% + } else { + newZoom = Math.max(MIN_ZOOM, Math.round((zoomFactor.value - ZOOM_STEP) * 20) / 20); + } + + setZoomFactor(newZoom); + }; + + // 重置缩放比例到系统建议值 + const resetZoom = async () => { + try { + window.ipcRenderer.send('reset-content-zoom'); + // 重置后重新获取系统计算的缩放比例 + const newZoom = await window.ipcRenderer.invoke('get-content-zoom'); + zoomFactor.value = newZoom; + } catch (error) { + console.error('重置缩放比例失败:', error); + } + }; + + // 设置为100%标准缩放 + const setZoom100 = () => { + setZoomFactor(1.0); + }; + + // 设置缩放比例 + const setZoomFactor = (zoom: number) => { + window.ipcRenderer.send('set-content-zoom', zoom); + zoomFactor.value = zoom; + }; + + // 检查是否为100%缩放 + const isZoom100 = () => { + return Math.abs(zoomFactor.value - 1.0) < 0.001; + }; + + return { + zoomFactor, + initZoomFactor, + increaseZoom, + decreaseZoom, + resetZoom, + setZoom100, + setZoomFactor, + isZoom100, + MIN_ZOOM, + MAX_ZOOM, + ZOOM_STEP + }; +} \ No newline at end of file diff --git a/src/renderer/layout/components/SearchBar.vue b/src/renderer/layout/components/SearchBar.vue index d1b0e43..a1ea291 100644 --- a/src/renderer/layout/components/SearchBar.vue +++ b/src/renderer/layout/components/SearchBar.vue @@ -62,6 +62,24 @@ {{ t('comp.searchBar.set') }} +