156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import { BrowserWindow, ipcMain, screen } from "electron";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { app } from "electron";
|
|
|
|
let overlayWindow: BrowserWindow | null = null;
|
|
|
|
function writeOverlayHtml(): string {
|
|
const dir = path.join(app.getPath("temp"), "rpa-studio-overlay");
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const file = path.join(dir, "overlay.html");
|
|
const html = `<!DOCTYPE html>
|
|
<html><head><style>
|
|
*{margin:0;padding:0;box-sizing:border-box;user-select:none;-webkit-user-select:none}
|
|
body{width:320px;height:56px;background:#1E2530;color:#E8ECF2;font-family:'Segoe UI',sans-serif;display:flex;align-items:center;padding:0 12px;gap:10px;overflow:hidden;border-radius:8px;border:1px solid #3A4556}
|
|
.spinner{width:14px;height:14px;border:2px solid #4A90D9;border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite;flex-shrink:0}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
.info{flex:1;min-width:0}
|
|
.step{font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#B8C4D4}
|
|
.progress-bar{height:3px;background:#3A4556;border-radius:2px;margin-top:4px;overflow:hidden}
|
|
.progress-fill{height:100%;background:#4A90D9;border-radius:2px;transition:width .3s ease}
|
|
.meta{font-size:10px;color:#7A8898;flex-shrink:0;white-space:nowrap}
|
|
.stop-btn{background:#3A2028;color:#E05252;border:1px solid #5A3038;border-radius:4px;padding:4px 8px;font-size:10px;cursor:pointer;flex-shrink:0}
|
|
.stop-btn:hover{background:#4A2830}
|
|
</style></head><body>
|
|
<div class="spinner" id="spin"></div>
|
|
<div class="info">
|
|
<div class="step" id="step">准备运行…</div>
|
|
<div class="progress-bar"><div class="progress-fill" id="fill" style="width:0%"></div></div>
|
|
</div>
|
|
<div class="meta" id="meta">0/0</div>
|
|
<button class="stop-btn" id="stopBtn" onclick="stopRun()">停止</button>
|
|
<script>
|
|
const { ipcRenderer } = require("electron");
|
|
let done = 0, total = 0, startTime = Date.now();
|
|
let timer = setInterval(function() {
|
|
var s = Math.floor((Date.now() - startTime) / 1000);
|
|
document.getElementById("meta").textContent = done + "/" + total + " · " + s + "s";
|
|
}, 1000);
|
|
|
|
ipcRenderer.on("run:event", function(_e, ev) {
|
|
if (ev.type === "node.started" && ev.nodeLabel) {
|
|
document.getElementById("step").textContent = ev.nodeLabel;
|
|
}
|
|
if (ev.type === "run.started") { startTime = Date.now(); }
|
|
if (ev.type === "run.total" && ev.total) { total = ev.total; }
|
|
if (ev.type === "node.finished") { done++; updateBar(); }
|
|
if (ev.type === "run.finished") {
|
|
clearInterval(timer);
|
|
document.getElementById("spin").style.display = "none";
|
|
var fill = document.getElementById("fill");
|
|
fill.style.background = ev.status === "succeeded" ? "#4CAF50" : "#E05252";
|
|
fill.style.width = "100%";
|
|
document.getElementById("step").textContent =
|
|
ev.status === "succeeded" ? "✓ 运行成功" :
|
|
ev.status === "failed" ? "✗ 运行失败" : "已取消";
|
|
document.getElementById("stopBtn").textContent = "关闭";
|
|
document.getElementById("stopBtn").onclick = function() { ipcRenderer.send("overlay:close"); };
|
|
setTimeout(function() { ipcRenderer.send("overlay:close"); }, ev.status === "succeeded" ? 3000 : 10000);
|
|
}
|
|
});
|
|
|
|
function updateBar() {
|
|
var pct = total > 0 ? Math.round(done / total * 100) : 0;
|
|
document.getElementById("fill").style.width = pct + "%";
|
|
document.getElementById("meta").textContent = done + "/" + total;
|
|
}
|
|
|
|
function stopRun() { ipcRenderer.send("overlay:stop"); }
|
|
</script>
|
|
</body></html>`;
|
|
fs.writeFileSync(file, html, "utf8");
|
|
return file;
|
|
}
|
|
|
|
export function showRunOverlay(mainWindow: BrowserWindow | null): void {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.minimize();
|
|
mainWindow.setProgressBar(0);
|
|
}
|
|
|
|
if (overlayWindow && !overlayWindow.isDestroyed()) {
|
|
overlayWindow.show();
|
|
return;
|
|
}
|
|
|
|
const w = 320;
|
|
const h = 56;
|
|
const workArea = screen.getPrimaryDisplay().workArea;
|
|
const x = workArea.x + workArea.width - w - 16;
|
|
const y = workArea.y + workArea.height - h - 16;
|
|
|
|
overlayWindow = new BrowserWindow({
|
|
width: w,
|
|
height: h,
|
|
x,
|
|
y,
|
|
frame: false,
|
|
resizable: false,
|
|
movable: false,
|
|
minimizable: false,
|
|
maximizable: false,
|
|
closable: false,
|
|
skipTaskbar: true,
|
|
alwaysOnTop: true,
|
|
fullscreenable: false,
|
|
hasShadow: true,
|
|
backgroundColor: "#1E2530",
|
|
webPreferences: {
|
|
contextIsolation: false,
|
|
nodeIntegration: true,
|
|
sandbox: false
|
|
}
|
|
});
|
|
overlayWindow.setAlwaysOnTop(true, "screen-saver");
|
|
overlayWindow.show();
|
|
|
|
const file = writeOverlayHtml();
|
|
overlayWindow.loadFile(file);
|
|
}
|
|
|
|
export function updateOverlayProgress(ratio: number): void {
|
|
if (overlayWindow && !overlayWindow.isDestroyed()) {
|
|
overlayWindow.setProgressBar(ratio >= 1 ? -1 : Math.max(0, Math.min(1, ratio)));
|
|
}
|
|
}
|
|
|
|
export function hideRunOverlay(mainWindow: BrowserWindow | null): void {
|
|
if (overlayWindow && !overlayWindow.isDestroyed()) {
|
|
overlayWindow.destroy();
|
|
overlayWindow = null;
|
|
}
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.setProgressBar(-1);
|
|
mainWindow.restore();
|
|
}
|
|
}
|
|
|
|
export function registerOverlayIpc(
|
|
getMainWindow: () => BrowserWindow | null,
|
|
onStop: () => void
|
|
): void {
|
|
ipcMain.on("overlay:close", () => {
|
|
hideRunOverlay(getMainWindow());
|
|
});
|
|
ipcMain.on("overlay:stop", () => {
|
|
onStop();
|
|
});
|
|
}
|
|
|
|
export function overlayBroadcast(event: unknown): void {
|
|
if (overlayWindow && !overlayWindow.isDestroyed()) {
|
|
overlayWindow.webContents.send("run:event", event);
|
|
}
|
|
}
|