初始化提交

This commit is contained in:
王立帮
2024-07-19 10:16:00 +08:00
parent 4c7b571f20
commit 4a2d56dcc4
7084 changed files with 741212 additions and 63 deletions

View File

@@ -0,0 +1,209 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mixly.Env');
goog.require('Mixly.FS');
goog.require('Mixly.Debug');
goog.require('Mixly.MJSON');
goog.require('Mixly.Electron.Ampy');
goog.provide('Mixly.Electron.AmpyFS');
const {
Env,
FS,
Debug,
MJSON,
Electron
} = Mixly;
const { Ampy } = Electron;
const fs_extra = Mixly.require('fs-extra');
class AmpyFS extends FS {
#ampy_ = null;
#port_ = '';
#baud_ = 115200;
constructor() {
super();
this.#ampy_ = new Ampy();
}
async rename(oldPath, newPath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.rename(this.#port_, this.#baud_, oldPath, newPath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async createFile(filePath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.mkfile(this.#port_, this.#baud_, filePath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async readFile(filePath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.get(this.#port_, this.#baud_, filePath);
stdout = output.stdout;
stdout = stdout.replaceAll('\r\r', '\r');
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async writeFile(filePath, data) {
let stdout = '', error = null;
try {
const startFilePath = path.join(Env.clientPath, 'temp/temp');
await fs_extra.outputFile(startFilePath, data);
const output = await this.#ampy_.put(this.#port_, this.#baud_, startFilePath, filePath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async isFile(filePath) {
/*const [error, stdout] = await this.readDirectory(filePath);
if (error) {
return true;
}
return false;*/
let error = null;
if (path.extname(filePath)) {
return [error, true];
} else {
return [error, false];
}
}
async renameFile(oldFilePath, newFilePath) {
return this.rename(oldFilePath, newFilePath);
}
// async moveFile(oldFilePath, newFilePath) {}
// async copyFile(oldFilePath, newFilePath) {}
async deleteFile(filePath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.rm(this.#port_, this.#baud_, filePath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async createDirectory(folderPath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.mkdir(this.#port_, this.#baud_, folderPath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async readDirectory(folderPath) {
let stdout = [], error = null;
try {
const output = await this.#ampy_.ls(this.#port_, this.#baud_, folderPath);
const dirs = Array.from(output.stdout.split('\r\n'));
for (let i in dirs) {
if (!dirs[i]) {
continue;
}
stdout.push(MJSON.parse(dirs[i].replaceAll('\'', '"')));
}
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async isDirectory(folderPath) {
/*const [error, stdout] = await this.readDirectory(folderPath);
if (error) {
return false;
}
return true;*/
let error = null;
if (path.extname(folderPath)) {
return [error, false];
} else {
return [error, true];
}
}
async isDirectoryEmpty(folderPath) {
/*const [error, stdout] = await this.readDirectory(folderPath);
let isEmpty = false;
if (error || !stdout.length) {
isEmpty = true;
}*/
return [null, false];
}
async renameDirectory(oldFolderPath, newFolderPath) {
return this.rename(oldFolderPath, newFolderPath);
}
// async moveDirectory(oldFolderPath, newFolderPath) {}
// async copyDirectory(oldFolderPath, newFolderPath) {}
async deleteDirectory(folderPath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.rmdir(this.#port_, this.#baud_, folderPath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
setPortName(port) {
this.#port_ = port;
}
getPortName() {
return this.#port_;
}
setBaudRate(baud) {
this.#baud_ = baud;
}
getBaudRate() {
return this.#baud_;
}
}
Electron.AmpyFS = AmpyFS;
});

View File

@@ -0,0 +1,118 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mustache');
goog.require('Mixly.Ampy');
goog.require('Mixly.Env');
goog.require('Mixly.Serial');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.Ampy');
const {
Ampy,
Env,
Serial,
Electron
} = Mixly;
const util = Mixly.require('node:util');
const child_process = Mixly.require('node:child_process');
class AmpyExt extends Ampy {
static {
this.TEMPLATE = {
ls: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 ls "{{&folderPath}}"',
get: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 get "{{&filePath}}"',
mkdir: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 mkdir "{{&folderPath}}"',
mkfile: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 mkfile "{{&filePath}}"',
isdir: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 isdir "{{&folderPath}}"',
isfile: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 isfile "{{&filePath}}"',
put: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 put "{{&startPath}}" "{{&endPath}}"',
rm: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 rm "{{&filePath}}"',
rmdir: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 rmdir "{{&folderPath}}"',
rename: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 rename "{{&oldPath}}" "{{&newPath}}"',
run: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 run "{{&filePath}}"'
}
this.AMPY_PATH = path.join(Env.srcDirPath, './tools/python/ampy/cli.py');
this.AMPY_TEMPLATE = Mustache.render('"{{&python3}}" "{{&ampy}}"', {
python3: Env.python3Path,
ampy: this.AMPY_PATH
});
}
#exec_ = util.promisify(child_process.exec);
constructor() {
super();
}
async ls(port, baud, folderPath) {
return this.exec(port, this.render('ls', { port, baud, folderPath }));
}
async get(port, baud, filePath) {
return this.exec(port, this.render('get', { port, baud, filePath }));
}
async mkdir(port, baud, folderPath) {
return this.exec(port, this.render('mkdir', { port, baud, folderPath }));
}
async mkfile(port, baud, filePath) {
return this.exec(port, this.render('mkfile', { port, baud, filePath }));
}
async isdir(port, baud, folderPath) {
return this.exec(port, this.render('isdir', { port, baud, folderPath }));
}
async isfile(port, baud, filePath) {
return this.exec(port, this.render('isfile', { port, baud, filePath }));
}
async put(port, baud, startPath, endPath) {
return this.exec(port, this.render('put', { port, baud, startPath, endPath }));
}
async rm(port, baud, filePath) {
return this.exec(port, this.render('rm', { port, baud, filePath }));
}
async rmdir(port, baud, folderPath) {
return this.exec(port, this.render('rmdir', { port, baud, folderPath }));
}
async rename(port, baud, oldPath, newPath) {
return this.exec(port, this.render('rename', { port, baud, oldPath, newPath }));
}
async run(port, baud, filePath) {
return this.exec(port, this.render('run', { port, baud, filePath }));
}
render(templateName, args) {
return Mustache.render(AmpyExt.TEMPLATE[templateName], {
...args,
ampy: AmpyExt.AMPY_TEMPLATE
});
}
async exec(port, command) {
const portsName = Serial.getCurrentPortsName();
if (!portsName.includes(port)) {
throw new Error('无可用串口');
return;
}
const { mainStatusBarTabs } = Mixly;
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
if (statusBarSerial) {
await statusBarSerial.close();
}
return this.#exec_(command);
}
}
Electron.Ampy = AmpyExt;
});

View File

@@ -0,0 +1,627 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('layui');
goog.require('Blockly');
goog.require('Mixly.Env');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Config');
goog.require('Mixly.Title');
goog.require('Mixly.Boards');
goog.require('Mixly.MFile');
goog.require('Mixly.MArray');
goog.require('Mixly.Msg');
goog.require('Mixly.MString');
goog.require('Mixly.Workspace');
goog.require('Mixly.Serial');
goog.require('Mixly.Electron.Shell');
goog.provide('Mixly.Electron.ArduShell');
const {
Env,
Electron,
LayerExt,
Title,
Boards,
MFile,
MArray,
Msg,
MString,
Workspace,
Serial,
Config
} = Mixly;
const { BOARD, SOFTWARE, USER } = Config;
const fs = Mixly.require('node:fs');
const fs_plus = Mixly.require('fs-plus');
const fs_extra = Mixly.require('fs-extra');
const lodash_fp = Mixly.require('lodash/fp');
const child_process = Mixly.require('node:child_process');
const iconv_lite = Mixly.require('iconv-lite');
const {
ArduShell,
Shell
} = Electron;
ArduShell.DEFAULT_CONFIG = goog.getJSON(path.join(Env.templatePath, 'json/arduino-cli-config.json'));
ArduShell.binFilePath = '';
ArduShell.shellPath = null;
ArduShell.shell = null;
ArduShell.ERROR_ENCODING = Env.currentPlatform == 'win32' ? 'cp936' : 'utf-8';
ArduShell.updateShellPath = () => {
let shellPath = path.join(Env.clientPath, 'arduino-cli');
if (Env.currentPlatform === 'win32')
shellPath = path.join(shellPath, 'arduino-cli.exe');
else
shellPath = path.join(shellPath, 'arduino-cli');
if (!fs_plus.isFileSync(shellPath)) {
const { defaultPath = {} } = SOFTWARE;
if (typeof defaultPath[Env.currentPlatform] === 'object') {
let defaultShellPath = defaultPath[Env.currentPlatform].arduinoCli ?? '';
defaultShellPath = path.join(Env.clientPath, defaultShellPath);
if (fs_plus.isFileSync(defaultShellPath))
shellPath = defaultShellPath;
else
shellPath = null;
}
}
ArduShell.shellPath = shellPath;
}
ArduShell.updateConfig = (config) => {
if (!ArduShell.shellPath) return;
const configPath = path.join(ArduShell.shellPath, '../arduino-cli.json');
let nowConfig = fs_extra.readJsonSync(configPath, { throws: false }) ?? { ...ArduShell.DEFAULT_CONFIG };
if (typeof config === 'object') {
if (MArray.equals(nowConfig.directories, config.directories))
return;
nowConfig = {
...nowConfig,
...config
};
fs_extra.outputJson(configPath, nowConfig, {
spaces: ' '
})
.then(() => {
console.log('arduino-cli.json已更新');
})
.catch((error) => {
console.log(error);
});
}
}
ArduShell.init = () => {
ArduShell.updateShellPath();
if (!ArduShell.shellPath) return;
ArduShell.updateConfig({
directories: {
data: path.join(ArduShell.shellPath, '../Arduino15'),
downloads: path.join(ArduShell.shellPath, '../staging'),
user: path.join(ArduShell.shellPath, '../Arduino')
}
});
}
ArduShell.init();
ArduShell.burn = () => {
Mixly.Electron.BU.initBurn();
}
/**
* @function 编译
* @description 开始一个编译过程
* @return void
*/
ArduShell.initCompile = () => {
ArduShell.compile(() => {});
}
/**
* @function 编译
* @description 开始一个编译过程
* @return void
*/
ArduShell.compile = (doFunc = () => {}) => {
if (!ArduShell.shellPath) {
ArduShell.shellPath = '';
}
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo("output");
ArduShell.compiling = true;
ArduShell.uploading = false;
const boardType = Boards.getSelectedBoardCommandParam();
mainStatusBarTabs.show();
const layerNum = layer.open({
type: 1,
title: Msg.Lang['shell.compiling'] + "...",
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: () => {
$(".layui-layer-page").css("z-index", "198910151");
$("#mixly-loader-btn").off("click").click(() => {
$("#mixly-loader-btn").css('display', 'none');
layer.title(Msg.Lang['shell.aborting'] + '...', layerNum);
ArduShell.cancel();
});
},
end: () => {
$('#mixly-loader-div').css('display', 'none');
$("layui-layer-shade" + layerNum).remove();
$("#mixly-loader-btn").off("click");
$("#mixly-loader-btn").css('display', 'inline-block');
}
});
setTimeout(() => {
statusBarTerminal.setValue(Msg.Lang['shell.compiling'] + "...\n");
let myLibPath = path.join(Env.boardDirPath, "/libraries/myLib/");
if (fs_plus.isDirectorySync(myLibPath))
myLibPath += '\",\"';
else
myLibPath = '';
const thirdPartyPath = path.join(Env.boardDirPath, 'libraries/ThirdParty');
if (fs_plus.isDirectorySync(thirdPartyPath)) {
const libList = fs.readdirSync(thirdPartyPath);
for (let libName of libList) {
const libPath = path.join(thirdPartyPath, libName, 'libraries');
if (!fs_plus.isDirectorySync(libPath)) continue;
myLibPath += libPath + ',';
}
}
const configPath = path.join(ArduShell.shellPath, '../arduino-cli.json'),
defaultLibPath = path.join(ArduShell.shellPath, '../libraries'),
buildPath = path.join(Env.clientPath, './mixlyBuild'),
buildCachePath = path.join(Env.clientPath, './mixlyBuildCache'),
codePath = path.join(Env.clientPath, './testArduino/testArduino.ino');
const cmdStr = '\"'
+ ArduShell.shellPath
+ '\" compile -b '
+ boardType
+ ' --config-file \"'
+ configPath
+ '\" --build-cache-path \"' + buildCachePath + '\" --verbose --libraries \"'
+ myLibPath
+ defaultLibPath
+ '\" --build-path \"'
+ buildPath
+ '\" \"'
+ codePath
+ '\" --no-color';
ArduShell.runCmd(layerNum, 'compile', cmdStr, doFunc);
}, 100);
}
/**
* @function 初始化上传
* @description 关闭已打开的串口,获取当前所连接的设备数,然后开始上传程序
* @return void
*/
ArduShell.initUpload = () => {
const { mainStatusBarTabs } = Mixly;
ArduShell.compiling = false;
ArduShell.uploading = true;
const boardType = Boards.getSelectedBoardCommandParam();
const uploadType = Boards.getSelectedBoardConfigParam('upload_method');
let port = Serial.getSelectedPortName();
switch (uploadType) {
case 'STLinkMethod':
case 'jlinkMethod':
case 'usb':
port = 'None';
break;
}
if (port) {
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
if (statusBarSerial) {
statusBarSerial.close().finally(() => {
ArduShell.upload(boardType, port);
});
} else {
ArduShell.upload(boardType, port);
}
} else {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
}
}
/**
* @function 上传程序
* @description 通过所选择串口号开始一个上传过程
* @return void
*/
ArduShell.upload = (boardType, port) => {
if (!ArduShell.shellPath) {
ArduShell.shellPath = '';
}
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo('output');
const layerNum = layer.open({
type: 1,
title: Msg.Lang['shell.uploading'] + "...",
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: function () {
$(".layui-layer-page").css("z-index", "198910151");
$("#mixly-loader-btn").off("click").click(() => {
$("#mixly-loader-btn").css('display', 'none');
layer.title(Msg.Lang['shell.aborting'] + '...', layerNum);
ArduShell.cancel();
});
},
end: function () {
$('#mixly-loader-div').css('display', 'none');
$("layui-layer-shade" + layerNum).remove();
$("#mixly-loader-btn").off("click");
$("#mixly-loader-btn").css('display', 'inline-block');
}
});
mainStatusBarTabs.show();
statusBarTerminal.setValue(Msg.Lang['shell.uploading'] + "...\n");
const configPath = path.join(ArduShell.shellPath, '../arduino-cli.json'),
defaultLibPath = path.join(ArduShell.shellPath, '../libraries'),
buildPath = path.join(Env.clientPath, './mixlyBuild'),
buildCachePath = path.join(Env.clientPath, './mixlyBuildCache'),
codePath = path.join(Env.clientPath, './testArduino/testArduino.ino');
let cmdStr = '';
if (ArduShell.binFilePath !== '') {
cmdStr = '\"'
+ ArduShell.shellPath
+ '\" -b '
+ boardType
+ ' upload -p '
+ port
+ ' --config-file \"'
+ configPath
+ '\" --verbose '
+ '-i \"' + ArduShell.binFilePath + '\" --no-color';
ArduShell.binFilePath = '';
} else {
let myLibPath = path.join(Env.boardDirPath, "/libraries/myLib/");
if (fs_plus.isDirectorySync(myLibPath)) {
myLibPath += '\",\"';
} else {
myLibPath = '';
}
const thirdPartyPath = path.join(Env.boardDirPath, 'libraries/ThirdParty');
if (fs_plus.isDirectorySync(thirdPartyPath)) {
const libList = fs.readdirSync(thirdPartyPath);
for (let libName of libList) {
const libPath = path.join(thirdPartyPath, libName, 'libraries');
if (!fs_plus.isDirectorySync(libPath)) continue;
myLibPath += libPath + ',';
}
}
cmdStr = '\"'
+ ArduShell.shellPath
+ '\" compile -b '
+ boardType
+ ' --upload -p '
+ port
+ ' --config-file \"'
+ configPath
+ '\" --build-cache-path \"' + buildCachePath + '\" --verbose --libraries \"'
+ myLibPath
+ defaultLibPath
+ '\" --build-path \"'
+ buildPath
+ '\" \"'
+ codePath
+ '\" --no-color';
}
ArduShell.runCmd(layerNum, 'upload', cmdStr,
function () {
mainStatusBarTabs.add('serial', port);
mainStatusBarTabs.changeTo(port);
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
statusBarSerial.open();
}
);
}
/**
* @function 取消编译或上传
* @description 取消正在执行的编译或上传过程
* @return void
*/
ArduShell.cancel = function () {
ArduShell.shell && ArduShell.shell.kill();
ArduShell.shell = null;
}
/**
* @function 检测文件扩展名
* @description 检测文件扩展名是否在扩展名列表内
* @param fileName {String} 文件名
* @param extensionList {Array} 扩展名列表
* @return Boolean
*/
ArduShell.checkFileNameExtension = (fileName, extensionList) => {
if (!fileName) return false;
let fileNameToLowerCase = fileName;
let fileNameLen = fileNameToLowerCase.length;
let fileType = fileNameToLowerCase.substring(fileNameToLowerCase.lastIndexOf("."), fileNameLen);
if (extensionList.includes(fileType)) {
return true;
} else {
return false;
}
}
/**
* @function 检测文件扩展名
* @description 检测文件扩展名是否为.c/.cpp或.h/.hpp
* @param fileName {String} 文件名
* @return Boolean
*/
ArduShell.isCppOrHpp = (fileName) => {
return ArduShell.checkFileNameExtension(fileName, [".c", ".cpp", ".h", ".hpp"])
}
/**
* @function 检测文件扩展名
* @description 检测文件扩展名是否为.mix/.xml或.ino
* @param fileName {String} 文件名
* @return Boolean
*/
ArduShell.isMixOrIno = (fileName) => {
return ArduShell.checkFileNameExtension(fileName, [".mix", ".xml", ".ino"]);
}
/**
* @function 删除给定文件夹下文件
* @description 删除给定文件夹下.c/.cpp和.h/.hpp文件
* @param dir {String} 文件夹路径
* @return void
*/
ArduShell.clearDirCppAndHppFiles = (dir) => {
if (fs_plus.isDirectorySync(dir)) {
let libDir = fs.readdirSync(dir);
for (let i = 0; i < libDir.length; i++) {
if (ArduShell.isCppOrHpp(libDir[i])) {
const nowPath = path.join(dir, libDir[i]);
fs.unlinkSync(nowPath);
}
}
}
}
/**
* @function 拷贝文件
* @description 拷贝给定文件夹下.c/.cpp和.h/.hpp文件到目标目录
* @param oldDir {String} 起始文件夹路径
* @param newDir {String} 目标文件夹路径
* @return void
*/
ArduShell.copyHppAndCppFiles = (oldDir, newDir) => {
if (fs_plus.isDirectorySync(oldDir) && fs_plus.isDirectorySync(newDir)) {
let oldLibDir = fs.readdirSync(oldDir);
for (let i = 0; i < oldLibDir.length; i++) {
if (ArduShell.isCppOrHpp(oldLibDir[i])) {
const oldPath = path.join(oldDir, oldLibDir[i]);
const newPath = path.join(newDir, oldLibDir[i]);
try {
fs.copyFileSync(oldPath, newPath);
} catch (e) {
console.log(e);
}
}
}
}
}
/**
* @function 写库文件
* @description 将库文件数据写入本地
* @param inPath {string} 需要写入库文件的目录
* @return void
*/
ArduShell.writeLibFiles = (inPath) => {
return new Promise((resolve, reject) => {
const promiseList = [];
for (let name in Blockly.Arduino.libs_) {
const data = Blockly.Arduino.libs_[name];
const codePath = path.join(inPath, name + '.h');
promiseList.push(
new Promise((childResolve, childReject) => {
fs_extra.outputFile(codePath, data)
.finally(() => {
childResolve();
});
}
));
}
if (!promiseList.length) {
resolve();
return;
}
Promise.all(promiseList)
.finally(() => {
resolve();
});
});
}
/**
* @function 运行一个cmd命令
* @description 输入编译或上传的cmd命令
* @param cmd {String} 输入的cmd命令
* @return void
*/
ArduShell.runCmd = (layerNum, type, cmd, sucFunc) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
const testArduinoDirPath = path.join(Env.clientPath, 'testArduino');
const codePath = path.join(testArduinoDirPath, 'testArduino.ino');
const nowFilePath = Title.getFilePath();
ArduShell.clearDirCppAndHppFiles(testArduinoDirPath);
if (USER.compileCAndH === 'yes' && fs_plus.isFileSync(nowFilePath) && ArduShell.isMixOrIno(nowFilePath)) {
const nowDirPath = path.dirname(nowFilePath);
ArduShell.copyHppAndCppFiles(nowDirPath, testArduinoDirPath);
}
ArduShell.writeLibFiles(testArduinoDirPath)
.then(() => {
return fs_extra.outputFile(codePath, code);
})
.then(() => {
ArduShell.shell = new Shell(cmd);
return ArduShell.shell.exec(cmd);
})
.then((info) => {
layer.close(layerNum);
let message = '';
if (info.code) {
message = (type === 'compile' ? Msg.Lang['shell.compileFailed'] : Msg.Lang['shell.uploadFailed']);
statusBarTerminal.addValue("==" + message + "==\n");
} else {
message = (type === 'compile' ? Msg.Lang['shell.compileSucc'] : Msg.Lang['shell.uploadSucc']);
statusBarTerminal.addValue(`==${message}(${Msg.Lang['shell.timeCost']} ${info.time})==\n`);
sucFunc();
}
layer.msg(message, { time: 1000 });
})
.catch((error) => {
layer.close(layerNum);
console.log(error);
})
.finally(() => {
statusBarTerminal.scrollToBottom();
});
}
ArduShell.saveBinOrHex = function (writePath) {
ArduShell.writeFile(Env.clientPath + "/mixlyBuild", writePath);
}
ArduShell.writeFile = function (readPath, writePath) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
ArduShell.compile(function () {
window.setTimeout(function () {
const layerNum = layer.open({
type: 1,
title: Msg.Lang['shell.saving'] + '...',
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_ALL,
resize: false,
closeBtn: 0,
success: function () {
$(".layui-layer-page").css("z-index", "198910151");
$("#mixly-loader-btn").off("click").click(() => {
layer.close(layerNum);
ArduShell.cancel();
});
window.setTimeout(function () {
try {
readPath = readPath.replace(/\\/g, "/");
writePath = writePath.replace(/\\/g, "/");
} catch (e) {
console.log(e);
}
try {
let writeDirPath = writePath.substring(0, writePath.lastIndexOf("."));
let writeFileName = writePath.substring(writePath.lastIndexOf("/") + 1, writePath.lastIndexOf("."));
let writeFileType = writePath.substring(writePath.lastIndexOf(".") + 1);
if (!fs.existsSync(writeDirPath)) {
fs.mkdirSync(writeDirPath);
}
if (fs.existsSync(writePath)) {
fs.unlinkSync(writePath);
}
let readBinFilePath = readPath + "/testArduino.ino." + writeFileType;
let binFileData = fs.readFileSync(readBinFilePath);
fs.writeFileSync(writePath, binFileData);
let binFileType = [
".eep",
".hex",
".with_bootloader.bin",
".with_bootloader.hex",
".bin",
".elf",
".map",
".partitions.bin",
".bootloader.bin"
]
for (let i = 0; i < binFileType.length; i++) {
let readFilePath = readPath + "/testArduino.ino" + binFileType[i];
let writeFilePath = writeDirPath + "/" + writeFileName + binFileType[i];
if (fs.existsSync(readFilePath)) {
let binData = fs.readFileSync(readFilePath);
fs.writeFileSync(writeFilePath, binData);
}
}
layer.msg(Msg.Lang['shell.saveSucc'], {
time: 1000
});
} catch (e) {
console.log(e);
statusBarTerminal.addValue(e + "\n");
}
layer.close(layerNum);
}, 500);
},
end: function () {
$('#mixly-loader-div').css('display', 'none');
$("layui-layer-shade" + layerNum).remove();
$("#mixly-loader-btn").off("click");
}
});
}, 1000);
});
}
ArduShell.showUploadBox = function (filePath) {
const dirPath = path.dirname(filePath);
const fileName = path.basename(filePath, path.extname(filePath));
if (fs_plus.isDirectorySync(path.join(dirPath, fileName))) {
filePath = path.join(dirPath, fileName, path.basename(filePath));
}
const layerNum = layer.msg(Msg.Lang['shell.uploadWithFileInfo'], {
time: -1,
btn: [Msg.Lang['nav.btn.stop'], Msg.Lang['nav.btn.upload']],
shade: LayerExt.SHADE_ALL,
btnAlign: 'c',
yes: function () {
layer.close(layerNum);
ArduShell.binFilePath = '';
},
btn2: function () {
layer.close(layerNum);
ArduShell.uploadWithBinOrHex(filePath);
},
end: function () {
ArduShell.binFilePath = '';
}
});
}
ArduShell.uploadWithBinOrHex = function (filePath) {
layer.closeAll();
ArduShell.binFilePath = filePath;
ArduShell.initUpload();
}
});

