@plasosdk/plaso-electron-sdk
Version:
伯索课堂Electron SDK
449 lines (403 loc) • 20.5 kB
JavaScript
const LogFormatter = require('../scripts/logger');
const path = require('path');
const { getElectronRemote, parseUrlParams, getEnvironment, getDisplayMatching } = require('./util');
const remote = getElectronRemote();
const defaultLoggerPath = remote ? path.join(remote.app.getPath('userData'), '/P403FileTemp/') : '';
const logger = new LogFormatter(defaultLoggerPath);
logger.init();
const getConfig = require('../scripts/getConfig');
const { platform } = getConfig();
const isMac = platform === 'darwin';
const { ERROR_CODE, LESSON_TYPE, CLASS_WINDOW_MESG_TYPE, RENDER_TO_MAIN_MESG_TYPE, FILE_TYPE } = require('./macro');
let currentWinId = null;
let currentWebContentsId = null;
/**
* @typedef {Object} classOptionsType
* @property {boolean} [debug] - 是否开启课堂窗口debug模式
* @property {string} query - 进课堂的必备query
* @property {string} [version] - 格式参考:1.53.901
*/
/**
* @typedef {Object} FileParams
* @property {string[]} filePath - 备课文件本地地址
* @property {'png' | 'pb' | string} fileType
*/
/**
* @typedef {Object} classWindowType
* @property {classOptionsType} classOptions 进课堂参数对象
* @property {Object} [electronWinOptions] 自定义electron的窗口参数
* @property {(winId: number)=>void} [onClassWindowReadyFn] 课堂窗口打开渲染成功后的回调
* @property {(winId: number)=>void} [onClassWindowLeaveFn] 课堂窗口关闭后的回调
* @property {(meetingId: string)=>void} [onClassFinishedFn] 课堂结束后的回调
* @property {(
* params: {
* fileInfo: FileParams[],
* fileName?: string
* },
* callback: (result: boolean) => void
* ) => void} [onSaveBoardFn] 保存板书,具体的保存逻辑由外部实现,取消保存板书时,callback传false, 不然传true
* @property {()=>void} [onOpenResourceCenterFn] 通知外部用户打开自己的资料中心,资料中心的具体ui和逻辑由外部用户自己实现
* @property {(info: any, option: any)=>Promise<string>} [onGetExtFileNameFn] 通过insertObject插入的文件传入 参数 info 时,怎么从info中获取文件的可访问地址的逻辑在用户那,所以需要函数从外部用户获取外部用户传入的文件地址
* @property {(info: any, option: { suffix: string })=>Promise<string>} [onGetPreParseFileNameFn] 类似onGetExtFileNameFn,只是获取的文件地址是预解析文件地址,通过suffix得到预解析文件路径
* @property {(info: {
* time: string;
* id: string;
* name: string;
* bugDescription: string;
* })=>void} [onReportIssuesFn] 上报问题,具体的上报逻辑由外部实现
*
*/
/**
* @param {classWindowType} classWindowProps
* @returns
*/
function createClassWindow(classWindowProps) {
const {
classOptions,
electronWinOptions,
onClassWindowReadyFn,
onClassWindowLeaveFn,
onClassFinishedFn,
onSaveBoardFn,
onOpenResourceCenterFn,
onGetExtFileNameFn,
onGetPreParseFileNameFn,
onReportIssuesFn,
} = classWindowProps;
logger.info(
`课堂窗口创建信息: version is ${getVersion()}, classOptions is ${JSON.stringify(classOptions)},electronWinOptions is ${JSON.stringify(
electronWinOptions,
)}`,
);
if (currentWinId) {
logger.warn('课堂窗口同时仅能存在一个');
return ERROR_CODE.CLASS_WINODW_GREATER_THAN_ONE;
}
const isElectron = !!process?.versions?.['electron'];
if (isElectron) {
try {
const appProxy = require('@plasosdk/winproxy');
const { ipcRenderer } = window.require('electron');
const displayInfo = getDisplayMatching();
if (remote && displayInfo) {
const webContents = remote.getCurrentWebContents();
const win = remote.getCurrentWindow();
/**----------------------------------------------------课堂入参相关-----------------------------------*/
const classInfo = classOptions.query ? parseUrlParams(classOptions.query) : {};
const autoMaximized = true;
/** 课堂最大化按钮是否是切换全屏模式 */
let fullScreenable = false;
if (classInfo.userType === 'listener') {
fullScreenable = true;
}
// 环境确认
const env = classOptions.env ? classOptions.env.toLowerCase() : 'www';
let rhost, dhost;
if (env == 'local') {
rhost = 'http://localhost:4399/';
dhost = 'https://dev.plaso.cn/';
} else if (env == 'dev') {
rhost = `https://${env}.plaso.cn/static/yxtelectronsdk/`;
dhost = `https://${env}.plaso.cn/`;
} else if (env == 'test' || env == 'itest' || env == 'ftest') {
rhost = `https://${env}.plaso.cn/static/yxtsdk/`;
dhost = `https://${env}.plaso.cn/`;
} else {
rhost = `https://wwwr.plaso.cn/static/sdk/styleupime/${classOptions.version ?? '1.60.139'}/`;
dhost = 'https://www.plaso.cn/';
}
// 截图和虚拟声卡地址确认
let flameshotPath = path.join(__dirname, '../lib/flameshot/flameshot.exe');
let PlasoALD = '';
if (isMac) {
flameshotPath = path.join(__dirname, '../lib/flameshot.app/Contents/MacOS/flameshot');
PlasoALD = path.join(__dirname, '../lib/PlasoALD');
}
const classOptionsObj = {
...classOptions,
...classInfo,
rhost,
dhost,
query: classOptions.query,
appType: classOptions.appType,
markString: env,
env,
classType: classOptions.classType,
// 让课堂里退出时标识状态,详情见commFunction.js
onClose: 'close',
electronSdkLoggerPath: logger.loggerPath ?? '',
electronSdkLoggerName: logger.logFileName ?? '',
flameshotPath: flameshotPath ?? '',
PlasoALDPath: PlasoALD ?? '',
clientType: 'electron',
isElectronSdk: true,
openerId: webContents.id,
memoryConfigKey: classOptions.memoryConfigKey ?? classInfo.loginName ?? classOptions.loginName ?? '',
maximized: autoMaximized,
fullScreenable,
supportOnReportIssues: !!onReportIssuesFn,
};
if (classOptionsObj.userType === 'monitor') {
classOptionsObj.monitor = true;
}
/**----------------------------------------------------electron 窗口相关设置-----------------------------------*/
const bounds = win.getBounds();
const screenWidth = displayInfo.size.width;
const screenWorkAreaWidth = displayInfo.workAreaSize.width;
const screenHeight = displayInfo.size.height;
const screenWorkAreaHeight = displayInfo.workAreaSize.height;
const changeBoundsDate = (ischangeWidth, baseWidthOrHeight) => {
const newWidthOrHeight = baseWidthOrHeight - 10;
if (ischangeWidth) {
bounds.height = Math.round((bounds.height * newWidthOrHeight) / bounds.width);
bounds.width = newWidthOrHeight;
} else {
bounds.width = Math.round((bounds.width * newWidthOrHeight) / bounds.height);
bounds.height = newWidthOrHeight;
}
};
// 课堂窗口创建时的宽或高和 屏幕宽高或屏幕工作区宽高 一致时,此时透明窗口会失效,需要调整进课堂时的窗口宽高
if (bounds.width === screenWidth || bounds.width === screenWorkAreaWidth) {
changeBoundsDate(true, Math.min(screenWidth, screenWorkAreaWidth));
}
if (bounds.height === screenHeight || bounds.height === screenWorkAreaHeight) {
changeBoundsDate(false, Math.min(screenHeight, screenWorkAreaHeight));
}
const defaultElecteonWinOptions = {
frame: false,
/**
* electron 12.0.18之后,resizable设为true才能全屏和最大化,
* 而课堂窗口不允许通过electron自身的缩放行为来改变大小,因此进入课堂后会将resizable设为false
*/
resizable: true,
/** 设置为true,mac上setFullScreen(true)才生效,setSimpleFullScreen(true)不依赖此参数 */
fullscreenable: true,
...bounds,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false,
nodeIntegrationInWorker: true,
},
};
if (isMac) {
// 在Mac下要显式设置fullscreen为false,防止客户端全屏时进入课堂,课堂窗口也默认全屏,
// 导致部分按钮失效,以及独立窗口打开异常
defaultElecteonWinOptions.fullscreen = false;
}
// 学生无需使用透明窗口(透明窗口问题多),减少影响
if (classInfo.userType !== 'listener') {
defaultElecteonWinOptions.transparent = true;
defaultElecteonWinOptions.backgroundColor = '#00ffffff';
}
if (classInfo.topic) defaultElecteonWinOptions.title = classInfo.topic + '';
if (classOptions.topic && classOptions.classType === LESSON_TYPE.PREPARE_LESSONS)
defaultElecteonWinOptions.title = classOptions.topic + '';
const _electeonWinOptions = electronWinOptions
? {
...defaultElecteonWinOptions,
...electronWinOptions,
}
: defaultElecteonWinOptions;
/**---------------------------------------------------- 创建课堂/备课窗口-----------------------------------*/
const onClassWindowReady = (event) => {
const classWindow = remote.BrowserWindow.fromId(currentWinId);
classWindow.moveTop();
classWindow.focus();
logger.info(`课堂窗口ready,id:${currentWinId}`);
if (onClassWindowReadyFn && currentWinId) onClassWindowReadyFn(currentWinId);
};
const onClassFinished = (event, value) => {
const meetingId = value?.meetingId;
logger.info(`课堂已结束,id:${currentWinId}, meetingId is ${meetingId}`);
if (onClassFinishedFn && currentWinId) onClassFinishedFn(meetingId);
};
const onHtmlReady = (event, value) => {
currentWebContentsId = value?.webContentId;
logger.info(`课堂窗口 html ready,web contents id:${currentWebContentsId}`);
if (currentWebContentsId) {
ipcRenderer.sendTo(currentWebContentsId, CLASS_WINDOW_MESG_TYPE.INIT_GLOBAL_APPINFO, classOptionsObj);
}
};
const onSaveBoard = (event, value) => {
logger.info(`保存板书:${JSON.stringify(value)}`);
const cb = (value) => {
if (currentWebContentsId) {
ipcRenderer.sendTo(currentWebContentsId, CLASS_WINDOW_MESG_TYPE.RESP_SAVE_BOARD, value);
}
};
if (onSaveBoardFn && value) onSaveBoardFn(value, cb);
};
const onOpenResourceCenter = (event) => {
if (onOpenResourceCenterFn) onOpenResourceCenterFn();
};
const onGetExtFileName = async (event, requestId, ...args) => {
if (onGetExtFileNameFn && args.length > 0) {
const url = await onGetExtFileNameFn(...args);
if (url) {
if (currentWebContentsId) {
ipcRenderer.sendTo(currentWebContentsId, CLASS_WINDOW_MESG_TYPE.RESP_GET_EXT_FILE_NAME, requestId, url);
}
}
}
};
const onGetPreParseFileName = async (event, requestId, ...args) => {
if (onGetPreParseFileNameFn && args.length > 0) {
const url = await onGetPreParseFileNameFn(...args);
if (url) {
ipcRenderer.sendTo(currentWebContentsId, CLASS_WINDOW_MESG_TYPE.RESP_GET_PRE_PARSE_FILE_NAME, requestId, url);
}
}
};
const onReportIssues = (event, info) => {
onReportIssuesFn?.(info);
};
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.LIVE_WINDOW_READY, onClassWindowReady);
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.ON_CLASS_FINISHED, onClassFinished);
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.HTML_READY, onHtmlReady);
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.SAVE_BOARD, onSaveBoard);
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.OPEN_RESOURCE_CENTER, onOpenResourceCenter);
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.GET_EXT_FILE_NAME, onGetExtFileName);
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.GET_PRE_PARSE_FILE_NAME, onGetPreParseFileName);
ipcRenderer.on(CLASS_WINDOW_MESG_TYPE.REPORT_ISSUES, onReportIssues);
const url = require('url');
const htmlPath =
(isMac
? url.format({
pathname: path.join(__dirname, '../index.html'),
protocol: 'file:',
slashes: true,
})
: path.join(__dirname, '../index.html')) + `?openerId=${webContents.id}`;
appProxy.createWindow(
{
path: htmlPath,
debug: classOptions?.debug ?? false,
maximize: autoMaximized,
windowOptions: _electeonWinOptions,
},
async (id) => {
currentWinId = id;
appProxy.addWinEventListener(id, 'closed', () => {
logger.info(`课堂窗口关闭,id:${currentWinId}`);
if (onClassWindowLeaveFn) onClassWindowLeaveFn(currentWinId);
currentWinId = null;
currentWebContentsId = null;
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.LIVE_WINDOW_READY, onClassWindowReady);
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.ON_CLASS_FINISHED, onClassFinished);
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.HTML_READY, onHtmlReady);
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.SAVE_BOARD, onSaveBoard);
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.OPEN_RESOURCE_CENTER, onOpenResourceCenter);
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.GET_EXT_FILE_NAME, onGetExtFileName);
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.GET_PRE_PARSE_FILE_NAME, onGetPreParseFileName);
ipcRenderer.removeListener(CLASS_WINDOW_MESG_TYPE.REPORT_ISSUES, onReportIssues);
});
},
);
return 0;
} else {
logger.error('获取remote、displayInfo 失败');
if (!remote) return ERROR_CODE.GET_REMOTE_FAIL;
if (!displayInfo) return ERROR_CODE.GET_DISPLAY_INFO_FAIL;
}
} catch (error) {
logger.error(`异常错误,${JSON.stringify(error)}`);
return ERROR_CODE.COMMOM_ERROR;
}
} else {
logger.error('非Electron环境');
return ERROR_CODE.NO_ELECTRON_ENVIRONMENT;
}
}
/**
* @param {classWindowType} liveClassWindowProps
* @returns
*/
function createLiveClassWindow(liveClassWindowProps) {
const classWindowProps = {
...liveClassWindowProps,
classOptions: {
...liveClassWindowProps.classOptions,
appType: 'liveclassSDK',
classType: LESSON_TYPE.LIVECLASS,
},
};
return createClassWindow(classWindowProps);
}
/**
* @param {classWindowType} prepareClassWindowProps
* @returns
*/
function createPrepareClassWindow(prepareClassWindowProps) {
const classWindowProps = {
...prepareClassWindowProps,
classOptions: {
...prepareClassWindowProps.classOptions,
appType: 'prepareLessonSdk',
meetingType: LESSON_TYPE.PREPARE_LESSONS,
classType: LESSON_TYPE.PREPARE_LESSONS,
userType: 'speaker',
topic: prepareClassWindowProps.classOptions.topic ?? '备课课堂',
},
};
return createClassWindow(classWindowProps);
}
/** 插入外部云盘里的文件,文件需要遵循特定的数据结构 */
/**
* @typedef {Object} fileDataObj
* @property {number} type 插入文件的格式,内容参考 FILE_TYPE
* @property {string} [title] 文件名称,传入后会显示在文件窗口标题栏上,建议带上文件后缀名
* @property {string} [url] 公开的访问权限的文件全地址,建议https协议全地址
* @property {any[]} [info] 具体的文件信息,除备课外,内容都由用户自己定义
*/
/**
* @param {fileDataObj} fileData 文件数据
*/
function insertObject(fileData) {
try {
const { ipcRenderer } = window.require('electron');
if (currentWebContentsId) ipcRenderer.sendTo(currentWebContentsId, CLASS_WINDOW_MESG_TYPE.INSERT_OBJECT, fileData);
} catch (error) {
logger.error(`insertObject error is: ${JSON.stringify(error)}}`);
return ERROR_CODE.COMMOM_ERROR;
}
}
/**
* 更新严禁词列表
* @param {string[]} words 严禁词数组
*/
function updateProhibitedWords(words) {
try {
const { ipcRenderer } = window.require('electron');
if (currentWebContentsId) ipcRenderer.sendTo(currentWebContentsId, CLASS_WINDOW_MESG_TYPE.UPDATE_SENSITIVE_WORDS, words || []);
} catch (error) {
logger.error(`updateProhibitedWords error is: ${JSON.stringify(error)}}`);
}
}
function getVersion() {
try {
const packageJson = require('../package.json');
return packageJson.version;
} catch (error) {
logger.error(`getVersion error is: ${JSON.stringify(error)}}`);
return ERROR_CODE.COMMOM_ERROR;
}
}
/** 日志文件地址 */
function initLogConfig(logFilePath) {
if (logFilePath) {
logger.initLoggerFilePath(logFilePath);
const { ipcRenderer } = require('electron');
ipcRenderer.send(RENDER_TO_MAIN_MESG_TYPE.PLASO_INIT_LOG_PATH, logFilePath);
}
}
const PlasoElectronSdk = {
createLiveClassWindow: createLiveClassWindow,
createPrepareClassWindow: createPrepareClassWindow,
getVersion: getVersion,
initLogConfig: initLogConfig,
insertObject: insertObject,
updateProhibitedWords: updateProhibitedWords,
FILE_TYPE: FILE_TYPE,
};
module.exports = PlasoElectronSdk;