diff --git a/package.json b/package.json index 39aaade..c987989 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@electron-toolkit/utils": "^4.0.0", "better-sqlite3": "^12.6.0", "chokidar": "^5.0.0", + "mica-electron": "^1.5.16", "pinyin-pro": "^3.27.0", "react-router-dom": "^6.28.0", "reflect-metadata": "^0.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 658f469..d4329d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: chokidar: specifier: ^5.0.0 version: 5.0.0 + mica-electron: + specifier: ^1.5.16 + version: 1.5.16 pinyin-pro: specifier: ^3.27.0 version: 3.27.0 @@ -753,66 +756,79 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -2199,6 +2215,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mica-electron@1.5.16: + resolution: {integrity: sha512-08/o9vv4cr4ozltO2U4/37R70Y6hqAjH2nr7GzpDLAdqCWkeAAge5ZPrzl+ndUDhO6IsoL2gFmpbU17Jj406Zg==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -5568,6 +5587,8 @@ snapshots: math-intrinsics@1.1.0: {} + mica-electron@1.5.16: {} + mime-db@1.52.0: {} mime-types@2.1.35: diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index 8638710..7bfcd61 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -59,7 +59,6 @@ export class SettingsService extends Service { wc.setZoomFactor(zoom) }) - // 仅更新全局侧边栏窗口的位置(不调整大小,大小由 GlobalSidebar 组件管理) const sidebarWindow = BrowserWindow.getAllWindows().find((win) => { try { return win.webContents.getURL().includes('#global-sidebar') @@ -81,6 +80,24 @@ export class SettingsService extends Service { }) } } + }, + window_theme: { + kind: 'string', + defaultValue: 'auto', + writePermission: 'admin', + validate: (v) => v === 'auto' || v === 'dark' || v === 'light' + }, + window_effect: { + kind: 'string', + defaultValue: 'mica', + writePermission: 'admin', + validate: (v) => ['mica', 'tabbed', 'acrylic', 'blur', 'transparent', 'none'].includes(v as string) + }, + window_radius: { + kind: 'string', + defaultValue: 'rounded', + writePermission: 'admin', + validate: (v) => v === 'rounded' || v === 'small' || v === 'square' } } diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts index bb4ffeb..3cc2852 100644 --- a/src/main/services/WindowManager.ts +++ b/src/main/services/WindowManager.ts @@ -3,6 +3,39 @@ import { MainContext } from '../context' import { BrowserWindow, shell, screen } from 'electron' import type { BrowserWindowConstructorOptions } from 'electron' +let micaElectron: typeof import('mica-electron') | null = null +let MicaBrowserWindow: any = null +let IS_WINDOWS_11 = false + +try { + if (process.platform === 'win32') { + const micaModule = require('mica-electron') + micaElectron = micaModule + MicaBrowserWindow = micaModule.MicaBrowserWindow + IS_WINDOWS_11 = micaModule.IS_WINDOWS_11 + } +} catch (error) { + console.warn('mica-electron not available:', error) +} + +interface MicaWindow extends BrowserWindow { + setMicaEffect(): void + setMicaTabbedEffect(): void + setMicaAcrylicEffect(): void + setAcrylic(): void + setBlur(): void + setTransparent(): void + setDarkTheme(): void + setLightTheme(): void + setAutoTheme(): void + setRoundedCorner(): void + setSmallRoundedCorner(): void + setSquareCorner(): void + setBorderColor(color: string | null): void + setCaptionColor(color: string | null): void + setTitleTextColor(color: string | null): void +} + export type windowOpenInput = { key: string title?: string @@ -52,7 +85,6 @@ export class WindowManager extends Service { const existing = this.get(input.key) if (existing) { if (input.route) { - // Use soft navigation if window exists to prevent reload existing.webContents.send('app:navigate', input.route) } existing.show() @@ -60,12 +92,12 @@ export class WindowManager extends Service { return existing } - const win = new BrowserWindow({ + const baseOptions: BrowserWindowConstructorOptions = { width: 900, height: 670, show: false, autoHideMenuBar: true, - frame: false, // Custom title bar + frame: false, icon: this.opts.icon, title: input.title, webPreferences: { @@ -73,7 +105,15 @@ export class WindowManager extends Service { sandbox: false }, ...input.options - }) + } + + let win: BrowserWindow + if (MicaBrowserWindow) { + win = new MicaBrowserWindow(baseOptions) + this.applyMicaEffect(win) + } else { + win = new BrowserWindow(baseOptions) + } // Special positioning for global sidebar if (input.key === 'global-sidebar') { @@ -130,6 +170,156 @@ export class WindowManager extends Service { return win } + private applyMicaEffect(win: BrowserWindow) { + if (!micaElectron) return + const micaWin = win as MicaWindow + + const theme = this.mainCtx.settings.getValue('window_theme') + switch (theme) { + case 'dark': + micaWin.setDarkTheme() + break + case 'light': + micaWin.setLightTheme() + break + default: + micaWin.setAutoTheme() + } + + const effect = this.mainCtx.settings.getValue('window_effect') + switch (effect) { + case 'mica': + micaWin.setMicaEffect() + break + case 'tabbed': + micaWin.setMicaTabbedEffect() + break + case 'acrylic': + if (IS_WINDOWS_11) { + micaWin.setMicaAcrylicEffect() + } else { + micaWin.setAcrylic() + } + break + case 'blur': + if (!IS_WINDOWS_11) { + micaWin.setBlur() + } + break + case 'transparent': + if (!IS_WINDOWS_11) { + micaWin.setTransparent() + } + break + } + + const radius = this.mainCtx.settings.getValue('window_radius') + switch (radius) { + case 'small': + micaWin.setSmallRoundedCorner() + break + case 'square': + micaWin.setSquareCorner() + break + default: + micaWin.setRoundedCorner() + } + } + + public setMicaEffect(win: BrowserWindow, effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' = 'mica') { + if (!micaElectron) return + const micaWin = win as MicaWindow + + switch (effect) { + case 'mica': + micaWin.setMicaEffect() + break + case 'tabbed': + micaWin.setMicaTabbedEffect() + break + case 'acrylic': + if (IS_WINDOWS_11) { + micaWin.setMicaAcrylicEffect() + } else { + micaWin.setAcrylic() + } + break + case 'blur': + if (!IS_WINDOWS_11) { + micaWin.setBlur() + } + break + case 'transparent': + if (!IS_WINDOWS_11) { + micaWin.setTransparent() + } + break + case 'none': + if (IS_WINDOWS_11) { + micaWin.setMicaEffect() + } + break + } + } + + public setMicaTheme(win: BrowserWindow, theme: 'auto' | 'dark' | 'light' = 'auto') { + if (!micaElectron) return + const micaWin = win as MicaWindow + + switch (theme) { + case 'dark': + micaWin.setDarkTheme() + break + case 'light': + micaWin.setLightTheme() + break + default: + micaWin.setAutoTheme() + } + } + + public setMicaCorner(win: BrowserWindow, corner: 'rounded' | 'small' | 'square' = 'rounded') { + if (!micaElectron) return + const micaWin = win as MicaWindow + + switch (corner) { + case 'small': + micaWin.setSmallRoundedCorner() + break + case 'square': + micaWin.setSquareCorner() + break + default: + micaWin.setRoundedCorner() + } + } + + public setMicaBorderColor(win: BrowserWindow, color: string | null) { + if (!micaElectron) return + const micaWin = win as MicaWindow + micaWin.setBorderColor(color) + } + + public setMicaCaptionColor(win: BrowserWindow, color: string | null) { + if (!micaElectron) return + const micaWin = win as MicaWindow + micaWin.setCaptionColor(color) + } + + public setMicaTitleTextColor(win: BrowserWindow, color: string | null) { + if (!micaElectron) return + const micaWin = win as MicaWindow + micaWin.setTitleTextColor(color) + } + + public isMicaAvailable(): boolean { + return micaElectron !== null + } + + public isWindows11(): boolean { + return IS_WINDOWS_11 + } + public navigate(key: string, route: string) { const win = this.get(key) if (!win) return false @@ -236,7 +426,6 @@ export class WindowManager extends Service { const win = BrowserWindow.fromWebContents(event.sender) if (win) { const bounds = win.getBounds() - // Keep right side pinned const newX = bounds.x + (bounds.width - width) win.setBounds({ x: newX, @@ -246,5 +435,33 @@ export class WindowManager extends Service { }) } }) + + this.mainCtx.handle('window:mica-effect', (_event, effect: string) => { + const win = BrowserWindow.fromWebContents(_event.sender) + if (win) { + this.setMicaEffect(win, effect as any) + } + }) + + this.mainCtx.handle('window:mica-theme', (_event, theme: string) => { + const win = BrowserWindow.fromWebContents(_event.sender) + if (win) { + this.setMicaTheme(win, theme as any) + } + }) + + this.mainCtx.handle('window:mica-corner', (_event, corner: string) => { + const win = BrowserWindow.fromWebContents(_event.sender) + if (win) { + this.setMicaCorner(win, corner as any) + } + }) + + this.mainCtx.handle('window:mica-info', () => { + return { + available: this.isMicaAvailable(), + isWindows11: this.isWindows11() + } + }) } } diff --git a/src/preload/types.ts b/src/preload/types.ts index 485a7f9..285eaba 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -21,6 +21,9 @@ export type settingsSpec = { is_wizard_completed: boolean log_level: logLevel window_zoom: number + window_theme: 'auto' | 'dark' | 'light' + window_effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' + window_radius: 'rounded' | 'small' | 'square' } export type settingsKey = keyof settingsSpec