View File

@@ -0,0 +1,740 @@
goog.loadJs('electron', () => {
goog.require('layui');
goog.require('Mixly.Config');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Env');
goog.require('Mixly.Boards');
goog.require('Mixly.MString');
goog.require('Mixly.Msg');
goog.require('Mixly.Workspace');
goog.require('Mixly.Debug');
goog.require('Mixly.Electron.Serial');
goog.provide('Mixly.Electron.BU');
const {
Electron,
Config,
LayerExt,
Env,
Boards,
MString,
Msg,
Workspace,
Serial,
Debug
} = Mixly;
const { BU } = Electron;
const { BOARD, SELECTED_BOARD, USER } = Config;
var downloadShell = null;
const { form } = layui;
const fs = Mixly.require('node:fs');
const fs_plus = Mixly.require('fs-plus');
const fs_extra = Mixly.require('fs-extra');
const path = Mixly.require('node:path');
const lodash_fp = Mixly.require('lodash/fp');
const child_process = Mixly.require('node:child_process');
const iconv_lite = Mixly.require('iconv-lite');
const os = Mixly.require('node:os');
BU.uploading = false;
BU.burning = false;
BU.shell = null;
/**
* @function 根据传入的stdout判断磁盘数量并选择对应操作
* @param type {string} 值为'burn' | 'upload'
* @param stdout {string} 磁盘名称字符串,形如'G:K:F:'
* @param startPath {string} 需要拷贝的文件路径
* @return {void}
**/
BU.checkNumOfDisks = function (type, stdout, startPath) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
let wmicResult = stdout;
wmicResult = wmicResult.replace(/\s+/g, "");
wmicResult = wmicResult.replace("DeviceID", "");
// wmicResult = 'G:K:F:';
let result = wmicResult.split(':');
let pathAdd = (Env.currentPlatform === "win32") ? ':' : '';
if (stdout.indexOf(":") != stdout.lastIndexOf(":")) {
let form = layui.form;
let devicesName = $('#mixly-selector-type');
let oldDevice = $('#mixly-selector-type option:selected').val();
devicesName.empty();
for (let i = 0; i < result.length; i++) {
if (result[i]) {
if (oldDevice == result[i] + pathAdd) {
devicesName.append('<option value="' + result[i] + pathAdd + '" selected>' + result[i] + pathAdd + '</option>');
} else {
devicesName.append('<option value="' + result[i] + pathAdd + '">' + result[i] + pathAdd + '</option>');
}
}
}
form.render();
let initBtnClicked = false;
const layerNum = layer.open({
type: 1,
id: "serial-select",
title: Msg.Lang['shell.tooManyDevicesInfo'],
area: ['350px', '150px'],
content: $('#mixly-selector-div'),
shade: LayerExt.SHADE_ALL,
resize: false,
closeBtn: 0,
success: function (layero) {
$('#serial-select').css('height', '195px');
$(".layui-layer-page").css("z-index","198910151");
$("#mixly-selector-btn1").off("click").click(() => {
layer.close(layerNum);
BU.cancel();
});
$("#mixly-selector-btn2").off("click").click(() => {
layer.close(layerNum);
initBtnClicked = true;
});
},
end: function () {
$('#mixly-selector-div').css('display', 'none');
$("#layui-layer-shade" + layerNum).remove();
if (initBtnClicked) {
BU.initWithDropdownBox(type, startPath);
}
$("#mixly-selector-btn1").off("click");
$("#mixly-selector-btn2").off("click");
}
});
} else {
const layerNum = layer.open({
type: 1,
title: (type === 'burn'? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']) + '...',
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_ALL,
resize: false,
closeBtn: 0,
success: function (layero, index) {
if (type === 'burn') {
BU.copyFiles(type, index, startPath, result[0] + pathAdd + '/');
} else {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
fs_extra.outputFile(startPath, code)
.then(() => {
BU.copyFiles(type, index, startPath, result[0] + pathAdd + '/');
})
.catch((error) => {
layer.close(index);
BU.burning = false;
BU.uploading = false;
statusBarTerminal.setValue(error + '\n');
});
}
$("#mixly-loader-btn").off("click").click(() => {
layer.close(index);
BU.cancel();
});
},
end: function () {
$('#mixly-selector-div').css('display', 'none');
$("#layui-layer-shade" + layerNum).remove();
$("#mixly-loader-btn").off("click");
}
});
}
}
/**
* @function 将文件或文件夹下所有文件拷贝到指定文件夹
* @param type {string} 值为'burn' | 'upload'
* @param layerNum {number} 烧录或上传加载弹窗的编号,用于关闭此弹窗
* @param startPath {string} 需要拷贝的文件或文件夹的路径
* @param desPath {string} 文件的目的路径
**/
BU.copyFiles = (type, layerNum, startPath, desPath) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const { burn, upload } = SELECTED_BOARD;
if (type === 'upload' && upload.copyLib) {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
let startLibPath = path.dirname(upload.filePath);
let pyFileArr = BU.copyLib(startPath, code);
startPath = path.dirname(startPath);
}
// 如果需要拷贝的是文件,则在目的路径后要加上文件名
if (fs_plus.isFileSync(startPath)) {
desPath = path.join(desPath, path.basename(startPath));
}
fs_extra.copy(startPath, desPath)
.then(() => {
layer.close(layerNum);
const message = (type === 'burn'? Msg.Lang['shell.burnSucc'] : Msg.Lang['shell.uploadSucc']);
layer.msg(message, {
time: 1000
});
statusBarTerminal.setValue(`==${message}==`);
if (type === 'upload' && Serial.uploadPorts.length === 1) {
if (USER.autoOpenPort === 'no') {
return;
}
mainStatusBarTabs.add('serial', port);
mainStatusBarTabs.changeTo(port);
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
statusBarSerial.open();
}
})
.catch((error) => {
layer.close(layerNum);
statusBarTerminal.setValue(error + '\n');
console.log(error);
})
.finally(() => {
BU.burning = false;
BU.uploading = false;
});
}
/**
* @function 判断当前环境,以开始一个上传过程
* @param type {string} 值为'burn' | 'upload'
* @param startPath {string} 需要拷贝的文件或文件夹的路径
* @return {void}
*/
BU.initWithDropdownBox = function (type, startPath) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const layerNum = layer.open({
type: 1,
title: (type === 'burn'? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']) + '...',
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_ALL,
resize: false,
closeBtn: 0,
success: function (layero, index) {
$(".layui-layer-page").css("z-index","198910151");
$("#mixly-loader-btn").off("click").click(() => {
layer.close(index);
BU.cancel();
});
const desPath = $('#mixly-selector-type option:selected').val();
if (type === 'burn') {
BU.copyFiles(type, index, startPath, desPath);
} else {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
fs_extra.outputFile(startPath, code)
.then(() => {
BU.copyFiles(type, index, startPath, desPath);
})
.catch((error) => {
layer.close(index);
BU.burning = false;
BU.uploading = false;
statusBarTerminal.setValue(error + '\n');
});
}
},
end: function () {
$('#mixly-loader-div').css('display', 'none');
$("#layui-layer-shade" + layerNum).remove();
}
});
}
/**
* @function 根据传入的盘符名称获取对应的磁盘名称
* @param type {string} 值为'burn' | 'upload'
* @param volumeName {string} 所要查找盘符的名称
* @param startPath {string} 需要拷贝文件的路径
* @return {void}
*/
BU.getDisksWithVolumesName = function (type, volumeName, startPath) {
let dirPath = path.dirname(startPath);
fs_extra.ensureDirSync(dirPath);
if (Env.currentPlatform === "win32") {
child_process.exec('wmic logicaldisk where "' + volumeName + '" get DeviceID', function (err, stdout, stderr) {
if (err || stderr) {
$('#mixly-loader-div').css('display', 'none');
console.log("root path open failed" + err + stderr);
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
BU.burning = false;
BU.uploading = false;
return;
}
BU.checkNumOfDisks(type, stdout, startPath);
});
} else {
let diskPath = '/Volumes/';
let addChar = ' ';
if (Env.currentPlatform === "linux") {
diskPath = '/media/';
addChar = '';
}
let stdout = '';
let result = null;
result = volumeName.split('/');
let deviceNum = 0;
for (var i = 0; i < result.length; i++) {
if (result[i] === '') continue;
for (var j = 0; ; j++) {
if (fs_plus.isDirectorySync(diskPath + result[i] + (j == 0 ? '' : (addChar + j)))) {
stdout += diskPath + result[i] + (j == 0 ? '' : (addChar + j)) + ':';
deviceNum++;
} else if (fs_plus.isDirectorySync(diskPath + os.userInfo().username + '/' + result[i] + (j == 0 ? '' : (addChar + j)))) {
stdout += diskPath + os.userInfo().username + '/' + result[i] + (j == 0 ? '' : (addChar + j)) + ':';
deviceNum++;
} else {
break;
}
}
}
if (deviceNum === 0) {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
BU.burning = false;
BU.uploading = false;
return;
}
BU.checkNumOfDisks(type, stdout, startPath);
}
}
/**
* @function 取消烧录或上传
* @return {void}
*/
BU.cancel = function () {
if (BU.shell) {
BU.shell.stdout.end();
BU.shell.stdin.end();
if (Env.currentPlatform === 'win32') {
child_process.exec('taskkill /pid ' + BU.shell.pid + ' /f /t');
} else {
BU.shell.kill("SIGTERM");
}
BU.shell = null;
} else {
if (BU.uploading) {
BU.uploading = false;
layer.msg(Msg.Lang['shell.uploadCanceled'], {
time: 1000
});
} else if (BU.burning) {
BU.burning = false;
layer.msg(Msg.Lang['shell.burnCanceled'], {
time: 1000
});
}
}
}
/**
* @function 开始一个烧录过程
* @return {void}
*/
BU.initBurn = function () {
if (BU.burning) return;
const { burn } = SELECTED_BOARD;
BU.burning = true;
BU.uploading = false;
if (burn.type === 'volume') {
BU.getDisksWithVolumesName('burn', burn.volume, burn.filePath);
} else {
const port = Serial.getSelectedPortName();
BU.burnWithPort(port, burn.command);
}
}
/**
* @function 开始一个上传过程
* @return {void}
*/
BU.initUpload = function () {
if (BU.uploading) return;
const { upload } = SELECTED_BOARD;
BU.burning = false;
BU.uploading = true;
if (upload.type === "volume") {
BU.getDisksWithVolumesName('upload', upload.volume, upload.filePath);
} else {
const port = Serial.getSelectedPortName();
BU.uploadWithPort(port, upload.command);
}
}
/**
* @function 递归代码找出import项并拷贝对应库文件到filePath所在目录
* @param filePath {string} 主代码文件所在路径
* @param code {string} 主代码数据
* @return {array} 库列表
**/
BU.copyLib = function (filePath, code) {
const dirPath = path.dirname(filePath);
const fileName = path.basename(filePath);
fs_extra.ensureDirSync(dirPath);
try {
const libFiles = fs.readdirSync(dirPath);
for (let value of libFiles) {
if (value !== fileName) {
fs.unlinkSync(path.join(dirPath, value));
}
}
} catch (e) {
console.log(e);
}
var pyFileArr = [];
pyFileArr = BU.searchLibs(dirPath, code, pyFileArr);
return pyFileArr;
}
/**
* @function 获取当前代码数据中所使用的库并检测此文件是否在库目录下存在,
* 若存在则拷贝到主文件所在目录
* @param dirPath {string} 主代码文件所在目录的路径
* @param code {string} 主代码数据
* @param libArr {array} 当前已查找出的库列表
* @return {array} 库列表
**/
BU.searchLibs = function (dirPath, code, libArr) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const { upload } = SELECTED_BOARD;
let arrayObj = new Array();
code.trim().split("\n").forEach(function (v, i) {
arrayObj.push(v);
});
let moduleName = "";
let pyFileArr = [];
for (let i = 0; i < arrayObj.length; i++) {
let fromLoc = arrayObj[i].indexOf("from");
let importLoc = arrayObj[i].indexOf("import");
const str = arrayObj[i].substring(0, (fromLoc === -1)? importLoc : fromLoc);
str.split('').forEach((ch) => {
if (ch !== ' ' && ch !== '\t') {
fromLoc = -1;
importLoc = -1;
return;
}
});
if (fromLoc !== -1) {
moduleName = arrayObj[i].substring(fromLoc + 4, arrayObj[i].indexOf("import"));
} else if (importLoc !== -1) {
let endPos = arrayObj[i].indexOf("as");
if (endPos === -1) {
endPos = arrayObj[i].length;
}
moduleName = arrayObj[i].substring(importLoc + 6, endPos);
} else {
continue;
}
moduleName = moduleName.replaceAll(' ', '');
moduleName = moduleName.replaceAll('\r', '');
let moduleArr = moduleName.split(",");
for (let j = 0; j < moduleArr.length; j++) {
if (!libArr.includes(moduleArr[j] + '.py') && !libArr.includes(moduleArr[j] + '.mpy')) {
try {
let oldLibPath = null;
if (!(upload.libPath && upload.libPath.length))
return;
for (let nowDirPath of upload.libPath) {
const nowMpyFilePath = path.join(nowDirPath, moduleArr[j] + '.mpy');
const nowPyFilePath = path.join(nowDirPath, moduleArr[j] + '.py');
if (fs_plus.isFileSync(nowMpyFilePath)) {
oldLibPath = nowMpyFilePath;
break;
} else if (fs_plus.isFileSync(nowPyFilePath)) {
oldLibPath = nowPyFilePath;
}
}
if (oldLibPath) {
const extname = path.extname(oldLibPath);
const newLibPath = path.join(dirPath, moduleArr[j] + extname);
statusBarTerminal.addValue(Msg.Lang['shell.copyLib'] + ' ' + moduleArr[j] + '\n');
fs.copyFileSync(oldLibPath, newLibPath);
libArr.push(moduleArr[j] + extname);
if (extname === '.py') {
pyFileArr.push(moduleArr[j] + extname);
code = fs.readFileSync(oldLibPath, 'utf8');
libArr = BU.searchLibs(dirPath, code, libArr);
}
}
} catch (e) {
console.log(e);
}
}
}
}
return libArr;
}
/**
* @function 通过cmd烧录
* @param layerNum {number} 烧录或上传加载弹窗的编号,用于关闭此弹窗
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
*/
BU.burnByCmd = function (layerNum, port, command) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
BU.runCmd(layerNum, 'burn', port, command);
}
/**
* @function 通过cmd上传
* @param layerNum {number} 烧录或上传加载弹窗的编号,用于关闭此弹窗
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
*/
BU.uploadByCmd = async function (layerNum, port, command) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.uploading'] + '...\n');
const { upload } = SELECTED_BOARD;
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
if (upload.copyLib) {
BU.copyLib(upload.filePath, code);
}
fs_extra.outputFile(upload.filePath, code)
.then(() => {
BU.runCmd(layerNum, 'upload', port, command);
})
.catch((error) => {
statusBarTerminal.setValue(error.toString() + '\n');
console.log(error);
layer.close(layerNum);
BU.uploading = false;
});
}
/**
* @function 运行cmd
* @param layerNum {number} 烧录或上传加载弹窗的编号,用于关闭此弹窗
* @param type {string} 值为 'burn' | 'upload'
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @param sucFunc {function} 指令成功执行后所要执行的操作
* @return {void}
*/
BU.runCmd = function (layerNum, type, port, command, sucFunc) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
let nowCommand = MString.tpl(command, { com: port });
BU.shell = child_process.exec(nowCommand, { encoding: 'binary' }, function (error, stdout, stderr) {
layer.close(layerNum);
BU.burning = false;
BU.uploading = false;
BU.shell = null;
const text = statusBarTerminal.getValue();
if (text.lastIndexOf('\n') !== text.length - 1) {
statusBarTerminal.addValue('\n');
}
if (error) {
if (Env.currentPlatform === 'win32') {
error = iconv_lite.decode(Buffer.from(error.message, 'binary'), 'cp936');
} else {
let lines = error.message.split('\n');
for (let i in lines) {
if (lines[i].indexOf('Command failed') !== -1) {
continue;
}
lines[i] = iconv_lite.decode(Buffer.from(lines[i], 'binary'), 'utf-8');
}
error = lines.join('\n');
}
error = MString.decode(error);
statusBarTerminal.addValue(error);
statusBarTerminal.addValue('==' + (type === 'burn' ? Msg.Lang['shell.burnFailed'] : Msg.Lang['shell.uploadFailed']) + '==\n');
} else {
layer.msg((type === 'burn' ? Msg.Lang['shell.burnSucc'] : Msg.Lang['shell.uploadSucc']), {
time: 1000
});
statusBarTerminal.addValue('==' + (type === 'burn' ? Msg.Lang['shell.burnSucc'] : Msg.Lang['shell.uploadSucc']) + '==\n');
if (type === 'upload') {
mainStatusBarTabs.show();
mainStatusBarTabs.add('serial', port);
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
statusBarSerial.setValue('');
mainStatusBarTabs.changeTo(port);
statusBarSerial.open().catch(Debug.error);
}
}
})
BU.shell.stdout.on('data', function (data) {
if (BU.uploading || BU.burning) {
data = iconv_lite.decode(Buffer.from(data, 'binary'), 'utf-8');
data = MString.decode(data);
statusBarTerminal.addValue(data);
}
});
}
/**
* @function 特殊固件的烧录
* @return {void}
**/
BU.burnWithSpecialBin = () => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const $selector = $('#mixly-selector-type');
let oldOption = $('#mixly-selector-type option:selected').val();
$selector.empty();
const firmwareList = SELECTED_BOARD.burn.special;
let firmwareObj = {};
for (let firmware of firmwareList) {
if (!firmware?.name && !firmware?.command) return;
firmwareObj[firmware.name] = firmware.command;
if (`${firmware.name}` == oldOption) {
$selector.append($(`<option value="${firmware.name}" selected>${firmware.name}</option>`));
} else {
$selector.append($(`<option value="${firmware.name}">${firmware.name}</option>`));
}
}
form.render();
let initBtnClicked = false;
const layerNum = layer.open({
type: 1,
id: "serial-select",
title: "请选择固件:",
area: ['350px', '150px'],
content: $('#mixly-selector-div'),
shade: Mixly.LayerExt.SHADE_ALL,
resize: false,
closeBtn: 0,
success: function (layero) {
$('#serial-select').css('height', '180px');
$('#serial-select').css('overflow', 'inherit');
$(".layui-layer-page").css("z-index", "198910151");
$("#mixly-selector-btn1").off("click").click(() => {
layer.close(layerNum);
});
$("#mixly-selector-btn2").click(() => {
layer.close(layerNum);
initBtnClicked = true;
});
},
end: function () {
$("#mixly-selector-btn1").off("click");
$("#mixly-selector-btn2").off("click");
$('#mixly-selector-div').css('display', 'none');
$(".layui-layer-shade").remove();
if (initBtnClicked) {
let selectedFirmwareName = $('#mixly-selector-type option:selected').val();
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
BU.burning = true;
BU.uploading = false;
const port = Serial.getSelectedPortName();
BU.burnWithPort(port, firmwareObj[selectedFirmwareName]);
} else {
layer.msg(Msg.Lang['shell.burnCanceled'], { time: 1000 });
}
}
});
}
/**
* @function 通过串口执行命令行烧录或上传操作
* @param type {string} 值为 'burn' | 'upload'
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
**/
BU.operateWithPort = (type, port, command) => {
if (!port) {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
BU.burning = false;
BU.uploading = false;
return;
}
const title = (type === 'burn' ? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']) + '...';
const operate = () => {
const layerNum = layer.open({
type: 1,
title,
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: function (layero, index) {
$(".layui-layer-page").css("z-index","198910151");
switch (type) {
case 'burn':
BU.burnByCmd(index, port, command);
break;
case 'upload':
default:
BU.uploadByCmd(index, port, command);
}
$("#mixly-loader-btn").off("click").click(() => {
$("#mixly-loader-btn").css('display', 'none');
layer.title(Msg.Lang['shell.aborting'] + '...', index);
BU.cancel(type);
});
},
end: function () {
$('#mixly-loader-div').css('display', 'none');
$("layui-layer-shade" + layerNum).remove();
$("#mixly-loader-btn").off("click");
$("#mixly-loader-btn").css('display', 'inline-block');
}
});
}
const { mainStatusBarTabs } = Mixly;
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
if (statusBarSerial) {
statusBarSerial.close()
.finally(() => {
operate();
});
} else {
operate();
}
}
/**
* @function 通过串口执行命令行烧录操作
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
**/
BU.burnWithPort = (port, command) => {
BU.operateWithPort('burn', port, command);
}
/**
* @function 通过串口执行命令行上传操作
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
**/
BU.uploadWithPort = (port, command) => {
BU.operateWithPort('upload', port, command);
}
});

