goog.loadJs('common', () => {
goog.require('layui');
goog.require('Blockly');
goog.require('Base64');
goog.require('Mixly.Config');
goog.require('Mixly.MArray');
goog.require('Mixly.Boards');
goog.require('Mixly.XML');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Msg');
goog.provide('Mixly.MFile');
const { form, util } = layui;
const {
Config,
MArray,
Boards,
XML,
LayerExt,
Msg,
MFile
} = Mixly;
const { BOARD, SOFTWARE } = Config;
MFile.SAVE_FILTER_TYPE = {
mix: { name: Msg.Lang['file.type.mix'], extensions: ['mix'] },
py: { name: Msg.Lang['file.type.python'], extensions: ['py'] },
ino: { name: Msg.Lang['file.type.arduino'], extensions: ['ino'] },
hex: { name: Msg.Lang['file.type.hex'], extensions: ['hex'] },
bin: { name: Msg.Lang['file.type.bin'], extensions: ['bin'] },
mil: { name: Msg.Lang['file.type.mil'], extensions: ['mil'] }
};
MFile.saveFilters = [ MFile.SAVE_FILTER_TYPE.mix ];
MFile.OPEN_FILTER_TYPE = ['mix','xml', 'py', 'ino', 'hex', 'bin'];
MFile.openFilters = ['mix'];
/**
* @function 更新保存文件时可用的后缀
* @param config { array }
* config = ["py", "ino", "hex", "bin", "png"]
* 注:mix后缀为默认添加,列表后缀名顺序即为保存时自上而下显示的顺序
* @param priority { string },配置需要优先显示的后缀名,没有此项可不填
* @return void
**/
MFile.updateSaveFilters = (config, priority = null) => {
if (typeof config !== 'object')
config = [];
MFile.saveFilters = [ MFile.SAVE_FILTER_TYPE.mix ];
let saveFilterType = ['mix'];
for (let i of config)
if (MFile.SAVE_FILTER_TYPE[i] && !saveFilterType.includes(i)) {
saveFilterType.push(i);
if (i === priority) {
MFile.saveFilters.unshift(MFile.SAVE_FILTER_TYPE[i]);
} else {
MFile.saveFilters.push(MFile.SAVE_FILTER_TYPE[i]);
}
}
}
/**
* @function 更新打开文件时的可用后缀
* @param config { array }
* config = ["py", "ino", "hex", "bin"]
* @return void
**/
MFile.updateOpenFilters = (config, priority) => {
if (typeof config !== 'object')
config = [];
MFile.openFilters = ['mix', 'xml'];
for (let i of config)
if (MFile.OPEN_FILTER_TYPE.includes(i) && !MFile.openFilters.includes(i))
MFile.openFilters.push(i);
}
MFile.init = () => {
const saveConfig = BOARD?.nav?.save ?? {};
let saveFilters = [], openFilters = [];
for (let i in saveConfig)
if (saveConfig[i]) {
saveFilters.push(i);
openFilters.push(i);
}
if (BOARD?.nav?.setting?.thirdPartyLibrary)
saveFilters.push('mil');
MFile.updateOpenFilters(openFilters);
MFile.updateSaveFilters(saveFilters);
}
MFile.init();
MFile.getCode = (type) => {
const { mainEditor } = Editor;
const { codeEditor, blockEditor } = mainEditor;
const { editor, generator } = blockEditor;
if (mainEditor.selected === 'CODE')
return codeEditor.getValue();
else {
return generator.workspaceToCode(editor) || '';
}
}
MFile.getMix = () => {
const mixDom = $(Blockly.Xml.workspaceToDom(Editor.blockEditor)),
version = SOFTWARE?.version ?? 'Mixly 2.0',
boardName = Boards.getSelectedBoardName(),
board = BOARD?.boardType ?? 'default',
config = Boards.getSelectedBoardConfig();
mixDom.removeAttr('xmlns')
.attr('version', version)
.attr('board', board + '@' + boardName);
let xmlStr = mixDom[0].outerHTML;
let code = MFile.getCode();
if (config) {
try {
xmlStr += `${JSON.stringify(config)}`;
} catch (error) {
console.log(error);
}
}
if (BOARD.saveMixWithCode) {
code = Base64.encode(code);
xmlStr += `${code}`;
}
return xmlStr;
}
MFile.getMil = () => {
const mixDom = $(MFile.getMix());
let xmlDom, configDom, codeDom;
for (let i = 0; mixDom[i]; i++) {
switch (mixDom[i].nodeName) {
case 'XML':
xmlDom = $(mixDom[i]);
break;
case 'CONFIG':
configDom = $(mixDom[i]);
break;
case 'CODE':
codeDom = $(mixDom[i]);
break;
}
}
if (!xmlDom) return '';
configDom && configDom.remove();
codeDom && codeDom.remove();
xmlDom.attr('type', 'lib');
xmlDom.find('block,shadow').removeAttr('id varid x y');
const blocksDom = xmlDom.children('block');
let blockXmlList = [];
for (let i = 0; blocksDom[i]; i++) {
const outerHTML = blocksDom[i].outerHTML;
if (!blockXmlList.includes(outerHTML))
blockXmlList.push(outerHTML);
else
blocksDom[i].remove();
}
return xmlDom[0].outerHTML;
}
MFile.parseMix = (xml, useCode = false, useIncompleteBlocks = false, endFunc = (message) => {}) => {
const mixDom = xml;
let xmlDom, configDom, codeDom;
for (let i = 0; mixDom[i]; i++) {
switch (mixDom[i].nodeName) {
case 'XML':
xmlDom = $(mixDom[i]);
break;
case 'CONFIG':
configDom = $(mixDom[i]);
break;
case 'CODE':
codeDom = $(mixDom[i]);
break;
}
}
if (!xmlDom && !codeDom) {
layer.msg(Msg.Lang['editor.invalidData'], { time: 1000 });
return;
}
for (let i of ['version', 'id', 'type', 'varid', 'name', 'x', 'y', 'items']) {
const nowDom = xmlDom.find('*[' + i + ']');
if (nowDom.length) {
for (let j = 0; nowDom[j]; j++) {
let attr = $(nowDom[j]).attr(i);
try {
attr = attr.replaceAll('\\\"', '');
} catch (error) {
console.log(error);
}
$(nowDom[j]).attr(i, attr);
}
}
}
let config, configStr = configDom && configDom.html();
try {
if (configStr)
config = JSON.parse(configStr);
} catch (error) {
console.log(error);
}
let boardName = xmlDom.attr('board') ?? '';
Boards.setSelectedBoard(boardName, config);
let code = codeDom ? codeDom.html() : '';
if (Base64.isValid(code)) {
code = Base64.decode(code);
} else {
try {
code = util.unescape(code);
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;
}
});
} catch (error) {
console.log(error);
}
}
if (useCode) {
if (!codeDom) {
layer.msg(Msg.Lang['editor.invalidData'], { time: 1000 });
return;
}
Editor.mainEditor.drag.full('NEGATIVE'); // 完全显示代码编辑器
Editor.codeEditor.setValue(code, -1);
Editor.blockEditor.clear();
endFunc('USE_CODE');
return;
}
const blockDom = mixDom.find('block');
const shadowDom = mixDom.find('shadow');
blockDom.removeAttr('id varid');
shadowDom.removeAttr('id varid');
let blocks = [];
let undefinedBlocks = [];
for (let i = 0; blockDom[i]; i++) {
const blockType = $(blockDom[i]).attr('type');
if (blockType && !blocks.includes(blockType))
blocks.push(blockType);
}
for (let i = 0; shadowDom[i]; i++) {
const shadowType = $(shadowDom[i]).attr('type');
if (shadowType && !blocks.includes(shadowType))
blocks.push(shadowType);
}
const blocklyGenerator = Editor.mainEditor.blockEditor.generator;
for (let i of blocks) {
if (Blockly.Blocks[i] && blocklyGenerator.forBlock[i]) {
continue;
}
undefinedBlocks.push(i);
}
if (undefinedBlocks.length) {
MFile.showParseMixErrorDialog(mixDom, undefinedBlocks, endFunc);
return;
}
Editor.blockEditor.clear();
Blockly.Xml.domToWorkspace(xmlDom[0], Editor.blockEditor);
Editor.blockEditor.scrollCenter();
Blockly.hideChaff();
if (!useIncompleteBlocks && codeDom) {
const workspaceCode = MFile.getCode();
if (workspaceCode !== code) {
Editor.mainEditor.drag.full('NEGATIVE'); // 完全显示代码编辑器
Editor.codeEditor.setValue(code, -1);
}
endFunc();
return;
}
Editor.mainEditor.drag.full('POSITIVE'); // 完全显示块编辑器
if (useIncompleteBlocks)
endFunc('USE_INCOMPLETE_BLOCKS');
else
endFunc();
}
MFile.removeUndefinedBlocks = (xml, undefinedBlocks) => {
for (let i of undefinedBlocks) {
xml.find('*[type='+i+']').remove();
}
}
MFile.showParseMixErrorDialog = (xml, undefinedBlocks, endFunc = () => {}) => {
const { PARSE_MIX_ERROR_DIV } = XML.TEMPLATE_STR;
const renderStr = XML.render(PARSE_MIX_ERROR_DIV, {
text: undefinedBlocks.join('
'),
btn1Name: Msg.Lang['editor.cancel'],
btn2Name: Msg.Lang['editor.ignoreBlocks'],
btn3Name: Msg.Lang['editor.loadCode']
})
LayerExt.open({
title: Msg.Lang['editor.parseMixErrorInfo'],
id: 'parse-mix-error-layer',
area: ['50%', '250px'],
max: ['500px', '250px'],
min: ['350px', '100px'],
shade: LayerExt.SHADE_ALL,
content: renderStr,
borderRadius: '5px',
success: (layero, index) => {
$('#parse-mix-error-layer').css('overflow', 'hidden');
form.render(null, 'parse-mix-error-filter');
layero.find('button').click((event) => {
layer.close(index);
const mId = $(event.currentTarget).attr('m-id');
switch (mId) {
case '0':
break;
case '1':
for (let i of undefinedBlocks) {
xml.find('*[type='+i+']').remove();
}
MFile.parseMix(xml, false, true, endFunc);
break;
case '2':
MFile.parseMix(xml, true, false, endFunc);
break;
}
});
}
});
}
MFile.openFile = (filters, readType = 'text', sucFunc = () => {}) => {
const loadFileDom = $('');
loadFileDom.attr({
id: 'web-open-file',
type: 'file',
name: 'web-open-file',
accept: filters
});
loadFileDom.change(function(event) {
MFile.onclickOpenFile(this, readType, (data) => {
sucFunc(data);
});
});
loadFileDom.css('display', 'none');
$('#web-open-file').remove();
$('body').append(loadFileDom);
loadFileDom.click();
}
MFile.onclickOpenFile = (input, readType, endFunc) => {
const files = input.files;
//限制上传文件的 大小,此处为10M
if (files[0].size > 5 * 1024 * 1024) {
layer.msg('所选择文件大小必须在5MB内', { time: 1000 });
$('#web-open-file').remove();
endFunc(null);
return false;
}
const resultFile = input.files[0];
// 如果文件存在
if (resultFile) {
const filename = resultFile.name;
const reader = new FileReader();
switch (readType) {
case 'text':
reader.readAsText(resultFile);
break;
case 'bin':
reader.readAsBinaryString(resultFile);
break;
case 'url':
reader.readAsDataURL(resultFile);
break;
default:
reader.readAsArrayBuffer(resultFile);
}
reader.onload = function (e) {
const data = e.target.result;
$('#web-open-file').remove();
endFunc({ data, filename });
};
} else {
endFunc(null);
}
}
});