feat: add the ability to display the application in tray #297
This commit is contained in:
parent
612b39a878
commit
e11633310c
BIN
build/tray_icon_dark.png
Normal file
BIN
build/tray_icon_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
build/tray_icon_light.png
Normal file
BIN
build/tray_icon_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
9
src/main/electron.d.ts
vendored
Normal file
9
src/main/electron.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare global {
|
||||
namespace Electron {
|
||||
interface App {
|
||||
isQuitting: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@ -4,8 +4,9 @@ import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { registerZoomShortcut } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { updateUserDataPath } from './utils/upgrade'
|
||||
import { createMainWindow } from './window'
|
||||
|
||||
// Check for single instance lock
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@ -21,21 +22,19 @@ app.whenReady().then(async () => {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
const mainWindow = windowService.createMainWindow()
|
||||
new TrayService()
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
windowService.createMainWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
})
|
||||
|
||||
const mainWindow = createMainWindow()
|
||||
|
||||
registerZoomShortcut(mainWindow)
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
@ -56,13 +55,12 @@ app.on('second-instance', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
app.isQuitting = true
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app"s specific main process
|
||||
|
||||
@ -9,8 +9,8 @@ import BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import { createMinappWindow } from './window'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
@ -79,7 +79,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// minapp
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
createMinappWindow({
|
||||
windowService.createMinappWindow({
|
||||
url: args.url,
|
||||
parent: mainWindow,
|
||||
windowOptions: {
|
||||
|
||||
BIN
src/main/resources/icon.ico
Normal file
BIN
src/main/resources/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
68
src/main/services/TrayService.ts
Normal file
68
src/main/services/TrayService.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron'
|
||||
|
||||
import iconDark from '../../../build/tray_icon_dark.png?asset'
|
||||
import iconLight from '../../../build/tray_icon_light.png?asset'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export class TrayService {
|
||||
private tray: Tray | null = null
|
||||
|
||||
constructor() {
|
||||
this.createTray()
|
||||
}
|
||||
|
||||
private createTray() {
|
||||
const iconPath = nativeTheme.shouldUseDarkColors ? iconLight : iconDark
|
||||
const tray = new Tray(iconPath)
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
tray.setImage(iconPath)
|
||||
nativeTheme.on('updated', () => {
|
||||
const newIconPath = nativeTheme.shouldUseDarkColors ? iconLight : iconDark
|
||||
tray.setImage(newIconPath)
|
||||
})
|
||||
} else if (process.platform === 'darwin') {
|
||||
const image = nativeImage.createFromPath(iconPath)
|
||||
const resizedImage = image.resize({ width: 16, height: 16 })
|
||||
resizedImage.setTemplateImage(true)
|
||||
tray.setImage(resizedImage)
|
||||
} else if (process.platform === 'linux') {
|
||||
const image = nativeImage.createFromPath(iconPath)
|
||||
const resizedImage = image.resize({ width: 24, height: 24 })
|
||||
tray.setImage(resizedImage)
|
||||
nativeTheme.on('updated', () => {
|
||||
const newIconPath = nativeTheme.shouldUseDarkColors ? iconLight : iconDark
|
||||
const newImage = nativeImage.createFromPath(newIconPath)
|
||||
const newResizedImage = newImage.resize({ width: 24, height: 24 })
|
||||
tray.setImage(newResizedImage)
|
||||
})
|
||||
}
|
||||
|
||||
this.tray = tray
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '显示窗口',
|
||||
click: () => windowService.showMainWindow()
|
||||
},
|
||||
{
|
||||
label: '退出',
|
||||
click: () => this.quit()
|
||||
}
|
||||
])
|
||||
|
||||
this.tray.setToolTip('Cherry Studio')
|
||||
|
||||
this.tray.on('right-click', () => {
|
||||
this.tray?.popUpContextMenu(contextMenu)
|
||||
})
|
||||
|
||||
this.tray.on('click', () => {
|
||||
windowService.showMainWindow()
|
||||
})
|
||||
}
|
||||
|
||||
private quit() {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
194
src/main/services/WindowService.ts
Normal file
194
src/main/services/WindowService.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../../build/icon.png?asset'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||
import { locales } from '../utils/locales'
|
||||
import { configManager } from './ConfigManager'
|
||||
|
||||
export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
|
||||
public static getInstance(): WindowService {
|
||||
if (!WindowService.instance) {
|
||||
WindowService.instance = new WindowService()
|
||||
}
|
||||
return WindowService.instance
|
||||
}
|
||||
|
||||
public createMainWindow(): BrowserWindow {
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'fullscreen-ui',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
}
|
||||
})
|
||||
|
||||
this.setupMainWindow(this.mainWindow, mainWindowState)
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
public createMinappWindow({
|
||||
url,
|
||||
parent,
|
||||
windowOptions
|
||||
}: {
|
||||
url: string
|
||||
parent?: BrowserWindow
|
||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||
}): BrowserWindow {
|
||||
const width = windowOptions?.width || 1000
|
||||
const height = windowOptions?.height || 680
|
||||
|
||||
const minappWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
autoHideMenuBar: true,
|
||||
title: 'Cherry Studio',
|
||||
...windowOptions,
|
||||
parent,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/minapp.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
minappWindow.loadURL(url)
|
||||
return minappWindow
|
||||
}
|
||||
|
||||
private setupMainWindow(mainWindow: BrowserWindow, mainWindowState: any) {
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
this.setupContextMenu(mainWindow)
|
||||
this.setupWindowEvents(mainWindow)
|
||||
this.setupWebContentsHandlers(mainWindow)
|
||||
this.setupWindowLifecycleEvents(mainWindow)
|
||||
this.loadMainWindowContent(mainWindow)
|
||||
}
|
||||
|
||||
private setupContextMenu(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { common } = locale.translation
|
||||
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: common.copy, role: 'copy' }))
|
||||
menu.append(new MenuItem({ label: common.paste, role: 'paste' }))
|
||||
menu.append(new MenuItem({ label: common.cut, role: 'cut' }))
|
||||
menu.popup()
|
||||
})
|
||||
}
|
||||
|
||||
private setupWindowEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
}
|
||||
|
||||
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
this.setupWebRequestHeaders(mainWindow)
|
||||
}
|
||||
|
||||
private setupWebRequestHeaders(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||
delete details.responseHeaders['X-Frame-Options']
|
||||
}
|
||||
if (details.responseHeaders?.['x-frame-options']) {
|
||||
delete details.responseHeaders['x-frame-options']
|
||||
}
|
||||
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||
delete details.responseHeaders['Content-Security-Policy']
|
||||
}
|
||||
if (details.responseHeaders?.['content-security-policy']) {
|
||||
delete details.responseHeaders['content-security-policy']
|
||||
}
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||
})
|
||||
}
|
||||
|
||||
private loadMainWindowContent(mainWindow: BrowserWindow) {
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
public getMainWindow(): BrowserWindow | null {
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!app.isQuitting) {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('minimize', (event) => {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
})
|
||||
}
|
||||
|
||||
public showMainWindow() {
|
||||
if (this.mainWindow) {
|
||||
if (this.mainWindow.isMinimized()) {
|
||||
this.mainWindow.restore()
|
||||
}
|
||||
this.mainWindow.show()
|
||||
this.mainWindow.focus()
|
||||
} else {
|
||||
this.createMainWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const windowService = WindowService.getInstance()
|
||||
@ -1,132 +0,0 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { locales } from './utils/locales'
|
||||
|
||||
export function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
|
||||
// Create the browser window.
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'fullscreen-ui',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
// devTools: !app.isPackaged,
|
||||
}
|
||||
})
|
||||
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { common } = locale.translation
|
||||
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: common.copy, role: 'copy' }))
|
||||
menu.append(new MenuItem({ label: common.paste, role: 'paste' }))
|
||||
menu.append(new MenuItem({ label: common.cut, role: 'cut' }))
|
||||
menu.popup()
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||
delete details.responseHeaders['X-Frame-Options']
|
||||
}
|
||||
if (details.responseHeaders?.['x-frame-options']) {
|
||||
delete details.responseHeaders['x-frame-options']
|
||||
}
|
||||
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||
delete details.responseHeaders['Content-Security-Policy']
|
||||
}
|
||||
if (details.responseHeaders?.['content-security-policy']) {
|
||||
delete details.responseHeaders['content-security-policy']
|
||||
}
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
export function createMinappWindow({
|
||||
url,
|
||||
parent,
|
||||
windowOptions
|
||||
}: {
|
||||
url: string
|
||||
parent?: BrowserWindow
|
||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||
}) {
|
||||
const width = windowOptions?.width || 1000
|
||||
const height = windowOptions?.height || 680
|
||||
|
||||
const minappWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
autoHideMenuBar: true,
|
||||
title: 'Cherry Studio',
|
||||
...windowOptions,
|
||||
parent,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/minapp.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
minappWindow.loadURL(url)
|
||||
|
||||
return minappWindow
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user