View File

@@ -0,0 +1,115 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mixly.Env');
goog.require('Mixly.MJSON');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.CloudDownload');
const {
Env,
MJSON,
Electron
} = Mixly;
const { CloudDownload } = Electron;
const fs = Mixly.require('fs');
const fs_plus = Mixly.require('fs-plus');
const fs_extra = Mixly.require('fs-extra');
const node_downloader_helper = Mixly.require('node-downloader-helper');
CloudDownload.getJson = (url, downloadDir, endFunc) => {
if (url) {
CloudDownload.download(url, downloadDir)
.then((message) => {
if (message[0]) {
throw message[0];
} else {
let jsonObj = null;
if (fs_plus.isFileSync(message[1])) {
let data = fs.readFileSync(message[1], 'utf-8');
jsonObj = MJSON.parse(data);
}
if (jsonObj) {
return jsonObj;
} else {
throw('解析失败');
}
}
})
.then((configObj) => {
endFunc([null, configObj]);
})
.catch((error) => {
endFunc([error, null]);
});
} else {
endFunc(['url读取出错', null]);
}
}
CloudDownload.download = (url, downloadDir, options = {}) => {
return new Promise((resolve, reject) => {
const DEFAULT_OPTIONS = {
progress: null,
end: null,
error: null
}
if (typeof options !== 'object')
options = DEFAULT_OPTIONS;
else
options = { ...DEFAULT_OPTIONS, ...options };
try {
fs_extra.ensureDirSync(downloadDir);
const fileName = path.basename(url);
const filePath = path.join(downloadDir, './' + fileName);
if (fs_plus.isFileSync(filePath))
fs_extra.removeSync(filePath);
} catch (error) {
resolve([error, url]);
return;
}
const { DownloaderHelper } = node_downloader_helper;
const dl = new DownloaderHelper(url, downloadDir, {
override: true,
timeout: 15000,
retry: false
});
dl.on('progress', (stats) => {
if (typeof options.progress === 'function') {
options.progress(stats);
}
});
dl.on('end', (downloadInfo) => {
if (typeof options.end === 'function') {
options.end(downloadInfo);
}
resolve([null, downloadInfo.filePath]);
});
dl.on('error', (error) => {
console.log('Download Failed', error);
if (typeof options.error === 'function') {
options.error(error);
}
resolve([error, url]);
});
dl.on('timeout', () => {
console.log('Download Timeout');
if (typeof options.timeout === 'function') {
options.timeout('Download Timeout');
}
resolve(['Download Timeout', url]);
});
dl.start().catch((error) => {
console.log('Download Failed', error);
if (typeof options.error === 'function') {
options.error(error);
}
resolve([error, url]);
});
});
}
});

