Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
export const icons = {
success:
'<svg t="1733817409678" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1488" width="1024" height="1024"><path d="M512 832c-176.448 0-320-143.552-320-320S335.552 192 512 192s320 143.552 320 320-143.552 320-320 320m0-704C300.256 128 128 300.256 128 512s172.256 384 384 384 384-172.256 384-384S723.744 128 512 128" fill="#FFF" p-id="1489"></path><path d="M619.072 429.088l-151.744 165.888-62.112-69.6a32 32 0 1 0-47.744 42.624l85.696 96a32 32 0 0 0 23.68 10.688h0.192c8.96 0 17.536-3.776 23.616-10.4l175.648-192a32 32 0 0 0-47.232-43.2" fill="#FFF" p-id="1490"></path></svg>',
error: '<svg t="1733817396560" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1326" width="1024" height="1024"><path d="M512 128C300.8 128 128 300.8 128 512s172.8 384 384 384 384-172.8 384-384S723.2 128 512 128zM512 832c-179.2 0-320-140.8-320-320s140.8-320 320-320 320 140.8 320 320S691.2 832 512 832z" fill="#FFF" p-id="1327"></path><path d="M672 352c-12.8-12.8-32-12.8-44.8 0L512 467.2 396.8 352C384 339.2 364.8 339.2 352 352S339.2 384 352 396.8L467.2 512 352 627.2c-12.8 12.8-12.8 32 0 44.8s32 12.8 44.8 0L512 556.8l115.2 115.2c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8L556.8 512l115.2-115.2C684.8 384 684.8 364.8 672 352z" fill="#FFF" p-id="1328"></path></svg>',
info: '<svg t="1733992721464" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1357" width="1024" height="1024"><path d="M512 898.71874973C299.30468777 898.71874973 125.28125027 724.69531223 125.28125027 512S299.30468777 125.28125027 512 125.28125027s386.71874973 174.0234375 386.71874973 386.71874973-174.0234375 386.71874973-386.71874973 386.71874973z m0-696.09375c-170.15625027 0-309.37500027 139.21875-309.37500027 309.37500027 0 170.15625027 139.21875 309.37500027 309.37500027 309.37500027 170.15625027 0 309.37500027-139.21875 309.37500027-309.37500027 0-170.15625027-139.21875-309.37500027-309.37500027-309.37500027z" fill="#FFFFFF" p-id="1358"></path><path d="M512 746.59765652a37.96875 37.96875 0 0 1-38.67187473-38.67187554v-221.6953125c0-21.9375 16.76953125-38.67187473 38.67187473-38.67187473 21.90234348 0 38.67187473 16.73437473 38.67187473 38.67187473v221.6953125c0 21.9375-16.76953125 38.67187473-38.67187473 38.67187554zM512 390.81640625a37.96875 37.96875 0 0 1-38.67187473-38.67187473V316.07421902c0-21.9375 16.76953125-38.67187473 38.67187473-38.67187554 21.90234348 0 38.67187473 16.73437473 38.67187473 38.67187554v36.0703125c0 21.9375-18.03515625 38.67187473-38.67187473 38.67187473z" fill="#FFFFFF" p-id="1359"></path></svg>',
loading: `
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<rect fill="#FFFFFF" x="21.5" y="21.5" width="25" height="25" rx="3" ry="3">
<animate attributeName="x" calcMode="linear" values="21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5" keyTimes="0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1" dur="1.5" begin="-1.375s" repeatCount="indefinite"></animate>
<animate attributeName="y" calcMode="linear" values="21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5" keyTimes="0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1" dur="1.5" begin="-1s" repeatCount="indefinite"></animate>
</rect>
<rect fill="#FFFFFF" x="21.5" y="53.5" width="25" height="25" rx="3" ry="3">
<animate attributeName="x" calcMode="linear" values="21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5" keyTimes="0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1" dur="1.5" begin="-0.875s" repeatCount="indefinite"></animate>
<animate attributeName="y" calcMode="linear" values="21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5" keyTimes="0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1" dur="1.5" begin="-0.5s" repeatCount="indefinite"></animate>
</rect>
<rect fill="#FFFFFF" x="53.5" y="42.919" width="25" height="25" rx="3" ry="3">
<animate attributeName="x" calcMode="linear" values="21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5" keyTimes="0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1" dur="1.5" begin="-0.375s" repeatCount="indefinite"></animate>
<animate attributeName="y" calcMode="linear" values="21.5;53.5;53.5;53.5;53.5;21.5;21.5;21.5;21.5" keyTimes="0;0.083;0.25;0.333;0.5;0.583;0.75;0.833;1" dur="1.5" begin="0s" repeatCount="indefinite"></animate>
</rect>
</svg>`,
};
+370
View File
@@ -0,0 +1,370 @@
import iconv from "iconv-lite";
import {exec as _exec, spawn} from "node:child_process";
import net from "node:net";
import util from "node:util";
import {AppConfig} from "../../../src/config";
import {
extraResolveBin,
isLinux,
isMac,
isWin,
platformArch,
platformName,
platformUUID,
platformVersion,
} from "../../lib/env";
import {IconvUtil, ShellUtil, StrUtil} from "../../lib/util";
import {Log} from "../log/index";
const exec = util.promisify(_exec);
const outputStringConvert = (outputEncoding: "utf8" | "cp936", data: any) => {
if (!data) {
return "";
}
if (outputEncoding === "utf8") {
return data.toString();
}
let dataEncoding = "binary";
if (Buffer.isBuffer(data)) {
dataEncoding = IconvUtil.detect(data as any);
if ("UTF-8" === dataEncoding) {
return data.toString("utf8");
}
}
// dataEncoding UTF-8 cp936
// dataEncoding ISO-8859-1 cp936
// console.log('dataEncoding', dataEncoding, outputEncoding)
return iconv.decode(Buffer.from(data, dataEncoding as any), outputEncoding);
};
const shell = async (
command: string,
option?: {
cwd?: string;
outputEncoding?: string;
shell?: boolean;
}
) => {
option = Object.assign(
{
cwd: process.cwd(),
outputEncoding: isWin ? "cp936" : "utf8",
shell: true,
},
option
);
const result = await exec(command, {
env: {...process.env},
shell: option.shell,
encoding: "binary",
cwd: option["cwd"],
} as any);
return {
stdout: outputStringConvert(option.outputEncoding as any, result.stdout),
stderr: outputStringConvert(option.outputEncoding as any, result.stderr),
};
};
const spawnShell = async (
command: string | string[],
option: {
stdout?: (data: string, process: any) => void;
stderr?: (data: string, process: any) => void;
success?: (process: any) => void;
error?: (msg: string, exitCode: number, process: any) => void;
cwd?: string;
outputEncoding?: string;
env?: Record<string, any>;
shell?: boolean;
} | null = null
): Promise<{
stop: () => Promise<void>;
send: (data: any) => void;
result: () => Promise<string>;
}> => {
option = Object.assign(
{
cwd: process.cwd(),
outputEncoding: isWin ? "cp936" : "utf8",
env: {},
shell: true,
},
option
);
let commandEntry = "",
args = [];
if (Array.isArray(command)) {
commandEntry = command[0];
args = command.slice(1);
} else {
args = ShellUtil.parseCommandArgs(command);
commandEntry = args.shift() as string;
}
Log.info("App.spawnShell", {
commandEntry,
args,
option: {
cwd: option["cwd"],
outputEncoding: option["outputEncoding"],
},
});
const spawnProcess = spawn(commandEntry, args, {
env: {...process.env, ...option.env},
cwd: option["cwd"],
shell: option.shell,
encoding: "binary",
} as any);
let end = false;
let isSuccess = false;
let exitCode = -1;
const stdoutList: string[] = [];
const stderrList: string[] = [];
spawnProcess.stdout?.on("data", data => {
// console.log('App.spawnShell.stdout', data)
let dataString = outputStringConvert(option.outputEncoding as any, data);
Log.info("App.spawnShell.stdout", dataString);
stdoutList.push(dataString);
option.stdout?.(dataString, spawnProcess);
});
spawnProcess.stderr?.on("data", data => {
// console.log('App.spawnShell.stderr', data)
let dataString = outputStringConvert(option.outputEncoding as any, data);
Log.info("App.spawnShell.stderr", dataString);
stderrList.push(dataString);
option.stderr?.(dataString, spawnProcess);
});
spawnProcess.on("exit", (code, signal) => {
// console.log('App.spawnShell.exit', code)
Log.info("App.spawnShell.exit", {code, signal});
exitCode = code;
if (isWin) {
if (0 === code || 1 === code) {
isSuccess = true;
}
} else {
if (null === code || 0 === code) {
isSuccess = true;
}
}
if (isSuccess) {
option.success?.(spawnProcess);
} else {
option.error?.(`command ${command} failed with code ${code}`, exitCode, spawnProcess);
}
end = true;
});
spawnProcess.on("error", err => {
// console.log('App.spawnShell.error', err)
Log.info("App.spawnShell.error", err);
option.error?.(err.toString(), -1, spawnProcess);
end = true;
});
const waitForExit = async () => {
if (end) {
return;
}
await new Promise<void>(resolve => {
let settled = false;
const finish = () => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeoutId);
spawnProcess.off("exit", finish);
spawnProcess.off("error", finish);
resolve();
};
const timeoutId = setTimeout(finish, 5000);
spawnProcess.once("exit", finish);
spawnProcess.once("error", finish);
});
};
return {
stop: async () => {
Log.info("App.spawnShell.stop");
if (end) {
return;
}
if (isWin) {
await new Promise<void>(resolve => {
_exec(
`taskkill /pid ${spawnProcess.pid} /T /F`,
{
encoding: "binary",
},
(err, stdout, stderr) => {
if (stdout) {
stdout = outputStringConvert(option.outputEncoding as any, stdout);
}
if (stderr) {
stderr = outputStringConvert(option.outputEncoding as any, stderr);
}
Log.info("App.spawnShell.stop.taskkill", JSON.parse(JSON.stringify({err, stdout, stderr})));
resolve();
}
);
});
} else {
spawnProcess.kill("SIGINT");
}
await waitForExit();
},
send: data => {
Log.info("App.spawnShell.send", data);
spawnProcess.stdin.write(data);
},
result: async (): Promise<string> => {
if (end) {
return stdoutList.join("") + stderrList.join("");
}
return new Promise((resolve, reject) => {
const watchEnd = () => {
setTimeout(() => {
if (!end) {
watchEnd();
return;
}
if (isSuccess) {
resolve(stdoutList.join("") + stderrList.join(""));
} else {
reject(
[
`command ${command} failed with code ${exitCode} : `,
stdoutList.join(""),
stderrList.join(""),
].join("")
);
}
}, 10);
};
watchEnd();
});
},
};
};
const spawnBinary = async (
binary: string,
args: string[],
option: {
stdout?: (data: string, process: any) => void;
stderr?: (data: string, process: any) => void;
success?: (process: any) => void;
error?: (msg: string, exitCode: number, process: any) => void;
cwd?: string;
outputEncoding?: string;
env?: Record<string, any>;
shell?: boolean;
} | null = null
): Promise<string> => {
try {
args.unshift(extraResolveBin(binary));
} catch (error) {
// 如果extraResolveBin抛出错误,通过error回调通知调用者
if (option?.error) {
option.error(error.message, -1, null);
}
throw error;
}
const res = await Apps.spawnShell(args, {
...(option || {}),
shell: false,
});
return await res.result();
};
const availablePortLock: {
[port: number]: {
lockKey: string;
lockTime: number;
};
} = {};
/**
* 获取一个可用的端口
* @param start 开始的端口
* @param lockKey 锁定的key,避免其他进程获取,默认会创建一个随机的key
* @param lockTime 锁定时间,避免在本次获取后未启动服务导致其他进程重复获取
*/
const availablePort = async (start: number, lockKey?: string, lockTime?: number): Promise<number> => {
lockKey = lockKey || StrUtil.randomString(8);
lockTime = lockTime || 60;
// expire lock
const now = Date.now();
for (const port in availablePortLock) {
const lockInfo = availablePortLock[port];
if (lockInfo.lockTime < now) {
delete availablePortLock[port];
}
}
for (let i = start; i < 65535; i++) {
const available = await isPortAvailable(i, "0.0.0.0");
const availableLocal = await isPortAvailable(i, "127.0.0.1");
// console.log('isPortAvailable', i, available, availableLocal)
if (available && availableLocal) {
const lockInfo = availablePortLock[i];
if (lockInfo) {
if (lockInfo.lockKey === lockKey) {
return i;
} else {
// other lockKey lock the port
continue;
}
}
availablePortLock[i] = {
lockKey,
lockTime: Date.now() + lockTime * 1000,
};
return i;
}
}
throw new Error("no available port");
};
const isPortAvailable = async (port: number, host?: string): Promise<boolean> => {
return new Promise(resolve => {
const server = net.createServer();
server.listen(port, host);
server.on("listening", () => {
server.close();
resolve(true);
});
server.on("error", () => {
resolve(false);
});
});
};
const fixExecutable = async (executable: string) => {
if (isMac || isLinux) {
// chmod +x executable
await shell(`chmod +x "${executable}"`);
}
};
const getUserAgent = () => {
let param = [];
// URL-encode app name to handle Chinese characters in HTTP headers
const appName = encodeURIComponent(AppConfig.name);
param.push(`AppOpen/${appName}/${AppConfig.version}`);
param.push(`Platform/${platformName()}/${platformArch()}/${platformVersion()}/${platformUUID()}`);
return param.join(" ");
};
export const Apps = {
shell,
spawnShell,
spawnBinary,
availablePort,
isPortAvailable,
fixExecutable,
getUserAgent,
};
export default Apps;
+97
View File
@@ -0,0 +1,97 @@
import {screen} from "electron";
type PositionCache = {
x: 0;
y: 0;
screenWidth: 0;
screenHeight: 0;
id: -1;
};
export const AppPosition = {
caches: {} as Record<string, PositionCache>,
getCache(name: string): PositionCache {
if (!this.caches[name]) {
this.caches[name] = {
x: 0,
y: 0,
screenWidth: 0,
screenHeight: 0,
id: -1,
};
}
return this.caches[name];
},
get(
name: string,
calculator?: (
screenX: number,
screenY: number,
screenWidth: number,
screenHeight: number
) => {
x: number;
y: number;
}
): {
x: number;
y: number;
} {
const cache = this.getCache(name);
const {x, y} = screen.getCursorScreenPoint();
const currentDisplay = screen.getDisplayNearestPoint({x, y});
if (cache.id !== currentDisplay.id) {
cache.id = currentDisplay.id;
cache.screenWidth = currentDisplay.workArea.width;
cache.screenHeight = currentDisplay.workArea.height;
if (!calculator) {
calculator = (screenX: number, screenY: number, screenWidth: number, screenHeight: number) => {
// console.log('calculator', {screenX, screenY, screenWidth, screenHeight});
return {
x: screenX + screenWidth / 10,
y: screenY + screenHeight / 10,
};
};
}
const res = calculator(
currentDisplay.workArea.x,
currentDisplay.workArea.y,
cache.screenWidth,
cache.screenHeight
);
cache.x = parseInt(String(res.x));
cache.y = parseInt(String(res.y));
}
return {
x: cache.x,
y: cache.y,
};
},
set(name: string, x: number, y: number): void {
const cache = this.getCache(name);
cache.x = x;
cache.y = y;
},
getContextMenuPosition(
boxWidth: number,
boxHeight: number
): {
x: number;
y: number;
} {
const {x, y} = screen.getCursorScreenPoint();
const currentDisplay = screen.getDisplayNearestPoint({x, y});
let resultX = x;
let resultY = y;
if (currentDisplay.workArea.width - x < boxWidth) {
resultX = currentDisplay.workArea.width - boxWidth;
}
if (currentDisplay.workArea.height - y < boxHeight) {
resultY = currentDisplay.workArea.height - boxHeight;
}
return {
x: resultX,
y: resultY,
};
},
};
+171
View File
@@ -0,0 +1,171 @@
import {BrowserWindow} from "electron";
import {AppsMain} from "./main";
import {icons} from "./icons";
export const makeLoading = (
msg: string,
options?: {
timeout?: number;
percentAuto?: boolean;
percentTotalSeconds?: number;
}
): {
close: () => void;
percent: (value: number) => void;
} => {
options = Object.assign(
{
percentAuto: false,
percentTotalSeconds: 30,
timeout: 0,
},
options
);
if (options.timeout === 0) {
options.timeout = 60 * 10 * 1000;
}
// console.log('options', options)
const display = AppsMain.getCurrentScreenDisplay();
// console.log('xxxx', primaryDisplay);
const width = display.workArea.width;
const height = 60;
const icon = icons.loading;
const win = new BrowserWindow({
height,
width,
x: 0,
y: 0,
modal: false,
frame: false,
alwaysOnTop: true,
center: false,
transparent: true,
hasShadow: false,
show: false,
focusable: false,
skipTaskbar: true,
});
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<style>
html,body{
height: 100vh;
margin: 0;
padding: 0;
background: rgba(0, 0, 0, 0.4);
color: #FFFFFF;
}
.message-view {
height: 100vh;
display:flex;
text-align:center;
padding:0 10px;
position:relative;
}
.message-view #message{
margin: auto;
font-size: 16px;
display: inline-block;
line-height: 30px;
white-space: nowrap;
}
.message-view #message .icon{
width: 30px;
height: 30px;
display:inline-block;
margin-right: 5px;
vertical-align: top;
}
.message-view #percent{
position: absolute;
bottom: 5px;
left: 5px;
right: 5px;
height: 5px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.4);
overflow: hidden;
display:none;
}
.message-view #percent .value{
border-radius: 5px;
height: 100%;
width: 0%;
background: #FFFFFF;
}
::-webkit-scrollbar {
width: 0;
}
</style>
</head>
<body>
<div class="message-view">
<div id="message">${icon}${msg}</div>
<div id="percent">
<div class="value"></div>
</div>
</div>
</body>
</html>
`;
const encodedHTML = encodeURIComponent(htmlContent);
let percentAutoTimer = null;
win.loadURL(`data:text/html;charset=UTF-8,${encodedHTML}`);
win.on("ready-to-show", async () => {
const width = Math.ceil(
await win.webContents.executeJavaScript(`(()=>{
const message = document.getElementById('message');
const width = message.scrollWidth;
return width;
})()`)
);
win.setSize(width + 20, height);
const x = display.workArea.x + display.workArea.width / 2 - (width + 20) / 2;
const y = display.workArea.y + (display.workArea.height * 1) / 4;
win.setPosition(Math.floor(x), Math.floor(y));
win.show();
if (options.percentAuto) {
let percent = 0;
percentAutoTimer = setInterval(() => {
percent += 0.01;
if (percent >= 1) {
clearInterval(percentAutoTimer);
return;
}
controller.percent(percent);
}, (options.percentTotalSeconds * 1000) / 100);
}
// win.webContents.openDevTools({
// mode: 'detach'
// })
});
const winCloseTimer = setTimeout(() => {
win.close();
clearTimeout(winCloseTimer);
}, options.timeout);
const controller = {
close: () => {
win.close();
clearTimeout(winCloseTimer);
if (percentAutoTimer) {
clearInterval(percentAutoTimer);
}
},
percent: (value: number) => {
const percent = 100 * value;
win.webContents.executeJavaScript(`(()=>{
const percent = document.querySelector('#percent');
const percentValue = document.querySelector('#percent .value');
percent.style.display = 'block';
percentValue.style.width = '${percent}%';
})()`);
},
};
return controller;
};
+504
View File
@@ -0,0 +1,504 @@
import { app, BrowserWindow, clipboard, ipcMain, nativeImage, nativeTheme, screen, shell } from "electron";
import { spawn } from "child_process";
import { AppConfig } from "../../../src/config";
import { CommonConfig } from "../../config/common";
import { WindowConfig } from "../../config/window";
import { isDev, isMac, platformArch, platformName, platformUUID, platformVersion } from "../../lib/env";
import { preloadDefault, rendererDistPath } from "../../lib/env-main";
import { getResourceRoot } from "../../lib/resource-path";
import { Page } from "../../page";
import { ConfigMain } from "../config/main";
import { AppRuntime } from "../env";
import { Events } from "../event/main";
import { Files } from "../file/main";
import Apps from "./index";
import { AppPosition } from "./lib/position";
import { makeLoading } from "./loading";
import { SetupMain } from "./setup";
import { makeToast } from "./toast";
const getWindowByName = (name?: string) => {
if (!name || "main" === name) {
return AppRuntime.mainWindow;
}
return AppRuntime.windows[name];
};
const getCurrentWindow = (window, e) => {
let originWindow = BrowserWindow.fromWebContents(e.sender);
// if (originWindow !== window) originWindow = detachInstance.getWindow();
return originWindow;
};
const quit = () => {
// @ts-ignore
app.quitForce = true;
app.quit();
};
ipcMain.handle("app:quit", () => {
quit();
});
const restart = () => {
app.relaunch();
};
ipcMain.handle("app:restart", () => {
restart();
});
const windowMin = (name?: string) => {
getWindowByName(name)?.minimize();
};
const windowMax = (name?: string) => {
const win = getWindowByName(name);
if (!win) {
return;
}
if (win.isFullScreen()) {
win.setFullScreen(false);
win.unmaximize();
win.center();
} else if (win.isMaximized()) {
win.unmaximize();
win.center();
} else {
win.setMinimumSize(WindowConfig.minWidth, WindowConfig.minHeight);
win.maximize();
}
};
const windowSetSize = (
name: string | null,
width: number,
height: number,
option?: {
includeMinimumSize: boolean;
center: boolean;
}
) => {
width = parseInt(String(width));
height = parseInt(String(height));
// console.log('windowSetSize', name, width, height, option)
const win = getWindowByName(name);
if (!win) {
return;
}
option = Object.assign(
{
includeMinimumSize: true,
center: true,
},
option
);
if (option.includeMinimumSize) {
win.setMinimumSize(width, height);
}
win.setSize(width, height);
if (option.center) {
win.center();
}
};
ipcMain.handle("app:openExternal", (event, url: string) => {
return shell.openExternal(url);
});
ipcMain.handle("app:openPath", (event, url: string) => {
return shell.openPath(url);
});
ipcMain.handle("app:showItemInFolder", (event, url: string) => {
return shell.showItemInFolder(url);
});
ipcMain.handle("app:getPreload", event => {
let preload = preloadDefault;
if (!preload.startsWith("file://")) {
preload = `file://${preload}`;
}
return preload;
});
ipcMain.handle("window:min", (event, name: string) => {
windowMin(name);
});
ipcMain.handle("window:max", (event, name: string) => {
windowMax(name);
});
ipcMain.handle(
"window:setSize",
(
event,
name: string | null,
width: number,
height: number,
option?: {
includeMinimumSize: boolean;
center: boolean;
}
) => {
windowSetSize(name, width, height, option);
}
);
ipcMain.handle("window:close", (event, name: string) => {
getWindowByName(name)?.close();
});
const windowOpen = async (
name: string,
option?: {
singleton?: boolean;
parent?: BrowserWindow;
[key: string]: any;
}
) => {
name = name || "main";
return Page.open(name, option);
};
ipcMain.handle("window:open", (event, name: string, option: any) => {
return windowOpen(name, option);
});
ipcMain.handle("window:hide", (event, name: string) => {
getWindowByName(name)?.hide();
if (isMac) {
app.dock.hide();
}
});
ipcMain.handle(
"window:move",
(
event,
name: string | null,
data: {
mouseX: number;
mouseY: number;
width: number;
height: number;
}
) => {
const { x, y } = screen.getCursorScreenPoint();
const originWindow = getWindowByName(name);
if (!originWindow) return;
originWindow.setBounds({ x: x - data.mouseX, y: y - data.mouseY, width: data.width, height: data.height });
AppPosition.set(name, x - data.mouseX, y - data.mouseY);
}
);
const getClipboardText = () => {
return clipboard.readText("clipboard");
};
ipcMain.handle("app:getClipboardText", event => {
return getClipboardText();
});
const setClipboardText = (text: string) => {
clipboard.writeText(text, "clipboard");
};
ipcMain.handle("app:setClipboardText", (event, text: string) => {
setClipboardText(text);
});
const getClipboardImage = () => {
const image = clipboard.readImage("clipboard");
return image.isEmpty() ? "" : image.toDataURL();
};
ipcMain.handle("app:getClipboardImage", event => {
return getClipboardImage();
});
const setClipboardImage = (image: string) => {
const img = nativeImage.createFromDataURL(image);
clipboard.writeImage(img, "clipboard");
};
ipcMain.handle("app:setClipboardImage", (event, image: string) => {
setClipboardImage(image);
});
const isDarkMode = () => {
if (!CommonConfig.darkModeEnable) {
return false;
}
return nativeTheme.shouldUseDarkColors;
};
const shouldDarkMode = async () => {
if (!CommonConfig.darkModeEnable) {
return false;
}
const darkMode = (await ConfigMain.get("darkMode")) || "auto";
if ("dark" === darkMode) {
return true;
} else if ("light" === darkMode) {
return false;
} else if ("auto" === darkMode) {
return isDarkMode();
}
return false;
};
const defaultDarkModeBackgroundColor = async () => {
if (await shouldDarkMode()) {
return "#17171A";
}
return "#FFFFFF";
};
nativeTheme.on("updated", () => {
Events.broadcast("DarkModeChange", { isDarkMode: isDarkMode() });
AppsMain.defaultDarkModeBackgroundColor().then(color => {
AppRuntime.mainWindow.setBackgroundColor(color)
});
});
ipcMain.handle("app:isDarkMode", () => {
return isDarkMode();
});
const getCurrentScreenDisplay = () => {
const screenPoint = screen.getCursorScreenPoint();
const display = screen.getDisplayNearestPoint(screenPoint);
return {
bounds: display.bounds,
workArea: display.workArea,
};
};
const calcPositionInCurrentDisplay = (
position: "center" | "left-top" | "right-top" | "left-bottom" | "right-bottom",
width: number,
height: number
) => {
const { bounds, workArea } = getCurrentScreenDisplay();
let x = 0;
let y = 0;
switch (position) {
case "center":
x = workArea.x + (workArea.width - width) / 2;
y = workArea.y + (workArea.height - height) / 2;
break;
case "left-top":
x = workArea.x;
y = workArea.y;
break;
case "right-top":
x = workArea.x + workArea.width - width;
y = workArea.y;
break;
case "left-bottom":
x = workArea.x;
y = workArea.y + workArea.height - height;
break;
case "right-bottom":
x = workArea.x + workArea.width - width;
y = workArea.y + workArea.height - height;
break;
}
return {
x: Math.round(x),
y: Math.round(y),
};
};
const toast = (
msg: string,
options?: {
duration?: number;
status?: "success" | "error" | "info";
}
) => {
return makeToast(msg, options);
};
ipcMain.handle("app:toast", (event, msg: string, option?: any) => {
return toast(msg, option);
});
const loading = (
msg: string,
options?: {
timeout?: number;
percentAuto?: boolean;
percentTotalSeconds?: number;
}
): {
close: () => void;
percent: (value: number) => void;
} => {
return makeLoading(msg, options);
};
ipcMain.handle("app:loading", (event, msg: string, option?: any) => {
return loading(msg, option);
});
ipcMain.handle("app:setupList", async () => {
return SetupMain.list();
});
ipcMain.handle("app:setupOpen", async (event, name: string) => {
return SetupMain.open(name);
});
const setupIsOk = async () => {
return SetupMain.isOk();
};
ipcMain.handle("app:setupIsOk", async () => {
return setupIsOk();
});
const getBuildInfo = async () => {
if (isDev) {
return {
buildId: "Development",
};
}
const json = await Files.read(rendererDistPath("build.json"), {
isDataPath: false,
});
return JSON.parse(json);
};
ipcMain.handle("app:getBuildInfo", async () => {
return getBuildInfo();
});
const collect = async (options?: {}) => {
return {
userAgent: Apps.getUserAgent(),
name: AppConfig.name,
version: AppConfig.version,
uuid: platformUUID(),
platformVersion: platformVersion(),
platformName: platformName(),
platformArch: platformArch(),
};
};
ipcMain.handle("app:collect", async (event, options?: {}) => {
return collect(options);
});
const setAutoLaunch = async (enable: boolean, options?: {}) => {
return app.setLoginItemSettings({
openAtLogin: enable,
});
};
ipcMain.handle("app:setAutoLaunch", async (event, enable: boolean, options?: {}) => {
return setAutoLaunch(enable, options);
});
const getAutoLaunch = async (options?: {}) => {
return app.getLoginItemSettings().openAtLogin;
};
ipcMain.handle("app:getAutoLaunch", async (event, options?: {}) => {
return getAutoLaunch(options);
});
ipcMain.handle("app:getResourcesPath", () => {
return getResourceRoot();
});
// FFprobe video info handler
ipcMain.handle("app:ffprobeVideoInfo", async (event, videoPath: string) => {
const { getFFprobePath } = await import('../shell');
try {
const ffprobePath = await getFFprobePath();
// 获取视频流信息和格式信息(包含时长)
const args = [
"-v", "error",
"-show_entries", "stream=width,height,r_frame_rate:format=duration",
"-of", "json",
videoPath
];
return new Promise((resolve, reject) => {
const child = spawn(ffprobePath, args, { shell: false });
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`ffprobe failed with code ${code}: ${stderr}`));
return;
}
try {
const data = JSON.parse(stdout);
// 获取时长
let duration = undefined;
if (data.format && data.format.duration) {
duration = parseFloat(data.format.duration);
}
// 如果有视频流,获取视频信息
if (data.streams && data.streams.length > 0) {
const stream = data.streams[0];
const width = parseInt(stream.width, 10) || undefined;
const height = parseInt(stream.height, 10) || undefined;
// 解析帧率
let fps = 30;
if (stream.r_frame_rate) {
const fpsParts = stream.r_frame_rate.split("/");
fps = fpsParts.length === 2
? parseFloat(fpsParts[0]) / parseFloat(fpsParts[1])
: parseFloat(stream.r_frame_rate);
}
resolve({ width, height, fps, duration });
} else {
// 只有音频流的情况,只返回时长
resolve({ duration });
}
} catch (e) {
reject(e);
}
});
});
} catch (error) {
throw error;
}
});
export default {
quit,
};
export const AppsMain = {
shouldDarkMode,
defaultDarkModeBackgroundColor,
getWindowByName,
getClipboardText,
setClipboardText,
getClipboardImage,
setClipboardImage,
getCurrentScreenDisplay,
calcPositionInCurrentDisplay,
toast,
loading,
setupIsOk,
windowOpen,
};
+208
View File
@@ -0,0 +1,208 @@
import { ipcRenderer } from "electron";
import { resolve } from "node:path";
import { isPackaged, platformArch, platformName } from "../../lib/env";
import { AppEnv, waitAppEnvReady } from "../env";
import appIndex from "./index";
const isDarkMode = async () => {
return ipcRenderer.invoke("app:isDarkMode");
};
const quit = () => {
return ipcRenderer.invoke("app:quit");
};
const restart = () => {
return ipcRenderer.invoke("app:restart");
};
const isPlatform = (name: "win" | "osx" | "linux") => {
return platformName() === name;
};
const windowMin = (name?: string) => {
return ipcRenderer.invoke("window:min", name);
};
const windowMax = (name?: string) => {
return ipcRenderer.invoke("window:max", name);
};
const windowSetSize = (
name: string | null,
width: number,
height: number,
option?: {
includeMinimumSize: boolean;
center: boolean;
}
) => {
return ipcRenderer.invoke("window:setSize", name, width, height, option);
};
const windowOpen = (name: string, option: any) => {
return ipcRenderer.invoke("window:open", name, option);
};
const windowHide = (name: string) => {
return ipcRenderer.invoke("window:hide", name);
};
const windowClose = (name: string) => {
return ipcRenderer.invoke("window:close", name);
};
const windowMove = (name: string | null, data: { mouseX: number; mouseY: number; width: number; height: number }) => {
return ipcRenderer.invoke("window:move", name, data);
};
const openExternal = (url: string) => {
return ipcRenderer.invoke("app:openExternal", url);
};
const openPath = (url: string) => {
return ipcRenderer.invoke("app:openPath", url);
};
const showItemInFolder = (url: string) => {
return ipcRenderer.invoke("app:showItemInFolder", url);
};
const getPreload = async () => {
return ipcRenderer.invoke("app:getPreload");
};
const getResourcesPath = async () => {
return ipcRenderer.invoke("app:getResourcesPath");
};
const resourcePathResolve = async (filePath: string) => {
await waitAppEnvReady();
const basePath = isPackaged ? process.resourcesPath : AppEnv.appRoot;
return resolve(basePath, filePath);
};
const extraPathResolve = async (filePath: string) => {
await waitAppEnvReady();
const basePath = isPackaged ? process.resourcesPath : "electron/resources";
return resolve(basePath, "extra", filePath);
};
const appEnv = async () => {
await waitAppEnvReady();
return AppEnv;
};
const setRenderAppEnv = (env: any) => {
AppEnv.isInit = true;
AppEnv.appRoot = env.appRoot;
AppEnv.appData = env.appData;
AppEnv.userData = env.userData;
AppEnv.dataRoot = env.dataRoot;
};
const getClipboardText = () => {
return ipcRenderer.invoke("app:getClipboardText");
};
const setClipboardText = (text: string) => {
return ipcRenderer.invoke("app:setClipboardText", text);
};
const getClipboardImage = () => {
return ipcRenderer.invoke("app:getClipboardImage");
};
const setClipboardImage = (image: string) => {
return ipcRenderer.invoke("app:setClipboardImage", image);
};
const toast = (msg: string, option?: any) => {
return ipcRenderer.invoke("app:toast", msg, option);
};
const setupList = () => {
return ipcRenderer.invoke("app:setupList");
};
const setupOpen = (name: string) => {
return ipcRenderer.invoke("app:setupOpen", name);
};
const setupIsOk = async () => {
return ipcRenderer.invoke("app:setupIsOk");
};
const getBuildInfo = async () => {
return ipcRenderer.invoke("app:getBuildInfo");
};
const collect = async (options?: {}) => {
return ipcRenderer.invoke("app:collect", options);
};
const setAutoLaunch = async (enable: boolean, options?: {}) => {
return ipcRenderer.invoke("app:setAutoLaunch", enable, options);
};
const getAutoLaunch = async (options?: {}) => {
return ipcRenderer.invoke("app:getAutoLaunch", options);
};
export const AppsRender = {
isDarkMode,
resourcePathResolve,
extraPathResolve,
platformName,
platformArch,
isPlatform,
quit,
restart,
windowMin,
windowMax,
windowSetSize,
windowOpen,
windowHide,
windowClose,
windowMove,
openExternal,
openPath,
showItemInFolder,
getPreload,
getResourcesPath,
appEnv,
setRenderAppEnv,
getClipboardText,
setClipboardText,
getClipboardImage,
setClipboardImage,
toast,
setupList,
setupOpen,
setupIsOk,
getBuildInfo,
collect,
setAutoLaunch,
getAutoLaunch,
shell: appIndex.shell,
spawnShell: appIndex.spawnShell,
spawnBinary: appIndex.spawnBinary,
availablePort: appIndex.availablePort,
fixExecutable: appIndex.fixExecutable,
getUserAgent: appIndex.getUserAgent,
};
export default AppsRender;
+45
View File
@@ -0,0 +1,45 @@
import {Permissions} from "../../lib/permission";
export const SetupMain = {
async isOk() {
return true;
},
async list() {
return [
{
name: "accessibility",
title: "辅助功能",
status: (await Permissions.checkAccessibilityAccess()) ? "success" : "fail",
desc: "系统运行需要依赖辅助功能,请打开设置,找到辅助功能,开启本软件的辅助功能。",
steps: [
{
title: "打开 设置 → 隐私与安全性 → 辅助功能,开启本软件",
image: "/setup/accessibility.png",
},
],
},
{
name: "screen",
title: "屏幕录制",
status: (await Permissions.checkScreenCaptureAccess()) ? "success" : "fail",
desc: "系统运行需要依赖屏幕录制,请打开设置,找到屏幕录制,开启本软件的屏幕录制权限。",
steps: [
{
title: "打开 设置 → 隐私与安全性 → 录屏与系统录音,开启本软件",
image: "/setup/screen.png",
},
],
},
];
},
async open(name: string) {
switch (name) {
case "accessibility":
Permissions.askAccessibilityAccess().then();
break;
case "screen":
Permissions.askScreenCaptureAccess().then();
break;
}
},
};
+163
View File
@@ -0,0 +1,163 @@
import {BrowserWindow} from "electron";
import {icons} from "./icons";
import {AppsMain} from "./main";
let win = null;
let winCloseTimer = null;
let winShowTime = null;
const toastMsgQueue: { msg: string, options: any }[] = [];
export const makeToast = async (
msg: string,
options?: {
duration?: number;
status?: "success" | "error" | "info";
}
) => {
if (win) {
if (winShowTime && Date.now() - winShowTime < 1000) {
// make previous toast last at least 1 second
if (toastMsgQueue.length > 0) {
toastMsgQueue.forEach(item => {
item.options = Object.assign({}, item.options, {duration: 1000});
})
}
toastMsgQueue.push({msg, options});
await new Promise(resolve => setTimeout(resolve, 1000 - (Date.now() - winShowTime)));
if (win) {
win.close();
}
return;
}
win.close();
}
winShowTime = Date.now();
options = Object.assign(
{
status: "info",
duration: 0,
},
options
);
if (options.duration === 0) {
options.duration = Math.max(msg.length * 400, 3000);
}
// console.log('toast', msg, options)
const display = AppsMain.getCurrentScreenDisplay();
// console.log('xxxx', primaryDisplay);
const width = display.workArea.width;
const height = 60;
const icon = icons[options.status] || icons.success;
win = new BrowserWindow({
height,
width,
parent: null,
x: 0,
y: 0,
modal: false,
frame: false,
alwaysOnTop: true,
// opacity: 0.9,
center: false,
transparent: true,
hasShadow: false,
show: false,
focusable: false,
skipTaskbar: true,
});
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<style>
html,body{
height: 100%;
margin: 0;
padding: 0;
background: transparent;
color: #FFFFFF;
font-family: "PingFang SC", "Helvetica Neue", Helvetica, STHeiTi, "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}
.message-view {
height: 100%;
text-align:center;
box-sizing: border-box;
background-color:transparent;
padding: 10px;
}
.message-view div{
margin: auto;
font-size: 16px;
display: inline-flex;
align-items: center;
line-height: 20px;
background: rgba(0, 0, 0, 0.85);
border-radius: 15px;
padding: 10px 10px;
box-shadow: 5px 5px 5px rgba(0,0,0,0.3);
}
.message-view div .icon{
width: 30px;
height: 30px;
display:inline-block;
margin-right: 8px;
vertical-align: middle;
flex-shrink: 0;
}
::-webkit-scrollbar {
width: 0;
}
</style>
</head>
<body>
<div class="message-view" onclick="window.close()">
<div id="message">${icon}${msg}</div>
</div>
</body>
</html>
`;
const encodedHTML = encodeURIComponent(htmlContent);
win.loadURL(`data:text/html;charset=UTF-8,${encodedHTML}`);
win.on("ready-to-show", async () => {
if (!win) return;
const containerSize = await win.webContents.executeJavaScript(`(()=>{
const message = document.getElementById('message');
const width = message.scrollWidth;
const height = message.scrollHeight;
return {width:width,height:height};
})()`);
// console.log('containerSize', containerSize);
const containerWidth = containerSize.width + 20;
const containerHeight = containerSize.height + 20;
win.setSize(containerWidth, containerHeight);
const x = display.workArea.x + display.workArea.width / 2 - containerWidth / 2;
const y = display.workArea.y + (display.workArea.height * 1) / 4;
win.setPosition(Math.floor(x), Math.floor(y));
win.showInactive();
// win.webContents.openDevTools({
// mode: 'detach'
// })
});
win.on("closed", () => {
win = null;
if (winCloseTimer) {
clearTimeout(winCloseTimer);
}
setTimeout(() => {
if (toastMsgQueue.length > 0) {
const item = toastMsgQueue.shift();
makeToast(item.msg, item.options);
}
}, 0);
})
winCloseTimer = setTimeout(() => {
winCloseTimer = null;
if (!win) return;
win.close();
}, options.duration);
};