新增 pc-client:畅联 PC 端工程(Electron+React,按确认效果图改造,B-59)
- 基于 openim-electron-demo 改造:工号+密码登录(自研账号服务)、会话列表/聊天窗/通讯录界面重做
- 文字/语音/文件/图片消息、文件拖拽发送、表情面板、一对一语音通话(LiveKit)
- RTC 令牌对接账号服务带鉴权版 /api/rtc_token(Bearer imToken + {room,identity})
- account-service: CORS Allow-Headers 补 authorization(浏览器/渲染进程跨域调 rtc_token 需要)
- 附本地 mock 账号服务(scripts/mock-account-server.js)与截图联调脚本
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
export const IpcMainToRender = {
|
||||
appResume: "appResume",
|
||||
};
|
||||
|
||||
export const IpcRenderToMain = {
|
||||
showMainWindow: "showMainWindow",
|
||||
clearSession: "clearSession",
|
||||
minimizeWindow: "minimizeWindow",
|
||||
maxmizeWindow: "maxmizeWindow",
|
||||
closeWindow: "closeWindow",
|
||||
showMessageBox: "showMessageBox",
|
||||
setKeyStore: "setKeyStore",
|
||||
getKeyStore: "getKeyStore",
|
||||
getKeyStoreSync: "getKeyStoreSync",
|
||||
showInputContextMenu: "showInputContextMenu",
|
||||
getDataPath: "getDataPath",
|
||||
};
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite-electron-plugin/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
VSCODE_DEBUG?: "true";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import i18n from "i18next";
|
||||
|
||||
import { getStore } from "../main/storeManage";
|
||||
import { app } from "electron";
|
||||
|
||||
import translation_en from "./resources/en-US";
|
||||
import translation_zh from "./resources/zh-CN";
|
||||
|
||||
const store = getStore();
|
||||
|
||||
export const initI18n = () => {
|
||||
const systemLanguage = app.getLocale();
|
||||
const language = store.get("language", systemLanguage) as string;
|
||||
|
||||
const resources = {
|
||||
"en-US": {
|
||||
translation: translation_en,
|
||||
},
|
||||
"zh-CN": {
|
||||
translation: translation_zh,
|
||||
},
|
||||
zh: {
|
||||
translation: translation_zh,
|
||||
},
|
||||
};
|
||||
|
||||
i18n.init(
|
||||
{
|
||||
resources,
|
||||
lng: language,
|
||||
fallbackLng: "zh-CN",
|
||||
},
|
||||
(err) => {
|
||||
if (err) return console.error("Error loading i18n resources:", err);
|
||||
console.log("i18n resources loaded successfully");
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const changeLanguage = i18n.changeLanguage;
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
system: {
|
||||
showWindow: "ShowWindow",
|
||||
hideWindow: "HideWindow",
|
||||
hide: "Hide",
|
||||
about: "About",
|
||||
quit: "Quit",
|
||||
window: "Window",
|
||||
toggleDevTools: "ToggleDevTools",
|
||||
minimize: "Minimize",
|
||||
close: "Close",
|
||||
copy: "Copy",
|
||||
paste: "Paste",
|
||||
cut: "Cut",
|
||||
undo: "Undo",
|
||||
redo: "Redo",
|
||||
selectAll: "SelectAll",
|
||||
fastKeys: "FastKeys",
|
||||
magnifier_position_label:"Position"
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
system: {
|
||||
showWindow: "显示",
|
||||
hideWindow: "隐藏",
|
||||
hide: "隐藏",
|
||||
about: "关于",
|
||||
quit: "退出",
|
||||
window: "窗口",
|
||||
toggleDevTools: "调试",
|
||||
minimize: "最小化",
|
||||
close: "关闭",
|
||||
copy: "复制",
|
||||
paste: "粘贴",
|
||||
cut: "剪切",
|
||||
undo: "撤销",
|
||||
redo: "重做",
|
||||
selectAll: "全选",
|
||||
fastKeys: "快键键",
|
||||
magnifier_position_label: "坐标",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { app, powerMonitor } from "electron";
|
||||
import { isExistMainWindow, sendEvent, showWindow } from "./windowManage";
|
||||
import { join } from "node:path";
|
||||
import fs from "fs";
|
||||
import { isMac, isProd, isWin } from "../utils";
|
||||
import { getStore } from "./storeManage";
|
||||
import { IpcMainToRender } from "../constants";
|
||||
import { logger } from ".";
|
||||
|
||||
const store = getStore();
|
||||
|
||||
export const setSingleInstance = () => {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.on("second-instance", () => {
|
||||
showWindow();
|
||||
});
|
||||
};
|
||||
|
||||
export const setAppListener = (startApp: () => void) => {
|
||||
app.on("activate", () => {
|
||||
if (isExistMainWindow()) {
|
||||
showWindow();
|
||||
} else {
|
||||
startApp();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (isMac && !getIsForceQuit()) return;
|
||||
|
||||
app.quit();
|
||||
});
|
||||
|
||||
powerMonitor.on("suspend", () => {
|
||||
logger.debug("app suspend");
|
||||
});
|
||||
|
||||
powerMonitor.on("resume", () => {
|
||||
logger.debug("app resume");
|
||||
sendEvent(IpcMainToRender.appResume);
|
||||
});
|
||||
};
|
||||
|
||||
export const setAppGlobalData = () => {
|
||||
const electronDistPath = join(__dirname, "../");
|
||||
const distPath = join(electronDistPath, "../dist");
|
||||
const publicPath = isProd ? distPath : join(electronDistPath, "../public");
|
||||
const asarPath = process.resourcesPath;
|
||||
|
||||
global.pathConfig = {
|
||||
electronDistPath,
|
||||
distPath,
|
||||
publicPath,
|
||||
asarPath,
|
||||
logsPath: join(app.getPath("userData"), `/OpenIMData/logs`),
|
||||
sdkResourcesPath: join(app.getPath("userData"), `/OpenIMData/sdkResources`),
|
||||
imsdkLibPath: isProd
|
||||
? join(
|
||||
asarPath,
|
||||
"/app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets",
|
||||
)
|
||||
: join(__dirname, "../../node_modules/@openim/electron-client-sdk/assets"),
|
||||
trayIcon: join(publicPath, `/icons/${isWin ? "icon.ico" : "tray.png"}`),
|
||||
emptyTrayIcon: join(publicPath, `/icons/${"empty_tray.png"}`),
|
||||
indexHtml: join(distPath, "index.html"),
|
||||
splashHtml: join(distPath, "splash.html"),
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
};
|
||||
|
||||
if (isProd) {
|
||||
fs.promises
|
||||
.readdir(global.pathConfig.logsPath)
|
||||
.catch(
|
||||
(err) =>
|
||||
err.code === "ENOENT" &&
|
||||
fs.promises.mkdir(global.pathConfig.logsPath, { recursive: true }),
|
||||
);
|
||||
fs.promises
|
||||
.readdir(global.pathConfig.sdkResourcesPath)
|
||||
.catch(
|
||||
(err) =>
|
||||
err.code === "ENOENT" &&
|
||||
fs.promises.mkdir(global.pathConfig.sdkResourcesPath, { recursive: true }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsForceQuit = () =>
|
||||
store.get("closeAction") === "quit" || global.forceQuit;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { app } from "electron";
|
||||
import { join } from "node:path";
|
||||
import { createMainWindow } from "./windowManage";
|
||||
import { createTray } from "./trayManage";
|
||||
import { setIpcMainListener } from "./ipcHandlerManage";
|
||||
import { setAppGlobalData, setAppListener, setSingleInstance } from "./appManage";
|
||||
import createAppMenu from "./menuManage";
|
||||
import { isLinux } from "../utils";
|
||||
import { getLogger } from "../utils/log";
|
||||
import { initI18n } from "../i18n";
|
||||
|
||||
export const logger = getLogger(join(app.getPath("userData"), `/OpenIMData/logs`));
|
||||
|
||||
const init = () => {
|
||||
initI18n();
|
||||
createMainWindow();
|
||||
createAppMenu();
|
||||
createTray();
|
||||
};
|
||||
|
||||
setAppGlobalData();
|
||||
setIpcMainListener();
|
||||
setSingleInstance();
|
||||
setAppListener(init);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
isLinux ? setTimeout(init, 300) : init();
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { BrowserWindow, Menu, app, dialog, ipcMain } from "electron";
|
||||
import {
|
||||
clearCache,
|
||||
closeWindow,
|
||||
minimize,
|
||||
showWindow,
|
||||
splashEnd,
|
||||
updateMaximize,
|
||||
} from "./windowManage";
|
||||
import { t } from "i18next";
|
||||
import { IpcRenderToMain } from "../constants";
|
||||
import { getStore } from "./storeManage";
|
||||
import { changeLanguage } from "../i18n";
|
||||
|
||||
const store = getStore();
|
||||
|
||||
export const setIpcMainListener = () => {
|
||||
ipcMain.handle(IpcRenderToMain.clearSession, () => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
// window manage
|
||||
ipcMain.handle("changeLanguage", (_, locale) => {
|
||||
store.set("language", locale);
|
||||
changeLanguage(locale).then(() => {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
});
|
||||
ipcMain.handle("main-win-ready", () => {
|
||||
splashEnd();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.showMainWindow, () => {
|
||||
showWindow();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.minimizeWindow, () => {
|
||||
minimize();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.maxmizeWindow, () => {
|
||||
updateMaximize();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.closeWindow, () => {
|
||||
closeWindow();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.showMessageBox, (_, options) => {
|
||||
return dialog
|
||||
.showMessageBox(BrowserWindow.getFocusedWindow(), options)
|
||||
.then((res) => res.response);
|
||||
});
|
||||
|
||||
// data transfer
|
||||
ipcMain.handle(IpcRenderToMain.setKeyStore, (_, { key, data }) => {
|
||||
store.set(key, data);
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.getKeyStore, (_, { key }) => {
|
||||
return store.get(key);
|
||||
});
|
||||
ipcMain.on(IpcRenderToMain.getKeyStoreSync, (e, { key }) => {
|
||||
e.returnValue = store.get(key);
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.showInputContextMenu, () => {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: t("system.copy"),
|
||||
type: "normal",
|
||||
role: "copy",
|
||||
accelerator: "CommandOrControl+c",
|
||||
},
|
||||
{
|
||||
label: t("system.paste"),
|
||||
type: "normal",
|
||||
role: "paste",
|
||||
accelerator: "CommandOrControl+v",
|
||||
},
|
||||
{
|
||||
label: t("system.selectAll"),
|
||||
type: "normal",
|
||||
role: "selectAll",
|
||||
accelerator: "CommandOrControl+a",
|
||||
},
|
||||
]);
|
||||
menu.popup({
|
||||
window: BrowserWindow.getFocusedWindow()!,
|
||||
});
|
||||
});
|
||||
ipcMain.on(IpcRenderToMain.getDataPath, (e, key: string) => {
|
||||
switch (key) {
|
||||
case "public":
|
||||
e.returnValue = global.pathConfig.publicPath;
|
||||
break;
|
||||
case "sdkResources":
|
||||
e.returnValue = global.pathConfig.sdkResourcesPath;
|
||||
break;
|
||||
case "logsPath":
|
||||
e.returnValue = global.pathConfig.logsPath;
|
||||
break;
|
||||
default:
|
||||
e.returnValue = global.pathConfig.publicPath;
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { app, Menu } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { isMac } from "../utils";
|
||||
|
||||
const createAppMenu = () => {
|
||||
if (isMac) {
|
||||
const template: Electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{ label: t("system.about"), role: "about" },
|
||||
{ type: "separator" },
|
||||
{ label: t("system.hide"), role: "hide" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: t("system.quit"),
|
||||
click: () => {
|
||||
global.forceQuit = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("system.fastKeys"),
|
||||
submenu: [
|
||||
{ label: t("system.copy"), role: "copy", accelerator: "CmdOrCtrl+C" },
|
||||
{ label: t("system.paste"), role: "paste", accelerator: "CmdOrCtrl+V" },
|
||||
{ label: t("system.cut"), role: "cut", accelerator: "CmdOrCtrl+X" },
|
||||
{ label: t("system.undo"), role: "undo", accelerator: "CmdOrCtrl+Z" },
|
||||
{ label: t("system.redo"), role: "redo", accelerator: "CmdOrCtrl+Y" },
|
||||
{
|
||||
label: t("system.selectAll"),
|
||||
role: "selectAll",
|
||||
accelerator: "CmdOrCtrl+A",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("system.window"),
|
||||
role: "window",
|
||||
submenu: [
|
||||
{ label: t("system.minimize"), role: "minimize", accelerator: "CmdOrCtrl+W" },
|
||||
{ label: t("system.close"), role: "close" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
} else {
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
export default createAppMenu;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import { toggleDevTools } from "./windowManage";
|
||||
|
||||
export const registerShortcuts = () => {
|
||||
globalShortcut.register("CmdOrCtrl+F12", toggleDevTools);
|
||||
};
|
||||
|
||||
export const unregisterShortcuts = () => {
|
||||
globalShortcut.unregisterAll();
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import Store from "electron-store";
|
||||
|
||||
let store: Store;
|
||||
|
||||
export const getStore = () => {
|
||||
if (!store) {
|
||||
store = new Store();
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
||||
export { Store };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { app, Menu, Tray } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { hideWindow, showWindow } from "./windowManage";
|
||||
|
||||
let appTray: Tray;
|
||||
|
||||
export const createTray = () => {
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: t("system.showWindow"),
|
||||
click: showWindow,
|
||||
},
|
||||
{
|
||||
label: t("system.hideWindow"),
|
||||
click: hideWindow,
|
||||
},
|
||||
{
|
||||
label: t("system.toggleDevTools"),
|
||||
role: "toggleDevTools",
|
||||
},
|
||||
{
|
||||
label: t("system.quit"),
|
||||
click: () => {
|
||||
global.forceQuit = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
appTray = new Tray(global.pathConfig.trayIcon);
|
||||
appTray.setToolTip(app.getName());
|
||||
appTray.setIgnoreDoubleClickEvents(true);
|
||||
appTray.on("click", showWindow);
|
||||
|
||||
appTray.setContextMenu(trayMenu);
|
||||
};
|
||||
|
||||
export const destroyTray = () => {
|
||||
if (!appTray || appTray.isDestroyed()) return;
|
||||
appTray.destroy();
|
||||
appTray = null;
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
import { join } from "node:path";
|
||||
import { BrowserWindow, dialog, shell } from "electron";
|
||||
import { isLinux, isMac, isWin } from "../utils";
|
||||
import { destroyTray } from "./trayManage";
|
||||
import { getIsForceQuit } from "./appManage";
|
||||
import { registerShortcuts, unregisterShortcuts } from "./shortcutManage";
|
||||
import { initIMSDK } from "../utils/imsdk";
|
||||
import OpenIMSDKMain from "@openim/electron-client-sdk";
|
||||
|
||||
const url = process.env.VITE_DEV_SERVER_URL;
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let splashWindow: BrowserWindow | null = null;
|
||||
let sdkInstance: OpenIMSDKMain | null = null;
|
||||
|
||||
function createSplashWindow() {
|
||||
splashWindow = new BrowserWindow({
|
||||
frame: false,
|
||||
width: 200,
|
||||
height: 200,
|
||||
resizable: false,
|
||||
transparent: true,
|
||||
});
|
||||
splashWindow.loadFile(global.pathConfig.splashHtml);
|
||||
splashWindow.on("closed", () => {
|
||||
splashWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function createMainWindow() {
|
||||
createSplashWindow();
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "畅联",
|
||||
icon: join(global.pathConfig.publicPath, "favicon.ico"),
|
||||
frame: false,
|
||||
show: false,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 1024,
|
||||
minHeight: 726,
|
||||
titleBarStyle: "hiddenInset",
|
||||
webPreferences: {
|
||||
preload: global.pathConfig.preload,
|
||||
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
|
||||
// Consider using contextBridge.exposeInMainWorld
|
||||
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: false,
|
||||
devTools: true,
|
||||
webSecurity: false,
|
||||
},
|
||||
});
|
||||
|
||||
sdkInstance = initIMSDK(mainWindow.webContents);
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
// Open devTool if the app is not packaged
|
||||
mainWindow.loadURL(url);
|
||||
} else {
|
||||
mainWindow.loadFile(global.pathConfig.indexHtml);
|
||||
}
|
||||
|
||||
// Test actively push message to the Electron-Renderer
|
||||
mainWindow.webContents.on("did-finish-load", () => {
|
||||
mainWindow?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
});
|
||||
|
||||
// // Make all links open with the browser, not with the application
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith("https:") || url.startsWith("http:")) shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
mainWindow.on("focus", () => {
|
||||
mainWindow?.flashFrame(false);
|
||||
registerShortcuts();
|
||||
});
|
||||
|
||||
mainWindow.on("blur", () => {
|
||||
unregisterShortcuts();
|
||||
});
|
||||
|
||||
mainWindow.on("close", (e) => {
|
||||
if (getIsForceQuit() || !mainWindow.isVisible()) {
|
||||
mainWindow = null;
|
||||
destroyTray();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
if (isMac && mainWindow.isFullScreen()) {
|
||||
mainWindow.setFullScreen(false);
|
||||
}
|
||||
mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
export function splashEnd() {
|
||||
splashWindow?.close();
|
||||
mainWindow?.show();
|
||||
}
|
||||
|
||||
// utils
|
||||
export const isExistMainWindow = (): boolean =>
|
||||
!!mainWindow && !mainWindow?.isDestroyed();
|
||||
export const isShowMainWindow = (): boolean => {
|
||||
if (!mainWindow) return false;
|
||||
return mainWindow.isVisible() && (isWin ? true : mainWindow.isFocused());
|
||||
};
|
||||
|
||||
export const closeWindow = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.close();
|
||||
};
|
||||
|
||||
export const hotReload = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.reload();
|
||||
};
|
||||
|
||||
export const sendEvent = (name: string, ...args: any[]) => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.webContents.send(name, ...args);
|
||||
};
|
||||
|
||||
export const showSelectDialog = async (options: Electron.OpenDialogOptions) => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return await dialog.showOpenDialog(mainWindow, options);
|
||||
};
|
||||
export const showDialog = ({
|
||||
type,
|
||||
message,
|
||||
detail,
|
||||
}: Electron.MessageBoxSyncOptions) => {
|
||||
if (!mainWindow) return;
|
||||
dialog.showMessageBoxSync(mainWindow, {
|
||||
type,
|
||||
message,
|
||||
detail,
|
||||
});
|
||||
};
|
||||
export const showSaveDialog = async (options: Electron.SaveDialogOptions) => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return await dialog.showSaveDialog(mainWindow, options);
|
||||
};
|
||||
export const minimize = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.minimize();
|
||||
};
|
||||
export const updateMaximize = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
};
|
||||
export const toggleHide = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
|
||||
};
|
||||
export const toggleMinimize = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) {
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
} else {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
};
|
||||
export const showWindow = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.focus();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
};
|
||||
export const hideWindow = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.hide();
|
||||
};
|
||||
export const toggleWindowVisible = (visible: boolean) => {
|
||||
if (!mainWindow) return;
|
||||
const opacity = mainWindow.getOpacity() ? 0 : 1;
|
||||
if (Boolean(opacity) !== visible) return;
|
||||
mainWindow.setOpacity(opacity);
|
||||
};
|
||||
export const setProgressBar = (
|
||||
progress: number,
|
||||
options?: Electron.ProgressBarOptions,
|
||||
) => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.setProgressBar(progress, options);
|
||||
};
|
||||
export const taskFlicker = () => {
|
||||
if (
|
||||
isMac ||
|
||||
(mainWindow.isVisible() && mainWindow.isFocused() && !isExistMainWindow())
|
||||
)
|
||||
return;
|
||||
mainWindow?.flashFrame(true);
|
||||
};
|
||||
export const setIgnoreMouseEvents = (
|
||||
ignore: boolean,
|
||||
options?: Electron.IgnoreMouseEventsOptions,
|
||||
) => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.setIgnoreMouseEvents(ignore, options);
|
||||
};
|
||||
export const toggleDevTools = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.webContents.isDevToolsOpened()) {
|
||||
mainWindow.webContents.closeDevTools();
|
||||
} else {
|
||||
mainWindow.webContents.openDevTools({
|
||||
mode: "detach",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const setFullScreen = (isFullscreen: boolean): boolean => {
|
||||
if (!mainWindow) return false;
|
||||
if (isLinux) {
|
||||
// linux It needs to be resizable before it can be full screen
|
||||
if (isFullscreen) {
|
||||
mainWindow.setResizable(isFullscreen);
|
||||
mainWindow.setFullScreen(isFullscreen);
|
||||
} else {
|
||||
mainWindow.setFullScreen(isFullscreen);
|
||||
mainWindow.setResizable(isFullscreen);
|
||||
}
|
||||
} else {
|
||||
mainWindow.setFullScreen(isFullscreen);
|
||||
}
|
||||
return isFullscreen;
|
||||
};
|
||||
|
||||
export const clearCache = async () => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
await mainWindow.webContents.session.clearCache();
|
||||
await mainWindow.webContents.session.clearStorageData();
|
||||
};
|
||||
|
||||
export const getCacheSize = async () => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return await mainWindow.webContents.session.getCacheSize();
|
||||
};
|
||||
|
||||
export const getWebContents = (): Electron.WebContents => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return mainWindow.webContents;
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { DataPath, IElectronAPI } from "./../../src/types/globalExpose.d";
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { isProd } from "../utils";
|
||||
import "@openim/electron-client-sdk/lib/preload";
|
||||
import { Platform } from "@openim/wasm-client-sdk";
|
||||
|
||||
const getPlatform = () => {
|
||||
if (process.platform === "darwin") {
|
||||
return Platform.MacOSX;
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
return Platform.Windows;
|
||||
}
|
||||
return Platform.Linux;
|
||||
};
|
||||
|
||||
const getDataPath = (key: DataPath) => {
|
||||
switch (key) {
|
||||
case "public":
|
||||
return isProd ? ipcRenderer.sendSync("getDataPath", "public") : "";
|
||||
case "sdkResources":
|
||||
return isProd ? ipcRenderer.sendSync("getDataPath", "sdkResources") : "";
|
||||
case "logsPath":
|
||||
return isProd ? ipcRenderer.sendSync("getDataPath", "logsPath") : "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const subscribe = (channel: string, callback: (...args: any[]) => void) => {
|
||||
const subscription = (_, ...args) => callback(...args);
|
||||
ipcRenderer.on(channel, subscription);
|
||||
return () => ipcRenderer.removeListener(channel, subscription);
|
||||
};
|
||||
|
||||
const subscribeOnce = (channel: string, callback: (...args: any[]) => void) => {
|
||||
ipcRenderer.once(channel, (_, ...args) => callback(...args));
|
||||
};
|
||||
|
||||
const unsubscribeAll = (channel: string) => {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
};
|
||||
|
||||
const ipcInvoke = (channel: string, ...arg: any) => {
|
||||
return ipcRenderer.invoke(channel, ...arg);
|
||||
};
|
||||
|
||||
const ipcSendSync = (channel: string, ...arg: any) => {
|
||||
return ipcRenderer.sendSync(channel, ...arg);
|
||||
};
|
||||
|
||||
const getUniqueSavePath = (originalPath: string) => {
|
||||
let counter = 0;
|
||||
let savePath = originalPath;
|
||||
let fileDir = path.dirname(originalPath);
|
||||
let fileName = path.basename(originalPath);
|
||||
let fileExt = path.extname(originalPath);
|
||||
let baseName = path.basename(fileName, fileExt);
|
||||
|
||||
while (fs.existsSync(savePath)) {
|
||||
counter++;
|
||||
fileName = `${baseName}(${counter})${fileExt}`;
|
||||
savePath = path.join(fileDir, fileName);
|
||||
}
|
||||
|
||||
return savePath;
|
||||
};
|
||||
|
||||
const getFileByPath = async (filePath: string) => {
|
||||
try {
|
||||
const filename = path.basename(filePath);
|
||||
const data = await fs.promises.readFile(filePath);
|
||||
return new File([data], filename);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const saveFileToDisk = async ({
|
||||
file,
|
||||
sync,
|
||||
}: {
|
||||
file: File;
|
||||
sync?: boolean;
|
||||
}): Promise<string> => {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const saveDir = ipcRenderer.sendSync("getDataPath", "sdkResources");
|
||||
const savePath = path.join(saveDir, file.name);
|
||||
const uniqueSavePath = getUniqueSavePath(savePath);
|
||||
if (!fs.existsSync(saveDir)) {
|
||||
fs.mkdirSync(saveDir, { recursive: true });
|
||||
}
|
||||
if (sync) {
|
||||
await fs.promises.writeFile(uniqueSavePath, Buffer.from(arrayBuffer));
|
||||
} else {
|
||||
fs.promises.writeFile(uniqueSavePath, Buffer.from(arrayBuffer));
|
||||
}
|
||||
return uniqueSavePath;
|
||||
};
|
||||
|
||||
const Api: IElectronAPI = {
|
||||
getDataPath,
|
||||
getVersion: () => process.version,
|
||||
getPlatform,
|
||||
getSystemVersion: process.getSystemVersion,
|
||||
subscribe,
|
||||
subscribeOnce,
|
||||
unsubscribeAll,
|
||||
ipcInvoke,
|
||||
ipcSendSync,
|
||||
getFileByPath,
|
||||
saveFileToDisk,
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", Api);
|
||||
@@ -0,0 +1,22 @@
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import OpenIMSDKMain from "@openim/electron-client-sdk";
|
||||
import { WebContents } from "electron";
|
||||
|
||||
export const getLibSuffix = () => {
|
||||
const platform = process.platform;
|
||||
const arch = os.arch();
|
||||
if (platform === "darwin") {
|
||||
return path.join(`mac_${arch === "arm64" ? "arm64" : "x64"}`, "libopenimsdk.dylib");
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return path.join(`win_${arch === "ia32" ? "ia32" : "x64"}`, "libopenimsdk.dll");
|
||||
}
|
||||
return path.join(`linux_${arch === "arm64" ? "arm64" : "x64"}`, "libopenimsdk.so");
|
||||
};
|
||||
|
||||
export const initIMSDK = (webContents: WebContents) =>
|
||||
new OpenIMSDKMain(
|
||||
path.join(global.pathConfig.imsdkLibPath, getLibSuffix()),
|
||||
webContents,
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
export const isLinux = process.platform == "linux";
|
||||
export const isWin = process.platform == "win32";
|
||||
export const isMac = process.platform == "darwin";
|
||||
export const isProd = !process.env.VITE_DEV_SERVER_URL;
|
||||
@@ -0,0 +1,17 @@
|
||||
import log from "electron-log/main";
|
||||
import { join } from "node:path";
|
||||
import fs from "fs";
|
||||
|
||||
const getLogger = (logsPath: string) => {
|
||||
log.transports.file.level = "debug";
|
||||
log.transports.file.maxSize = 104857600; // max size 100M
|
||||
log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}";
|
||||
let date = new Date();
|
||||
// let dateStr = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
// log.transports.file.resolvePathFn = () => join(logsPath, `log${dateStr}.log`);
|
||||
log.transports.file.resolvePathFn = () => join(logsPath, `OpenIM.log`);
|
||||
log.initialize({ preload: true });
|
||||
return log.scope("ipcMain");
|
||||
};
|
||||
|
||||
export { getLogger };
|
||||
Reference in New Issue
Block a user