Track bundled vendor runtime sources
This commit is contained in:
25
vendor/aigcpanel/electron/lib/api.ts
vendored
Normal file
25
vendor/aigcpanel/electron/lib/api.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import Apps from "../mapi/app";
|
||||
|
||||
export type ResultType<T> = {
|
||||
// should follow the rules:
|
||||
// <0 business error
|
||||
// =0 success
|
||||
// 10000 error ( network error, server error, etc. )
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
export const post = async (url: string, data: any) => {
|
||||
data = data || {};
|
||||
const userAgent = Apps.getUserAgent();
|
||||
data["AppManagerUserAgent"] = userAgent;
|
||||
return await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"User-Agent": userAgent,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
};
|
||||
101
vendor/aigcpanel/electron/lib/devtools.ts
vendored
Normal file
101
vendor/aigcpanel/electron/lib/devtools.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
import { BrowserView, BrowserWindow, screen } from "electron";
|
||||
import { isDev } from "./env";
|
||||
import { WindowConfig } from "../config/window";
|
||||
|
||||
export const DevToolsManager = {
|
||||
enable: true,
|
||||
rowCount: 4,
|
||||
colCount: 3,
|
||||
windows: new Map<BrowserWindow | BrowserView, BrowserWindow>(),
|
||||
setEnable(enable: boolean) {
|
||||
DevToolsManager.enable = enable;
|
||||
},
|
||||
getWindow(win: BrowserWindow | BrowserView) {
|
||||
return this.windows.get(win);
|
||||
},
|
||||
getOrCreateWindow(name: string, win: BrowserWindow | BrowserView) {
|
||||
if (this.windows.has(win)) {
|
||||
return this.windows.get(win);
|
||||
}
|
||||
const { x, y, width, height } = this.getDisplayPosition();
|
||||
// console.log('DevToolsManager', name, {x, y, width, height})
|
||||
const devtools = new BrowserWindow({
|
||||
show: true,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
title: name,
|
||||
});
|
||||
devtools.on("closed", (e) => {
|
||||
// console.log('DevToolsManager', 'close', name)
|
||||
this.windows.delete(win);
|
||||
});
|
||||
// console.log('DevToolsManager', name, {x, y})
|
||||
win.webContents.setDevToolsWebContents(devtools.webContents);
|
||||
win.webContents.on("destroyed", () => {
|
||||
// console.log('DevToolsManager', 'destroyed', name)
|
||||
devtools.destroy();
|
||||
});
|
||||
devtools.webContents.on("dom-ready", () => {
|
||||
setTimeout(() => {
|
||||
if (!devtools.isDestroyed()) {
|
||||
devtools.setTitle(name);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
this.windows.set(win, devtools);
|
||||
return devtools;
|
||||
},
|
||||
getLargestDisplay(): Electron.Display {
|
||||
const displays = screen.getAllDisplays();
|
||||
return displays.reduce((max, display) => {
|
||||
const { width, height } = display.size;
|
||||
const maxResolution = max.size.width * max.size.height;
|
||||
const currentResolution = width * height;
|
||||
return currentResolution > maxResolution ? display : max;
|
||||
});
|
||||
},
|
||||
getDisplayPosition(): {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
const display = this.getLargestDisplay();
|
||||
const { x, y, width, height } = display.workArea;
|
||||
// console.log('DevToolsManager', 'getDisplayPosition', {x, y, width, height})
|
||||
if (width < 1300) {
|
||||
this.rowCount = 3;
|
||||
this.colCount = 2;
|
||||
}
|
||||
const itemWidth = Math.floor(width / this.rowCount);
|
||||
const itemHeight = Math.floor(height / this.colCount);
|
||||
const maxRow = Math.floor(width / itemWidth);
|
||||
const row = this.windows.size % maxRow;
|
||||
const col = Math.floor(this.windows.size / maxRow);
|
||||
return {
|
||||
x: x + row * itemWidth,
|
||||
y: y + col * itemHeight,
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
};
|
||||
},
|
||||
register(name: string, win: BrowserWindow | BrowserView) {
|
||||
if (!isDev || !DevToolsManager.enable) {
|
||||
return;
|
||||
}
|
||||
this.getOrCreateWindow(name, win);
|
||||
},
|
||||
autoShow(win: BrowserWindow | BrowserView) {
|
||||
if (!isDev || !DevToolsManager.enable) {
|
||||
return;
|
||||
}
|
||||
if (WindowConfig.alwaysOpenDevTools) {
|
||||
win.webContents.openDevTools({
|
||||
mode: "detach",
|
||||
activate: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
72
vendor/aigcpanel/electron/lib/env-main.ts
vendored
Normal file
72
vendor/aigcpanel/electron/lib/env-main.ts
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
import url, { fileURLToPath } from "node:url";
|
||||
import { BrowserView, BrowserWindow } from "electron";
|
||||
import { isPackaged } from "./env";
|
||||
import path, { join } from "node:path";
|
||||
import { Log } from "../mapi/log/main";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
process.env.APP_ROOT = path.join(__dirname, "../..");
|
||||
|
||||
export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
|
||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
|
||||
export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
|
||||
|
||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
||||
? path.join(process.env.APP_ROOT, "public")
|
||||
: RENDERER_DIST;
|
||||
|
||||
export const preloadDefault = path.join(MAIN_DIST, "preload/index.cjs");
|
||||
|
||||
export const rendererLoadPath = (
|
||||
window: BrowserWindow | BrowserView,
|
||||
fileName: string,
|
||||
) => {
|
||||
if (!isPackaged && process.env.VITE_DEV_SERVER_URL) {
|
||||
const x = new url.URL(rendererDistPath(fileName));
|
||||
if (window instanceof BrowserView) {
|
||||
window.webContents.loadURL(x.toString());
|
||||
} else {
|
||||
window.loadURL(x.toString());
|
||||
}
|
||||
} else {
|
||||
if (window instanceof BrowserView) {
|
||||
window.webContents.loadFile(rendererDistPath(fileName));
|
||||
} else {
|
||||
window.loadFile(rendererDistPath(fileName));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const rendererDistPath = (fileName: string) => {
|
||||
if (!isPackaged && process.env.VITE_DEV_SERVER_URL) {
|
||||
return `${process.env.VITE_DEV_SERVER_URL.replace(/\/+$/, "")}/${fileName}`;
|
||||
}
|
||||
return join(RENDERER_DIST, fileName);
|
||||
};
|
||||
|
||||
export const rendererIsUrl = (url: string) => {
|
||||
return url.startsWith("http://") || url.startsWith("https://");
|
||||
};
|
||||
|
||||
export const getGpuInfo = async () => {
|
||||
const list = [] as {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
}[];
|
||||
try {
|
||||
// @ts-ignore
|
||||
const si = await import("systeminformation");
|
||||
const graphics = await si.graphics();
|
||||
graphics.controllers.forEach((controller, index) => {
|
||||
const size = Math.ceil(controller.vram / 1024);
|
||||
let id = index + "";
|
||||
const name = controller.model;
|
||||
list.push({ id, name, size });
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error("getGpuInfo", e);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
169
vendor/aigcpanel/electron/lib/env.ts
vendored
Normal file
169
vendor/aigcpanel/electron/lib/env.ts
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
import { execSync } from "child_process";
|
||||
import { resolve } from "node:path";
|
||||
import fs from "node:fs";
|
||||
import os from "os";
|
||||
import { Log } from "../mapi/log";
|
||||
import FileIndex from "../mapi/file";
|
||||
|
||||
export const isPackaged = ["true"].includes(process.env.IS_PACKAGED);
|
||||
|
||||
export const isDev = !isPackaged;
|
||||
|
||||
export const isWin = process.platform === "win32";
|
||||
|
||||
export const isMac = process.platform === "darwin";
|
||||
|
||||
export const isLinux = process.platform === "linux";
|
||||
|
||||
export const isMain = process.type === "browser";
|
||||
|
||||
export const isRender = process.type === "renderer";
|
||||
|
||||
export const platformName = (): "win" | "osx" | "linux" | null => {
|
||||
if (isWin) return "win";
|
||||
if (isMac) return "osx";
|
||||
if (isLinux) return "linux";
|
||||
return null;
|
||||
};
|
||||
|
||||
export const memoryInfo = () => {
|
||||
return {
|
||||
total: os.totalmem(),
|
||||
free: os.freemem(),
|
||||
};
|
||||
};
|
||||
|
||||
const tryFirst = (functionList: (() => any)[]) => {
|
||||
for (const fun of functionList) {
|
||||
try {
|
||||
return fun();
|
||||
} catch (e) {}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let platformVersionCache: string | null = null;
|
||||
export const platformVersion = () => {
|
||||
if (null === platformVersionCache) {
|
||||
const functionList: any[] = [];
|
||||
if (isWin) {
|
||||
functionList.push(() =>
|
||||
execSync("wmic os get Version")
|
||||
.toString()
|
||||
.split("\n")[1]
|
||||
.trim(),
|
||||
);
|
||||
functionList.push(() =>
|
||||
execSync(
|
||||
"powershell -command \"(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ReleaseId\"",
|
||||
)
|
||||
.toString()
|
||||
.trim(),
|
||||
);
|
||||
} else if (isMac) {
|
||||
functionList.push(() =>
|
||||
execSync("sw_vers -productVersion").toString().trim(),
|
||||
);
|
||||
} else if (isLinux) {
|
||||
functionList.push(() =>
|
||||
execSync("cat /etc/os-release | grep VERSION_ID")
|
||||
.toString()
|
||||
.split("=")[1]
|
||||
.trim()
|
||||
.replace(/"/g, ""),
|
||||
);
|
||||
}
|
||||
platformVersionCache = tryFirst(functionList);
|
||||
if (!platformVersionCache) {
|
||||
Log.error("env.platformVersion.error");
|
||||
platformVersionCache = "0.0.0";
|
||||
}
|
||||
}
|
||||
return platformVersionCache;
|
||||
};
|
||||
|
||||
export const platformArch = (): "x86" | "arm64" | null => {
|
||||
switch (os.arch()) {
|
||||
case "x64":
|
||||
return "x86";
|
||||
case "arm64":
|
||||
return "arm64";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let platformUUIDCache: string | null = null;
|
||||
export const platformUUID = () => {
|
||||
if (null === platformUUIDCache) {
|
||||
const functionList: any[] = [];
|
||||
if (isWin) {
|
||||
functionList.push(() =>
|
||||
execSync("wmic csproduct get UUID")
|
||||
.toString()
|
||||
.split("\n")[1]
|
||||
.trim(),
|
||||
);
|
||||
functionList.push(() =>
|
||||
execSync(
|
||||
'powershell -command "(Get-WmiObject Win32_ComputerSystemProduct).UUID"',
|
||||
)
|
||||
.toString()
|
||||
.trim(),
|
||||
);
|
||||
} else if (isMac) {
|
||||
functionList.push(() =>
|
||||
execSync("system_profiler SPHardwareDataType | grep UUID")
|
||||
.toString()
|
||||
.split(": ")[1]
|
||||
.trim(),
|
||||
);
|
||||
} else if (isLinux) {
|
||||
functionList.push(() =>
|
||||
execSync("cat /var/lib/dbus/machine-id")
|
||||
.toString()
|
||||
.trim()
|
||||
.toUpperCase(),
|
||||
);
|
||||
}
|
||||
platformUUIDCache = tryFirst(functionList);
|
||||
if (!platformUUIDCache) {
|
||||
Log.error("env.platformUUID.error");
|
||||
platformUUIDCache = "000000";
|
||||
}
|
||||
}
|
||||
return platformUUIDCache;
|
||||
};
|
||||
|
||||
export const buildResolve = (value: string): string => {
|
||||
return resolve(`electron/resources/build/${value}`);
|
||||
};
|
||||
|
||||
export const binResolve = (value: string): string => {
|
||||
return resolve(process.resourcesPath, "bin", value);
|
||||
};
|
||||
|
||||
export const extraResolve = (filePath: string): string => {
|
||||
const basePath = isPackaged ? process.resourcesPath : "electron/resources";
|
||||
return resolve(basePath, "extra", filePath);
|
||||
};
|
||||
|
||||
export const extraResolveWithPlatform = (filePath: string): string => {
|
||||
const dir = [platformName(), platformArch()].join("-");
|
||||
const p = [dir, filePath].join("/");
|
||||
return extraResolve(p);
|
||||
};
|
||||
|
||||
export const extraResolveBin = (filePath: string): string => {
|
||||
if (isWin) {
|
||||
if (!filePath.endsWith(".exe")) {
|
||||
filePath += ".exe";
|
||||
}
|
||||
}
|
||||
const dir = [platformName(), platformArch()].join("-");
|
||||
const p = [dir, filePath].join("/");
|
||||
const binaryPath = extraResolve(p);
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
throw new Error(`Binary file not found: ${binaryPath}`);
|
||||
}
|
||||
return binaryPath;
|
||||
};
|
||||
52
vendor/aigcpanel/electron/lib/hooks.ts
vendored
Normal file
52
vendor/aigcpanel/electron/lib/hooks.ts
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import { BrowserView, BrowserWindow } from "electron";
|
||||
import { AppsMain } from "../mapi/app/main";
|
||||
|
||||
type HookType =
|
||||
| never
|
||||
| "Show"
|
||||
| "Hide"
|
||||
| "EnterFullScreen"
|
||||
| "LeaveFullScreen"
|
||||
| "ShowQuitConfirmDialog";
|
||||
|
||||
export const executeHooks = async (
|
||||
win: BrowserWindow,
|
||||
hook: HookType,
|
||||
data?: any,
|
||||
) => {
|
||||
const evalJs = `
|
||||
if(window.__page && window.__page.hooks && typeof window.__page.hooks.on${hook} === 'function' ) {
|
||||
try {
|
||||
window.__page.hooks.on${hook}(${JSON.stringify(data)});
|
||||
} catch(e) {
|
||||
console.log('executeHooks.on${hook}.error', e);
|
||||
}
|
||||
}`;
|
||||
return win.webContents?.executeJavaScript(evalJs);
|
||||
};
|
||||
|
||||
export const executeDarkMode = async (
|
||||
view: BrowserWindow | BrowserView,
|
||||
data: {
|
||||
isSystem: boolean;
|
||||
},
|
||||
) => {
|
||||
data = Object.assign(
|
||||
{
|
||||
isSystem: false,
|
||||
},
|
||||
data,
|
||||
);
|
||||
if (await AppsMain.shouldDarkMode()) {
|
||||
// body and html
|
||||
view.webContents.executeJavaScript(`
|
||||
document.body.setAttribute('data-theme', 'dark');
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
`);
|
||||
if (data.isSystem) {
|
||||
view.webContents.executeJavaScript(
|
||||
`document.body.setAttribute('arco-theme', 'dark');`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
42
vendor/aigcpanel/electron/lib/permission.ts
vendored
Normal file
42
vendor/aigcpanel/electron/lib/permission.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
import { isMac } from "./env";
|
||||
|
||||
let nodeMacPermissions = null;
|
||||
if (isMac) {
|
||||
(async () => {
|
||||
try {
|
||||
nodeMacPermissions = await import("node-mac-permissions");
|
||||
nodeMacPermissions = nodeMacPermissions.default;
|
||||
// console.log('nodeMacPermissions',nodeMacPermissions);
|
||||
} catch (e) {}
|
||||
})();
|
||||
}
|
||||
|
||||
export const Permissions = {
|
||||
async checkAccessibilityAccess(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isMac) {
|
||||
const status =
|
||||
nodeMacPermissions.getAuthStatus("accessibility");
|
||||
resolve(status === "authorized");
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
async askAccessibilityAccess() {
|
||||
nodeMacPermissions.askForAccessibilityAccess();
|
||||
},
|
||||
async checkScreenCaptureAccess(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isMac) {
|
||||
const status = nodeMacPermissions.getAuthStatus("screen");
|
||||
resolve(status === "authorized");
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
async askScreenCaptureAccess() {
|
||||
nodeMacPermissions.askForScreenCaptureAccess(true);
|
||||
},
|
||||
};
|
||||
28
vendor/aigcpanel/electron/lib/pinyin-util.ts
vendored
Normal file
28
vendor/aigcpanel/electron/lib/pinyin-util.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import PinyinMatch from "pinyin-match";
|
||||
|
||||
export const PinyinUtil = {
|
||||
match(input, keywords) {
|
||||
const index = PinyinMatch.match(input, keywords);
|
||||
let inputMark = input;
|
||||
let similarity = 0;
|
||||
if (index) {
|
||||
const indexStart = index[0];
|
||||
const indexEnd = index[1];
|
||||
inputMark =
|
||||
input.substring(0, indexStart) +
|
||||
"<mark>" +
|
||||
input.substring(indexStart, indexEnd + 1) +
|
||||
"</mark>" +
|
||||
input.substring(indexEnd + 1);
|
||||
similarity = (indexEnd - indexStart + 1) / input.length;
|
||||
}
|
||||
return {
|
||||
matched: !!index,
|
||||
inputMark,
|
||||
similarity,
|
||||
};
|
||||
},
|
||||
mark(text) {
|
||||
return `<mark>${text}</mark>`;
|
||||
},
|
||||
};
|
||||
17
vendor/aigcpanel/electron/lib/process.ts
vendored
Normal file
17
vendor/aigcpanel/electron/lib/process.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/** 在主进程中获取关键信息存储到环境变量中,从而在预加载脚本中及渲染进程中使用 */
|
||||
import { app } from "electron";
|
||||
|
||||
/** 注意: app.isPackaged 可能被被某些方法改变所以请将该文件放到 main.js 必须位于非依赖项的顶部 */
|
||||
import fixPath from "fix-path";
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
fixPath();
|
||||
}
|
||||
|
||||
process.env.IS_PACKAGED = String(app.isPackaged);
|
||||
|
||||
process.env.DESKTOP_PATH = app.getPath("desktop");
|
||||
|
||||
process.env.CWD = process.cwd();
|
||||
|
||||
export const isDummy = false;
|
||||
932
vendor/aigcpanel/electron/lib/util.ts
vendored
Normal file
932
vendor/aigcpanel/electron/lib/util.ts
vendored
Normal file
@@ -0,0 +1,932 @@
|
||||
import chardet from "chardet";
|
||||
import dayjs from "dayjs";
|
||||
import iconvLite from "iconv-lite";
|
||||
import { Base64 } from "js-base64";
|
||||
import * as crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Showdown from "showdown";
|
||||
// import {Iconv} from "iconv"
|
||||
import { isMac, isWin } from "./env";
|
||||
import FileIndex from "../mapi/file";
|
||||
|
||||
export const EncodeUtil = {
|
||||
base32Alphabet: "abcdefghijklmnopqrstuvwxyz234567",
|
||||
base32Encode(str: string) {
|
||||
const buffer = Buffer.from(str, "utf8");
|
||||
let bits = "";
|
||||
let output = "";
|
||||
// 将每个字节转为8位二进制
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
const byte = buffer[i];
|
||||
bits += byte.toString(2).padStart(8, "0");
|
||||
}
|
||||
// 每5位一组,转为 Base32 字符
|
||||
for (let i = 0; i < bits.length; i += 5) {
|
||||
const chunk = bits.slice(i, i + 5);
|
||||
const paddedChunk = chunk.padEnd(5, "0"); // 不足5位补0
|
||||
const index = parseInt(paddedChunk, 2);
|
||||
output += EncodeUtil.base32Alphabet[index];
|
||||
}
|
||||
return output;
|
||||
},
|
||||
base32Decode(str: string) {
|
||||
const base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
||||
let bits = "";
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str[i];
|
||||
const index = base32Alphabet.indexOf(char);
|
||||
if (index === -1) {
|
||||
throw new Error("Invalid Base32 character: " + char);
|
||||
}
|
||||
bits += index.toString(2).padStart(5, "0");
|
||||
}
|
||||
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i + 8 <= bits.length; i += 8) {
|
||||
const byte = bits.slice(i, i + 8);
|
||||
bytes.push(parseInt(byte, 2));
|
||||
}
|
||||
|
||||
return Buffer.from(bytes).toString("utf8");
|
||||
},
|
||||
base64Encode(str: string) {
|
||||
return Base64.encode(str);
|
||||
},
|
||||
base64Decode(str: string) {
|
||||
return Base64.decode(str);
|
||||
},
|
||||
md5(str: string) {
|
||||
return crypto.createHash("md5").update(str).digest("hex");
|
||||
},
|
||||
aesEncode(str: string, key: string) {
|
||||
const cipher = crypto.createCipheriv("aes-128-ecb", key, "");
|
||||
let crypted = cipher.update(str, "utf8", "base64");
|
||||
crypted += cipher.final("base64");
|
||||
return crypted;
|
||||
},
|
||||
aesDecode(str: string, key: string) {
|
||||
const decipher = crypto.createDecipheriv("aes-128-ecb", key, "");
|
||||
let dec = decipher.update(str, "base64", "utf8");
|
||||
dec += decipher.final("utf8");
|
||||
return dec;
|
||||
},
|
||||
async fileXzipEncode(pathname: string): Promise<string> {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
throw new Error(`Input file not found: ${pathname}`);
|
||||
}
|
||||
|
||||
// Generate new filepath with .xzip extension
|
||||
const basePath = pathname.substring(0, pathname.lastIndexOf("."));
|
||||
const outputPath = basePath + ".xzip";
|
||||
|
||||
// Get file info
|
||||
const fileStats = fs.statSync(pathname);
|
||||
const fileSize = fileStats.size;
|
||||
const fileExt = pathname.split(".").pop() || "";
|
||||
|
||||
// Generate random 16-character key
|
||||
const encryptionKey = StrUtil.randomString(16);
|
||||
|
||||
// Create metadata
|
||||
const filemeta = {
|
||||
version: 1,
|
||||
format: fileExt,
|
||||
size: fileSize,
|
||||
key: encryptionKey,
|
||||
};
|
||||
|
||||
// Convert metadata to JSON and then base64 encode
|
||||
const metaJson = JSON.stringify(filemeta);
|
||||
const metaB64 = Buffer.from(metaJson, "utf-8").toString("base64");
|
||||
const metaLength = metaB64.length;
|
||||
|
||||
// Prepare encryption key
|
||||
const keyBytes = Buffer.from(encryptionKey, "utf-8");
|
||||
const keyLength = keyBytes.length;
|
||||
|
||||
// Stream processing: read, encrypt and write in chunks
|
||||
const inputStream = fs.createReadStream(pathname);
|
||||
const outputStream = fs.createWriteStream(outputPath);
|
||||
|
||||
// Write metadata length (4 bytes, little-endian)
|
||||
const metaLengthBuffer = Buffer.allocUnsafe(4);
|
||||
metaLengthBuffer.writeUInt32LE(metaLength, 0);
|
||||
outputStream.write(metaLengthBuffer);
|
||||
|
||||
// Write base64 encoded metadata
|
||||
outputStream.write(Buffer.from(metaB64, "utf-8"));
|
||||
|
||||
// Stream encrypt the file content
|
||||
let bytesProcessed = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
inputStream.on("data", (chunk: Buffer) => {
|
||||
// XOR encrypt the chunk
|
||||
const encryptedChunk = Buffer.alloc(chunk.length);
|
||||
for (let i = 0; i < chunk.length; i++) {
|
||||
encryptedChunk[i] =
|
||||
chunk[i] ^ keyBytes[bytesProcessed % keyLength];
|
||||
bytesProcessed++;
|
||||
}
|
||||
|
||||
// Write encrypted chunk
|
||||
outputStream.write(encryptedChunk);
|
||||
});
|
||||
|
||||
inputStream.on("end", () => {
|
||||
outputStream.end();
|
||||
resolve(outputPath);
|
||||
});
|
||||
|
||||
inputStream.on("error", (error) => {
|
||||
outputStream.destroy();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
outputStream.on("error", (error) => {
|
||||
inputStream.destroy();
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
async fileXzipDecode(pathname: string): Promise<string> {
|
||||
if (!fs.existsSync(pathname)) {
|
||||
throw new Error(`Input file not found: ${pathname}`);
|
||||
}
|
||||
|
||||
if (!pathname.endsWith(".xzip")) {
|
||||
return pathname; // Not an xzip file, return as is
|
||||
}
|
||||
|
||||
let outputPath = pathname.replace(/\.xzip$/, "");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const inputStream = fs.createReadStream(pathname);
|
||||
let metadataRead = false;
|
||||
let filemeta: any = null;
|
||||
let keyBytes: Buffer;
|
||||
let bytesProcessed = 0;
|
||||
let outputStream: fs.WriteStream;
|
||||
let remainingMetaBytes = 0;
|
||||
let metaBuffer = Buffer.alloc(0);
|
||||
|
||||
inputStream.on("data", (chunk: Buffer) => {
|
||||
let chunkOffset = 0;
|
||||
|
||||
if (!metadataRead) {
|
||||
if (remainingMetaBytes === 0) {
|
||||
// Read metadata length (first 4 bytes)
|
||||
if (chunk.length < 4) {
|
||||
reject(
|
||||
new Error(
|
||||
"Invalid xzip file: insufficient data for metadata length",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const metaLength = chunk.readUInt32LE(0);
|
||||
remainingMetaBytes = metaLength;
|
||||
chunkOffset = 4;
|
||||
}
|
||||
|
||||
// Read metadata
|
||||
const availableMetaBytes = Math.min(
|
||||
remainingMetaBytes,
|
||||
chunk.length - chunkOffset,
|
||||
);
|
||||
const metaChunk = chunk.subarray(
|
||||
chunkOffset,
|
||||
chunkOffset + availableMetaBytes,
|
||||
);
|
||||
metaBuffer = Buffer.concat([
|
||||
metaBuffer,
|
||||
metaChunk,
|
||||
] as readonly Uint8Array[]);
|
||||
remainingMetaBytes -= availableMetaBytes;
|
||||
chunkOffset += availableMetaBytes;
|
||||
|
||||
if (remainingMetaBytes === 0) {
|
||||
// Parse metadata
|
||||
try {
|
||||
const metaB64 = metaBuffer.toString("utf-8");
|
||||
const metaJson = Buffer.from(
|
||||
metaB64,
|
||||
"base64",
|
||||
).toString("utf-8");
|
||||
filemeta = JSON.parse(metaJson);
|
||||
keyBytes = Buffer.from(filemeta.key, "utf-8");
|
||||
|
||||
// Create output file with correct extension
|
||||
const finalOutputPath =
|
||||
outputPath +
|
||||
(filemeta.format ? "." + filemeta.format : "");
|
||||
outputStream =
|
||||
fs.createWriteStream(finalOutputPath);
|
||||
|
||||
metadataRead = true;
|
||||
|
||||
// Set the final output path for resolution
|
||||
outputPath = finalOutputPath;
|
||||
} catch (error) {
|
||||
reject(
|
||||
new Error(
|
||||
"Invalid xzip file: corrupted metadata",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataRead && chunkOffset < chunk.length) {
|
||||
// Decrypt remaining chunk data
|
||||
const encryptedChunk = chunk.subarray(chunkOffset);
|
||||
const decryptedChunk = Buffer.alloc(encryptedChunk.length);
|
||||
const keyLength = keyBytes.length;
|
||||
|
||||
for (let i = 0; i < encryptedChunk.length; i++) {
|
||||
decryptedChunk[i] =
|
||||
encryptedChunk[i] ^
|
||||
keyBytes[bytesProcessed % keyLength];
|
||||
bytesProcessed++;
|
||||
}
|
||||
|
||||
outputStream.write(decryptedChunk);
|
||||
}
|
||||
});
|
||||
|
||||
inputStream.on("end", () => {
|
||||
if (outputStream) {
|
||||
outputStream.end();
|
||||
resolve(outputPath);
|
||||
} else {
|
||||
reject(new Error("Invalid xzip file: incomplete metadata"));
|
||||
}
|
||||
});
|
||||
|
||||
inputStream.on("error", (error) => {
|
||||
if (outputStream) {
|
||||
outputStream.destroy();
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (outputStream) {
|
||||
outputStream.on("error", (error) => {
|
||||
inputStream.destroy();
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const IconvUtil = {
|
||||
convert(str: string, to?: string, from?: string) {
|
||||
if (!from) {
|
||||
from = chardet.detect(Buffer.from(str));
|
||||
}
|
||||
to = to || "utf8";
|
||||
const buffer = iconvLite.encode(str, from);
|
||||
return iconvLite.decode(buffer, to);
|
||||
},
|
||||
bufferToUtf8(buffer: Buffer) {
|
||||
const encoding = chardet.detect(buffer);
|
||||
// if ('ISO-2022-CN' === encoding) {
|
||||
// const iconvInstance = new Iconv('ISO-2022-CN', 'UTF-8//TRANSLIT//IGNORE');
|
||||
// return iconvInstance.convert(buffer).toString()
|
||||
// }
|
||||
return iconvLite.decode(buffer, encoding).toString();
|
||||
},
|
||||
detect(buffer: Uint8Array) {
|
||||
// detect str encoding
|
||||
return chardet.detect(buffer);
|
||||
},
|
||||
};
|
||||
|
||||
export const StrUtil = {
|
||||
randomString(len: number = 32) {
|
||||
const chars =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let result = "";
|
||||
for (let i = len; i > 0; --i) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
uuid() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
||||
/[xy]/g,
|
||||
function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
},
|
||||
);
|
||||
},
|
||||
hashCode(str: string, length: number = 8) {
|
||||
let hash = 0;
|
||||
if (str.length === 0) return hash + "";
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
let result = Math.abs(hash).toString(16);
|
||||
if (result.length < length) {
|
||||
result = "0".repeat(length - result.length) + result;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
hashCodeWithDuplicateCheck(
|
||||
str: string,
|
||||
check: string[],
|
||||
length: number = 8,
|
||||
) {
|
||||
let code = this.hashCode(str, length);
|
||||
while (check.includes(code)) {
|
||||
code = this.uuid().substring(0, length);
|
||||
}
|
||||
return code;
|
||||
},
|
||||
bigIntegerId() {
|
||||
return [
|
||||
Date.now(),
|
||||
(Math.floor(Math.random() * 1000000) + "").padStart(6, "0"),
|
||||
].join("");
|
||||
},
|
||||
ucFirst(str: string) {
|
||||
if (!str) return "";
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
},
|
||||
};
|
||||
|
||||
export const TimeUtil = {
|
||||
timestampInMs() {
|
||||
return Date.now();
|
||||
},
|
||||
timestamp() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
},
|
||||
format(time: number, format: string = "YYYY-MM-DD HH:mm:ss") {
|
||||
return dayjs(time).format(format);
|
||||
},
|
||||
formatDate(time: number) {
|
||||
return dayjs(time).format("YYYY-MM-DD");
|
||||
},
|
||||
dateString() {
|
||||
return dayjs().format("YYYYMMDD");
|
||||
},
|
||||
datetimeString() {
|
||||
return dayjs().format("YYYYMMDD_HHmmss_SSS");
|
||||
},
|
||||
timestampDayStart(msTimestamp?: number) {
|
||||
let date = msTimestamp ? new Date(msTimestamp) : new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
},
|
||||
replacePattern(text: string) {
|
||||
// @ts-ignore
|
||||
return text
|
||||
.replaceAll("{year}", dayjs().format("YYYY"))
|
||||
.replaceAll("{month}", dayjs().format("MM"))
|
||||
.replaceAll("{day}", dayjs().format("DD"))
|
||||
.replaceAll("{hour}", dayjs().format("HH"))
|
||||
.replaceAll("{minute}", dayjs().format("mm"))
|
||||
.replaceAll("{second}", dayjs().format("ss"));
|
||||
},
|
||||
};
|
||||
|
||||
export const FileUtil = {
|
||||
MIME_TYPES: {
|
||||
html: "text/html",
|
||||
htm: "text/html",
|
||||
js: "application/javascript",
|
||||
css: "text/css",
|
||||
json: "application/json",
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
svg: "image/svg+xml",
|
||||
webp: "image/webp",
|
||||
woff: "font/woff",
|
||||
woff2: "font/woff2",
|
||||
ttf: "font/ttf",
|
||||
otf: "font/otf",
|
||||
mp3: "audio/mpeg",
|
||||
mp4: "video/mp4",
|
||||
wav: "audio/wav",
|
||||
wasm: "application/wasm",
|
||||
eot: "application/vnd.ms-fontobject",
|
||||
},
|
||||
getMimeByExt(ext: string, defaultMime: string = ""): string {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext.startsWith(".")) {
|
||||
ext = ext.substring(1);
|
||||
}
|
||||
return FileUtil.MIME_TYPES[ext] || defaultMime;
|
||||
},
|
||||
getMimeByPath(p: string, defaultMime: string = ""): string {
|
||||
const extension = p.split(".").pop().toLowerCase();
|
||||
return FileUtil.getMimeByExt(extension, defaultMime);
|
||||
},
|
||||
streamToBase64(stream: NodeJS.ReadableStream): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
stream.on("data", (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on("end", () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
resolve(buffer.toString("base64"));
|
||||
});
|
||||
stream.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
bufferToBase64(buffer: Buffer) {
|
||||
let binary = "";
|
||||
let bytes = new Uint8Array(buffer);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return EncodeUtil.base64Encode(binary);
|
||||
},
|
||||
base64ToBuffer(base64: string): Buffer {
|
||||
if (base64.startsWith("data:")) {
|
||||
base64 = base64.split("base64,")[1];
|
||||
}
|
||||
return Buffer.from(base64, "base64");
|
||||
},
|
||||
formatSize(size: number) {
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + "KB";
|
||||
} else if (size < 1024 * 1024 * 1024) {
|
||||
return (size / 1024 / 1024).toFixed(2) + "MB";
|
||||
} else {
|
||||
return (size / 1024 / 1024 / 1024).toFixed(2) + "GB";
|
||||
}
|
||||
},
|
||||
async md5(filePath: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash("md5");
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on("data", (data) => {
|
||||
hash.update(data);
|
||||
});
|
||||
stream.on("end", () => {
|
||||
resolve(hash.digest("hex"));
|
||||
});
|
||||
stream.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const JsonUtil = {
|
||||
stringifyOrdered(obj: any) {
|
||||
return JSON.stringify(obj, Object.keys(obj).sort(), 4);
|
||||
},
|
||||
stringifyValueOrdered(obj: any) {
|
||||
const sortedData = Object.fromEntries(
|
||||
Object.entries(obj).sort(([, a], [, b]) => {
|
||||
// @ts-ignore
|
||||
return ((a as any) - b) as any;
|
||||
}),
|
||||
);
|
||||
return JSON.stringify(sortedData, null, 4);
|
||||
},
|
||||
};
|
||||
|
||||
export const ImportUtil = {
|
||||
async loadCommonJs(cjsPath: string, forceReload: boolean = true) {
|
||||
let tempPath = cjsPath;
|
||||
if (forceReload) {
|
||||
const md5 = await FileUtil.md5(cjsPath);
|
||||
tempPath = path.join(
|
||||
await FileIndex.tempDir("commonJs"),
|
||||
`${md5}.cjs`,
|
||||
);
|
||||
if (!fs.existsSync(tempPath)) {
|
||||
fs.copyFileSync(cjsPath, tempPath);
|
||||
}
|
||||
}
|
||||
const backend = await import(/* @vite-ignore */ `file://${tempPath}`);
|
||||
// console.log('loadCommonJs', `${cjsPath}?t=${md5}`)
|
||||
return backend.default;
|
||||
},
|
||||
};
|
||||
|
||||
export const MemoryCacheUtil = {
|
||||
pool: {} as {
|
||||
[key: string]: {
|
||||
value: any;
|
||||
expire: number;
|
||||
};
|
||||
},
|
||||
_gc() {
|
||||
const now = TimeUtil.timestamp();
|
||||
for (const key in this.pool) {
|
||||
if (this.pool[key].expire < now) {
|
||||
delete this.pool[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
async remember(
|
||||
key: string,
|
||||
callback: () => Promise<any>,
|
||||
ttl: number = 60,
|
||||
) {
|
||||
if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {
|
||||
return this.pool[key].value;
|
||||
}
|
||||
const value = await callback();
|
||||
this.pool[key] = {
|
||||
value,
|
||||
expire: TimeUtil.timestamp() + ttl,
|
||||
};
|
||||
this._gc();
|
||||
return value;
|
||||
},
|
||||
get(key: string) {
|
||||
if (this.pool[key] && this.pool[key].expire > TimeUtil.timestamp()) {
|
||||
return this.pool[key].value;
|
||||
}
|
||||
this._gc();
|
||||
return null;
|
||||
},
|
||||
set(key: string, value: any, ttl: number = 86400) {
|
||||
this.pool[key] = {
|
||||
value,
|
||||
expire: TimeUtil.timestamp() + ttl,
|
||||
};
|
||||
this._gc();
|
||||
},
|
||||
forget(key: string) {
|
||||
delete this.pool[key];
|
||||
},
|
||||
};
|
||||
|
||||
export const MemoryMapCacheUtil = {
|
||||
pool: {} as {
|
||||
[group: string]: {
|
||||
[key: string]: {
|
||||
value: any;
|
||||
expire: number;
|
||||
};
|
||||
};
|
||||
},
|
||||
_gc() {
|
||||
const now = TimeUtil.timestamp();
|
||||
for (const group in this.pool) {
|
||||
for (const key in this.pool[group]) {
|
||||
if (this.pool[group][key].expire < now) {
|
||||
delete this.pool[group][key];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
get(group: string, key: string) {
|
||||
if (
|
||||
this.pool[group] &&
|
||||
this.pool[group][key] &&
|
||||
this.pool[group][key].expire > TimeUtil.timestamp()
|
||||
) {
|
||||
return this.pool[group][key].value;
|
||||
}
|
||||
this._gc();
|
||||
return null;
|
||||
},
|
||||
set(group: string, key: string, value: any, ttl: number = 86400) {
|
||||
if (!this.pool[group]) {
|
||||
this.pool[group] = {};
|
||||
}
|
||||
this.pool[group][key] = {
|
||||
value,
|
||||
expire: TimeUtil.timestamp() + ttl,
|
||||
};
|
||||
this._gc();
|
||||
},
|
||||
forget(group: string, key: string) {
|
||||
if (this.pool[group]) {
|
||||
delete this.pool[group][key];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const ShellUtil = {
|
||||
quotaPath(p: string) {
|
||||
return `"${p}"`;
|
||||
},
|
||||
parseCommandArgs(command: string) {
|
||||
let args = [];
|
||||
let arg = "";
|
||||
let quote = "";
|
||||
let escape = false;
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
const c = command[i];
|
||||
if (escape) {
|
||||
arg += c;
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if ("\\" === c) {
|
||||
escape = true;
|
||||
arg += c;
|
||||
continue;
|
||||
}
|
||||
if ("" === quote && (" " === c || "\t" === c)) {
|
||||
if (arg) {
|
||||
args.push(arg);
|
||||
arg = "";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ("" === quote && ('"' === c || "'" === c)) {
|
||||
quote = c;
|
||||
arg += c;
|
||||
continue;
|
||||
}
|
||||
if ('"' === quote && '"' === c) {
|
||||
quote = "";
|
||||
arg += c;
|
||||
continue;
|
||||
}
|
||||
if ("'" === quote && "'" === c) {
|
||||
quote = "";
|
||||
arg += c;
|
||||
continue;
|
||||
}
|
||||
arg += c;
|
||||
}
|
||||
if (arg) {
|
||||
args.push(arg);
|
||||
}
|
||||
return args;
|
||||
},
|
||||
};
|
||||
|
||||
export const VersionUtil = {
|
||||
/**
|
||||
* 检测版本是否匹配
|
||||
* @param v string
|
||||
* @param match string 如 * 或 >=1.0.0 或 >1.0.0 或 <1.0.0 或 <=1.0.0 或 1.0.0
|
||||
*/
|
||||
match(v: string, match: string) {
|
||||
if (match === "*") {
|
||||
return true;
|
||||
}
|
||||
if (match.startsWith(">=")) {
|
||||
if (this.ge(v, match.substring(2))) {
|
||||
return true;
|
||||
}
|
||||
} else if (match.startsWith("<=")) {
|
||||
if (this.le(v, match.substring(2))) {
|
||||
return true;
|
||||
}
|
||||
} else if (match.startsWith(">")) {
|
||||
if (this.gt(v, match.substring(1))) {
|
||||
return true;
|
||||
}
|
||||
} else if (match.startsWith("<")) {
|
||||
if (this.lt(v, match.substring(1))) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return this.eq(v, match);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
compare(v1: string, v2: string) {
|
||||
const v1Arr = v1.split(".");
|
||||
const v2Arr = v2.split(".");
|
||||
for (let i = 0; i < v1Arr.length; i++) {
|
||||
const v1Num = parseInt(v1Arr[i]);
|
||||
const v2Num = parseInt(v2Arr[i]);
|
||||
if (v1Num > v2Num) {
|
||||
return 1;
|
||||
} else if (v1Num < v2Num) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
gt(v1: string, v2: string) {
|
||||
return VersionUtil.compare(v1, v2) > 0;
|
||||
},
|
||||
ge(v1: string, v2: string) {
|
||||
return VersionUtil.compare(v1, v2) >= 0;
|
||||
},
|
||||
lt(v1: string, v2: string) {
|
||||
return VersionUtil.compare(v1, v2) < 0;
|
||||
},
|
||||
le: (v1: string, v2: string) => {
|
||||
return VersionUtil.compare(v1, v2) <= 0;
|
||||
},
|
||||
eq: (v1: string, v2: string) => {
|
||||
return VersionUtil.compare(v1, v2) === 0;
|
||||
},
|
||||
};
|
||||
|
||||
export const UIUtil = {
|
||||
sizeToPx(size: string, sizeFull: number) {
|
||||
if (/^\d+$/.test(size)) {
|
||||
// 纯数字
|
||||
return parseInt(size);
|
||||
} else if (size.endsWith("%")) {
|
||||
// 百分比
|
||||
let result = Math.floor((sizeFull * parseInt(size)) / 100);
|
||||
result = Math.min(result, sizeFull);
|
||||
return result;
|
||||
} else {
|
||||
throw "UnsupportSizeString";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const ReUtil = {
|
||||
match(regex: string, text: string) {
|
||||
if ("" === regex || null === regex) {
|
||||
return false;
|
||||
}
|
||||
if (regex.startsWith("/")) {
|
||||
const index = regex.lastIndexOf("/");
|
||||
const source = regex.slice(1, index);
|
||||
const flags = regex.slice(index + 1);
|
||||
return new RegExp(source, flags).test(text);
|
||||
}
|
||||
return new RegExp(regex).test(text);
|
||||
},
|
||||
};
|
||||
|
||||
const converter = new Showdown.Converter({
|
||||
tables: true,
|
||||
});
|
||||
export const MarkdownUtil = {
|
||||
toHtml(markdown: string): string {
|
||||
return converter.makeHtml(markdown);
|
||||
},
|
||||
};
|
||||
|
||||
type HotkeyModifierType =
|
||||
| "Control"
|
||||
| "Option"
|
||||
| "Command"
|
||||
| "Ctrl"
|
||||
| "Alt"
|
||||
| "Win"
|
||||
| "Meta"
|
||||
| "Shift";
|
||||
type HotkeyType = { key: string; modifiers: HotkeyModifierType[] };
|
||||
|
||||
export const HotKeyUtil = {
|
||||
orderModifiers(modifiers: HotkeyModifierType[]) {
|
||||
const order = [
|
||||
"Control",
|
||||
"Ctrl",
|
||||
"Command",
|
||||
"Meta",
|
||||
"Win",
|
||||
"Option",
|
||||
"Alt",
|
||||
"Shift",
|
||||
];
|
||||
return modifiers.sort((a, b) => {
|
||||
return order.indexOf(a) - order.indexOf(b);
|
||||
});
|
||||
},
|
||||
unifyObject(hotkey: HotkeyType) {
|
||||
return {
|
||||
key: hotkey.key.toUpperCase(),
|
||||
modifiers: this.orderModifiers(
|
||||
hotkey.modifiers.map((modifier) => StrUtil.ucFirst(modifier)),
|
||||
),
|
||||
};
|
||||
},
|
||||
unifyString(hotkey: string): HotkeyType {
|
||||
const parts = hotkey.split("+");
|
||||
const key = parts.pop() || "";
|
||||
const modifiers: any[] = [];
|
||||
parts.forEach((part) => {
|
||||
modifiers.push(StrUtil.ucFirst(part.trim()));
|
||||
});
|
||||
return this.unifyObject({ key, modifiers });
|
||||
},
|
||||
unify(
|
||||
hotkeys: string | string[] | HotkeyType | HotkeyType[],
|
||||
): HotkeyType[] {
|
||||
if (typeof hotkeys === "string") {
|
||||
return [this.unifyString(hotkeys)];
|
||||
} else if (Array.isArray(hotkeys)) {
|
||||
return hotkeys.map((hotkey) => {
|
||||
if (typeof hotkey === "string") {
|
||||
return this.unifyString(hotkey);
|
||||
} else {
|
||||
return this.unifyObject(hotkey);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return [this.unifyObject(hotkeys)];
|
||||
}
|
||||
},
|
||||
getFromEvent(event: any): HotkeyType | null {
|
||||
const valid = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"0",
|
||||
"Space",
|
||||
];
|
||||
const key = (event.key || "").toUpperCase();
|
||||
if (!event || !event.key || !valid.includes(key)) {
|
||||
return null;
|
||||
}
|
||||
const modifiers: HotkeyModifierType[] = [];
|
||||
if (isWin) {
|
||||
if (event.ctrlKey || event.control) {
|
||||
modifiers.push("Ctrl");
|
||||
}
|
||||
if (event.altKey || event.alt) {
|
||||
modifiers.push("Alt");
|
||||
}
|
||||
if (event.metaKey || event.meta) {
|
||||
modifiers.push("Win");
|
||||
}
|
||||
} else if (isMac) {
|
||||
if (event.ctrlKey || event.control) {
|
||||
modifiers.push("Control");
|
||||
}
|
||||
if (event.altKey || event.alt) {
|
||||
modifiers.push("Option");
|
||||
}
|
||||
if (event.metaKey || event.meta) {
|
||||
modifiers.push("Command");
|
||||
}
|
||||
} else {
|
||||
if (event.ctrlKey || event.control) {
|
||||
modifiers.push("Ctrl");
|
||||
}
|
||||
if (event.altKey || event.alt) {
|
||||
modifiers.push("Alt");
|
||||
}
|
||||
if (event.metaKey || event.meta) {
|
||||
modifiers.push("Meta");
|
||||
}
|
||||
}
|
||||
if (event.shiftKey || event.shift) {
|
||||
modifiers.push("Shift");
|
||||
}
|
||||
return this.unifyObject({ key, modifiers });
|
||||
},
|
||||
match(hotkeysForMatch: HotkeyType[], hotkey: HotkeyType): boolean {
|
||||
if (!hotkeysForMatch || !hotkey) {
|
||||
return false;
|
||||
}
|
||||
const hotKeyStr = hotkey.modifiers.join("+") + "+" + hotkey.key;
|
||||
for (const key of hotkeysForMatch) {
|
||||
const keyStr = key.modifiers.join("+") + "+" + key.key;
|
||||
if (keyStr === hotKeyStr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user