mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-30 23:26:52 +00:00
Merge pull request #1022 from J1amo/codex/macos-close-fallback
fix: allow macOS close fallback when tray is unavailable
This commit is contained in:
@@ -440,6 +440,7 @@ const pruneChatHistoryPayloadStore = (): void => {
|
||||
}
|
||||
|
||||
type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
|
||||
type CloseRestoreMethod = 'tray' | 'dock'
|
||||
|
||||
// 更新下载状态管理(Issue #294 修复)
|
||||
let isDownloadInProgress = false
|
||||
@@ -813,29 +814,47 @@ const isSilentStartupEnabled = (): boolean => {
|
||||
return configService?.get('silentStartup') === true
|
||||
}
|
||||
|
||||
const getCloseRestoreMethod = (): CloseRestoreMethod | null => {
|
||||
if (tray) return 'tray'
|
||||
if (process.platform === 'darwin') return 'dock'
|
||||
return null
|
||||
}
|
||||
|
||||
const canKeepMainWindowInBackground = (): boolean => {
|
||||
return getCloseRestoreMethod() !== null
|
||||
}
|
||||
|
||||
const getPlatformIconName = (): string => {
|
||||
if (process.platform === 'linux') return 'icon.png'
|
||||
if (process.platform === 'darwin') return 'icon.icns'
|
||||
return 'icon.ico'
|
||||
}
|
||||
|
||||
const resolveAppIconPath = (): string => {
|
||||
const iconName = getPlatformIconName()
|
||||
if (!process.env.VITE_DEV_SERVER_URL) {
|
||||
return join(process.resourcesPath, iconName)
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
return join(__dirname, '../resources/icons/macos/icon.icns')
|
||||
}
|
||||
return join(__dirname, `../public/${iconName}`)
|
||||
}
|
||||
|
||||
const requestMainWindowCloseConfirmation = (win: BrowserWindow): void => {
|
||||
if (isClosePromptVisible) return
|
||||
isClosePromptVisible = true
|
||||
const restoreMethod = getCloseRestoreMethod()
|
||||
win.webContents.send('window:confirmCloseRequested', {
|
||||
canMinimizeToTray: Boolean(tray)
|
||||
canMinimizeToTray: restoreMethod !== null,
|
||||
restoreMethod: restoreMethod ?? undefined
|
||||
})
|
||||
}
|
||||
|
||||
function createWindow(options: { autoShow?: boolean } = {}) {
|
||||
// 获取图标路径 - 打包后在 resources 目录
|
||||
const { autoShow = true } = options
|
||||
let iconName = 'icon.ico';
|
||||
if (process.platform === 'linux') {
|
||||
iconName = 'icon.png';
|
||||
} else if (process.platform === 'darwin') {
|
||||
iconName = 'icon.icns';
|
||||
}
|
||||
|
||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
||||
|
||||
const iconPath = isDev
|
||||
? join(__dirname, `../public/${iconName}`)
|
||||
: join(process.resourcesPath, iconName);
|
||||
const iconPath = resolveAppIconPath()
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 1400,
|
||||
@@ -908,7 +927,7 @@ function createWindow(options: { autoShow?: boolean } = {}) {
|
||||
return
|
||||
}
|
||||
|
||||
if (closeBehavior === 'tray' && tray) {
|
||||
if (closeBehavior === 'tray' && canKeepMainWindowInBackground()) {
|
||||
win.hide()
|
||||
return
|
||||
}
|
||||
@@ -2184,7 +2203,7 @@ function registerIpcHandlers() {
|
||||
|
||||
try {
|
||||
if (action === 'tray') {
|
||||
if (tray) {
|
||||
if (canKeepMainWindowInBackground()) {
|
||||
mainWindow.hide()
|
||||
return true
|
||||
}
|
||||
@@ -4330,18 +4349,7 @@ app.whenReady().then(async () => {
|
||||
ensureWeChatRequestHeaderInterceptor()
|
||||
mainWindow = createWindow({ autoShow: false })
|
||||
|
||||
let iconName = 'icon.ico';
|
||||
if (process.platform === 'linux') {
|
||||
iconName = 'icon.png';
|
||||
} else if (process.platform === 'darwin') {
|
||||
iconName = 'icon.icns';
|
||||
}
|
||||
|
||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
||||
|
||||
const resolvedTrayIcon = isDev
|
||||
? join(__dirname, `../public/${iconName}`)
|
||||
: join(process.resourcesPath, iconName);
|
||||
const resolvedTrayIcon = resolveAppIconPath()
|
||||
|
||||
|
||||
try {
|
||||
@@ -4419,6 +4427,14 @@ app.whenReady().then(async () => {
|
||||
await httpService.autoStart()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show()
|
||||
}
|
||||
mainWindow.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
mainWindow = createWindow()
|
||||
}
|
||||
@@ -4464,4 +4480,3 @@ app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
type CloseConfirmPayload = {
|
||||
canMinimizeToTray: boolean
|
||||
restoreMethod?: 'tray' | 'dock'
|
||||
}
|
||||
|
||||
// 暴露给渲染进程的 API
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 配置
|
||||
@@ -106,8 +111,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
return () => ipcRenderer.removeListener('window:maximizeStateChanged', listener)
|
||||
},
|
||||
close: () => ipcRenderer.send('window:close'),
|
||||
onCloseConfirmRequested: (callback: (payload: { canMinimizeToTray: boolean }) => void) => {
|
||||
const listener = (_: unknown, payload: { canMinimizeToTray: boolean }) => callback(payload)
|
||||
onCloseConfirmRequested: (callback: (payload: CloseConfirmPayload) => void) => {
|
||||
const listener = (_: unknown, payload: CloseConfirmPayload) => callback(payload)
|
||||
ipcRenderer.on('window:confirmCloseRequested', listener)
|
||||
return () => ipcRenderer.removeListener('window:confirmCloseRequested', listener)
|
||||
},
|
||||
|
||||
@@ -94,6 +94,7 @@ function App() {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||
const [showCloseDialog, setShowCloseDialog] = useState(false)
|
||||
const [canMinimizeToTray, setCanMinimizeToTray] = useState(false)
|
||||
const [closeRestoreMethod, setCloseRestoreMethod] = useState<'tray' | 'dock'>('tray')
|
||||
|
||||
// 锁定状态
|
||||
// const [isLocked, setIsLocked] = useState(false) // Moved to store
|
||||
@@ -120,6 +121,7 @@ function App() {
|
||||
useEffect(() => {
|
||||
const removeCloseConfirmListener = window.electronAPI.window.onCloseConfirmRequested((payload) => {
|
||||
setCanMinimizeToTray(Boolean(payload.canMinimizeToTray))
|
||||
setCloseRestoreMethod(payload.restoreMethod === 'dock' ? 'dock' : 'tray')
|
||||
setShowCloseDialog(true)
|
||||
})
|
||||
|
||||
@@ -685,6 +687,7 @@ function App() {
|
||||
<WindowCloseDialog
|
||||
open={showCloseDialog}
|
||||
canMinimizeToTray={canMinimizeToTray}
|
||||
restoreMethod={closeRestoreMethod}
|
||||
onSelect={(action, rememberChoice) => handleWindowCloseAction(action, rememberChoice)}
|
||||
onCancel={() => handleWindowCloseAction('cancel')}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import './WindowCloseDialog.scss'
|
||||
interface WindowCloseDialogProps {
|
||||
open: boolean
|
||||
canMinimizeToTray: boolean
|
||||
restoreMethod?: 'tray' | 'dock'
|
||||
onSelect: (action: 'tray' | 'quit', rememberChoice: boolean) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
@@ -12,10 +13,12 @@ interface WindowCloseDialogProps {
|
||||
export default function WindowCloseDialog({
|
||||
open,
|
||||
canMinimizeToTray,
|
||||
restoreMethod = 'tray',
|
||||
onSelect,
|
||||
onCancel
|
||||
}: WindowCloseDialogProps) {
|
||||
const [rememberChoice, setRememberChoice] = useState(false)
|
||||
const isDockRestore = restoreMethod === 'dock'
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
@@ -57,7 +60,9 @@ export default function WindowCloseDialog({
|
||||
<h2 id="window-close-dialog-title">关闭 WeFlow</h2>
|
||||
<p>
|
||||
{canMinimizeToTray
|
||||
? '你可以保留后台进程与本地 API,或者直接完全退出应用。'
|
||||
? isDockRestore
|
||||
? '你可以隐藏主窗口并保留后台进程与本地 API,稍后可从 Dock 或重新打开应用恢复。'
|
||||
: '你可以保留后台进程与本地 API,或者直接完全退出应用。'
|
||||
: '当前系统托盘不可用,本次只能完全退出应用。'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -73,8 +78,12 @@ export default function WindowCloseDialog({
|
||||
<Minimize2 size={18} />
|
||||
</span>
|
||||
<span className="window-close-dialog-option-text">
|
||||
<strong>最小化到系统托盘</strong>
|
||||
<span>继续保留后台进程和本地 API,稍后可从托盘恢复。</span>
|
||||
<strong>{isDockRestore ? '隐藏主窗口' : '最小化到系统托盘'}</strong>
|
||||
<span>
|
||||
{isDockRestore
|
||||
? '继续保留后台进程和本地 API,稍后可从 Dock 或重新打开应用恢复。'
|
||||
: '继续保留后台进程和本地 API,稍后可从托盘恢复。'}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
7
src/types/electron.d.ts
vendored
7
src/types/electron.d.ts
vendored
@@ -308,6 +308,11 @@ export interface BackupManifest {
|
||||
}
|
||||
}
|
||||
|
||||
export type CloseConfirmPayload = {
|
||||
canMinimizeToTray: boolean
|
||||
restoreMethod?: 'tray' | 'dock'
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
window: {
|
||||
minimize: () => void
|
||||
@@ -315,7 +320,7 @@ export interface ElectronAPI {
|
||||
isMaximized: () => Promise<boolean>
|
||||
onMaximizeStateChanged: (callback: (isMaximized: boolean) => void) => () => void
|
||||
close: () => void
|
||||
onCloseConfirmRequested: (callback: (payload: { canMinimizeToTray: boolean }) => void) => () => void
|
||||
onCloseConfirmRequested: (callback: (payload: CloseConfirmPayload) => void) => () => void
|
||||
respondCloseConfirm: (action: 'tray' | 'quit' | 'cancel') => Promise<boolean>
|
||||
openAgreementWindow: () => Promise<boolean>
|
||||
completeOnboarding: () => Promise<boolean>
|
||||
|
||||
Reference in New Issue
Block a user