View File

@@ -0,0 +1,50 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mixly.Url');
goog.provide('Mixly.Electron');
const {
Url,
Env,
Electron
} = Mixly;
const electron_remote = Mixly.require('@electron/remote');
const {
Menu,
BrowserWindow
} = electron_remote;
Electron.newBrowserWindow = (indexPath, config = {}) => {
Menu.setApplicationMenu(null);
const win = new BrowserWindow({
...{
show: false,
minHeight: 400,
minWidth: 700,
width: 0,
height: 0,
icon: path.join(Env.indexDirPath, '../files/mixly.ico'),
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false,
spellcheck: false
}
},
...(config.window ?? {})
});
win.loadFile(indexPath);
win.once('ready-to-show', () => {
win.maximize();
win.show();
});
return win;
}
});

View File

@@ -0,0 +1,23 @@
goog.loadJs('electron', () => {
goog.require('Mixly.Command');
goog.require('Mixly.Config');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.Events');
const { Command, Config, Electron } = Mixly;
const { Events } = Electron;
const { SOFTWARE } = Config;
const electron = Mixly.require('electron');
const ipcRenderer = electron.ipcRenderer;
ipcRenderer.on('command', (event, message) => {
if (SOFTWARE.debug) {
console.log('receive -> ', message);
}
const commandObj = Command.parse(message);
Command.run(commandObj);
});
});

