diff --git a/src/main/index.ts b/src/main/index.ts index 907ef2b6..970dbaf5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,6 +4,7 @@ import { app, ipcMain } from 'electron' import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer' import { registerIpc } from './ipc' +import { configManager } from './services/ConfigManager' import { registerShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' import { windowService } from './services/WindowService' @@ -21,6 +22,12 @@ if (!app.requestSingleInstanceLock()) { // Set app user model id for windows electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio') + // Mac: Hide dock icon before window creation when launch to tray is set + const isLaunchToTray = configManager.getLaunchToTray() + if (isLaunchToTray) { + app.dock?.hide() + } + const mainWindow = windowService.createMainWindow() new TrayService() diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 10255e56..db2eb0ea 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,5 +1,6 @@ import fs from 'node:fs' +import { isMac, isWin } from '@main/constant' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { MCPServer, Shortcut, ThemeMode } from '@types' import { BrowserWindow, ipcMain, session, shell } from 'electron' @@ -68,11 +69,38 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { configManager.setLanguage(language) }) + // launch on boot + ipcMain.handle('app:set-launch-on-boot', (_, isActive: boolean) => { + // Set login item settings for windows and mac + // linux is not supported because it requires more file operations + if (isWin || isMac) { + if (isActive) { + app.setLoginItemSettings({ + openAtLogin: true + }) + } else { + app.setLoginItemSettings({ + openAtLogin: false + }) + } + } + }) + + // launch to tray + ipcMain.handle('app:set-launch-to-tray', (_, isActive: boolean) => { + configManager.setLaunchToTray(isActive) + }) + // tray ipcMain.handle('app:set-tray', (_, isActive: boolean) => { configManager.setTray(isActive) }) + // to tray on close + ipcMain.handle('app:set-tray-on-close', (_, isActive: boolean) => { + configManager.setTrayOnClose(isActive) + }) + ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray()) ipcMain.handle('config:set', (_, key: string, value: any) => { diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index 719d089e..da20bc41 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -30,6 +30,14 @@ export class ConfigManager { this.store.set('theme', theme) } + getLaunchToTray(): boolean { + return !!this.store.get('launchToTray', false) + } + + setLaunchToTray(value: boolean) { + this.store.set('launchToTray', value) + } + getTray(): boolean { return !!this.store.get('tray', true) } @@ -39,6 +47,14 @@ export class ConfigManager { this.notifySubscribers('tray', value) } + getTrayOnClose(): boolean { + return !!this.store.get('trayOnClose', true) + } + + setTrayOnClose(value: boolean) { + this.store.set('trayOnClose', value) + } + getZoomFactor(): number { return this.store.get('zoomFactor', 1) as number } diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts index 0738a6b9..10d46730 100644 --- a/src/main/services/ShortcutService.ts +++ b/src/main/services/ShortcutService.ts @@ -115,7 +115,20 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm } export function registerShortcuts(window: BrowserWindow) { - const register = () => { + window.once('ready-to-show', () => { + if (configManager.getLaunchToTray()) { + registerOnlyUniversalShortcuts() + } + }) + + //only for clearer code + const registerOnlyUniversalShortcuts = () => { + register(true) + } + + //onlyUniversalShortcuts is used to register shortcuts that are not window specific, like show_app & mini_window + //onlyUniversalShortcuts is needed when we launch to tray + const register = (onlyUniversalShortcuts: boolean = false) => { if (window.isDestroyed()) return const shortcuts = configManager.getShortcuts() @@ -131,6 +144,11 @@ export function registerShortcuts(window: BrowserWindow) { if (!shortcut.enabled) { return } + + // only register universal shortcuts when needed + if (onlyUniversalShortcuts && !['show_app', 'mini_window'].includes(shortcut.key)) { + return + } const handler = getShortcutHandler(shortcut) if (!handler) { diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 14bdef4e..9ae1c1dc 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -1,5 +1,5 @@ import { is } from '@electron-toolkit/utils' -import { isDev, isLinux, isWin } from '@main/constant' +import { isDev, isLinux, isMac, isWin } from '@main/constant' import { getFilesDir } from '@main/utils/file' import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron' import Logger from 'electron-log' @@ -39,8 +39,6 @@ export class WindowService { }) const theme = configManager.getTheme() - const isMac = process.platform === 'darwin' - const isLinux = process.platform === 'linux' this.mainWindow = new BrowserWindow({ x: mainWindowState.x, @@ -146,7 +144,12 @@ export class WindowService { private setupWindowEvents(mainWindow: BrowserWindow) { mainWindow.once('ready-to-show', () => { mainWindow.webContents.setZoomFactor(configManager.getZoomFactor()) - mainWindow.show() + + // show window only when laucn to tray not set + const isLaunchToTray = configManager.getLaunchToTray() + if (!isLaunchToTray) { + mainWindow.show() + } }) // 处理全屏相关事件 @@ -255,11 +258,18 @@ export class WindowService { return app.quit() } - // 没有开启托盘,且是Windows或Linux系统,直接退出 - const notInTray = !configManager.getTray() - if ((isWin || isLinux) && notInTray) { - return app.quit() + // 托盘及关闭行为设置 + const isShowTray = configManager.getTray() + const isTrayOnClose = configManager.getTrayOnClose() + // 没有开启托盘,或者开启了托盘,但设置了直接关闭,应执行直接退出 + if (!isShowTray || (isShowTray && !isTrayOnClose)) { + // 如果是Windows或Linux,直接退出 + // mac按照系统默认行为,不退出 + if (isWin || isLinux) { + return app.quit() + } } + //上述逻辑以下,是“开启托盘+设置关闭时最小化到托盘”的情况 // 如果是Windows或Linux,且处于全屏状态,则退出应用 if (this.wasFullScreen) { @@ -273,6 +283,7 @@ export class WindowService { } event.preventDefault() mainWindow.hide() + app.dock?.hide() //for mac to hide to tray }) mainWindow.on('closed', () => { @@ -301,6 +312,8 @@ export class WindowService { this.mainWindow = this.createMainWindow() this.mainWindow.focus() } + //for mac users, when window is shown, should show dock icon (dock may be set to hide when launch) + app.dock?.show() } public showMiniWindow() { @@ -310,9 +323,6 @@ export class WindowService { return } - if (this.mainWindow && !this.mainWindow.isDestroyed()) { - this.mainWindow.hide() - } if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) { this.selectionMenuWindow.hide() } @@ -327,8 +337,6 @@ export class WindowService { return } - const isMac = process.platform === 'darwin' - this.miniWindow = new BrowserWindow({ width: 500, height: 520, @@ -403,7 +411,6 @@ export class WindowService { } const theme = configManager.getTheme() - const isMac = process.platform === 'darwin' this.selectionMenuWindow = new BrowserWindow({ width: 280, diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 4d0b8804..7384816b 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -23,7 +23,10 @@ declare global { openWebsite: (url: string) => void setProxy: (proxy: string | undefined) => void setLanguage: (theme: LanguageVarious) => void + setLaunchOnBoot: (isActive: boolean) => void + setLaunchToTray: (isActive: boolean) => void setTray: (isActive: boolean) => void + setTrayOnClose: (isActive: boolean) => void restartTray: () => void setTheme: (theme: 'light' | 'dark') => void minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void diff --git a/src/preload/index.ts b/src/preload/index.ts index 831b59a7..0a1ea8c5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,7 +11,10 @@ const api = { checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'), showUpdateDialog: () => ipcRenderer.invoke('app:show-update-dialog'), setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang), + setLaunchOnBoot: (isActive: boolean) => ipcRenderer.invoke('app:set-launch-on-boot', isActive), + setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke('app:set-launch-to-tray', isActive), setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive), + setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke('app:set-tray-on-close', isActive), restartTray: () => ipcRenderer.invoke('app:restart-tray'), setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme), openWebsite: (url: string) => ipcRenderer.invoke('open:website', url), diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index abe673c2..0a05a162 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -1,6 +1,8 @@ import store, { useAppDispatch, useAppSelector } from '@renderer/store' import { SendMessageShortcut, + setLaunchOnBoot, + setLaunchToTray, setSendMessageShortcut as _setSendMessageShortcut, setShowAssistantIcon, setSidebarIcons, @@ -8,7 +10,8 @@ import { setTheme, SettingsState, setTopicPosition, - setTray, + setTray as _setTray, + setTrayOnClose, setWindowStyle } from '@renderer/store/settings' import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types' @@ -22,10 +25,30 @@ export function useSettings() { setSendMessageShortcut(shortcut: SendMessageShortcut) { dispatch(_setSendMessageShortcut(shortcut)) }, - setTray(isActive: boolean) { - dispatch(setTray(isActive)) - window.api.setTray(isActive) + + setLaunch(isLaunchOnBoot: boolean | undefined, isLaunchToTray: boolean | undefined = undefined) { + if (isLaunchOnBoot !== undefined) { + dispatch(setLaunchOnBoot(isLaunchOnBoot)) + window.api.setLaunchOnBoot(isLaunchOnBoot) + } + + if (isLaunchToTray !== undefined) { + dispatch(setLaunchToTray(isLaunchToTray)) + window.api.setLaunchToTray(isLaunchToTray) + } }, + + setTray(isShowTray: boolean | undefined, isTrayOnClose: boolean | undefined = undefined) { + if (isShowTray !== undefined) { + dispatch(_setTray(isShowTray)) + window.api.setTray(isShowTray) + } + if (isTrayOnClose !== undefined) { + dispatch(setTrayOnClose(isTrayOnClose)) + window.api.setTrayOnClose(isTrayOnClose) + } + }, + setTheme(theme: ThemeMode) { dispatch(setTheme(theme)) }, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8c68ccdb..5223dc5f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1095,7 +1095,12 @@ "topic.position.left": "Left", "topic.position.right": "Right", "topic.show.time": "Show topic time", - "tray.title": "Enable System Tray Icon", + "tray.title": "Tray", + "tray.show": "Show Tray Icon", + "tray.onclose": "Minimize to Tray on Close", + "launch.title": "Launch", + "launch.onboot": "Start Automatically on Boot", + "launch.totray": "Minimize to Tray on Launch", "websearch": { "blacklist": "Blacklist", "blacklist_description": "Results from the following websites will not appear in search results", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 68066152..165d242d 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1095,7 +1095,12 @@ "topic.position.left": "左", "topic.position.right": "右", "topic.show.time": "トピックの時間を表示", - "tray.title": "システムトレイアイコンを有効にする", + "tray.title": "トレイ", + "tray.show": "トレイアイコンを表示", + "tray.onclose": "閉じるときにトレイに最小化", + "launch.title": "起動", + "launch.onboot": "起動時に自動で開始", + "launch.totray": "起動時にトレイに最小化", "websearch": { "blacklist": "ブラックリスト", "blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b1b32680..46286ff5 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1095,7 +1095,12 @@ "topic.position.left": "Слева", "topic.position.right": "Справа", "topic.show.time": "Показывать время топика", - "tray.title": "Включить значок системного трея", + "tray.title": "Трей", + "tray.show": "Показать значок в трее", + "tray.onclose": "Свернуть в трей при закрытии", + "launch.title": "Запуск", + "launch.onboot": "Автозапуск при включении", + "launch.totray": "Свернуть в трей при запуске", "websearch": { "blacklist": "Черный список", "blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index b08379f0..af39e01e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1095,7 +1095,12 @@ "topic.position.left": "左侧", "topic.position.right": "右侧", "topic.show.time": "显示话题时间", - "tray.title": "启用系统托盘图标", + "tray.title": "托盘", + "tray.show": "显示托盘图标", + "tray.onclose": "关闭时最小化到托盘", + "launch.title": "启动", + "launch.onboot": "开机自动启动", + "launch.totray": "启动时最小化到托盘", "websearch": { "blacklist": "黑名单", "blacklist_description": "在搜索结果中不会出现以下网站的结果", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 12d92d30..937b1d52 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1095,7 +1095,12 @@ "topic.position.left": "左側", "topic.position.right": "右側", "topic.show.time": "顯示話題時間", - "tray.title": "啟用系統工具列圖示", + "tray.title": "系统匣", + "tray.show": "顯示系统匣圖示", + "tray.onclose": "關閉時最小化到系统匣", + "launch.title": "啟動", + "launch.onboot": "開機自動啟動", + "launch.totray": "啟動時最小化到系统匣", "websearch": { "blacklist": "黑名單", "blacklist_description": "以下網站不會出現在搜尋結果中", diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index e73e3382..eed439d8 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -13,13 +13,47 @@ import { useTranslation } from 'react-i18next' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.' const GeneralSettings: FC = () => { - const { language, proxyUrl: storeProxyUrl, theme, setTray, tray, proxyMode: storeProxyMode } = useSettings() + const { + language, + proxyUrl: storeProxyUrl, + theme, + setLaunch, + setTray, + launchOnBoot, + launchToTray, + trayOnClose, + tray, + proxyMode: storeProxyMode + } = useSettings() const [proxyUrl, setProxyUrl] = useState(storeProxyUrl) const { theme: themeMode } = useTheme() - const updateTray = (value: boolean) => { - setTray(value) - window.api.setTray(value) + const updateTray = (isShowTray: boolean) => { + setTray(isShowTray) + //only set tray on close/launch to tray when tray is enabled + if (!isShowTray) { + updateTrayOnClose(false) + updateLaunchToTray(false) + } + } + + const updateTrayOnClose = (isTrayOnClose: boolean) => { + setTray(undefined, isTrayOnClose) + //in case tray is not enabled, enable it + if (isTrayOnClose && !tray) { + updateTray(true) + } + } + + const updateLaunchOnBoot = (isLaunchOnBoot: boolean) => { + setLaunch(isLaunchOnBoot) + } + + const updateLaunchToTray = (isLaunchToTray: boolean) => { + setLaunch(undefined, isLaunchToTray) + if (isLaunchToTray && !tray) { + updateTray(true) + } } const dispatch = useAppDispatch() @@ -52,8 +86,10 @@ const GeneralSettings: FC = () => { dispatch(setProxyMode(mode)) if (mode === 'system') { window.api.setProxy('system') + dispatch(_setProxyUrl(undefined)) } else if (mode === 'none') { window.api.setProxy(undefined) + dispatch(_setProxyUrl(undefined)) } } @@ -111,11 +147,32 @@ const GeneralSettings: FC = () => { )} + + + {t('settings.launch.title')} - {t('settings.tray.title')} + {t('settings.launch.onboot')} + updateLaunchOnBoot(checked)} /> + + + + {t('settings.launch.totray')} + updateLaunchToTray(checked)} /> + + + + {t('settings.tray.title')} + + + {t('settings.tray.show')} updateTray(checked)} /> + + + {t('settings.tray.onclose')} + updateTrayOnClose(checked)} disabled={!tray} /> + ) diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index f0330d58..a781101a 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -791,6 +791,9 @@ const migrateConfig = { }, '83': (state: RootState) => { state.settings.messageNavigation = 'buttons' + state.settings.launchOnBoot = false + state.settings.launchToTray = false + state.settings.trayOnClose = true return state } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index b532c8e6..85999c54 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -28,6 +28,9 @@ export interface SettingsState { showMessageDivider: boolean messageFont: 'system' | 'serif' showInputEstimatedTokens: boolean + launchOnBoot: boolean + launchToTray: boolean + trayOnClose: boolean tray: boolean theme: ThemeMode windowStyle: 'transparent' | 'opaque' @@ -103,6 +106,9 @@ const initialState: SettingsState = { showMessageDivider: true, messageFont: 'system', showInputEstimatedTokens: false, + launchOnBoot: false, + launchToTray: false, + trayOnClose: true, tray: true, theme: ThemeMode.auto, windowStyle: 'transparent', @@ -205,9 +211,18 @@ const settingsSlice = createSlice({ setShowInputEstimatedTokens: (state, action: PayloadAction) => { state.showInputEstimatedTokens = action.payload }, + setLaunchOnBoot: (state, action: PayloadAction) => { + state.launchOnBoot = action.payload + }, + setLaunchToTray: (state, action: PayloadAction) => { + state.launchToTray = action.payload + }, setTray: (state, action: PayloadAction) => { state.tray = action.payload }, + setTrayOnClose: (state, action: PayloadAction) => { + state.trayOnClose = action.payload + }, setTheme: (state, action: PayloadAction) => { state.theme = action.payload }, @@ -386,6 +401,9 @@ export const { setShowMessageDivider, setMessageFont, setShowInputEstimatedTokens, + setLaunchOnBoot, + setLaunchToTray, + setTrayOnClose, setTray, setTheme, setFontSize,