592 lines
19 KiB
JavaScript
592 lines
19 KiB
JavaScript
import { app, screen, safeStorage, ipcMain, BrowserWindow, nativeTheme, dialog, nativeImage } from "electron";
|
|
import { join } from "path";
|
|
import { readFileSync, writeFile } from "fs";
|
|
import Store from "electron-store";
|
|
import Redis from "ioredis";
|
|
import __cjs_mod__ from "node:module";
|
|
const __filename = import.meta.filename;
|
|
const __dirname = import.meta.dirname;
|
|
const require2 = __cjs_mod__.createRequire(import.meta.url);
|
|
const STATE_FILE = join(app.getPath("userData"), "jrdm-win-state.json");
|
|
const MIN_WIDTH = 900;
|
|
const MIN_HEIGHT = 600;
|
|
const DEFAULT_WIDTH = 1200;
|
|
const DEFAULT_HEIGHT = 800;
|
|
function parseJson(str) {
|
|
try {
|
|
return JSON.parse(str);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
function getLastState() {
|
|
try {
|
|
const raw = readFileSync(STATE_FILE, "utf-8");
|
|
const state = parseJson(raw);
|
|
if (state) {
|
|
const display = screen.getPrimaryDisplay().workArea;
|
|
if (state.x != null && (state.x < 0 || state.x > display.width - 100)) {
|
|
state.x = null;
|
|
state.y = null;
|
|
}
|
|
if (state.y != null && (state.y < 0 || state.y > display.height - 100)) {
|
|
state.x = null;
|
|
state.y = null;
|
|
}
|
|
state.width = Math.max(state.width || DEFAULT_WIDTH, MIN_WIDTH);
|
|
state.height = Math.max(state.height || DEFAULT_HEIGHT, MIN_HEIGHT);
|
|
return state;
|
|
}
|
|
} catch {
|
|
}
|
|
return { x: null, y: null, width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, maximized: false };
|
|
}
|
|
function saveState(win) {
|
|
const bounds = win.getBounds();
|
|
const state = { ...bounds, maximized: win.isMaximized() };
|
|
writeFile(STATE_FILE, JSON.stringify(state), "utf-8", () => {
|
|
});
|
|
}
|
|
function restoreAndTrack(win) {
|
|
const lastState = getLastState();
|
|
if (lastState.x != null && lastState.y != null) {
|
|
win.setBounds({ x: lastState.x, y: lastState.y, width: lastState.width, height: lastState.height });
|
|
}
|
|
if (lastState.maximized) {
|
|
win.maximize();
|
|
}
|
|
win.on("close", () => saveState(win));
|
|
}
|
|
const store = new Store({
|
|
name: "jrdm-config",
|
|
defaults: {
|
|
connections: [],
|
|
settings: {
|
|
theme: "system",
|
|
locale: "en",
|
|
fontSize: 13,
|
|
scanCount: 500
|
|
}
|
|
}
|
|
});
|
|
function encrypt(text) {
|
|
if (!safeStorage.isEncryptionAvailable()) {
|
|
return Buffer.from(text).toString("base64");
|
|
}
|
|
return safeStorage.encryptString(text).toString("base64");
|
|
}
|
|
function decrypt(encoded) {
|
|
if (!safeStorage.isEncryptionAvailable()) {
|
|
return Buffer.from(encoded, "base64").toString("utf-8");
|
|
}
|
|
return safeStorage.decryptString(Buffer.from(encoded, "base64"));
|
|
}
|
|
const clients = /* @__PURE__ */ new Map();
|
|
function buildRedisOptions(opts) {
|
|
const options = {
|
|
host: opts.host,
|
|
port: opts.port,
|
|
username: opts.username || void 0,
|
|
password: opts.password || void 0,
|
|
stringNumbers: true,
|
|
enableReadyCheck: true,
|
|
family: 0,
|
|
connectTimeout: 1e4,
|
|
retryStrategy(times) {
|
|
if (times > 3) return null;
|
|
return Math.min(times * 200, 1e3);
|
|
},
|
|
maxRetriesPerRequest: 3
|
|
};
|
|
if (opts.tls) {
|
|
options.tls = {
|
|
rejectUnauthorized: false,
|
|
checkServerIdentity: () => void 0
|
|
};
|
|
}
|
|
return options;
|
|
}
|
|
function getClient(id) {
|
|
return clients.get(id);
|
|
}
|
|
function isClientConnected(id) {
|
|
const client = clients.get(id);
|
|
return client?.status === "ready";
|
|
}
|
|
async function connect(id, opts) {
|
|
if (clients.has(id)) {
|
|
await disconnect(id);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const redis = new Redis(buildRedisOptions(opts));
|
|
redis.once("ready", () => {
|
|
clients.set(id, redis);
|
|
resolve();
|
|
});
|
|
redis.once("error", (err) => {
|
|
redis.disconnect();
|
|
reject(err);
|
|
});
|
|
setTimeout(() => {
|
|
if (!clients.has(id)) {
|
|
redis.disconnect();
|
|
reject(new Error("连接超时,请检查主机和端口"));
|
|
}
|
|
}, 12e3);
|
|
});
|
|
}
|
|
async function disconnect(id) {
|
|
const client = clients.get(id);
|
|
if (client) {
|
|
client.disconnect();
|
|
clients.delete(id);
|
|
}
|
|
}
|
|
async function getKeyType(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.type(key);
|
|
}
|
|
async function getKeyTTL(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.ttl(key);
|
|
}
|
|
async function deleteKey(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
await client.del(key);
|
|
}
|
|
async function setKeyTTL(id, key, ttl) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
if (ttl < 0) {
|
|
await client.persist(key);
|
|
} else {
|
|
await client.expire(key, ttl);
|
|
}
|
|
}
|
|
async function renameKey(id, oldKey, newKey) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
await client.rename(oldKey, newKey);
|
|
}
|
|
async function existsKey(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return await client.exists(key) === 1;
|
|
}
|
|
async function batchDelete(id, keys) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
if (keys.length === 0) return 0;
|
|
const pipeline = client.pipeline();
|
|
for (const key of keys) {
|
|
pipeline.del(key);
|
|
}
|
|
const results = await pipeline.exec();
|
|
return results?.filter(([err]) => !err).length ?? 0;
|
|
}
|
|
async function memoryUsage(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.memory("USAGE", key);
|
|
}
|
|
async function scanKeys(id, cursor, pattern, count) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", count);
|
|
return { cursor: nextCursor, keys };
|
|
}
|
|
async function getStringValue(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.get(key);
|
|
}
|
|
async function setStringValue(id, key, value) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
await client.set(key, value);
|
|
}
|
|
async function getHashFields(id, key, cursor, count) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
const [nextCursor, raw] = await client.hscan(key, cursor, "COUNT", count);
|
|
const fields = [];
|
|
for (let i = 0; i < raw.length; i += 2) {
|
|
fields.push([raw[i], raw[i + 1]]);
|
|
}
|
|
return { cursor: nextCursor, fields };
|
|
}
|
|
async function setHashField(id, key, field, value) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
await client.hset(key, field, value);
|
|
}
|
|
async function deleteHashField(id, key, field) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
await client.hdel(key, field);
|
|
}
|
|
async function listRange(id, key, start, stop) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.lrange(key, start, stop);
|
|
}
|
|
async function listLength(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.llen(key);
|
|
}
|
|
async function listPush(id, key, ...values) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.rpush(key, ...values);
|
|
}
|
|
async function listSet(id, key, index, value) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
await client.lset(key, index, value);
|
|
}
|
|
async function listRemove(id, key, count, value) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.lrem(key, count, value);
|
|
}
|
|
async function setMembers(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.smembers(key);
|
|
}
|
|
async function setSize(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.scard(key);
|
|
}
|
|
async function setAdd(id, key, ...members) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.sadd(key, ...members);
|
|
}
|
|
async function setRemove(id, key, ...members) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.srem(key, ...members);
|
|
}
|
|
async function zsetRange(id, key, start, stop, withScores) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
if (withScores) return client.zrange(key, start, stop, "WITHSCORES");
|
|
return client.zrange(key, start, stop);
|
|
}
|
|
async function zsetSize(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.zcard(key);
|
|
}
|
|
async function zsetAdd(id, key, score, member) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.zadd(key, score, member);
|
|
}
|
|
async function zsetRemove(id, key, ...members) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.zrem(key, ...members);
|
|
}
|
|
async function streamInfo(id, key) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.xinfo("STREAM", key);
|
|
}
|
|
async function streamRange(id, key, start, end, count) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.xrange(key, start, end, "COUNT", count);
|
|
}
|
|
async function streamAdd(id, key, streamId, fieldValues) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.xadd(key, streamId, ...fieldValues);
|
|
}
|
|
async function streamTrim(id, key, maxLen) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.xtrim(key, "MAXLEN", maxLen);
|
|
}
|
|
async function streamDel(id, key, ...ids) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.xdel(key, ...ids);
|
|
}
|
|
async function execute(id, command, args) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.call(command, ...args);
|
|
}
|
|
async function getServerInfo(id) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.info();
|
|
}
|
|
async function selectDb(id, db) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
await client.select(db);
|
|
}
|
|
async function dbSize(id) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.dbsize();
|
|
}
|
|
async function ping(id) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.ping();
|
|
}
|
|
async function slowLogGet(id, count) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.slowlog("GET", count);
|
|
}
|
|
async function slowLogLen(id) {
|
|
const client = getClient(id);
|
|
if (!client) throw new Error(`Client ${id} not found`);
|
|
return client.slowlog("LEN");
|
|
}
|
|
function registerIpcHandlers() {
|
|
ipcMain.on("window:minimize", () => BrowserWindow.getFocusedWindow()?.minimize());
|
|
ipcMain.on("window:maximize", () => {
|
|
const win = BrowserWindow.getFocusedWindow();
|
|
if (win) win.isMaximized() ? win.unmaximize() : win.maximize();
|
|
});
|
|
ipcMain.on("window:close", () => BrowserWindow.getFocusedWindow()?.close());
|
|
ipcMain.handle("theme:get", () => nativeTheme.shouldUseDarkColors);
|
|
ipcMain.on("theme:set", (_e, theme) => {
|
|
nativeTheme.themeSource = theme;
|
|
});
|
|
ipcMain.handle("dialog:openFile", async (_e, options) => {
|
|
const win = BrowserWindow.getFocusedWindow();
|
|
if (!win) return null;
|
|
const result = await dialog.showOpenDialog(win, options);
|
|
if (result.canceled || result.filePaths.length === 0) return null;
|
|
const fs = await import("fs");
|
|
try {
|
|
return { path: result.filePaths[0], content: fs.readFileSync(result.filePaths[0], "utf-8") };
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
ipcMain.handle("credential:encrypt", (_e, text) => encrypt(text));
|
|
ipcMain.handle("credential:decrypt", (_e, encoded) => decrypt(encoded));
|
|
ipcMain.handle("storage:getConnections", () => store.get("connections", []));
|
|
ipcMain.handle("storage:saveConnection", (_e, conn) => {
|
|
const connections = store.get("connections", []);
|
|
const idx = connections.findIndex((c) => c.id === conn.id);
|
|
if (idx >= 0) connections[idx] = conn;
|
|
else connections.push(conn);
|
|
store.set("connections", connections);
|
|
return connections;
|
|
});
|
|
ipcMain.handle("storage:deleteConnection", (_e, id) => {
|
|
const connections = store.get("connections", []).filter((c) => c.id !== id);
|
|
store.set("connections", connections);
|
|
return connections;
|
|
});
|
|
ipcMain.handle("storage:reorderConnections", (_e, ids) => {
|
|
const connections = store.get("connections", []);
|
|
const reordered = ids.map((id, i) => {
|
|
const conn = connections.find((c) => c.id === id);
|
|
conn.order = i;
|
|
return conn;
|
|
});
|
|
store.set("connections", reordered);
|
|
return reordered;
|
|
});
|
|
ipcMain.handle("storage:getSettings", () => store.get("settings"));
|
|
ipcMain.handle("storage:saveSettings", (_e, settings) => {
|
|
store.set("settings", settings);
|
|
});
|
|
ipcMain.handle("redis:connect", async (_e, id, opts) => {
|
|
try {
|
|
await connect(id, opts);
|
|
return { success: true };
|
|
} catch (err) {
|
|
return { success: false, error: err.message };
|
|
}
|
|
});
|
|
ipcMain.handle("redis:disconnect", async (_e, id) => {
|
|
await disconnect(id);
|
|
});
|
|
ipcMain.handle("redis:ping", async (_e, id) => {
|
|
return ping(id);
|
|
});
|
|
ipcMain.handle("redis:isConnected", (_e, id) => {
|
|
return isClientConnected(id);
|
|
});
|
|
ipcMain.handle("redis:execute", async (_e, id, command, args) => {
|
|
return execute(id, command, args);
|
|
});
|
|
ipcMain.handle("redis:getInfo", async (_e, id) => {
|
|
return getServerInfo(id);
|
|
});
|
|
ipcMain.handle("redis:selectDb", async (_e, id, db) => {
|
|
await selectDb(id, db);
|
|
});
|
|
ipcMain.handle("redis:dbSize", async (_e, id) => {
|
|
return dbSize(id);
|
|
});
|
|
ipcMain.handle("redis:slowLogGet", async (_e, id, count) => {
|
|
return slowLogGet(id, count);
|
|
});
|
|
ipcMain.handle("redis:slowLogLen", async (_e, id) => {
|
|
return slowLogLen(id);
|
|
});
|
|
ipcMain.handle("redis:scanKeys", async (_e, id, cursor, pattern, count) => {
|
|
return scanKeys(id, cursor, pattern, count);
|
|
});
|
|
ipcMain.handle("redis:getKeyType", async (_e, id, key) => {
|
|
return getKeyType(id, key);
|
|
});
|
|
ipcMain.handle("redis:getKeyTTL", async (_e, id, key) => {
|
|
return getKeyTTL(id, key);
|
|
});
|
|
ipcMain.handle("redis:setTTL", async (_e, id, key, ttl) => {
|
|
await setKeyTTL(id, key, ttl);
|
|
});
|
|
ipcMain.handle("redis:deleteKey", async (_e, id, key) => {
|
|
await deleteKey(id, key);
|
|
});
|
|
ipcMain.handle("redis:renameKey", async (_e, id, oldKey, newKey) => {
|
|
await renameKey(id, oldKey, newKey);
|
|
});
|
|
ipcMain.handle("redis:existsKey", async (_e, id, key) => {
|
|
return existsKey(id, key);
|
|
});
|
|
ipcMain.handle("redis:batchDelete", async (_e, id, keys) => {
|
|
return batchDelete(id, keys);
|
|
});
|
|
ipcMain.handle("redis:memoryUsage", async (_e, id, key) => {
|
|
return memoryUsage(id, key);
|
|
});
|
|
ipcMain.handle("redis:getString", async (_e, id, key) => {
|
|
return getStringValue(id, key);
|
|
});
|
|
ipcMain.handle("redis:setString", async (_e, id, key, value) => {
|
|
await setStringValue(id, key, value);
|
|
});
|
|
ipcMain.handle("redis:hashScan", async (_e, id, key, cursor, count) => {
|
|
return getHashFields(id, key, cursor, count);
|
|
});
|
|
ipcMain.handle("redis:hashSet", async (_e, id, key, field, value) => {
|
|
await setHashField(id, key, field, value);
|
|
});
|
|
ipcMain.handle("redis:hashDel", async (_e, id, key, field) => {
|
|
await deleteHashField(id, key, field);
|
|
});
|
|
ipcMain.handle("redis:listRange", async (_e, id, key, start, stop) => {
|
|
return listRange(id, key, start, stop);
|
|
});
|
|
ipcMain.handle("redis:listLength", async (_e, id, key) => {
|
|
return listLength(id, key);
|
|
});
|
|
ipcMain.handle("redis:listPush", async (_e, id, key, values) => {
|
|
return listPush(id, key, ...values);
|
|
});
|
|
ipcMain.handle("redis:listSet", async (_e, id, key, index, value) => {
|
|
await listSet(id, key, index, value);
|
|
});
|
|
ipcMain.handle("redis:listRemove", async (_e, id, key, count, value) => {
|
|
return listRemove(id, key, count, value);
|
|
});
|
|
ipcMain.handle("redis:setMembers", async (_e, id, key) => {
|
|
return setMembers(id, key);
|
|
});
|
|
ipcMain.handle("redis:setSize", async (_e, id, key) => {
|
|
return setSize(id, key);
|
|
});
|
|
ipcMain.handle("redis:setAdd", async (_e, id, key, members) => {
|
|
return setAdd(id, key, ...members);
|
|
});
|
|
ipcMain.handle("redis:setRemove", async (_e, id, key, members) => {
|
|
return setRemove(id, key, ...members);
|
|
});
|
|
ipcMain.handle("redis:zsetRange", async (_e, id, key, start, stop, withScores) => {
|
|
return zsetRange(id, key, start, stop, withScores);
|
|
});
|
|
ipcMain.handle("redis:zsetSize", async (_e, id, key) => {
|
|
return zsetSize(id, key);
|
|
});
|
|
ipcMain.handle("redis:zsetAdd", async (_e, id, key, score, member) => {
|
|
return zsetAdd(id, key, score, member);
|
|
});
|
|
ipcMain.handle("redis:zsetRemove", async (_e, id, key, members) => {
|
|
return zsetRemove(id, key, ...members);
|
|
});
|
|
ipcMain.handle("redis:streamInfo", async (_e, id, key) => {
|
|
return streamInfo(id, key);
|
|
});
|
|
ipcMain.handle("redis:streamRange", async (_e, id, key, start, end, count) => {
|
|
return streamRange(id, key, start, end, count);
|
|
});
|
|
ipcMain.handle("redis:streamAdd", async (_e, id, key, streamId, fieldValues) => {
|
|
return streamAdd(id, key, streamId, fieldValues);
|
|
});
|
|
ipcMain.handle("redis:streamTrim", async (_e, id, key, maxLen) => {
|
|
return streamTrim(id, key, maxLen);
|
|
});
|
|
ipcMain.handle("redis:streamDel", async (_e, id, key, ids) => {
|
|
return streamDel(id, key, ...ids);
|
|
});
|
|
}
|
|
let mainWindow = null;
|
|
function getIconPath() {
|
|
return join(__dirname, "../../build/icons/icon_256.png");
|
|
}
|
|
function createWindow() {
|
|
const iconPath = getIconPath();
|
|
mainWindow = new BrowserWindow({
|
|
width: 1200,
|
|
height: 800,
|
|
minWidth: 900,
|
|
minHeight: 600,
|
|
title: "JRedisDesktop",
|
|
icon: nativeImage.createFromPath(iconPath),
|
|
backgroundColor: nativeTheme.shouldUseDarkColors ? "#0f0f1a" : "#f8fafc",
|
|
webPreferences: {
|
|
preload: join(__dirname, "../preload/index.mjs"),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
sandbox: false
|
|
},
|
|
frame: false,
|
|
titleBarStyle: "hidden"
|
|
});
|
|
if (process.platform === "linux" && mainWindow) {
|
|
mainWindow.setIcon(nativeImage.createFromPath(iconPath));
|
|
}
|
|
restoreAndTrack(mainWindow);
|
|
if (process.env.ELECTRON_RENDERER_URL) {
|
|
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
|
mainWindow.webContents.openDevTools();
|
|
} else {
|
|
mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
|
|
}
|
|
mainWindow.on("closed", () => {
|
|
mainWindow = null;
|
|
});
|
|
nativeTheme.on("updated", () => {
|
|
mainWindow?.webContents.send("theme:os-updated", nativeTheme.shouldUseDarkColors);
|
|
});
|
|
}
|
|
app.whenReady().then(() => {
|
|
registerIpcHandlers();
|
|
createWindow();
|
|
});
|
|
app.on("window-all-closed", () => {
|
|
app.quit();
|
|
});
|
|
app.on("activate", () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
});
|