View File

@@ -0,0 +1,172 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mixly.FileTree');
goog.require('Mixly.Events');
goog.require('Mixly.Registry');
goog.require('Mixly.Debug');
goog.require('Mixly.Electron.FS');
goog.provide('Mixly.Electron.FileTree');
const {
FileTree,
Events,
Registry,
Debug,
Electron
} = Mixly;
const { FS } = Electron;
const chokidar = Mixly.require('chokidar');
class FileTreeExt extends FileTree {
static {
this.worker = new Worker('../common/modules/mixly-modules/workers/nodejs/node-file-watcher.js', {
name: 'nodeFileWatcher'
});
this.watcherEventsRegistry = new Registry();
this.worker.addEventListener('message', (event) => {
const { data } = event;
const events = this.watcherEventsRegistry.getItem(data.watcher);
if (!events) {
return;
}
events.run('change', data);
});
this.worker.addEventListener('error', (event) => {
Debug.error(event);
});
this.addEventListener = function(folderPath, func) {
FileTreeExt.watch(folderPath);
let events = this.watcherEventsRegistry.getItem(folderPath);
if (!events) {
events = new Events(['change']);
this.watcherEventsRegistry.register(folderPath, events);
}
return events.bind('change', func);
}
this.removeEventListener = function(folderPath, eventId) {
let events = this.watcherEventsRegistry.getItem(folderPath);
if (!events) {
return;
}
if (!events.length('change')) {
this.watcherEventsRegistry.unregister(folderPath);
this.unwatch(folderPath);
}
}
this.watch = function(folderPath) {
FileTreeExt.worker.postMessage({
func: 'watch',
args: [folderPath]
});
}
this.unwatch = function(folderPath) {
FileTreeExt.worker.postMessage({
func: 'unwatch',
args: [folderPath]
});
}
}
constructor() {
super(FS);
this.watcher = null;
this.watcherEventsListenerIdRegistry = new Registry();
}
async readFolder(inPath) {
let output = [];
const fs = this.getFS();
const status = await fs.isDirectory(inPath);
if (!status) {
return output;
}
const children = await fs.readDirectory(inPath);
for (let data of children) {
const dataPath = path.join(inPath, data);
if (await fs.isDirectory(dataPath)) {
const isDirEmtpy = await fs.isDirectoryEmpty(dataPath);
output.push({
type: 'folder',
id: dataPath,
children: !isDirEmtpy
});
if (isDirEmtpy) {
this.watchEmptyFolder(dataPath);
}
} else {
output.push({
type: 'file',
id: dataPath,
children: false
});
}
}
return output;
}
watchFolder(folderPath) {
super.watchFolder(folderPath);
let id = this.watcherEventsListenerIdRegistry.getItem(folderPath);
if (id) {
return;
}
id = FileTreeExt.addEventListener(folderPath, (data) => {
if (data.event === 'unlinkDir') {
this.unwatchFolder(path.join(data.path));
}
const watcherPath = path.join(data.watcher);
if (this.isWatched(watcherPath)) {
this.refreshFolder(watcherPath);
}
});
this.watcherEventsListenerIdRegistry.register(folderPath, id);
}
watchEmptyFolder(folderPath) {
super.watchFolder(folderPath);
let id = this.watcherEventsListenerIdRegistry.getItem(folderPath);
if (id) {
return;
}
id = FileTreeExt.addEventListener(folderPath, (data) => {
const watcherPath = path.join(data.watcher);
if (this.isWatched(watcherPath)) {
this.refreshFolder(watcherPath);
}
if (this.isClosed(watcherPath)) {
this.unwatchFolder(watcherPath);
}
});
this.watcherEventsListenerIdRegistry.register(folderPath, id);
}
unwatchFolder(folderPath) {
const keys = this.watchRegistry.keys();
for (let key of keys) {
if (key.indexOf(folderPath) === -1) {
continue;
}
const type = this.watchRegistry.getItem(key);
if (type === 'file') {
this.unwatchFile(key);
}
const id = this.watcherEventsListenerIdRegistry.getItem(key);
if (!id) {
continue;
}
FileTreeExt.removeEventListener(key, id);
this.watcherEventsListenerIdRegistry.unregister(key);
}
super.unwatchFolder(folderPath);
}
}
Electron.FileTree = FileTreeExt;
});

View File

@@ -0,0 +1,291 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Blockly');
goog.require('Mixly.Env');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Config');
goog.require('Mixly.Title');
goog.require('Mixly.MFile');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.require('Mixly.Workspace');
goog.require('Mixly.Electron.ArduShell');
goog.require('Mixly.Electron.BU');
goog.provide('Mixly.Electron.File');
const {
Env,
LayerExt,
Config,
Title,
MFile,
XML,
Msg,
Workspace,
Electron
} = Mixly;
const { BOARD } = Config;
const { ArduShell, BU, File } = Electron;
const fs = Mixly.require('fs');
const fs_plus = Mixly.require('fs-plus');
const fs_extra = Mixly.require('fs-extra');
const fs_promise = Mixly.require('node:fs/promises');
const electron_remote = Mixly.require('@electron/remote');
const { dialog, app } = electron_remote;
const { MSG } = Blockly.Msg;
File.DEFAULT_PATH = path.join(app.getAppPath(), 'src/sample');
File.workingPath = File.DEFAULT_PATH;
File.openedFilePath = null;
File.userPath = {
img: null,
mix: null,
code: null,
hex: null
}
File.showSaveDialog = (title, filters, endFunc) => {
const currentWindow = electron_remote.getCurrentWindow();
currentWindow.focus();
dialog.showSaveDialog(currentWindow, {
title,
defaultPath: File.workingPath,
filters,
// nameFieldLabel: Msg.Lang['替换文件'],
showsTagField: true,
properties: ['showHiddenFiles'],
message: title
}).then(result => {
let res = result.filePath;
if (res)
endFunc(res);
}).catch(error => {
console.log(error);
});
}
File.showOpenDialog = (title, filters, endFunc) => {
const currentWindow = electron_remote.getCurrentWindow();
currentWindow.focus();
dialog.showOpenDialog(currentWindow, {
title,
defaultPath: File.workingPath,
filters,
properties: ['openFile', 'showHiddenFiles'],
message: title
})
.then(result => {
let res = result.filePaths[0];
if (res)
endFunc(res);
})
.catch(error => {
console.log(error);
});
}
File.save = (endFunc = () => {}) => {
if (File.openedFilePath) {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const extname = path.extname(File.openedFilePath);
let data = '';
switch (extname) {
case '.mix':
data = editor.getValue();
break;
case '.py':
case '.ino':
data = editor.getCode();
break;
default:
layer.msg(Msg.Lang['file.type.error'], { time: 1000 });
return;
}
fs_extra.outputFile(File.openedFilePath, data)
.then(() => {
Title.updeteFilePath(File.openedFilePath);
layer.msg(Msg.Lang['file.saveSucc'], { time: 1000 });
})
.catch((error) => {
File.openedFilePath = null;
console.log(error);
layer.msg(Msg.Lang['file.saveFailed'], { time: 1000 });
})
.finally(() => {
endFunc();
})
} else {
File.saveAs(endFunc);
}
}
File.saveAs = (endFunc = () => {}) => {
File.showSaveDialog(Msg.Lang['file.saveAs'], MFile.saveFilters, (filePath) => {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const extname = path.extname(filePath);
if (['.mix', '.py', '.ino'].includes(extname)) {
File.openedFilePath = filePath;
File.workingPath = path.dirname(filePath);
File.save(endFunc);
} else {
switch (extname) {
case '.bin':
case '.hex':
if (BOARD?.nav?.compile) {
ArduShell.saveBinOrHex(filePath);
} else {
const hexStr = MFile.getHex();
fs_extra.outputFile(filePath, hexStr)
.then(() => {
layer.msg(Msg.Lang['file.saveSucc'], { time: 1000 });
})
.catch((error) => {
console.log(error);
layer.msg(Msg.Lang['file.saveFailed'], { time: 1000 });
})
.finally(() => {
endFunc();
});
}
break;
case '.mil':
const milStr = editor.getMil();
const $mil = $(milStr);
$mil.attr('name', path.basename(filePath, '.mil'));
fs_extra.outputFile(filePath, $mil[0].outerHTML)
.then(() => {
layer.msg('file.saveSucc', { time: 1000 });
})
.catch((error) => {
console.log(error);
layer.msg(Msg.Lang['file.saveFailed'], { time: 1000 });
})
.finally(() => {
endFunc();
});
break;
default:
layer.msg(Msg.Lang['file.type.error'], { time: 1000 });
endFunc();
}
}
});
}
File.exportLib = (endFunc = () => {}) => {
File.showSaveDialog(Msg.Lang['file.exportAs'], [ MFile.SAVE_FILTER_TYPE.mil ], (filePath) => {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const milStr = editor.getMil();
const $mil = $(milStr);
$mil.attr('name', path.basename(filePath, '.mil'));
fs_extra.outputFile(filePath, $mil[0].outerHTML)
.then(() => {
layer.msg('file.saveSucc', { time: 1000 });
})
.catch((error) => {
console.log(error);
layer.msg(Msg.Lang['file.saveFailed'], { time: 1000 });
})
.finally(() => {
endFunc();
});
});
}
File.new = () => {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const blockEditor = editor.getPage('block').getEditor();
const codeEditor = editor.getPage('code').getEditor();
const generator = Blockly.generator;
const blocksList = blockEditor.getAllBlocks();
if (editor.getPageType() === 'code') {
const code = codeEditor.getValue(),
workspaceToCode = generator.workspaceToCode(blockEditor) || '';
if (!blocksList.length && workspaceToCode === code) {
layer.msg(Msg.Lang['editor.codeEditorEmpty'], { time: 1000 });
File.openedFilePath = null;
Title.updateTitle(Title.title);
return;
}
} else {
if (!blocksList.length) {
layer.msg(Msg.Lang['editor.blockEditorEmpty'], { time: 1000 });
File.openedFilePath = null;
Title.updateTitle(Title.title);
return;
}
}
layer.confirm(MSG['confirm_newfile'], {
title: false,
shade: LayerExt.SHADE_ALL,
resize: false,
success: (layero) => {
const { classList } = layero[0].childNodes[1].childNodes[0];
classList.remove('layui-layer-close2');
classList.add('layui-layer-close1');
},
btn: [Msg.Lang['nav.btn.ok'], Msg.Lang['nav.btn.cancel']],
btn2: (index, layero) => {
layer.close(index);
}
}, (index, layero) => {
layer.close(index);
blockEditor.clear();
blockEditor.scrollCenter();
Blockly.hideChaff();
codeEditor.setValue(generator.workspaceToCode(blockEditor) || '', -1);
File.openedFilePath = null;
Title.updateTitle(Title.title);
});
}
File.open = () => {
File.showOpenDialog(Msg.Lang['file.open'], [
{ name: Msg.Lang['file.type.mix'], extensions: MFile.openFilters }
], (filePath) => {
File.openFile(filePath);
});
}
File.openFile = (filePath) => {
const extname = path.extname(filePath);
let data;
if (!fs_plus.isFileSync(filePath)) {
console.log(filePath + '不存在');
return;
}
try {
data = fs.readFileSync(filePath, 'utf-8');
} catch (error) {
console.log(error);
return;
}
if (['.bin', '.hex'].includes(extname)) {
if (BOARD?.nav?.compile) {
ArduShell.showUploadBox(filePath);
} else {
MFile.loadHex(data);
}
} else if (['.mix', '.xml', '.ino', '.py'].includes(extname)) {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
editor.setValue(data, extname);
File.openedFilePath = filePath;
File.workingPath = path.dirname(filePath);
Title.updeteFilePath(File.openedFilePath);
} else {
layer.msg(Msg.Lang['file.type.error'], { time: 1000 });
}
}
});

