mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-07 13:34:08 +08:00
485 lines
14 KiB
JavaScript
485 lines
14 KiB
JavaScript
const { ipcRenderer } = require("electron");
|
||
const os = require("os");
|
||
const { createBrowserWindow } = utools;
|
||
|
||
const dialogPath = "lib/dialog/view.html";
|
||
const preloadPath = "lib/dialog/controller.js";
|
||
|
||
const commonBrowserWindowOptions = {
|
||
resizable: false,
|
||
minimizable: false,
|
||
maximizable: false,
|
||
fullscreenable: false,
|
||
skipTaskbar: true,
|
||
alwaysOnTop: true,
|
||
frame: false,
|
||
movable: true,
|
||
webPreferences: {
|
||
preload: preloadPath,
|
||
devTools: utools.isDev(),
|
||
},
|
||
};
|
||
|
||
/**
|
||
* 创建对话框窗口
|
||
* @param {object} config - 对话框配置
|
||
* @param {object} [customDialogOptions] - 自定义窗口选项
|
||
* @returns {Promise} 返回对话框结果
|
||
*/
|
||
const createDialog = (config, customDialogOptions = {}) => {
|
||
return new Promise((resolve) => {
|
||
// linux 和 win32 都使用 win32 的样式
|
||
const platform = os.platform() === "darwin" ? "darwin" : "win32";
|
||
|
||
const dialogWidth =
|
||
config.type === "textarea" || config.type === "select"
|
||
? 500
|
||
: platform === "win32"
|
||
? 370
|
||
: 420;
|
||
|
||
const dialogOptions = {
|
||
title: config.title || "对话框",
|
||
width: dialogWidth,
|
||
height: 80,
|
||
opacity: 0,
|
||
...commonBrowserWindowOptions,
|
||
...customDialogOptions, // 合并自定义选项
|
||
};
|
||
|
||
// 创建窗口
|
||
const UBrowser = createBrowserWindow(dialogPath, dialogOptions, () => {
|
||
const windowResponseHandler = (event, result) => {
|
||
resolve(result);
|
||
// 移除监听器
|
||
UBrowser.destroy();
|
||
};
|
||
|
||
const windowResizeHandler = (event, height) => {
|
||
// 获取当前窗口位置
|
||
const bounds = UBrowser.getBounds();
|
||
// 调整y坐标,保持窗口中心点不变
|
||
const y = Math.round(bounds.y - (height - bounds.height) / 2);
|
||
// 确保坐标和尺寸都是有效的整数
|
||
const newBounds = {
|
||
x: Math.round(bounds.x),
|
||
y: Math.max(0, y), // 确保不会超出屏幕顶部
|
||
width: dialogWidth,
|
||
height: Math.round(height),
|
||
};
|
||
// 设置新的位置和大小
|
||
UBrowser.setBounds(newBounds);
|
||
UBrowser.setOpacity(1);
|
||
};
|
||
|
||
// 监听子窗口返回的计算高度, 等待按钮有自己的计算逻辑
|
||
config.type !== "wait-button" &&
|
||
ipcRenderer.once("window-resize", windowResizeHandler);
|
||
|
||
// 监听子窗口返回的返回值
|
||
ipcRenderer.once("window-response", windowResponseHandler);
|
||
|
||
// 发送配置到子窗口
|
||
ipcRenderer.sendTo(UBrowser.webContents.id, "window-config", {
|
||
...config,
|
||
isDark: utools.isDarkColors(),
|
||
platform,
|
||
});
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 显示一个系统级消息框
|
||
* @param {string} content - 消息内容
|
||
* @param {string} [title] - 标题,默认为空
|
||
* @returns {Promise<void>} Promise
|
||
*/
|
||
const showSystemMessageBox = async (content, title = "") => {
|
||
await createDialog({
|
||
type: "message",
|
||
title,
|
||
content,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 显示一个系统级输入框组对话框
|
||
* @param {string[]|{label:string,value:string,hint:string}[]} options - 输入框配置,可以是标签数组或者带属性的对象数组
|
||
* @param {string} [title] - 标题,默认为空
|
||
* @returns {Promise<string[]>} 输入的内容数组
|
||
*/
|
||
const showSystemInputBox = async (options, title = "") => {
|
||
// 确保 options 是数组
|
||
const optionsArray = Array.isArray(options) ? options : [options];
|
||
|
||
// 转换每个选项为正确的格式
|
||
const inputOptions = optionsArray.map((opt) => {
|
||
if (typeof opt === "string") {
|
||
return opt;
|
||
}
|
||
if (typeof opt === "object" && opt.label) {
|
||
return {
|
||
label: opt.label,
|
||
value: opt.value || "",
|
||
hint: opt.hint || "",
|
||
};
|
||
}
|
||
throw new Error("输入框配置格式错误");
|
||
});
|
||
|
||
return await createDialog({
|
||
type: "input",
|
||
title,
|
||
inputOptions,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 显示一个系统级确认框,返回是否点击了确认
|
||
* @param {string} content - 确认内容
|
||
* @param {string} [title] - 标题,默认为空
|
||
* @returns {Promise<boolean>} 是否确认
|
||
*/
|
||
const showSystemConfirmBox = async (content, title = "") => {
|
||
const result = await createDialog({
|
||
type: "confirm",
|
||
title,
|
||
content,
|
||
});
|
||
return !!result;
|
||
};
|
||
|
||
/**
|
||
* 显示一个系统级按钮组对话框,返回点击的按钮的索引和文本
|
||
* @param {string[]} buttons - 按钮文本数组
|
||
* @param {string} [title] - 标题,默认为空
|
||
* @returns {Promise<{id: number, text: string}>} 选择的按钮信息
|
||
*/
|
||
const showSystemButtonBox = async (buttons, title = "") => {
|
||
return await createDialog({
|
||
type: "buttons",
|
||
title,
|
||
buttons: Array.isArray(buttons) ? buttons : [buttons],
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 显示一个系统级多行文本输入框
|
||
* @param {string} [placeholder] - 输入框的提示文本
|
||
* @param {string} [defaultText] - 输入框的默认文本,默认为空
|
||
* @returns {Promise<string>} 编辑后的文本
|
||
*/
|
||
const showSystemTextArea = async (placeholder = "请输入", defaultText = "") => {
|
||
return await createDialog({
|
||
type: "textarea",
|
||
placeholder,
|
||
defaultText,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 显示一个系统级选择列表对话框
|
||
* @param {Array<string|{title: string, description?: string, icon?: string}>} items - 选项列表
|
||
* @param {object} [options] - 配置选项
|
||
* @param {string} [options.title] - 对话框标题
|
||
* @param {string} [options.placeholder] - 输入框占位符
|
||
* @param {boolean} [options.enableSearch] - 是否启用搜索
|
||
* @returns {Promise<{id: number, text: string, data: any}>} 选择的结果
|
||
*/
|
||
const showSystemSelectList = async (items, options = {}) => {
|
||
const {
|
||
title = "请选择",
|
||
placeholder = "",
|
||
enableSearch = true,
|
||
optionType = "plaintext",
|
||
} = options;
|
||
|
||
return await createDialog({
|
||
type: "select",
|
||
title,
|
||
placeholder,
|
||
enableSearch,
|
||
items,
|
||
optionType,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 计算窗口位置
|
||
* @param {object} options - 配置选项
|
||
* @param {string} [options.position="bottom-right"] - 窗口位置,可选值:top-left, top-right, bottom-left, bottom-right
|
||
* @param {number} options.width - 窗口宽度
|
||
* @param {number} options.height - 窗口高度
|
||
* @param {number} [options.padding=20] - 边距
|
||
* @returns {{x: number, y: number}} 窗口位置坐标
|
||
*/
|
||
const calculateWindowPosition = (options) => {
|
||
const { position = "bottom-right", width, height, padding = 20 } = options;
|
||
|
||
// 获取主屏幕尺寸
|
||
const primaryDisplay = utools.getPrimaryDisplay();
|
||
const { width: screenWidth, height: screenHeight } =
|
||
primaryDisplay.workAreaSize;
|
||
|
||
let x, y;
|
||
switch (position) {
|
||
case "top-left":
|
||
x = padding;
|
||
y = padding;
|
||
break;
|
||
case "top-right":
|
||
x = screenWidth - width - padding;
|
||
y = padding;
|
||
break;
|
||
case "bottom-left":
|
||
x = padding;
|
||
y = screenHeight - height - padding;
|
||
break;
|
||
case "bottom-right":
|
||
default:
|
||
x = screenWidth - width - padding;
|
||
y = screenHeight - height - padding;
|
||
break;
|
||
}
|
||
|
||
return { x, y };
|
||
};
|
||
|
||
/**
|
||
* 显示一个系统级等待按钮
|
||
* @param {object} options - 配置选项
|
||
* @param {string} [options.text="等待操作"] - 按钮文本
|
||
* @param {string} [options.position="bottom-right"] - 按钮位置,可选值:top-left, top-right, bottom-left, bottom-right
|
||
* @param {boolean} [options.showCancel=false] - 是否显示取消按钮
|
||
* @returns {Promise<boolean>} 点击确定返回true,点击取消返回false
|
||
*/
|
||
const showSystemWaitButton = async (options = {}) => {
|
||
const {
|
||
text = "等待操作",
|
||
position = "bottom-right",
|
||
showCancel = true,
|
||
} = options;
|
||
|
||
// 创建临时span计算文本宽度
|
||
const span = document.createElement("span");
|
||
span.style.font =
|
||
'14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||
span.style.visibility = "hidden";
|
||
span.style.position = "absolute";
|
||
span.textContent = text;
|
||
document.body.appendChild(span);
|
||
|
||
// 计算窗口尺寸
|
||
const textWidth = span.offsetWidth;
|
||
document.body.removeChild(span);
|
||
|
||
const width = Math.max(textWidth + 32 + (showCancel ? 25 : 0), 80);
|
||
const height = 36;
|
||
|
||
// 计算窗口位置
|
||
const { x, y } = calculateWindowPosition({ position, width, height });
|
||
|
||
return await createDialog(
|
||
{
|
||
type: "wait-button",
|
||
text,
|
||
showCancel,
|
||
},
|
||
{
|
||
width,
|
||
height,
|
||
x,
|
||
y,
|
||
opacity: 1,
|
||
}
|
||
);
|
||
};
|
||
|
||
let lastProcessBar = null;
|
||
|
||
/**
|
||
* 显示一个进度条对话框
|
||
* @param {object} options - 配置选项
|
||
* @param {string} [options.title="进度"] - 对话框标题
|
||
* @param {string} [options.text="处理中..."] - 进度条上方的文本
|
||
* @param {number} [options.value] - 初始进度值(0-100),不传则显示加载动画
|
||
* @param {string} [options.position="bottom-right"] - 进度条位置,可选值:top-left, top-right, bottom-left, bottom-right
|
||
* @param {Function} [options.onClose] - 关闭按钮点击时的回调函数
|
||
* @param {Function} [options.onPause] - 暂停按钮点击时的回调函数
|
||
* @param {Function} [options.onResume] - 恢复按钮点击时的回调函数
|
||
* @returns {Promise<{id: number, close: Function}>} 返回进度条窗口ID和关闭函数
|
||
* @throws {Error} 如果只配置了onPause或onResume中的一个会抛出错误
|
||
*/
|
||
const showProcessBar = async (options = {}) => {
|
||
const {
|
||
text = "处理中...",
|
||
value,
|
||
position = "bottom-right",
|
||
onClose,
|
||
onPause,
|
||
onResume,
|
||
} = options;
|
||
|
||
// 校验暂停/恢复回调必须同时配置
|
||
if ((onPause && !onResume) || (!onPause && onResume)) {
|
||
throw new Error("onPause 和 onResume 必须同时配置");
|
||
}
|
||
|
||
const windowWidth = 350;
|
||
const windowHeight = 60;
|
||
|
||
const { x, y } = calculateWindowPosition({
|
||
position,
|
||
width: windowWidth,
|
||
height: windowHeight,
|
||
});
|
||
|
||
return new Promise((resolve) => {
|
||
const UBrowser = createBrowserWindow(
|
||
dialogPath,
|
||
{
|
||
width: windowWidth,
|
||
height: windowHeight,
|
||
x,
|
||
y,
|
||
opacity: 0,
|
||
focusable: false,
|
||
...commonBrowserWindowOptions,
|
||
},
|
||
() => {
|
||
const windowId = UBrowser.webContents.id;
|
||
let windowResizeHandler;
|
||
let processPauseHandler;
|
||
|
||
// 创建事件处理器
|
||
windowResizeHandler = (event, height) => {
|
||
if (event.senderId !== windowId) return;
|
||
const bounds = UBrowser.getBounds();
|
||
const y = Math.round(bounds.y - (height - bounds.height));
|
||
const newBounds = {
|
||
x: Math.round(bounds.x),
|
||
y: Math.max(0, y),
|
||
width: windowWidth,
|
||
height: Math.round(height),
|
||
};
|
||
UBrowser.setBounds(newBounds);
|
||
UBrowser.setOpacity(1);
|
||
};
|
||
|
||
// 监听暂停/恢复事件
|
||
if (onPause && onResume) {
|
||
processPauseHandler = (event, isPaused) => {
|
||
if (event.senderId !== windowId) return;
|
||
if (isPaused) {
|
||
onPause();
|
||
} else {
|
||
onResume();
|
||
}
|
||
};
|
||
ipcRenderer.on("process-pause", processPauseHandler);
|
||
}
|
||
|
||
// 监听子窗口返回的计算高度
|
||
ipcRenderer.on("window-resize", windowResizeHandler);
|
||
|
||
// 监听对话框结果
|
||
ipcRenderer.once("window-response", (event, result) => {
|
||
if (event.senderId !== windowId) return;
|
||
if (result === "close" && typeof onClose === "function") {
|
||
onClose();
|
||
}
|
||
// 清理所有事件监听器
|
||
ipcRenderer.removeListener("window-resize", windowResizeHandler);
|
||
if (processPauseHandler) {
|
||
ipcRenderer.removeListener("process-pause", processPauseHandler);
|
||
}
|
||
lastProcessBar = null;
|
||
UBrowser.destroy();
|
||
});
|
||
|
||
// 发送配置到子窗口
|
||
ipcRenderer.sendTo(windowId, "window-config", {
|
||
type: "process",
|
||
text,
|
||
value: value === undefined ? 0 : value,
|
||
isDark: utools.isDarkColors(),
|
||
platform: process.platform,
|
||
showPause: Boolean(onPause && onResume),
|
||
isLoading: value === undefined,
|
||
});
|
||
|
||
const processBar = {
|
||
id: windowId,
|
||
close: () => {
|
||
if (typeof onClose === "function") {
|
||
onClose();
|
||
}
|
||
// 清理所有事件监听器
|
||
ipcRenderer.removeListener("window-resize", windowResizeHandler);
|
||
if (processPauseHandler) {
|
||
ipcRenderer.removeListener("process-pause", processPauseHandler);
|
||
}
|
||
lastProcessBar = null;
|
||
UBrowser.destroy();
|
||
},
|
||
};
|
||
|
||
lastProcessBar = processBar;
|
||
resolve(processBar);
|
||
}
|
||
);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 更新进度条的进度
|
||
* @param {object} options - 配置选项
|
||
* @param {number} [options.value] - 新的进度值(0-100),不传则显示加载动画
|
||
* @param {string} [options.text] - 新的进度文本
|
||
* @param {boolean} [options.complete] - 是否完成并关闭进度条
|
||
* @param {{id: number, close: Function}|undefined} processBar - 进度条对象, 如果不传入则使用上一次创建的进度条
|
||
* @throws {Error} 如果传入的processBar对象不是有效的进度条对象
|
||
*/
|
||
const updateProcessBar = (options = {}, processBar = null) => {
|
||
if (!processBar) {
|
||
if (!lastProcessBar) {
|
||
throw new Error("没有找到已创建的进度条");
|
||
}
|
||
processBar = lastProcessBar;
|
||
}
|
||
// 校验processBar对象
|
||
if (
|
||
typeof processBar !== "object" ||
|
||
typeof processBar.id !== "number" ||
|
||
typeof processBar.close !== "function"
|
||
) {
|
||
throw new Error("processBar对象格式错误");
|
||
}
|
||
|
||
const { value, text, complete } = options;
|
||
ipcRenderer.sendTo(processBar.id, "update-process", {
|
||
value,
|
||
text,
|
||
isLoading: value === undefined,
|
||
});
|
||
|
||
if (complete) {
|
||
setTimeout(() => {
|
||
processBar.close();
|
||
}, 600);
|
||
}
|
||
};
|
||
|
||
module.exports = {
|
||
showSystemMessageBox,
|
||
showSystemInputBox,
|
||
showSystemConfirmBox,
|
||
showSystemButtonBox,
|
||
showSystemTextArea,
|
||
showSystemSelectList,
|
||
showSystemWaitButton,
|
||
showProcessBar,
|
||
updateProcessBar,
|
||
};
|