feat: sync mixly root files and common folder

This commit is contained in:
yczpf2019
2026-01-24 16:12:04 +08:00
parent 93e17c00ae
commit c8c5fcf726
2920 changed files with 186461 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
goog.loadJs('common', () => {
goog.require('Mixly.Env');
goog.require('Mixly.FileTree');
goog.require('Mixly.Electron.AmpyFS');
goog.require('Mixly.Web.AmpyFS');
goog.require('Mixly.WebSocket.AmpyFS');
goog.provide('Mixly.AmpyFileTree');
const {
Env,
FileTree,
Electron = {},
Web = {},
WebSocket = {}
} = Mixly;
let currentObj = null;
if (goog.isElectron) {
currentObj = Electron;
} else {
if (Env.hasSocketServer) {
currentObj = WebSocket;
} else {
currentObj = Web;
}
}
const { AmpyFS } = currentObj;
class AmpyFileTree extends FileTree {
constructor() {
super(new AmpyFS());
}
async readFolder(inPath) {
let output = [];
const fs = this.getFS();
const [_a, status] = await fs.isDirectory(inPath);
if (!status) {
return output;
}
const [_b, children] = await fs.readDirectory(inPath);
for (let data of children) {
if (!data) {
continue;
}
const dataPath = data[0];
const isDirectory = ['dir', 'empty dir'].includes(data[1]);
if (isDirectory) {
const isDirEmtpy = data[1] === 'empty dir';
output.push({
type: 'folder',
id: dataPath,
children: !isDirEmtpy
});
} else {
output.push({
type: 'file',
id: dataPath,
children: false
});
}
}
return output;
}
}
Mixly.AmpyFileTree = AmpyFileTree;
});

View File

@@ -0,0 +1,31 @@
goog.loadJs('common', () => {
goog.provide('Mixly.Ampy');
class Ampy {
constructor() {}
unhexlify(hexString) {
if (hexString.length % 2 !== 0) {
hexString = hexString + '0';
}
let bytes = [];
for (let c = 0; c < hexString.length; c += 2) {
bytes.push(parseInt(hexString.substr(c, 2), 16));
}
return new Uint8Array(bytes);
}
async get() {}
async ls() {}
async mkdir() {}
async put() {}
async reset() {}
async rm() {}
async rmdir() {}
async run() {}
}
Mixly.Ampy = Ampy;
});

View File

@@ -0,0 +1,689 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('hotkeys');
goog.require('Mixly.Url');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Drag');
goog.require('Mixly.Nav');
goog.require('Mixly.Menu');
goog.require('Mixly.Workspace');
goog.require('Mixly.FooterBar');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Debug');
goog.require('Mixly.Component');
goog.require('Mixly.EditorMix');
goog.require('Mixly.Electron.Loader');
goog.require('Mixly.Electron.FS');
goog.require('Mixly.Electron.File');
goog.require('Mixly.Electron.LibManager');
goog.require('Mixly.Electron.Serial');
goog.require('Mixly.Electron.ArduShell');
goog.require('Mixly.Electron.BU');
goog.require('Mixly.Electron.PythonShell');
goog.require('Mixly.Web.BU');
goog.require('Mixly.Web.FS');
goog.require('Mixly.Web.File');
goog.require('Mixly.Web.Serial');
goog.require('Mixly.WebCompiler.ArduShell');
goog.require('Mixly.WebSocket.File');
goog.require('Mixly.WebSocket.Serial');
goog.require('Mixly.WebSocket.ArduShell');
goog.require('Mixly.WebSocket.BU');
goog.provide('Mixly.App');
const {
Url,
Config,
Env,
Msg,
Drag,
Nav,
Menu,
Workspace,
FooterBar,
HTMLTemplate,
LayerExt,
Debug,
Component,
EditorMix,
Electron = {},
Web = {},
WebCompiler = {},
WebSocket = {}
} = Mixly;
const { Loader } = Electron;
let currentObj = null;
if (goog.isElectron) {
currentObj = Electron;
} else {
if (Env.hasSocketServer) {
currentObj = WebSocket;
} else {
currentObj = Web;
}
}
const {
FS,
File,
LibManager,
BU,
PythonShell,
Serial
} = currentObj;
let ArduShell = null;
if (!goog.isElectron && Env.hasCompiler) {
ArduShell = WebCompiler.ArduShell;
} else {
ArduShell = currentObj.ArduShell;
}
const { BOARD, SELECTED_BOARD } = Config;
class App extends Component {
static {
HTMLTemplate.add(
'html/app.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/app.html')))
);
}
#resizeObserver_ = null;
#workspace_ = null;
#nav_ = null;
#footerbar_ = null;
constructor(element) {
super();
const $content = $(HTMLTemplate.get('html/app.html').render());
$content.on('contextmenu', (e) => e.preventDefault());
this.setContent($content);
this.mountOn($(element));
this.#nav_ = new Nav();
this.#nav_.mountOn($content.find('.mixly-nav'));
this.#workspace_ = new Workspace($content.find('.mixly-workspace')[0]);
const editorsManager = this.#workspace_.getEditorsManager();
editorsManager.add({
type: '.mix',
id: 'Untitled-1.mix',
name: 'Untitled-1.mix',
title: 'Untitled-1.mix',
favicon: 'fileicon-mix'
});
this.#footerbar_ = new FooterBar();
this.#footerbar_.mountOn($content.find('.mixly-footerbar'));
this.#addEventsListenerForNav_();
this.#addObserver_();
Mixly.mainStatusBarTabs = this.#workspace_.getStatusBarsManager();
Serial.refreshPorts();
if (goog.isElectron) {
PythonShell.init();
}
}
#addEventsListenerForNav_() {
const editorsManager = this.#workspace_.getEditorsManager();
this.#nav_.register({
id: 'home-btn',
preconditionFn: () => {
return true;
},
callback: () => {
this.#onbeforeunload_();
},
scopeType: Nav.Scope.LEFT,
weight: -1
});
this.#nav_.register({
icon: 'icon-ccw',
title: 'undo(ctrl+z)',
id: 'undo-btn',
displayText: Msg.Lang['nav.btn.undo'],
preconditionFn: () => {
return !!editorsManager.getActive();
},
callback: () => editorsManager.getActive().undo(),
scopeType: Nav.Scope.LEFT,
weight: 0
});
this.#nav_.register({
icon: 'icon-cw',
title: 'redo(ctrl+y)',
id: 'redo-btn',
displayText: Msg.Lang['nav.btn.redo'],
preconditionFn: () => {
return !!editorsManager.getActive();
},
callback: () => editorsManager.getActive().redo(),
scopeType: Nav.Scope.LEFT,
weight: 1
});
this.#nav_.register({
icon: 'icon-link',
title: '',
id: 'port-add-btn',
displayText: Msg.Lang['nav.btn.addDevice'],
preconditionFn: () => {
if (goog.isElectron || Env.hasSocketServer) {
return false;
}
return true;
},
callback: () => BU.requestPort().catch(Debug.error),
scopeType: Nav.Scope.LEFT,
weight: 3
});
this.#nav_.register({
icon: 'icon-check',
title: '',
id: 'arduino-compile-btn',
displayText: Msg.Lang['nav.btn.compile'],
preconditionFn: () => {
if (!goog.isElectron && !Env.hasSocketServer && !Env.hasCompiler) {
return false;
}
if (!SELECTED_BOARD?.nav?.compile || !SELECTED_BOARD?.nav?.upload) {
return false;
}
const workspace = Workspace.getMain();
const editorsManager = workspace.getEditorsManager();
const editor = editorsManager.getActive();
if (!editor) {
return false;
}
if (editor instanceof EditorMix) {
return true;
}
return false;
},
callback: () => ArduShell.initCompile(),
scopeType: Nav.Scope.LEFT,
weight: 4
});
this.#nav_.register({
icon: 'icon-upload',
title: '',
id: 'arduino-upload-btn',
displayText: Msg.Lang['nav.btn.upload'],
preconditionFn: () => {
if (!goog.isElectron && !Env.hasSocketServer && !Env.hasCompiler) {
return false;
}
if (!SELECTED_BOARD?.nav?.compile || !SELECTED_BOARD?.nav?.upload) {
return false;
}
const workspace = Workspace.getMain();
const editorsManager = workspace.getEditorsManager();
const editor = editorsManager.getActive();
if (!editor) {
return false;
}
if (editor instanceof EditorMix) {
return true;
}
return false;
},
callback: () => ArduShell.initUpload(),
scopeType: Nav.Scope.LEFT,
weight: 5
});
this.#nav_.register({
icon: 'icon-upload-1',
title: '',
id: 'command-burn-btn',
displayText: Msg.Lang['nav.btn.burn'],
preconditionFn: () => {
if (goog.isElectron || Env.hasSocketServer) {
return SELECTED_BOARD?.nav?.burn;
}
if (Serial.devicesRegistry.hasKey('hid')) {
return false;
} else {
return SELECTED_BOARD?.nav?.burn;
}
},
callback: () => BU.initBurn(),
scopeType: Nav.Scope.LEFT,
weight: 3
});
this.#nav_.register({
icon: 'icon-upload',
title: '',
id: 'command-upload-btn',
displayText: Msg.Lang['nav.btn.upload'],
preconditionFn: () => {
return SELECTED_BOARD?.nav?.upload && !SELECTED_BOARD?.nav?.compile;
},
callback: () => BU.initUpload(),
scopeType: Nav.Scope.LEFT,
weight: 5
});
this.#nav_.register({
icon: 'icon-play-circled',
title: '',
id: 'python-run-btn',
displayText: Msg.Lang['nav.btn.run'],
preconditionFn: () => {
return goog.isElectron && SELECTED_BOARD?.nav?.run;
},
callback: () => PythonShell.run(),
scopeType: Nav.Scope.LEFT,
weight: 4
});
this.#nav_.register({
icon: 'icon-cancel',
title: '',
id: 'python-stop-btn',
displayText: Msg.Lang['nav.btn.stop'],
preconditionFn: () => {
return goog.isElectron && SELECTED_BOARD?.nav?.cancel;
},
callback: () => PythonShell.stop(),
scopeType: Nav.Scope.LEFT,
weight: 5
});
this.#nav_.register({
icon: 'icon-usb',
title: '',
id: 'serial-open-btn',
displayText: Msg.Lang['statusbar.serial.port'],
preconditionFn: () => {
return true;
},
callback: () => {
const statusBarsManager = this.#workspace_.getStatusBarsManager();
statusBarsManager.openSelectedPort();
},
scopeType: Nav.Scope.LEFT,
weight: 10
});
/*const leftSideBarOption = this.#nav_.register({
icon: 'codicon-layout-sidebar-left-off',
title: '操作左侧边栏',
id: 'left-sidebar-btn',
preconditionFn: () => {
return true;
},
callback: (element) => {
const $a = $(element).children('a');
const drag = this.#workspace_.dragVLeft;
if (drag.shown === Drag.Extend.NEGATIVE) {
drag.exitfull(Drag.Extend.NEGATIVE);
} else {
drag.full(Drag.Extend.NEGATIVE);
}
},
scopeType: Nav.Scope.CENTER,
weight: 1
});
const leftSideBarEvents = this.#workspace_.dragVLeft;
leftSideBarEvents.bind('onfull', (type) => {
const { $btn } = leftSideBarOption;
const $a = $btn.children('a');
if (type !== Drag.Extend.NEGATIVE) {
return;
}
$a.removeClass('codicon-layout-sidebar-left');
$a.addClass('codicon-layout-sidebar-left-off');
});
leftSideBarEvents.bind('exitfull', (type) => {
const { $btn } = leftSideBarOption;
const $a = $btn.children('a');
if (type !== Drag.Extend.NEGATIVE) {
return;
}
$a.removeClass('codicon-layout-sidebar-left-off');
$a.addClass('codicon-layout-sidebar-left');
});
const rightSideBarOption = this.#nav_.register({
icon: 'codicon-layout-sidebar-right-off',
title: '操作右侧边栏',
id: 'right-sidebar-btn',
preconditionFn: () => {
return true;
},
callback: (element) => {
const $a = $(element).children('a');
const drag = this.#workspace_.dragVRight;
if (drag.shown === Drag.Extend.POSITIVE) {
drag.exitfull(Drag.Extend.POSITIVE);
} else {
drag.full(Drag.Extend.POSITIVE);
}
},
scopeType: Nav.Scope.CENTER,
weight: 3
});
const rightSideBarEvents = this.#workspace_.dragVRight;
rightSideBarEvents.bind('onfull', (type) => {
const { $btn } = rightSideBarOption;
const $a = $btn.children('a');
if (type !== Drag.Extend.POSITIVE) {
return;
}
$a.removeClass('codicon-layout-sidebar-right');
$a.addClass('codicon-layout-sidebar-right-off');
});
rightSideBarEvents.bind('exitfull', (type) => {
const { $btn } = rightSideBarOption;
const $a = $btn.children('a');
if (type !== Drag.Extend.POSITIVE) {
return;
}
$a.removeClass('codicon-layout-sidebar-right-off');
$a.addClass('codicon-layout-sidebar-right');
});*/
const bottomSideBarOption = this.#nav_.register({
icon: 'codicon-layout-panel-off',
title: Msg.Lang['nav.btn.toggleStatusbar'],
id: 'bottom-sidebar-btn',
preconditionFn: () => {
return true;
},
callback: (element) => {
const $a = $(element).children('a');
const drag = this.#workspace_.dragH;
if (drag.shown === Drag.Extend.POSITIVE) {
drag.exitfull(Drag.Extend.POSITIVE);
} else {
drag.full(Drag.Extend.POSITIVE);
}
},
scopeType: Nav.Scope.CENTER,
weight: 2
});
const bottomSideBarEvents = this.#workspace_.dragH;
bottomSideBarEvents.bind('onfull', (type) => {
const { $btn } = bottomSideBarOption;
const $a = $btn.children('a');
if (type !== Drag.Extend.POSITIVE) {
return;
}
$a.removeClass('codicon-layout-panel');
$a.addClass('codicon-layout-panel-off');
});
bottomSideBarEvents.bind('exitfull', (type) => {
const { $btn } = bottomSideBarOption;
const $a = $btn.children('a');
if (type !== Drag.Extend.POSITIVE) {
return;
}
$a.removeClass('codicon-layout-panel-off');
$a.addClass('codicon-layout-panel');
});
const fileMenu = new Menu();
const settingMenu = new Menu();
this.#nav_.register({
id: 'file',
displayText: Msg.Lang['nav.btn.file'],
scopeType: Nav.Scope.RIGHT,
weight: 1,
menu: fileMenu
});
this.#nav_.register({
id: 'setting',
displayText: Msg.Lang['nav.btn.setting'],
scopeType: Nav.Scope.RIGHT,
weight: 2,
menu: settingMenu
});
fileMenu.add({
weight: 0,
id: 'new',
preconditionFn: () => {
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.file.new'], 'Ctrl+N', 'icon-doc-new'),
callback: () => File.new()
}
});
hotkeys('ctrl+n', function(event) {
event.preventDefault();
event.stopImmediatePropagation();
File.new();
});
fileMenu.add({
weight: 1,
id: 'open-file',
preconditionFn: () => {
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.file.open'], 'Ctrl+O', 'icon-doc'),
callback: (key, opt) => File.open()
}
});
hotkeys('ctrl+o', function(event) {
event.preventDefault();
File.open();
});
fileMenu.add({
weight: 2,
id: 'sep1',
data: '---------'
});
fileMenu.add({
weight: 3,
id: 'save',
preconditionFn: () => {
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.file.save'], 'Ctrl+S', 'icon-floppy'),
callback: () => File.save()
}
});
hotkeys('ctrl+s', function(event) {
event.preventDefault();
File.save();
});
fileMenu.add({
weight: 4,
id: 'save-as',
preconditionFn: () => {
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.file.saveAs'], 'Ctrl+Shift+S', 'icon-save-as'),
callback: () => File.saveAs()
}
});
hotkeys('ctrl+shift+s', function(event) {
event.preventDefault();
File.saveAs();
});
fileMenu.add({
weight: 5,
id: 'sep2',
preconditionFn: () => {
return goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary;
},
data: '---------'
});
fileMenu.add({
weight: 6,
id: 'export',
preconditionFn: () => {
return goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.file.exportAs'], 'Ctrl+E', 'icon-export'),
callback: () => File.exportLib()
}
});
if (goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary) {
hotkeys('ctrl+e', function(event) {
event.preventDefault();
File.exportLib();
});
}
settingMenu.add({
weight: 0,
id: 'manage-libraries',
preconditionFn: () => {
return goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.setting.manageLibs'], 'Ctrl+M', 'icon-menu'),
callback: () => LibManager.showManageDialog()
}
});
if (goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary) {
hotkeys('ctrl+m', function(event) {
LibManager.showManageDialog();
});
}
settingMenu.add({
weight: 1,
id: 'sep1',
preconditionFn: () => {
return goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary;
},
data: '---------'
});
settingMenu.add({
weight: 2,
id: 'feedback',
preconditionFn: () => {
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.setting.feedback'], 'Ctrl+Shift+F', 'icon-comment-1'),
callback: () => {
const href = 'https://gitee.com/bnu_mixly/mixly3/issues';
Url.open(href);
}
}
});
hotkeys('ctrl+shift+f', function(event) {
const href = 'https://gitee.com/bnu_mixly/mixly3/issues';
Url.open(href);
});
settingMenu.add({
weight: 3,
id: 'wiki',
preconditionFn: () => {
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['nav.btn.setting.wiki'], 'Ctrl+H', 'icon-book-open'),
callback: () => {
const href = 'https://mixly.readthedocs.io/zh-cn/latest/contents.html';
Url.open(href);
}
}
});
hotkeys('ctrl+h', function(event) {
const href = 'https://mixly.readthedocs.io/zh-cn/latest/contents.html';
Url.open(href);
});
}
#addObserver_() {
this.#resizeObserver_ = new ResizeObserver((entries) => {
let contentRect = entries[0].contentRect;
if (!(contentRect.width || contentRect.height)) return;
this.resize();
});
this.#resizeObserver_.observe(this.getContent()[0]);
}
#onbeforeunload_() {
if (goog.isElectron) {
Loader.onbeforeunload();
} else {
let href = path.join(Env.srcDirPath, 'index.html') + '?' + Url.jsonToUrl({ boardType: BOARD.boardType });
window.location.replace(href);
}
}
getNav() {
return this.#nav_;
}
getWorkspace() {
return this.#workspace_;
}
getFooterBar() {
return this.#footerbar_;
}
resize() {
this.#nav_.resize();
this.#workspace_.resize();
this.#footerbar_.resize();
}
removeSkeleton() {
const $appLoading = $('.mixly-app-loading');
$appLoading.remove();
}
dispose() {
this.#resizeObserver_.disconnect();
this.#workspace_.dispose();
super.dispose();
}
}
Mixly.App = App;
});

View File

@@ -0,0 +1,67 @@
goog.loadJs('common', () => {
goog.require('path');
goog.provide('Mixly.ArduinoCLI');
class ArduinoCLI {
static {
this.COMMAND = {
boardAttach: 'board attach',
boardDetails: 'board details',
boardList: 'board list',
boardListAll: 'board listall',
boardSearch: 'board search',
burnBootloader: 'burn-bootloader',
cacheClean: 'cache clean',
compile: 'compile',
completion: 'completion',
configDump: 'config dump',
configInit: 'config init',
configAdd: 'config add',
configDelete: 'config delete',
configRemove: 'config remove',
configSet: 'config set',
coreDownload: 'core download',
coreInstall: 'core install',
coreList: 'core list',
coreSearch: 'core search',
coreUninstall: 'core uninstall',
coreUpdateIndex: 'core update-index',
coreUpgrade: 'core upgrade',
daemon: 'daemon',
debug: 'debug',
libDeps: 'lib deps',
libDownload: 'lib download',
libExamples: 'lib examples',
libInstall: 'lib install',
libList: 'lib list',
libSearch: 'lib search',
libUninstall: 'lib uninstall',
libUpdateIndex: 'lib update-index',
libUpgrade: 'lib upgrade',
monitor: 'monitor',
outdated: 'outdated',
sketchArchive: 'sketch archive',
sketchNew: 'sketch new',
update: 'update',
upgrade: 'upgrade',
upload: 'upload',
version: 'version'
};
}
#cliPath_ = '';
#configPath_ = '';
constructor(cliPath) {
this.#cliPath_ = cliPath;
this.#configPath_ = path.join(path.dirname(cliPath), 'config.json');
}
async exist() {
return true;
}
async exec(name, config) {}
}
});

View File

@@ -0,0 +1,20 @@
goog.loadJs('common', () => {
goog.provide('Mixly.ArduShell');
class ArduShell {
#arduinoCLI_ = null;
constructor(arduinoCLI) {
this.#arduinoCLI_ = arduinoCLI;
}
async upload(name, params) {}
async compile(name, params) {}
async checkBoardVersion() {}
}
Mixly.ArduShell = ArduShell;
});

View File

@@ -0,0 +1,265 @@
goog.loadJs('common', () => {
goog.require('Mixly.Env');
goog.require('Mixly.Config');
goog.require('Mixly.Storage');
goog.provide('Mixly.BoardConfigItem');
const {
Env,
Config,
Storage
} = Mixly;
const { USER, BOARD } = Config;
class BoardConfigItem {
/**
* @param boardName {string} 板卡名
* @param boardInfo {object} 某个板卡的配置信息
------------------------------------------
"板卡名": {
"group": "板卡所处分组",
"key": "板卡key",
"config": [
{
"label": "配置x的描述信息",
"key": "配置x的key",
"options": [
{
"key": "选项x的key",
"label": "选项x"
},
...
]
},
...
],
ignore: []
}
------------------------------------------
// 不支持
"板卡名": {
"key": "板卡key",
"config": {
"配置x的key": [
{
"key": "选项x的key",
"label": "选项x"
},
...
],
...
}
}
------------------------------------------
"板卡名": "板卡key"
------------------------------------------
*
**/
constructor(boardName, boardInfo) {
this.name = boardName;
this.config = { ...boardInfo.config };
this.ignore = [];
if (typeof boardInfo === 'string') {
this.key = boardInfo;
} else if (boardInfo instanceof Object) {
this.key = boardInfo.key;
this.ignore = boardInfo.ignore ?? [];
this.group = boardInfo.group;
} else {
this.key = boardName;
}
this.generateOptions();
}
/**
* @method 根据传入的某个板卡配置构造字典
--------------------------------------------
this.options {array}:
[
{
"name": "Flash Mode",
"key": "FlashMode",
--------------
"message": {
"zh-hans": "",
"zh-hant": "",
"en": ""
},
"message": "",
--------------
"options": [
{
"title": "QIO",
"id": "qio"
},
...
]
},
...
]
--------------------------------------------
this.selectedOptions {object}:
{
"xxxkey": {
"label": "xxx",
"key": "xxx"
},
...
}
例如:
{
"FlashMode": {
"label": "QIO",
"key": "qio"
},
...
}
--------------------------------------------
this.defaultOptions = this.selectedOptions;
--------------------------------------------
this.optionsInfo {object}:
{
"xxxkey": {
"key": [ "xxx", "xxx", ... ],
"label": [ "xxx", "xxx", ... ]
},
...
}
例如:
{
"FlashMode": {
"key": [ "qio", "dio", ... ],
"label": [ "QIO", "DIO", ... ]
},
...
}
--------------------------------------------
* @return {void}
**/
generateOptions() {
this.options = [];
this.selectedOptions = {};
this.defaultOptions = {};
this.optionsInfo = {};
if (!(this.config instanceof Object)) {
return;
}
for (let i in this.config) {
let child = this.config[i];
if (!(child.options instanceof Object)) {
continue;
}
if (!child.options.length) {
continue;
}
this.defaultOptions[child.key] = { ...child.options[0] };
this.optionsInfo[child.key] = {
key: [],
label: []
};
let childOptions = [];
for (let j in child.options) {
let childOption = child.options[j];
if (!(childOption instanceof Object)) {
continue;
}
childOptions.push({
title: childOption.label,
id: childOption.key
});
this.optionsInfo[child.key].label.push(childOption.label);
this.optionsInfo[child.key].key.push(childOption.key);
}
this.options.push({
name: child.label,
key: child.key,
messageId: child.messageId,
options: childOptions
})
}
this.selectedOptions = { ...this.defaultOptions };
}
/**
* @method 判断所要更新的配置项是否合法,
也即为判断新的配置项是否存在以及配置项的选中项是否在该配置项中存在
* @param name {string} 所要设置的配置项xxxkey
* @param value {object} 所要设置的配置项的新的值
value = {
"label": "xxx",
"key": "xxx"
}
* @return {boolean}
**/
optionIsLegal(name, value) {
let optionsType = Object.keys(this.defaultOptions);
if (!optionsType.includes(name)
|| !this.optionsInfo[name].key.includes(value.key)
|| !this.optionsInfo[name].label.includes(value.label)) {
return false;
} else {
return true;
}
}
/**
* @method 设置当前某个配置项的选中项
* @param name {string} 所要设置的配置项xxxkey
* @param value {object} 所要设置的配置项的新的值
value = {
"label": "xxx",
"key": "xxx"
}
* @return {void}
**/
setSelectedOption(name, value) {
if (this.optionIsLegal(name, value)) {
this.selectedOptions[name] = { ...value };
}
}
/**
* @method 设置当前某些配置项的选中项
* @param newOptions {object} 所要设置的配置项
newOptions = {
"xxxkey": {
"label": "xxx",
"key": "xxx"
},
...
}
* @return {void}
**/
setSelectedOptions(newOptions) {
if (!(newOptions instanceof Object)) {
return;
}
this.selectedOptions = this.selectedOptions ?? {};
for (let i in newOptions) {
this.setSelectedOption(i, newOptions[i]);
}
}
/**
* @method 储存新的配置项设置
* @return {void}
**/
writeSelectedOptions() {
USER.board = USER.board ?? {};
const { board } = USER;
board[BOARD.boardType] = board[BOARD.boardType] ?? {};
board[BOARD.boardType].key = this.key;
board[BOARD.boardType].default = { ...this.selectedOptions };
Storage.user('/', USER);
}
getCommandParam() {
}
}
Mixly.BoardConfigItem = BoardConfigItem;
});

View File

@@ -0,0 +1,405 @@
goog.loadJs('common', () => {
goog.require('tippy');
goog.require('layui');
goog.require('path');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Config');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.ToolboxSearcher');
goog.require('Mixly.MString');
goog.require('Mixly.Msg');
goog.require('Mixly.BoardConfigItem');
goog.require('Mixly.Profile');
goog.require('Mixly.EditorBlockly');
goog.require('Mixly.Debug');
goog.require('Mixly.Nav');
goog.provide('Mixly.Boards');
const {
Config,
LayerExt,
XML,
Env,
ToolboxSearcher,
MString,
Msg,
BoardConfigItem,
Profile,
EditorBlockly,
Debug,
Nav,
Boards
} = Mixly;
const { form } = layui;
const { BOARD, USER, SELECTED_BOARD } = Config;
Boards.NAME = [];
Boards.HAS_CONFIG_SETTING = false;
Boards.init = () => {
Boards.dict = {};
if (BOARD.board instanceof Object || BOARD.board instanceof String) {
for (let i in BOARD.board) {
Boards.dict[i] = new BoardConfigItem(i, BOARD.board[i]);
if (USER.board && USER.board[BOARD.boardType]) {
Boards.dict[i].setSelectedOptions(USER.board[BOARD.boardType].default);
}
}
} else {
Boards.dict[BOARD.boardType] = new BoardConfigItem(BOARD.boardType, BOARD.boardType);
}
Boards.NAME = Object.keys(Boards.dict);
const $boards = $('#boards-type');
const $boardSelector = Nav.getMain().getBoardSelector();
$boardSelector.empty();
for (let name of Object.keys(Boards.dict)) {
const { group } = Boards.dict[name];
const option = new Option(name, Boards.dict[name].key);
if (group) {
let $optGroup = $boardSelector.children(`optgroup[label="${group}"]`);
if (!$optGroup.length) {
$optGroup = $(`<optgroup label="${group}"></optgroup>`);
$boardSelector.append($optGroup);
}
$optGroup.append(option);
} else {
$boardSelector.append(option);
}
}
Nav.getMain().bind('changeBoard', (data) => {
Boards.changeTo(data.text);
Boards.updateCategories(data.text);
});
}
Boards.addLayer = (boardConfigLayer) => {
Boards.configMenu = boardConfigLayer;
}
Boards.getType = () => {
const { boardIndex = '' } = BOARD;
return path.basename(path.dirname(boardIndex)) ?? BOARD.boardType;
}
Boards.getSelectedBoardName = () => {
return $('#boards-type option:selected').text();
}
Boards.getSelectedBoardKey = () => {
return $('#boards-type option:selected').val();
}
Boards.setSelectedBoard = (name, userConfig) => {
const charIndex = name.indexOf('@');
if (charIndex !== -1) {
name = name.substring(charIndex + 1, name.length);
}
if (!Boards.NAME.includes(name))
return;
const boardInfo = Boards.dict[name];
$("#boards-type").val(boardInfo.key).trigger('change');
boardInfo.setSelectedOptions(userConfig);
Boards.changeTo(name);
Boards.updateCategories(name);
if (Profile[name]) {
Profile['default'] = Profile[name];
}
}
Boards.getSelectedBoardConfig = () => {
const boardName = Boards.getSelectedBoardName();
return structuredClone(Boards.dict[boardName]?.selectedOptions ?? {});
}
Boards.getSelectedBoardCommandParam = () => {
return Boards.configMenu.getSelectedParams();
}
Boards.getSelectedBoardConfigParam = (name) => {
return Boards.configMenu.getSelectedParamByName(name);
}
/**
* @function 更新当前所选择板卡及其相关配置
* @param boardName {string} 板卡名
* @return {void}
**/
Boards.changeTo = (boardName) => {
Boards.configMenu.changeTo(boardName);
if (Profile[boardName]) {
Profile['default'] = Profile[boardName];
} else {
Profile['default'] = Profile['default'] ?? {};
}
EditorBlockly.reloadWorkspace();
const boardKey = Boards.dict[boardName].key;
for (let i in SELECTED_BOARD) {
delete SELECTED_BOARD[i];
}
for (let i in BOARD) {
if (BOARD[i] instanceof Object) {
SELECTED_BOARD[i] = { ...BOARD[i] };
} else {
SELECTED_BOARD[i] = BOARD[i];
}
}
SELECTED_BOARD['boardName'] = boardName;
if (BOARD.web instanceof Object) {
for (let value of [{
type: 'burn',
obj: BOARD.web.burn
}, {
type: 'upload',
obj: BOARD.web.upload
}, {
type: 'devices',
obj: BOARD.web.devices
}]) {
if (!(value.obj instanceof Object)) {
continue;
}
let outObj;
if (value.obj[boardKey]) {
outObj = { ...value.obj, ...value.obj[boardKey] };
} else {
outObj = { ...value.obj };
}
for (let i in Boards.dict) {
const key = Boards.dict[i].key;
if (outObj[key]) {
delete outObj[key];
}
}
SELECTED_BOARD.web[value.type] = outObj;
}
}
for (let value of [{
type: 'burn',
obj: BOARD.burn
}, {
type: 'upload',
obj: BOARD.upload
}, {
type: 'serial',
obj: BOARD.serial
}]) {
if (!(value.obj instanceof Object)) {
continue;
}
let outObj;
if (value.obj[boardKey]) {
outObj = { ...value.obj, ...value.obj[boardKey] };
} else {
outObj = { ...value.obj };
}
for (let i in Boards.dict) {
const key = Boards.dict[i].key;
if (outObj[key]) {
delete outObj[key];
}
}
const pathObj = {
path: Env.clientPath,
indexPath: Env.boardDirPath,
srcPath: Env.srcDirPath
};
switch (outObj.type) {
case 'volume':
if (Env.currentPlatform == "win32") {
if (typeof outObj.volumeName == "string") {
outObj.volume = "VolumeName='" + outObj.volumeName + "'";
} else if (typeof outObj.volumeName == "object") {
outObj.volume = "VolumeName='" + outObj.volumeName[0] + "'";
for (let i = 1; i < outObj.volumeName.length; i++) {
outObj.volume += " or VolumeName='" + outObj.volumeName[i] + "'";
}
} else {
outObj.volume = "VolumeName='CIRCUITPY'";
}
} else {
if (typeof outObj.volumeName == "string") {
outObj.volume = outObj.volumeName;
} else if (typeof outObj.volumeName == "object") {
outObj.volume = outObj.volumeName[0];
for (var i = 1; i < outObj.volumeName.length; i++) {
outObj.volume += "/" + outObj.volumeName[i];
}
} else {
outObj.volume = "CIRCUITPY";
}
}
if (!Env.hasSocketServer) {
outObj.filePath = MString.tpl(outObj.filePath, pathObj);
}
break;
case 'command':
let pyToolsPath = "{srcPath}/tools/python/";
let obj = {};
let pyTools = {
'esptool': 'esptool_main.py',
'kflash': 'kflash.py',
'stm32loader': 'stm32loader.py',
'stm32bl': 'stm32bl.py',
'ampy': 'ampy_main.py'
};
if (!Env.hasSocketServer) {
for (let key in pyTools) {
obj[key] = Env.python3Path + "\" \"" + pyToolsPath + pyTools[key];
}
}
if (outObj.reset) {
let resetStr = '{}';
try {
resetStr = JSON.stringify(outObj.reset);
resetStr = resetStr.replaceAll('\"', '\\\"');
obj.reset = resetStr;
} catch (error) {
Debug.error(error);
}
}
outObj.command = MString.tpl(outObj.command, obj);
if (!Env.hasSocketServer) {
outObj.command = MString.tpl(outObj.command, pathObj);
}
if (outObj.special && outObj.special instanceof Array) {
for (let key in outObj.special) {
if (!outObj.special[key]?.name
|| !outObj.special[key]?.command) {
continue;
}
outObj.special[key].command = MString.tpl(outObj.special[key].command, obj);
if (!Env.hasSocketServer) {
outObj.special[key].command = MString.tpl(outObj.special[key].command, pathObj);
}
}
}
break;
}
if (value.type === 'upload' && (goog.isElectron || Env.hasSocketServer) && outObj.copyLib) {
if (outObj.libPath) {
if (!Env.hasSocketServer) {
let libPath = [];
for (let dirPath of outObj.libPath) {
libPath.push(MString.tpl(dirPath, pathObj));
}
outObj.libPath = libPath;
}
} else {
if (Env.hasSocketServer) {
outObj.libPath = [ 'build/lib/' ];
} else {
outObj.libPath = [ path.join(Env.boardDirPath, 'build/lib/') ];
}
}
}
if (value.type === 'upload' && (goog.isElectron && !Env.hasSocketServer)) {
if (outObj.filePath) {
outObj.filePath = MString.tpl(outObj.filePath, pathObj);
} else {
outObj.filePath = path.join(Env.boardDirPath, 'build/main.py');
}
}
SELECTED_BOARD[value.type] = outObj;
}
}
Boards.updateCategories = (boardName, enforce = false) => {
if (Boards.selected === boardName && !enforce) return;
Boards.selected = boardName;
$('#mixly-footer-boardname').html(boardName);
let thirdPartyStr = '';
if (goog.isElectron) {
thirdPartyStr = Env.thirdPartyXML.join('');
}
const searchCategoryStr = '<category id="catSearch" hidden="true" colour="#ff6666"><label text="'
+ Msg.Lang['toolboxSearcher.empty']
+ '"></label></category>';
thirdPartyStr = Boards.selectCategories(boardName, thirdPartyStr);
const toolboxDom = $('#toolbox');
toolboxDom.html(
Boards.selectCategories(boardName, XML.CATEGORIES_STR[boardName] ?? Env.defaultXML)
);
toolboxDom.append(thirdPartyStr);
toolboxDom.append(searchCategoryStr);
const categoriesDom = toolboxDom.find('category');
for (let i = 0; categoriesDom[i]; i++) {
if (categoriesDom[i].hasAttribute('toolboxitemid')) continue;
categoriesDom[i].setAttribute('toolboxitemid', categoriesDom[i].id);
}
Msg.renderToolbox(false);
EditorBlockly.updateToolbox();
}
Boards.selectCategories = (boardName, categoriesStr) => {
const boardKeyList = (Boards.dict[boardName] ? Boards.dict[boardName].key : '').split(':');
if (!boardKeyList.length) return categoriesStr;
const xmlDom = $('<xml></xml>');
xmlDom.html(categoriesStr);
const categories = xmlDom.find('category');
for (let i = 0; categories[i]; i++) {
const removed = Boards.removeBlocks($(categories[i]), boardKeyList);
if (!removed) {
const blocks = $(categories[i]).children('block');
for (let j = 0; blocks[j]; j++) {
Boards.removeBlocks($(blocks[j]), boardKeyList);
}
}
}
return xmlDom.html();
}
Boards.removeBlocks = (blocksdom, boardKeyList) => {
const mShow = blocksdom.attr('m-show');
const mHide = blocksdom.attr('m-hide');
if (mShow || mHide) {
const select = mShow ? mShow : mHide;
let needRemove = mShow ? true : false;
const selectList = select.split(' ');
for (let key of selectList) {
const keyList = key.split(':');
const len = keyList.length;
if (![1, 2, 3].includes(len)) {
continue;
}
if ([2, 3].includes(len)) {
const param3 = (len === 3 ? String(keyList[2]).split(',') : []);
if (keyList[0] === boardKeyList[0]
&& keyList[1] === boardKeyList[1]) {
if (!param3.length) {
needRemove = mShow ? false : true;
break;
}
for (let value of param3) {
if (value === boardKeyList[2]) {
needRemove = mShow ? false : true;
break;
}
}
}
} else {
if (keyList[0] === boardKeyList[2]) {
needRemove = mShow ? false : true;
break;
}
}
if ((!needRemove && mShow) || (needRemove && !mShow))
break;
}
if (needRemove) {
blocksdom.remove();
return true;
}
}
return false;
}
});

View File

@@ -0,0 +1,60 @@
goog.loadJs('common', () => {
goog.require('workerpool');
goog.provide('Mixly.Registry');
goog.provide('Mixly.CodeFormatter');
const {
Registry,
CodeFormatter
} = Mixly;
CodeFormatter.supportCodeFormatters_ = new Registry();
CodeFormatter.activeCodeFormatters_ = new Registry();
CodeFormatter.supportCodeFormatters_.register('python', {
name: 'pythonFormatterService',
path: '../common/modules/mixly-modules/workers/common/python-formatter/index.js'
});
CodeFormatter.supportCodeFormatters_.register('cpp', {
name: 'cppFormatterService',
path: '../common/modules/mixly-modules/workers/common/cpp-formatter/index.js'
});
CodeFormatter.activateFormatter = async function (type) {
if (!this.supportCodeFormatters_.hasKey(type)) {
return null;
}
const info = this.supportCodeFormatters_.getItem(type);
if (this.activeCodeFormatters_.hasKey(type)) {
const formatter = this.activeCodeFormatters_.getItem(type);
if (formatter.loading) {
await formatter.loading;
}
return formatter;
}
const formatter = workerpool.pool(info.path, {
workerOpts: {
name: info.name
},
workerType: 'web'
});
formatter.loading = formatter.exec('init');
this.activeCodeFormatters_.register(type, formatter);
await formatter.loading;
formatter.loading = null;
return formatter;
}
CodeFormatter.format = async function (type, code) {
const formatter = await this.activateFormatter(type);
if (!formatter) {
return code;
}
const formattedCode = await formatter.exec('format', [code]);
return formattedCode;
}
});

View File

@@ -0,0 +1,52 @@
goog.loadJs('common', () => {
goog.require('Mixly.Config');
goog.require('Mixly.MJson')
goog.require('Mixly.Debug');
goog.provide('Mixly.Command');
const {
Config,
Command,
MJson,
Debug
} = Mixly;
const { SOFTWARE } = Config;
Command.DEFAULT = {
obj: '',
func: '',
args: []
}
Command.parse = (commandStr) => {
return MJson.decode(MJson.parse(commandStr));
}
Command.run = (commandObj) => {
Debug.log('收到指令:', commandObj);
if (typeof commandObj !== 'object') return;
commandObj = {
...Command.DEFAULT,
...commandObj
};
const { obj, func, args } = commandObj;
const objList = obj.split('.');
if (objList.length === 0) return;
let nowObj = window[objList[0]];
if (!nowObj) return;
objList.shift();
for (let i of objList) {
nowObj = nowObj[i];
if (!nowObj) return;
}
try {
if (nowObj[func])
nowObj[func](...args);
} catch (error) {
Debug.error(error);
}
}
});

View File

@@ -0,0 +1,110 @@
goog.loadJs('common', () => {
goog.require('Mixly.Events');
goog.require('Mixly.IdGenerator');
goog.provide('Mixly.Component');
const {
Events,
IdGenerator
} = Mixly;
class Component {
#$content_ = null;
#mounted_ = false;
#disposed_ = false;
#events_ = new Events(['destroyed']);
#id_ = IdGenerator.generate();
constructor() {}
mountOn($container) {
if (this.#mounted_) {
this.unmount();
}
$container.append(this.#$content_);
this.onMounted();
}
unmount() {
this.#$content_ && this.#$content_.detach();
this.onUnmounted();
}
onMounted() {
this.#mounted_ = true;
}
onUnmounted() {
this.#mounted_ = false;
}
isMounted() {
return this.#mounted_;
}
isDisposed() {
return this.#disposed_;
}
setContent($elem) {
if (this.#$content_) {
this.#$content_.replaceWith($elem);
}
this.#$content_ = $elem;
$elem.attr('page-id', this.#id_);
}
getContent() {
return this.#$content_;
}
resize() {}
getId() {
return this.#id_;
}
setId(id) {
this.#id_ = id;
}
dispose() {
this.#$content_.remove();
this.#$content_ = null;
this.runEvent('destroyed');
this.resetEvent();
this.#events_ = null;
this.#disposed_ = true;
this.#mounted_ = false;
}
bind(type, func) {
return this.#events_.bind(type, func);
}
unbind(id) {
this.#events_.unbind(id);
}
addEventsType(eventsType) {
this.#events_.addType(eventsType);
}
runEvent(eventsType, ...args) {
return this.#events_.run(eventsType, ...args);
}
offEvent(eventsType) {
this.#events_.off(eventsType);
}
resetEvent() {
this.#events_.reset();
}
}
Mixly.Component = Component;
});

View File

@@ -0,0 +1,124 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('FingerprintJS');
goog.require('Mixly.Url');
goog.require('Mixly.Env');
goog.require('Mixly.LocalStorage');
goog.provide('Mixly.Config');
const {
Config,
Url,
Env,
LocalStorage
} = Mixly;
// 被选中板卡的配置信息
Config.SELECTED_BOARD = {};
// 板卡页面的配置信息
Config.BOARD = {};
// 软件的配置信息
Config.SOFTWARE = {};
Config.USER = {
theme: 'light',
language: 'zh-hans',
winSize: 1,
blockRenderer: 'geras',
compileCAndH: 'true'
};
const BOARD_DEFAULT_CONFIG = {
"burn": "None",
"upload": "None",
"nav": "None",
"serial": "None",
"saveMixWithCode": true,
"thirdPartyBoard": false
};
const SOFTWARE_DEFAULT_CONFIG = {
"version": "Mixly3.0"
};
/**
* @function 读取软件、板卡的配置信息
* @return {void}
**/
Config.init = () => {
const urlConfig = Url.getConfig();
Config.BOARD = {
...goog.readJsonSync(path.join(Env.boardDirPath, 'config.json'), BOARD_DEFAULT_CONFIG),
...urlConfig
};
if (typeof Config.BOARD.board === 'string'
&& path.extname(Config.BOARD.board) === '.json') {
Config.BOARD.board = goog.readJsonSync(path.join(Env.boardDirPath, Config.BOARD.board));
}
let pathPrefix = '../';
Config.SOFTWARE = goog.readJsonSync(path.join(Env.srcDirPath, 'sw-config.json'), SOFTWARE_DEFAULT_CONFIG);
Config.pathPrefix = pathPrefix;
Env.hasSocketServer = Config.SOFTWARE?.webSocket?.enabled ? true : false;
Env.hasCompiler = Config.SOFTWARE?.webCompiler?.enabled ? true : false;
Config.USER = {
...Config.USER,
...LocalStorage.get(LocalStorage.PATH['USER']) ?? {}
};
if (Config.USER.themeAuto) {
const themeMedia = window.matchMedia("(prefers-color-scheme: light)");
Config.USER.theme = themeMedia.matches ? 'light' : 'dark';
}
if (Config.USER.languageAuto) {
switch (navigator.language) {
case 'zh-CN':
Config.USER.language = 'zh-hans';
break;
case 'zh-HK':
case 'zh-SG':
case 'zh-TW':
Config.USER.language = 'zh-hant';
break;
default:
Config.USER.language = 'en';
}
}
if (Config.USER.visitorId) {
Config.BOARD.visitorId = { ...Config.USER.visitorId };
} else {
FingerprintJS.load()
.then(fp => fp.get())
.then(result => {
let visitorId16 = result.visitorId;
let VisitorIdNum = parseInt(visitorId16, 16);
let visitorId32 = VisitorIdNum.toString(32);
let visitorId = {
str16: visitorId16,
str32: visitorId32,
str16CRC32b: Url.CRC32(visitorId16, 16),
str32CRC32b: Url.CRC32(visitorId32, 16)
};
LocalStorage.set(LocalStorage.PATH['USER'] + '/visitorId', visitorId);
Config.BOARD.visitorId = visitorId;
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log(Config.BOARD);
});
}
}
Config.init();
});

View File

@@ -0,0 +1,116 @@
goog.loadJs('common', () => {
goog.require('$.contextMenu');
goog.require('Mixly.Menu');
goog.require('Mixly.Events');
goog.require('Mixly.Registry');
goog.provide('Mixly.ContextMenu');
const {
Menu,
Events,
Registry
} = Mixly;
class ContextMenu {
static {
this.generate = (menu, $trigger) => {
let menuItems = {};
for (let item of menu.getAllItems()) {
if (typeof item.preconditionFn === 'function'
&& !item.preconditionFn($trigger)) {
continue;
}
if (item.children) {
item.data.items = this.generate(item.children);
}
menuItems[item.id] = item.data;
}
return menuItems;
}
}
#selector_ = null;
#menus_ = new Registry();
#events_ = new Events(['getMenu']);
constructor(selector, config = {}) {
this.#selector_ = selector;
this.menu = $.contextMenu({
selector,
build: ($trigger) => {
return { items: this.#getMenu_($trigger) }
},
animation: { duration: 0, show: 'show', hide: 'hide' },
...config
});
}
#getMenu_($trigger, e) {
const outputs = this.runEvent('getMenu', $trigger, e);
if (!outputs.length) {
return {};
}
const menu = this.#menus_.getItem(outputs[0]);
if (!menu) {
return {};
}
return ContextMenu.generate(menu, $trigger, e);
}
show() {
$(this.#selector_).contextMenu();
}
hide() {
$(this.#selector_).contextMenu('hide');
}
dispose() {
$.contextMenu('destroy', this.#selector_);
this.#events_.reset();
this.#menus_.reset();
this.menu = null;
this.#selector_ = null;
}
bind(type, func) {
return this.#events_.bind(type, func);
}
unbind(id) {
this.#events_.unbind(id);
}
addEventsType(eventsType) {
this.#events_.addType(eventsType);
}
runEvent(eventsType, ...args) {
return this.#events_.run(eventsType, ...args);
}
offEvent(eventsType) {
this.#events_.off(eventsType);
}
resetEvent() {
this.#events_.reset();
}
register(id, menu) {
this.#menus_.register(id, menu);
}
unregister(id) {
this.#menus_.unregister(id);
}
getItem(id) {
return this.#menus_.getItem(id);
}
}
Mixly.ContextMenu = ContextMenu;
});

View File

@@ -0,0 +1,44 @@
goog.loadJs('common', () => {
goog.require('Mixly');
goog.provide('Mixly.CssLoader');
const { CssLoader } = Mixly;
/**
* 加载 link 文件
* @param href
*/
CssLoader.loadCss = function (href) {
let addSign = true;
let links = document.getElementsByTagName('link');
for (let i = 0; i < links.length; i++) {
if (links[i] && links[i].href && links[i].href.indexOf(href) !== -1) {
addSign = false;
}
}
if (addSign) {
let $link = document.createElement('link');
$link.setAttribute('rel', 'stylesheet');
$link.setAttribute('type', 'text/css');
$link.setAttribute('href', href);
document.getElementsByTagName('head').item(0).appendChild($link);
}
}
/**
* 删除 link 文件
* @param href
*/
CssLoader.removeCss = function (href) {
let links = document.getElementsByTagName('link');
for (let i = 0; i < links.length; i++) {
let _href = links[i].href;
if (links[i] && links[i].href && links[i].href.indexOf(href) !== -1) {
links[i].parentNode.removeChild(links[i]);
}
}
}
});

View File

@@ -0,0 +1,22 @@
goog.loadJs('common', () => {
goog.require('Mixly.Config');
goog.provide('Mixly.Debug');
const { Config, Debug } = Mixly;
const { SOFTWARE } = Config;
for (let key in console) {
if (typeof console[key] !== 'function') {
continue;
}
Debug[key] = (...args) => {
if (SOFTWARE.debug) {
console[key](...args);
} else {
console.log(`[${key.toUpperCase()}]`, ...args);
}
}
}
});

View File

@@ -0,0 +1,368 @@
goog.loadJs('common', () => {
goog.require('Mixly.Config');
goog.require('Mixly.Events');
goog.provide('Mixly.Drag');
goog.provide('Mixly.DragH');
goog.provide('Mixly.DragV');
const { Config, Events } = Mixly;
const { BOARD } = Config;
class Drag {
static {
this.DEFAULT_CONFIG = {
type: 'h', // 'h' - 水平拖拽,'v' - 垂直拖拽
min: '100px', // 元素由于拖拽产生尺寸改变时可以减小到的最小值,
full: [true, true], // 允许元素拖拽直至占满整个容器
startSize: '100%',
startExitFullSize: '30%'
};
this.Extend = {
POSITIVE: 1, // 左或上
NEGATIVE: 2, // 右或下
BOTH: 3 // 左+右或上+下
};
}
#events_ = null;
constructor(element, config) {
this.config = { ...Drag.DEFAULT_CONFIG, ...config };
this.$container = $(element);
const $children = this.$container.children();
this.$first = $($children[0]);
this.$last = $($children[1]);
this.config.elem = [ this.$first, this.$last ];
this.$dragElem = $('<div class="drag-elem"></div>');
const dragType = this.config.type === 'h'? 's' : 'w';
this.$container.addClass(`drag-${dragType}-container`);
this.shown = Drag.Extend.POSITIVE;
let dragCssType;
if (this.config.type === 'h') {
this.$dragElem.addClass('drag-s-elem horizontal-line');
dragCssType = 'top';
} else {
this.$dragElem.addClass('drag-w-elem vertical-line');
dragCssType = 'left';
}
let size = parseFloat(this.config.startSize);
if (size >= 100) {
this.$dragElem.css(dragCssType, 'calc(100% - 4px)');
size = 100;
this.shown = Drag.Extend.POSITIVE;
} else if (size > 0) {
this.$dragElem.css(dragCssType, `calc(${size}% - 2px)`);
this.shown = Drag.Extend.BOTH;
} else {
this.$dragElem.css(dragCssType, '0px');
size = 0;
this.shown = Drag.Extend.NEGATIVE;
}
this.$container.prepend(this.$dragElem);
this.size = [`${size}%`, `${100 - size}%`];
if (size >=100 || size <=0) {
const startExitFullSize = parseFloat(this.config.startExitFullSize);
this.prevSize = [`${startExitFullSize}%`, `${100 - startExitFullSize}%`];
} else {
this.prevSize = this.size;
}
this.firstDisplay = this.$first.css('display');
this.lastDisplay = this.$last.css('display');
if (!this.firstDisplay || this.firstDisplay === 'none') {
this.firstDisplay = 'unset';
}
if (!this.lastDisplay || this.lastDisplay === 'none') {
this.lastDisplay = 'unset';
}
this.#events_ = new Events(['ondragStart', 'ondragEnd', 'onfull', 'exitfull', 'sizeChanged']);
this.#addEventListener_();
}
#getTouch_(event) {
if (event.touches && event.touches.length > 0) {
return {
clientX: event.touches[0].clientX,
clientY: event.touches[0].clientY
};
} else {
return { clientX: 0, clientY: 0 };
}
};
#addEventListener_() {
const dragElem = this.$dragElem[0];
const container = this.$container[0];
const { type, min, elem, full } = this.config;
dragElem.onpointerdown = (elemEvent) => {
const { currentTarget } = elemEvent;
currentTarget.setPointerCapture(elemEvent.pointerId);
};
dragElem.onpointerup = (elemEvent) => {
const { currentTarget } = elemEvent;
currentTarget.releasePointerCapture(elemEvent.pointerId);
};
dragElem.onmousedown = (elemEvent) => {
let dis, prev;
if (type === 'h') {
dis = elemEvent.clientY;
dragElem.top = dragElem.offsetTop;
} else {
dis = elemEvent.clientX;
dragElem.left = dragElem.offsetLeft;
}
const prevSize = this.size;
document.onmousemove = (docEvent) => {
this.runEvent('ondragStart');
let iT, maxT, minT = parseInt(min), movement;
if (type === 'h') {
iT = dragElem.top + (docEvent.clientY - dis);
maxT = container.clientHeight - minT;
movement = docEvent.movementY;
} else {
iT = dragElem.left + (docEvent.clientX - dis);
maxT = container.clientWidth - minT;
movement = docEvent.movementX;
}
iT += 1;
if (prev === iT) {
return false;
}
prev = iT;
if (full[0] && movement < 0 && iT < minT * 0.4) { // 向上移动或向左移动
this.changeTo('0%');
this.runEvent('onfull', Drag.Extend.NEGATIVE);
} else if (full[1] && movement > 0 && iT > (maxT + minT * 0.6)) { // 向下移动或向右移动
this.changeTo('100%');
this.runEvent('onfull', Drag.Extend.POSITIVE);
} else if (iT < maxT && iT > minT) { // 在minT和maxT间移动
let shown = this.shown;
this.changeTo(iT);
if (shown !== Drag.Extend.BOTH) {
this.runEvent('exitfull', shown);
}
}
this.runEvent('ondragEnd');
return false;
};
document.onmouseup = () => {
this.prevSize = prevSize;
document.onmousemove = null;
document.onmouseup = null;
};
};
dragElem.ontouchstart = (touchEvent) => {
touchEvent.preventDefault();
this.$container.addClass('dragging');
let touch = this.#getTouch_(touchEvent);
let dis, prev;
if (type === 'h') {
dis = touch.clientY;
dragElem.top = dragElem.offsetTop;
} else {
dis = touch.clientX;
dragElem.left = dragElem.offsetLeft;
}
const prevSize = this.size;
document.ontouchmove = (moveEvent) => {
moveEvent.preventDefault();
this.runEvent('ondragStart');
const current = this.#getTouch_(moveEvent);
let iT, maxT, minT = parseInt(min), movement;
if (type === 'h') {
iT = dragElem.top + (current.clientY - dis);
maxT = container.clientHeight - minT;
movement = current.clientY - dis;
} else {
iT = dragElem.left + (current.clientX - dis);
maxT = container.clientWidth - minT;
movement = current.clientX - dis;
}
iT += 1;
if (prev === iT) return false;
prev = iT;
if (full[0] && movement < 0 && iT < minT * 0.4) { // 向上或向左
this.changeTo('0%');
this.runEvent('onfull', Drag.Extend.NEGATIVE);
} else if (full[1] && movement > 0 && iT > (maxT + minT * 0.6)) { // 向下或向右
this.changeTo('100%');
this.runEvent('onfull', Drag.Extend.POSITIVE);
} else if (iT < maxT && iT > minT) { // 中间区域
let shown = this.shown;
this.changeTo(iT);
if (shown !== Drag.Extend.BOTH) {
this.runEvent('exitfull', shown);
}
}
this.runEvent('ondragEnd');
return false;
};
document.ontouchend = () => {
this.$container.removeClass('dragging');
this.prevSize = prevSize;
document.ontouchmove = null;
document.ontouchend = null;
};
};
}
changeTo(part) {
const { type, elem } = this.config;
const elemCssType = type === 'h'? 'height' : 'width';
const dragCssType = type === 'h'? 'top' : 'left';
let elem1Size, elem2Size, precent;
if (typeof part === 'string' && part.indexOf('%') !== -1) {
precent = parseFloat(part);
} else {
let all;
if (type === 'h') {
all = this.$container.height();
} else {
all = this.$container.width();
}
precent = 100 * parseFloat(part) / all;
}
elem1Size = `${precent}%`;
elem2Size = `${(100 - precent)}%`;
if (this.size[0] === elem1Size && this.size[1] === elem2Size) {
return;
}
this.prevSize = this.size;
this.size = [elem1Size, elem2Size];
if (!precent) {
this.$dragElem.css(dragCssType, '0px');
this.shown = Drag.Extend.NEGATIVE;
this.$first.css('display', 'none');
this.$last.css('display', this.lastDisplay);
} else if (precent >= 100) {
this.$dragElem.css(dragCssType, 'calc(100% - 4px)');
this.shown = Drag.Extend.POSITIVE;
this.$first.css('display', this.firstDisplay);
this.$last.css('display', 'none');
} else {
this.$dragElem.css(dragCssType, `calc(${elem1Size} - 2px)`);
this.shown = Drag.Extend.BOTH;
this.$first.css('display', this.firstDisplay);
this.$last.css('display', this.lastDisplay);
}
elem[0].css(elemCssType, elem1Size);
elem[1].css(elemCssType, elem2Size);
this.runEvent('sizeChanged');
}
full(type) {
if ([this.shown, Drag.Extend.BOTH].includes(type)) {
return;
}
if (this.shown !== Drag.Extend.BOTH) {
this.runEvent('exitfull', this.shown);
}
switch(type) {
case Drag.Extend.NEGATIVE:
this.changeTo('0%');
break;
case Drag.Extend.POSITIVE:
this.changeTo('100%');
}
this.runEvent('onfull', type);
}
exitfull() {
if (this.shown === Drag.Extend.BOTH) {
return;
}
let prevSize = this.prevSize[0];
if (prevSize === '100%') {
prevSize = '70%';
} else if (prevSize === '0%') {
prevSize = '30%';
}
let shown = this.shown;
this.changeTo(prevSize);
this.runEvent('exitfull', shown);
}
show(type) {
if ([Drag.Extend.BOTH, type].includes(this.shown)) {
return;
}
this.exitfull();
}
hide(type) {
switch (type) {
case Drag.Extend.NEGATIVE:
this.full(Drag.Extend.POSITIVE);
break;
case Drag.Extend.POSITIVE:
this.full(Drag.Extend.NEGATIVE);
break;
}
}
dispose() {
this.resetEvent();
this.$dragElem[0].onmousedown = null;
this.$dragElem[0].onpointerdown = null;
this.$dragElem[0].onpointerup = null;
this.$dragElem.remove();
}
bind(type, func) {
return this.#events_.bind(type, func);
}
unbind(id) {
this.#events_.unbind(id);
}
addEventsType(eventsType) {
this.#events_.addType(eventsType);
}
runEvent(eventsType, ...args) {
this.#events_.run(eventsType, ...args);
}
offEvent(eventsType) {
this.#events_.off(eventsType);
}
resetEvent() {
this.#events_.reset();
}
}
class DragH extends Drag {
constructor(elem, config) {
super(elem, {
...config,
type: 'h'
});
}
}
class DragV extends Drag {
constructor(elem, config) {
super(elem, {
...config,
type: 'v'
});
}
}
Mixly.DragH = DragH;
Mixly.DragV = DragV;
Mixly.Drag = Drag;
});

View File

@@ -0,0 +1,233 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('tippy');
goog.require('Mixly.Env');
goog.require('Mixly.Menu');
goog.require('Mixly.Registry');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.ContextMenu');
goog.require('Mixly.DropdownMenu');
goog.provide('Mixly.DropdownMenuGroup');
const {
Env,
Menu,
Registry,
HTMLTemplate,
IdGenerator,
ContextMenu,
DropdownMenu
} = Mixly;
class DropdownMenuGroup {
static {
HTMLTemplate.add(
'html/dropdown-menu-item.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/dropdown-menu-item.html')))
);
}
#singleton_ = null;
#menuItems_ = [];
#ids_ = {};
#instanceIds_ = {};
#activeInstance_ = null;
#instanceShown_ = false;
#menuShown_ = false;
#trigged_ = false;
#$instancePopper_ = null;
#$instanceContent_ = null;
#$content_ = null;
constructor(elem) {
this.#$content_ = $(elem);
this.#$content_.css('z-index', 200);
this.#singleton_ = tippy.createSingleton([], {
interactive: true,
maxWidth: 'none',
offset: [0, 3],
appendTo: document.body,
arrow: false,
placement: 'bottom-end',
animation: 'shift-toward-extreme',
hideOnClick: false,
delay: [200, null],
onShow: () => {
if (this.#activeInstance_) {
this.showMenu(this.#activeInstance_.id);
}
this.#instanceShown_ = true;
},
onTrigger: (_, event) => {
const id = $(event.currentTarget).attr('data-id');
if (this.#instanceShown_) {
if (this.#activeInstance_) {
this.#trigged_ = true;
this.hideMenu(this.#activeInstance_.id);
this.#activeInstance_ = null;
}
this.showMenu(id);
}
this.#activeInstance_ = this.#instanceIds_[id].instance;
},
onHide: () => {
if (this.#menuShown_) {
return false;
}
this.#instanceShown_ = false;
return true;
}
});
this.#$instancePopper_ = $(this.#singleton_.popper);
this.#$instancePopper_.addClass('mixly-drapdown-menu');
this.#$instanceContent_ = this.#$instancePopper_.children().children();
}
add(item) {
if (!item.id) {
if (item.type) {
item.id = item.type;
} else {
item.id = IdGenerator.generate();
}
}
if (!item.weight) {
item.weight = 0;
}
this.remove(item.id);
item.$elem = $(HTMLTemplate.get('html/dropdown-menu-item.html').render({
text: item.displayText
}));
const instance = tippy(item.$elem[0]);
item.$elem.attr('data-id', instance.id);
item.$i = item.$elem.children('i');
item.instance = instance;
const contextMenuId = IdGenerator.generate();
const selector = `body > .mixly-dropdown-menus > div[m-id="${contextMenuId}"]`;
const contextMenu = new ContextMenu(selector, {
trigger: 'none',
appendTo: this.#$instanceContent_,
shadow: true,
autoHide: false,
async: false,
zIndex: 150,
position: (opt) => {
opt.$menu.css('margin', 0);
},
events: {
show: (opt) => {
item.$i.addClass('menu-shown');
this.#menuShown_ = true;
this.#singleton_.setProps({});
},
hide: (opt) => {
item.$i.removeClass('menu-shown');
if (this.#trigged_) {
this.#trigged_ = false;
return true;
}
this.#menuShown_ = false;
this.#singleton_.hide();
}
}
});
item.contextMenu = contextMenu;
contextMenu.register('menu', item.menu);
contextMenu.bind('getMenu', () => 'menu');
item.$menu = $(`<div m-id="${contextMenuId}"><div>`);
DropdownMenu.$container.append(item.$menu);
let i = 0;
for (; i < this.#menuItems_.length; i++) {
if (this.#menuItems_[i].weight <= item.weight) {
continue;
}
break;
}
if (i === this.#menuItems_.length) {
if (this.#menuItems_.length) {
this.#menuItems_[i - 1].$elem.after(item.$elem);
} else {
this.#$content_.append(item.$elem);
}
} else {
this.#menuItems_[i].$elem.before(item.$elem);
}
this.#menuItems_.splice(i, 0, item);
this.#ids_[item.id] = item;
this.#instanceIds_[instance.id] = item;
const instances = [];
for (let menuItem of this.#menuItems_) {
instances.push(menuItem.instance);
}
this.#singleton_.setInstances(instances);
return item.id;
}
getContextMenu(id) {
if (!this.#ids_[id]) {
return null;
}
return this.#ids_[id].contextMenu;
}
getInstance() {
return this.#singleton_;
}
remove(id) {
let item = this.#ids_[id];
if (!item) {
return;
}
delete this.#ids_[id];
const instanceId = item.instance.id;
delete this.#instanceIds_[instanceId];
for (let i in this.#menuItems_) {
if (this.#menuItems_[i].id !== id) {
continue;
}
this.#menuItems_.splice(i, 1);
break;
}
item.instance.destroy();
item.contextMenu.dispose();
item.$elem.remove();
item.$menu.remove();
item = null;
}
showMenu(instanceId) {
const item = this.#instanceIds_[instanceId];
item.$menu.contextMenu();
}
hideMenu(instanceId) {
const item = this.#instanceIds_[instanceId];
item.$menu.contextMenu('hide');
}
dispose() {
super.dispose();
this.#$instanceContent_.remove();
this.#$instanceContent_ = null;
this.#$instancePopper_.remove();
this.#$instancePopper_ = null;
this.#$content_.empty();
this.#$content_ = null;
for (let id in this.#ids_) {
this.remove(id);
}
this.#singleton_.destroy();
this.#singleton_ = null;
this.#menuItems_ = null;
this.#ids_ = null;
this.#instanceIds_ = null;
this.#activeInstance_ = null;
}
}
Mixly.DropdownMenuGroup = DropdownMenuGroup;
});

View File

@@ -0,0 +1,97 @@
goog.loadJs('common', () => {
goog.require('tippy');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.ContextMenu');
goog.provide('Mixly.DropdownMenu');
const {
IdGenerator,
ContextMenu
} = Mixly;
class DropdownMenu extends ContextMenu {
static {
this.$container = $('<div class="mixly-dropdown-menus"></div>');
$(document.body).append(this.$container);
}
#shown_ = false;
#layer_ = null;
#contextMenuId_ = '';
#$contextMenuElem_ = null;
constructor(elem) {
const layer = tippy(elem, {
allowHTML: true,
content: '',
trigger: 'click',
interactive: true,
maxWidth: 'none',
offset: [0, 0],
appendTo: document.body,
arrow: false,
placement: 'bottom-start',
delay: 0,
duration: [0, 0],
onCreate: (instance) => {
$(instance.popper).addClass('mixly-drapdown-menu');
},
onMount: () => {
this.show();
},
onHide: () => {
this.#shown_ && this.hide();
}
});
const contextMenuId = IdGenerator.generate();
const selector = `body > .mixly-dropdown-menus > div[m-id="${contextMenuId}"]`;
super(selector, {
trigger: 'none',
appendTo: $(layer.popper).children().children(),
zIndex: 1001,
position: (opt) => {
opt.$menu.css('margin', 0);
},
events: {
show: () => {
this.#shown_ = true;
this.#layer_.setProps({});
},
hide: () => {
this.#shown_ = false;
if (this.#layer_.state.isShown) {
this.#layer_.hide();
}
}
}
});
this.#$contextMenuElem_ = $(`<div m-id="${contextMenuId}"><div>`);
DropdownMenu.$container.append(this.#$contextMenuElem_);
this.#contextMenuId_ = contextMenuId;
this.#layer_ = layer;
}
show() {
this.#$contextMenuElem_.contextMenu();
}
hide() {
this.#$contextMenuElem_.contextMenu('hide');
}
dispose() {
super.dispose();
this.#layer_.destroy();
this.#layer_ = null;
this.#$contextMenuElem_.remove();
this.#$contextMenuElem_ = null;
}
}
Mixly.DropdownMenu = DropdownMenu;
});

View File

@@ -0,0 +1,409 @@
goog.loadJs('common', () => {
goog.require('ace');
goog.require('ace.ExtLanguageTools');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.EditorBase');
goog.provide('Mixly.EditorAce');
const {
XML,
Env,
Msg,
Debug,
HTMLTemplate,
EditorBase
} = Mixly;
class EditorAce extends EditorBase {
static {
this.CTRL_BTNS = ['resetFontSize', 'increaseFontSize', 'decreaseFontSize'];
this.CTRL_BTN_TEMPLATE = '<div m-id="{{d.mId}}" class="code-editor-btn setFontSize"></div>';
this.MODE_MAP = goog.readJsonSync(path.join(Env.templatePath, 'json/ace-mode-map.json'));
HTMLTemplate.add(
'html/editor/editor-code.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-code.html')))
);
if (['zh-hans', 'zh-hant'].includes(Msg.nowLang)) {
ace.config.setMessages(
goog.readJsonSync(path.join(Env.templatePath, `json/ace.i18n.${Msg.nowLang}.json`))
);
}
}
#editor_ = null;
#destroyed_ = null;
#cursorLayer_ = null;
constructor() {
super();
const editorHTMLTemplate = HTMLTemplate.get('html/editor/editor-code.html');
this.setContent($(editorHTMLTemplate.render()));
this.id = editorHTMLTemplate.id;
}
init() {
super.init();
this.#editor_ = ace.edit(this.getContent()[0]);
this.resetFontSize();
this.#addCursorLayer_();
this.#addCursorEventsListener_();
this.#addDefaultCommand_();
}
getEditor() {
return this.#editor_;
}
dispose() {
super.dispose();
this.#editor_.destroy();
this.#destroyed_ = true;
}
setValue(data, scroll = true) {
if (this.#destroyed_) {
return;
}
this.#editor_.updateSelectionMarkers();
const { selection } = this.#editor_;
const initCursor = selection.getCursor();
if (this.getValue() !== data) {
this.#editor_.setValue(data);
}
if (scroll) {
this.scrollToBottom();
} else {
selection.moveCursorTo(initCursor.row, initCursor.column, true);
selection.clearSelection();
}
}
addValue(data, scroll = true) {
if (this.#destroyed_) {
return;
}
const { session } = this.#editor_;
const endCursor = this.getEndPos();
session.insert(endCursor, data);
if (scroll) {
this.scrollToBottom();
}
}
replaceLine(lineNumber, newContent) {
const session = this.#editor_.getSession();
const totalLines = session.getLength();
let targetLine = lineNumber < 0 ? totalLines + lineNumber : lineNumber;
if (targetLine < 0) {
targetLine = 0;
} else if (targetLine >= totalLines) {
targetLine = totalLines - 1;
}
const cursorPos = this.#editor_.getCursorPosition();
const oldLine = session.getLine(targetLine);
const range = new ace.Range(targetLine, 0, targetLine, oldLine.length);
session.replace(range, newContent);
if (cursorPos.row > targetLine) {
this.#editor_.moveCursorTo(cursorPos.row, cursorPos.column);
} else if (cursorPos.row === targetLine) {
const newCol = Math.min(cursorPos.column, newContent.length);
this.#editor_.moveCursorTo(targetLine, newCol);
} else {
this.#editor_.moveCursorTo(cursorPos.row, cursorPos.column);
}
this.#editor_.renderer.scrollCursorIntoView(null, 0.5);
}
appendLine(text) {
const session = this.#editor_.getSession();
const totalLines = session.getLength();
const lastLineIndex = totalLines - 1;
const lastLineText = session.getLine(lastLineIndex);
if (lastLineText.trim()) {
session.insert({ row: totalLines, column: 0 }, `\n${text}`);
} else {
const range = new ace.Range(lastLineIndex, 0, lastLineIndex, lastLineText.length);
session.replace(range, text);
}
this.scrollToBottom();
}
getValue() {
return this.#destroyed_ ? '' : this.#editor_.getValue();
}
getValueRange(startPos, endPos) {
if (this.#destroyed_ || !startPos || !endPos
|| typeof startPos !== 'object' || typeof endPos !== 'object') {
return "";
}
const session = this.#editor_.getSession();
return session.getTextRange(new ace.Range(
startPos.row,
startPos.column,
endPos.row,
endPos.column
));
}
getEndPos() {
if (this.#destroyed_) {
return { row: 0, column: 0 };
}
const session = this.#editor_.getSession();
const row = session.getLength() - 1;
const column = session.getLine(row).length;
return { row, column };
}
clear() {
this.setValue('', true);
}
scrollToBottom() {
if (this.#destroyed_) {
return;
}
const { selection, session } = this.#editor_;
this.#editor_.updateSelectionMarkers();
this.#editor_.gotoLine(session.getLength());
selection.moveCursorLineEnd();
}
scrollToTop() {
if (this.#destroyed_) {
return;
}
this.#editor_.gotoLine(0);
}
#addDefaultCommand_() {
const { commands } = this.#editor_;
commands.addCommands([{
name: "increaseFontSize",
bindKey: "Ctrl-=|Ctrl-+",
exec: (editor) => {
this.increaseFontSize();
}
}, {
name: "decreaseFontSize",
bindKey: "Ctrl+-|Ctrl-_",
exec: (editor) => {
this.decreaseFontSize();
}
}, {
name: "resetFontSize",
bindKey: "Ctrl+0|Ctrl-Numpad0",
exec: (editor) => {
this.resetFontSize();
}
}, {
name: "mixly-message",
bindKey: "backspace|delete|enter",
readOnly: true,
exec: (editor) => {
if (!editor.getReadOnly()) {
return false;
}
this.#cursorLayer_.show();
return false;
}
}]);
}
#addCursorLayer_() {
this.#cursorLayer_ = tippy(this.getContent().find('.ace_cursor')[0], {
content: Msg.Lang['editor.viewReadOnly'],
trigger: 'manual',
hideOnClick: true,
delay: 0,
duration: [ 0, 0 ],
placement: 'right',
offset: [ 0, 0 ],
popperOptions: {
strategy: 'fixed',
modifiers: [
{
name: 'flip',
options: {
fallbackPlacements: ['top-end', 'right']
}
}
]
}
});
}
#addCursorEventsListener_() {
const editor = this.getEditor();
$('#mixly-footer-cursor').hide();
editor.on('focus', () => {
const cursor = selection.getCursor();
$('#mixly-footer-row').html(cursor.row + 1);
$('#mixly-footer-column').html(cursor.column + 1);
$('#mixly-footer-cursor').show();
});
editor.on("blur", () => {
this.#cursorLayer_.hide();
$('#mixly-footer-cursor').hide();
});
const { selection } = editor.getSession();
const { session } = editor;
selection.on('changeCursor', () => {
const cursor = selection.getCursor();
$('#mixly-footer-row').html(cursor.row + 1);
$('#mixly-footer-column').html(cursor.column + 1);
});
selection.on("changeSelection", () => {
if (selection.isEmpty()) {
$('#mixly-footer-selected').parent().hide();
} else {
const range = selection.getRange();
const text = session.getTextRange(range);
$('#mixly-footer-selected').parent().css('display', 'inline-flex');
$('#mixly-footer-selected').html(text.length);
}
});
session.on("changeScrollTop", () => {
this.#cursorLayer_.hide();
});
}
addCtrlBtns() {
const $content = this.getContent();
for (let mId of EditorAce.CTRL_BTNS) {
$content.append(XML.render(EditorAce.CTRL_BTN_TEMPLATE, { mId }));
}
this.$ctrlBtns = $content.children('.code-editor-btn');
this.$ctrlBtns.off().click((event) => {
const mId = $(event.target).attr('m-id');
this[mId]();
});
}
showCtrlBtns() {
this.$ctrlBtns.css('display', 'block');
}
hideCtrlBtns() {
this.$ctrlBtns.css('display', 'none');
}
resetFontSize() {
this.#editor_.setFontSize(17);
}
increaseFontSize() {
const size = parseInt(this.#editor_.getFontSize(), 10) || 17;
this.#editor_.setFontSize(size + 1);
}
decreaseFontSize() {
const size = parseInt(this.#editor_.getFontSize(), 10) || 17;
this.#editor_.setFontSize(Math.max(size - 1 || 1));
}
undo() {
super.undo();
this.#editor_.undo();
}
redo() {
super.redo();
this.#editor_.redo();
}
setReadOnly(status) {
this.#editor_.setReadOnly(status);
}
setMode(type) {
this.#editor_.session.setMode(`ace/mode/${type}`);
}
setFileMode(extname) {
const mode = this.getFileMode(extname) ?? 'text';
this.setMode(mode);
}
getFileMode(extname) {
for (const [mode, extensions] of Object.entries(EditorAce.MODE_MAP)) {
if (extensions.includes(extname)) {
return mode;
}
}
return null;
}
cut() {
const { selection, session } = this.#editor_;
const cutLine = selection.isEmpty();
const range = cutLine ? selection.getLineRange() : selection.getRange();
this.#editor_._emit('cut', range);
if (!range.isEmpty()) {
const copyText = session.getTextRange(range);
navigator.clipboard.writeText(copyText)
.then(() => {
Debug.log('clipboard复制成功', copyText);
}).catch((error) => {
Debug.error('clipboard复制失败', error);
});
session.remove(range);
}
this.#editor_.clearSelection();
}
copy() {
const copyText = this.#editor_.getSelectedText();
this.#editor_.clearSelection();
if (!copyText) {
return;
}
navigator.clipboard.writeText(copyText)
.then(() => {
Debug.log('clipboard复制成功', copyText);
}).catch((error) => {
Debug.error('clipboard复制失败', error);
});
}
paste() {
navigator.clipboard.readText()
.then((message) => {
this.#editor_.execCommand('paste', message);
Debug.log('clipboard粘贴成功', message);
})
.catch((error) => {
Debug.error('clipboard粘贴失败', error);
});
}
commentLine() {
this.#editor_.execCommand('togglecomment');
}
blockComment() {
this.#editor_.execCommand('toggleBlockComment');
}
resize() {
this.#editor_.resize();
super.resize();
}
focus() {
this.#editor_.focus();
}
}
Mixly.EditorAce = EditorAce;
});

View File

@@ -0,0 +1,50 @@
goog.loadJs('common', () => {
goog.require('Mixly.Events');
goog.require('Mixly.PageBase');
goog.provide('Mixly.EditorBase');
const { Events, PageBase } = Mixly;
class EditorBase extends PageBase {
#$btnsContent_ = null;
constructor() {
super();
}
getBtnsContent() {
return this.#$btnsContent_;
}
setBtnsContent($elem) {
this.#$btnsContent_ = $elem;
}
getValue() {
return '';
}
setValue(data, ext) {
this.removeDirty();
}
getCode() {
return this.getValue();
}
dispose() {
this.#$btnsContent_ && this.#$btnsContent_.remove();
super.dispose();
}
undo() {}
redo() {}
empty() {}
}
Mixly.EditorBase = EditorBase;
});

View File

@@ -0,0 +1,367 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Blockly');
goog.require('WorkspaceSearch');
goog.require('Backpack');
goog.require('Minimap');
goog.require('PositionedMinimap');
goog.require('ContentHighlight');
goog.require('ZoomToFitControl');
goog.require('Blockly.FieldGridDropdown');
goog.require('Blockly.FieldDependentDropdown');
goog.require('Blockly.FieldSlider');
goog.require('Blockly.FieldBitmap');
goog.require('Blockly.FieldColourHsvSliders');
goog.require('Blockly.FieldDate');
goog.require('Blockly.FieldAngle');
goog.require('Blockly.FieldColour');
goog.require('Blockly.FieldMultilineInput');
goog.require('Blockly.Screenshot');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.XML');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.ToolboxSearcher');
goog.require('Mixly.Debug');
goog.require('Mixly.EditorBase');
goog.provide('Mixly.EditorBlockly');
const {
Config,
Env,
XML,
HTMLTemplate,
ToolboxSearcher,
Debug,
EditorBase
} = Mixly;
const { USER, BOARD } = Config;
Blockly.ALIGN_LEFT = Blockly.inputs.Align.LEFT;
Blockly.ALIGN_CENTRE = Blockly.inputs.Align.CENTRE;
Blockly.ALIGN_RIGHT = Blockly.inputs.Align.RIGHT;
class EditorBlockly extends EditorBase {
static {
HTMLTemplate.add(
'html/editor/editor-blockly.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-blockly.html')))
);
HTMLTemplate.add(
'xml/default-categories.xml',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'xml/default-categories.xml')))
);
this.$blockly = $('<div class="page-item"></div>');
this.editor = null;
this.workspace = null;
this.initBlockly = () => {
const DEFAULT_CATEGORIES = HTMLTemplate.get('xml/default-categories.xml').render();
const media = path.join(Env.srcDirPath, 'common/media/blockly');
const renderer = ['geras', 'zelos', 'thrasos'].includes(USER.blockRenderer) ? USER.blockRenderer : 'geras';
this.editor = Blockly.inject(this.$blockly[0], {
media,
toolbox: DEFAULT_CATEGORIES,
renderer,
zoom: {
controls: true,
wheel: true,
scaleSpeed: 1.03
},
grid: USER.blocklyShowGrid ==='yes' ? {
spacing: 20,
length: 3,
colour: '#ccc',
snap: true
} : {}
});
const $blocklyScrollbarHandle = this.$blockly.find('.blocklyScrollbarHandle');
$blocklyScrollbarHandle.on('pointerdown', (event) => {
const { currentTarget } = event;
currentTarget.setPointerCapture(event.pointerId);
});
$blocklyScrollbarHandle.on('pointerup', (event) => {
const { currentTarget } = event;
currentTarget.releasePointerCapture(event.pointerId);
});
this.editor.registerToolboxCategoryCallback(
Blockly.Variables.CATEGORY_NAME,
(...args) => Blockly.Variables.flyoutCategory(...args)
);
this.editor.registerToolboxCategoryCallback(
Blockly.Procedures.CATEGORY_NAME,
(...args) => Blockly.Procedures.flyoutCategory(...args)
);
this.editor.setTheme(Blockly.Themes[
USER.theme === 'dark' ? 'Dark' : 'Classic'
]);
this.addPlugins();
this.workspace = new Blockly.Workspace(new Blockly.Options({
toolbox: null
}));
}
this.addPlugins = () => {
const { editor } = this;
Blockly.ContextMenuItems.registerCommentOptions();
editor.configureContextMenu = (menuOptions, e) => {
const workspaceSearchOption = {
text: Blockly.Msg['WORKSPACE_SEARCH_OPEN'],
enabled: editor.getTopBlocks().length,
callback: () => {
this.workspaceSearch.open();
}
};
menuOptions.push(workspaceSearchOption);
const screenshotOption = {
text: Blockly.Msg['DOWNLOAD_SCREENSHOT'],
enabled: editor.getTopBlocks().length,
callback: function() {
Blockly.Screenshot.downloadScreenshot(editor);
},
};
menuOptions.push(screenshotOption);
}
this.toolboxSeacher = new ToolboxSearcher(editor);
this.workspaceSearch = new WorkspaceSearch(editor);
this.workspaceSearch.init();
this.zoomToFit = new ZoomToFitControl(editor);
this.zoomToFit.init();
this.backpack = new Backpack(editor, {
useFilledBackpackImage: true,
skipSerializerRegistration: false,
contextMenu: {
emptyBackpack: true,
removeFromBackpack: true,
copyToBackpack: true,
copyAllToBackpack: true,
pasteAllToBackpack: true,
disablePreconditionChecks: false
}
});
this.backpack.init();
/*if (USER.blocklyMultiselect === 'yes') {
this.multiselectPlugin = new Multiselect(editor);
this.multiselectPlugin.init({
useDoubleClick: false,
bumpNeighbours: false,
multiselectIcon: {
hideIcon: true
},
multiselectCopyPaste: {
crossTab: true,
menu: false
}
});
}
if (USER.blocklyShowMinimap === 'yes') {
this.minimap = new PositionedMinimap(editor);
this.minimap.init();
}*/
if (USER.blocklyContentHighlight === 'yes') {
this.contentHighlight = new ContentHighlight(editor);
this.contentHighlight.init();
}
}
this.getEditor = () => {
return this.editor;
}
this.getContent = () => {
return this.$blockly;
}
this.getXML = (workspace) => {
const $xml = $(Blockly.Xml.workspaceToDom(workspace));
return $xml[0].outerHTML;
}
this.getRawCode = (workspace, generator) => {
return generator?.workspaceToCode(workspace) || '';
}
this.getCode = (workspace, generator) => {
let code = generator?.workspaceToCode(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) {
Debug.error(error);
return s;
}
});
return code;
}
this.updateToolbox = () => {
this.editor.updateToolbox($('#toolbox')[0]);
}
this.reloadWorkspace = () => {
let workspaceState = Blockly.serialization.workspaces.save(this.editor);
let undoStack = [...this.editor.undoStack_];
let redoStack = [...this.editor.redoStack_];
Blockly.serialization.workspaces.load(workspaceState, this.editor, {
recordUndo: false
});
this.editor.undoStack_ = [...undoStack];
this.editor.redoStack_ = [...redoStack];
}
this.initBlockly();
}
#editor_ = null;
#workspaceState_ = null;
#undoStack_ = null;
#redoStack_ = null;
constructor() {
super();
this.setContent(
$(HTMLTemplate.get('html/editor/editor-blockly.html').render())
);
this.#editor_ = EditorBlockly.getEditor();
}
getEditor() {
return this.#editor_;
}
undo() {
super.undo();
this.#editor_.undo(0);
}
redo() {
super.redo();
this.#editor_.undo(1);
}
setVisible(status) {
this.#editor_.setVisible(status);
}
scrollCenter() {
this.#editor_.scrollCenter();
}
resize() {
// 重新调整编辑器尺寸
this.#editor_.hideChaff(false);
this.#editor_.hideComponents(true);
Blockly.common.svgResize(this.#editor_);
Blockly.bumpObjects.bumpTopObjectsIntoBounds(this.#editor_);
super.resize();
}
dispose() {
super.dispose();
if (this.isActive()) {
Blockly.Events.disable();
this.#editor_.clear();
Blockly.Events.enable();
EditorBlockly.getContent().detach();
}
this.#undoStack_ = null;
this.#redoStack_ = null;
this.#editor_ = null;
this.getContent().remove();
}
onMounted() {
super.onMounted();
this.getContent().append(EditorBlockly.getContent());
setTimeout(() => {
Blockly.Events.disable();
if (this.#workspaceState_) {
Blockly.serialization.workspaces.load(this.#workspaceState_, this.#editor_, {
recordUndo: false
});
} else {
this.#editor_.clear();
}
if (this.#undoStack_) {
this.#editor_.undoStack_ = [...this.#undoStack_];
}
if (this.#redoStack_) {
this.#editor_.redoStack_ = [...this.#redoStack_];
}
Blockly.Events.enable();
}, 0);
}
onUnmounted() {
super.onUnmounted();
EditorBlockly.getContent().detach();
this.getContent().empty();
this.#workspaceState_ = Blockly.serialization.workspaces.save(this.#editor_);
this.#undoStack_ = [...this.#editor_.undoStack_];
this.#redoStack_ = [...this.#editor_.redoStack_];
Blockly.Events.disable();
this.#editor_.clearUndo();
Blockly.Events.enable();
}
setValue(data, ext) {}
#getTargetWorkspace_() {
let workspace = this.#editor_;
if (!this.isActive()) {
if (this.isDirty()) {
this.#workspaceState_ = Blockly.serialization.workspaces.save(this.#editor_);
}
Blockly.serialization.workspaces.load(this.#workspaceState_, EditorBlockly.workspace, {
recordUndo: false
});
workspace = EditorBlockly.workspace;
}
return workspace;
}
getXML() {
const workspace = this.#getTargetWorkspace_();
return EditorBlockly.getXML(workspace);
}
getRawCode() {
const workspace = this.#getTargetWorkspace_();
return EditorBlockly.getRawCode(workspace, Blockly.generator);
}
getCode() {
const workspace = this.#getTargetWorkspace_();
return EditorBlockly.getCode(workspace, Blockly.generator);
}
getMix() {
const workspace = this.#getTargetWorkspace_();
return {
block: EditorBlockly.getXML(workspace),
code: EditorBlockly.getCode(workspace, Blockly.generator)
};
}
getValue() {
return this.getCode();
}
}
Mixly.EditorBlockly = EditorBlockly;
});

View File

@@ -0,0 +1,231 @@
goog.loadJs('common', () => {
goog.require('Mixly.Config');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.Menu');
goog.require('Mixly.ContextMenu');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.CodeFormatter');
goog.require('Mixly.MonacoTheme');
goog.require('Mixly.MonacoTreeSitter');
goog.require('Mixly.EditorMonaco');
goog.provide('Mixly.EditorCode');
const {
Config,
XML,
Env,
Msg,
Debug,
Menu,
ContextMenu,
IdGenerator,
CodeFormatter,
MonacoTheme,
MonacoTreeSitter,
EditorMonaco
} = Mixly;
const { USER } = Config;
class EditorCode extends EditorMonaco {
#contextMenu_ = null;
#monacoTreeSitter_ = null;
constructor() {
super();
}
init() {
super.init();
this.setLanguage('text');
this.setTabSize(4);
this.#addContextMenu_();
this.setTheme(USER.theme);
this.#monacoTreeSitter_ = new MonacoTreeSitter(this);
}
onMounted() {
super.onMounted();
this.#addChangeEventListenerExt_();
}
#addContextMenu_() {
this.#contextMenu_ = new ContextMenu(`div[page-id="${this.getId()}"]`, {
zIndex: 300
});
let menu = new Menu();
menu.add({
weight: 0,
id: 'cut',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.cut'], 'Ctrl+X'),
callback: (key, opt) => this.cut()
}
});
menu.add({
weight: 1,
id: 'copy',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], 'Ctrl+C'),
callback: (key, opt) => this.copy()
}
});
menu.add({
weight: 2,
id: 'paste',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.paste'], 'Ctrl+V'),
callback: (key, opt) => this.paste()
}
});
menu.add({
weight: 3,
id: 'sep1',
data: '---------'
});
menu.add({
weight: 4,
id: 'togglecomment',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.togglecomment'], 'Ctrl+/'),
callback: (key, opt) => this.commentLine()
}
});
menu.add({
weight: 5,
id: 'toggleBlockComment',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.toggleBlockComment'], 'Shift+Alt+A'),
callback: (key, opt) => this.blockComment()
}
});
this.#contextMenu_.register('code', menu);
this.#contextMenu_.bind('getMenu', () => 'code');
}
getContextMenu() {
return this.#contextMenu_;
}
setValue(data, ext) {
this.disableChangeEvent();
super.setValue(data);
const language = this.getLanguageByExt(ext);
if (MonacoTheme.supportThemes.includes(language)) {
this.setTheme(`${USER.theme}-${language}`);
} else {
this.setTheme(USER.theme);
}
this.setLanguage(language);
this.enableChangeEvent();
this.setCodeFormatter(language, ext).catch(Debug.error);
}
diffEdits(data, ext) {
this.disableChangeEvent();
super.diffEdits(data);
const language = this.getLanguageByExt(ext);
this.setLanguage(language);
if (MonacoTheme.supportThemes.includes(language)) {
this.setTheme(`${USER.theme}-${language}`);
} else {
this.setTheme(USER.theme);
}
this.#monacoTreeSitter_.setValue(language, USER.theme, data);
this.enableChangeEvent();
this.setCodeFormatter(language, ext).catch(Debug.error);
}
async setCodeFormatter(language, ext) {
const formatter = await CodeFormatter.activateFormatter(language);
const menu = this.#contextMenu_.getItem('code');
if (!formatter) {
menu.remove('sep-format');
menu.remove('format');
return;
}
menu.add({
weight: 6,
id: 'sep-format',
data: '---------'
});
menu.add({
weight: 6,
id: 'format',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.formatDocument'], ''),
callback: () => {
CodeFormatter.format(language, this.getValue())
.then((data) => {
super.setValue(data, ext);
})
.catch(Debug.error);
}
}
});
}
getTreeSitter() {
return this.#monacoTreeSitter_;
}
getLanguageByExt(ext) {
let language = 'plaintext';
switch(ext) {
case '.json':
language = 'json';
break;
case '.c':
case '.cpp':
case '.h':
case '.hpp':
case '.ino':
language = 'cpp';
break;
case '.js':
language = 'javascript';
break;
case '.py':
language = 'python';
break;
case '.lua':
language = 'lua';
break;
case '.md':
case '.mdx':
language = 'markdown';
break;
default:
language = 'plaintext';
}
return language;
}
#addChangeEventListenerExt_() {
this.offEvent('change');
this.bind('change', () => {
this.addDirty();
this.#monacoTreeSitter_.setValue(this.getLanguage(), USER.theme, this.getValue());
});
}
dispose() {
this.#contextMenu_.dispose();
this.#contextMenu_ = null;
super.dispose();
}
}
Mixly.EditorCode = EditorCode;
});

View File

@@ -0,0 +1,170 @@
goog.loadJs('common', () => {
goog.require('marked');
goog.require('markedKatex');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Drag');
goog.require('Mixly.DragV');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.EditorBase');
goog.require('Mixly.EditorCode');
goog.provide('Mixly.EditorMd');
const {
XML,
Env,
Drag,
DragV,
IdGenerator,
HTMLTemplate,
EditorBase,
EditorCode
} = Mixly;
class EditorMd extends EditorBase {
static {
HTMLTemplate.add(
'html/editor/editor-md.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-md.html')))
);
HTMLTemplate.add(
'html/editor/editor-md-btns.html',
goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-md-btns.html'))
);
marked.use(markedKatex({ throwOnError: false }));
}
#prevCode_ = '';
#listener_ = null;
#drag_ = null;
#$preview_ = null;
#$btns_ = null;
constructor() {
super();
const $content = $(HTMLTemplate.get('html/editor/editor-md.html').render());
const $btnsContent = $(HTMLTemplate.get('html/editor/editor-md-btns.html'));
this.#$preview_ = $content.find('.markdown-body');
this.addPage($content.find('.editor-code'), 'code', new EditorCode());
this.#$btns_ = $btnsContent.find('button');
this.#drag_ = null;
this.setContent($content);
this.setBtnsContent($btnsContent);
}
init() {
super.init();
this.#addDragEventsListener_();
this.#addBtnEventsListener_();
}
onMounted() {
super.onMounted();
this.#addChangeEventListener_();
}
#addDragEventsListener_() {
this.#drag_ = new DragV(this.getContent()[0], {
min: '200px',
full: [true, true],
startSize: '0%',
startExitFullSize: '70%'
});
this.#drag_.bind('sizeChanged', () => this.resize());
this.#drag_.bind('onfull', (type) => {
this.#$btns_.removeClass('self-adaption-btn');
let $btn = null;
switch(type) {
case Drag.Extend.POSITIVE:
$btn = this.#$btns_.filter('[m-id="code"]');
break;
case Drag.Extend.NEGATIVE:
$btn = this.#$btns_.filter('[m-id="preview"]');
break;
}
$btn.addClass('self-adaption-btn');
});
this.#drag_.bind('exitfull', (type) => {
this.#$btns_.removeClass('self-adaption-btn');
const $btn = this.#$btns_.filter('[m-id="mixture"]');
$btn.addClass('self-adaption-btn');
if (type === Drag.Extend.POSITIVE) {
this.updatePreview();
}
});
}
#addBtnEventsListener_() {
this.#$btns_.on('click', (event) => {
const $btn = $(event.currentTarget);
const mId = $btn.attr('m-id');
if (!$btn.hasClass('self-adaption-btn')) {
this.#$btns_.removeClass('self-adaption-btn');
$btn.addClass('self-adaption-btn');
}
switch (mId) {
case 'code':
this.#drag_.full(Drag.Extend.POSITIVE);
break;
case 'mixture':
this.#drag_.exitfull(Drag.Extend.POSITIVE);
this.#drag_.exitfull(Drag.Extend.NEGATIVE);
break;
case 'preview':
this.#drag_.full(Drag.Extend.NEGATIVE);
break;
}
})
}
#addChangeEventListener_() {
const codePage = this.getPage('code');
codePage.offEvent('change');
codePage.bind('change', () => {
this.addDirty();
if (this.#drag_.shown === 'NEGATIVE') {
return;
}
this.updatePreview();
});
}
updatePreview() {
const code = this.getPage('code').getValue();
if (code === this.#prevCode_) {
return;
}
this.#prevCode_ = code;
const $dom = $(marked.parse(code));
const $as = $dom.find('a');
$as.attr('target', '_blank');
this.#$preview_.html($dom);
}
setValue(data, ext) {
const codePage = this.getPage('code');
codePage.disableChangeEvent();
codePage.setValue(data, ext);
this.updatePreview();
codePage.enableChangeEvent();
}
getValue() {
const codePage = this.getPage('code');
return codePage.getValue();
}
dispose() {
this.#drag_.dispose();
super.dispose();
}
}
Mixly.EditorMd = EditorMd;
});

View File

@@ -0,0 +1,644 @@
goog.loadJs('common', () => {
goog.require('layui');
goog.require('tippy');
goog.require('Base64');
goog.require('Blockly');
goog.require('Mixly.Drag');
goog.require('Mixly.DragV');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.Debug');
goog.require('Mixly.Menu');
goog.require('Mixly.Boards');
goog.require('Mixly.MJson');
goog.require('Mixly.LayerExt');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.EditorBlockly');
goog.require('Mixly.EditorCode');
goog.require('Mixly.EditorBase');
goog.provide('Mixly.EditorMix');
const { dropdown } = layui;
const {
EditorBlockly,
EditorCode,
EditorBase,
Drag,
DragV,
XML,
Msg,
Config,
Env,
Debug,
Menu,
Boards,
MJson,
HTMLTemplate,
LayerExt
} = Mixly;
const { BOARD, SOFTWARE, USER } = Config;
const { form } = layui;
class EditorMix extends EditorBase {
static {
HTMLTemplate.add(
'html/editor/editor-mix.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-mix.html')))
);
HTMLTemplate.add(
'html/editor/editor-mix-btns.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-mix-btns.html')))
);
}
#language_ = null;
#tabSize_ = null;
#$btns_ = null;
#temp_ = '';
constructor() {
super();
const $content = $(HTMLTemplate.get('html/editor/editor-mix.html').render());
const $btnsContent = $(HTMLTemplate.get('html/editor/editor-mix-btns.html').render({
block: Msg.Lang['editor.block'],
mix: Msg.Lang['editor.mix'],
code: Msg.Lang['editor.code']
}));
this.drag = null;
this.#$btns_ = $btnsContent.find('button');
this.setContent($content);
this.setBtnsContent($btnsContent);
this.addPage($content.find('.editor-blockly'), 'block', new EditorBlockly());
this.addPage($content.find('.editor-code'), 'code', new EditorCode());
}
init() {
super.init();
this.#addDragEventsListener_();
this.#addBtnEventsListener_();
this.#language_ = this.getDefaultLanguageExt();
this.#tabSize_ = this.getDefaultTabSize();
const codePage = this.getPage('code');
codePage.setLanguage(codePage.getLanguageByExt(this.#language_));
codePage.setTabSize(this.#tabSize_);
codePage.setReadOnly(true);
this.#py2BlockEditorInit_();
const contextMenu = codePage.getContextMenu();
let codeMenu = contextMenu.getItem('code');
codeMenu.add({
weight: 7,
id: 'sep2',
data: '---------'
});
codeMenu.add({
weight: 8,
id: 'block',
data: {
isHtmlName: false,
name: Msg.Lang['editor.contextMenu.exitCodeEditor'],
callback: (key, opt) => this.drag.exitfull(Drag.Extend.NEGATIVE)
}
});
let blockMenu = new Menu();
blockMenu.add({
weight: 0,
id: 'copy',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], 'Ctrl+C'),
callback: (key, opt) => codePage.copy()
}
});
blockMenu.add({
weight: 1,
id: 'sep1',
data: '---------'
});
blockMenu.add({
weight: 2,
id: 'code',
data: {
isHtmlName: false,
name: Msg.Lang['editor.contextMenu.enterCodeEditor'],
callback: (key, opt) => this.drag.full(Drag.Extend.NEGATIVE)
}
});
contextMenu.register('block', blockMenu);
contextMenu.offEvent('getMenu');
contextMenu.bind('getMenu', () => {
return this.getPageType();
});
}
#getCodeExtname_() {
let extname = '.c';
const language = BOARD.language.toLowerCase();
switch (language) {
case 'python':
case 'circuitpython':
case 'micropython':
extname = '.py';
break;
case 'lua':
extname = '.lua';
break;
case 'javascript':
extname = '.js';
break;
case 'c/c++':
default:
extname = '.c';
}
return extname;
}
#py2BlockEditorInit_() {
const codePage = this.getPage('code');
if (typeof Sk === 'object'
&& typeof PythonToBlocks === 'function'
&& typeof Py2blockEditor === 'function') {
const py2blockConverter = new PythonToBlocks();
this.py2BlockEditor = new Py2blockEditor(py2blockConverter, codePage.getEditor());
}
}
#addCodeChangeEventListener_() {
const codePage = this.getPage('code');
codePage.offEvent('change');
codePage.bind('change', () => {
this.addDirty();
codePage.getTreeSitter().setValue(codePage.getLanguage(), USER.theme, codePage.getValue());
});
}
#addDragEventsListener_() {
const blockPage = this.getPage('block');
const codePage = this.getPage('code');
this.drag = new DragV(this.getContent()[0], {
min: '200px',
full: [true, true],
startSize: '100%',
startExitFullSize: '70%'
});
this.drag.bind('sizeChanged', () => this.resize());
this.drag.bind('onfull', (type) => {
this.#$btns_.removeClass('self-adaption-btn');
let $btn = null;
switch(type) {
case Drag.Extend.POSITIVE:
$btn = this.#$btns_.filter('[m-id="block"]');
blockPage.scrollCenter();
break;
case Drag.Extend.NEGATIVE:
$btn = this.#$btns_.filter('[m-id="code"]');
codePage.setReadOnly(false);
codePage.focus();
if (this.py2BlockEditor && BOARD.pythonToBlockly) {
this.py2BlockEditor.fromCode = true;
}
break;
}
$btn.addClass('self-adaption-btn');
});
this.drag.bind('exitfull', (type) => {
this.#$btns_.removeClass('self-adaption-btn');
const $btn = this.#$btns_.filter('[m-id="mixture"]');
$btn.addClass('self-adaption-btn');
switch(type) {
case Drag.Extend.NEGATIVE:
codePage.setReadOnly(true);
if (this.py2BlockEditor
&& BOARD.pythonToBlockly
&& typeof this.py2BlockEditor.updateBlock === 'function') {
this.py2BlockEditor.updateBlock();
} else {
codePage.diffEdits(blockPage.getValue(), this.#language_);
}
break;
case Drag.Extend.POSITIVE:
this.#temp_ = blockPage.getValue();
codePage.diffEdits(this.#temp_, this.#language_);
break;
}
blockPage.resize();
blockPage.scrollCenter();
});
}
#addBtnEventsListener_() {
this.#$btns_.on('click', (event) => {
const $btn = $(event.currentTarget);
const mId = $btn.attr('m-id');
if (mId === 'deps') {
return;
}
if (!$btn.hasClass('self-adaption-btn')) {
this.#$btns_.removeClass('self-adaption-btn');
$btn.addClass('self-adaption-btn');
}
switch (mId) {
case 'block':
this.drag.full(Drag.Extend.POSITIVE);
break;
case 'mixture':
this.drag.exitfull(Drag.Extend.POSITIVE);
this.drag.exitfull(Drag.Extend.NEGATIVE);
break;
case 'code':
this.drag.full(Drag.Extend.NEGATIVE);
break;
}
})
}
getCurrentEditor() {
const blockPage = this.getPage('block');
const codePage = this.getPage('code');
if (this.getPageType() === 'code') {
return codePage;
} else {
return blockPage;
}
}
getDefaultLanguageExt() {
let ext = '.txt';
let type = (BOARD.language || '').toLowerCase();
switch(type) {
case 'python':
case 'micropython':
case 'circuitpython':
ext = '.py';
break;
case 'c/c++':
ext = '.cpp';
break;
case 'javascript':
ext = '.js';
break;
case 'markdown':
ext = '.md';
break;
case 'lua':
ext = '.lua';
break;
default:
ext = '.txt';
}
return ext;
}
getDefaultTabSize() {
let tabSize = 4;
let type = (BOARD.language || '').toLowerCase();
switch(type) {
case 'c/c++':
case 'markdown':
tabSize = 2;
break;
case 'python':
case 'micropython':
case 'circuitpython':
case 'javascript':
default:
tabSize = 4;
}
return tabSize;
}
getPageType() {
if (this.drag.shown !== Drag.Extend.NEGATIVE) {
return 'block';
} else {
return 'code';
}
}
undo() {
super.undo();
const editor = this.getCurrentEditor();
editor.undo();
}
redo() {
super.redo();
const editor = this.getCurrentEditor();
editor.redo();
}
dispose() {
const blockPage = this.getPage('block');
const blocklyWorkspace = blockPage.getEditor();
blocklyWorkspace.removeChangeListener(this.codeChangeListener);
this.drag.dispose();
super.dispose();
this.drag = null;
}
onMounted() {
super.onMounted();
const blockPage = this.getPage('block');
const blocklyWorkspace = blockPage.getEditor();
this.codeChangeListener = blocklyWorkspace.addChangeListener((event) => {
const blockPage = this.getPage('block');
const codePage = this.getPage('code');
if (
event.isUiEvent ||
event.type == Blockly.Events.FINISHED_LOADING ||
blocklyWorkspace.isDragging()
) {
return;
}
this.addDirty();
if (this.drag.shown !== Drag.Extend.BOTH) {
return;
}
const code = blockPage.getCode();
if (this.#temp_ === code) {
return;
}
this.#temp_ = code;
codePage.diffEdits(code, this.#language_);
});
this.#addCodeChangeEventListener_();
}
onUnmounted() {
super.onUnmounted();
const blockPage = this.getPage('block');
const blocklyWorkspace = blockPage.getEditor();
blocklyWorkspace.removeChangeListener(this.codeChangeListener);
}
setValue(data, ext) {
const blockPage = this.getPage('block');
const codePage = this.getPage('code');
switch (ext) {
case '.mix':
case '.xml':
Blockly.Events.disable();
try {
data = XML.convert(data, true);
data = data.replace(/\\(u[0-9a-fA-F]{4})/g, function (s) {
return unescape(s.replace(/\\(u[0-9a-fA-F]{4})/g, '%$1'));
});
} catch (error) {
Debug.error(error);
}
this.parseMix($(data), false, false, (message) => {
if (message) {
switch (message) {
case 'USE_CODE':
Debug.log('已从code标签中读取代码');
break;
case 'USE_INCOMPLETE_BLOCKS':
Debug.log('一些块已被忽略');
break;
}
blockPage.scrollCenter();
Blockly.hideChaff();
}
});
Blockly.Events.enable();
break;
default:
this.drag.full(Drag.Extend.NEGATIVE);
this.getPage('code').setValue(data, ext);
break;
}
}
getValue() {
const blockPage = this.getPage('block');
const codePage = this.getPage('code');
const mix = blockPage.getMix();
const $xml = $(mix.block);
const config = Boards.getSelectedBoardConfig();
const boardName = Boards.getSelectedBoardName();
const board = BOARD?.boardType ?? 'default';
let xml = '';
let code = '';
$xml.removeAttr('xmlns')
.attr('version', SOFTWARE?.version ?? 'Mixly 2.0')
.attr('board', board + '@' + boardName);
if (this.drag.shown !== Drag.Extend.NEGATIVE) {
$xml.attr('shown', 'block');
code = mix.code;
} else {
$xml.attr('shown', 'code');
code = codePage.getValue();
}
xml = $xml[0].outerHTML;
if (config) {
xml += `<config>${MJson.stringify(config)}</config>`;
}
xml += `<code>${Base64.encode(code)}</code>`;
return xml;
}
getCode() {
const blockPage = this.getPage('block');
const codePage = this.getPage('code');
if (this.drag.shown !== Drag.Extend.NEGATIVE) {
return blockPage.getRawCode();
} else {
return codePage.getCode();
}
}
getMil() {
const $mix = $(this.getValue());
let $xml, $config, $code;
for (let i = 0; $mix[i]; i++) {
switch ($mix[i].nodeName) {
case 'XML':
$xml = $($mix[i]);
break;
case 'CONFIG':
$config = $($mix[i]);
break;
case 'CODE':
$code = $($mix[i]);
break;
}
}
if (!$xml) return '';
$config && $config.remove();
$code && $code.remove();
$xml.attr('type', 'lib');
$xml.find('block,shadow').removeAttr('id varid x y');
const $blocks = $xml.children('block');
let blockXmlList = [];
for (let i = 0; $blocks[i]; i++) {
const outerHTML = $blocks[i].outerHTML;
if (!blockXmlList.includes(outerHTML)) {
blockXmlList.push(outerHTML);
} else {
$blocks[i].remove();
}
}
return $xml[0].outerHTML;
}
parseMix(xml, useCode = false, useIncompleteBlocks = false, endFunc = (message) => {}) {
const blockPage = this.getPage('block');
const codePage = this.getPage('code');
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) {
Debug.error(error);
}
$(nowDom[j]).attr(i, attr);
}
}
}
let config, configStr = configDom && configDom.html();
config = configStr? MJson.parse(configStr) : {};
let boardName = xmlDom.attr('board') ?? '';
blockPage.getEditor().clear();
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) {
Debug.error(error);
}
}
if (useCode) {
if (!codeDom) {
layer.msg(Msg.Lang['editor.invalidData'], { time: 1000 });
return;
}
this.drag.full(Drag.Extend.NEGATIVE); // 完全显示代码编辑器
codePage.diffEdits(code, this.#language_);
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 = Blockly.generator;
for (let i of blocks) {
if (Blockly.Blocks[i] && blocklyGenerator.forBlock[i]) {
continue;
}
undefinedBlocks.push(i);
}
if (undefinedBlocks.length) {
this.showParseMixErrorDialog(mixDom, undefinedBlocks, endFunc);
return;
}
Blockly.Xml.clearWorkspaceAndLoadFromXml(xmlDom[0], blockPage.getEditor());
blockPage.getEditor().scrollCenter();
Blockly.hideChaff();
if (!useIncompleteBlocks && codeDom && xmlDom.attr('shown') === 'code') {
this.drag.full(Drag.Extend.NEGATIVE); // 完全显示代码编辑器
codePage.diffEdits(code, this.#language_);
endFunc();
return;
}
this.drag.full(Drag.Extend.POSITIVE); // 完全显示块编辑器
if (useIncompleteBlocks)
endFunc('USE_INCOMPLETE_BLOCKS');
else
endFunc();
}
showParseMixErrorDialog(xml, undefinedBlocks, endFunc = () => {}) {
const { PARSE_MIX_ERROR_DIV } = XML.TEMPLATE_STR;
const renderStr = XML.render(PARSE_MIX_ERROR_DIV, {
text: undefinedBlocks.join('<br/>'),
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();
}
this.parseMix(xml, false, true, endFunc);
break;
case '2':
this.parseMix(xml, true, false, endFunc);
break;
}
});
}
});
}
}
Mixly.EditorMix = EditorMix;
});

View File

@@ -0,0 +1,396 @@
goog.loadJs('common', () => {
goog.require('Diff');
goog.require('monaco');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.Events');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.EditorBase');
goog.provide('Mixly.EditorMonaco');
const {
XML,
Env,
Msg,
Debug,
Events,
HTMLTemplate,
EditorBase
} = Mixly;
class EditorMonaco extends EditorBase {
static {
HTMLTemplate.add(
'html/editor/editor-code.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-code.html')))
);
this.$monaco = $('<div class="page-item"></div>');
this.editor = null;
this.events = new Events(['change']);
this.addEventsListener = () => {
const { editor } = this;
$('#mixly-footer-cursor').hide();
editor.onDidBlurEditorText(() => {
$('#mixly-footer-cursor').hide();
});
editor.onDidFocusEditorText(() => {
const position = editor.getPosition();
$('#mixly-footer-row').html(position.lineNumber);
$('#mixly-footer-column').html(position.column);
const selection = editor.getSelection();
if (selection.isEmpty()) {
$('#mixly-footer-selected').parent().hide();
} else {
const text = editor.getModel().getValueInRange(selection);
$('#mixly-footer-selected').parent().css('display', 'inline-flex');
$('#mixly-footer-selected').html(text.length);
}
$('#mixly-footer-cursor').show();
});
editor.onDidChangeCursorPosition((e) => {
$('#mixly-footer-row').html(e.position.lineNumber);
$('#mixly-footer-column').html(e.position.column);
});
editor.onDidChangeCursorSelection((e) => {
if (e.selection.isEmpty()) {
$('#mixly-footer-selected').parent().hide();
} else {
const text = editor.getModel().getValueInRange(e.selection);
$('#mixly-footer-selected').parent().css('display', 'inline-flex');
$('#mixly-footer-selected').html(text.length);
}
});
editor.onDidChangeModelContent((...args) => {
this.events.run('change', ...args);
});
}
this.getEditor = () => {
return this.editor;
}
this.getContent = () => {
return this.$monaco;
}
this.initMonaco = () => {
this.editor = monaco.editor.create(this.$monaco[0], {
theme: 'vs-dark',
disableLayerHinting: true, // 等宽优化
emptySelectionClipboard: false, // 空选择剪切板
selectionClipboard: false, // 选择剪切板
codeLens: true, // 代码镜头
scrollBeyondLastLine: true, // 滚动完最后一行后再滚动一屏幕
colorDecorators: true, // 颜色装饰器
accessibilitySupport: 'off', // 辅助功能支持 "auto" | "off" | "on"
lineNumbers: 'on', // 行号 取值: "on" | "off" | "relative" | "interval" | function
lineNumbersMinChars: 5, // 行号最小字符 number
enableSplitViewResizing: false,
contextmenu: false,
fontSize: 17,
automaticLayout: false,
wordWrap: 'wordWrapColumn',
wordWrapColumn: 300,
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10
}
});
this.addEventsListener();
}
this.initMonaco();
}
#readOnly_ = false;
#changeListener_ = null;
#enableChangeEvent_ = true;
#editor_ = null;
#state_ = null;
#tabSize_ = null;
#language_ = null;
#mode_ = null;
constructor() {
super();
const editorHTMLTemplate = HTMLTemplate.get('html/editor/editor-code.html');
this.setContent($(editorHTMLTemplate.render()));
this.addEventsType(['change']);
}
init() {
super.init();
this.#editor_ = monaco.editor.createModel('');
this.#editor_.setEOL(monaco.editor.EndOfLineSequence.LF);
}
onMounted() {
super.onMounted();
const editor = EditorMonaco.getEditor();
editor.setModel(this.#editor_);
if (this.#state_) {
editor.restoreViewState(this.#state_);
}
this.setReadOnly(this.#readOnly_);
this.getContent().append(EditorMonaco.getContent());
if (!this.#readOnly_) {
this.focus();
}
this.#addChangeEventListener_();
}
disableChangeEvent() {
this.#enableChangeEvent_ = false;
}
enableChangeEvent() {
this.#enableChangeEvent_ = true;
}
#addChangeEventListener_() {
const $content = EditorMonaco.getContent();
this.#changeListener_ = EditorMonaco.events.bind('change', () => {
this.#enableChangeEvent_ && this.runEvent('change');
});
}
#removeChangeEventListener_() {
this.#changeListener_ && EditorMonaco.events.unbind(this.#changeListener_);
this.offEvent('change');
}
onUnmounted() {
super.onUnmounted();
const editor = EditorMonaco.getEditor();
const $content = EditorMonaco.getContent();
this.#state_ = editor.saveViewState();
$content.detach();
this.getContent().empty();
this.#removeChangeEventListener_();
}
dispose() {
this.#removeChangeEventListener_();
this.#editor_.dispose();
super.dispose();
this.#editor_ = null;
}
setTheme(mode) {
if (this.#mode_ === mode) {
return;
}
const editor = EditorMonaco.getEditor();
editor.updateOptions({
theme: `vs-${mode}`
});
this.#mode_ = mode;
}
getTheme() {
return this.#mode_;
}
setValue(data) {
if (this.getValue() === data) {
return;
}
this.#editor_.setValue(data);
}
addValue(data) {
const prevData = this.getValue();
this.setValue(prevData + data);
}
getValue() {
return this.#editor_.getValue();
}
diffEdits(data) {
const prevData = this.getValue();
if (prevData === data) {
return;
}
const edits = this.buildDiffEdits(prevData, data);
if (!edits.length) {
return;
}
this.#editor_.pushEditOperations([], edits, () => null);
}
buildDiffEdits(prevData, nextData) {
if (prevData === nextData) {
return [];
}
const diffs = Diff.diffChars(prevData, nextData);
const edits = [];
const state = {
index: 0,
pendingStart: null,
pendingText: ''
};
for (const d of diffs) {
if (d.added) {
if (state.pendingStart == null) {
state.pendingStart = state.index;
}
state.pendingText += d.value;
}
else if (d.removed) {
if (state.pendingStart == null) {
state.pendingStart = state.index;
}
state.index += d.value.length;
}
else {
this.flushPendingEdit(state, edits);
state.index += d.value.length;
}
}
this.flushPendingEdit(state, edits);
return edits;
}
flushPendingEdit(state, edits) {
const { pendingStart, index, pendingText } = state;
if (pendingStart == null) {
return;
}
const startPos = this.#editor_.getPositionAt(pendingStart);
const endPos = this.#editor_.getPositionAt(index);
edits.push({
range: new monaco.Range(
startPos.lineNumber,
startPos.column,
endPos.lineNumber,
endPos.column
),
text: pendingText,
forceMoveMarkers: true
});
state.pendingStart = null;
state.pendingText = '';
}
clear() {
this.setValue('', true);
}
scrollToBottom() {
const editor = EditorMonaco.getEditor();
editor.setScrollTop(editor.getScrollHeight());
}
scrollToTop() {
const editor = EditorMonaco.getEditor();
editor.setScrollTop(0);
}
resize() {
super.resize();
const editor = EditorMonaco.getEditor();
editor.layout(null, true);
}
undo() {
this.#editor_.undo();
}
redo() {
this.#editor_.redo();
}
cut() {
const editor = EditorMonaco.getEditor();
let selection = editor.getSelection();
let selectedText = this.#editor_.getValueInRange(selection);
if (selection) {
editor.executeEdits('cut', [{ range: selection, text: '' }]);
navigator.clipboard.writeText(selectedText);
}
this.focus();
}
copy() {
const editor = EditorMonaco.getEditor();
editor.trigger('source', 'editor.action.clipboardCopyWithSyntaxHighlightingAction');
}
paste() {
const editor = EditorMonaco.getEditor();
navigator.clipboard.readText()
.then((clipboardText) => {
editor.trigger('source', 'type', { text: clipboardText });
this.focus();
})
.catch(Debug.error);
}
getEditor() {
return this.#editor_;
}
setReadOnly(readOnly) {
const editor = EditorMonaco.getEditor();
editor.updateOptions({ readOnly });
this.#readOnly_ = readOnly;
}
setLanguage(language) {
if (this.#language_ === language) {
return;
}
this.#language_ = language;
monaco.editor.setModelLanguage(this.#editor_, language);
}
getLanguage() {
return this.#language_;
}
setTabSize(tabSize) {
if (this.#tabSize_ === tabSize) {
return;
}
this.#tabSize_ = tabSize;
this.#editor_.updateOptions({ tabSize });
}
setFontSize(fontSize) {
const editor = EditorMonaco.getEditor();
editor.updateOptions({ fontSize });
}
focus() {
const editor = EditorMonaco.getEditor();
editor.focus();
}
commentLine() {
const editor = EditorMonaco.getEditor();
EditorMonaco.getEditor().trigger('source', 'editor.action.commentLine');
}
blockComment() {
const editor = EditorMonaco.getEditor();
editor.trigger('source', 'editor.action.blockComment');
}
}
Mixly.EditorMonaco = EditorMonaco;
});

View File

@@ -0,0 +1,35 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.EditorBase');
goog.provide('Mixly.EditorUnknown');
const {
XML,
Env,
HTMLTemplate,
EditorBase
} = Mixly;
class EditorUnknown extends EditorBase {
static {
HTMLTemplate.add(
'html/editor/editor-unknown.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-unknown.html')))
);
}
constructor() {
super();
this.setContent(
$(HTMLTemplate.get('html/editor/editor-unknown.html').render())
);
}
}
Mixly.EditorUnknown = EditorUnknown;
});

View File

@@ -0,0 +1,34 @@
goog.loadJs('common', () => {
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.EditorBase');
goog.provide('Mixly.EditorWelcome');
const {
XML,
Env,
HTMLTemplate,
EditorBase
} = Mixly;
class EditorWelcome extends EditorBase {
static {
HTMLTemplate.add(
'html/editor/editor-welcome.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-welcome.html')))
);
}
constructor() {
super();
this.setContent(
$(HTMLTemplate.get('html/editor/editor-welcome.html').render())
);
}
}
Mixly.EditorWelcome = EditorWelcome;
});

View File

@@ -0,0 +1,144 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Nav');
goog.require('Mixly.EditorMix');
goog.require('Mixly.EditorCode');
goog.require('Mixly.EditorMd');
goog.require('Mixly.EditorBlockly');
goog.require('Mixly.EditorUnknown');
goog.require('Mixly.EditorWelcome');
goog.require('Mixly.Registry');
goog.require('Mixly.Debug');
goog.require('Mixly.PagesManager');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Electron.FS');
goog.require('Mixly.Web.FS');
goog.provide('Mixly.EditorsManager');
const {
XML,
Env,
Nav,
EditorMix,
EditorCode,
EditorMd,
EditorBlockly,
EditorUnknown,
EditorWelcome,
Registry,
Debug,
PagesManager,
HTMLTemplate,
Electron = {},
Web = {}
} = Mixly;
const { FS } = goog.isElectron? Electron : Web;
class EditorsManager extends PagesManager {
static {
HTMLTemplate.add(
'html/editor/editor-manager.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-manager.html')))
);
HTMLTemplate.add(
'html/editor/editor-tab.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-tab.html')))
);
this.typesRegistry = new Registry();
this.typesRegistry.register(['.mix', '.mil'], EditorMix);
this.typesRegistry.register(['.txt', '.ino', '.json'], EditorCode);
this.typesRegistry.register('.md', EditorMd);
this.typesRegistry.register('#default', EditorUnknown);
this.typesRegistry.register('#welcome', EditorWelcome);
}
constructor(element) {
const managerHTMLTemplate = HTMLTemplate.get('html/editor/editor-manager.html');
const tabHTMLTemplate = HTMLTemplate.get('html/editor/editor-tab.html');
const $manager = $(managerHTMLTemplate.render());
const $tab = $(tabHTMLTemplate.render());
super({
parentElem: element,
managerContentElem: $manager[0],
bodyElem: $manager.find('.body')[0],
tabElem: $manager.find('.tabs')[0],
tabContentElem: $tab[0],
typesRegistry: EditorsManager.typesRegistry
});
this.#addEventsListenerExt_();
}
#addEventsListenerExt_() {
const editorTabs = this.getTabs();
// active Tab被改变时触发
editorTabs.bind('activeTabChange', (event) => {
const $btnsContainer = Nav.getMain().getEditorBtnsContainer();
$btnsContainer.children('div').detach();
const { tabEl } = event.detail;
const id = $(tabEl).attr('data-tab-id');
const editor = this.get(id);
const $btns = editor.getBtnsContent();
if ($btns && $btns.length) {
$btnsContainer.removeClass('empty');
$btnsContainer.children('a').css('display', 'none');
$btnsContainer.append($btns);
} else {
$btnsContainer.addClass('empty');
$btnsContainer.children('a').css('display', 'block');
}
Nav.getMain().resize();
});
// 添加新Tab时触发
editorTabs.bind('tabCreated', (event) => {
const { tabEl } = event.detail;
const id = $(tabEl).attr('data-tab-id');
const editor = this.get(id);
FS.isFile(id)
.then((isFile) => {
if (!isFile) {
return;
}
return FS.readFile(id);
})
.then((data) => {
data && editor.setValue(data, path.extname(id));
})
.catch(Debug.error);
const tabs = this.getTabs();
editor.bind('addDirty', () => {
tabs.updateTab(id, {
title: id + ' - 未保存'
});
});
editor.bind('removeDirty', () => {
tabs.updateTab(id, {
title: id
});
});
});
// 移除已有Tab时触发
editorTabs.bind('tabDestroyed', (event) => {
if (this.length()) {
return;
}
const $btnsContainer = Nav.getMain().getEditorBtnsContainer();
$btnsContainer.children('div').detach();
$btnsContainer.children('a').css('display', 'block');
$btnsContainer.addClass('empty');
Nav.getMain().resize();
});
}
}
Mixly.EditorsManager = EditorsManager;
});

View File

@@ -0,0 +1,141 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.Url');
goog.provide('Mixly.Env');
const fs_plus = Mixly.require('fs-plus');
const electron_remote = Mixly.require('@electron/remote');
const timers = Mixly.require('node:timers');
const { Env, Url } = Mixly;
/**
* 检测是否启用node服务器
* @type {Boolean}
*/
Env.hasSocketServer = false;
/**
* 检测是否启用node编译服务器
* @type {Boolean}
*/
Env.hasCompiler = false;
/**
* 获取当前mixly的路径
* @type {String}
*/
Env.clientPath = null;
/**
* 检测当前系统
* @type {String} win32、darwin、linux
*/
Env.currentPlatform = goog.platform();
/**
* 对于win系统获取免安装python3的路径对于mac与linux则此变量值为python3
* @type {String}
*/
Env.python3Path = null;
/**
* 获取mixly.py的路径
* @type {String}
*/
Env.pyFilePath = null;
let htmlPath = decodeURIComponent((new URL($('html')[0].baseURI)).pathname);
let baseJsPath = decodeURIComponent((new URL(goog.basePath)).pathname);
if (goog.isElectron && Env.currentPlatform === 'win32') {
htmlPath = htmlPath.substring(1);
baseJsPath = baseJsPath.substring(1);
}
/**
* 获取板卡index或主页面index的路径
* @type {String}
*/
Env.indexDirPath = path.join(htmlPath, '..');
/**
* 资源文件夹所在路径
* @type {String}
*/
Env.srcDirPath = path.join(Env.indexDirPath, '..');
/**
* 获取板卡index或主页面index的缩放比例
* @type {String}
*/
Env.winSize = null;
/**
* 获取板卡index默认xml
* @type {String}
*/
Env.defaultXML = null;
/**
* 获取第三方库所用css
* @type {Array}
*/
Env.thirdPartyCSS = [];
/**
* 获取第三方库所用js
* @type {Array}
*/
Env.thirdPartyJS = [];
/**
* 默认模板路径
* @type {String}
*/
Env.templatePath = path.join(baseJsPath, '../templates');
/**
* 语言文件路径
* @type {String}
*/
Env.msgPath = path.join(baseJsPath, '../msg');
/**
* 模板index所在路径
* @type {String}
*/
const urlConfig = Url.getConfig() ?? {};
Env.boardIndexPath = path.join(Env.indexDirPath, '../', urlConfig.boardIndex ?? 'index.xml');
/**
* 模板index所在目录路径
* @type {String}
*/
Env.boardDirPath = path.join(Env.boardIndexPath, '..');
if (goog.isElectron) {
window.setInterval = timers.setInterval;
window.clearInterval = timers.clearInterval;
window.setTimeout = timers.setTimeout;
window.clearTimeout = timers.clearTimeout;
window.setImmediate = timers.setImmediate;
window.clearImmediate = timers.clearImmediate;
const { app } = electron_remote;
if (Env.currentPlatform === 'darwin') {
Env.clientPath = path.join(app.getPath('exe'), '../../../../');
} else {
Env.clientPath = path.join(app.getPath('exe'), '../');
}
Env.pyFilePath = path.join(Env.clientPath, 'mixpyBuild/mixly.py');
if (Env.currentPlatform === 'win32') {
Env.python3Path = path.join(Env.clientPath, 'mixpyBuild/win_python3/python3.exe');
} else {
Env.python3Path = '/usr/bin/python3';
if (fs_plus.isFileSync('/usr/local/bin/python3')) {
Env.python3Path = '/usr/local/bin/python3';
}
}
}
});

View File

@@ -0,0 +1,98 @@
goog.loadJs('common', () => {
goog.require('Mixly.IdGenerator');
goog.require('Mixly.MArray');
goog.require('Mixly.Registry');
goog.require('Mixly.Debug');
goog.provide('Mixly.Events');
const {
IdGenerator,
MArray,
Registry,
Debug
} = Mixly;
class Events {
#events_ = new Registry();
#eventsType_ = null;
constructor(eventsType = []) {
this.#eventsType_ = eventsType;
}
addType(eventsType) {
this.#eventsType_ = MArray.unique([...this.#eventsType_, ...eventsType]);
}
exist(type) {
if (!this.#eventsType_.includes(type)) {
Debug.warn(`${type} event does not exist under the class`);
return false;
}
return true;
}
bind(type, func) {
if (!this.exist(type)) {
return this;
}
const id = IdGenerator.generate();
let typeEvent = this.#events_.getItem(type);
if (!typeEvent) {
typeEvent = new Registry();
this.#events_.register(type, typeEvent);
}
typeEvent.register(id, func);
return id;
}
unbind(id) {
for (let [_, value] of this.#events_.getAllItems()) {
let typeEvent = value;
if (!typeEvent.getItem(id)) {
continue;
}
typeEvent.unregister(id);
}
return this;
}
off(type) {
if (this.#events_.getItem(type)) {
this.#events_.unregister(type);
}
return this;
}
run(type, ...args) {
let outputs = [];
if (!this.exist(type)) {
return outputs;
}
const eventsFunc = this.#events_.getItem(type);
if (!eventsFunc) {
return outputs;
}
for (let [_, func] of eventsFunc.getAllItems()) {
outputs.push(func(...args));
}
return outputs;
}
reset() {
this.#events_.reset();
}
length(type) {
const typeEvent = this.#events_.getItem(type);
if (typeEvent) {
return typeEvent.length();
}
return 0;
}
}
Mixly.Events = Events;
});

View File

@@ -0,0 +1,998 @@
goog.loadJs('common', () => {
goog.require('XScrollbar');
goog.require('path');
goog.require('$.jstree');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Config');
goog.require('Mixly.Events');
goog.require('Mixly.Menu');
goog.require('Mixly.ContextMenu');
goog.require('Mixly.Registry');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.Debug');
goog.require('Mixly.Component');
goog.require('Mixly.HTMLTemplate');
goog.provide('Mixly.FileTree');
const {
Env,
Msg,
Config,
Menu,
Events,
ContextMenu,
Registry,
IdGenerator,
Debug,
Component,
HTMLTemplate
} = Mixly;
const { USER } = Config;
class FileTree extends Component {
static {
this.FILE_ICON_MAP = goog.readJsonSync(path.join(Env.templatePath, 'json/file-icons.json'));
this.FOLDER_ICON_MAP = goog.readJsonSync(path.join(Env.templatePath, 'json/folder-icons.json'));
HTMLTemplate.add(
'html/file-tree.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/file-tree.html')))
);
}
#$openFolderContent_ = null;
#$folderContent_ = null;
#$rootFolder_ = null;
#$iconTriangle_ = null;
#$iconFolder_ = null;
#$name_ = null;
#$children_ = null;
#$progress_ = null;
#$mask_ = null;
#$fileTree_ = null;
#mprogress_ = null;
#rootFolderOpened_ = false;
#rootPath_ = '';
#rootName_ = '';
#rootTitle_ = '';
#fs_ = null;
#contextMenu_ = null;
#selected_ = null;
#jstree_ = null;
#scrollbar_ = null;
constructor(fs) {
super();
const $content = $(HTMLTemplate.get('html/file-tree.html').render());
this.setContent($content);
this.#$rootFolder_ = $content.find('.folder-title');
this.#$iconTriangle_ = this.#$rootFolder_.find('.triangle');
this.#$iconFolder_ = this.#$rootFolder_.find('.folder');
this.#$name_ = this.#$rootFolder_.find('.name');
this.#$children_ = $content.find('.children');
this.#$progress_ = $content.children('.progress');
this.#$mask_ = $content.children('.mask');
this.#fs_ = fs;
this.#rootPath_ = '';
this.#scrollbar_ = new XScrollbar(this.#$children_[0], {
onlyHorizontal: false,
thumbSize: '4px',
thumbRadius: '1px',
thumbBackground: USER.theme === 'dark'? '#b0b0b0' : '#5f5f5f'
});
this.#$fileTree_ = $(this.#scrollbar_.$content);
this.#$fileTree_.jstree({
core: {
strings: {
'Loading ...': Msg.Lang['fileTree.loading'] + '...'
},
multiple: false,
animation: false,
worker: false,
dblclick_toggle: false,
check_callback: function(operation, node, parent, position, more) {
if(operation === 'copy_node' || operation === 'move_node') {
if(parent.id === '#') {
return false;
}
}
return true;
},
data: (node, cb) => {
if (!this.#rootPath_) {
cb([]);
return;
}
this.showProgress();
let folderPath = this.#rootPath_;
if(node.id !== '#') {
let $li = this.#jstree_.get_node(node, true);
let $i = $li.find('.jstree-anchor > .jstree-icon');
$i.addClass('layui-anim layui-anim-fadein layui-anim-fadeout layui-anim-loop');
folderPath = node.id;
}
this.#getChildren_(folderPath)
.then((data) => {
cb(data);
})
.catch(Debug.error)
.finally(() => this.hideProgress());
},
themes: {
dots: true,
name: USER.theme === 'light'? 'default' : 'default-dark',
responsive: false,
ellipsis: true
}
},
plugins: ['wholerow', 'unique']
});
this.#jstree_ = this.#$fileTree_.jstree(true);
this.addEventsType([
'beforeSelectLeaf', 'afterSelectLeaf', 'afterOpenNode', 'afterCloseNode', 'afterRefreshNode',
'afterCreateNode', 'afterDeleteNode', 'afterRenameNode'
]);
this.#addEventsListener_();
this.#addContextMenu_();
this.nodeAliveRegistry = new Registry();
this.delayRefreshRegistry = new Registry();
this.watchRegistry = new Registry();
}
#addEventsListener_() {
this.#$fileTree_
.on('click.jstree', '.jstree-open>a', ({ target }) => {
setTimeout(() => {
$(target).parent().removeClass('jstree-leaf').addClass('jstree-opened');
this.#jstree_.close_node(target);
});
})
.on('click.jstree', '.jstree-closed>a', ({ target }) => {
setTimeout(() => {
$(target).parent().removeClass('jstree-leaf').addClass('jstree-closed');
this.#jstree_.open_node(target);
});
})
.on('open_node.jstree', (e, data) => {
const { id } = data.node;
let elem = document.getElementById(id);
let $i = $(elem).children('.jstree-anchor').children('.jstree-icon');
$i.addClass('opened');
})
.on('close_node.jstree', (e, data) => {
const { id } = data.node;
let elem = document.getElementById(id);
let $i = $(elem).children('.jstree-anchor').children('.jstree-icon');
$i.removeClass('opened');
})
.on('after_open.jstree', (e, data) => {
const { id } = data.node;
const eventId = this.nodeAliveRegistry.getItem(id);
if (eventId) {
clearTimeout(eventId);
this.nodeAliveRegistry.unregister(id);
} else {
this.watchFolder(id);
}
this.runEvent('afterOpenNode', data);
this.reselect();
})
.on('after_close.jstree', (e, data) => {
const { id } = data.node;
const eventId = setTimeout(() => {
this.unwatchFolder(id);
}, 60 * 1000);
if (!this.nodeAliveRegistry.getItem(id)) {
this.nodeAliveRegistry.register(id, eventId);
}
this.runEvent('afterCloseNode', data);
this.reselect();
})
.on('changed.jstree', (e, data) => {
const selected = data.instance.get_selected(true);
if (!selected.length) {
// this.#selected_ = null;
return;
}
if ((selected[0].icon || '').indexOf('foldericon') !== -1) {
this.reselect();
return;
}
if (selected[0].id === this.#selected_) {
return;
}
const result = this.runEvent('beforeSelectLeaf', selected);
if ((result.length && result[0]) || !result.length) {
this.#selected_ = selected[0].id;
this.runEvent('afterSelectLeaf', selected);
} else {
this.deselect(selected[0].id);
this.reselect();
}
})
.on('refresh.jstree', (e, data) => {
this.runEvent('afterRefreshNode', data.node);
})
.on('refresh_node.jstree', (e, data) => {
this.runEvent('afterRefreshNode', data.node);
});
this.#$rootFolder_.click(() => {
if (this.isRootFolderOpened()) {
this.closeRootFolder();
} else {
this.openRootFolder();
this.reselect();
}
});
}
#addContextMenu_() {
const selector = `div[page-id="${this.getId()}"] .jstree-node, div[page-id="${this.getId()}"] > button`;
this.#contextMenu_ = new ContextMenu(selector, {
zIndex: 300,
events: {
hide: ({ $trigger }) => {
$trigger.removeClass('active');
},
activated: ({ $trigger }) => {
$trigger.addClass('active');
}
}
});
this.#addFileContextMenuItems_();
this.#contextMenu_.bind('getMenu', () => 'menu');
}
#addFileContextMenuItems_() {
let menu = new Menu();
menu.add({
weight: 0,
id: 'new_folder',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['root', 'folder'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['fileTree.newFolder'], ''),
callback: (_, { $trigger }) => {
const type = $trigger.attr('type');
if (type === 'root') {
this.openRootFolder();
this.createRootChildFolderNode();
} else {
const id = $trigger.attr('id');
this.createFolderNode(id);
}
}
}
});
menu.add({
weight: 1,
id: 'new_file',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['root', 'folder'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['fileTree.newFile'], ''),
callback: (_, { $trigger }) => {
const type = $trigger.attr('type');
if (type === 'root') {
this.openRootFolder();
this.createRootChildFileNode();
} else {
const id = $trigger.attr('id');
this.createFileNode(id);
}
}
}
});
menu.add({
weight: 2,
id: 'sep1',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['root', 'folder'].includes(type);
},
data: '---------'
});
menu.add({
weight: 3,
id: 'cut',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['file', 'folder'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.cut'], ''),
callback: (_, { $trigger }) => {
const id = $trigger.attr('id');
this.cutNode(id);
}
}
});
menu.add({
weight: 4,
id: 'copy',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['file', 'folder'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], ''),
callback: (_, { $trigger }) => {
const id = $trigger.attr('id');
this.copyNode(id);
}
}
});
menu.add({
weight: 5,
id: 'paste',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['root', 'folder'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.paste'], ''),
callback: (_, { $trigger }) => {
const type = $trigger.attr('type');
if (type === 'root') {
this.openRootFolder();
this.pasteNode('/');
} else {
const id = $trigger.attr('id');
this.pasteNode(id);
}
}
}
});
menu.add({
weight: 6,
id: 'sep2',
data: '---------'
});
menu.add({
weight: 7,
id: 'copy_path',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['fileTree.copyPath'], ''),
callback: (_, { $trigger }) => {
let outPath = null;
const type = $trigger.attr('type');
if (type === 'root') {
outPath = this.#rootPath_;
} else {
outPath = $trigger.attr('id');
}
navigator.clipboard.writeText(outPath)
.catch(Debug.error);
}
}
});
menu.add({
weight: 8,
id: 'rename',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['file', 'folder'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['fileTree.rename'], ''),
callback: (_, { $trigger }) => {
const type = $trigger.attr('type');
const id = $trigger.attr('id');
if (type === 'folder') {
this.renameFolderNode(id);
} else {
this.renameFileNode(id);
}
}
}
});
menu.add({
weight: 9,
id: 'del',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
return ['file', 'folder'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['fileTree.delete'], ''),
callback: (_, { $trigger }) => {
const type = $trigger.attr('type');
const id = $trigger.attr('id');
if (type === 'folder') {
if (this.#selected_) {
const relative = path.relative(id, this.#selected_);
if (relative.indexOf('../') !== 0) {
this.deselect(this.#selected_);
}
}
this.deleteFolderNode(id);
} else {
if (this.#selected_ === id) {
this.deselect(id);
}
this.deleteFileNode(id);
}
}
}
});
this.#contextMenu_.register('menu', menu);
}
getContextMenu() {
return this.#contextMenu_;
}
openRootFolder() {
if (this.isRootFolderOpened()) {
return;
}
this.#$iconTriangle_.removeClass('codicon-chevron-right');
this.#$iconTriangle_.addClass('codicon-chevron-down');
this.#$iconFolder_.addClass('opened');
this.#$rootFolder_.addClass('opened');
this.#$children_.css('display', 'block');
this.#rootFolderOpened_ = true;
}
closeRootFolder() {
if (!this.isRootFolderOpened()) {
return;
}
this.#$iconTriangle_.removeClass('codicon-chevron-down');
this.#$iconTriangle_.addClass('codicon-chevron-right');
this.#$iconFolder_.removeClass('opened');
this.#$rootFolder_.removeClass('opened');
this.#$children_.css('display', 'none');
const selected = this.#selected_;
this.deselectAll();
this.#selected_ = selected;
this.#rootFolderOpened_ = false;
}
isRootFolderOpened() {
return this.#rootFolderOpened_;
}
setFolderPath(folderPath) {
let newFolderPath = path.join(folderPath);
if (newFolderPath === this.#rootPath_) {
this.#jstree_.refresh();
return;
}
if (this.#rootPath_) {
this.unwatchFolder(this.#rootPath_);
}
this.#rootPath_ = newFolderPath;
this.nodeAliveRegistry.reset();
this.#jstree_.refresh();
this.watchFolder(this.#rootPath_);
this.setRootFolderTitle(this.#rootPath_);
const rootNodeName = path.basename(folderPath).toUpperCase();
this.setRootFolderName(rootNodeName);
}
getFolderPath() {
return this.#rootPath_;
}
setRootFolderName(name) {
this.#rootName_ = name;
this.#$name_.text(name);
}
getRootFolderName() {
return this.#rootName_;
}
setRootFolderTitle(name) {
this.#rootTitle_ = name;
this.#$rootFolder_.attr('title', name);
}
getRootFolderTitle() {
return this.#rootTitle_;
}
refreshFolder(folderPath) {
// 延迟刷新节点防止过于频繁的IO操作
let eventId = this.delayRefreshRegistry.getItem(folderPath);
if (eventId) {
clearTimeout(eventId);
this.delayRefreshRegistry.unregister(folderPath);
}
eventId = setTimeout(() => {
if (folderPath === this.#rootPath_) {
this.#jstree_.refresh();
return;
}
const node = this.#jstree_.get_node(folderPath);
const nodeIsOpened = node && !this.isClosed(folderPath);
if (nodeIsOpened) {
this.watchFolder(folderPath);
this.clearFolderTemp(folderPath);
this.#jstree_.refresh_node(folderPath);
} else {
this.unwatchFolder(folderPath);
}
}, 500);
this.delayRefreshRegistry.register(folderPath, eventId);
}
clearFolderTemp(folderPath) {
const node = this.#jstree_.get_node(folderPath);
if (!node) {
return;
}
node.state.loaded = false;
}
watchFolder(folderPath) {
if (this.isWatched(folderPath)) {
return;
}
this.watchRegistry.register(folderPath, 'folder');
}
unwatchFolder(folderPath) {
if (!this.isWatched(folderPath)) {
return;
}
this.clearFolderTemp(folderPath);
const keys = this.nodeAliveRegistry.keys();
for (let key of keys) {
if (key.indexOf(folderPath) === -1) {
continue;
}
const eventId = this.nodeAliveRegistry.getItem(key);
if (eventId) {
clearTimeout(eventId);
this.nodeAliveRegistry.unregister(key);
}
}
this.watchRegistry.unregister(folderPath);
}
watchFile(filePath) {}
unwatchFile(filePath) {}
isWatched(inPath) {
return !!this.watchRegistry.getItem(inPath);
}
isClosed(inPath) {
return this.#jstree_.is_closed(inPath);
}
select(inPath) {
let elem = document.getElementById(inPath);
if (!elem) {
this.#selected_ = null;
return;
}
this.#selected_ = inPath;
this.#jstree_.select_node(inPath, true, true);
$(elem).children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
}
reselect() {
if (!this.#selected_) {
return;
}
let elem = document.getElementById(this.#selected_);
if (!elem) {
return;
}
this.#jstree_.select_node(this.#selected_, true, true);
$(elem).children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
}
deselect(inPath) {
if (this.#selected_ === inPath) {
this.#selected_ = null;
}
let elem = document.getElementById(inPath);
if (!elem) {
return;
}
this.#jstree_.deselect_node(elem, true);
$(elem).children('.jstree-wholerow').removeClass('jstree-wholerow-clicked');
}
deselectAll() {
this.#selected_ = null;
this.#jstree_.deselect_all();
}
getSelectedNodeId() {
return this.#selected_;
}
getNode(inPath) {
return this.#jstree_.get_node(inPath);
}
getSelectedNodes() {
return this.#jstree_.get_selected(true);
}
async #getChildren_(inPath) {
let output = [];
const content = await this.readFolder(inPath);
for (let item of content) {
const { type, id, title, children } = item;
const text = item.text ?? path.basename(id);
let icon = item.icon ?? 'icon-doc';
if (!item.icon) {
if (type === 'folder') {
icon = this.#getFolderIcon_(text);
} else {
icon = this.#getFileIcon_(text);
}
}
output.push({
text,
id,
children,
li_attr: {
type,
name: text,
title: title ?? id
},
icon
});
}
return output;
}
async readFolder(inPath) {
return [];
}
#getFileIcon_(filename) {
const prefix = 'fileicon-';
if (FileTree.FILE_ICON_MAP[filename]) {
return prefix + FileTree.FILE_ICON_MAP[filename];
}
const extname = path.extname(filename).toLowerCase();
if (FileTree.FILE_ICON_MAP[extname]) {
return prefix + FileTree.FILE_ICON_MAP[extname];
}
return prefix + FileTree.FILE_ICON_MAP['default'];
}
#getFolderIcon_(foldername) {
const prefix = 'foldericon-';
if (FileTree.FOLDER_ICON_MAP[foldername]) {
return prefix + FileTree.FOLDER_ICON_MAP[foldername];
}
return prefix + FileTree.FOLDER_ICON_MAP['default'];
}
createRootChildNode(type) {
this.showProgress();
this.hideMask();
const node = this.#jstree_.get_node('#');
const children = false;
let icon = 'foldericon-default';
if (type === 'file') {
icon = 'fileicon-mix';
}
const folderPath = this.#rootPath_;
this.#jstree_.create_node(node, { children, icon }, 'first', (childNode) => {
this.#jstree_.edit(childNode, '', (newNode) => {
this.showMask();
const desPath = path.join(folderPath, newNode.text);
this.#jstree_.delete_node(newNode);
const oldNode = this.#jstree_.get_node(desPath);
if (oldNode || !newNode.text) {
this.hideProgress();
return;
}
let createPromise = null;
if (type === 'file') {
createPromise = this.#fs_.createFile(desPath);
} else {
createPromise = this.#fs_.createDirectory(desPath);
}
createPromise
.catch(Debug.error)
.finally(() => {
this.hideProgress();
this.runEvent('afterCreateNode', folderPath);
});
});
});
}
createRootChildFileNode() {
this.createRootChildNode('file');
}
createRootChildFolderNode() {
this.createRootChildNode('folder');
}
createNode(type, folderPath) {
this.showProgress();
this.hideMask();
const node = this.#jstree_.get_node(folderPath);
const children = false;
let icon = 'foldericon-default';
if (type === 'file') {
icon = 'fileicon-mix';
}
if (folderPath === '#') {
folderPath = this.#rootPath_;
}
this.#jstree_.open_node(node, () => {
this.#jstree_.create_node(node, { children, icon }, 'first', (childNode) => {
this.#jstree_.edit(childNode, '', (newNode) => {
this.showMask();
this.#jstree_.delete_node(newNode);
if (!newNode.text) {
this.hideProgress();
return;
}
const desPath = path.join(folderPath, newNode.text);
const parentNode = this.#jstree_.get_node(folderPath) ?? {};
for (let nodeId of parentNode.children ?? []) {
if (nodeId !== desPath) {
continue;
}
this.hideProgress();
return;
}
let createPromise = null;
if (type === 'file') {
createPromise = this.#fs_.createFile(desPath);
} else {
createPromise = this.#fs_.createDirectory(desPath);
}
createPromise
.catch(Debug.error)
.finally(() => {
this.hideProgress();
this.runEvent('afterCreateNode', folderPath);
});
});
});
});
}
createFileNode(folderPath) {
this.createNode('file', folderPath);
}
createFolderNode(folderPath) {
this.createNode('folder', folderPath);
}
renameNode(type, inPath) {
this.showProgress();
this.hideMask();
const node = this.#jstree_.get_node(inPath);
const oldNodeName = node.text;
this.#jstree_.edit(node, oldNodeName, (newNode) => {
this.showMask();
const folderPath = path.join(inPath, '..');
const desPath = path.join(folderPath, newNode.text);
this.#jstree_.close_node(newNode);
if (oldNodeName === newNode.text) {
this.hideProgress();
return;
}
this.#jstree_.rename_node(newNode, oldNodeName);
const parentNode = this.#jstree_.get_node(folderPath) ?? {};
for (let nodeId of parentNode.children ?? []) {
if (nodeId !== desPath) {
continue;
}
this.hideProgress();
return;
}
let renamePromise = null;
if (type === 'file') {
renamePromise = this.#fs_.renameFile(inPath, desPath);
} else {
renamePromise = this.#fs_.renameDirectory(inPath, desPath);
}
renamePromise
.catch(Debug.error)
.finally(() => {
this.hideProgress();
this.runEvent('afterRenameNode', path.join(inPath, '..'));
});
});
}
renameFileNode(filePath) {
this.renameNode('file', filePath);
}
renameFolderNode(folderPath) {
this.renameNode('folder', folderPath);
}
deleteNode(type, inPath) {
this.showProgress();
let deletePromise = null;
if (type === 'file') {
deletePromise = this.#fs_.deleteFile(inPath);
} else {
deletePromise = this.#fs_.deleteDirectory(inPath);
}
deletePromise
.catch(Debug.error)
.finally(() => {
this.hideProgress();
this.runEvent('afterDeleteNode', path.join(inPath, '..'));
});
}
deleteFileNode(filePath) {
this.deleteNode('file', filePath);
}
deleteFolderNode(folderPath) {
this.deleteNode('folder', folderPath);
}
copyNode(inPath) {
const node = this.#jstree_.get_node(inPath);
this.#jstree_.copy(node);
}
cutNode(inPath) {
const node = this.#jstree_.get_node(inPath);
this.#jstree_.cut(node);
}
pasteNode(folderPath) {
if (!this.#jstree_.can_paste()) {
return;
}
this.showProgress();
const oldNodes = this.#jstree_.get_buffer();
const oldNode = oldNodes.node[0];
const { mode } = oldNodes;
const { type } = oldNode.li_attr;
let pastePromise = null;
let startPath = oldNode.id;
let endPath = path.join(folderPath, oldNode.text);
if (mode === 'move_node') {
const relativePath = path.relative(startPath, endPath);
if (relativePath.indexOf('..') === -1) {
pastePromise = Promise.resolve();
} else {
if (type === 'file') {
pastePromise = this.#fs_.moveFile(startPath, endPath);
} else {
pastePromise = this.#fs_.createDirectory(endPath)
.then(() => {
return this.#fs_.moveDirectory(startPath, endPath);
});
}
}
} else if (mode === 'copy_node') {
if (type === 'file') {
pastePromise = this.#fs_.copyFile(startPath, endPath);
} else {
pastePromise = this.#fs_.createDirectory(endPath)
.then(() => {
return this.#fs_.copyDirectory(startPath, endPath);
});
}
}
pastePromise
.then(async () => {
if (mode === 'move_node') {
const temp = path.join(startPath, '..');
const relativePath = path.relative(temp, endPath);
if (relativePath.indexOf('..') === -1) {
this.clearFolderTemp(temp);
await this.loadNode(temp);
await this.openNode(temp);
this.clearFolderTemp(folderPath);
await this.loadNode(folderPath);
await this.openNode(folderPath);
} else {
this.clearFolderTemp(folderPath);
await this.loadNode(folderPath);
await this.openNode(folderPath);
this.clearFolderTemp(temp);
await this.loadNode(temp);
await this.openNode(temp);
}
} else {
this.clearFolderTemp(folderPath);
this.#jstree_.refresh_node(folderPath);
this.openNode(folderPath);
}
})
.catch(Debug.error)
.finally(() => {
this.hideProgress();
});
}
loadNode(inPath) {
return new Promise((resolve, reject) => {
if (inPath === '/') {
inPath = '#';
}
const node = this.#jstree_.get_node(inPath);
if (!node) {
resolve();
}
this.#jstree_.load_node(node, () => {
resolve();
});
});
}
openNode(folderPath) {
return new Promise((resolve, reject) => {
const node = this.#jstree_.get_node(folderPath);
if (!node) {
resolve();
}
this.#jstree_.open_node(node, () => {
resolve();
});
});
}
dispose() {
this.#jstree_.destroy();
this.#scrollbar_.destroy();
this.#contextMenu_.dispose();
super.dispose();
}
getFS() {
return this.#fs_;
}
setFS(fs) {
this.#fs_ = fs;
}
showProgress() {
this.#$progress_.css('display', 'block');
this.showMask();
}
hideProgress() {
this.#$progress_.css('display', 'none');
this.hideMask();
}
showMask() {
this.#$mask_.css('display', 'block');
}
hideMask() {
this.#$mask_.css('display', 'none');
}
}
Mixly.FileTree = FileTree;
});

View File

@@ -0,0 +1,43 @@
goog.loadJs('common', () => {
goog.provide('Mixly.File');
class File {
static {
this.showOpenFilePicker = async () => {
return new Promise((resolve, reject) => {
reject();
});
}
this.showDirectoryPicker = async () => {
return new Promise((resolve, reject) => {
reject();
});
}
this.showSaveFilePicker = async () => {
return new Promise((resolve, reject) => {
reject();
});
}
}
constructor() {}
async readFile() {
return new Promise((resolve, reject) => {
reject();
});
}
async writeFile(data) {
return new Promise((resolve, reject) => {
reject();
});
}
}
Mixly.File = File;
});

View File

@@ -0,0 +1,94 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.Env');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.require('Mixly.Config');
goog.require('Mixly.Boards');
goog.require('Mixly.FooterLayer');
goog.require('Mixly.FooterLayerBoardConfig');
goog.require('Mixly.FooterLayerMessage');
goog.require('Mixly.Component');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Electron.FooterLayerExample');
goog.require('Mixly.Web.FooterLayerExample');
goog.provide('Mixly.FooterBar');
const {
Env,
XML,
Msg,
Config,
Boards,
FooterLayer,
FooterLayerBoardConfig,
FooterLayerMessage,
Component,
HTMLTemplate,
Electron = {},
Web = {}
} = Mixly;
const { FooterLayerExample } = goog.isElectron? Electron : Web;
const { BOARD } = Config;
class FooterBar extends Component {
static {
HTMLTemplate.add(
'html/footerbar.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/footerbar.html')))
);
}
#exampleLayer_ = null;
#messageLayer_ = null;
#boardConfigLayer_ = null;
constructor() {
super();
let content = $(HTMLTemplate.get('html/footerbar.html').render({
outputAceName: Msg.Lang['footerbar.output'],
row: Msg.Lang['footerbar.cursor.row'],
column: Msg.Lang['footerbar.cursor.column'],
unknown: Msg.Lang['footerbar.board.unknown'],
config: Msg.Lang['footerbar.config'],
selected: Msg.Lang['footerbar.cursor.selected'],
on: Msg.Lang['footerbar.board.on'],
message: Msg.Lang['footerbar.message'],
example: Msg.Lang['footerbar.examples']
}));
Boards.init();
this.setContent(content);
content.find('.code-lang').html(BOARD.language ?? Msg.Lang['footerbar.language.unknown']);
this.#exampleLayer_ = new FooterLayerExample(content.find('.example')[0]);
this.#messageLayer_ = new FooterLayerMessage(content.find('.message')[0]);
this.#boardConfigLayer_ = new FooterLayerBoardConfig(content.find('.board-config')[0], Boards.dict);
Boards.addLayer(this.#boardConfigLayer_);
}
resize() {
super.resize();
this.#exampleLayer_.resize();
this.#messageLayer_.resize();
this.#boardConfigLayer_.resize();
}
getExampleLayer() {
return this.#exampleLayer_;
}
getMessageLayer() {
return this.#messageLayer_;
}
getBoardConfigLayer() {
return this.#boardConfigLayer_;
}
}
Mixly.FooterBar = FooterBar;
});

View File

@@ -0,0 +1,226 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('tippy');
goog.require('Blockly');
goog.require('layui');
goog.require('$.select2');
goog.require('Mixly.Env');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.FooterLayer');
goog.provide('Mixly.FooterLayerBoardConfig');
const {
FooterLayer,
Env,
XML,
Msg,
HTMLTemplate
} = Mixly;
const { dropdown } = layui;
class FooterLayerBoardConfig extends FooterLayer {
static {
// 弹层模板
this.menuHTMLTemplate = new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/footerlayer/footerlayer-board-config.html'))
);
}
/**
* @param domId { string } 绑定dom的id
* @param boardsInfo { obj } 板卡配置信息
{
"xxx板卡": BoardConfigItem,
...
}
* @return { FooterLayerBoardConfig obj }
**/
#canvas_ = null;
constructor(element, boardsInfo) {
super(element, {
onHidden: (instance) => {
this.boardsInfo[this.boardName].writeSelectedOptions();
},
onMount: (instance) => {
if (this.renderBoardName === this.boardName) {
return;
}
this.renderMenu();
},
btns: [
{
class: 'reset',
title: Msg.Lang['footerbar.config.default'],
icon: 'layui-icon-refresh-1',
onclick: () => {
const selectedBoardName = this.boardName;
let { defaultOptions } = this.boardsInfo[selectedBoardName];
this.setSelectedOptions(defaultOptions);
}
}
]
});
this.$content.addClass('footer-layer-board-config');
this.$footerContainer = $(element);
this.boardsInfo = boardsInfo;
// 当前用户所选择的板卡
this.boardName = null;
// 当前渲染的板卡配置所对应的板卡名
this.renderBoardName = null;
this.dropdownItems = {};
}
getSelectedParams() {
let paramList = [];
let { ignore, selectedOptions } = this.boardsInfo[this.boardName];
for (let i in selectedOptions) {
if (ignore.includes(i)) {
continue;
}
paramList.push(i + '=' + selectedOptions[i].key);
}
let { boardKey } = this;
const index = boardKey.indexOf('@');
if (index !== -1) {
boardKey = boardKey.substring(0, index);
}
return paramList.length ? (boardKey + ':' + paramList.join(',')) : boardKey;
}
getSelectedParamByName(name) {
let { selectedOptions } = this.boardsInfo[this.boardName];
for (let i in selectedOptions) {
if (i === name) {
return selectedOptions[i].key;
}
}
return '';
}
renderMenu() {
let { options, selectedOptions } = this.boardsInfo[this.boardName];
this.renderTemplate(options);
this.renderOptions(options);
this.setSelectedOptions(selectedOptions);
this.renderBoardName = this.boardName
}
setSelectedOptions(selectedOptions) {
// 每次打开板卡设置窗口时设置其默认选中项
const boardsInfo = this.boardsInfo[this.boardName];
let optionsType = Object.keys(boardsInfo.defaultOptions);
for (let i in selectedOptions) {
if (!boardsInfo.defaultOptions[i]) {
continue;
}
let id = boardsInfo.defaultOptions[i].key;
if (boardsInfo.optionIsLegal(i, selectedOptions[i])) {
id = selectedOptions[i].key;
}
this.$body.find(`[mid=${i}]`).val(id).trigger('change');
boardsInfo.setSelectedOption(i, selectedOptions[i]);
}
// 重新计算窗口的位置
this.setProps({});
}
renderOptions(options) {
const _this = this;
for (let item of options) {
this.createDropdownMenu(item.key, item.options);
this.createMessageLayer(item.key, item.messageId);
}
}
createMessageLayer(mId, messageId) {
if (!messageId) {
return;
}
if (!Blockly.Msg[messageId]) {
return;
}
let $container = this.$body.find(`[mid="${mId}-label"]`);
tippy($container[0], {
content: Blockly.Msg[messageId],
allowHTML: true,
interactive: true,
placement: 'left',
offset: [0, 16]
});
}
createDropdownMenu(mId, options) {
let $container = this.$body.find(`[mid="${mId}"]`);
if (!$container.length) {
return;
}
let menu = [];
let maxLength = 0;
for (let item of options) {
menu.push({
id: item.id,
text: item.title
});
let nowLength = this.getStrWidth(item.title);
if (maxLength < nowLength) {
maxLength = nowLength;
}
}
$container.select2({
data: menu,
minimumResultsForSearch: Infinity,
width: 'auto',
dropdownCssClass: 'mixly-scrollbar select2-board-config'
});
$container.next().css('min-width', (maxLength + 30) + 'px');
$container.on('select2:select', (e) => {
const boardName = this.boardName;
const data = e.params.data;
this.boardsInfo[boardName].setSelectedOption(mId, {
key: data.id,
label: data.text
});
this.setProps({});
});
}
renderTemplate(options) {
this.$body.find('select').select2('destroy');
const xmlStr = FooterLayerBoardConfig.menuHTMLTemplate.render({ options });
this.updateContent(xmlStr);
}
changeTo(boardName) {
if (this.boardsInfo[boardName]?.options.length) {
this.$footerContainer.css('display', 'inline-flex');
} else {
this.$footerContainer.css('display', 'none');
this.hide();
}
this.boardName = boardName;
this.boardKey = this.boardsInfo[boardName].key;
this.renderMenu(this.layer);
}
getStrWidth(str, font = '14px Arial') {
try {
this.#canvas_ = this.#canvas_ || document.createElement('canvas').getContext('2d');
this.#canvas_.font = font;
return this.#canvas_.measureText(str).width;
} catch (e) {
return 0;
}
}
resize() {
this.$body.find('select').select2('close');
}
}
Mixly.FooterLayerBoardConfig = FooterLayerBoardConfig;
});

View File

@@ -0,0 +1,86 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('layui');
goog.require('Mixly.Title');
goog.require('Mixly.Env');
goog.require('Mixly.Debug');
goog.require('Mixly.Workspace');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.FooterLayer');
goog.provide('Mixly.FooterLayerExample');
const {
Title,
Env,
Debug,
Workspace,
HTMLTemplate,
FooterLayer
} = Mixly;
const { dropdown, tree } = layui;
class FooterLayerExample extends FooterLayer {
static {
// 弹层模板
this.menuHTMLTemplate = new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/footerlayer/footerlayer-example.html'))
);
}
constructor(element) {
super(element, {
onMount: (instance) => {
this.examplesTree.reload({ data: this.getRoot() });
}
});
this.$content.addClass('footer-layer-example');
this.updateContent(FooterLayerExample.menuHTMLTemplate.render());
this.$treeBody = this.$body.children('.example-tree-body');
this.DEPTH = 5;
this.render();
}
render() {
this.examplesTree = tree.render({
elem: this.$treeBody[0],
data: this.getRoot(),
accordion: true,
anim: false,
icon: [ 'icon-folder-empty', 'icon-folder-open-empty-1', 'icon-file-code' ],
getChildren: (obj) => {
return this.getChildren(obj.data.id);
},
click: (obj) => {
if (obj.data.children) {
return;
}
this.dataToWorkspace(obj.data.id);
},
statusChange: () => {
this.setProps({});
}
});
this.examplesTree.config.statusChange();
}
// 可覆盖
getRoot() {}
// 可覆盖
getChildren(inPath) {}
// 可覆盖
dataToWorkspace(inPath) {}
updateCode(ext, data) {
const editorMix = Workspace.getMain().getEditorsManager().getActive();
editorMix.setValue(data, ext);
Title.updateTitle(Title.title);
}
}
Mixly.FooterLayerExample = FooterLayerExample;
});

View File

@@ -0,0 +1,204 @@
goog.loadJs('common', () => {
goog.require('shortid');
goog.require('path');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.require('Mixly.Env');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.FooterLayer');
goog.provide('Mixly.FooterLayerMessage');
const {
XML,
Msg,
Env,
HTMLTemplate,
FooterLayer
} = Mixly;
class FooterLayerMessage extends FooterLayer {
// 弹层模板
static {
this.menuHTMLTemplate = new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/footerlayer/footerlayer-message.html'))
);
this.menuItemHTMLTemplate = new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/footerlayer/footerlayer-message-item.html'))
);
this.menuItemWithIconHTMLTemplate = new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/footerlayer/footerlayer-message-item-with-icon.html'))
);
this.STYLES = ['primary', 'secondary', 'success', 'danger', 'warning'];
}
constructor(element) {
super(element, {
onMount: (instance) => {
this.$body.scrollTop(this.$container.parent().prop('scrollHeight'));
},
btns: [
{
class: 'clear',
title: Msg.Lang['footerbar.message.clear'],
icon: 'layui-icon-delete',
onclick: () => {
this.clear();
}
}
]
});
this.DEFALUT_ITEM_CONFIG = {
type: 0,
style: 'primary',
src: '../common/media/mixly.png',
name: '',
onCreate: (obj) => {},
onDestroy: () => {},
btns: [],
checkbox: false
};
const { menuHTMLTemplate } = FooterLayerMessage;
this.updateContent(menuHTMLTemplate.render());
this.messageQuery = [];
this.$content.addClass('footer-layer-message');
this.$container = this.$content.find('.toast-container');
this.clear();
}
append(config) {
config.operate = 'append';
if (!this.$container.length) {
this.messageQuery.push(config);
if (this.messageQuery.length > 50) {
this.messageQuery.shift();
}
return;
}
this.add_(config);
}
add_(config) {
config = { ...this.DEFALUT_ITEM_CONFIG, ...config };
this.$body.css('width', '350px');
let $children = this.$container.children('div');
if (!$children.length) {
this.$container.html('');
} else if ($children.length >= 50) {
for (let i = 0; i <= $children.length - 50; i++) {
$($children[i]).remove();
}
}
if (!FooterLayerMessage.STYLES.includes(config.style)) {
config.style = FooterLayerMessage.STYLES[0];
}
let btnsFunc = [];
config.btns = config.btns ?? [];
for (let i in config.btns) {
if (!(config.btns[i] instanceof Object)) {
continue;
}
btnsFunc.push(config.btns[i].onclick);
delete config.btns[i].onclick;
config.btns[i].mId = i;
}
let checkbox = {
checked: '',
show: false,
title: ''
};
if (config.checkbox instanceof Object) {
checkbox.id = shortid.generate();
checkbox.show = true;
checkbox.title = config.checkbox.title;
checkbox.checked = config.checkbox.checked ? 'checked' : '';
}
let template = this.genItemTemplate({
type: config.type,
style: config.style,
src: config.src,
name: config.name,
message: config.message,
btns: config.btns,
checkbox
});
let $template = $(template);
this.$container.append($template);
this.createBtnsClickEvent_($template, config.checkbox, btnsFunc);
this.scrollToBottom();
this.setProps({});
if (typeof config.onCreate === 'function') {
config.onCreate($template);
}
if (typeof config.onDestroy === 'function') {
$template.find('.btn-close[data-bs-dismiss="toast"]').off().click(() => {
$template.remove();
if (!this.$container.children('div').length) {
this.clear();
}
config.onDestroy();
});
}
}
createBtnsClickEvent_(container, checkbox, btnsFunc) {
container.find('.btns').children('button').off().click((event) => {
let $target = $(event.target);
let mId = parseInt($target.attr('m-id'));
let checkboxValue = null;
if (checkbox) {
checkboxValue = container.find('input[class="form-check-input"]').prop('checked');
}
if (typeof btnsFunc[mId] === 'function') {
btnsFunc[mId](event, container, checkboxValue);
}
if (!this.$container.children('div').length) {
this.clear();
}
});
}
remove() {
if (!this.$container.children('div').length) {
return;
}
$container.eq(-1).remove();
}
clear() {
this.$container.html(Msg.Lang['footerbar.message.empty']);
this.$body.css('width', '100px');
this.setProps({});
}
genItemTemplate(items) {
const {
menuItemWithIconHTMLTemplate,
menuItemHTMLTemplate
} = FooterLayerMessage;
let htmlTemplate;
if (items.type === 1) {
htmlTemplate = menuItemWithIconHTMLTemplate;
} else {
htmlTemplate = menuItemHTMLTemplate;
}
return htmlTemplate.render(items);
}
scrollToTop() {
this.$body.scrollTop(0);
}
scrollToBottom() {
this.$body.scrollTop(this.$body.prop('scrollHeight'));
}
}
Mixly.FooterLayerMessage = FooterLayerMessage;
});

View File

@@ -0,0 +1,115 @@
goog.loadJs('common', () => {
goog.require('tippy');
goog.require('path');
goog.require('Mixly.Env');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.provide('Mixly.FooterLayer');
const { Env, XML, Msg } = Mixly;
class FooterLayer {
static {
// 弹层模板和一些默认配置项
this.TEMPLATE = goog.readFileSync(path.join(Env.templatePath, 'html/footerlayer/footerlayer.html'));
this.DEFAULT_CONFIG_TIPPY = {
allowHTML: true,
trigger: 'manual',
interactive: true,
hideOnClick: false,
maxWidth: 'none',
offset: [ 0, 6 ],
delay: 0,
duration: [ 0, 0 ]
};
}
/**
* @param domId 被绑定元素的ID
* @param config 配置项
{
...DEFAULT_CONFIG_TIPPY
}
**/
constructor(element, config) {
this.config = {
...FooterLayer.DEFAULT_CONFIG_TIPPY,
...(config ?? {})
};
this.btns = config.btns ?? [];
this.btnsClickEvent = {};
this.element = element;
this.layer = null;
this.#create_();
this.#addSharedMethod_();
this.setContent(XML.render(FooterLayer.TEMPLATE, {
content: '',
btns: this.btns,
close: Msg.Lang['footerlayer.close']
}));
this.$content = $(this.layer.popper).find('.tippy-content');
this.$body = this.$content.find('.footer-layer-body');
this.#addContainerClickEvent_();
this.#addBtnsClickEvent_();
}
#create_() {
this.layer = tippy(this.element, this.config);
}
updateContent(content) {
if (this.$body.length) {
this.$body.html(content);
} else {
this.setContent(content);
}
}
#addBtnsClickEvent_() {
for (let i of this.btns) {
if (!(i instanceof Object)) {
continue;
}
if (typeof i.onclick !== 'function' || !(i.class)) {
continue;
}
this.btnsClickEvent[i.class] = i.onclick;
delete i.onclick;
}
this.btnsClickEvent['close'] = this.hide;
for (let key in this.btnsClickEvent) {
$(this.layer.popper).find('.layui-layer-title').children(`.${key}`)
.off().click(() => {
this.btnsClickEvent[key]();
});
}
}
#addSharedMethod_() {
let sharedMethod = ['hide', 'show', 'destroy', 'setProps', 'setContent'];
for (let type of sharedMethod) {
this[type] = this.layer[type];
}
}
/**
* @method 为绑定的元素添加鼠标单击事件
* @return {void}
**/
#addContainerClickEvent_() {
$(this.element).off().click(() => {
if (this.layer.state.isShown) {
this.hide();
} else {
this.show();
}
});
}
resize() {}
}
Mixly.FooterLayer = FooterLayer;
});

View File

@@ -0,0 +1,144 @@
goog.loadJs('common', () => {
goog.require('Mixly.Registry');
goog.provide('Mixly.FSBoardHandler');
const { Registry } = Mixly;
class FSBoardHandler {
static {
this.FsType = {
LITTLEFS: 'littlefs',
FATFS: 'fatfs',
SPIFFS: 'spiffs'
};
this.CommandType = {
DOWNLOAD: 'download',
UPLOAD: 'upload'
};
}
#config_ = {
type: FSBoardHandler.FsType.LITTLEFS,
offset: 0x610000,
size: 0x9F0000,
blockSize: 4096,
pageSize: 256,
usrFolder: '/'
};
#commandsRegistry_ = new Registry();
constructor() {
for (let key in FSBoardHandler.FsType) {
const fsRegistry = new Registry();
fsRegistry.register(FSBoardHandler.CommandType.DOWNLOAD, '');
fsRegistry.register(FSBoardHandler.CommandType.UPLOAD, '');
this.#commandsRegistry_.register(FSBoardHandler.FsType[key], fsRegistry);
}
}
getConfig() {
return this.#config_;
}
updateConfig(config) {
Object.assign(this.#config_, config);
}
setFSType(type) {
this.#config_.type = type;
}
getFSType() {
return this.#config_.type;
}
setFSSize() {
this.#config_.size = size;
}
getFSSize() {
return this.#config_.size;
}
setFSPageSize(size) {
this.#config_.pageSize = size;
}
getFSPageSize() {
return this.#config_.pageSize;
}
setFSBlockSize(size) {
this.#config_.blockSize = size;
}
getFSBlockSize() {
return this.#config_.blockSize;
}
setFSOffset(offset) {
this.#config_.offset = offset;
}
getFSOffset() {
return this.#config_.offset;
}
setUsrFolder(usrFolder) {
this.#config_.usrFolder = usrFolder;
}
getUsrFolder() {
return this.#config_.usrFolder;
}
setFSCommands(type, commands) {
const fsRegistry = this.getFSCommands(type);
if (!fsRegistry) {
return;
}
for (let key in commands) {
fsRegistry.hasKey(key) && fsRegistry.unregister(key);
fsRegistry.register(key, commands[key]);
}
}
getFSCommands(type) {
return this.#commandsRegistry_.getItem(type);
}
setLittleFSCommands(commands) {
this.setFSCommands(FSBoardHandler.FsType.LITTLEFS, commands);
}
getLittleFSCommands() {
return this.getFSCommands(FSBoardHandler.FsType.LITTLEFS);
}
setFatFSCommands(commands) {
this.setFSCommands(FSBoardHandler.FsType.FATFS, commands);
}
getFatFSCommands() {
return this.getFSCommands(FSBoardHandler.FsType.FATFS);
}
setSpifFSCommands(commands) {
this.setFSCommands(FSBoardHandler.FsType.SPIFFS, commands);
}
getSpifFSCommands() {
return this.getFSCommands(FSBoardHandler.FsType.SPIFFS);
}
onBeforeUpload() {}
onBeforeDownload() {}
}
Mixly.FSBoardHandler = FSBoardHandler;
});

View File

@@ -0,0 +1,96 @@
goog.loadJs('common', () => {
goog.require('Mixly.Serial');
goog.require('Mixly.Boards');
goog.require('Mixly.FSBoardHandler');
goog.provide('Mixly.FSBoard');
const {
Serial,
FSBoardHandler,
Boards
} = Mixly;
class FSBoard {
#handler_ = null;
constructor() {}
#addParams_(usrFolder, fsType) {
this.#handler_.updateConfig({
port: Serial.getSelectedPortName()
});
this.#handler_.setUsrFolder(usrFolder);
this.#handler_.setFSType(fsType);
}
async download(usrFolder, fsType) {
this.#addParams_(usrFolder, fsType);
this.#handler_.onBeforeDownload();
}
async upload(usrFolder, fsType) {
this.#addParams_(usrFolder, fsType);
this.#handler_.onBeforeUpload();
}
getConfig() {
return this.#handler_.getConfig();
}
getFSType() {
return this.#handler_.getFSType();
}
getLittleFSCommands() {
return this.#handler_.getLittleFSCommands();
}
getFatFSCommands() {
return this.#handler_.getFatFSCommands();
}
getSpifFSCommands() {
return this.#handler_.getSpifFSCommands();
}
getFSCommands(type) {
return this.#handler_.getFSCommands(type);
}
getSelectedFSCommands() {
return this.getFSCommands(this.getFSType());
}
getSelectedFSDownloadCommand() {
const fsRegistry = this.getSelectedFSCommands();
if (!fsRegistry) {
return '';
}
return fsRegistry.getItem(FSBoardHandler.CommandType.DOWNLOAD);
}
getSelectedFSUploadCommand() {
const fsRegistry = this.getSelectedFSCommands();
if (!fsRegistry) {
return '';
}
return fsRegistry.getItem(FSBoardHandler.CommandType.UPLOAD);
}
setHandler(handler) {
this.#handler_ = handler;
}
getHandler() {
return this.#handler_;
}
dispose() {
this.#handler_ = null;
}
}
Mixly.FSBoard = FSBoard;
});

View File

@@ -0,0 +1,47 @@
goog.loadJs('common', () => {
goog.require('Mixly');
goog.provide('Mixly.FS');
class FS {
constructor() {}
async rename(oldPath, newPath) {}
async createFile(filePath) {}
async readFile(filePath, encoding = 'utf8') {}
async writeFile(filePath, data) {}
async isFile(filePath) {}
async renameFile(oldFilePath, newFilePath) {}
async moveFile(oldFilePath, newFilePath) {}
async copyFile(oldFilePath, newFilePath) {}
async deleteFile(filePath) {}
async createDirectory(folderPath) {}
async readDirectory(folderPath) {}
async isDirectory(folderPath) {}
async isDirectoryEmpty(folderPath) {}
async renameDirectory(oldFolderPath, newFolderPath) {}
async moveDirectory(oldFolderPath, newFolderPath) {}
async copyDirectory(oldFolderPath, newFolderPath) {}
async deleteDirectory(folderPath) {}
}
Mixly.FS = FS;
});

View File

@@ -0,0 +1,74 @@
goog.loadJs('common', () => {
goog.require('Mixly.XML');
goog.require('Mixly.Registry');
goog.require('Mixly.IdGenerator');
goog.provide('Mixly.HTMLTemplate');
const {
XML,
Registry,
IdGenerator,
} = Mixly;
class HTMLTemplate {
static {
this.regexStyle_ = /(?<=<style>)[\s\S]*?(?=<\/style>)/gm;
this.regexHTML_ = /<style>[\s\S]*?<\/style>/gm;
this.templateRegistry = new Registry();
this.add = function(id, htmlTemplate) {
if (this.get(id)) {
return;
}
this.templateRegistry.register(id, htmlTemplate);
}
this.remove = function(id) {
this.templateRegistry.unregister(id);
}
this.get = function(id) {
return this.templateRegistry.getItem(id);
}
}
#style_ = null;
#html_ = null;
constructor(template) {
this.id = IdGenerator.generate();
this.#style_ = XML.render((template.match(HTMLTemplate.regexStyle_) || []).join('\n'), {
mId: this.id
});
this.#html_ = template.replace(HTMLTemplate.regexHTML_, '');
if (this.#style_) {
this.#addCss_();
}
}
render(config = {}) {
if (!config.mId) {
config.mId = this.id;
}
return XML.render(this.#html_, config);
}
#addCss_() {
let hasStyleNode = $('head').find(`style[style-id="${this.id}"]`).length;
if (hasStyleNode) {
return;
}
let $style = $('<style></style>');
$style.attr('style-id', this.id);
$style.attr('type', 'text/css').html(this.#style_);
$('head').append($style);
}
getId() {
return this.id;
}
}
Mixly.HTMLTemplate = HTMLTemplate;
});

View File

@@ -0,0 +1,24 @@
goog.loadJs('common', () => {
goog.require('shortid');
goog.require('Mixly');
goog.provide('Mixly.IdGenerator');
const { IdGenerator } = Mixly;
IdGenerator.generate = function(input) {
let output = {};
if (input instanceof Array) {
for (let i of input) {
if (typeof i !== 'string') {
continue;
}
output[i] = shortid.generate();
}
return output;
} else {
return shortid.generate();
}
}
});

View File

@@ -0,0 +1,44 @@
goog.loadJs('common', () => {
goog.require('ifvisible');
goog.require('Mixly.Events');
goog.provide('Mixly.IfVisible');
const { Events, IfVisible } = Mixly;
IfVisible.events = new Events(['blur', 'focus', 'idle', 'wakeup']);
IfVisible.init = function () {
ifvisible.on('blur', () => this.runEvent('blur'));
ifvisible.on('focus', () => this.runEvent('focus'));
ifvisible.on('idle', () => this.runEvent('idle'));
ifvisible.on('wakeup', () => this.runEvent('wakeup'));
}
IfVisible.bind = function (type, func) {
return this.events.bind(type, func);
}
IfVisible.unbind = function (id) {
this.events.unbind(id);
}
IfVisible.addEventsType = function (eventsType) {
this.events.addType(eventsType);
}
IfVisible.runEvent = function (eventsType, ...args) {
return this.events.run(eventsType, ...args);
}
IfVisible.offEvent = function (eventsType) {
this.events.off(eventsType);
}
IfVisible.resetEvent = function () {
this.events.reset();
}
IfVisible.init();
});

View File

@@ -0,0 +1,6 @@
goog.provide('Mixly.JSFuncs');
goog.require('Mixly.Config');
Mixly.JSFuncs.getPlatform = function () {
return Mixly.Config.BOARD.boardType;
};

View File

@@ -0,0 +1,167 @@
goog.loadJs('common', () => {
goog.require('layui');
goog.require('Mixly.Config');
goog.require('Mixly.Msg');
goog.provide('Mixly.LayerExt');
const {
LayerExt,
Config,
Msg
} = Mixly;
const { BOARD, USER } = Config;
const { layer } = layui;
LayerExt.SHADE_ALL = [1, 'transparent'];
LayerExt.SHADE_NAV = [1, 'transparent', '40px'];
// 默认的弹层标题高度
LayerExt.DEFAULT_TITLE_HEIGHT = 42;
// 默认的弹层配置项
LayerExt.DEFAULT_CONFIG = {
area: ['50%', '50%'],
max: ['850px', '543px'],
min: ['350px', '243px'],
title: '信息',
id: 'info',
content: '',
resize: true,
shade: LayerExt.SHADE_ALL,
success: null,
beforeEnd: null,
end: null,
cancel: null,
resizing: null,
offset: 'auto',
fixed: true,
borderRadius: '5px',
maxmin: false,
zIndex: 19891014
};
LayerExt.open = (toolConfig) => {
if (typeof toolConfig !== 'object')
toolConfig = LayerExt.DEFAULT_CONFIG;
else
toolConfig = {
...LayerExt.DEFAULT_CONFIG,
...toolConfig
};
const { title } = toolConfig;
let layerOffset = 42;
let layerTitle = null;
if (title) {
if (typeof title === 'object' && !isNaN(parseInt(title[1]))) {
layerOffset = parseInt(title[1]);
layerTitle = title[0];
} else {
layerTitle = title;
}
} else {
layerOffset = 0;
layerTitle = false;
}
return layer.open({
...toolConfig,
type: 1,
title: layerTitle,
closeBtn: 1,
success: function (layero, index) {
const { borderRadius, id, max, min, success } = toolConfig;
layer.style(index, { borderRadius });
const pageBody = $('#' + id);
pageBody.addClass('mixly-scrollbar');
if (typeof max === 'object') {
layero.css({
'maxWidth': max[0],
'maxHeight': max[1]
});
pageBody.css('maxWidth', max[0]);
if (max[1].indexOf('%') !== -1)
pageBody.css('maxHeight', max[1]);
else
pageBody.css('maxHeight', (parseInt(max[1]) - layerOffset) + 'px');
}
if (typeof min === 'object') {
layero.css({
'minHeight': min[1],
'minWidth': min[0]
});
pageBody.css('minWidth', min[0]);
if (min[1].indexOf('%') !== -1)
pageBody.css('minHeight', min[1]);
else
pageBody.css('minHeight', (parseInt(min[1]) - layerOffset) + 'px');
}
const winHeight = $(window).height();
const winWidth = $(window).width();
layero.css({
'left': (winWidth - layero.width()) / 2 + 'px',
'top': (winHeight - layero.height()) / 2 + 'px'
});
const pageTitle = layero.find('.layui-layer-title');
pageTitle.css('borderRadius', borderRadius + ' ' + borderRadius + ' 0px 0px');
pageBody.css('borderRadius', '0px 0px ' + borderRadius + ' ' + borderRadius);
const $close = layero.find('.layui-layer-setwin');
if (layerOffset && layerOffset !== 42) {
pageTitle.css({
'height': layerOffset + 'px',
'line-height': layerOffset + 'px'
});
$close.css({
'right': '10px',
'top': '10px'
});
pageBody.css({
'height': (layero.height() - layerOffset) + 'px'
});
}
if (typeof success === 'function')
success(layero, index);
},
beforeEnd: function (layero, index, that) {
const { beforeEnd } = toolConfig;
if (typeof beforeEnd === 'function') {
beforeEnd(layero, index, that);
}
},
end: function () {
const { end } = toolConfig;
if (typeof end === 'function') {
end();
}
},
cancel: function (index, layero) {
$('#layui-layer-shade' + index).remove();
const { cancel } = toolConfig;
if (typeof cancel === 'function') {
cancel(index, layero);
}
},
resizing: function (layero) {
const winHeight = $(window).height();
const winWidth = $(window).width();
const width = layero.width()/winWidth*100 + "%";
const height = layero.height()/winHeight*100 + "%";
layero.css({ width, height });
const { resizing } = toolConfig;
if (typeof resizing === 'function') {
const $content = layero.children('.layui-layer-content');
resizing({
layero: [ width, height ],
content: [ $content.width(), $content.height() ]
});
}
}
});
}
LayerExt.openProgressLayer = () => {
}
});

View File

@@ -0,0 +1,94 @@
goog.loadJs('common', () => {
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Layer');
goog.require('Mixly.HTMLTemplate');
goog.provide('Mixly.LayerFirmware');
const {
Env,
Msg,
Layer,
HTMLTemplate
} = Mixly;
class LayerFirmware extends Layer {
static {
HTMLTemplate.add(
'html/dialog/firmware.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/dialog/firmware.html')))
);
}
#dialog_ = null;
#$dialogContent_ = null;
#$cancel_ = null;
#$burn_ = null;
#$select_ = null;
#map_ = {};
constructor(config = {}, shadowType = 'nav') {
const $dialogContent_ = $(HTMLTemplate.get('html/dialog/firmware.html').render({
cancel: Msg.Lang['nav.btn.cancel'],
burn: Msg.Lang['nav.btn.burn']
}));
config.content = $dialogContent_;
super(config, shadowType);
this.#$dialogContent_ = $dialogContent_;
this.#$cancel_ = $dialogContent_.find('.cancel');
this.#$burn_ = $dialogContent_.find('.burn');
this.#$select_ = $dialogContent_.find('.type');
this.#$select_.select2({
data: [],
minimumResultsForSearch: Infinity,
width: '380px',
dropdownCssClass: 'mixly-scrollbar'
});
this.addEventsType(['burn']);
this.#addEventsListener_();
}
#addEventsListener_() {
this.#$cancel_.click(() => {
this.hide();
});
this.#$burn_.click(() => {
this.hide();
this.runEvent('burn', this.#map_[this.#$select_.val()]);
});
}
setMenu(items) {
this.#$select_.empty();
for (let item of items) {
const newOption = new window.Option(item.text, item.id);
this.#$select_.append(newOption);
}
this.#$select_.trigger('change');
}
setMap(firmwareMap) {
this.#map_ = firmwareMap;
}
dispose() {
this.#$select_.select2('destroy');
this.#$select_.remove();
this.#$select_ = null;
this.#$cancel_.remove();
this.#$cancel_ = null;
this.#$burn_.remove();
this.#$burn_ = null;
this.#$dialogContent_.remove();
this.#$dialogContent_ = null;
this.#map_ = null;
super.dispose();
}
}
Mixly.LayerFirmware = LayerFirmware;
});

View File

@@ -0,0 +1,76 @@
goog.loadJs('common', () => {
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Layer');
goog.require('Mixly.HTMLTemplate');
goog.provide('Mixly.LayerNewFile');
const {
Env,
Msg,
Layer,
HTMLTemplate
} = Mixly;
class LayerNewFile extends Layer {
static {
HTMLTemplate.add(
'html/dialog/new-file.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/dialog/new-file.html')))
);
}
#dialog_ = null;
#$dialogContent_ = null;
#$cancel_ = null;
#$ok_ = null;
constructor(config = {}, shadowType = 'all') {
const $dialogContent_ = $(HTMLTemplate.get('html/dialog/new-file.html').render({
message: Msg.Lang['file.emptyInfo'],
cancel: Msg.Lang['nav.btn.cancel'],
ok: Msg.Lang['nav.btn.ok']
}));
config.title = Msg.Lang['nav.btn.file.new'];
config.cancelValue = Msg.Lang['nav.btn.cancel'];
config.cancel = () => {
this.hide();
return false;
};
config.cancelDisplay = false;
config.content = $dialogContent_;
super(config, shadowType);
this.#$dialogContent_ = $dialogContent_;
this.#$cancel_ = $dialogContent_.find('.cancel');
this.#$ok_ = $dialogContent_.find('.ok');
this.addEventsType(['empty']);
this.#addEventsListener_();
}
#addEventsListener_() {
this.#$cancel_.click(() => {
this.hide();
});
this.#$ok_.click(() => {
this.hide();
this.runEvent('empty');
});
}
dispose() {
this.#$cancel_.remove();
this.#$cancel_ = null;
this.#$ok_.remove();
this.#$ok_ = null;
this.#$dialogContent_.remove();
this.#$dialogContent_ = null;
super.dispose();
}
}
Mixly.LayerNewFile = LayerNewFile;
});

View File

@@ -0,0 +1,42 @@
goog.loadJs('common', () => {
goog.require('Mixly.Env');
goog.require('Mixly.Layer');
goog.require('Mixly.HTMLTemplate');
goog.provide('Mixly.LayerProgress');
const {
Env,
Layer,
HTMLTemplate
} = Mixly;
class LayerProgress extends Layer {
static {
HTMLTemplate.add(
'html/dialog/progress.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/dialog/progress.html')))
);
}
#dialog_ = null;
#$dialogContent_ = null;
constructor(config = {}, shadowType = 'nav') {
const $dialogContent_ = $(HTMLTemplate.get('html/dialog/progress.html').render());
config.content = $dialogContent_;
super(config, shadowType);
this.#$dialogContent_ = $dialogContent_;
}
dispose() {
this.#$dialogContent_.remove();
this.#$dialogContent_ = null;
super.dispose();
}
}
Mixly.LayerProgress = LayerProgress;
});

View File

@@ -0,0 +1,91 @@
goog.loadJs('common', () => {
goog.require('dialog');
goog.require('Mixly.Env');
goog.require('Mixly.Registry');
goog.require('Mixly.Component');
goog.require('Mixly.HTMLTemplate');
goog.provide('Mixly.Layer');
const {
Env,
Registry,
Component,
HTMLTemplate
} = Mixly;
class Layer extends Component {
static {
this.templates = new Registry();
this.register = function (key, value) {
this.templates.register(key, value);
}
this.register('all', (new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/dialog/shadow-all.html'))
)).render());
this.register('nav', (new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/dialog/shadow-nav.html'))
)).render());
}
#dialog_ = null;
constructor(config = {}, shadowType = 'nav') {
super();
const shadow = Layer.templates.getItem(shadowType);
this.setContent($(shadow));
config.skin = config.skin ?? 'layui-anim layui-anim-scale';
this.#dialog_ = dialog({
padding: 0,
...config
});
}
show() {
this.mountOn($(window.document.body));
this.#dialog_.show();
}
hide() {
this.getContent().detach();
this.#dialog_.close();
this.onUnmounted();
}
title(text) {
this.#dialog_.title(text);
}
width(value) {
this.#dialog_.width(value);
}
height(value) {
this.#dialog_.height(value);
}
reset() {
this.#dialog_.reset();
}
focus() {
this.#dialog_.focus();
}
blur() {
this.#dialog_.blur();
}
dispose() {
this.#dialog_.remove();
this.#dialog_ = null;
super.dispose();
}
}
Mixly.Layer = Layer;
});

View File

@@ -0,0 +1,148 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('d3');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.Boards');
goog.require('Mixly.App');
goog.require('Mixly.Msg');
goog.require('Mixly.UserEvents');
goog.require('Mixly.UserOPEvents');
goog.require('Mixly.JSFuncs');
goog.require('Mixly.Title');
goog.require('Mixly.LocalStorage');
goog.require('Mixly.Storage');
goog.require('Mixly.Debug');
goog.require('Mixly.API2');
goog.require('Mixly.Electron.LibManager');
goog.require('Mixly.Electron.File');
goog.require('Mixly.WebCompiler.Loader');
goog.require('Mixly.WebSocket.Loader');
goog.provide('Mixly.Loader');
const {
Config,
Boards,
Loader,
Env,
App,
Msg,
UserEvents,
UserOPEvents,
Title,
LocalStorage,
Storage,
Debug,
API2,
Electron = {},
Web = {},
WebCompiler = {},
WebSocket = {}
} = Mixly;
const { LibManager, File } = goog.isElectron? Electron : Web;
const { USER } = Config;
window.addEventListener('load', () => {
if (!goog.isElectron) {
if (Env.hasSocketServer) {
WebSocket.Loader.init();
} else if (Env.hasCompiler) {
WebCompiler.Loader.init();
}
}
const app = new App($('body')[0]);
Mixly.app = app;
const $xml = $(goog.readFileSync(Env.boardIndexPath));
let scrpitPaths = [];
let cssPaths = [];
let $categories = null;
for (let i = 0; i < $xml.length; i++) {
const $xmli = $($xml[i]);
let rePath = '';
switch ($xml[i].nodeName) {
case 'SCRIPT':
rePath = $xmli.attr('src');
rePath && scrpitPaths.push(path.join(Env.boardDirPath, rePath));
break;
case 'LINK':
rePath = $xmli.attr('href');
rePath && cssPaths.push(path.join(Env.boardDirPath, rePath));
break;
case 'XML':
$categories = $xmli;
break;
}
}
$categories && $('#toolbox').html($categories.html());
cssPaths.length && LazyLoad.css(cssPaths);
if (scrpitPaths.length) {
LazyLoad.js(scrpitPaths, () => {
Loader.start();
});
} else {
Loader.start();
}
});
Loader.start = () => {
if (window.frames.length !== parent.frames.length) {
window.userEvents = new UserEvents(Editor.blockEditor);
}
if (!goog.isElectron && window.location.host.indexOf('mixly.cn')) {
window.userOpEvents = new UserOPEvents();
}
if (goog.isElectron && typeof LibManager === 'object') {
LibManager.init(() => Loader.init());
} else {
Env.defaultXML = $('#toolbox').html();
Loader.init();
}
}
Loader.init = () => {
const selectedBoardName = Boards.getSelectedBoardName();
Boards.setSelectedBoard(selectedBoardName, {});
Msg.renderToolbox(true);
Mixly.app.getNav().resize();
const workspace = Mixly.app.getWorkspace();
const editor = workspace.getEditorsManager().getActive();
if (USER.cache !== 'no') {
Loader.restoreBlocks(editor);
}
Mixly.app.removeSkeleton();
window.addEventListener('unload', () => Loader.backupBlocks(editor), false);
API2.init();
}
Loader.restoreBlocks = (editor) => {
const filePath = LocalStorage.get(`${LocalStorage.PATH['USER']}/filePath`);
const mix = Storage.board('mix');
const openedPath = Storage.board('path');
if (filePath) {
LocalStorage.set(`${LocalStorage.PATH['USER']}/filePath`, '');
goog.isElectron && File.openFile(filePath);
} else if (mix) {
try {
editor.setValue(mix, '.mix');
if (openedPath && goog.isElectron) {
File.openedFilePath = openedPath;
File.workingPath = path.dirname(openedPath);
Title.updeteFilePath(File.openedFilePath);
}
} catch (error) {
Storage.board('mix', '');
Debug.error(error);
}
}
}
Loader.backupBlocks = (editor) => {
const mix = editor.getValue();
Storage.board('mix', mix);
Storage.board('path', Title.getFilePath());
}
});

View File

@@ -0,0 +1,59 @@
goog.loadJs('common', () => {
goog.require('store');
goog.require('Mixly.MArray');
goog.provide('Mixly.LocalStorage');
const { MArray, LocalStorage } = Mixly;
LocalStorage.PATH = {
USER: 'mixly3.0/user',
BOARD: 'mixly3.0/boards/{{d.boardType}}/user',
THIRD_PARTY: 'mixly3.0/boards/{{d.boardType}}/third_party/{{d.thirdPartyName}}'
};
LocalStorage.set = function (path, value) {
let { first, last, firstKey, lastKey } = this.find(path);
if (!first) {
return;
}
last[lastKey] = value;
store.set(firstKey, first);
}
LocalStorage.get = function (path) {
let { first, last, lastKey } = this.find(path);
if (!first) {
return undefined;
}
return last[lastKey];
}
LocalStorage.getItems = function (path) {
let items = path.split('/');
MArray.remove(items, '');
return items;
}
LocalStorage.find = function (path) {
let items = this.getItems(path);
if (!items.length) {
return {};
}
let rootObj = {};
rootObj[items[0]] = store.get(items[0]);
let last = rootObj;
let value;
for (let i = 0; i < items.length - 1; i++) {
if (!(last[items[i]] instanceof Object)) {
last[items[i]] = {};
}
last = last[items[i]];
}
let first = rootObj[items[0]];
let firstKey = items[0];
let lastKey = items[items.length - 1];
return { first, last, firstKey, lastKey };
}
});

View File

@@ -0,0 +1,68 @@
goog.loadJs('common', () => {
goog.require('Mixly');
goog.provide('Mixly.MArray');
const { MArray } = Mixly;
MArray.equals = (x, y) => {
// If both x and y are null or undefined and exactly the same
if (x === y) {
return true;
}
// If they are not strictly equal, they both need to be Objects
if (!(x instanceof Object ) || !(y instanceof Object)) {
return false;
}
//They must have the exact same prototype chain,the closest we can do is
//test the constructor.
if (x.constructor !== y.constructor) {
return false;
}
for (var p in x) {
//Inherited properties were tested using x.constructor === y.constructor
if (x.hasOwnProperty(p)) {
// Allows comparing x[ p ] and y[ p ] when set to undefined
if (!y.hasOwnProperty(p)) {
return false;
}
// If they have the same strict value or identity then they are equal
if (x[p] === y[p]) {
continue;
}
// Numbers, Strings, Functions, Booleans must be strictly equal
if ( typeof(x[p]) !== "object" ) {
return false;
}
// Objects and Arrays must be tested recursively
if (!MArray.equals(x[p], y[p])) {
return false;
}
}
}
for (p in y) {
// allows x[ p ] to be set to undefined
if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
return false;
}
}
return true;
}
MArray.remove = (arr, data) => {
let arrLen = arr.length;
for (let i = 0; i < arrLen; i++) {
if (arr[i] !== data) {
continue;
}
arr.splice(i, 1);
i--;
arrLen--;
}
}
//数组去除重复元素
MArray.unique = (arr) => {
return Array.from(new Set(arr));
}
});

View File

@@ -0,0 +1,135 @@
goog.loadJs('common', () => {
goog.require('Mixly.Env');
goog.require('Mixly.Debug');
goog.require('Mixly.Events');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.HTMLTemplate');
goog.provide('Mixly.Menu');
const {
Env,
Debug,
Events,
IdGenerator,
HTMLTemplate
} = Mixly;
class Menu {
static {
HTMLTemplate.add(
'html/menu-item.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/menu-item.html')))
);
this.getItem = (name, hotKey = '', icon = '') => {
return HTMLTemplate.get('html/menu-item.html').render({ name, hotKey, icon });
};
}
#menuItems_ = [];
#ids_ = {};
#isDynamic_ = false;
#events_ = new Events(['onRead']);
constructor(isDynamic) {
this.#ids_ = {};
this.#menuItems_ = [];
this.#isDynamic_ = isDynamic;
}
add(item) {
if (!item.id) {
if (item.type) {
item.id = item.type;
} else {
item.id = IdGenerator.generate();
}
}
if (!item.weight) {
item.weight = 0;
}
const { id, weight } = item;
if (this.#ids_[id]) {
delete this.#ids_[id];
}
let i = 0;
for (; i < this.#menuItems_.length; i++) {
if (this.#menuItems_[i].weight <= weight) {
continue;
}
break;
}
this.#menuItems_.splice(i, 0, item);
this.#ids_[id] = item;
return id;
}
remove(id) {
if (!this.#ids_[id]) {
return;
}
delete this.#ids_[id];
for (let i in this.#menuItems_) {
if (this.#menuItems_[i].id !== id) {
continue;
}
this.#menuItems_.splice(i, 1);
break;
}
}
empty() {
this.#ids_ = {};
this.#menuItems_ = [];
}
hasKey(id) {
return !!this.#ids_[id];
}
getItem(id) {
return this.#ids_[id] ?? null;
}
getAllItems() {
if (this.#isDynamic_) {
this.empty();
const results = this.runEvent('onRead');
if (results?.length) {
for (let item of results[0]) {
this.add(item);
}
}
}
return this.#menuItems_;
}
bind(type, func) {
return this.#events_.bind(type, func);
}
unbind(id) {
this.#events_.unbind(id);
}
addEventsType(eventsType) {
this.#events_.addType(eventsType);
}
runEvent(eventsType, ...args) {
return this.#events_.run(eventsType, ...args);
}
offEvent(eventsType) {
this.#events_.off(eventsType);
}
resetEvent() {
this.#events_.reset();
}
}
Mixly.Menu = Menu;
});

View File

@@ -0,0 +1,388 @@
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 += `<config>${JSON.stringify(config)}</config>`;
} catch (error) {
console.log(error);
}
}
if (BOARD.saveMixWithCode) {
code = Base64.encode(code);
xmlStr += `<code>${code}</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('<br/>'),
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 = $('<input></input>');
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);
}
}
});

View File

@@ -0,0 +1,12 @@
goog.loadJs('common', () => {
goog.provide('Mixly');
Mixly.require = function(moduleName) {
if (!goog.isElectron) {
return;
}
return require(moduleName);
}
});

View File

@@ -0,0 +1,50 @@
goog.loadJs('common', () => {
goog.require('Blockly');
goog.require('Mixly');
goog.provide('Mixly.API2');
goog.provide('Mixly.Editor');
const { API2, Editor } = Mixly;
const HUES = {
ACTUATOR_HUE: 100,
BLYNK0_HUE: 0,
BLYNK1_HUE: 159,
COMMUNICATE_HUE: 140,
LOOPS_HUE: 120,
DISPLAY_HUE: 180,
ETHERNET_HUE: 0,
FACTORY_HUE: '#777777',
BASE_HUE: 20,
LISTS_HUE: 260,
LOGIC_HUE: 210,
MATH_HUE: 230,
PINS_HUE: 230,
PROCEDURES_HUE: 290,
SCOOP_HUE: 120,
SENSOR_HUE: 40,
SERIAL_HUE: 65,
STORAGE_HUE: 0,
TEXTS_HUE: 160,
TOOLS_HUE: '#555555',
VARIABLES_HUE: 330,
HANDBIT_HUE: 65
};
API2.init = () => {
const workspace = Mixly.app.getWorkspace();
const editorsManager = workspace.getEditorsManager();
const mixEditor = editorsManager.getActive();
const blockPage = mixEditor.getPage('block');
const codePage = mixEditor.getPage('code');
Blockly.mainWorkspace = blockPage.getEditor();
Editor.blockEditor = blockPage.getEditor();
Editor.codeEditor = codePage.getEditor();
Object.assign(Blockly.Msg, HUES);
Blockly.ALIGN_LEFT = Blockly.inputs.Align.LEFT;
Blockly.ALIGN_CENTRE = Blockly.inputs.Align.CENTRE;
Blockly.ALIGN_RIGHT = Blockly.inputs.Align.RIGHT;
}
});

View File

@@ -0,0 +1,68 @@
goog.loadJs('common', () => {
goog.require('Mixly.Debug');
goog.provide('Mixly.MJson');
const { Debug, MJson } = Mixly;
MJson.operate = (jsonObj, optFunc) => {
// 循环所有键
for (var key in jsonObj) {
//如果对象类型为object类型且数组长度大于0 或者 是对象 ,继续递归解析
var element = jsonObj[key];
if (element.length > 0 && typeof (element) == "object" || typeof (element) == "object") {
let data = MJson.operate(element, optFunc);
for (let i in data) {
jsonObj[key][i] = data[i];
}
} else { //不是对象或数组、直接输出
if (typeof (element) === 'string') {
try {
jsonObj[key] = optFunc(jsonObj[key]);
} catch (error) {
Debug.error(error);
}
}
}
}
return jsonObj;
}
MJson.decode = (jsonObj) => {
// 深度拷贝对象,防止解码或编码时篡改原有对象
let newJsonObj = structuredClone(jsonObj);
return MJson.operate(newJsonObj, decodeURIComponent);
}
MJson.encode = (jsonObj) => {
// 深度拷贝对象,防止解码或编码时篡改原有对象
let newJsonObj = structuredClone(jsonObj);
return MJson.operate(newJsonObj, encodeURIComponent);;
}
MJson.parse = (jsonStr) => {
let jsonObj = null;
try {
jsonStr = jsonStr.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m);
jsonObj = JSON.parse(jsonStr);
} catch (error) {
Debug.error(error);
}
return jsonObj;
}
MJson.stringify = (jsonObj) => {
let jsonStr = '';
try {
jsonStr = JSON.stringify(jsonObj);
} catch (error) {
Debug.error(error);
}
return jsonStr;
}
MJson.get = (inPath) => {
return goog.readJsonSync(inPath);
}
});

View File

@@ -0,0 +1,119 @@
goog.loadJs('common', () => {
goog.require('monaco');
goog.require('Mixly.Registry');
goog.provide('Mixly.MonacoTheme');
const { Registry } = Mixly;
class MonacoTheme {
static {
this.cssClassNamePrefix = 'mts';
this.themesRegistry = new Registry();
this.supportThemes = [];
// this.supportThemes = ['cpp', 'python'];
this.getClassNameOfTerm = function (type, theme, term) {
return `${this.cssClassNamePrefix}-${type}-${theme}-${term}`;
}
/*this.themesRegistry.register('vs-dark-cpp', new MonacoTheme(
'vs-dark-cpp',
'cpp',
'dark',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/dark-cpp.json')
));
this.themesRegistry.register('vs-light-cpp', new MonacoTheme(
'vs-light-cpp',
'cpp',
'light',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/light-cpp.json')
));
this.themesRegistry.register('vs-dark-python', new MonacoTheme(
'vs-dark-python',
'python',
'dark',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/dark-python.json')
));
this.themesRegistry.register('vs-light-python', new MonacoTheme(
'vs-light-python',
'python',
'light',
goog.readJsonSync('../common/templates/json/tree-sitter/themes/light-python.json')
));*/
}
#id_ = null;
#type_ = null;
#theme_ = null;
#$tag_ = null;
#config_ = null;
constructor(id, type, theme, config) {
this.#id_ = id;
this.#type_ = type;
this.#theme_ = theme;
this.load(config);
}
load(config) {
monaco.editor.defineTheme(this.#id_, config.base);
this.#config_ = config;
if (!this.#$tag_) {
let hasStyleNode = $('head').find(`style[style-id='${this.id_}']`).length;
if (hasStyleNode) {
return;
}
this.#$tag_ = $('<style></style>');
this.#$tag_.attr('style-id', this.id);
this.#$tag_.attr('type', 'text/css')
$('head').append(this.#$tag_);
}
this.#$tag_.html(this.generateCss());
}
generateCss() {
return Object.keys(this.#config_.monacoTreeSitter)
.map(term =>
`span.${MonacoTheme.getClassNameOfTerm(this.#type_, this.#theme_, term)}{${this.generateStyleOfTerm(term)}}`
)
.join('');
}
generateStyleOfTerm(term) {
const style = this.#config_.monacoTreeSitter[term];
if (!style) {
return '';
}
if (typeof style === 'string') {
return `color:${style}`;
}
return `color:${style.color};${style.extraCssStyles || ''}`;
}
getColorOfTerm(term) {
const style = this.#config_.monacoTreeSitter[term];
if (!style) {
return undefined;
}
return typeof style === 'object' ? style.color : style;
}
dispose() {
this.#$tag_ && this.#$tag_.remove();
this.#$tag_ = null;
this.#config_ = null;
}
}
Mixly.MonacoTheme = MonacoTheme;
});

View File

@@ -0,0 +1,127 @@
goog.loadJs('common', () => {
goog.require('_');
goog.require('monaco');
goog.require('Mixly.Registry');
goog.require('Mixly.MonacoTheme');
goog.provide('Mixly.MonacoTreeSitter');
const { Registry, MonacoTheme } = Mixly;
class MonacoTreeSitter {
static {
this.workerPath = '../common/modules/mixly-modules/workers/common/tree-sitter/index.js';
this.supportTreeSitters_ = new Registry();
this.activeTreeSitters_ = new Registry();
/*this.supportTreeSitters_.register('python', {
workerName: 'pythonTreeSitterService',
wasm: 'tree-sitter-python.wasm'
});
this.supportTreeSitters_.register('cpp', {
workerName: 'cppTreeSitterService',
wasm: 'tree-sitter-cpp.wasm'
});*/
this.activateTreeSitter = async function (type) {
if (!this.supportTreeSitters_.hasKey(type)) return null;
const info = this.supportTreeSitters_.getItem(type);
if (this.activeTreeSitters_.hasKey(type)) {
const ts = this.activeTreeSitters_.getItem(type);
if (ts.loading) await ts.loading;
return ts;
}
const treeSitter = workerpool.pool(this.workerPath, {
workerOpts: { name: info.workerName },
workerType: 'web'
});
const grammar = await goog.readJson(
`../common/templates/json/tree-sitter/grammars/${type}.json`
);
treeSitter.loading = treeSitter.exec(
'init',
[info.wasm, grammar]
);
this.activeTreeSitters_.register(type, treeSitter);
await treeSitter.loading;
treeSitter.loading = null;
return treeSitter;
};
this.treeSitterPostion = function (pos) {
return {
row: pos.lineNumber - 1,
column: pos.column - 1
};
};
}
constructor(editor, opts) {
this.editor = editor;
this.seq = 0;
this.decorations = [];
this.refresh = _.debounce(
this.refresh.bind(this),
opts?.debounceUpdate ?? 15
);
}
dispose() {
this.pool.terminate(true);
this.decorations = [];
}
async updateWorker(type, theme, text) {
const treeSitter = await MonacoTreeSitter.activateTreeSitter(type);
if (!treeSitter) {
return;
}
const id = ++this.seq;
const dto = await treeSitter.exec('update', [text]);
if (id !== this.seq) return;
this.applyDecorations(type, theme, dto);
}
applyDecorations(type, theme, dto) {
const decos = [];
for (const [term, ranges] of Object.entries(dto)) {
const className = MonacoTheme.getClassNameOfTerm(type, theme, term);
for (const r of ranges) {
decos.push({
range: new monaco.Range(
r.startLineNumber,
r.startColumn,
r.endLineNumber,
r.endColumn
),
options: {
inlineClassName: className
}
});
}
}
this.decorations = this.editor.getEditor().deltaDecorations(this.decorations, decos);
}
refresh(type, theme, newText) {
this.updateWorker(type, theme, newText);
}
setValue(type, theme, newText) {
this.refresh(type, theme, newText);
}
}
Mixly.MonacoTreeSitter = MonacoTreeSitter;
});

View File

@@ -0,0 +1,85 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.MJson');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Blockly');
goog.require('Blockly.Lang.ZhHans');
goog.require('Blockly.Lang.ZhHant');
goog.require('Blockly.Lang.En');
goog.provide('Mixly.Msg');
const {
Msg,
MJson,
Config,
Env
} = Mixly;
const { USER } = Config;
const {
ZhHans,
ZhHant,
En
} = Blockly.Lang;
Msg.LANG_TYPE = ['zh-hans', 'zh-hant', 'en'];
Msg.LANG = {
'zh-hans': MJson.get(path.join(Env.msgPath, 'mixly/zh-hans.json')),
'zh-hant': MJson.get(path.join(Env.msgPath, 'mixly/zh-hant.json')),
'en': MJson.get(path.join(Env.msgPath, 'mixly/en.json'))
};
Msg.BLOCKLY_LANG = {
'zh-hans': ZhHans,
'zh-hant': ZhHant,
'en': En
};
Msg.BLOCKLY_LANG_DEFAULT = {
'zh-hans': MJson.get(path.join(Env.msgPath, 'blockly/default/zh-hans.json')),
'zh-hant': MJson.get(path.join(Env.msgPath, 'blockly/default/zh-hant.json')),
'en': MJson.get(path.join(Env.msgPath, 'blockly/default/en.json'))
};
Msg.nowLang = USER.language ?? 'zh-hans';
Msg.blocklyDefault = Blockly.Msg;
Msg.getLang = (str) => {
return Msg.LANG[Msg.nowLang][str] ?? '';
}
Msg.changeTo = (lang) => {
lang = Msg.LANG_TYPE.includes(lang) ? lang : 'zh-hans';
Msg.nowLang = lang;
Msg.Lang = Msg.LANG[lang];
Blockly.Msg = Msg.BLOCKLY_LANG[lang];
Object.assign(Msg.blocklyDefault, Msg.BLOCKLY_LANG_DEFAULT[lang]);
}
Msg.renderToolbox = (addToolboxitemid = false) => {
let $categories = $('#toolbox').find('category');
for (let i = 0; i < $categories.length; i++) {
let { id } = $categories[i];
if (!Blockly.Msg.MSG[id]) {
continue;
}
let $category = $($categories[i]);
$category.attr('name', Blockly.Msg.MSG[id]);
if (addToolboxitemid) {
if ($category.attr('toolboxitemid')) {
continue;
}
$category.attr({
'toolboxitemid': id,
'name': Blockly.Msg.MSG[id]
});
} else {
$(`span[id="${id}.label"]`).html(Blockly.Msg.MSG[id]);
}
}
}
Msg.changeTo(Msg.nowLang);
});

View File

@@ -0,0 +1,76 @@
goog.loadJs('common', () => {
goog.require('Mixly.Debug');
goog.provide('Mixly.MString');
const { Debug, MString } = Mixly;
/**
* @function 使用传入值替换字符串中{xxx}
* @param str {string} 传入字符串
* @param obj {object}
* obj = {
* xxx: value1
* xxx: value2
* }
* 使用value替换{xxx}
* @return {string} 返回处理后的字符串
**/
MString.tpl = (str, obj) => {
if (typeof str !== 'string' || !(obj instanceof Object)) {
return str;
}
for (let key in obj) {
let re = new RegExp("{[\s]*" + key + "[\s]*}", "gim");
str = str.replace(re, obj[key]);
}
return str;
}
MString.decode = (str) => {
try {
str = unescape(str.replace(/(_E[0-9A-F]{1}_[0-9A-F]{2}_[0-9A-F]{2})+/gm, '%$1'));
str = unescape(str.replace(/\\(u[0-9a-fA-F]{4})/gm, '%$1'));
} catch (error) {
Debug.error(error);
}
return str;
}
MString.strToByte = (str) => {
var len, c;
len = str.length;
var bytes = [];
for (var i = 0; i < len; i++) {
c = str.charCodeAt(i);
if (c >= 0x010000 && c <= 0x10FFFF) {
bytes.push(((c >> 18) & 0x07) | 0xF0);
bytes.push(((c >> 12) & 0x3F) | 0x80);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000800 && c <= 0x00FFFF) {
bytes.push(((c >> 12) & 0x0F) | 0xE0);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000080 && c <= 0x0007FF) {
bytes.push(((c >> 6) & 0x1F) | 0xC0);
bytes.push((c & 0x3F) | 0x80);
} else {
bytes.push(c & 0xFF);
}
}
return new Int8Array(bytes);
}
MString.uint8ArrayToStr = (fileData) => {
var dataString = "";
for (var i = 0; i < fileData.length; i++) {
var convert = (fileData[i]).toString(16);
if (convert.length % 2 == 1)
convert = "0" + convert;
dataString = dataString + " " + convert.toUpperCase();
}
return dataString;
}
});

View File

@@ -0,0 +1,464 @@
goog.loadJs('common', () => {
goog.require('layui');
goog.require('$.select2');
goog.require('Mixly.Env');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Component');
goog.require('Mixly.DropdownMenuGroup');
goog.provide('Mixly.Nav');
const {
Env,
XML,
Msg,
HTMLTemplate,
Component,
DropdownMenuGroup
} = Mixly;
const { element } = layui;
class Nav extends Component {
static {
/**
* nav容器html片段
* @type {String}
*/
HTMLTemplate.add(
'html/nav/nav.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/nav/nav.html')))
);
/**
* nav按钮html片段
* @type {String}
*/
HTMLTemplate.add(
'html/nav/nav-btn.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/nav/nav-btn.html')))
);
/**
* nav子元素容器html片段
* @type {String}
*/
HTMLTemplate.add(
'html/nav/nav-item-container.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/nav/nav-item-container.html')))
);
/**
* nav子元素html片段
* @type {String}
*/
HTMLTemplate.add(
'html/nav/nav-item.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/nav/nav-item.html')))
);
/**
* 板卡选择器html片段
* @type {String}
*/
HTMLTemplate.add(
'html/nav/board-selector-div.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/nav/board-selector-div.html')))
);
/**
* 端口选择器html片段
* @type {String}
*/
HTMLTemplate.add(
'html/nav/port-selector-div.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/nav/port-selector-div.html')))
);
/**
* 下拉菜单遮罩
* @type {String}
*/
HTMLTemplate.add(
'html/nav/shadow.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/nav/shadow.html')))
);
Nav.Scope = {
'LEFT': -1,
'CENTER': 0,
'RIGHT': 1,
'-1': 'LEFT',
'0': 'CENTER',
'1': 'RIGHT'
};
this.navs = [];
this.getAll = () => {
return this.navs;
}
this.add = (nav) => {
this.remove(nav);
this.navs.push(nav);
}
this.remove = (nav) => {
for (let i in this.workspaces) {
if (this.navs[i].id !== nav.id) {
continue;
}
this.navs.slice(i, 1);
}
}
this.getMain = () => {
if (this.navs.length) {
return this.navs[0];
}
return null;
}
}
#$container_ = null;
#$leftBtnContainer_ = null;
#$leftBtnExtContainer_ = null;
#$rightBtnContainer_ = null;
#$dropdownContainer_ = null;
#$rightMenuContainer_ = null;
#$rightArea_ = null;
#$editorBtnsContainer_ = null;
#$boardSelect_ = null;
#$portSelect_ = null;
#$shadow_ = $(HTMLTemplate.get('html/nav/shadow.html').render());
#btns_ = [];
#rightDropdownMenuGroup_ = null;
constructor() {
super();
const navTemplate = HTMLTemplate.get('html/nav/nav.html');
this.setId(navTemplate.getId());
this.#$container_ = $(navTemplate.render({
more: Msg.Lang['nav.more']
}));
this.setContent(this.#$container_);
this.#$leftBtnContainer_ = this.#$container_.find('.left-btn-container');
this.#$leftBtnExtContainer_ = this.#$container_.find('.left-btn-ext-container');
this.#$rightBtnContainer_ = this.#$container_.find('.right-btn-container');
this.#$dropdownContainer_ = this.#$container_.find('.dropdown-container');
this.#$rightMenuContainer_ = this.#$container_.find('.right-menu-container');
this.#$rightArea_ = this.#$container_.find('.right-area');
this.#$editorBtnsContainer_ = this.#$container_.find('.editor-btn-container');
const boardSelectTemplate = HTMLTemplate.get('html/nav/board-selector-div.html');
this.#$boardSelect_ = $(boardSelectTemplate.render());
this.#$dropdownContainer_.append(this.#$boardSelect_);
this.#$boardSelect_.select2({
width: '150px',
minimumResultsForSearch: 10,
dropdownCssClass: `mixly-scrollbar mixly-${boardSelectTemplate.getId()}`,
dropdownAutoWidth: true,
placeholder: Msg.Lang['nav.selectBoard'],
language: Msg.nowLang
});
const portSelectTemplate = HTMLTemplate.get('html/nav/port-selector-div.html');
this.#$portSelect_ = $(portSelectTemplate.render());
this.#$dropdownContainer_.append(this.#$portSelect_);
this.#$portSelect_.select2({
width: '100px',
minimumResultsForSearch: Infinity,
dropdownCssClass: `mixly-scrollbar mixly-${portSelectTemplate.getId()}`,
dropdownAutoWidth: true,
placeholder: Msg.Lang['nav.selectPort']
});
const $merge = this.#$boardSelect_.add(this.#$portSelect_);
let count = 0;
$merge.on('select2:opening', (event) => {
count += 1;
$(document.body).append(this.#$shadow_);
});
$merge.on('select2:closing', () => {
count -= 1;
!count && this.#$shadow_.detach();
});
this.#$shadow_.click(() => $merge.select2('close'));
this.addEventsType(['changeBoard', 'changePort']);
this.#addEventsListener_();
this.#rightDropdownMenuGroup_ = new DropdownMenuGroup(this.#$rightMenuContainer_[0]);
Nav.add(this);
this.list = [];
}
onMounted() {
super.onMounted();
element.render('nav', 'nav-filter');
}
getEditorBtnsContainer() {
return this.#$editorBtnsContainer_;
}
getBoardSelector() {
return this.#$boardSelect_;
}
getPortSelector() {
return this.#$portSelect_;
}
getBoardName() {
return this.#$boardSelect_.find(':selected').text();
}
getBoardKey() {
return this.#$boardSelect_.val();
}
getPortName() {
return this.#$portSelect_.find(':selected').text();
}
getPortKey() {
return this.#$portSelect_.val();
}
/**
* @function 注册函数
* @param config 选项
* {
* icon: String,
* title: String,
* id: String | Array,
* displayText: String,
* preconditionFn: Function,
* callback: Function,
* scopeType: Nav.SCOPE_TYPE,
* weight: Number
* }
* @return {void}
**/
register(config) {
const { scopeType = Nav.ScopeType.LEFT } = config;
config = {
preconditionFn: () => true,
...config
};
const {
id = '',
title = '',
icon = '',
displayText = ''
} = config;
switch (scopeType) {
case Nav.Scope.LEFT:
config.$moreBtn = $(HTMLTemplate.get('html/nav/nav-item.html').render({
mId: id,
icon,
text: displayText
}));
case Nav.Scope.CENTER:
config.$btn = $(HTMLTemplate.get('html/nav/nav-btn.html').render({
title,
mId: id,
icon,
text: displayText
}));
break;
}
this.#add_(config);
return config;
}
#add_(config) {
const {
scopeType = Nav.ScopeType.LEFT,
id = ''
} = config;
switch (scopeType) {
case Nav.Scope.LEFT:
if (id === 'home-btn') {
this.#btns_[config.id] = config;
} else {
this.#addLeftBtn_(config);
}
break;
case Nav.Scope.CENTER:
this.#addCenterBtn_(config);
break;
case Nav.Scope.RIGHT:
this.#addRightBtn_(config);
break;
}
}
/**
* @function 取消注册函数
* @param config 选项
* {
* id: String | Array,
* scopeType: Nav.SCOPE_TYPE
* }
* @return {void}
**/
unregister(config) {
}
#getElemWidth_(elem) {
const display = elem.css('display');
if (display !== 'none') {
return elem.outerWidth(true);
}
const visibility = elem.css('visibility');
const position = elem.css('position');
elem.css({
display: 'block',
visibility: 'hidden',
position: 'absolute'
});
const width = elem.outerWidth(true);
elem.css({ display, visibility, position });
return width;
}
#addEventsListener_() {
$(document).off('click', '.mixly-nav')
.on('click', '.mixly-nav', (event) => {
const mId = $(event.currentTarget).attr('m-id');
if (this.#btns_[mId]
&& typeof this.#btns_[mId].callback === 'function') {
this.#btns_[mId].callback(this);
}
});
this.#$boardSelect_.on('select2:select', (event) => {
const { data } = event.params;
this.runEvent('changeBoard', data);
});
this.#$portSelect_.on('select2:select', (event) => {
const { data } = event.params;
this.runEvent('changePort', data);
$('#mixly-footer-port-div').css('display', 'inline-flex');
$('#mixly-footer-port').html(data.id);
});
}
#addLeftBtn_(config) {
const { id = '', weight = 0 } = config;
if (Object.keys(this.#btns_).includes(id)) {
this.#btns_[id].$btn.remove();
this.#btns_[id].$moreBtn.remove();
delete this.#btns_[id];
}
let $btn = null;
let $moreBtn = null;
const $btns = this.#$leftBtnContainer_.children('button');
for (let i = 0; $btns[i]; i++) {
const mId = $($btns[i]).attr('m-id');
if (!this.#btns_[mId]) {
continue;
}
if (weight < this.#btns_[mId].weight) {
$btn = this.#btns_[mId].$btn;
$moreBtn = this.#btns_[mId].$moreBtn;
break;
}
}
if ($btn) {
$btn.before(config.$btn);
$moreBtn.before(config.$moreBtn);
} else {
this.#$leftBtnContainer_.append(config.$btn);
this.#$leftBtnExtContainer_.append(config.$moreBtn);
}
config.width = this.#getElemWidth_(config.$btn);
this.#btns_[id] = config;
this.resize();
}
#addCenterBtn_(config) {
const { id = '', weight = 0 } = config;
let $btn = null;
const $btns = this.#$rightBtnContainer_.children('button');
for (let i = 0; $btns[i]; i++) {
const mId = $($btns[i]).attr('m-id');
if (!this.#btns_[mId]) {
continue;
}
if (weight < this.#btns_[mId].weight) {
$btn = this.#btns_[mId].$btn;
break;
}
}
if ($btn) {
$btn.before(config.$btn);
} else {
this.#$rightBtnContainer_.append(config.$btn);
}
config.width = this.#getElemWidth_(config.$btn);
this.#btns_[id] = config;
this.resize();
}
#addRightBtn_(config) {
const { preconditionFn } = config;
if (!preconditionFn()) {
return;
}
this.#rightDropdownMenuGroup_.add(config);
this.resize();
}
resize() {
super.resize();
this.#$boardSelect_.select2('close');
this.#$portSelect_.select2('close');
const navRightWidth = this.#getElemWidth_(this.#$rightArea_);
const navWidth = this.#getElemWidth_(this.#$container_);
const $btns = this.#$leftBtnContainer_.children('button');
let nowWidth = navRightWidth;
let showMoreBtnContainer = false;
for (let i = 0; $btns[i]; i++) {
const mId = $($btns[i]).attr('m-id');
if (mId === 'home-btn') {
continue;
}
const config = this.#btns_[mId];
let newWidth = nowWidth;
if (config) {
const { preconditionFn } = config;
if (!preconditionFn()) {
config.$btn.css('display', 'none');
config.$moreBtn.css('display', 'none');
continue;
}
newWidth += config.width;
} else {
newWidth += this.#getElemWidth_($($btns[i]));
continue;
}
if (navWidth < newWidth + this.#$editorBtnsContainer_.outerWidth(true) + 130) {
config.$btn.css('display', 'none');
config.$moreBtn.css('display', 'block');
showMoreBtnContainer = true;
} else {
config.$btn.css('display', 'block');
config.$moreBtn.css('display', 'none');
nowWidth = newWidth;
}
}
if (navWidth < nowWidth + this.#$editorBtnsContainer_.outerWidth(true) + 130) {
showMoreBtnContainer = false;
}
const parent = this.#$leftBtnExtContainer_.parent();
parent.css('display', showMoreBtnContainer? 'block' : 'none');
}
}
Mixly.Nav = Nav;
});

View File

@@ -0,0 +1,159 @@
goog.loadJs('common', () => {
goog.require('Mixly.Component');
goog.require('Mixly.Events');
goog.require('Mixly.Registry');
goog.require('Mixly.Component');
goog.provide('Mixly.PageBase');
const {
Events,
Registry,
Component
} = Mixly;
class PageBase extends Component {
#pages_ = new Registry();
#$tab_ = null;
#$close_ = null;
#dirty_ = false;
#active_ = true;
#inited_ = false;
constructor() {
super();
this.addEventsType(['created', 'addDirty', 'removeDirty', 'active']);
}
init() {
this.#forward_('init');
this.#inited_ = true;
this.runEvent('created');
}
addPage($child, id, page) {
this.#pages_.register(id, page);
$child.append(page.getContent());
}
removePage(id) {
const page = this.getPage(id);
if (!page) {
return;
}
this.#pages_.unregister(id);
page.dispose();
}
getPage(id) {
return this.#pages_.getItem(id);
}
mountPage($child, id, page) {
if (!this.isMounted()) {
return;
}
this.addPage($child, id, page);
page.onMounted();
}
resize() {
super.resize();
this.#forward_('resize');
}
dispose() {
this.#forward_('dispose');
this.#pages_.reset();
this.#$close_ = null;
this.#$tab_ && this.#$tab_.remove();
this.#$tab_ = null;
super.dispose();
}
#forward_(type, ...args) {
this.#pages_.getAllItems().forEach((page) => {
if (typeof page[type] !== 'function') {
return;
}
page[type](...args);
});
}
onMounted() {
super.onMounted();
this.#forward_('onMounted');
this.setActive(true);
this.runEvent('active');
}
onUnmounted() {
super.onUnmounted();
this.#forward_('onUnmounted');
this.setActive(false);
}
setActive(status) {
this.#forward_('setActive', status);
this.#active_ = status;
}
setTab($tab) {
this.#$tab_ = $tab;
this.#$close_ = $tab.find('.chrome-tab-close');
}
hideCloseBtn() {
this.#$close_.css('display', 'none');
}
showCloseBtn() {
this.#$close_.css('display', 'block');
}
getTab() {
return this.#$tab_;
}
setMarkStatus(styleClass) {
this.#$close_.attr('class', `chrome-tab-close layui-badge-dot ${styleClass}`);
}
addDirty() {
this.#forward_('addDirty');
const $tab = this.getTab();
if (!$tab || this.isDirty()) {
return;
}
$tab.addClass('dirty');
this.runEvent('addDirty');
this.#dirty_ = true;
}
removeDirty() {
this.#forward_('removeDirty');
const $tab = this.getTab();
if (!$tab || !this.isDirty()) {
return;
}
this.runEvent('removeDirty');
$tab.removeClass('dirty');
this.#dirty_ = false;
}
isDirty() {
return this.#dirty_;
}
isInited() {
return this.#inited_;
}
isActive() {
return this.#active_;
}
}
Mixly.PageBase = PageBase;
});

View File

@@ -0,0 +1,212 @@
goog.loadJs('common', () => {
goog.require('Mixly.PagesTab');
goog.require('Mixly.Registry');
goog.require('Mixly.Component');
goog.provide('Mixly.PagesManager');
const {
PagesTab,
Registry,
Component
} = Mixly;
class PagesManager extends Component {
#tabs_ = null;
#welcomePage_ = null;
#$editorContainer_ = null;
#$tabsContainer_ = null;
#$parentContainer_ = null;
#pagesRegistry_ = new Registry();
#page_ = 'welcome';
#activeId_ = null;
#typesRegistry_ = null;
/**
* config = {
* parentElem: element,
* managerContentElem: element,
* bodyElem: element,
* tabElem: element,
* tabContentElem: element,
* typesRegistry: Mixly.Registry
* }
**/
constructor(config) {
super();
this.#$parentContainer_ = $(config.parentElem);
const $content = $(config.managerContentElem);
this.#typesRegistry_ = config.typesRegistry;
this.#$tabsContainer_ = $(config.tabElem);
this.#$editorContainer_ = $(config.bodyElem);
this.#tabs_ = new PagesTab({
parentElem: config.tabElem,
contentElem: config.tabContentElem
});
$content.append(this.#$editorContainer_);
this.setContent($content);
this.mountOn(this.#$parentContainer_);
let PageType = this.#typesRegistry_.getItem('#welcome');
if (PageType) {
this.#welcomePage_ = new PageType();
this.#showWelcomePage_();
}
this.#addEventsListener_();
}
#addEventsListener_() {
const pageTabs = this.getTabs();
// active Tab被改变时触发
pageTabs.bind('activeTabChange', (event) => {
const prevEditor = this.getActive();
const { tabEl } = event.detail;
const $tab = $(tabEl);
const id = $tab.attr('data-tab-id');
$tab.find('.chrome-tab-favicon').addClass('active');
const page = this.get(id);
this.#activeId_ = id;
if (prevEditor) {
const $prevTab = prevEditor.getTab();
$prevTab.find('.chrome-tab-favicon').removeClass('active');
prevEditor.unmount();
}
this.#$editorContainer_.empty();
this.#$editorContainer_.append(page.getContent());
page.onMounted();
page.resize();
});
// 添加新Tab时触发
pageTabs.bind('tabCreated', (event) => {
const { tabEl } = event.detail;
const id = $(tabEl).attr('data-tab-id');
const type = $(tabEl).attr('data-tab-type');
let PageType = this.#typesRegistry_.getItem(type);
if (!PageType) {
PageType = this.#typesRegistry_.getItem('#default');
}
let page = new PageType();
this.#pagesRegistry_.register(id, page);
page.setTab($(tabEl));
if (this.#welcomePage_
&& this.#pagesRegistry_.length()
&& this.#page_ === 'welcome') {
this.#hideWelcomePage_();
}
page.init();
});
// 移除已有Tab时触发
pageTabs.bind('tabDestroyed', (event) => {
const { tabEl } = event.detail;
const id = $(tabEl).attr('data-tab-id');
const page = this.get(id);
if (!page) {
return;
}
page.dispose();
this.#pagesRegistry_.unregister(id);
if (this.#welcomePage_
&& !this.#pagesRegistry_.length()
&& this.#page_ !== 'welcome') {
this.#showWelcomePage_();
}
});
}
#showWelcomePage_() {
this.unmount();
this.#welcomePage_.mountOn(this.#$parentContainer_);
this.#page_ = 'welcome';
}
#hideWelcomePage_() {
this.#welcomePage_.unmount();
this.mountOn(this.#$parentContainer_);
this.#page_ = 'editor';
}
resize() {
super.resize();
const page = this.getActive();
page && page.resize();
this.#tabs_.resize();
}
onMounted() {
super.onMounted();
const page = this.getActive();
page && page.onMounted();
this.#tabs_.onMounted();
}
onUnmounted() {
const page = this.getActive();
page && page.onUnmounted();
this.#tabs_.onUnmounted();
super.onUnmounted();
}
getActive() {
return this.get(this.#activeId_);
}
add(...args) {
if (args[0] && typeof args[0] === 'object') {
this.#tabs_.addTab(args[0]);
} else {
const [type, id, name, title, favicon] = args;
this.#tabs_.addTab({
type,
id,
name: name ?? id,
title: title ?? id,
favicon
});
}
}
remove(id) {
this.#tabs_.removeTab(id);
}
changeTo(id) {
if (!this.get(id)) {
return;
}
this.#tabs_.setCurrentTab(id);
}
get(id) {
return this.#pagesRegistry_.getItem(id);
}
keys() {
return this.#pagesRegistry_.keys();
}
length() {
return this.#pagesRegistry_.length();
}
getTabs() {
return this.#tabs_;
}
dispose() {
for (let id of this.keys()) {
this.remove(id);
}
this.#tabs_.dispose();
this.#welcomePage_ && this.#welcomePage_.dispose();
this.#tabs_ = null;
this.#welcomePage_ = null;
this.#$tabsContainer_ = null;
this.#$editorContainer_ = null;
super.dispose();
}
}
Mixly.PagesManager = PagesManager;
});

View File

@@ -0,0 +1,164 @@
goog.loadJs('common', () => {
goog.require('_');
goog.require('path');
goog.require('ChromeTabs');
goog.require('XScrollbar');
goog.require('Sortable');
goog.require('$.contextMenu');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.Config');
goog.require('Mixly.Events');
goog.require('Mixly.Registry');
goog.require('Mixly.Component');
goog.provide('Mixly.PagesTab');
const {
IdGenerator,
Config,
Events,
Registry,
Component
} = Mixly;
const { USER } = Config;
class PagesTab extends Component {
#chromeTabs_ = null;
#scrollbar_ = null;
#tabsRegistry_ = null;
#sortable_ = null;
/**
* config = {
* parentElem: element,
* contentElem: element
* }
**/
constructor(config) {
super();
const $parentsContainer = $(config.parentElem);
const $content = $(config.contentElem);
this.$tabsContainer = $content.children('div');
this.#chromeTabs_ = new ChromeTabs();
this.#chromeTabs_.init(this.$tabsContainer[0]);
this.#scrollbar_ = new XScrollbar($content.find('.chrome-tabs-content')[0], {
onlyHorizontal: true,
thumbSize: 1.7,
thumbRadius: 0,
thumbBackground: USER.theme === 'dark'? '#b0b0b0' : '#5f5f5f'
});
this.#sortable_ = new Sortable(this.#chromeTabs_.tabContentEl, {
animation: 150,
ghostClass: 'blue-background-class',
direction: 'horizontal'
});
this.setContent($content);
this.mountOn($parentsContainer);
this.#addEventsListener_();
this.#tabsRegistry_ = new Registry();
this.addEventsType(['activeTabChange', 'tabCreated', 'tabDestroyed', 'tabCheckDestroy', 'tabBeforeDestroy']);
}
#addEventsListener_() {
const { $tabsContainer } = this;
const container = $tabsContainer[0];
this.#chromeTabs_.checkDestroy = (event) => {
const status = this.runEvent('tabCheckDestroy', event);
return _.sum(status) == status.length;
}
// active Tab被改变时触发
container.addEventListener('activeChange', (event) => {
this.runEvent('activeTabChange', event);
});
// 添加新Tab时触发
container.addEventListener('created', (event) => {
const { tabEl } = event.detail;
const tabId = $(tabEl).attr('data-tab-id');
this.#tabsRegistry_.register(tabId, tabEl);
this.runEvent('tabCreated', event);
setTimeout(() => {
this.#scrollbar_.update();
}, 500);
});
// 移除已有Tab之前触发
container.addEventListener('beforeDestroy', (event) => {
this.runEvent('tabBeforeDestroy', event);
});
// 移除已有Tab时触发
container.addEventListener('destroyed', (event) => {
const { tabEl } = event.detail;
const tabId = $(tabEl).attr('data-tab-id');
this.#tabsRegistry_.unregister(tabId);
this.runEvent('tabDestroyed', event);
setTimeout(() => {
this.#scrollbar_ && this.#scrollbar_.update();
}, 500);
});
}
addTab(tabProperties, others = {}) {
tabProperties = { ...tabProperties };
const { id } = tabProperties;
tabProperties.id = id ?? IdGenerator.generate();
let tab = this.#tabsRegistry_.getItem(tabProperties.id);
if (tab) {
this.updateTab(tabProperties.id, tabProperties);
this.setCurrentTab(tabProperties.id);
} else {
tab = this.#chromeTabs_.addTab(tabProperties, others);
}
let { left } = $(tab).position();
this.#scrollbar_.$container.scrollLeft = left;
this.#scrollbar_.update();
}
removeTab(id) {
const elem = this.#tabsRegistry_.getItem(id);
this.#chromeTabs_.removeTab(elem);
}
setCurrentTab(id) {
const elem = this.#tabsRegistry_.getItem(id);
this.#chromeTabs_.setCurrentTab(elem);
}
updateTab(id, tabProperties) {
const elem = this.#tabsRegistry_.getItem(id);
const newId = tabProperties.id || id;
this.#chromeTabs_.updateTab(elem, tabProperties);
if (id !== newId) {
this.#tabsRegistry_.unregister(id);
this.#tabsRegistry_.register(id, elem);
}
}
getScrollbar() {
return this.#scrollbar_;
}
getSortable() {
return this.#sortable_;
}
dispose() {
this.#chromeTabs_.dispose();
this.#chromeTabs_ = null;
this.#tabsRegistry_.reset();
this.#tabsRegistry_ = null;
this.#sortable_.destroy();
this.#sortable_ = null;
this.#scrollbar_.destroy();
this.#scrollbar_ = null;
super.dispose();
}
}
Mixly.PagesTab = PagesTab;
});

View File

@@ -0,0 +1,57 @@
goog.loadJs('common', () => {
goog.require('Mixly');
goog.provide('Mixly.Profile');
const { Profile } = Mixly;
Profile.parse = function (range) {
let pinList = [];
for (let i of range) {
const pinInfo = i.split('-');
switch (pinInfo.length) {
case 1:
const pinNumStr = pinInfo[0].toString();
if (!isNaN(pinNumStr)) {
const pinNum = parseInt(pinNumStr);
pinList.push(pinNum);
}
break;
case 2:
const pinNumStr0 = pinInfo[0].toString(),
pinNumStr1 = pinInfo[1].toString();
if (!isNaN(pinNumStr0) && !isNaN(pinNumStr1)) {
let pinNum0 = parseInt(pinNumStr0);
let pinNum1 = parseInt(pinNumStr1);
if (pinNum0 < 0 || pinNum1 < 0) break;
if (pinNum0 > pinNum1) {
[ pinNum0, pinNum1 ] = [ pinNum1, pinNum0 ];
}
for (let j = pinNum0; j <= pinNum1; j++) {
if (!pinList.includes(j)) {
pinList.push(j);
}
}
}
break;
}
}
return pinList;
};
Profile.generate = function (pinMap, add1 = '', add2 = '') {
const getPins = (list) => {
let pins = [];
for (let i of list) {
const pin = [ add1 + i, add2 + i ];
pins.push(pin);
}
return pins;
}
const pinList = this.parse(pinMap);
return getPins(pinList);
};
window.profile = Profile;
});

View File

@@ -0,0 +1,71 @@
goog.loadJs('common', () => {
goog.require('Mixly.Debug');
goog.provide('Mixly.Registry');
const { Debug } = Mixly;
class Registry {
#registry_ = new Map();
constructor() {
this.reset();
}
reset() {
this.#registry_.clear();
}
validate(keys) {
if (!(keys instanceof Array)) {
keys = [keys];
}
return keys;
}
register(keys, value) {
keys = this.validate(keys);
for (let key of keys) {
if (this.#registry_.has(key)) {
Debug.warn(`${key}已存在,不可重复注册`);
continue;
}
this.#registry_.set(key, value);
}
}
unregister(keys) {
keys = this.validate(keys);
for (let key of keys) {
if (!this.#registry_.has(key)) {
Debug.warn(`${key}不存在,无需取消注册`);
continue;
}
this.#registry_.delete(key);
}
}
length() {
return this.#registry_.size;
}
hasKey(key) {
return this.#registry_.has(key);
}
keys() {
return [...this.#registry_.keys()];
}
getItem(key) {
return this.#registry_.get(key) ?? null;
}
getAllItems() {
return this.#registry_;
}
}
Mixly.Registry = Registry;
});

View File

@@ -0,0 +1,84 @@
goog.loadJs('common', () => {
goog.require('Mixly');
goog.provide('Mixly.Regression');
class Regression {
constructor() {
this.x = [];
this.y = [];
this.n = 0;
this.beta = 1;
this.alpha = 0;
}
/**
* 适配
* @param {Array} x
* @param {Array} y
*/
fit(x, y) {
this.x = x;
this.y = y;
this.n = x.length;
this.beta = this.getBeta();
this.alpha = this.getAlpha(this.beta);
}
/**
* 预测
* @param {Array} x 数据集
* @returns {Array} 预测结果数据集
*/
predict(x) {
if (!Array.isArray(x)) x = [x];
const y = [];
for (const num of x) {
y.push(this.alpha + num * this.beta);
}
return y;
}
/**
* 获取beta
* @returns {Number} 斜率
*/
getBeta() {
const beta = (this.sum(this.x, (v, k) => v * this.y[k]) * this.n
- this.sum(this.x) * this.sum(this.y)) /
(this.sum(this.x, (v) => v * v) * this.n
- Math.pow(this.sum(this.x), 2));
return beta;
}
/**
* 获取alpha
* @param {Number} beta 斜率
* @returns {Number} 偏移量
*/
getAlpha(beta) {
return this.avg(this.y) - this.avg(this.x) * beta;
}
/**
* 求和(Σ)
* @param {Array} arr 数字集合
* @param {Function} fun 每个集合的操作方法
*/
sum(arr, fun = (v, k) => v) {
let s = 0;
const operate = fun;
for (let i = 0; i < arr.length; i++) {
let num = arr[i];
s += operate(num, i);
}
return s;
}
/**
* 均值
* @param {Array} arr 数字集合
*/
avg(arr) {
const s = this.sum(arr);
return s / arr.length;
}
}
Mixly.Regression = Regression;
});

View File

@@ -0,0 +1,65 @@
goog.loadJs('common', () => {
goog.require('Mixly.Env');
goog.provide('Mixly.ScriptLoader');
const { Env, ScriptLoader } = Mixly;
/**
* 加载 script 文件
* @param src
*/
ScriptLoader.loadScript = function (src) {
let addSign = true;
let scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
if (scripts[i] && scripts[i].src && scripts[i].src.indexOf(src) !== -1) {
addSign = false;
}
}
if (addSign) {
let $script = document.createElement('script');
$script.setAttribute('type', 'text/javascript');
$script.setAttribute('src', src);
//$script.setAttribute('async', '');
document.getElementsByTagName('head').item(0).appendChild($script);
}
}
/**
* 删除 script 文件
* @param src
*/
ScriptLoader.removeScript = function (src) {
let scripts = document.getElementsByTagName('script');
if (src.indexOf('../') !== -1) {
src = src.substring(src.lastIndexOf('../') + 3, src.length);
}
for (let i = 0; i < scripts.length; i++) {
if (scripts[i] && scripts[i].src && scripts[i].src.indexOf(src) !== -1) {
scripts[i].parentNode.removeChild(scripts[i]);
}
}
}
ScriptLoader.loadLangJs = function (oldLang, newLang, doFunc) {
let scripts = document.querySelectorAll('script');
let newLangPathArr = [];
for (let i = 0; i < scripts.length; i++) {
if (scripts[i] && scripts[i].src && scripts[i].src.indexOf(oldLang + '.js') !== -1) {
let oldLangPath = scripts[i].src;
let newLangPath = oldLangPath.replace(oldLang + '.js', newLang + '.js');
scripts[i].parentNode.removeChild(scripts[i]);
newLangPathArr.push(newLangPath);
}
}
for (let i = 0; i < Env.thirdPartyJS.length; i++) {
Env.thirdPartyJS[i] = Env.thirdPartyJS[i].replace(oldLang + '.js', newLang + '.js');
}
LazyLoad.js(newLangPathArr, function () {
doFunc();
});
}
});

View File

@@ -0,0 +1,398 @@
goog.loadJs('common', () => {
goog.require('Mixly.Config');
goog.require('Mixly.Events');
goog.require('Mixly.Registry');
goog.require('Mixly.Nav');
goog.require('Mixly.Msg');
goog.provide('Mixly.Serial');
const {
Config,
Events,
Registry,
Nav,
Msg
} = Mixly;
const { SELECTED_BOARD } = Config;
class Serial {
static {
this.portsName = [];
this.portToNameRegistry = new Registry();
this.nameToPortRegistry = new Registry();
this.DEFAULT_CONFIG = {
ctrlCBtn: false,
ctrlDBtn: false,
baudRates: 9600,
yMax: 100,
yMin: 0,
pointNum: 100,
rts: true,
dtr: true
};
this.AVAILABEL_BAUDS = [
300, 600, 750, 1200, 2400, 4800, 9600, 19200, 31250, 38400, 57600,
74880, 115200, 230400, 250000, 460800, 500000, 921600, 1000000, 2000000,
];
this.getSelectedPortName = function () {
return Nav.getMain().getPortSelector().val();
}
this.getCurrentPortsName = function () {
return this.portsName;
}
this.refreshPorts = function () {
let portsName = [];
for (let name of Serial.nameToPortRegistry.keys()) {
portsName.push({ name });
}
Serial.renderSelectBox(portsName);
}
this.getConfig = function () {
let config = SELECTED_BOARD?.serial ?? {};
return {
...this.DEFAULT_CONFIG,
...config
};
}
this.portIsLegal = function (port) {
return this.portsName.includes(port);
}
/**
* @function 重新渲染串口下拉框
* @param {array} 当前可用的所有串口
* @return {void}
**/
this.renderSelectBox = function (ports) {
const $portSelector = Nav.getMain().getPortSelector();
const selectedPort = $portSelector.val();
$portSelector.empty();
if (ports.length) {
let portsName = [];
ports.map(port => {
portsName.push(port.name);
});
portsName.sort();
this.portsName = portsName;
portsName.map(name => {
let newOption = new Option(name, name);
if (selectedPort === name) {
newOption.setAttribute('selected', true);
}
$portSelector.append(newOption);
});
} else {
this.portsName = [];
let newOption = new Option(Msg.Lang['statusbar.serial.noPort']);
newOption.setAttribute('disabled', true);
$portSelector.append(newOption);
}
$portSelector.select2('close');
$portSelector.trigger('change');
let footerStatus = ports.length ? 'inline-flex' : 'none';
$('#mixly-footer-port-div').css('display', footerStatus);
$('#mixly-footer-port').html($portSelector.val());
}
}
#buffer_ = [];
#bufferLength_ = 0;
#encoder_ = new TextEncoder('utf8');
#decoder_ = new TextDecoder('utf8');
#baud_ = 0;
#dtr_ = false;
#rts_ = false;
#vid_ = 0x0000;
#pid_ = 0x0000;
#isOpened_ = false;
#port_ = '';
#special_ = [];
#specialLength_ = 0;
#events_ = new Events(['onOpen', 'onClose', 'onError', 'onBuffer', 'onString', 'onByte', 'onChar']);
constructor(port) {
this.#port_ = port;
this.resetBuffer();
}
decodeBuffer(buffer) {
let output = '';
for (let i = 0; i < buffer.length; i++) {
output += this.decodeByte(buffer[i]);
}
return output;
}
/* UTF-8编码方式
* ------------------------------------------------------------
* |1字节 0xxxxxxx |
* |2字节 110xxxxx 10xxxxxx |
* |3字节 1110xxxx 10xxxxxx 10xxxxxx |
* |4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
* |5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
* |6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx|
* ------------------------------------------------------------
**/
decodeByte(byte) {
let output = '';
if (byte !== 0x5F && this.#special_.length && !this.#specialLength_) {
const str = this.#special_.join('');
try {
output += decodeURIComponent(str.replace(/_([0-9a-fA-F]{2})/gm, '%$1'));
} catch (_) {
output += str;
}
this.#special_ = [];
}
if ((byte & 0x80) === 0x00) {
// 1字节
this.#buffer_ = [];
this.#bufferLength_ = 0;
if (byte === 0x5F) {
// 如果当前字节是 "_"
this.#specialLength_ = 2;
this.#special_.push(String.fromCharCode(byte));
} else if (byte !== 0x0D) {
// 如果当前字节不是 "\r"
if (this.#specialLength_) {
this.#specialLength_--;
this.#special_.push(String.fromCharCode(byte));
} else {
output += String.fromCharCode(byte);
}
}
} else if ((byte & 0xC0) === 0x80) {
/*
* 2字节以上的中间字节10xxxxxx
* 如果没有起始头,则丢弃这个字节
* 如果不是2字节及以上的起始头则丢弃这个字节
**/
if (!this.#buffer_.length || this.#bufferLength_ < 2) {
return output;
}
this.#buffer_.push(byte);
if (this.#bufferLength_ === this.#buffer_.length) {
output += this.decode(new Uint8Array(this.#buffer_));
this.#buffer_ = [];
}
} else {
// 2字节以上的起始头
if (this.#buffer_.length) {
this.#buffer_ = [];
}
this.#bufferLength_ = this.#getBufferLength_(byte);
this.#buffer_.push(byte);
}
return output;
}
#getBufferLength_(byte) {
let len = 1;
if ((byte & 0xFC) === 0xFC) {
len = 6;
} else if ((byte & 0xF8) === 0xF8) {
len = 5;
} else if ((byte & 0xF0) === 0xF0) {
len = 4;
} else if ((byte & 0xE0) === 0xE0) {
len = 3;
} else if ((byte & 0xC0) === 0xC0) {
len = 2;
}
return len;
}
resetBuffer() {
this.#buffer_ = [];
this.#bufferLength_ = 0;
}
async open(baud) {
this.#isOpened_ = true;
}
async close() {
this.#isOpened_ = false;
this.#baud_ = 0;
}
async toggle() {
if (this.isOpened()) {
return this.close();
} else {
return this.open();
}
}
baudRateIsLegal(baud) {
return Serial.AVAILABEL_BAUDS.includes(baud);
}
async setBaudRate(baud) {
this.#baud_ = baud;
}
async setDTR(dtr) {
this.#dtr_ = dtr;
}
async setRTS(rts) {
this.#rts_ = rts;
}
async setDTRAndRTS(dtr, rts) {
this.#dtr_ = dtr;
this.#rts_ = rts;
}
async setVID(vid) {
this.#vid_ = vid;
}
async setPID(pid) {
this.#pid_ = pid;
}
async setVIDAndPID(vid, pid) {
this.#vid_ = vid;
this.#pid_ = pid;
}
getPortName() {
return this.#port_;
}
getBaudRate() {
return this.#baud_ || Serial.getConfig().baudRates;
}
getRawBaudRate() {
return this.#baud_;
}
getDTR() {
return this.#dtr_;
}
getRTS() {
return this.#rts_;
}
getVID() {
return this.#vid_;
}
getPID() {
return this.#pid_;
}
async sendString(str) {}
async sendBuffer(buffer) {}
async interrupt() {
return this.sendBuffer([3]);
}
async reset() {
return this.sendBuffer([4]);
}
onBuffer(buffer) {
this.#events_.run('onBuffer', buffer);
}
onString(string) {
this.#events_.run('onString', string);
}
onByte(byte) {
this.#events_.run('onByte', byte);
}
onChar(char) {
this.#events_.run('onChar', char);
}
onOpen() {
this.#isOpened_ = true;
this.#events_.run('onOpen');
}
onClose(code) {
this.#isOpened_ = false;
this.#events_.run('onClose', code);
}
onError(error) {
this.#events_.run('onError', error);
}
isOpened() {
return this.#isOpened_;
}
encode(str) {
return this.#encoder_.encode(str);
}
decode(uint8Array) {
return this.#decoder_.decode(uint8Array);
}
async config(info) {
return Promise.all([
this.setBaudRate(info.baud),
this.setDTRAndRTS(info.dtr, info.rts)
]);
}
async sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async dispose() {
this.#events_.reset();
if (this.isOpened()) {
await this.close();
}
this.#events_ = null;
this.#encoder_ = null;
this.#decoder_ = null;
}
bind(type, func) {
return this.#events_.bind(type, func);
}
unbind(id) {
this.#events_.unbind(id);
}
addEventsType(eventsType) {
this.#events_.addType(eventsType);
}
runEvent(eventsType, ...args) {
return this.#events_.run(eventsType, ...args);
}
offEvent(eventsType) {
this.#events_.off(eventsType);
}
resetEvent() {
this.#events_.reset();
}
}
Mixly.Serial = Serial;
});

View File

@@ -0,0 +1,57 @@
goog.loadJs('common', () => {
goog.require('XScrollbar');
goog.require('Mixly.Env');
goog.require('Mixly.Config');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Debug');
goog.require('Mixly.PageBase');
goog.provide('Mixly.SideBarLibs');
const {
Env,
Config,
HTMLTemplate,
Debug,
PageBase
} = Mixly;
const { USER } = Config;
class SideBarLibs extends PageBase {
static {
HTMLTemplate.add(
'html/sidebar/sidebar-libs.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/sidebar/sidebar-libs.html')))
);
}
#scrollbar_ = null;
constructor() {
super();
const $content = $(HTMLTemplate.get('html/sidebar/sidebar-libs.html').render());
this.#scrollbar_ = new XScrollbar($content[0], {
onlyHorizontal: false,
thumbSize: '4px',
thumbRadius: '1px',
thumbBackground: USER.theme === 'dark'? '#b0b0b0' : '#5f5f5f'
});
this.setContent($content);
}
init() {
super.init();
this.hideCloseBtn();
}
dispose() {
this.#scrollbar_.destroy();
super.dispose();
}
}
Mixly.SideBarLibs = SideBarLibs;
});

View File

@@ -0,0 +1,126 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mprogress');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Debug');
goog.require('Mixly.Menu');
goog.require('Mixly.PageBase');
goog.require('Mixly.Electron.FileTree');
goog.require('Mixly.Web.FileTree');
goog.require('Mixly.Electron.FS');
goog.require('Mixly.Web.FS');
goog.provide('Mixly.SideBarLocalStorage');
const {
IdGenerator,
XML,
Env,
HTMLTemplate,
Debug,
Menu,
PageBase,
Electron = {},
Web = {}
} = Mixly;
const {
FileTree,
FS
} = goog.isElectron? Electron : Web;
class SideBarLocalStorage extends PageBase {
static {
HTMLTemplate.add(
'html/sidebar/sidebar-local-storage-open-folder.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/sidebar/sidebar-local-storage-open-folder.html')))
);
}
#$openFolderContent_ = null;
#fileTree_ = null;
#contextMenu_ = null;
#folderPath_ = null;
constructor() {
super();
const $openFolderContent = $(HTMLTemplate.get('html/sidebar/sidebar-local-storage-open-folder.html').render());
this.#$openFolderContent_ = $openFolderContent;
this.setContent($openFolderContent);
this.#fileTree_ = new FileTree();
this.#addEventsListener_();
}
init() {
super.init();
this.hideCloseBtn();
}
#addEventsListener_() {
this.#$openFolderContent_.find('button').click(() => {
this.showDirectoryPicker();
});
const contextMenu = this.#fileTree_.getContextMenu();
const menu = contextMenu.getItem('menu');
menu.add({
weight: 10,
type: 'open_new_folder',
preconditionFn: ($trigger) => {
let type = $trigger.attr('type');
return ['root'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem('打开新文件夹', ''),
callback: () => {
this.showDirectoryPicker();
}
}
});
}
showDirectoryPicker() {
FS.showDirectoryPicker()
.then((folderPath) => {
if (!folderPath) {
return;
}
this.setFolderPath(folderPath);
this.setContent(this.#fileTree_.getContent());
this.#$openFolderContent_.remove();
})
.catch(Debug.error);
}
getFileTree() {
return this.#fileTree_;
}
resize() {
super.resize();
this.#fileTree_ && this.#fileTree_.resize();
}
dispose() {
this.#fileTree_.dispose();
}
setFolderPath(folderPath) {
if (goog.isElectron) {
this.#fileTree_.setFolderPath(folderPath);
} else {
this.#fileTree_.setFolderPath('/');
}
this.#folderPath_ = this.#fileTree_.getFolderPath();
this.#fileTree_.openRootFolder();
}
}
Mixly.SideBarLocalStorage = SideBarLocalStorage;
});

View File

@@ -0,0 +1,144 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Registry');
goog.require('Mixly.Events');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.SideBarLocalStorage');
goog.require('Mixly.SideBarLibs');
goog.require('Mixly.PagesManager');
goog.require('Mixly.IdGenerator');
goog.provide('Mixly.SideBarsManager');
goog.provide('Mixly.LeftSideBarsManager');
goog.provide('Mixly.RightSideBarsManager');
const {
Env,
Msg,
Registry,
Events,
HTMLTemplate,
SideBarLocalStorage,
SideBarLibs,
PagesManager,
IdGenerator,
Boards
} = Mixly;
class SideBarsManager extends PagesManager {
static {
HTMLTemplate.add(
'html/sidebar/left-sidebars-manager.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/sidebar/left-sidebars-manager.html')))
);
HTMLTemplate.add(
'html/sidebar/left-sidebars-tab.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/sidebar/left-sidebars-tab.html')))
);
HTMLTemplate.add(
'html/sidebar/right-sidebars-manager.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/sidebar/right-sidebars-manager.html')))
);
HTMLTemplate.add(
'html/sidebar/right-sidebars-tab.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/sidebar/right-sidebars-tab.html')))
);
this.typesRegistry = new Registry();
this.managersRegistry = new Registry();
this.typesRegistry.register(['#default', 'local_storage'], SideBarLocalStorage);
this.typesRegistry.register(['libs'], SideBarLibs);
this.Align = {
LEFT: 0,
RIGHT: 1,
0: 'LEFT',
1: 'RIGHT'
};
this.getMain = function() {
if (!this.managersRegistry.length()) {
return null;
}
const key = this.managersRegistry.keys()[0];
return this.managersRegistry.getItem(key);
}
this.add = function(manager) {
this.managersRegistry.register(manager.id, manager);
}
this.remove = function(manager) {
this.managersRegistry.unregister(manager.id);
}
}
#shown_ = false;
constructor(element, align = SideBarsManager.Align.LEFT) {
let managerHTMLTemplate = '', tabHTMLTemplate = '';
if (align === SideBarsManager.Align.RIGHT) {
managerHTMLTemplate = HTMLTemplate.get('html/sidebar/right-sidebars-manager.html');
tabHTMLTemplate = HTMLTemplate.get('html/sidebar/right-sidebars-tab.html');
} else {
managerHTMLTemplate = HTMLTemplate.get('html/sidebar/left-sidebars-manager.html');
tabHTMLTemplate = HTMLTemplate.get('html/sidebar/left-sidebars-tab.html');
}
const $manager = $(managerHTMLTemplate.render());
const $tab = $(tabHTMLTemplate.render());
super({
parentElem: element,
managerContentElem: $manager[0],
bodyElem: $manager.find('.body')[0],
tabElem: $manager.find('.tabs')[0],
tabContentElem: $tab[0],
typesRegistry: SideBarsManager.typesRegistry
});
this.id = IdGenerator.generate();
this.addEventsType(['show', 'hide']);
SideBarsManager.add(this);
}
show() {
this.runEvent('show');
}
hide() {
this.runEvent('hide');
}
toggle() {
this.isShown() ? this.hide() : this.show();
}
isShown() {
return this.#shown_;
}
dispose() {
SideBarsManager.remove(this);
super.dispose();
}
}
class LeftSideBarsManager extends SideBarsManager {
constructor(element) {
super(element, SideBarsManager.Align.LEFT);
}
}
class RightSideBarsManager extends SideBarsManager {
constructor(element) {
super(element, SideBarsManager.Align.RIGHT);
}
}
Mixly.SideBarsManager = SideBarsManager;
Mixly.LeftSideBarsManager = LeftSideBarsManager;
Mixly.RightSideBarsManager = RightSideBarsManager;
});

View File

@@ -0,0 +1,91 @@
goog.loadJs('common', () => {
goog.require('io');
goog.require('Mixly');
goog.provide('Mixly.Socket');
class Socket {
#socket_ = null;
constructor(path, option) {
this.#socket_ = io(path, option);
}
#detectStatus_(status, callback) {
window.setTimeout(() => {
if (status.finished) {
return;
}
if (this.isConnected()) {
this.#detectStatus_(status, callback);
} else {
callback({
error: 'socket is not connected'
});
status.finished = true;
}
}, 1000);
}
emit(eventName, ...args) {
const callback = args.pop();
if (this.isConnected()) {
let emitStatus = {
finished: false
};
let status = this.#socket_.emit(eventName, ...args, (...callbackArgs) => {
if (emitStatus.finished) {
return;
}
emitStatus.finished = true;
callback(...callbackArgs);
});
this.#detectStatus_(emitStatus, callback);
return status;
} else {
callback({
error: 'socket is not connected'
});
return false;
}
}
async emitAsync(eventName, ...args) {
return new Promise((resolve, reject) => {
if (this.isConnected()) {
const callback = (...callbackArgs) => {
if (callbackArgs[0].error) {
reject(callbackArgs[0].error);
return;
}
resolve(...callbackArgs);
}
let emitStatus = {
finished: false
};
let status = this.#socket_.emit(eventName, ...args, (...callbackArgs) => {
if (emitStatus.finished) {
return;
}
emitStatus.finished = true;
callback(...callbackArgs);
});
this.#detectStatus_(emitStatus, callback);
} else {
reject('socket is not connected');
}
})
}
getSocket() {
return this.#socket_;
}
isConnected() {
return this.#socket_?.connected;
}
}
Mixly.Socket = Socket;
});

View File

@@ -0,0 +1,596 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('layui');
goog.require('Mixly.PageBase');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.DragV');
goog.require('Mixly.StatusBar');
goog.require('Mixly.Serial');
goog.require('Mixly.Menu');
goog.require('Mixly.AmpyFileTree');
goog.provide('Mixly.StatusBarAmpy');
const {
PageBase,
Env,
Msg,
Debug,
HTMLTemplate,
DragV,
StatusBar,
Serial,
Menu,
AmpyFileTree
} = Mixly;
const { layer } = layui;
class StatusBarAmpy extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-ampy.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-ampy.html')))
);
HTMLTemplate.add(
'html/statusbar/statusbar-ampy-open-fs.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-ampy-open-fs.html')))
);
HTMLTemplate.add(
'html/statusbar/statusbar-ampy-editor-empty.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-ampy-editor-empty.html')))
);
}
#$fileTree_ = null;
#$editor_ = null;
#$openFS_ = null;
#$editorEmpty_ = null;
#editor_ = null;
#fileTree_ = null;
#drag_ = null;
#fileTreeShown_ = false;
#editorShown_ = false;
#changed_ = false;
constructor() {
super();
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-ampy.html').render());
this.setContent($content);
this.#fileTree_ = new AmpyFileTree();
this.#$fileTree_ = $content.children('.file-tree');
this.#$openFS_ = $(HTMLTemplate.get('html/statusbar/statusbar-ampy-open-fs.html').render({
loadBoardFS: Msg.Lang['statusbar.ampy.loadBoardFS']
}));
this.#$fileTree_.append(this.#$openFS_);
this.#editor_ = new StatusBar();
this.#$editor_ = $content.children('.editor');
this.#$editorEmpty_ = $(HTMLTemplate.get('html/statusbar/statusbar-ampy-editor-empty.html').render());
this.#$editor_.append(this.#$editorEmpty_);
}
#addEventsListener_() {
this.#drag_ = new DragV(this.getContent()[0], {
min: '150px',
startSize: '15%',
full: [false, false]
});
this.#drag_.bind('sizeChanged', () => {
this.resize();
});
this.#$openFS_.children('button').click(() => {
this.openFS();
});
this.#fileTree_.bind('beforeSelectLeaf', (selected) => {
const filePath = selected[0].id;
const mode = this.#editor_.getFileMode(path.extname(filePath));
if (!mode) {
layer.msg(Msg.Lang['statusbar.ampy.cannotEdit'], { time: 1000 });
return false;
}
this.#editor_.setMode(mode);
return true;
});
this.#fileTree_.bind('afterSelectLeaf', async (selected) => {
const filePath = selected[0].id;
this.#fileTree_.showProgress();
const fs = this.#fileTree_.getFS();
const [error, result] = await fs.readFile(filePath);
if (error) {
this.hideEditor();
this.#fileTree_.deselectAll();
} else {
this.showEditor();
this.#editor_.setValue(result);
this.#editor_.scrollToTop();
this.#editor_.focus();
this.setStatus(false);
}
this.#fileTree_.hideProgress();
});
this.#fileTree_.bind('afterCreateNode', (folderPath) => {
this.#fileTree_.refreshFolder(folderPath);
});
this.#fileTree_.bind('afterDeleteNode', (folderPath) => {
this.#fileTree_.refreshFolder(folderPath);
});
this.#fileTree_.bind('afterRenameNode', (folderPath) => {
this.#fileTree_.refreshFolder(folderPath);
});
this.#fileTree_.bind('afterRefreshNode', (refreshedNode) => {
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
if (!selectedNodeId) {
this.hideEditor();
}
});
const fileTreeContextMenu = this.#fileTree_.getContextMenu();
const fileTreeMenu = fileTreeContextMenu.getItem('menu');
fileTreeMenu.add({
weight: 14,
id: 'sep5',
data: '---------'
});
fileTreeMenu.add({
weight: 15,
id: 'upload-folder',
preconditionFn: ($trigger) => {
let type = $trigger.attr('type');
if (type === 'file') {
return false;
}
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy.uploadFolder'], ''),
callback: async (_, { $trigger }) => {
this.#fileTree_.showProgress();
try {
const fp = await window.showDirectoryPicker();
if (!fp) {
this.#fileTree_.hideProgress();
return;
}
const type = $trigger.attr('type');
let folderPath = '/';
if (type !== 'root') {
folderPath = $trigger.attr('id');
}
const fs = this.#fileTree_.getFS();
try {
for await (const [key, value] of fp.entries()) {
if (value.kind !== 'file') {
continue;
}
const file = await value.getFile();
const arrayBuffer = await file.arrayBuffer();
const [error,] = await fs.writeFile(path.join(folderPath, file.name), arrayBuffer);
if (error) {
Debug.error(error);
}
}
} catch (error) {
Debug.error(error);
}
this.#fileTree_.refreshFolder(folderPath);
} catch (_) { }
this.#fileTree_.hideProgress();
}
}
});
fileTreeMenu.add({
weight: 16,
id: 'upload-file',
preconditionFn: ($trigger) => {
let type = $trigger.attr('type');
if (type === 'file') {
return false;
}
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy.uploadFile'], ''),
callback: async (_, { $trigger }) => {
this.#fileTree_.showProgress();
try {
const [ fp ] = await window.showOpenFilePicker({
multiple: false
});
if (!fp) {
this.#fileTree_.hideProgress();
return;
}
const type = $trigger.attr('type');
let folderPath = '/';
if (type !== 'root') {
folderPath = $trigger.attr('id');
}
const file = await fp.getFile();
const arrayBuffer = await file.arrayBuffer();
const fs = this.#fileTree_.getFS();
const [error,] = await fs.writeFile(path.join(folderPath, file.name), arrayBuffer);
if (error) {
Debug.error(error);
} else {
this.#fileTree_.refreshFolder(folderPath);
}
} catch (_) { }
this.#fileTree_.hideProgress();
}
}
});
fileTreeMenu.add({
weight: 17,
id: 'download',
preconditionFn: ($trigger) => {
const type = $trigger.attr('type');
if (type === 'file') {
return true;
}
return false;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy.download'], ''),
callback: async (_, { $trigger }) => {
this.#fileTree_.showProgress();
try {
const filePath = $trigger.attr('id');
const fp = await window.showSaveFilePicker({
suggestedName: path.basename(filePath)
});
if (!fp) {
this.#fileTree_.hideProgress();
return;
}
const fs = this.#fileTree_.getFS();
const [error, result] = await fs.readFile(filePath, 'buffer');
if (error) {
Debug.error(error);
} else {
const writer = await fp.createWritable();
await writer.write(result);
await writer.close();
}
} catch (_) { }
this.#fileTree_.hideProgress();
}
}
});
fileTreeMenu.add({
weight: 18,
id: 'sep6',
preconditionFn: ($trigger) => {
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
let type = $trigger.attr('type');
let id = $trigger.attr('id');
if (type === 'file' && selectedNodeId !== id) {
return false;
}
return true;
},
data: '---------'
});
fileTreeMenu.add({
weight: 19,
id: 'refresh',
preconditionFn: ($trigger) => {
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
let type = $trigger.attr('type');
let id = $trigger.attr('id');
if (type === 'file' && selectedNodeId !== id) {
return false;
}
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy.refresh'], ''),
callback: (_, { $trigger }) => {
let type = $trigger.attr('type');
if (type === 'root') {
this.#fileTree_.openRootFolder();
this.#fileTree_.refreshFolder('/');
} else if (type === 'folder') {
let id = $trigger.attr('id');
this.#fileTree_.openNode(id);
this.#fileTree_.refreshFolder(id);
} else {
const nodes = this.#fileTree_.getSelectedNodes();
this.#fileTree_.runEvent('afterSelectLeaf', nodes);
}
}
}
});
fileTreeMenu.add({
weight: 20,
id: 'sep6',
preconditionFn: ($trigger) => {
let type = $trigger.attr('type');
return ['root'].includes(type);
},
data: '---------'
});
fileTreeMenu.add({
weight: 21,
id: 'exit',
preconditionFn: ($trigger) => {
let type = $trigger.attr('type');
return ['root'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy.exit'], ''),
callback: () => {
this.closeFS();
}
}
});
const editorContextMenu = this.#editor_.getContextMenu();
const editorMenu = editorContextMenu.getItem('code');
editorMenu.empty();
editorMenu.add({
weight: 0,
id: 'cut',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.cut'], 'Ctrl+X'),
callback: () => this.#editor_.cut()
}
});
editorMenu.add({
weight: 1,
id: 'copy',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], 'Ctrl+C'),
callback: () => this.#editor_.copy()
}
});
editorMenu.add({
weight: 2,
id: 'paste',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.paste'], 'Ctrl+V'),
callback: () => this.#editor_.paste()
}
});
editorMenu.add({
weight: 3,
id: 'sep1',
data: '---------'
});
editorMenu.add({
weight: 4,
id: 'togglecomment',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.togglecomment'], 'Ctrl+/'),
callback: () => this.#editor_.commentLine()
}
});
/*editorMenu.add({
weight: 5,
id: 'toggleBlockComment',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.toggleBlockComment'], 'Shift+Alt+A'),
callback: (key, opt) => this.#editor_.blockComment()
}
});*/
editorMenu.add({
weight: 6,
id: 'sep2',
preconditionFn: () => {
return this.#changed_;
},
data: '---------'
});
editorMenu.add({
weight: 7,
id: 'save',
preconditionFn: () => {
return this.#changed_;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['file.save'], 'Ctrl+S'),
callback: async () => {
await this.put();
}
}
});
const { commands } = this.#editor_.getEditor();
commands.addCommand({
name: "save",
bindKey: "Ctrl-S",
exec: async () => {
if (!this.#changed_) {
return;
}
await this.put();
}
});
}
async put() {
this.#fileTree_.showProgress();
const id = this.#fileTree_.getSelectedNodeId();
const fs = this.#fileTree_.getFS();
const [error, stdout] = await fs.writeFile(id, this.#editor_.getValue());
this.#fileTree_.hideProgress();
if (!error) {
this.setStatus(false);
}
}
showFileTree() {
if (this.#fileTreeShown_) {
return;
}
this.#$openFS_.detach();
this.#$fileTree_.empty();
this.#$fileTree_.append(this.#fileTree_.getContent());
this.#fileTreeShown_ = true;
}
hideFileTree() {
if (!this.#fileTreeShown_) {
return;
}
this.#fileTree_.getContent().detach();
this.#$fileTree_.empty();
this.#$fileTree_.append(this.#$openFS_);
this.#fileTreeShown_ = false;
}
showEditor() {
if (this.#editorShown_) {
return;
}
this.#$editorEmpty_.detach();
this.#$editor_.empty();
this.#$editor_.append(this.#editor_.getContent());
this.#editorShown_ = true;
}
hideEditor() {
if (!this.#editorShown_) {
return;
}
this.#editor_.getContent().detach();
this.#$editor_.empty();
this.#$editor_.append(this.#$editorEmpty_);
this.#editorShown_ = false;
this.setStatus(false);
}
getDrag() {
return this.#drag_;
}
init() {
super.init();
this.addDirty();
this.setMarkStatus('negative');
this.#editor_.init();
this.#addEventsListener_();
const editor = this.#editor_.getEditor();
editor.setReadOnly(false);
editor.renderer.setShowGutter(true);
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
newLineMode: 'unix'
});
editor.on('change', () => {
this.setStatus(true);
});
}
openFS() {
const port = Serial.getSelectedPortName();
if (!port) {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
return;
}
const fs = this.#fileTree_.getFS();
fs.setPortName(port);
this.#fileTree_.setFolderPath('/');
this.#fileTree_.setRootFolderName(`${port} - /`);
this.#fileTree_.openRootFolder();
this.showFileTree();
}
closeFS() {
this.#fileTree_.deselectAll();
this.hideFileTree();
this.hideEditor();
this.setStatus(false);
}
onMounted() {
super.onMounted();
this.#editor_.onMounted();
this.#fileTree_.onMounted();
// this.#fileTree_.refreshFolder('/');
}
onUnmounted() {
// this.hideEditor();
// this.#fileTree_.deselectAll();
super.onUnmounted();
this.#editor_.onUnmounted();
this.#fileTree_.onUnmounted();
}
resize() {
super.resize();
this.#editor_.resize();
this.#fileTree_.resize();
}
setStatus(isChanged) {
if (this.#changed_ === isChanged) {
return;
}
this.#changed_ = isChanged;
if (isChanged) {
this.setMarkStatus('positive');
} else {
this.setMarkStatus('negative');
}
}
dispose() {
this.#$fileTree_ = null;
this.#$editor_ = null;
this.#$openFS_ = null;
this.#$editorEmpty_ = null;
this.#editor_.dispose();
this.#editor_ = null;
this.#fileTree_.dispose();
this.#fileTree_ = null;
this.#drag_.dispose();
this.#drag_ = null;
super.dispose();
}
}
Mixly.StatusBarAmpy = StatusBarAmpy;
});

View File

@@ -0,0 +1,331 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('layui');
goog.require('Mprogress');
goog.require('$.select2');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.PageBase');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Debug');
goog.require('Mixly.Component');
goog.require('Mixly.Registry');
goog.require('Mixly.Serial');
goog.require('Mixly.FSBoardHandler');
goog.require('Mixly.Electron.FS');
goog.require('Mixly.Electron.FSBoard');
goog.provide('Mixly.StatusBarFS');
const {
Env,
Msg,
PageBase,
HTMLTemplate,
Debug,
Component,
Registry,
Serial,
FSBoardHandler,
Electron = {}
} = Mixly;
const { FS, FSBoard } = Electron;
const { layer } = layui;
const os = Mixly.require('os');
class Panel extends Component {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-fs-panel.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-fs-panel.html')))
);
}
#$folderInput_ = null;
#$closeBtn_ = null;
#$selectFolderBtn_ = null;
#$downloadBtn_ = null;
#$uploadBtn_ = null;
#$fsSelect_ = null;
#$progress_ = null;
#folderPath_ = '';
#fs_ = 'default';
#opened_ = false;
constructor() {
super();
const template = HTMLTemplate.get('html/statusbar/statusbar-fs-panel.html');
const $content = $(template.render({
mapFolder: Msg.Lang['statusbar.fs.mapFolder'],
comment: Msg.Lang['statusbar.fs.comment'],
commentInfo: Msg.Lang['statusbar.fs.commentInfo'],
filesystem: Msg.Lang['statusbar.fs.filesystem'],
path: Msg.Lang['statusbar.fs.path'],
selectFolder: Msg.Lang['statusbar.fs.selectFolder'],
download: Msg.Lang['statusbar.fs.download'],
upload: Msg.Lang['statusbar.fs.upload']
}));
this.setContent($content);
this.#$folderInput_ = $content.find('.folder-input');
this.#$closeBtn_ = $content.find('.close-btn');
this.#$selectFolderBtn_ = $content.find('.folder-btn');
this.#$downloadBtn_ = $content.find('.download-btn');
this.#$uploadBtn_ = $content.find('.upload-btn');
this.#$fsSelect_ = $content.find('.fs-type');
this.#$progress_ = $content.find('.progress');
this.addEventsType(['download', 'upload']);
this.#addEventsListener_();
this.#$fsSelect_.select2({
width: '100%',
minimumResultsForSearch: Infinity,
dropdownCssClass: 'mixly-scrollbar'
});
}
#addEventsListener_() {
this.#$fsSelect_.on('select2:select', (event) => {
const { data } = event.params;
this.#fs_ = data.id;
});
this.#$closeBtn_.click(() => {
this.dispose();
});
this.#$selectFolderBtn_.click(() => {
FS.showDirectoryPicker()
.then((folderPath) => {
if (!folderPath) {
return;
}
this.#folderPath_ = path.join(folderPath);
this.#$folderInput_.val(this.#folderPath_);
})
.catch(Debug.error);
});
this.#$downloadBtn_.click(() => {
this.#checkFolder_()
.then((status) => {
if (!status) {
return;
}
this.runEvent('download', {
folderPath: this.#folderPath_,
fsType: this.#fs_
});
})
.catch(Debug.error);
});
this.#$uploadBtn_.click(() => {
this.#checkFolder_()
.then((status) => {
if (!status) {
return;
}
this.runEvent('upload', {
folderPath: this.#folderPath_,
fsType: this.#fs_
});
})
.catch(Debug.error);
});
}
#checkFolder_() {
return new Promise((resolve, reject) => {
if (!this.#folderPath_) {
layer.msg(Msg.Lang['statusbar.fs.localFolderNotExist'], { time: 1000 });
resolve(false);
return;
}
FS.isDirectory(this.#folderPath_)
.then((status) => {
if (status) {
resolve(true);
} else {
layer.msg(Msg.Lang['statusbar.fs.localFolderNotExist'], { time: 1000 });
resolve(false);
}
})
.catch(reject);
});
}
setFSMenu(menu) {
this.#$fsSelect_.empty();
this.#fs_ = menu[0].id;
for (let i in menu) {
this.#$fsSelect_.append(new Option(menu[i].text, menu[i].id));
}
}
setStatus(isOpened) {
if (this.#opened_ === isOpened) {
return;
}
this.#opened_ = isOpened;
if (isOpened) {
this.#$progress_.css('display', 'block');
} else {
this.#$progress_.css('display', 'none');
}
}
dispose() {
this.#$fsSelect_.select2('destroy');
this.#$folderInput_ = null;
this.#$closeBtn_ = null;
this.#$selectFolderBtn_ = null;
this.#$downloadBtn_ = null;
this.#$uploadBtn_ = null;
this.#$fsSelect_ = null;
this.#$progress_ = null;
super.dispose();
}
}
class StatusBarFS extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-fs.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-fs.html')))
);
}
#$btn_ = null;
#$mask_ = null;
#fsBoard_ = null;
#registry_ = new Registry();
#opened_ = false;
constructor() {
super();
const template = HTMLTemplate.get('html/statusbar/statusbar-fs.html');
const $content = $(template.render({
new: Msg.Lang['statusbar.fs.newMapFolder']
}));
this.setContent($content);
this.#$btn_ = $content.find('.manage-btn');
this.#$mask_ = $content.children('.mask');
this.#fsBoard_ = new FSBoard();
this.#fsBoard_.setHandler(new FSBoardHandler());
this.#addEventsListener_();
}
#addEventsListener_() {
this.#$btn_.click(() => {
this.addPanel();
});
}
init() {
this.addDirty();
this.setMarkStatus('negative');
}
setStatus(isOpened) {
if (this.#opened_ === isOpened) {
return;
}
this.#opened_ = isOpened;
if (isOpened) {
this.#$mask_.css('display', 'block');
this.setMarkStatus('positive');
} else {
this.#$mask_.css('display', 'none');
this.setMarkStatus('negative');
}
}
addPanel() {
const panel = new Panel();
panel.setFSMenu(this.#fsBoard_.getHandler().getFSMenu());
this.#$btn_.parent().before(panel.getContent());
this.#registry_.register(panel.getId(), panel);
panel.bind('download', (config) => {
this.#ensureSerial_()
.then((status) => {
if (!status) {
return;
}
this.setStatus(true);
panel.setStatus(true);
return this.#fsBoard_.download(config.folderPath, config.fsType);
})
.catch(Debug.error)
.finally(() => {
this.setStatus(false);
panel.setStatus(false);
});
});
panel.bind('upload', (config) => {
this.#ensureSerial_()
.then((status) => {
if (!status) {
return;
}
this.setStatus(true);
panel.setStatus(true);
return this.#fsBoard_.upload(config.folderPath, config.fsType);
})
.catch(Debug.error)
.finally(() => {
this.setStatus(false);
panel.setStatus(false);
});
});
panel.bind('destroyed', () => {
this.#registry_.unregister(panel.getId());
});
}
#ensureSerial_() {
return new Promise((resolve, reject) => {
const port = Serial.getSelectedPortName();
if (!port) {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], { time: 1000 });
resolve(false);
return;
}
const { mainStatusBarTabs } = Mixly;
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
let closePromise = Promise.resolve();
if (statusBarSerial) {
closePromise = statusBarSerial.close();
}
closePromise.then(() => {
resolve(true);
}).catch(reject);
});
}
dispose() {
for (let id of this.#registry_.keys()) {
this.#registry_.getItem(id).dispose();
}
this.#$btn_ = null;
this.#$mask_ = null;
this.#fsBoard_.dispose();
this.#fsBoard_ = null;
this.#registry_.reset();
this.#registry_ = null;
super.dispose();
}
setHandler(handler) {
this.#fsBoard_.setHandler(handler);
}
}
Mixly.StatusBarFS = StatusBarFS;
});

View File

@@ -0,0 +1,39 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.Env');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.PageBase');
goog.provide('Mixly.StatusBarLibsCode');
const {
Env,
HTMLTemplate,
PageBase
} = Mixly;
class StatusBarLibsCode extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-libs-mix.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-libs-mix.html')))
);
}
constructor() {
super();
const template = HTMLTemplate.get('html/statusbar/statusbar-libs-mix.html');
const $content = $(template.render());
this.setContent($content);
}
init() {
super.init();
this.hideCloseBtn();
}
}
Mixly.StatusBarLibsCode = StatusBarLibsCode;
});

View File

@@ -0,0 +1,41 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('layui');
goog.require('Mixly.Env');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.PageBase');
goog.provide('Mixly.StatusBarLibsMix');
const {
Env,
HTMLTemplate,
PageBase
} = Mixly;
class StatusBarLibsMix extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-libs-mix.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-libs-mix.html')))
);
}
constructor() {
super();
const template = HTMLTemplate.get('html/statusbar/statusbar-libs-mix.html');
const $content = $(template.render());
this.setContent($content);
this.id = template.getId();
}
init() {
super.init();
this.hideCloseBtn();
}
}
Mixly.StatusBarLibsMix = StatusBarLibsMix;
});

View File

@@ -0,0 +1,89 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('dayjs');
goog.require('$.select2');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.StatusBar');
goog.require('Mixly.SideBarsManager');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.PageBase');
goog.require('Mixly.StatusBarLibsMix');
goog.require('Mixly.StatusBarLibsCode');
goog.provide('Mixly.StatusBarLibs');
const {
Env,
Msg,
Debug,
StatusBar,
SideBarsManager,
RightSideBarsManager,
HTMLTemplate,
PageBase,
StatusBarLibsMix,
StatusBarLibsCode,
} = Mixly;
class StatusBarLibs extends PageBase {
static {
HTMLTemplate.add(
'statusbar/statusbar-libs.html',
new HTMLTemplate(`<div class="page-item" m-id="{{d.mId}}"></div>`)
);
SideBarsManager.typesRegistry.register(['libs_mix'], StatusBarLibsMix);
SideBarsManager.typesRegistry.register(['libs_code'], StatusBarLibsCode);
}
#manager_ = null;
constructor() {
super();
const template = HTMLTemplate.get('statusbar/statusbar-libs.html');
const $content = $(template.render());
this.setContent($content);
this.id = template.getId();
this.#manager_ = new RightSideBarsManager($content[0]);
this.#manager_.add('libs_mix', 'libs_mix', 'Mixly库');
this.#manager_.add('libs_code', 'libs_code', '代码库');
this.#manager_.changeTo('libs_mix');
}
init() {
this.addDirty();
const $tab = this.getTab();
this.setMarkStatus('negative');
}
getManager() {
return this.#manager_;
}
resize() {
super.resize();
this.#manager_.resize();
}
onMounted() {
super.onMounted();
this.#manager_.onMounted();
}
onUnmounted() {
this.#manager_.onUnmounted();
super.onUnmounted();
}
dispose() {
this.#manager_.dispose();
this.#manager_ = null;
super.dispose();
}
}
Mixly.StatusBarLibs = StatusBarLibs;
});

View File

@@ -0,0 +1,21 @@
goog.loadJs('common', () => {
goog.require('Mixly.StatusBar');
goog.provide('Mixly.StatusBarOutput');
const { StatusBar } = Mixly;
class StatusBarOutput extends StatusBar {
constructor() {
super();
}
init() {
super.init();
this.hideCloseBtn();
}
}
Mixly.StatusBarOutput = StatusBarOutput;
});

View File

@@ -0,0 +1,267 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('$.ui');
goog.require('$.flot');
goog.require('$.select2');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.PageBase');
goog.require('Mixly.Regression');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Config');
goog.provide('Mixly.StatusBarSerialChart');
const {
Env,
Msg,
PageBase,
Regression,
HTMLTemplate,
Config
} = Mixly;
const { USER, SELECTED_BOARD } = Config;
class StatusBarSerialChart extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-serial-chart.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-serial-chart.html')))
);
}
#data_ = [];
#totalPoints_ = SELECTED_BOARD?.serial?.pointNum ?? 100;
#yMax_ = SELECTED_BOARD?.serial?.yMax ?? 100;
#yMin_ = SELECTED_BOARD?.serial?.yMin ?? 0;
#needUpdate_ = false;
#regression_ = new Regression();
#plot_ = null;
#opened_ = false;
#started_ = false;
#$pointNum_ = null;
#$yMax_ = null;
#$yMin_ = null;
#isOpened_ = false;
constructor() {
super();
const template = HTMLTemplate.get('html/statusbar/statusbar-serial-chart.html');
const $template = $(template.render({
pointsNum: Msg.Lang['statusbar.serial.chart.pointsNum'],
yMin: Msg.Lang['statusbar.serial.chart.yMin'],
yMax: Msg.Lang['statusbar.serial.chart.yMax']
}));
this.setContent($template);
this.#$pointNum_ = $template.find('select');
this.#$pointNum_.select2({
width: '100%',
minimumResultsForSearch: Infinity,
dropdownCssClass: 'mixly-scrollbar'
});
this.#$pointNum_.val(this.#totalPoints_).trigger('change');
this.#$yMax_ = $template.find('.y-max');
this.#$yMax_.val(this.#yMax_);
this.#$yMin_ = $template.find('.y-min');
this.#$yMin_.val(this.#yMin_);
const axisFontColor = USER.theme === 'light' ? '#000' : '#c2c3c2';
this.#plot_ = $.plot($template.find('.figure'), this.getValue(), {
series: {
shadowSize: 1
},
colors: ['#777'],
yaxis: {
min: this.#yMin_,
max: this.#yMax_,
show: true,
font: {
fill: axisFontColor
}
},
xaxis: {
show: true,
font: {
fill: axisFontColor
},
mode: 'time',
timezone: 'browser',
twelveHourClock: true,
timeBase: 'milliseconds',
minTickSize: [1, 'second'],
min: Date.now(),
max: Date.now() + 1000 * 10,
}
});
this.#addEventsListener_();
}
#addEventsListener_() {
this.#$pointNum_.on('select2:select', (event) => {
const { data } = event.params;
this.#totalPoints_ = data.id;
this.setValue([]);
});
this.#$yMax_.change(() => {
const yMax = parseInt(this.#$yMax_.val());
if (isNaN(yMax) || yMax <= this.#yMin_) {
this.#$yMax_.val(this.#yMax_);
} else {
this.#yMax_ = yMax;
let { yaxis } = this.#plot_.getAxes();
yaxis.options.max = yMax;
}
});
this.#$yMin_.change(() => {
const yMin = parseInt(this.#$yMin_.val());
if (isNaN(yMin) || yMin >= this.#yMax_) {
this.#$yMin_.val(this.#yMin_);
} else {
this.#yMin_ = yMin;
let { yaxis } = this.#plot_.getAxes();
yaxis.options.min = yMin;
}
});
}
init() {
super.init();
this.hideCloseBtn();
this.resize();
}
resize() {
this.#plot_.getSurface().clearCache();
super.resize();
this.#plot_.resize();
this.#plot_.setupGrid(false);
this.#plot_.draw();
}
stop() {
this.setValue([]);
this.#opened_ = false;
this.#started_ = false;
}
start() {
this.#opened_ = true;
if (this.#started_) {
return;
}
this.#started_ = true;
this.update();
}
update() {
if (!this.#started_) {
return;
}
if (!this.isActive()) {
this.#started_ = false;
return;
}
if (!this.#needUpdate_) {
setTimeout(() => this.update(), 50);
return;
}
this.#plot_.setData(this.getValue());
this.#plot_.getSurface().clearCache();
this.#plot_.setupGrid(false);
this.#plot_.draw();
this.setRange(this.#plot_);
this.#needUpdate_ = false;
window.requestAnimationFrame(() => this.update());
}
getValue() {
return [{
data: this.#data_,
lines: {
show: true,
fill: false,
fillColor: '#007acc'
}
}];
}
addValue(data) {
if (!this.#started_) {
return;
}
if (this.#data_.length
&& data[0] === this.#data_[this.#data_.length - 1][0]) {
return;
}
while (this.#data_.length > this.#totalPoints_) {
this.#data_.shift();
}
this.#data_.push([Date.now(), data]);
this.#needUpdate_ = true;
}
setValue(data) {
if (!this.#started_) {
return;
}
this.#data_ = data;
this.#needUpdate_ = true;
}
setRange() {
let { xaxis } = this.#plot_.getAxes();
let { data = [] } = this.#plot_.getData()[0] ?? {};
if (!data.length) {
return;
}
if (data.length >= this.#totalPoints_) {
xaxis.options.min = data[0][0];
xaxis.options.max = data[this.#totalPoints_ - 1][0];
return;
}
let x = [], y = [];
for (let i in data) {
x.push((i - 0) + 1);
y.push(data[i][0] - data[0][0]);
}
this.#regression_.fit(x, y);
let xMax = this.#regression_.predict([this.#totalPoints_])[0] + data[0][0];
let xMin = data[0][0];
xaxis.options.min = xMin;
xaxis.options.max = xMax;
}
setStatus(isOpened) {
this.#isOpened_ = isOpened;
}
isOpened() {
return this.#isOpened_;
}
onMounted() {
super.onMounted();
if (this.#opened_) {
this.start();
}
}
onUnmounted() {
this.setValue([]);
super.onUnmounted();
}
dispose() {
this.#plot_.shutdown();
this.#$pointNum_.select2('destroy');
super.dispose();
}
}
Mixly.StatusBarSerialChart = StatusBarSerialChart;
});

View File

@@ -0,0 +1,155 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.PageBase');
goog.require('Mixly.StatusBar');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.HTMLTemplate')
goog.provide('Mixly.StatusBarSerialOutput');
const {
PageBase,
StatusBar,
Env,
Msg,
HTMLTemplate
} = Mixly;
class StatusBarSerialOutput extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-serial-output.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-serial-output.html')))
);
}
#$empty_ = null;
#$scroll_ = null;
#$timestamp_ = null;
#$hex_ = null;
#scroll_ = true;
#timestamp_ = false;
#hex_ = false;
#active_ = false;
#isOpened_ = false;
constructor() {
super();
const template = HTMLTemplate.get('html/statusbar/statusbar-serial-output.html');
const $content = $(template.render({
empty: Msg.Lang['statusbar.serial.output.empty'],
scroll: Msg.Lang['statusbar.serial.output.scroll'],
timestamp: Msg.Lang['statusbar.serial.output.timestamp']
}));
this.setContent($content);
this.#$empty_ = $content.find('.empty');
this.#$scroll_ = $content.find('.scroll');
this.#$timestamp_ = $content.find('.timestamp');
this.#$hex_ = $content.find('.hex');
this.addPage($content.find('.body'), 'editor', new StatusBar());
this.#addEventsListener_();
}
#addEventsListener_() {
this.#$empty_.click(() => {
this.empty({ startRow: 0 });
});
this.#$scroll_.change((event) => {
let scroll = false;
if (this.#$scroll_.prop('checked')) {
scroll = true;
}
this.#scroll_ = scroll;
});
this.#$timestamp_.change((event) => {
let timestamp = false;
if (this.#$timestamp_.prop('checked')) {
timestamp = true;
}
this.#timestamp_ = timestamp;
});
this.#$hex_.change((event) => {
let hex = false;
if (this.#$hex_.prop('checked')) {
hex = true;
}
this.#hex_ = hex;
});
}
init() {
super.init();
this.hideCloseBtn();
this.#$scroll_.prop('checked', this.#scroll_);
this.#$timestamp_.prop('checked', this.#timestamp_);
this.#$hex_.prop('checked', this.#hex_);
}
empty() {
const editor = this.getEditor();
const endRow = editor.session.getLength();
if (this.isOpened()) {
editor.session.removeFullLines(1, endRow);
this.addValue('\n');
} else {
editor.session.removeFullLines(0, endRow);
}
}
setStatus(isOpened) {
this.#isOpened_ = isOpened;
}
isOpened() {
return this.#isOpened_;
}
getValue() {
return this.getPage('editor').getValue();
}
setValue(data, scroll) {
this.getPage('editor').setValue(data, scroll);
}
addValue(data, scroll) {
this.getPage('editor').addValue(data, scroll);
}
getPageAce() {
return this.getPage('editor');
}
getEditor() {
return this.getPage('editor').getEditor();
}
getEditorPageId() {
return this.getPage('editor').getId();
}
getContextMenu() {
return this.getPage('editor').getContextMenu();
}
scrollChecked() {
return this.#scroll_;
}
timestampChecked() {
return this.#timestamp_;
}
hexChecked() {
return this.#hex_;
}
}
Mixly.StatusBarSerialOutput = StatusBarSerialOutput;
});

View File

@@ -0,0 +1,657 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('dayjs');
goog.require('$.select2');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Debug');
goog.require('Mixly.Config');
goog.require('Mixly.StatusBar');
goog.require('Mixly.SideBarsManager');
goog.require('Mixly.RightSideBarsManager');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.PageBase');
goog.require('Mixly.Menu');
goog.require('Mixly.StatusBarSerialOutput');
goog.require('Mixly.StatusBarSerialChart');
goog.require('Mixly.Electron.Serial');
goog.require('Mixly.Web.Serial');
goog.require('Mixly.WebSocket.Serial');
goog.provide('Mixly.StatusBarSerial');
const {
Env,
Msg,
Debug,
Config,
StatusBar,
SideBarsManager,
RightSideBarsManager,
HTMLTemplate,
PageBase,
Menu,
StatusBarSerialOutput,
StatusBarSerialChart,
Electron = {},
Web = {},
WebSocket = {}
} = Mixly;
let currentObj = null;
if (goog.isElectron) {
currentObj = Electron;
} else {
if (Env.hasSocketServer) {
currentObj = WebSocket;
} else {
currentObj = Web;
}
}
const { Serial } = currentObj;
const { SELECTED_BOARD } = Config;
class StatusBarSerial extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-serial.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbar-serial.html')))
);
SideBarsManager.typesRegistry.register(['serial_output'], StatusBarSerialOutput);
SideBarsManager.typesRegistry.register(['serial_chart'], StatusBarSerialChart);
this.getMenu = function () {
let ports = [];
let menu = { list: ports };
Serial.getCurrentPortsName().map((name) => {
ports.push(name);
});
if (!ports.length) {
menu.empty = Msg.Lang['statusbar.serial.noPort'];
}
return menu;
}
}
#$sendInput_ = null;
#$settingMenu_ = null;
#$scroll_ = null;
#$timestamp_ = null;
#$dtr_ = null;
#$rts_ = null;
#$hex_ = null;
#opened_ = false;
#valueTemp_ = '';
#receiveTemp_ = '';
#manager_ = null;
#output_ = null;
#chart_ = null;
#serial_ = null;
#port_ = '';
#config_ = {
baud: 115200,
dtr: true,
rts: false,
sendWith: '\r\n',
hex: false
};
#addTimestamp_ = false;
#maxLine_ = 200;
#lastUpdate_ = 0;
#refreshFrequency_ = 50;
#timer_ = null;
#reading_ = false;
constructor() {
super();
const template = HTMLTemplate.get('html/statusbar/statusbar-serial.html');
const $content = $(template.render({
sendInfo: Msg.Lang['statusbar.serial.sendInfo']
}));
this.setContent($content);
this.#$settingMenu_ = $content.find('.setting-menu');
this.#$settingMenu_.select2({
minimumResultsForSearch: Infinity,
dropdownAutoWidth: true,
dropdownCssClass: 'mixly-scrollbar'
});
this.id = template.getId();
this.#$sendInput_ = $content.find('.send > .box > input');
this.#$scroll_ = $content.find('.scroll');
this.#$timestamp_ = $content.find('.timestamp');
this.#$dtr_ = $content.find('.dtr');
this.#$rts_ = $content.find('.rts');
this.#$hex_ = $content.find('.hex');
this.#manager_ = new RightSideBarsManager($content.find('.content')[0]);
this.#manager_.add('serial_output', 'serial_output', Msg.Lang['statusbar.serial.output'], '');
this.#manager_.add('serial_chart', 'serial_chart', Msg.Lang['statusbar.serial.chart'], '');
this.#manager_.changeTo('serial_output');
this.#output_ = this.#manager_.get('serial_output');
this.#chart_ = this.#manager_.get('serial_chart');
this.#addCommandsForOutput_();
const config = Serial.getConfig();
this.#config_.dtr = config.dtr;
this.#config_.rts = config.rts;
this.#config_.baud = config.baudRates;
this.#config_.pointNum = config.pointNum;
this.#config_.reset = config.ctrlDBtn;
this.#config_.interrupt = config.ctrlCBtn;
this.#config_.yMax = config.yMax;
this.#config_.yMin = config.yMin;
}
#addCommandsForOutput_() {
const { commands } = this.#output_.getEditor();
commands.addCommands([{
name: 'copy',
bindKey: 'Ctrl-C',
readOnly: true,
exec: (editor) => {
const copyText = editor.getSelectedText();
if (!copyText) {
this.interrupt();
return true;
}
return false;
}
}, {
name: 'reset',
bindKey: 'Ctrl-D',
readOnly: true,
exec: (editor) => {
this.reset();
return true;
}
}]);
}
#addContextMenuItemsForOutput_() {
let menu = this.#output_.getContextMenu().getItem('code');
menu.add({
weight: 1,
type: 'sep1',
preconditionFn: () => {
return this.portExit();
},
data: '---------'
});
if (this.#config_.interrupt) {
menu.add({
weight: 2,
type: 'interrupt',
preconditionFn: () => {
return this.portExit();
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.serial.interrupt'], 'Ctrl+C'),
callback: (key, opt) => this.interrupt().catch(Debug.error)
}
});
}
if (this.#config_.reset) {
menu.add({
weight: 3,
type: 'reset',
preconditionFn: () => {
return this.portExit();
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.serial.reset'], 'Ctrl+D'),
callback: (key, opt) => this.reset().catch(Debug.error)
}
});
}
menu.add({
weight: 4,
type: 'toggle',
preconditionFn: () => {
return this.portExit();
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.serial.toggle'], ''),
callback: (key, opt) => this.toggle().catch(Debug.error)
}
});
}
#addEventsListener_() {
this.getTab().dblclick(() => {
if (!this.portExit()) {
return;
}
this.toggle().catch(Debug.error);
});
this.#serial_.bind('onOpen', () => {
this.setStatus(true);
const portName = this.getPortName();
this.setValue(
`==${Msg.Lang['statusbar.serial.port']} ${portName} ${Msg.Lang['statusbar.serial.open']}==\n`
);
this.#$sendInput_.attr('disabled', false);
if (this.#output_.timestampChecked()) {
this.#addTimestamp_ = true;
}
this.#chart_.start();
});
this.#serial_.bind('onClose', () => {
this.stopRead();
this.#timer_ && clearTimeout(this.#timer_);
this.#timer_ = null;
if (this.isDisposed() || !this.isOpened()) {
return;
}
this.setStatus(false);
const portName = this.getPortName();
const output = `${this.getValue() + this.#valueTemp_}\n`
+ `==${Msg.Lang['statusbar.serial.port']} ${portName} ${Msg.Lang['statusbar.serial.close']}==`;
this.setValue(output);
this.#valueTemp_ = '';
this.#receiveTemp_ = '';
this.#$sendInput_.val('');
this.#$sendInput_.attr('disabled', true);
this.#chart_.stop();
});
this.#serial_.bind('onError', (error) => {
this.stopRead();
this.#timer_ && clearTimeout(this.#timer_);
this.#timer_ = null;
if (this.isDisposed()) {
return;
}
if (!this.isOpened()) {
this.setValue(`${String(error)}\n`);
return;
}
this.setValue(`${this.getValue() + this.#valueTemp_}\n${String(error)}\n`);
this.#valueTemp_ = '';
});
this.#serial_.bind('onChar', (char) => {
if (!this.#reading_) {
this.#receiveTemp_ += char;
}
if (!this.#reading_ || this.#output_.hexChecked()) {
return;
}
if (this.#output_.timestampChecked()) {
if (this.#addTimestamp_) {
const timestamp = dayjs().format('HH:mm:ss.SSS');
this.addValue(`${timestamp} -> ${char}`);
} else {
this.addValue(char);
}
if (char === '\n') {
this.#addTimestamp_ = true;
} else {
this.#addTimestamp_ = false;
}
} else {
this.addValue(char);
}
if (this.#timer_) {
return;
}
this.#timer_ = setTimeout(this.#timedRefresh_.bind(this), this.#refreshFrequency_);
});
this.#serial_.bind('onString', (str) => {
if (!this.#chart_.isActive() || !this.isActive() || !this.#reading_) {
return;
}
const num = parseFloat(str);
if (isNaN(num)) {
return;
}
this.#chart_.addValue(num);
});
this.#serial_.bind('onByte', (byte) => {
if (!this.#reading_ || !this.#output_.hexChecked()) {
return;
}
let str = byte.toString(16).toUpperCase();
if (str.length < 2) {
str = '0' + str;
}
str = '0x' + str + (byte === 0x0A ? '\n' : ' ');
if (this.#output_.timestampChecked()) {
if (this.#addTimestamp_) {
const timestamp = dayjs().format('HH:mm:ss.SSS');
this.addValue(`${timestamp} -> ${str}`);
} else {
this.addValue(str);
}
if (byte === 0x0A) {
this.#addTimestamp_ = true;
} else {
this.#addTimestamp_ = false;
}
} else {
this.addValue(str);
}
if (this.#timer_) {
return;
}
this.#timer_ = setTimeout(this.#timedRefresh_.bind(this), this.#refreshFrequency_);
});
this.#$settingMenu_.on('select2:select', (event) => {
const { id } = event.currentTarget.dataset;
const { data } = event.params;
if (id === 'baud') {
this.setBaudRate(data.id - 0).catch(Debug.error);
} else if (id === 'send-with') {
if (data.id === 'no') {
this.#config_.sendWith = '';
} else {
this.#config_.sendWith = data.id.replace('\\r', '\r').replace('\\n', '\n');
}
}
});
this.#$sendInput_.keydown((event) => {
if (event.keyCode !== 13) {
return;
}
const { sendWith } = this.#config_;
let data = this.#$sendInput_.val();
if (this.#config_.hex) {
let hexstr = data.split(' ');
let hexs = [];
for (let str of hexstr) {
let hex = parseInt(str, 16);
if (isNaN(hex)) {
continue;
}
hexs.push(hex);
}
for (let char of sendWith) {
hexs.push(char.charCodeAt(0));
}
this.#serial_.sendBuffer(hexs).catch(Debug.error);
} else {
data += sendWith;
this.#serial_.sendString(data).catch(Debug.error);
}
this.#$sendInput_.val('');
});
this.#$dtr_.change((event) => {
let dtr = false;
if (this.#$dtr_.prop('checked')) {
dtr = true;
}
if (this.isOpened()) {
this.#serial_.setDTR(dtr)
.then(() => {
this.#config_.dtr = dtr;
})
.catch(Debug.error);
} else {
this.#config_.dtr = dtr;
}
});
this.#$rts_.change((event) => {
let rts = false;
if (this.#$rts_.prop('checked')) {
rts = true;
}
if (this.isOpened()) {
this.#serial_.setRTS(rts)
.then(() => {
this.#config_.rts = rts;
})
.catch(Debug.error);
} else {
this.#config_.rts = rts;
}
});
this.#$hex_.change((event) => {
let hex = false;
if (this.#$hex_.prop('checked')) {
hex = true;
}
this.#config_.hex = hex;
});
}
#timedRefresh_() {
this.#timer_ = null;
if (!this.#valueTemp_ || !this.isOpened()) {
return;
}
if (Date.now() - this.#lastUpdate_ < this.#refreshFrequency_) {
this.#timer_ = setTimeout(this.#timedRefresh_.bind(this), this.#refreshFrequency_);
return;
}
this.addValue('');
}
init() {
super.init();
this.addDirty();
this.setMarkStatus('negative');
const $tab = this.getTab();
this.#port_ = $tab.attr('data-tab-id');
this.#serial_ = new Serial(this.getPortName());
this.#serial_.config(this.#config_).catch(Debug.error);
this.#addEventsListener_();
this.#addContextMenuItemsForOutput_();
this.setValue(this.#valueTemp_);
this.#valueTemp_ = '';
this.#$settingMenu_.filter('[data-id="baud"]').val(this.#config_.baud).trigger('change');
this.#$dtr_.prop('checked', this.#config_.dtr);
this.#$rts_.prop('checked', this.#config_.rts);
}
async open() {
await this.#serial_.open(this.#config_.baud);
if (SELECTED_BOARD?.serial?.ctrlCBtn) {
await this.#serial_.sleep(1000);
await this.#serial_.interrupt();
const startTime = Number(new Date());
let endTime = startTime;
while (endTime - startTime < 2000) {
await this.#serial_.sleep(50);
if (this.#receiveTemp_.indexOf('>>>') !== -1) {
break;
}
endTime = Number(new Date());
}
this.#valueTemp_ = '';
this.empty();
this.startRead();
this.#receiveTemp_ = '';
await this.#serial_.reset();
} else {
this.startRead();
}
}
async close() {
await this.#serial_.close();
}
async toggle() {
if (this.isOpened()) {
await this.#serial_.close();
} else {
await this.#serial_.open();
await this.#serial_.sleep(200);
try {
await this.#serial_.setDTRAndRTS(this.#config_.dtr, this.#config_.rts);
} catch (error) {
Debug.error(error);
}
this.startRead();
}
}
async interrupt() {
await this.#serial_.open(this.#config_.baud);
this.startRead();
await this.#serial_.interrupt();
}
async reset() {
await this.#serial_.open(this.#config_.baud);
this.startRead();
await this.#serial_.interrupt();
await this.#serial_.reset();
}
setStatus(isOpened) {
if (this.isOpened() === isOpened) {
return;
}
this.#opened_ = isOpened;
if (isOpened) {
this.setMarkStatus('positive');
} else {
this.setMarkStatus('negative');
}
this.#output_.setStatus(isOpened);
this.#chart_.setStatus(isOpened);
}
async setBaudRate(baud) {
if (!this.isOpened()) {
this.#config_.baud = baud;
return;
}
if (this.#serial_.baudRateIsLegal(baud)) {
try {
await this.#serial_.setBaudRate(baud);
this.#config_.baud = baud;
} catch (error) {
Debug.error(error);
}
}
this.#$settingMenu_.filter('[data-id="baud"]').val(this.#config_.baud).trigger('change');
this.startRead();
}
isOpened() {
return this.#opened_;
}
portExit() {
const portsName = Serial.getCurrentPortsName();
return portsName.includes(this.getPortName());
}
getPortName() {
return this.#port_;
}
getSerial() {
return this.#serial_;
}
startRead() {
this.#reading_ = true;
}
stopRead() {
this.#reading_ = false;
}
dispose() {
this.#$settingMenu_.select2('destroy');
this.#$settingMenu_ = null;
this.#serial_.close()
.catch(Debug.error)
.finally(() => {
this.#$sendInput_ = null;
this.#$settingMenu_ = null;
this.#$scroll_ = null;
this.#$timestamp_ = null;
this.#$dtr_ = null;
this.#$rts_ = null;
this.#$hex_ = null;
this.#output_ = null;
this.#chart_ = null;
this.#manager_.dispose();
this.#manager_ = null;
this.#serial_.dispose();
this.#serial_ = null;
super.dispose();
});
}
getValue() {
if (!this.isInited()) {
return this.#valueTemp_;
} else {
return this.#output_.getValue();
}
}
empty() {
this.#output_.empty();
}
setValue(data) {
if (!this.isInited()) {
this.#valueTemp_ = data;
return;
}
this.#output_.setValue(data, this.#output_.scrollChecked());
}
addValue(data) {
if (!this.isInited()) {
this.#valueTemp_ += data;
return;
}
this.#valueTemp_ += data;
if (Date.now() - this.#lastUpdate_ < this.#refreshFrequency_ || !this.isOpened()) {
return;
}
this.#output_.addValue(this.#valueTemp_, this.#output_.scrollChecked());
this.#valueTemp_ = '';
const editor = this.#output_.getEditor();
const row = editor.session.getLength();
if (row > this.#maxLine_) {
const initCursor = editor.selection.getCursor();
const removedLine = row - this.#maxLine_;
editor.session.removeFullLines(1, removedLine);
}
this.#lastUpdate_ = Date.now();
}
getManager() {
return this.#manager_;
}
resize() {
super.resize();
this.getManager().resize();
}
onMounted() {
super.onMounted();
this.#manager_.onMounted();
}
onUnmounted() {
this.#manager_.onUnmounted();
super.onUnmounted();
}
}
Mixly.StatusBarSerial = StatusBarSerial;
});

View File

@@ -0,0 +1,89 @@
goog.loadJs('common', () => {
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Config');
goog.require('Mixly.EditorAce');
goog.require('Mixly.ContextMenu');
goog.require('Mixly.Menu');
goog.require('Mixly.Msg');
goog.require('Mixly.IdGenerator');
goog.provide('Mixly.StatusBar');
const {
XML,
Env,
Config,
EditorAce,
ContextMenu,
Menu,
Msg,
IdGenerator
} = Mixly;
const { USER } = Config;
class StatusBar extends EditorAce {
#contextMenu_ = null;
constructor() {
super();
this.#addContextMenu_();
}
init() {
super.init();
this.#toStatusBar_();
}
#addContextMenu_() {
this.#contextMenu_ = new ContextMenu(`div[page-id="${this.getId()}"]`, {
zIndex: 300
});
let menu = new Menu();
menu.add({
weight: 0,
id: 'copy',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], 'Ctrl+C'),
callback: (key, opt) => this.copy()
}
});
this.#contextMenu_.register('code', menu);
this.#contextMenu_.bind('getMenu', () => 'code');
}
#toStatusBar_() {
const editor = this.getEditor();
if (USER.theme === 'dark') {
editor.setOption('theme', 'ace/theme/tomorrow_night');
} else {
editor.setOption('theme', 'ace/theme/xcode');
}
editor.getSession().setMode('ace/mode/python');
editor.setReadOnly(true);
// editor.setScrollSpeed(0.3);
editor.setShowPrintMargin(false);
editor.renderer.setShowGutter(false);
editor.setOptions({
enableBasicAutocompletion: false,
enableSnippets: false,
enableLiveAutocompletion: false
});
editor.setHighlightActiveLine(false);
}
getContextMenu() {
return this.#contextMenu_;
}
dispose() {
this.#contextMenu_.dispose();
this.#contextMenu_ = null;
super.dispose();
}
}
Mixly.StatusBar = StatusBar;
});

View File

@@ -0,0 +1,289 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('layui');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Registry');
goog.require('Mixly.Events');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.StatusBarOutput');
goog.require('Mixly.StatusBarSerial');
goog.require('Mixly.StatusBarFS');
goog.require('Mixly.StatusBarLibs');
goog.require('Mixly.StatusBarAmpy')
goog.require('Mixly.PagesManager');
goog.require('Mixly.DropdownMenu');
goog.require('Mixly.Menu');
goog.require('Mixly.IdGenerator');
goog.require('Mixly.Serial');
goog.require('Mixly.Config');
goog.require('Mixly.Debug');
goog.provide('Mixly.StatusBarsManager');
const {
XML,
Env,
Msg,
Registry,
Events,
HTMLTemplate,
StatusBarOutput,
StatusBarSerial,
StatusBarFS,
StatusBarLibs,
StatusBarAmpy,
PagesManager,
DropdownMenu,
Menu,
IdGenerator,
Serial,
Config,
Debug
} = Mixly;
const { layer } = layui;
const { BOARD } = Config;
class StatusBarsManager extends PagesManager {
static {
HTMLTemplate.add(
'html/statusbar/statusbars-manager.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbars-manager.html')))
);
HTMLTemplate.add(
'html/statusbar/statusbars-tab.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/statusbar/statusbars-tab.html')))
);
this.typesRegistry = new Registry();
this.managersRegistry = new Registry();
this.typesRegistry.register(['#default', 'terminal'], StatusBarOutput);
this.typesRegistry.register(['serial'], StatusBarSerial);
this.typesRegistry.register(['board-fs'], StatusBarFS);
this.typesRegistry.register(['libs'], StatusBarLibs);
this.typesRegistry.register(['ampy'], StatusBarAmpy);
this.getMain = function() {
if (!this.managersRegistry.length()) {
return null;
}
const key = this.managersRegistry.keys()[0];
return this.managersRegistry.getItem(key);
}
this.add = function(manager) {
this.managersRegistry.register(manager.id, manager);
}
this.remove = function(manager) {
this.managersRegistry.unregister(manager.id);
}
}
#shown_ = false;
#dropdownMenu_ = null;
#$dropdownBtn_ = null;
constructor(element) {
const managerHTMLTemplate = HTMLTemplate.get('html/statusbar/statusbars-manager.html');
const tabHTMLTemplate = HTMLTemplate.get('html/statusbar/statusbars-tab.html');
const $manager = $(managerHTMLTemplate.render());
const $tab = $(tabHTMLTemplate.render());
super({
parentElem: element,
managerContentElem: $manager[0],
bodyElem: $manager.find('.body')[0],
tabElem: $manager.find('.tabs')[0],
tabContentElem: $tab[0],
typesRegistry: StatusBarsManager.typesRegistry
});
this.tabId = tabHTMLTemplate.id;
this.id = IdGenerator.generate();
this.addEventsType(['show', 'hide', 'onSelectMenu', 'getMenu']);
this.#$dropdownBtn_ = $tab.find('.statusbar-dropdown-menu > button');
$tab.find('.statusbar-close > button').click(() => this.hide());
this.#addDropdownMenu_();
this.#addEventsListener_();
StatusBarsManager.add(this);
}
getStatusBarById(id) {
return this.get(id);
}
removeStatusBarById(id) {
this.remove(id);
}
show() {
this.runEvent('show');
}
hide() {
this.runEvent('hide');
}
toggle() {
this.isShown() ? this.hide() : this.show();
}
isShown() {
return this.#shown_;
}
openSelectedPort() {
const port = Serial.getSelectedPortName();
if (port) {
this.show();
this.#onSelectMenu_(port);
} else {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
}
}
#addDropdownMenu_() {
let menu = new Menu();
let serialChildMenu = new Menu(true);
menu.add({
weight: 0,
id: 'serial-default',
preconditionFn: () => {
return !!Serial.getCurrentPortsName().length;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.openSelectedPort'], ''),
callback: (key, opt) => {
this.openSelectedPort();
}
}
});
menu.add({
weight: 1,
id: 'serial',
children: serialChildMenu,
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.openPort'], '')
}
});
/*menu.add({
weight: 2,
id: 'lib',
data: {
isHtmlName: true,
name: Menu.getItem('第三方库管理', ''),
callback: (key, opt) => {
this.add('libs', 'libs', '第三方库管理');
this.changeTo('libs');
}
}
});*/
if (['micropython', 'circuitpython'].includes(BOARD.language.toLowerCase())
&& !['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
menu.add({
weight: 2,
id: 'sep1',
data: '---------'
});
menu.add({
weight: 3,
id: 'ampy',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy'], ''),
callback: (key, opt) => {
this.add({
id: 'ampy',
type: 'ampy',
name: Msg.Lang['statusbar.ampy'],
title: Msg.Lang['statusbar.ampy']
});
this.changeTo('ampy');
}
}
});
}
serialChildMenu.bind('onRead', () => {
const options = this.#getMenu_() ?? {};
options.list = options.list ?? [];
options.empty = options.empty ?? Msg.Lang['statusbar.serial.noPort'];
const result = [];
if (!options.list.length) {
result.push({
weight: 1,
id: 'empty',
data: {
isHtmlName: true,
name: options.empty,
disabled: true
}
});
}
for (let i in options.list) {
result.push({
weight: 1,
id: `serial${i}`,
data: {
isHtmlName: true,
name: options.list[i],
callback: (key, opt) => this.#onSelectMenu_(options.list[i])
}
});
}
return result;
});
this.#dropdownMenu_ = new DropdownMenu(this.#$dropdownBtn_[0]);
this.#dropdownMenu_.register('menu', menu);
this.#dropdownMenu_.bind('getMenu', () => 'menu');
}
#onSelectMenu_(port) {
this.runEvent('onSelectMenu', port);
}
#getMenu_() {
let menus = this.runEvent('getMenu');
if (menus && menus.length) {
return menus[0];
}
return { list: [], empty: Msg.Lang['statusbar.dropdownMenu.noOptions'] };
}
#addEventsListener_() {
this.bind('getMenu', () => {
return StatusBarSerial.getMenu();
});
this.bind('onSelectMenu', (port) => {
this.add('serial', port);
this.changeTo(port);
const statusBarSerial = this.getStatusBarById(port);
if (statusBarSerial.isInited() && !statusBarSerial.isOpened()) {
statusBarSerial.open().catch(Debug.error);
}
});
}
getDropdownMenu() {
return this.#dropdownMenu_;
}
dispose() {
StatusBarsManager.remove(this);
this.#dropdownMenu_.dispose();
this.#$dropdownBtn_.remove();
this.#$dropdownBtn_ = null;
super.dispose();
}
}
Mixly.StatusBarsManager = StatusBarsManager;
});

View File

@@ -0,0 +1,54 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('layui');
goog.require('Mixly.LocalStorage');
goog.require('Mixly.Config');
goog.provide('Mixly.Storage');
const {
LocalStorage,
Config,
Storage
} = Mixly;
const { laytpl } = layui;
const { BOARD } = Config;
Storage.user = function (key, value) {
let storagePath = path.join(LocalStorage.PATH['USER'], key);
if (arguments.length > 1) {
LocalStorage.set(storagePath, value);
} else {
value = LocalStorage.get(storagePath);
}
return value;
}
Storage.board = function (key, value) {
let storagePath = path.join(laytpl(LocalStorage.PATH['BOARD']).render({
boardType: BOARD.boardType
}), key);
if (arguments.length > 1) {
LocalStorage.set(storagePath, value);
} else {
value = LocalStorage.get(storagePath);
}
return value;
}
Storage.thirdParty = function (name, key, value) {
let storagePath = path.join(laytpl(LocalStorage.PATH['THIRD_PARTY']).render({
boardType: BOARD.boardType,
thirdPartyName: name ?? 'default'
}), key);
if (arguments.length > 1) {
LocalStorage.set(storagePath, value);
} else {
value = LocalStorage.get(storagePath);
}
return value;
}
});

View File

@@ -0,0 +1,70 @@
goog.loadJs('common', () => {
goog.require('Mixly.Config');
goog.require('Mixly.Debug');
goog.provide('Mixly.Title');
let {
Config,
Debug,
Title
} = Mixly;
let { BOARD, SOFTWARE } = Config;
if (SOFTWARE?.version && BOARD?.boardType) {
document.title = SOFTWARE.version + " For " + BOARD.boardType;
}
Title.title = document.title;
Title.NOWTITLE = Title.title;
Title.updeteVersionNumber = (newVersionNumber) => {
try {
Title.NOWTITLE = document.title.replace(/Mixly[\s]?[\d.]+/g, "Mixly " + newVersionNumber);
document.title = Title.NOWTITLE;
} catch (error) {
Debug.error(error);
}
}
Title.getVersionNumber = () => {
try {
Title.NOWTITLE = document.title.match(/Mixly[\s]?[\d.]+/g);
return Title.NOWTITLE;
} catch (error) {
Debug.error(error);
return '';
}
}
Title.updeteFilePath = function (newPath) {
try {
var pathArr = Title.NOWTITLE.match(/\([^\n\r]+\)/g);
if (pathArr) {
Title.NOWTITLE = document.title.replace(/\([^\n\r]+\)/g, "(" + newPath + ")");
document.title = Title.NOWTITLE;
} else {
Title.NOWTITLE = document.title + " (" + newPath + ")";
document.title = Title.NOWTITLE;
}
} catch (error) {
Debug.error(error);
}
}
Title.getFilePath = () => {
try {
let filePathArr = document.title.match(/(?<=\()[^\n\r]+(?=\))/g);
if (filePathArr && filePathArr.length > 0) {
return filePathArr[0];
}
} catch (error) {
Debug.error(error);
}
return null;
}
Title.updateTitle = (newTitle) => {
Title.NOWTITLE = newTitle;
document.title = newTitle;
}
});

View File

@@ -0,0 +1,208 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Blockly');
goog.require('Mixly.Env');
goog.require('Mixly.XML');
goog.require('Mixly.Msg');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Debug');
goog.provide('Mixly.ToolboxSearcher');
const {
Env,
XML,
Msg,
HTMLTemplate,
Debug
} = Mixly;
class ToolboxSearcher {
static {
this.searchHtmlTemplate = new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/search-div.html'))
);
}
constructor(mainWorkspace) {
this.mainWorkspace = mainWorkspace;
this.searchWorkspace = new Blockly.Workspace(new Blockly.Options({
toolbox: null
}));
this.mainToolbox = this.mainWorkspace.getToolbox();
this.$search = $(ToolboxSearcher.searchHtmlTemplate.render({
search: Msg.Lang['toolboxSearcher.search']
}));
this.$i = this.$search.find('i');
this.$input = this.$search.find('input');
this.prevText = '';
$(this.mainToolbox.HtmlDiv).append(this.$search);
this.addEventsListener();
}
addEventsListener() {
this.$input.change(() => this.startSearch());
this.$input.bind('input propertychange', (event) => {
const searchCategory = this.mainToolbox.getToolboxItemById('catSearch');
const keyText = event.target.value;
if (!keyText.replaceAll(' ', '')) {
searchCategory.hide();
this.$i.addClass('disabled');
return;
} else {
searchCategory.show();
}
this.scrollTop();
if (keyText === this.prevText) {
this.$i.addClass('disabled');
} else {
this.$i.removeClass('disabled');
}
});
}
scrollTop() {
const { HtmlDiv } = this.mainToolbox;
$(HtmlDiv).scrollTop(HtmlDiv.scrollHeight);
}
checkBlock(blockxml, keys) {
this.searchWorkspace.clear();
try {
Blockly.Xml.domToBlock(blockxml, this.searchWorkspace);
} catch (error) {
Debug.error(error);
return false;
}
const blocks = this.searchWorkspace.getAllBlocks(true);
let select = false;
for (let block of blocks) {
const { inputList } = block;
for (let input of inputList) {
const { fieldRow } = input;
for (let field of fieldRow) {
const fieldText = field.getText().toLowerCase();
let times = 0;
for (let key = 0; key < keys.length; key++) {
if (fieldText.indexOf(keys[key]) === -1) {
continue;
}
times++;
}
if (keys.length === times) {
return true;
}
}
}
}
return false;
}
searchBlocks(keys) {
return new Promise((resolve, reject) => {
const searchCategory = this.mainToolbox.getToolboxItemById('catSearch');
let outputXML = [];
let selectedBlocksLen = 0;
const categories = this.mainToolbox.getToolboxItems();
for (let j = 0; categories[j]; j++) {
const category = categories[j];
if (category.id_ === 'catSearch') continue;
if (typeof category.getContents !== 'function') continue;
const blocksList = category.getContents();
let addLabel = true;
for (let blockDef of blocksList) {
const { type, kind, blockxml } = blockDef;
if (kind !== 'BLOCK') {
continue;
}
if (!this.checkBlock(blockxml, keys)) {
continue;
}
if (addLabel) {
const categoryPath = this.getCategoryPath(category);
outputXML.push({
kind: 'LABEL',
text: categoryPath
});
addLabel = false;
}
outputXML.push(blockDef);
selectedBlocksLen++;
if (selectedBlocksLen > 30) {
break;
}
}
if (selectedBlocksLen > 30) {
outputXML.unshift({
kind: 'LABEL',
text: Msg.Lang['toolboxSearcher.tooManyResultsInfo']
});
break;
}
}
this.searchWorkspace.clear();
searchCategory.updateFlyoutContents(
outputXML.length ?
outputXML : [{
kind: 'LABEL',
text: Msg.Lang['toolboxSearcher.empty']
}]
);
this.mainToolbox.refreshSelection();
this.mainToolbox.setSelectedItem(searchCategory);
const { selectedItem_ } = this.mainToolbox;
if (selectedItem_ && selectedItem_.isCollapsible()) {
selectedItem_.setExpanded(true);
}
this.scrollTop();
resolve();
});
}
getCategoryPath(category) {
let categoryPath = '';
for (; category; category = category.getParent()) {
categoryPath = category.toolboxItemDef_.name + (categoryPath && (' > ' + categoryPath));
}
return categoryPath;
}
startSearch() {
if (this.$i.hasClass('disabled')) {
return
};
let text = this.$input.val();
this.prevText = text;
try {
if (!text.replaceAll(' ', '')) {
return;
}
} catch(error) {
Debug.error(error);
}
this.$input.attr('disabled', true);
this.$i.removeClass('codicon-search-fuzzy');
this.$i.addClass('codicon-loading layui-anim-rotate layui-anim-loop');
setTimeout(() => {
let keys = text.toLowerCase().split(' ');
this.searchBlocks(keys)
.catch(Debug.error)
.finally(() => {
this.$i.removeClass('codicon-loading layui-anim-rotate layui-anim-loop');
this.$i.addClass('codicon-search-fuzzy disabled');
this.$input.removeAttr('disabled');
});
}, 100);
}
restart() {
this.prevText = '';
const searchCategory = this.mainToolbox.getToolboxItemById('catSearch');
this.$input.val('');
searchCategory.hide();
}
}
Mixly.ToolboxSearcher = ToolboxSearcher;
});

View File

@@ -0,0 +1,213 @@
goog.loadJs('common', () => {
goog.require('Mixly');
goog.provide('Mixly.Url');
const { Url } = Mixly;
/**
* @function 输入url返回json
* @param url {String} 输入的url字符串
* @return object
*/
Url.urlToJson = (url) => {
// 递归字符串生成json对象
function strToObj(obj, str, value) {
if(str.indexOf('.') !== -1) {
let key = str.substring(0, str.indexOf('.'));
str = str.substring(str.indexOf('.') + 1, str.length);
if (obj[key] === undefined) {
obj[key] = {};
}
obj[key] = strToObj(obj[key], str, value);
return obj;
} else {
const decodeValue = decodeURIComponent(value);
const decodeKey = decodeURIComponent(str);
if (isNaN(decodeValue)) {
const decodeValueLower = decodeValue.toLowerCase();
switch (decodeValueLower) {
case 'true':
obj[decodeKey] = true;
break;
case 'false':
obj[decodeKey] = false;
break;
case 'undefined':
obj[decodeKey] = undefined;
break;
case 'null':
obj[decodeKey] = null;
break;
default:
obj[decodeKey] = decodeValue;
}
} else {
obj[decodeKey] = decodeValue-0;
}
return obj;
}
}
var hash;
var myJson = {};
var hashes = url.slice(url.indexOf('?') + 1).split('&');
for (var i = 0; i < hashes.length; i++) {
hash = hashes[i].split('=');
try {
var hash0 = hash[0].replaceAll('@', '=');
hash0 = hash0.replaceAll('$', '&');
var hash1 = hash[1].replaceAll('@', '=');
hash1 = hash1.replaceAll('$', '&');
myJson = strToObj(myJson, hash0, hash1);
} catch (e) {
myJson = strToObj(myJson, hash[0], hash[1]);
}
}
return myJson;
}
/**
* @function JSON对象转Url字符串
* @param param {object | array | string | number | boolean} 传入的JSON对象或者是转换过程中某个键的对应值
* @param key {null | string} 转换过程中传入的JSON对象中的某个键
* @return {string} 转换后的Url字符串
**/
Url.jsonToUrl = (param, key = null) => {
var paramStr = '';
if (param instanceof String || param instanceof Number || param instanceof Boolean) {
try {
var newKey = key.toString().replaceAll('=', '@');
newKey = newKey.replaceAll('&', '$');
var newParam = param.toString().replaceAll('=', '@')
newParam = newParam.replaceAll('&', '$');
paramStr += '&' + newKey + '=' + encodeURIComponent(newParam);
} catch (e) {
//console.log(e);
paramStr += '&' + key + '=' + encodeURIComponent(param);
}
} else {
$.each(param, function (i) {
var k = key == null ? i : key + (param instanceof Array ? '[' + i + ']' : '.' + i);
paramStr += '&' + Url.jsonToUrl(this, k);
});
}
return paramStr.substr(1);
};
/**
* @function 获取主页面传递的配置信息
* @return {object}
*/
Url.getConfig = () => {
var href = '';
try {
href = window.location.href.replaceAll('#', '');
} catch (e) {
//console.log(e);
href = window.location.href;
}
href = href.substring(href.indexOf('?') + 1, href.length);
var boardConfig = Url.urlToJson(href);
return boardConfig;
}
/**
* @function 更改传入Url字符串中某个参数的值如果没有则添加该参数
* @param url {string} Url字符串
* @param arg {string} 参数名
* @param argVal {string} 参数名对应值
* @return {string} 修改后的Url字符串
**/
Url.changeURLArg = (url, arg, argVal) => {
var pattern = arg + '=([^&]*)';
var replaceText = arg + '=' + argVal;
if (url.match(pattern)) {
var tmp = '/(' + arg + '=)([^&]*)/gi';
tmp = url.replace(eval(tmp), replaceText);
return tmp;
} else {
if (url.match('[\?]')) {
return url + '&' + replaceText;
} else {
return url + '?' + replaceText;
}
}
}
/**
* @function 求解某个字符串的CRC32值
* @param str {string} 传入的字符串
* @param radix {number} 返回文本的进制,默认十进制
* @return {string}
**/
Url.CRC32 = (str, radix = 10) => {
const Utf8Encode = function (string) {
string = string.replace(/\r\n/g, '\n');
let text = '';
for (let n = 0; n < string.length; n++) {
const c = string.charCodeAt(n);
if (c < 128) {
text += String.fromCharCode(c);
} else if ((c > 127) && (c < 2048)) {
text += String.fromCharCode((c >> 6) | 192);
text += String.fromCharCode((c & 63) | 128);
} else {
text += String.fromCharCode((c >> 12) | 224);
text += String.fromCharCode(((c >> 6) & 63) | 128);
text += String.fromCharCode((c & 63) | 128);
}
}
return text;
}
const makeCRCTable = function () {
let c;
const crcTable = [];
for (let n = 0; n < 256; n++) {
c = n;
for (let k = 0; k < 8; k++) {
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
crcTable[n] = c;
}
return crcTable;
}
const crcTable = makeCRCTable();
const strUTF8 = Utf8Encode(str);
let crc = 0 ^ (-1);
for (let i = 0; i < strUTF8.length; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ strUTF8.charCodeAt(i)) & 0xFF];
}
crc = (crc ^ (-1)) >>> 0;
return crc.toString(radix);
};
/**
* @function 获取当前网页的IP地址
* @return {string | null}
**/
Url.getIPAddress = () => {
const url = window.location.host;
const IPList = url.match(/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/g);
if (IPList && IPList.length > 0) {
return IPList[0];
}
return null;
}
/**
* @function 在新窗口打开某个网址
* @param href {string} 传入的网址
* @return {void}
**/
Url.open = (href) => {
if (goog.isElectron) {
const { shell } = Mixly.require('electron');
shell.openExternal(href);
} else {
window.open(href, '_blank');
}
}
});

View File

@@ -0,0 +1,134 @@
goog.loadJs('common', () => {
goog.require('Blockly');
goog.require('Mixly.MFile');
goog.require('Mixly.Config');
goog.require('Mixly.Boards');
goog.require('Mixly.LocalStorage');
goog.provide('Mixly.UserEvents');
const {
MFile,
Config,
Boards,
LocalStorage
} = Mixly;
const { USER, SOFTWARE } = Config;
class UserEvents {
#DEFAULT_DATA = {
uid: () => {
return LocalStorage.get('Authorization/user_id') ?? '';
},
mid: () => {
return LocalStorage.get('module_id') ?? '';
},
type: () => {
return Boards.getSelectedBoardName();
},
code: () => {
return MFile.getCode();
},
blocks: () => {
return MFile.getMil();
},
time: () => {
return (new Date()).toLocaleString();
},
fileName: () => {
return LocalStorage.get('file_name') ?? '';
}
};
#actionFlow_ = [];
#prevCode_ = '';
constructor(workspace) {
this.workspace = workspace;
this.#addBlocklyEventListener_();
}
#addBlocklyEventListener_() {
this.blocklyEventListener = this.workspace.addChangeListener((event) => {
if (![
Blockly.Events.BLOCK_MOVE,
Blockly.Events.BLOCK_DELETE,
Blockly.Events.BLOCK_CREATE
].includes(event.type)) {
return;
}
const currentCode = MFile.getCode();
if (Blockly.Events.BLOCK_MOVE === event.type && this.#prevCode_ === currentCode) {
return;
}
this.#prevCode_ = currentCode;
const recordLine = {};
recordLine.blockId = event.blockId;
recordLine.currentCode = currentCode;
recordLine.currentBlocks = MFile.getMil();
recordLine.time = (new Date()).toLocaleString();
let actionType = 1;
switch (event.type) {
case Blockly.Events.BLOCK_MOVE:
let block = this.workspace.getBlockById(event.blockId);
if (!block) {
return;
}
recordLine.blockType = block.type;
actionType = 3;
break;
case Blockly.Events.BLOCK_DELETE:
recordLine.blockType = event.oldJson.type;
actionType = 2;
break;
case Blockly.Events.BLOCK_CREATE:
recordLine.blockType = event.json.type;
actionType = 1;
break;
}
recordLine.actionType = actionType;
this.addFlowItem(recordLine);
});
}
addRecord(message) {
if (this.flowIsEmpty()) {
return;
}
let data = {};
for (let key in this.#DEFAULT_DATA) {
data[key] = this.#DEFAULT_DATA[key]();
}
for (let i in message) {
data[i] = message[i];
}
data.actionFlow = this.getFlowItems();
$.post(SOFTWARE?.behaviorRecord?.url, data, () => {
this.resetFlow();
})
.fail(() => {
this.resetFlow();
});
}
addFlowItem(data) {
this.#actionFlow_.push(data);
}
getFlowItems() {
return this.#actionFlow_;
}
resetFlow() {
this.#actionFlow_ = [];
}
flowIsEmpty() {
return this.#actionFlow_.length === 0;
}
}
Mixly.UserEvents = UserEvents;
});

View File

@@ -0,0 +1,84 @@
goog.loadJs('common', () => {
goog.require('Mixly.MFile');
goog.require('Mixly.Config');
goog.require('Mixly.Boards');
goog.provide('Mixly.UserOPEvents');
const {
MFile,
Boards,
Config,
Editor
} = Mixly;
const { USER } = Config;
class UserOPEvents {
#DEFAULT_DATA = {
code: () => {
return MFile.getCode();
},
board_name: () => {
return Boards.getSelectedBoardName();
},
time: () => {
return (new Date()).toLocaleString();
},
blocks: () => {
return MFile.getMil();
},
output: () => {
const { mainStatusBarTab } = Mixly;
const statusBarTerminal = mainStatusBarTab.getStatusBarById('output');
return statusBarTerminal.getValue();
},
id: () => {
return USER.visitorId.str32CRC32b;
}
};
#actionArrayRecord = [];
constructor() {
this.addTimer();
}
sendAll() {
let sendPromise = [];
let len = this.#actionArrayRecord.length;
for (;this.#actionArrayRecord.length;) {
let data = this.#actionArrayRecord.shift();
sendPromise.push(this.send(data));
}
Promise.all(sendPromise)
.finally(() => {
this.addTimer();
});
}
send(data) {
for (let key in this.#DEFAULT_DATA) {
data[key] = this.#DEFAULT_DATA[key]();
}
return new Promise((resolve, reject) => {
$.post('https://cc.mixly.cn/api/behaviorrecord', data, function() {
resolve();
})
.fail(function() {
resolve();
});
});
}
addTimer() {
setTimeout(() => this.sendAll(), 10000);
}
addRecord(data) {
if (Editor.mainEditor.selected === 'BLOCK') {
this.#actionArrayRecord.push(data);
}
}
}
Mixly.UserOPEvents = UserOPEvents;
});

View File

@@ -0,0 +1,223 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('Mixly.XML');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Drag');
goog.require('Mixly.DragH');
goog.require('Mixly.DragV');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.EditorsManager');
goog.require('Mixly.StatusBarsManager');
goog.require('Mixly.LeftSideBarsManager');
goog.require('Mixly.RightSideBarsManager');
goog.require('Mixly.Component');
goog.provide('Mixly.Workspace');
const {
XML,
Env,
Msg,
Drag,
DragH,
DragV,
HTMLTemplate,
EditorsManager,
StatusBarsManager,
LeftSideBarsManager,
RightSideBarsManager,
Component
} = Mixly;
class Workspace extends Component {
static {
HTMLTemplate.add(
'html/workspace.html',
new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/workspace.html')))
);
this.workspaces = [];
this.getAll = () => {
return this.workspaces;
}
this.add = (workspace) => {
this.remove(workspace);
this.workspaces.push(workspace);
}
this.remove = (workspace) => {
for (let i in this.workspaces) {
if (this.workspaces[i].id !== workspace.id) {
continue;
}
this.workspaces.slice(i, 1);
}
}
this.getMain = () => {
if (this.workspaces.length) {
return this.workspaces[0];
}
return null;
}
}
#statusBarsManager_ = null;
#editorsManager_ = null;
#leftSideBarsManager_ = null;
#rightSideBarsManager_ = null;
#$dragVLeft_ = null;
#$dragVRight_ = null;
#$dragH_ = null;
constructor(element) {
super();
const $content = $(HTMLTemplate.get('html/workspace.html').render());
this.setContent($content);
this.mountOn($(element));
this.#$dragVLeft_ = $content.find('.drag-v-left');
this.#$dragVRight_ = $content.find('.drag-v-right');
this.#$dragH_ = $content.find('.drag-h');
this.#statusBarsManager_ = new StatusBarsManager($content.find('.statusbars')[0]);
this.#statusBarsManager_.add({
type: 'terminal',
id: 'output',
name: Msg.Lang['statusbar.output'],
title: Msg.Lang['statusbar.output']
});
this.#statusBarsManager_.changeTo('output');
this.#editorsManager_ = new EditorsManager($content.find('.editors')[0]);
this.#leftSideBarsManager_ = new LeftSideBarsManager($content.find('.left-sidebars')[0]);
// this.#leftSideBarsManager_.add('local_storage', 'local_storage', '本地');
// this.#leftSideBarsManager_.changeTo('local_storage');
this.#rightSideBarsManager_ = new RightSideBarsManager($content.find('.right-sidebars')[0]);
this.dragH = null;
this.dragVLeft = null;
this.dragVRight = null;
Workspace.add(this);
// this.#addEventsListenerForFileTree_();
this.#addDragEventsListener_();
// this.#addEventsListenerForEditorManager_();
this.#addFuncForStatusbarTabs_();
}
#addEventsListenerForFileTree_() {
const leftSideBarLocalStorage = this.getLeftSideBarsManager().get('local_storage');
const fileTree = leftSideBarLocalStorage.getFileTree();
fileTree.bind('afterSelectLeaf', (selected) => {
const tabs = this.#editorsManager_.getTabs();
tabs.addTab({
name: selected[0].text,
title: selected[0].id,
id: selected[0].id,
type: path.extname(selected[0].id),
favicon: selected[0].icon,
attr: {
'data-link-file': 'true'
}
});
});
}
#addEventsListenerForEditorManager_() {
const tabs = this.#editorsManager_.getTabs();
tabs.bind('activeTabChange', (event) => {
const leftSideBarLocalStorage = this.getLeftSideBarsManager().get('local_storage');
const fileTree = leftSideBarLocalStorage.getFileTree();
const { tabEl } = event.detail;
const tabId = $(tabEl).attr('data-tab-id');
fileTree.deselectAll();
fileTree.select(tabId);
});
tabs.bind('tabDestroyed', (event) => {
const leftSideBarLocalStorage = this.getLeftSideBarsManager().get('local_storage');
const fileTree = leftSideBarLocalStorage.getFileTree();
const { tabEl } = event.detail;
const tabId = $(tabEl).attr('data-tab-id');
fileTree.deselect(tabId);
});
}
#addFuncForStatusbarTabs_() {
this.#statusBarsManager_.bind('show', () => this.dragH.show(Drag.Extend.NEGATIVE));
this.#statusBarsManager_.bind('hide', () => this.dragH.hide(Drag.Extend.NEGATIVE));
}
#addDragEventsListener_() {
// 编辑器(上)+状态栏(下)
this.dragH = new DragH(this.#$dragH_[0], {
min: '50px',
startSize: '100%',
startExitFullSize: '70%'
});
this.dragH.bind('sizeChanged', () => {
this.resize();
});
/*// 侧边栏(左)+[编辑器(上)+状态栏(下)]
this.dragVLeft = new DragV(this.#$dragVLeft_[0], {
min: '100px',
full: [true, false],
startSize: '0%',
startExitFullSize: '15%'
});
this.dragVLeft.bind('sizeChanged', () => {
this.resize();
});
// 侧边栏(右)+[编辑器(上)+状态栏(下)]
this.dragVRight = new DragV(this.#$dragVRight_[0], {
min: '100px',
full: [false, true],
startSize: '100%',
startExitFullSize: '85%'
});
this.dragVRight.bind('sizeChanged', () => {
this.resize();
});*/
}
getEditorsManager() {
return this.#editorsManager_;
}
getLeftSideBarsManager() {
return this.#leftSideBarsManager_;
}
getRightSideBarsManager() {
return this.#rightSideBarsManager_;
}
getStatusBarsManager() {
return this.#statusBarsManager_;
}
resize() {
super.resize();
this.getEditorsManager().resize();
this.getLeftSideBarsManager().resize();
this.getRightSideBarsManager().resize();
this.getStatusBarsManager().resize();
}
dispose() {
Workspace.remove(this);
this.getEditorsManager().dispose();
this.getLeftSideBarsManager().dispose();
this.getRightSideBarsManager().dispose();
this.getStatusBarsManager().dispose();
super.dispose();
}
}
Mixly.Workspace = Workspace;
});

View File

@@ -0,0 +1,158 @@
goog.loadJs('common', () => {
goog.require('path');
goog.require('layui');
goog.require('Mixly.Env');
goog.require('Mixly.Config');
goog.require('Mixly.Msg');
goog.provide('Mixly.XML');
const {
Env,
Config,
Msg,
XML
} = Mixly;
const { BOARD } = Config;
const { laytpl } = layui;
XML.TEMPLATE_CONFIG = [
{
type: 'LOADER_DIV',
path: '/html/loader-div.html',
config: {
btnName: Msg.Lang['nav.btn.stop']
},
appendToBody: true
}, {
type: 'SELECTOR_DIV',
path: '/html/selector-div.html',
config: {
btn1Name: Msg.Lang['nav.btn.stop'],
btn2Name: Msg.Lang['nav.btn.ok']
},
appendToBody: true
}, {
type: 'PARSE_MIX_ERROR_DIV',
path: '/html/parse-mix-error-div.html',
config: {},
appendToBody: false
}, {
type: 'READ_BITMAP_DIV',
path: '/html/read-bitmap-div.html',
config: {},
appendToBody: false
}, {
type: 'PROGRESS_BAR_DIV',
path: '/html/progress-bar-div.html',
config: {},
appendToBody: false
}, {
type: 'LIB_MANAGER_DIV',
path: '/html/lib-manager-div.html',
config: {},
appendToBody: false
}
];
XML.TEMPLATE_ENV = {
LOADER_DIV: true,
SELECTOR_DIV: true,
PARSE_MIX_ERROR_DIV: true,
READ_BITMAP_DIV: true,
PROGRESS_BAR_DIV: goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary,
LIB_MANAGER_DIV: goog.isElectron && BOARD?.nav?.setting?.thirdPartyLibrary
};
XML.TEMPLATE_STR = {};
XML.TEMPLATE_STR_RENDER = {};
XML.TEMPLATE_DOM = {};
XML.CATEGORIES_STR = {};
XML.getDom = (xmlStr, config = {}) => {
return $(laytpl(xmlStr).render(config));
}
XML.render = (xmlStr, config = {}) => {
const newConfig = {};
for (let i in config) {
if (typeof config[i] === 'function') {
newConfig[i] = config[i]();
} else {
newConfig[i] = config[i];
}
}
return laytpl(xmlStr).render(newConfig);
}
XML.convert = function (str, trimEscaped) {
var xml = "";
var hasComleteAngleBracket = true;
var lenStr = str.length;
for (var i = 0; i < lenStr; i++) {
if (str[i] === "<") {
hasComleteAngleBracket = false;
} else if (str[i] === ">") {
hasComleteAngleBracket = true;
}
if (trimEscaped
&& !hasComleteAngleBracket
&& i + 1 < lenStr
&& str[i] === "\\"
&& str[i + 1] === '"') {
i += 1;
}
if (!trimEscaped
&& !hasComleteAngleBracket
&& i > 0
&& str[i - 1] !== "\\"
&& str[i] === '"') {
xml += "\\";
}
xml += str[i];
}
return xml;
}
for (let i of XML.TEMPLATE_CONFIG) {
const { type, config, appendToBody } = i;
if (XML.TEMPLATE_ENV[type]) {
const xmlStr = goog.readFileSync(path.join(Env.templatePath, i.path));
if (xmlStr) {
XML.TEMPLATE_STR[type] = xmlStr;
XML.TEMPLATE_STR_RENDER[type] = XML.render(xmlStr, config);
XML.TEMPLATE_DOM[type] = XML.getDom(xmlStr, config);
if (appendToBody)
$('body').append(XML.TEMPLATE_DOM[type]);
}
}
}
if (layui._typeof(BOARD.board) === 'object') {
for (let i in BOARD.board) {
const boardConfig = BOARD.board[i];
if (layui._typeof(boardConfig) === 'object'
&& layui._typeof(boardConfig.xmlPath) === 'string') {
const categoriesStr = goog.readFileSync(path.join(Env.boardDirPath, boardConfig.xmlPath));
if (categoriesStr)
XML.CATEGORIES_STR[i] = categoriesStr;
}
}
}
window.addEventListener('load', () => {
for (let i of XML.TEMPLATE_CONFIG) {
const { type, appendToBody } = i;
if (XML.TEMPLATE_ENV[type] && XML.TEMPLATE_DOM[type] && appendToBody) {
$('body').append(XML.TEMPLATE_DOM[type]);
}
}
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,241 @@
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;
#decoder_ = new TextDecoder('utf8');
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, encoding = 'utf8') {
let stdout = '', error = null;
try {
const output = await this.#ampy_.get(this.#port_, this.#baud_, filePath);
stdout = output.stdout;
if (encoding = 'utf8') {
stdout = this.#decoder_.decode(this.#ampy_.unhexlify(stdout));
} else {
stdout = this.#ampy_.unhexlify(stdout);
}
} 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');
if (data.constructor.name === 'ArrayBuffer') {
data = Buffer.from(data);
}
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) {
return this.rename(oldFilePath, newFilePath);
}
async copyFile(oldFilePath, newFilePath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.cpfile(this.#port_, this.#baud_, oldFilePath, newFilePath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
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(new Set(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) {
return this.rename(oldFolderPath, newFolderPath);
}
async copyDirectory(oldFolderPath, newFolderPath) {
let stdout = '', error = null;
try {
const output = await this.#ampy_.cpdir(this.#port_, this.#baud_, oldFolderPath, newFolderPath);
stdout = output.stdout;
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
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,131 @@
goog.loadJs('electron', () => {
goog.require('path');
goog.require('Mustache');
goog.require('Mixly.Ampy');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Serial');
goog.require('Mixly.Electron');
goog.provide('Mixly.Electron.Ampy');
const {
Ampy,
Env,
Msg,
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}}"',
cpdir: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 cpdir "{{&startPath}}" "{{&endPath}}"',
cpfile: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 cpfile "{{&startPath}}" "{{&endPath}}"',
run: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 run "{{&filePath}}"'
}
this.AMPY_PATH = path.join(Env.srcDirPath, './tools/python/ampy_main.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, encoding = 'utf8') {
return this.exec(port, this.render('get', { port, baud, filePath, encoding }));
}
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 cpdir(port, baud, startPath, endPath) {
return this.exec(port, this.render('cpdir', { port, baud, startPath, endPath }));
}
async cpfile(port, baud, startPath, endPath) {
return this.exec(port, this.render('cpfile', { port, baud, startPath, endPath }));
}
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(Msg.Lang['statusbar.serial.noPort']);
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,598 @@
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.LayerProgress');
goog.require('Mixly.Debug');
goog.require('Mixly.Electron.Shell');
goog.provide('Mixly.Electron.ArduShell');
const {
Env,
Electron,
LayerExt,
Config,
Title,
Boards,
MFile,
MArray,
Msg,
MString,
Workspace,
Serial,
LayerProgress,
Debug
} = 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 child_process = Mixly.require('node:child_process');
const iconv_lite = Mixly.require('iconv-lite');
const {
ArduShell,
Shell
} = Electron;
ArduShell.DEFAULT_CONFIG = goog.readJsonSync(path.join(Env.templatePath, 'json/arduino-cli-config.json'));
ArduShell.ERROR_ENCODING = Env.currentPlatform == 'win32' ? 'cp936' : 'utf-8';
ArduShell.binFilePath = '';
ArduShell.shellPath = null;
ArduShell.shell = null;
ArduShell.compiling = false;
ArduShell.uploading = false;
ArduShell.killing = false;
ArduShell.progressLayer = new LayerProgress({
width: 200,
cancelValue: Msg.Lang['nav.btn.stop'],
cancel: () => {
if (ArduShell.killing) {
return false;
}
ArduShell.progressLayer.title(`${Msg.Lang['shell.aborting']}...`);
ArduShell.killing = true;
ArduShell.cancel();
return false;
},
cancelDisplay: false
});
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");
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
ArduShell.compiling = true;
ArduShell.uploading = false;
ArduShell.killing = false;
const boardType = Boards.getSelectedBoardCommandParam();
mainStatusBarTabs.show();
ArduShell.progressLayer.title(`${Msg.Lang['shell.compiling']}...`);
ArduShell.progressLayer.show();
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('compile', cmdStr, code, doFunc);
}
/**
* @function 初始化上传
* @description 关闭已打开的串口,获取当前所连接的设备数,然后开始上传程序
* @return void
*/
ArduShell.initUpload = () => {
const { mainStatusBarTabs } = Mixly;
ArduShell.compiling = false;
ArduShell.uploading = true;
ArduShell.killing = false;
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 mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
ArduShell.progressLayer.title(`${Msg.Lang['shell.uploading']}...`);
mainStatusBarTabs.show();
ArduShell.progressLayer.title(`${Msg.Lang['shell.uploading']}...`);
ArduShell.progressLayer.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('upload', cmdStr, code, () => {
if (!Serial.portIsLegal(port)) {
return;
}
mainStatusBarTabs.add('serial', port);
mainStatusBarTabs.changeTo(port);
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
statusBarSerial.open()
.then(() => {
const baudRates = code.match(/(?<=Serial.begin[\s]*\([\s]*)[0-9]*(?=[\s]*\))/g);
if (!baudRates?.length) {
statusBarSerial.setBaudRate(9600);
} else {
statusBarSerial.setBaudRate(baudRates[0] - 0);
}
});
});
}
/**
* @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 = (type, cmd, code, sucFunc) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
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) => {
ArduShell.progressLayer.hide();
let message = '';
if (info.code) {
message = (type === 'compile' ? Msg.Lang['shell.compileFailed'] : Msg.Lang['shell.uploadFailed']);
statusBarTerminal.addValue("\n==" + message + "==\n");
} else {
message = (type === 'compile' ? Msg.Lang['shell.compileSucc'] : Msg.Lang['shell.uploadSucc']);
statusBarTerminal.addValue(`\n==${message}(${Msg.Lang['shell.timeCost']} ${info.time})==\n`);
sucFunc();
}
layer.msg(message, { time: 1000 });
})
.catch((error) => {
ArduShell.progressLayer.hide();
Debug.error(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 () {
ArduShell.progressLayer.title(`${Msg.Lang['shell.saving']}...`);
ArduShell.progressLayer.show();
window.setTimeout(function () {
try {
readPath = readPath.replace(/\\/g, "/");
writePath = writePath.replace(/\\/g, "/");
} catch (error) {
Debug.error(error);
}
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 (error) {
Debug.error(error);
statusBarTerminal.addValue(e + "\n");
}
ArduShell.progressLayer.hide();
}, 500);
}, 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,701 @@
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.HTMLTemplate');
goog.require('Mixly.LayerFirmware');
goog.require('Mixly.LayerProgress');
goog.require('Mixly.Electron.Serial');
goog.provide('Mixly.Electron.BU');
const {
Electron,
Config,
LayerExt,
Env,
Boards,
MString,
Msg,
Workspace,
Serial,
Debug,
HTMLTemplate,
LayerFirmware,
LayerProgress
} = 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 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;
BU.firmwareLayer = new LayerFirmware({
width: 400,
title: Msg.Lang['nav.btn.burn'],
cancelValue: false,
cancel: false,
cancelDisplay: false
});
BU.firmwareLayer.bind('burn', (info) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
BU.burning = true;
BU.uploading = false;
const port = Serial.getSelectedPortName();
BU.burnWithPort(port, info);
});
BU.killing = false;
BU.progressLayer = new LayerProgress({
width: 200,
cancelValue: Msg.Lang['nav.btn.stop'],
cancel: () => {
if (BU.killing) {
return false;
}
BU.progressLayer.title(`${Msg.Lang['shell.aborting']}...`);
BU.killing = true;
BU.cancel();
return false;
},
cancelDisplay: false
});
/**
* @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 startPath {string} 需要拷贝的文件或文件夹的路径
* @param desPath {string} 文件的目的路径
**/
BU.copyFiles = async (type, 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));
}
try {
await fs_extra.copy(startPath, desPath);
BU.progressLayer.hide();
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) {
Debug.error(error);
BU.progressLayer.hide();
statusBarTerminal.setValue(error + '\n');
console.log(error);
}
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 message = (type === 'burn'? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']);
BU.progressLayer.title(`${message}...`);
const desPath = $('#mixly-selector-type option:selected').val();
if (type === 'burn') {
BU.copyFiles(type, startPath, desPath)
.catch((error) => {
BU.progressLayer.hide();
BU.burning = false;
BU.uploading = false;
statusBarTerminal.setValue(error + '\n');
});
} else {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
fs_extra.outputFile(startPath, code)
.then(() => {
return BU.copyFiles(type, startPath, desPath);
})
.catch((error) => {
BU.progressLayer.hide();
BU.burning = false;
BU.uploading = false;
statusBarTerminal.setValue(error + '\n');
});
}
}
/**
* @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
});
}
}
BU.killing = false;
}
/**
* @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();
if (burn.special && burn.special instanceof Array) {
BU.burning = false;
BU.burnWithSpecialBin();
} else {
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 port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
*/
BU.burnByCmd = function (port, command) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
command = MString.tpl(command, {
baudrate: Boards.getSelectedBoardConfigParam('BurnSpeed') ?? '460800'
});
BU.runCmd('burn', port, command);
}
/**
* @function 通过cmd上传
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
*/
BU.uploadByCmd = async function (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);
} else {
BU.copyLib(upload.filePath, '');
}
fs_extra.outputFile(upload.filePath, code)
.then(() => {
BU.runCmd('upload', port, command);
})
.catch((error) => {
BU.progressLayer.hide();
statusBarTerminal.setValue(error.toString() + '\n');
Debug.error(error);
BU.uploading = false;
});
}
/**
* @function 运行cmd
* @param type {string} 值为 'burn' | 'upload'
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @param sucFunc {function} 指令成功执行后所要执行的操作
* @return {void}
*/
BU.runCmd = function (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) {
BU.progressLayer.hide();
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 firmwares = SELECTED_BOARD.burn.special;
let menu = [];
let commandMap = {};
for (let firmware of firmwares) {
if (!firmware?.name && !firmware?.command) continue;
menu.push({
id: firmware.name,
text: firmware.name
});
commandMap[firmware.name] = firmware.command;
}
BU.firmwareLayer.setMap(commandMap);
BU.firmwareLayer.setMenu(menu);
BU.firmwareLayer.show();
}
/**
* @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']) + '...';
BU.progressLayer.title(title);
BU.progressLayer.show();
const { mainStatusBarTabs } = Mixly;
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
let serialClosePromise = null;
if (statusBarSerial) {
serialClosePromise = statusBarSerial.close();
} else {
serialClosePromise = Promise.resolve();
}
serialClosePromise.finally(() => {
if (type === 'burn') {
BU.burnByCmd(port, command);
} else {
BU.uploadByCmd(port, command);
}
});
}
/**
* @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);
});
});

Some files were not shown because too many files have changed in this diff Show More