View File

@@ -0,0 +1,110 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('layui');
goog.require('Mixly.Env');
goog.require('Mixly.Config');
goog.require('Mixly.MFile');
goog.require('Mixly.Title');
goog.require('Mixly.XML');
goog.require('Mixly.FooterLayerExample');
goog.require('Mixly.Electron.File');
goog.provide('Mixly.Electron.FooterLayerExample');
const {
Env,
Config,
MFile,
Title,
XML,
FooterLayerExample,
Electron
} = Mixly;
const { dropdown, tree } = layui;
const { File } = Electron;
const { BOARD } = Config;
const fs = Mixly.require('fs');
const fs_plus = Mixly.require('fs-plus');
const electron_remote = Mixly.require('@electron/remote');
const { app } = electron_remote;
class FooterLayerExampleExt extends FooterLayerExample {
constructor(element) {
super(element);
}
getRoot() {
let exampleList = [];
let samplePath = path.join(Env.boardDirPath, 'examples');
const sampleList = this.getExamplesByPath(samplePath, '.mix');
if (sampleList.length) {
exampleList.push({
id: samplePath,
title: BOARD.boardType,
children: []
});
}
const thirdPartyPath = path.join(Env.boardDirPath, 'libraries/ThirdParty');
if (fs_plus.isDirectorySync(thirdPartyPath)) {
const libList = fs.readdirSync(thirdPartyPath);
for (let lib of libList) {
const libPath = path.join(thirdPartyPath, lib);
if (fs_plus.isFileSync(libPath))
continue;
const examplesPath = path.join(libPath, 'examples');
if (fs_plus.isFileSync(examplesPath))
continue;
const thirdPartyList = this.getExamplesByPath(examplesPath, '.mix');
if (thirdPartyList.length) {
exampleList.push({
id: examplesPath,
title: lib,
children: []
});
}
}
}
return exampleList;
}
getChildren(inPath) {
return this.getExamplesByPath(inPath, '.mix');
}
dataToWorkspace(inPath) {
if (!fs_plus.isFileSync(inPath)) {
return;
}
const data = fs.readFileSync(inPath, 'utf8');
const extname = path.extname(inPath);
this.updateCode(extname, data);
File.openedFilePath = null;
}
getExamplesByPath(inPath, fileExtname) {
let exampleList = [];
if (fs_plus.isDirectorySync(inPath)) {
const dataList = fs.readdirSync(inPath);
for (let data of dataList) {
const dataPath = path.join(inPath, data);
if (fs_plus.isDirectorySync(dataPath)) {
exampleList.push({ title: data, id: dataPath, children: [] });
} else {
const extname = path.extname(data);
if (extname === fileExtname) {
exampleList.push({ title: data, id: dataPath });
}
}
}
}
return exampleList;
}
}
Electron.FooterLayerExample = FooterLayerExampleExt;
});

View File

@@ -0,0 +1,153 @@
goog.loadJs('electron', () => {
goog.require('layui');
goog.require('path');
goog.require('Mustache');
goog.require('Mixly.Env');
goog.require('Mixly.FSBoard');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Debug');
goog.require('Mixly.Msg');
goog.require('Mixly.Electron.Shell');
goog.provide('Mixly.Electron.FSBoard');
const {
Env,
FSBoard,
LayerExt,
Debug,
Msg,
Electron = {}
} = Mixly;
const { Shell } = Electron;
const fs_extra = Mixly.require('fs-extra');
const { layer } = layui;
class FSBoardExt extends FSBoard {
#shell_ = null;
constructor() {
super();
this.#shell_ = new Shell();
}
download(usrFolder, fsType) {
return new Promise(async (resolve, reject) => {
try {
await super.download(usrFolder, fsType);
} catch (error) {
Debug.error(error);
resolve();
return;
}
const layerNum = layer.open({
type: 1,
title: `${Msg.Lang['shell.downloading']}...`,
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: () => {
$("#mixly-loader-btn").off("click").click(() => {
$("#mixly-loader-btn").css('display', 'none');
layer.title(Msg.Lang['shell.aborting'] + '...', layerNum);
this.cancel();
});
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo('output');
const commandTemplate = this.getSelectedFSDownloadCommand();
const command = this.#renderTemplate_(commandTemplate);
this.#shell_.exec(command)
.then((info) => {
if (info.code) {
statusBarTerminal.addValue(`\n==${Msg.Lang['dushell.downloadFailed']}==\n`);
} else {
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.burdownloadSucc']}(${Msg.Lang['shell.timeCost']} ${info.time})==\n`);
}
})
.catch(Debug.error)
.finally(() => {
layer.close(layerNum);
resolve();
});
},
end: function () {
$("#mixly-loader-btn").off("click");
$("#mixly-loader-btn").css('display', 'inline-block');
}
});
});
}
upload(usrFolder, fsType) {
return new Promise(async (resolve, reject) => {
try {
await super.upload(usrFolder, fsType);
} catch (error) {
Debug.error(error);
resolve();
return;
}
const layerNum = layer.open({
type: 1,
title: Msg.Lang['shell.uploading'] + '...',
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: () => {
$("#mixly-loader-btn").off("click").click(() => {
$("#mixly-loader-btn").css('display', 'none');
layer.title('上传终止中...', layerNum);
this.cancel();
});
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo('output');
const commandTemplate = this.getSelectedFSUploadCommand();
const command = this.#renderTemplate_(commandTemplate);
this.#shell_.exec(command)
.then((info) => {
if (info.code) {
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.uploadFailed']}==\n`);
} else {
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.uploadSucc']}(${Msg.Lang['shell.timeCost']} ${info.time})==\n`);
}
})
.catch(Debug.error)
.finally(() => {
layer.close(layerNum);
resolve();
});
},
end: function () {
$("#mixly-loader-btn").off("click");
$("#mixly-loader-btn").css('display', 'inline-block');
}
});
});
}
#renderTemplate_(template) {
const config = this.getConfig();
return Mustache.render(template, {
...config,
python3: `"${Env.python3Path}"`,
esptool: `"${Env.python3Path}" "${path.join(Env.srcDirPath, 'tools/python/esptool/__init__.py')}"`
});
}
cancel() {
this.#shell_ && this.#shell_.kill();
}
}
Electron.FSBoard = FSBoardExt;
});

View File

@@ -0,0 +1,158 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mixly.Msg');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.FS');
const { Msg, Electron } = Mixly;
const { FS } = Electron;
const fs_plus = Mixly.require('fs-plus');
const fs_extra = Mixly.require('fs-extra');
const fs_promise = Mixly.require('node:fs/promises');
const electron_remote = Mixly.require('@electron/remote');
const { dialog, app } = electron_remote;
FS.showOpenFilePicker = () => {
return new Promise((resolve, reject) => {
const currentWindow = electron_remote.getCurrentWindow();
currentWindow.focus();
dialog.showOpenDialog(currentWindow, {
title: Msg.Lang['file.openFile'],
defaultPath: File.workingPath,
filters,
properties: ['openFile', 'showHiddenFiles'],
message: Msg.Lang['file.openFile']
})
.then(result => {
const filePath = result.filePaths[0];
if (filePath) {
resolve(new File(filePath));
} else {
reject('file not found');
}
})
.catch(error => {
reject(error);
});
});
}
FS.showDirectoryPicker = () => {
return new Promise((resolve, reject) => {
const currentWindow = electron_remote.getCurrentWindow();
currentWindow.focus();
dialog.showOpenDialog(currentWindow, {
title: Msg.Lang['file.openFolder'],
// defaultPath: File.workingPath,
// filters,
properties: ['openDirectory', 'createDirectory'],
message: Msg.Lang['file.openFolder']
})
.then(result => {
const folderPath = result.filePaths[0];
if (folderPath) {
resolve(folderPath);
} else {
resolve(null);
}
})
.catch(reject);
});
}
FS.showSaveFilePicker = (fileName, ext) => {
return new Promise((resolve, reject) => {
const currentWindow = electron_remote.getCurrentWindow();
currentWindow.focus();
dialog.showSaveDialog(currentWindow, {
filters: [{
name: Msg.Lang['file.type.mix'],
extensions: [ext.substring(ext.lastIndexOf('.') + 1)]
}],
defaultPath: fileName,
showsTagField: true,
properties: ['showHiddenFiles']
}).then(result => {
let filePath = result.filePath;
if (filePath) {
resolve(filePath);
} else {
resolve(null);
}
}).catch(reject);
});
}
FS.createFile = (filePath) => {
return fs_extra.ensureFile(filePath);
}
FS.readFile = (filePath) => {
return fs_promise.readFile(filePath, { encoding: 'utf8' });
}
FS.writeFile = (filePath, data) => {
return fs_promise.writeFile(filePath, data, { encoding: 'utf8' });
}
FS.isFile = (filePath) => {
return new Promise((resolve, reject) => {
resolve(fs_plus.isFileSync(filePath));
});
}
FS.renameFile = (oldFilePath, newFilePath) => {
return fs_promise.rename(oldFilePath, newFilePath);
}
FS.moveFile = (oldFilePath, newFilePath) => {
return fs_extra.move(oldFilePath, newFilePath, { overwrite: true });
}
FS.copyFile = (oldFilePath, newFilePath) => {
return fs_extra.copy(oldFilePath, newFilePath);
}
FS.deleteFile = (filePath) => {
return fs_extra.remove(filePath);
}
FS.createDirectory = (folderPath) => {
return fs_extra.ensureDir(folderPath);
}
FS.readDirectory = (folderPath) => {
return fs_promise.readdir(folderPath);
}
FS.isDirectory = (folderPath) => {
return new Promise((resolve, reject) => {
fs_plus.isDirectory(folderPath, (status) => {
resolve(status);
});
});
}
FS.isDirectoryEmpty = async (folderPath) => {
return !(await FS.readDirectory(folderPath)).length;
}
FS.renameDirectory = (oldFolderPath, newFolderPath) => {
return fs_promise.rename(oldFolderPath, newFolderPath);
}
FS.moveDirectory = (oldFolderPath, newFolderPath) => {
return fs_extra.move(oldFolderPath, newFolderPath, { overwrite: true });
}
FS.copyDirectory = (oldFolderPath, newFolderPath) => {
return fs_extra.copy(oldFolderPath, newFolderPath);
}
FS.deleteDirectory = (folderPath) => {
return fs_extra.remove(folderPath);
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
goog.loadJs('electron', () => {
goog.require('Mixly.Url');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.Electron.Serial');
goog.require('Mixly.Electron.PythonShell');
goog.require('Mixly.Electron.Events');
goog.provide('Mixly.Electron.Loader');
const {
Url,
Config,
Env,
Electron
} = Mixly;
const { BOARD } = Config;
const {
Serial,
PythonShell,
Loader
} = Electron;
Loader.onbeforeunload = function(reload = false) {
const pageReload = (href) => {
if (!reload) {
window.location.replace(href);
} else {
window.location.reload(true);
}
}
let href = Config.pathPrefix + 'index.html?' + Url.jsonToUrl({ boardType: BOARD.boardType ?? 'None' });
let endPromise = [];
const { mainStatusBarTabs } = Mixly;
Serial.getCurrentPortsName().map((name) => {
const statusBarSerial = mainStatusBarTabs.getStatusBarById(name);
if (statusBarSerial) {
endPromise.push(statusBarSerial.close());
}
});
endPromise.push(PythonShell.stop());
Promise.all(endPromise)
.finally(() => {
pageReload(href);
});
};
Loader.closePort = (serialport) => {
return new Promise((resolve, reject) => {
serialport.close(() => {
resolve();
});
})
}
Loader.reload = () => {
Loader.onbeforeunload(true);
}
});

View File

@@ -0,0 +1,214 @@
goog.loadJs('electron', () => {
goog.require('dayjs.duration');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.Workspace');
goog.require('Mixly.MString');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.PythonShell');
const {
Env,
Msg,
Debug,
Workspace,
MString,
Electron
} = Mixly;
const iconv_lite = Mixly.require('iconv-lite');
const python_shell = Mixly.require('python-shell');
const child_process = Mixly.require('node:child_process');
const fs_extra = Mixly.require('fs-extra');
class PythonShell {
static {
this.ENCODING = Env.currentPlatform == 'win32' ? 'cp936' : 'utf-8';
this.pythonShell = null;
this.init = function () {
this.pythonShell = new PythonShell();
}
this.run = function () {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
return this.pythonShell.run(code);
}
this.stop = function () {
return this.pythonShell.stop();
}
}
#shell_ = null;
#statusBarTerminal_ = null;
#options_ = {
pythonPath: Env.python3Path,
pythonOptions: ['-u'],
encoding: 'binary',
mode: 'utf-8'
};
#cursor_ = {
row: 0,
column: 0
};
#prompt_ = '';
#waittingForInput_ = false;
#running_ = false;
#onCursorChangeEvent_ = () => this.#onCursorChange_();
#commands_ = [
{
name: 'REPL-Enter',
bindKey: 'Enter',
exec: (editor) => {
const session = editor.getSession();
const cursor = session.selection.getCursor();
if (cursor.row === this.#cursor_.row) {
const newPos = this.#statusBarTerminal_.getEndPos();
let str = this.#statusBarTerminal_.getValueRange(this.#cursor_, newPos);
str = str.replace(this.#prompt_, '');
this.#shell_.stdin.write(escape(str) + '\n');
this.#statusBarTerminal_.addValue('\n');
this.#exitInput_();
return true;
}
return false;
}
}, {
name: 'REPL-ChangeEditor',
bindKey: 'Delete|Ctrl-X|Backspace',
exec: (editor) => {
const session = editor.getSession();
const cursor = session.selection.getCursor();
if (cursor.row < this.#cursor_.row || cursor.column <= this.#cursor_.column) {
return true;
}
return false;
}
}
];
constructor(config) {
Object.assign(this.#options_, config);
const { mainStatusBarTabs } = Mixly;
this.#statusBarTerminal_ = mainStatusBarTabs.getStatusBarById('output');
}
#addEventsListener_() {
const { stdout, stderr } = this.#shell_;
stdout.setEncoding('binary');
stdout.on('data', (data) => {
data = iconv_lite.decode(Buffer.from(data, 'binary'), PythonShell.ENCODING);
data = MString.decode(data);
this.#statusBarTerminal_.addValue(data);
const keyIndex = data.lastIndexOf('>>>');
if (keyIndex !== -1) {
this.#prompt_ = data.substring(keyIndex);
setTimeout(() => this.#enterInput_(), 500);
}
});
stderr.setEncoding('binary');
stderr.on('data', (data) => {
data = iconv_lite.decode(Buffer.from(data, 'binary'), PythonShell.ENCODING);
data = MString.decode(data);
data = data.replace(/(?<![\w+])__import__\("pyinput"\).input\(/g, 'input(');
this.#statusBarTerminal_.addValue(data);
});
}
#onCursorChange_() {
const editor = this.#statusBarTerminal_.getEditor();
const session = editor.getSession();
const cursor = session.selection.getCursor();
editor.setReadOnly(
cursor.row < this.#cursor_.row || cursor.column < this.#cursor_.column
);
}
#enterInput_() {
if (!this.#running_) {
return;
}
this.#waittingForInput_ = true;
this.#cursor_ = this.#statusBarTerminal_.getEndPos();
const editor = this.#statusBarTerminal_.getEditor();
editor.setReadOnly(false);
editor.focus();
const session = editor.getSession();
session.selection.on('changeCursor', this.#onCursorChangeEvent_);
editor.commands.addCommands(this.#commands_);
}
#exitInput_() {
this.#waittingForInput_ = false;
const editor = this.#statusBarTerminal_.getEditor();
const session = editor.getSession();
session.selection.off('changeCursor', this.#onCursorChangeEvent_);
editor.commands.removeCommands(this.#commands_);
this.#prompt_ = '';
this.cursor_ = { row: 0, column: 0 };
editor.setReadOnly(true);
}
run(code) {
this.stop()
.then(() => {
try {
code = code.replace(/(?<![\w+])input\(/g, '__import__("pyinput").input(');
if (code.indexOf('import turtle') !== -1) {
code += '\nturtle.done()\n';
}
} catch (error) {
Debug.error(error);
}
const { mainStatusBarTabs } = Mixly;
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
return fs_extra.outputFile(Env.pyFilePath, code, 'utf8');
})
.then(() => {
this.#statusBarTerminal_.setValue(`${Msg.Lang['shell.running']}...\n`);
const startTime = Number(new Date());
this.#shell_ = new python_shell.PythonShell(Env.pyFilePath, this.#options_);
this.#running_ = true;
this.#addEventsListener_();
this.#shell_.on('close', (code) => {
this.#running_ = false;
const endTime = Number(new Date());
const duration = dayjs.duration(endTime - startTime).format('HH:mm:ss.SSS');
this.#statusBarTerminal_.addValue(`\n==${Msg.Lang['shell.finish']}(${Msg.Lang['shell.timeCost']} ${duration})==`);
});
})
.catch(Debug.error);
}
stop() {
return new Promise((resolve, reject) => {
if (this.#waittingForInput_) {
this.#exitInput_();
}
if (this.#running_) {
this.#shell_.childProcess.on('exit', () => {
resolve();
});
this.#shell_.stdin.end();
this.#shell_.stdout.end();
if (Env.currentPlatform === 'win32') {
child_process.exec(`taskkill /pid ${this.#shell_.childProcess.pid} /f /t`);
} else {
this.#shell_.kill('SIGTERM');
}
} else {
resolve();
}
});
}
}
Electron.PythonShell = PythonShell;
});

