Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion resources/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,10 @@ window.ztools = {
// 显示文件保存对话框
showSaveDialog: (options) => electron.ipcRenderer.sendSync('show-save-dialog', options),
// 显示文件打开对话框
showOpenDialog: (options) => electron.ipcRenderer.sendSync('show-open-dialog', options),
showOpenDialog: (options) => {
const data = electron.ipcRenderer.sendSync('show-open-dialog', options);
return data;
},
// 屏幕截图
screenCapture: async (callback) => {
const { image, bounds } = await electron.ipcRenderer.invoke('screen-capture')
Expand Down
26 changes: 18 additions & 8 deletions src/main/api/plugin/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,15 @@ export class PluginDialogAPI {
event.returnValue = undefined
return
}
const result = windowManager.withBlurHideSuppressedSync(() =>
dialog.showSaveDialogSync(targetWindow, options)
)
event.returnValue = result
windowManager
.withBlurHideSuppressed(() => dialog.showSaveDialog(targetWindow, options))
.then((data: Electron.SaveDialogReturnValue) => {
event.returnValue = data.canceled ? undefined : data.filePath
})
.catch((error: Error) => {
console.error('[PluginDialog] 显示文件保存对话框失败:', error)
event.returnValue = undefined
})
Comment on lines +79 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

PluginDialogAPI 中,show-save-dialog 是通过同步 IPC(渲染进程使用 sendSync)调用的。主进程必须同步地为 event.returnValue 赋值。

在当前修改中,您使用了异步的 withBlurHideSuppresseddialog.showSaveDialog,并在 .then() 回调中异步设置 event.returnValue。这会导致主进程在 Promise 解析之前就结束了同步事件处理,渲染进程会立即收到 undefined 返回值,而不会等待用户在对话框中选择文件,从而导致保存文件对话框功能完全失效。

为了保持同步 API 的正确行为,必须恢复使用同步的 withBlurHideSuppressedSyncdialog.showSaveDialogSync

        const result = windowManager.withBlurHideSuppressedSync(() =>
          dialog.showSaveDialogSync(targetWindow, options)
        )
        event.returnValue = result

} catch (error) {
console.error('[PluginDialog] 显示文件保存对话框失败:', error)
event.returnValue = undefined
Expand All @@ -97,10 +102,15 @@ export class PluginDialogAPI {
event.returnValue = []
return
}
const result = windowManager.withBlurHideSuppressedSync(() =>
dialog.showOpenDialogSync(targetWindow, options)
)
event.returnValue = result || []
windowManager
.withBlurHideSuppressed(() => dialog.showOpenDialog(targetWindow, options))
.then((data: Electron.OpenDialogReturnValue) => {
event.returnValue = data.canceled ? [] : data.filePaths
})
.catch((error: Error) => {
console.error('[PluginDialog] 显示文件打开对话框失败:', error)
event.returnValue = []
})
Comment on lines +105 to +113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

show-save-dialog 类似,show-open-dialog 也是通过同步 IPC(渲染进程使用 sendSync)调用的。

使用异步的 withBlurHideSuppresseddialog.showOpenDialog 会导致渲染进程在对话框弹出时立即收到空数组 [] 返回值,而不会等待用户选择文件。

必须恢复使用同步的 withBlurHideSuppressedSyncdialog.showOpenDialogSync 以确保同步返回选中的文件路径。

        const result = windowManager.withBlurHideSuppressedSync(() =>
          dialog.showOpenDialogSync(targetWindow, options)
        )
        event.returnValue = result || []

} catch (error) {
console.error('[PluginDialog] 显示文件打开对话框失败:', error)
event.returnValue = []
Expand Down
2 changes: 1 addition & 1 deletion src/main/api/plugin/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class PluginShellAPI {
'FESearchHost.exe',
'prevhost.exe'
]
if (!EXPLORER_APPS.includes(windowInfo.app)) {
if (!windowInfo.app || !EXPLORER_APPS.includes(windowInfo.app)) {
console.log(
`[PluginShell] readCurrentFolderPath: 当前窗口非 Explorer (app=${windowInfo.app})`
)
Expand Down
10 changes: 6 additions & 4 deletions src/main/api/renderer/systemCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,23 +736,25 @@ function handleAddToWakeupBlacklist(ctx: SystemCommandContext): any {
const blacklist: Array<{ app: string; bundleId?: string; label?: string }> =
settings.wakeupBlacklist ?? []

const appName = winInfo.app

Comment on lines +739 to +740

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

由于 WindowActivationInfopreviousActiveWindow 中的 app 属性现在被改为了可选属性(app?: string),winInfo.app 有可能为 undefined

如果 appNameundefined,直接调用 appName.toLowerCase() 将会抛出 TypeError: Cannot read properties of undefined (reading 'toLowerCase') 运行时崩溃。

建议在获取 appName 后进行非空校验,如果为空则安全返回。

Suggested change
const appName = winInfo.app
const appName = winInfo.app
if (!appName) {
return { success: false, error: '无法获取应用名称' }
}

// 去重:macOS 按 bundleId,Windows 按 app 名称
const isDuplicate =
process.platform === 'darwin' && winInfo.bundleId
? blacklist.some((item) => item.bundleId === winInfo.bundleId)
: blacklist.some((item) => item.app.toLowerCase() === winInfo.app.toLowerCase())
: blacklist.some((item) => item.app.toLowerCase() === appName.toLowerCase())

if (isDuplicate) {
ctx.mainWindow?.hide()
if (Notification.isSupported()) {
new Notification({ title: 'ZTools', body: `${winInfo.app} 已在唤醒黑名单中` }).show()
new Notification({ title: 'ZTools', body: `${appName} 已在唤醒黑名单中` }).show()
}
return { success: false, error: '该应用已在唤醒黑名单中' }
}

const label = winInfo.app.replace(/\.(exe|app)$/i, '')
const label = appName.replace(/\.(exe|app)$/i, '')
blacklist.push({
app: winInfo.app,
app: appName,
bundleId: winInfo.bundleId,
label
})
Expand Down
96 changes: 96 additions & 0 deletions src/main/core/native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ interface UwpAppInfo {
installLocation: string
}

export interface FileLocationWindowInfo {
platform?: 'win32' | 'darwin'
kind?: 'windows-explorer' | 'windows-file-dialog' | 'mac-finder' | 'mac-file-dialog'
preciseTarget?: boolean
hwnd?: number
windowId?: number
finderId?: number
pid?: number
bundleId?: string
app?: string
title?: string
className?: string
axRole?: string
axSubrole?: string
path?: string
url?: string
}

interface NativeAddon {
startMonitor: (callback: () => void) => void
stopMonitor: () => void
Expand Down Expand Up @@ -60,6 +78,12 @@ interface NativeAddon {
unicodeType: (segment: string) => boolean
/** Windows: 通过 COM IShellWindows 查询指定窗口句柄对应的 Explorer 文件夹路径 */
getExplorerFolderPath: (hwnd: number) => string | null
/** Windows/macOS: 获取文件管理器窗口 */
getAllExplorerWindows: () => Array<FileLocationWindowInfo | string>
/** Windows/macOS: 设置文件管理器或文件选择对话框地址栏路径 */
setAddressBar: (identifier: number | string | FileLocationWindowInfo, address: string) => boolean
/** Windows: 判断窗口是否是可安全修改地址栏的文件定位窗口 */
isFileLocationWindow?: (hwnd: number) => boolean
/** Windows: 读取指定浏览器窗口的当前 URL,结果通过 callback 返回 */
readBrowserWindowUrl: (
browserName: string,
Expand All @@ -82,6 +106,9 @@ interface NativeAddon {

interface WindowInfo {
app: string // 应用名称(如 "Finder.app")
platform?: 'win32' | 'darwin'
kind?: FileLocationWindowInfo['kind']
preciseTarget?: boolean
bundleId?: string // macOS 独有
pid?: number // 进程ID (macOS 和 Windows 都有)
title?: string // 窗口标题
Expand All @@ -92,6 +119,12 @@ interface WindowInfo {
appPath?: string // 应用路径
className?: string // Windows 窗口类名(用于区分 CabinetWClass/Progman/WorkerW 等)
hwnd?: number // Windows 窗口句柄(用于 COM 查询 Explorer 路径)
windowId?: number
finderId?: number
axRole?: string
axSubrole?: string
path?: string
url?: string
}

interface ActiveWindowResult {
Expand Down Expand Up @@ -435,6 +468,69 @@ export class WindowManager {
return (addon as NativeAddon).getExplorerFolderPath(hwnd)
}

/**
* Windows/macOS: 获取所有文件管理器窗口
* @returns 文件管理器窗口列表
*/
static getAllExplorerWindows(): Array<FileLocationWindowInfo | string> {
if (platform !== 'win32' && platform !== 'darwin') {
throw new Error('getAllExplorerWindows is only available on Windows and macOS')
}
return (addon as NativeAddon).getAllExplorerWindows()
}

static isFileLocationWindow(hwnd: number): boolean {
if (platform !== 'win32') {
throw new Error('isFileLocationWindow is only available on Windows')
}
if (typeof hwnd !== 'number' || !Number.isFinite(hwnd) || hwnd <= 0) {
throw new TypeError('hwnd must be a positive number')
}
return Boolean((addon as NativeAddon).isFileLocationWindow?.(hwnd))
}

/**
* Windows/macOS: 设置文件管理器或文件选择对话框地址栏路径
* @param target 目标窗口标识或窗口信息
* @param address 要跳转的路径或地址
* @returns 是否设置成功
*/
static setAddressBar(
target: number | string | FileLocationWindowInfo,
address: string
): boolean {
if (platform !== 'win32' && platform !== 'darwin') {
throw new Error('setAddressBar is only available on Windows and macOS')
}
if (typeof address !== 'string' || address.trim() === '') {
throw new TypeError('address must be a non-empty string')
}

const identifier =
typeof target === 'object' && target !== null
? platform === 'darwin'
? target
: target.hwnd
: target

if (platform === 'win32') {
if (typeof identifier !== 'number' || !Number.isFinite(identifier) || identifier <= 0) {
throw new TypeError('target must include a valid hwnd')
}
} else if (typeof identifier === 'object' && identifier !== null) {
if (!identifier.preciseTarget) {
throw new TypeError('target must include precise macOS window identity')
}
} else if (
(typeof identifier !== 'number' || !Number.isFinite(identifier) || identifier <= 0) &&
(typeof identifier !== 'string' || identifier.trim() === '')
) {
throw new TypeError('target must include a valid bundleId or pid')
}

return Boolean((addon as NativeAddon).setAddressBar(identifier, address))
}

/**
* Windows: 读取指定浏览器窗口的当前 URL
* @param browserName 浏览器标识(如 chrome/msedge/firefox)
Expand Down
Loading