View File

@@ -0,0 +1,260 @@
goog.loadJs('electron', () => {
goog.require('layui');
goog.require('Mixly.Serial');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.Serial');
const lodash_fp = Mixly.require('lodash/fp');
const child_process = Mixly.require('node:child_process');
const serialport = Mixly.require('serialport');
const {
SerialPort,
ReadlineParser,
ByteLengthParser
} = serialport;
const {
Serial,
Env,
Msg,
Debug,
Electron
} = Mixly;
const { form } = layui;
class ElectronSerial extends Serial {
static {
this.getConfig = function () {
return Serial.getConfig();
}
this.getSelectedPortName = function () {
return Serial.getSelectedPortName();
}
this.getCurrentPortsName = function () {
return Serial.getCurrentPortsName();
}
this.getPorts = async function () {
return new Promise((resolve, reject) => {
if (Env.currentPlatform === 'linux') {
child_process.exec('ls /dev/ttyACM* /dev/ttyUSB* /dev/tty*USB*', (err, stdout, stderr) => {
let portsName = stdout.split('\n');
let newPorts = [];
for (let i = 0; i < portsName.length; i++) {
if (!portsName[i]) {
continue;
}
newPorts.push({
vendorId: 'None',
productId: 'None',
name: portsName[i]
});
}
resolve(newPorts);
});
} else {
SerialPort.list().then(ports => {
let newPorts = [];
for (let i = 0; i < ports.length; i++) {
let port = ports[i];
newPorts.push({
vendorId: port.vendorId,
productId: port.productId,
name: port.path
});
}
resolve(newPorts);
}).catch(reject);
}
});
}
this.refreshPorts = function () {
this.getPorts()
.then((ports) => {
Serial.renderSelectBox(ports);
})
.catch(Debug.error);
}
}
#serialport_ = null;
#parserBytes_ = null;
#parserLine_ = null;
constructor(port) {
super(port);
}
#addEventsListener_() {
this.#parserBytes_.on('data', (buffer) => {
this.onBuffer(buffer);
});
this.#parserLine_.on('data', (str) => {
this.onString(str);
});
this.#serialport_.on('error', (error) => {
this.onError(error);
this.onClose(1);
});
this.#serialport_.on('open', () => {
this.onOpen();
});
this.#serialport_.on('close', () => {
this.onClose(1);
});
}
async open(baud) {
return new Promise((resolve, reject) => {
const portsName = Serial.getCurrentPortsName();
const currentPort = this.getPortName();
if (!portsName.includes(currentPort)) {
reject('无可用串口');
return;
}
if (this.isOpened()) {
resolve();
return;
}
baud = baud ?? this.getBaudRate();
this.#serialport_ = new SerialPort({
path: currentPort,
baudRate: baud, // 波特率
dataBits: 8, // 数据位
parity: 'none', // 奇偶校验
stopBits: 1, // 停止位
flowControl: false,
autoOpen: false // 不自动打开
}, false);
this.#parserBytes_ = this.#serialport_.pipe(new ByteLengthParser({ length: 1 }));
this.#parserLine_ = this.#serialport_.pipe(new ReadlineParser());
this.#serialport_.open((error) => {
if (error) {
this.onError(error);
reject(error);
} else {
super.open(baud);
this.setBaudRate(baud);
resolve();
}
});
this.#addEventsListener_();
});
}
async close() {
return new Promise((resolve, reject) => {
if (!this.isOpened()) {
resolve();
return;
}
super.close();
this.#serialport_.close((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
async setBaudRate(baud) {
return new Promise((resolve, reject) => {
if (!this.isOpened() || this.getBaudRate() === baud) {
resolve();
return;
}
this.#serialport_.update({ baudRate: baud - 0 }, (error) => {
if (error) {
reject(error);
} else {
super.setBaudRate(baud);
resolve();
}
});
});
}
async send(data) {
return new Promise((resolve, reject) => {
if (!this.isOpened()) {
resolve();
return;
}
this.#serialport_.write(data, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
async sendString(str) {
return this.send(str);
}
async sendBuffer(buffer) {
return this.send(buffer);
}
async setDTRAndRTS(dtr, rts) {
return new Promise((resolve, reject) => {
if (!this.isOpened()) {
resolve();
return;
}
this.#serialport_.set({ dtr, rts }, (error) => {
if (error) {
reject(error);
} else {
super.setDTRAndRTS(dtr, rts);
resolve();
}
});
});
}
async setDTR(dtr) {
return this.setDTRAndRTS(dtr, this.getRTS());
}
async setRTS(rts) {
return this.setDTRAndRTS(this.getDTR(), rts);
}
onBuffer(buffer) {
super.onBuffer(buffer);
for (let i = 0; i < buffer.length; i++) {
super.onByte(buffer[i]);
}
const string = this.decodeBuffer(buffer);
if (!string) {
return;
}
for (let char of string) {
super.onChar(char);
}
}
}
Electron.Serial = ElectronSerial;
});

View File

@@ -0,0 +1,88 @@
goog.loadJs('electron', () => {
goog.require('dayjs.duration');
goog.require('Mixly.Env');
goog.require('Mixly.MString');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.Shell');
const {
Env,
MString,
Electron
} = Mixly;
const child_process = Mixly.require('node:child_process');
const iconv_lite = Mixly.require('iconv-lite');
class Shell {
static {
this.ENCODING = Env.currentPlatform == 'win32' ? 'cp936' : 'utf-8';
}
#shell_ = null;
constructor() {}
#addEventsListener_() {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const { stdout, stderr } = this.#shell_;
stdout.on('data', (data) => {
if (data.length > 1000) {
return;
}
data = iconv_lite.decode(Buffer.from(data, 'binary'), 'utf-8');
statusBarTerminal.addValue(data);
});
stderr.on('data', (data) => {
let lines = data.split('\n');
for (let i in lines) {
let encoding = 'utf-8';
if (lines[i].indexOf('can\'t open device') !== -1) {
encoding = Shell.ENCODING;
}
lines[i] = iconv_lite.decode(Buffer.from(lines[i], 'binary'), encoding);
}
data = lines.join('\n');
data = MString.decode(data);
statusBarTerminal.addValue(data);
});
}
exec(command) {
return new Promise((resolve, reject) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const startTime = Number(new Date());
this.#shell_ = child_process.exec(command, {
maxBuffer: 4096 * 1000000,
encoding: 'binary'
});
this.#addEventsListener_();
this.#shell_.on('close', (code) => {
const endTime = Number(new Date());
const duration = dayjs.duration(endTime - startTime).format('HH:mm:ss.SSS');
const info = { code, time: duration };
resolve(info);
});
});
}
kill() {
this.#shell_.stdin.end();
this.#shell_.stdout.end();
if (Env.currentPlatform === 'win32') {
child_process.exec(`taskkill /pid ${this.#shell_.pid} /f /t`);
} else {
this.#shell_.kill('SIGTERM');
}
}
getShell() {
return this.#shell_;
}
}
Mixly.Electron.Shell = Shell;
});

View File

@@ -0,0 +1,154 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Blockly');
goog.require('Mustache');
goog.require('Mixly.Env');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.WikiGenerator');
const {
Env,
IdGenerator,
Electron
} = Mixly;
const fs_extra = Mixly.require('fs-extra');
const fs = Mixly.require('fs');
class WikiGenerator {
static {
this.WIKI_PAGE_FILE = goog.get(path.join(Env.templatePath, 'markdown/wiki-page-file.md'));
this.WIKI_PAGE_DIR = goog.get(path.join(Env.templatePath, 'markdown/wiki-page-dir.md'));
}
#$xml_ = null;
#desPath_ = '';
#tree_ = [];
constructor($xml, desPath) {
this.#$xml_ = $xml;
this.#desPath_ = desPath;
this.workspace = Mixly.Workspace.getMain().getEditorsManager().getActive().getPage('block').getEditor();
this.generator = Mixly.Workspace.getMain().getEditorsManager().getActive().getPage('block').generator;
}
buildTree($nodes) {
let output = [];
for (let i = 0; i < $nodes.length; i++) {
let child = {};
child.id = $nodes[i].getAttribute('id') ?? IdGenerator.generate();
if ($nodes[i].nodeName == 'CATEGORY') {
child.name = $nodes[i].getAttribute('name') ?? child.id;
child.children = this.buildTree($($nodes[i]).children());
output.push(child);
} else if ($nodes[i].nodeName == 'BLOCK') {
child.name = $nodes[i].getAttribute('type') ?? child.id;
child.children = false;
child.xml = $nodes[i].outerHTML;
output.push(child);
}
}
return output;
}
async generate() {
let output = this.buildTree(this.#$xml_.children());
fs_extra.ensureDirSync('./outputBlock/');
fs_extra.emptyDirSync('./outputBlock/');
let info = await this.generateWikiPage('./outputBlock/', {
title: 'Micropython ESP32',
order: 1
}, output);
fs_extra.outputFileSync(path.join('./outputBlock/', 'README.md'), info.md);
}
generateImage(desPath) {
return new Promise((resolve, reject) => {
Blockly.Screenshot.workspaceToSvg_(this.workspace, (datauri) => {
const base64 = datauri.replace(/^data:image\/\w+;base64,/, '');
const dataBuffer = new Buffer(base64, 'base64');
fs_extra.outputFile(desPath, dataBuffer, (error) => {
resolve(error);
});
});
});
}
async generateWikiPage(rootPath, parentInfo, nodes) {
let config = {
id: parentInfo.id,
title: parentInfo.title,
order: parentInfo.order
};
let blocksNum = 0;
for (let node of nodes) {
if (!node.children) {
blocksNum += 1;
}
}
config.index = !!blocksNum;
if (blocksNum) {
let blocks = [];
for (let node of nodes) {
if (node.children) {
continue;
}
this.workspace.clear();
let xmlNode = Blockly.utils.xml.textToDom(`<xml>${node.xml}</xml>`);
Blockly.Xml.domToWorkspace(xmlNode, this.workspace);
let code = this.generator.workspaceToCode(this.workspace) || '';
code = code.replace(/(_E[0-9A-F]{1}_[0-9A-F]{2}_[0-9A-F]{2})+/g, function (s) {
try {
return decodeURIComponent(s.replace(/_/g, '%'));
} catch (error) {
return s;
}
});
await this.generateImage(path.join(rootPath, '../assets', parentInfo.title, node.id + '.png'))
blocks.push({
imgPath: `./assets/${parentInfo.title}/${node.id}.png`,
code: code,
type: node.name
});
}
config.blocks = blocks;
return {
file: true,
md: Mustache.render(WikiGenerator.WIKI_PAGE_FILE, config)
};
} else {
let index = 0;
for (let i in nodes) {
if (!nodes[i].children) {
continue;
}
index += 1;
let order = String(index * 5);
let mdIndex = Array(4 - Math.min(order.length, 4)).fill('0').join('') + order;
let desPath = path.join(rootPath, mdIndex + '-' + nodes[i].id);
let info = await this.generateWikiPage(desPath, {
id: nodes[i].id,
title: nodes[i].name,
order: order,
}, nodes[i].children);
if (info.file) {
fs_extra.outputFileSync(`${desPath}.md`, info.md);
} else if (!info.isEmpty) {
fs_extra.outputFileSync(path.join(desPath, 'README.md'), info.md);
} else if (info.isEmpty) {
index -= 1;
}
}
config.blocks = [];
return {
file: false,
isEmpty: !index,
md: Mustache.render(WikiGenerator.WIKI_PAGE_DIR, config)
};
}
}
}
Electron.WikiGenerator = WikiGenerator;
});

View File

@@ -0,0 +1,284 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.WikiManager');
const {
Config,
Env,
Msg,
Electron
} = Mixly;
const { WikiManager } = Electron;
const { BOARD } = Config;
const fs = Mixly.require('fs');
const fs_plus = Mixly.require('fs-plus');
const fs_extra = Mixly.require('fs-extra');
const path = Mixly.require('path');
const json2md = Mixly.require('json2md');
const electron_localshortcut = Mixly.require('electron-localshortcut');
const electron_remote = Mixly.require('@electron/remote');
const { ipcMain } = electron_remote;
class WikiPage {
constructor(indexPath, gotoInfo = null) {
this.gotoInfo = gotoInfo;
this.updateContentFile();
this.win = Electron.newBrowserWindow(indexPath);
this.isDestroyed = false;
this.addReceiveCommandEvent();
this.addLocalShortcutEvent();
this.win.on('close', () => {
this.isDestroyed = true;
});
$(window).unload(() => {
if (!this.isDestroyed)
this.win.close();
});
}
addLocalShortcutEvent() {
//打开或关闭开发者工具
electron_localshortcut.register(this.win, 'CmdOrCtrl+Shift+I', () => {
if (!this.isDestroyed)
this.win.webContents.toggleDevTools();
});
//重载页面
electron_localshortcut.register(this.win, 'CmdOrCtrl+R', () => {
this.reload();
});
}
addReceiveCommandEvent() {
ipcMain.on('command', (event, command) => {
if (typeof command !== 'object') return;
switch (command.func) {
case 'getPath':
this.updateWiki();
break;
}
});
}
sendCommand(command) {
if (this.isDestroyed || typeof command !== 'object') return;
this.win.webContents.send('command', command);
}
reload() {
if (!this.isDestroyed) {
this.updateContentFile();
this.win.reload();
}
}
getPagePath(contentPath, contentList) {
if (typeof contentList !== 'object' || !contentPath.length) return null;
if (contentPath.length === 1) {
for (let key in contentList) {
const child = contentList[key];
if (child?.link?.title !== contentPath[0]) {
continue;
}
const { title, source } = child.link;
if (title !== contentPath[0] || typeof source !== 'string') {
return null;
}
try {
const filePath = source.match(/(?<=(\?file=))[^\s]*/g);
if (filePath?.length) {
return filePath[0];
}
} catch (error) {
console.log(error);
}
return null;
}
return null;
} else {
for (let key in contentList) {
const child = contentList[key];
if (child
&& child.length === 2
&& child[0].h5 === contentPath[0]) {
let childPath = [ ...contentPath ];
childPath.shift();
return this.getPagePath(childPath, child[1].ul);
}
}
}
}
goto(pageList, scrollPos) {
const args = [];
const pagePath = this.getPagePath(pageList, this.contentList);
if (!pageList) return;
args.push(pagePath);
scrollPos && args.push(scrollPos);
this.sendCommand({
func: 'goto',
args
});
this.win.focus();
}
updateContentFile() {
const wikiContentPath = path.join(Env.boardDirPath, 'wiki/content.md');
const defaultWikiPath = path.join(Env.boardDirPath, 'wiki/wiki-libs/' + Msg.nowLang);
const wikiHomePagePath = path.join(defaultWikiPath, 'home');
const thirdPartyLibsPath = path.join(Env.boardDirPath, 'libraries/ThirdParty/');
const changelogPath = path.join(Env.clientPath, 'CHANGELOG');
const wikiList = [];
if (fs_plus.isFileSync(wikiHomePagePath + '.md'))
wikiList.push({
h4: {
link: {
title: Msg.Lang['wiki.home'],
source: '?file=' + encodeURIComponent(wikiHomePagePath)
}
}
});
if (fs_plus.isDirectorySync(defaultWikiPath)) {
const childContentList = this.getContentJson(defaultWikiPath, BOARD.boardType);
if (childContentList)
wikiList.push(childContentList);
}
if (fs_plus.isDirectorySync(thirdPartyLibsPath)) {
const libsName = fs.readdirSync(thirdPartyLibsPath);
for (let name of libsName) {
const libWikiPath = path.join(thirdPartyLibsPath, name , 'wiki', Msg.nowLang);
if (fs_plus.isDirectorySync(libWikiPath)) {
const childContentList = this.getContentJson(libWikiPath, name);
if (childContentList) {
wikiList.push(childContentList);
}
}
}
}
this.contentList = wikiList;
try {
const md = json2md(wikiList);
const lineList = md.split('\n');
for (let i = 0; i < lineList.length; i++) {
if (!lineList[i].replaceAll(' ', '')) {
lineList.splice(i, 1);
i--;
} else {
if (!lineList[i].indexOf('#####'))
lineList[i] = '\n' + lineList[i];
}
}
fs_extra.outputFile(wikiContentPath, lineList.join('\n'));
} catch (error) {
console.log(error);
}
}
updateWiki() {
const args = [
{
default: path.join(Env.boardDirPath, 'wiki/wiki-libs/'),
thirdParty: path.join(Env.boardDirPath, 'libraries/ThirdParty/'),
content: path.join(Env.boardDirPath, 'wiki/content.md')
}
];
if (this.gotoInfo) {
const { page, scrollPos } = this.gotoInfo;
const pagePath = this.getPagePath(this.gotoInfo.page, this.contentList);
if (pagePath) {
const goto = [];
goto.push(pagePath);
scrollPos && goto.push(scrollPos);
args[0].goto = goto;
}
this.gotoInfo = null;
}
this.sendCommand({
func: 'setPath',
args
});
}
getContentJson(dirPath, title = null) {
const dirNameList = path.basename(dirPath).split('-');
if (dirNameList.length !== 2 && !title) return null;
const contentList = [];
contentList.push({ h5: title ?? dirNameList[1] });
contentList.push({ ul: [] });
const { ul } = contentList[1];
const keyList = fs.readdirSync(dirPath);
for (let key of keyList) {
const nowPath = path.join(dirPath, key);
if (fs_plus.isDirectorySync(nowPath)) {
const childContentList = this.getContentJson(nowPath);
if (childContentList && childContentList[1].ul.length)
ul.push(childContentList);
} else {
const extname = path.extname(key);
if (extname !== '.md') continue;
const fileNameList = path.basename(key, '.md').split('-');
if (fileNameList.length !== 2) continue;
const newPath = path.join(path.dirname(nowPath), path.basename(key, '.md'));
ul.push({ link: { title: fileNameList[1], source: '?file=' + encodeURIComponent(newPath) + ' \"' + fileNameList[1] + '\"' } });
}
}
return contentList;
}
}
WikiManager.WikiPage = WikiPage;
WikiManager.openWiki = (gotoInfo) => {
const goto = (gotoInfo && typeof gotoInfo === 'object') ? gotoInfo[Msg.nowLang] : null;
if (!WikiManager.wiki || WikiManager.wiki.isDestroyed) {
const wikiPath = path.join(Env.indexDirPath, '../common/wiki/index.html');
if (fs_plus.isFileSync(wikiPath)) {
WikiManager.wiki = new WikiPage(wikiPath, goto);
} else {
layer.msg(Msg.Lang['wiki.pageNotFound'], { time: 1000 });
}
} else {
const { win } = WikiManager.wiki;
win && win.focus();
if (goto) {
const { page, scrollPos } = goto;
WikiManager.wiki.goto(page, scrollPos);
}
}
}
WikiManager.registerContextMenu = () => {
const openWikiPage = {
displayText: Msg.Lang['wiki.open'],
preconditionFn: function(scope) {
const { wiki } = scope.block;
if (typeof wiki === 'object') {
if (typeof wiki[Msg.nowLang] === 'object'
&& typeof wiki[Msg.nowLang].page === 'object') {
return 'enabled';
}
}
return 'hidden';
},
callback: function(scope) {
const { wiki } = scope.block;
WikiManager.openWiki(wiki);
},
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
id: 'wiki_open',
weight: 200
};
Blockly.ContextMenuRegistry.registry.register(openWikiPage);
}
});