feat: sync mixly root files and common folder
This commit is contained in:
74
mixly/common/modules/mixly-modules/common/ampy-file-tree.js
Normal file
74
mixly/common/modules/mixly-modules/common/ampy-file-tree.js
Normal 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;
|
||||
|
||||
});
|
||||
31
mixly/common/modules/mixly-modules/common/ampy.js
Normal file
31
mixly/common/modules/mixly-modules/common/ampy.js
Normal 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;
|
||||
|
||||
});
|
||||
689
mixly/common/modules/mixly-modules/common/app.js
Normal file
689
mixly/common/modules/mixly-modules/common/app.js
Normal 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;
|
||||
|
||||
});
|
||||
67
mixly/common/modules/mixly-modules/common/arduino-cli.js
Normal file
67
mixly/common/modules/mixly-modules/common/arduino-cli.js
Normal 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) {}
|
||||
}
|
||||
|
||||
});
|
||||
20
mixly/common/modules/mixly-modules/common/arduino-shell.js
Normal file
20
mixly/common/modules/mixly-modules/common/arduino-shell.js
Normal 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;
|
||||
|
||||
});
|
||||
265
mixly/common/modules/mixly-modules/common/board-config-item.js
Normal file
265
mixly/common/modules/mixly-modules/common/board-config-item.js
Normal 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;
|
||||
|
||||
});
|
||||
405
mixly/common/modules/mixly-modules/common/boards.js
Normal file
405
mixly/common/modules/mixly-modules/common/boards.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
60
mixly/common/modules/mixly-modules/common/code-formatter.js
Normal file
60
mixly/common/modules/mixly-modules/common/code-formatter.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
52
mixly/common/modules/mixly-modules/common/command.js
Normal file
52
mixly/common/modules/mixly-modules/common/command.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
110
mixly/common/modules/mixly-modules/common/component.js
Normal file
110
mixly/common/modules/mixly-modules/common/component.js
Normal 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;
|
||||
|
||||
});
|
||||
124
mixly/common/modules/mixly-modules/common/config.js
Normal file
124
mixly/common/modules/mixly-modules/common/config.js
Normal 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();
|
||||
|
||||
});
|
||||
116
mixly/common/modules/mixly-modules/common/context-menu.js
Normal file
116
mixly/common/modules/mixly-modules/common/context-menu.js
Normal 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;
|
||||
|
||||
});
|
||||
44
mixly/common/modules/mixly-modules/common/css-loader.js
Normal file
44
mixly/common/modules/mixly-modules/common/css-loader.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
22
mixly/common/modules/mixly-modules/common/debug.js
Normal file
22
mixly/common/modules/mixly-modules/common/debug.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
368
mixly/common/modules/mixly-modules/common/drag.js
Normal file
368
mixly/common/modules/mixly-modules/common/drag.js
Normal 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;
|
||||
|
||||
});
|
||||
233
mixly/common/modules/mixly-modules/common/dropdown-menu-group.js
Normal file
233
mixly/common/modules/mixly-modules/common/dropdown-menu-group.js
Normal 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;
|
||||
|
||||
});
|
||||
97
mixly/common/modules/mixly-modules/common/dropdown-menu.js
Normal file
97
mixly/common/modules/mixly-modules/common/dropdown-menu.js
Normal 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;
|
||||
|
||||
});
|
||||
409
mixly/common/modules/mixly-modules/common/editor-ace.js
Normal file
409
mixly/common/modules/mixly-modules/common/editor-ace.js
Normal 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;
|
||||
|
||||
});
|
||||
50
mixly/common/modules/mixly-modules/common/editor-base.js
Normal file
50
mixly/common/modules/mixly-modules/common/editor-base.js
Normal 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;
|
||||
|
||||
});
|
||||
367
mixly/common/modules/mixly-modules/common/editor-blockly.js
Normal file
367
mixly/common/modules/mixly-modules/common/editor-blockly.js
Normal 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;
|
||||
|
||||
});
|
||||
231
mixly/common/modules/mixly-modules/common/editor-code.js
Normal file
231
mixly/common/modules/mixly-modules/common/editor-code.js
Normal 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;
|
||||
|
||||
});
|
||||
170
mixly/common/modules/mixly-modules/common/editor-md.js
Normal file
170
mixly/common/modules/mixly-modules/common/editor-md.js
Normal 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;
|
||||
|
||||
});
|
||||
644
mixly/common/modules/mixly-modules/common/editor-mix.js
Normal file
644
mixly/common/modules/mixly-modules/common/editor-mix.js
Normal 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;
|
||||
|
||||
});
|
||||
396
mixly/common/modules/mixly-modules/common/editor-monaco.js
Normal file
396
mixly/common/modules/mixly-modules/common/editor-monaco.js
Normal 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;
|
||||
|
||||
});
|
||||
35
mixly/common/modules/mixly-modules/common/editor-unknown.js
Normal file
35
mixly/common/modules/mixly-modules/common/editor-unknown.js
Normal 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;
|
||||
|
||||
});
|
||||
34
mixly/common/modules/mixly-modules/common/editor-welcome.js
Normal file
34
mixly/common/modules/mixly-modules/common/editor-welcome.js
Normal 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;
|
||||
|
||||
});
|
||||
144
mixly/common/modules/mixly-modules/common/editors-manager.js
Normal file
144
mixly/common/modules/mixly-modules/common/editors-manager.js
Normal 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;
|
||||
|
||||
});
|
||||
141
mixly/common/modules/mixly-modules/common/env.js
Normal file
141
mixly/common/modules/mixly-modules/common/env.js
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
98
mixly/common/modules/mixly-modules/common/events.js
Normal file
98
mixly/common/modules/mixly-modules/common/events.js
Normal 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;
|
||||
|
||||
});
|
||||
998
mixly/common/modules/mixly-modules/common/file-tree.js
Normal file
998
mixly/common/modules/mixly-modules/common/file-tree.js
Normal 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;
|
||||
|
||||
});
|
||||
43
mixly/common/modules/mixly-modules/common/file.js
Normal file
43
mixly/common/modules/mixly-modules/common/file.js
Normal 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;
|
||||
|
||||
});
|
||||
94
mixly/common/modules/mixly-modules/common/footerbar.js
Normal file
94
mixly/common/modules/mixly-modules/common/footerbar.js
Normal 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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
204
mixly/common/modules/mixly-modules/common/footerlayer-message.js
Normal file
204
mixly/common/modules/mixly-modules/common/footerlayer-message.js
Normal 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;
|
||||
|
||||
});
|
||||
115
mixly/common/modules/mixly-modules/common/footerlayer.js
Normal file
115
mixly/common/modules/mixly-modules/common/footerlayer.js
Normal 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;
|
||||
|
||||
});
|
||||
144
mixly/common/modules/mixly-modules/common/fs-board-handler.js
Normal file
144
mixly/common/modules/mixly-modules/common/fs-board-handler.js
Normal 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;
|
||||
|
||||
});
|
||||
96
mixly/common/modules/mixly-modules/common/fs-board.js
Normal file
96
mixly/common/modules/mixly-modules/common/fs-board.js
Normal 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;
|
||||
|
||||
});
|
||||
47
mixly/common/modules/mixly-modules/common/fs.js
Normal file
47
mixly/common/modules/mixly-modules/common/fs.js
Normal 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;
|
||||
|
||||
});
|
||||
74
mixly/common/modules/mixly-modules/common/html-template.js
Normal file
74
mixly/common/modules/mixly-modules/common/html-template.js
Normal 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;
|
||||
|
||||
});
|
||||
24
mixly/common/modules/mixly-modules/common/id-generator.js
Normal file
24
mixly/common/modules/mixly-modules/common/id-generator.js
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
44
mixly/common/modules/mixly-modules/common/if-visiable.js
Normal file
44
mixly/common/modules/mixly-modules/common/if-visiable.js
Normal 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();
|
||||
|
||||
});
|
||||
6
mixly/common/modules/mixly-modules/common/js-funcs.js
Normal file
6
mixly/common/modules/mixly-modules/common/js-funcs.js
Normal file
@@ -0,0 +1,6 @@
|
||||
goog.provide('Mixly.JSFuncs');
|
||||
goog.require('Mixly.Config');
|
||||
|
||||
Mixly.JSFuncs.getPlatform = function () {
|
||||
return Mixly.Config.BOARD.boardType;
|
||||
};
|
||||
167
mixly/common/modules/mixly-modules/common/layer-ext.js
Normal file
167
mixly/common/modules/mixly-modules/common/layer-ext.js
Normal 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 = () => {
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
94
mixly/common/modules/mixly-modules/common/layer-firmware.js
Normal file
94
mixly/common/modules/mixly-modules/common/layer-firmware.js
Normal 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;
|
||||
|
||||
});
|
||||
76
mixly/common/modules/mixly-modules/common/layer-new-file.js
Normal file
76
mixly/common/modules/mixly-modules/common/layer-new-file.js
Normal 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;
|
||||
|
||||
});
|
||||
42
mixly/common/modules/mixly-modules/common/layer-progress.js
Normal file
42
mixly/common/modules/mixly-modules/common/layer-progress.js
Normal 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;
|
||||
|
||||
});
|
||||
91
mixly/common/modules/mixly-modules/common/layer.js
Normal file
91
mixly/common/modules/mixly-modules/common/layer.js
Normal 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;
|
||||
|
||||
});
|
||||
148
mixly/common/modules/mixly-modules/common/loader.js
Normal file
148
mixly/common/modules/mixly-modules/common/loader.js
Normal 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());
|
||||
}
|
||||
|
||||
});
|
||||
59
mixly/common/modules/mixly-modules/common/local-storage.js
Normal file
59
mixly/common/modules/mixly-modules/common/local-storage.js
Normal 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 };
|
||||
}
|
||||
|
||||
});
|
||||
68
mixly/common/modules/mixly-modules/common/marray.js
Normal file
68
mixly/common/modules/mixly-modules/common/marray.js
Normal 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));
|
||||
}
|
||||
|
||||
});
|
||||
135
mixly/common/modules/mixly-modules/common/menu.js
Normal file
135
mixly/common/modules/mixly-modules/common/menu.js
Normal 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;
|
||||
|
||||
});
|
||||
388
mixly/common/modules/mixly-modules/common/mfile.js
Normal file
388
mixly/common/modules/mixly-modules/common/mfile.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
12
mixly/common/modules/mixly-modules/common/mixly.js
Normal file
12
mixly/common/modules/mixly-modules/common/mixly.js
Normal file
@@ -0,0 +1,12 @@
|
||||
goog.loadJs('common', () => {
|
||||
|
||||
goog.provide('Mixly');
|
||||
|
||||
Mixly.require = function(moduleName) {
|
||||
if (!goog.isElectron) {
|
||||
return;
|
||||
}
|
||||
return require(moduleName);
|
||||
}
|
||||
|
||||
});
|
||||
50
mixly/common/modules/mixly-modules/common/mixly2-api.js
Normal file
50
mixly/common/modules/mixly-modules/common/mixly2-api.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
68
mixly/common/modules/mixly-modules/common/mjson.js
Normal file
68
mixly/common/modules/mixly-modules/common/mjson.js
Normal 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);
|
||||
}
|
||||
|
||||
});
|
||||
119
mixly/common/modules/mixly-modules/common/monaco-theme.js
Normal file
119
mixly/common/modules/mixly-modules/common/monaco-theme.js
Normal 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;
|
||||
|
||||
});
|
||||
127
mixly/common/modules/mixly-modules/common/monaco-tree-sitter.js
Normal file
127
mixly/common/modules/mixly-modules/common/monaco-tree-sitter.js
Normal 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;
|
||||
|
||||
});
|
||||
85
mixly/common/modules/mixly-modules/common/msg.js
Normal file
85
mixly/common/modules/mixly-modules/common/msg.js
Normal 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);
|
||||
|
||||
});
|
||||
76
mixly/common/modules/mixly-modules/common/mstring.js
Normal file
76
mixly/common/modules/mixly-modules/common/mstring.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
464
mixly/common/modules/mixly-modules/common/nav.js
Normal file
464
mixly/common/modules/mixly-modules/common/nav.js
Normal 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;
|
||||
|
||||
});
|
||||
159
mixly/common/modules/mixly-modules/common/page-base.js
Normal file
159
mixly/common/modules/mixly-modules/common/page-base.js
Normal 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;
|
||||
|
||||
});
|
||||
212
mixly/common/modules/mixly-modules/common/pages-manager.js
Normal file
212
mixly/common/modules/mixly-modules/common/pages-manager.js
Normal 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;
|
||||
|
||||
});
|
||||
164
mixly/common/modules/mixly-modules/common/pages-tab.js
Normal file
164
mixly/common/modules/mixly-modules/common/pages-tab.js
Normal 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;
|
||||
|
||||
});
|
||||
57
mixly/common/modules/mixly-modules/common/profile.js
Normal file
57
mixly/common/modules/mixly-modules/common/profile.js
Normal 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;
|
||||
|
||||
});
|
||||
71
mixly/common/modules/mixly-modules/common/registry.js
Normal file
71
mixly/common/modules/mixly-modules/common/registry.js
Normal 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;
|
||||
|
||||
});
|
||||
84
mixly/common/modules/mixly-modules/common/regression.js
Normal file
84
mixly/common/modules/mixly-modules/common/regression.js
Normal 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;
|
||||
|
||||
});
|
||||
65
mixly/common/modules/mixly-modules/common/script-loader.js
Normal file
65
mixly/common/modules/mixly-modules/common/script-loader.js
Normal 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();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
398
mixly/common/modules/mixly-modules/common/serial.js
Normal file
398
mixly/common/modules/mixly-modules/common/serial.js
Normal 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;
|
||||
|
||||
});
|
||||
57
mixly/common/modules/mixly-modules/common/sidebar-libs.js
Normal file
57
mixly/common/modules/mixly-modules/common/sidebar-libs.js
Normal 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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
144
mixly/common/modules/mixly-modules/common/sidebars-manager.js
Normal file
144
mixly/common/modules/mixly-modules/common/sidebars-manager.js
Normal 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;
|
||||
|
||||
});
|
||||
91
mixly/common/modules/mixly-modules/common/socket.js
Normal file
91
mixly/common/modules/mixly-modules/common/socket.js
Normal 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;
|
||||
|
||||
});
|
||||
596
mixly/common/modules/mixly-modules/common/statusbar-ampy.js
Normal file
596
mixly/common/modules/mixly-modules/common/statusbar-ampy.js
Normal 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;
|
||||
|
||||
});
|
||||
331
mixly/common/modules/mixly-modules/common/statusbar-fs.js
Normal file
331
mixly/common/modules/mixly-modules/common/statusbar-fs.js
Normal 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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
89
mixly/common/modules/mixly-modules/common/statusbar-libs.js
Normal file
89
mixly/common/modules/mixly-modules/common/statusbar-libs.js
Normal 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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
657
mixly/common/modules/mixly-modules/common/statusbar-serial.js
Normal file
657
mixly/common/modules/mixly-modules/common/statusbar-serial.js
Normal 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;
|
||||
|
||||
});
|
||||
89
mixly/common/modules/mixly-modules/common/statusbar.js
Normal file
89
mixly/common/modules/mixly-modules/common/statusbar.js
Normal 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;
|
||||
|
||||
});
|
||||
289
mixly/common/modules/mixly-modules/common/statusbars-manager.js
Normal file
289
mixly/common/modules/mixly-modules/common/statusbars-manager.js
Normal 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;
|
||||
|
||||
});
|
||||
54
mixly/common/modules/mixly-modules/common/storage.js
Normal file
54
mixly/common/modules/mixly-modules/common/storage.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
70
mixly/common/modules/mixly-modules/common/title.js
Normal file
70
mixly/common/modules/mixly-modules/common/title.js
Normal 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;
|
||||
}
|
||||
});
|
||||
208
mixly/common/modules/mixly-modules/common/toolbox-searcher.js
Normal file
208
mixly/common/modules/mixly-modules/common/toolbox-searcher.js
Normal 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;
|
||||
|
||||
});
|
||||
213
mixly/common/modules/mixly-modules/common/url.js
Normal file
213
mixly/common/modules/mixly-modules/common/url.js
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
134
mixly/common/modules/mixly-modules/common/user-events.js
Normal file
134
mixly/common/modules/mixly-modules/common/user-events.js
Normal 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;
|
||||
|
||||
});
|
||||
84
mixly/common/modules/mixly-modules/common/user-op-events.js
Normal file
84
mixly/common/modules/mixly-modules/common/user-op-events.js
Normal 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;
|
||||
|
||||
});
|
||||
223
mixly/common/modules/mixly-modules/common/workspace.js
Normal file
223
mixly/common/modules/mixly-modules/common/workspace.js
Normal 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;
|
||||
|
||||
});
|
||||
158
mixly/common/modules/mixly-modules/common/xml.js
Normal file
158
mixly/common/modules/mixly-modules/common/xml.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
1968
mixly/common/modules/mixly-modules/deps.json
Normal file
1968
mixly/common/modules/mixly-modules/deps.json
Normal file
File diff suppressed because it is too large
Load Diff
241
mixly/common/modules/mixly-modules/electron/ampy-fs.js
Normal file
241
mixly/common/modules/mixly-modules/electron/ampy-fs.js
Normal 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;
|
||||
|
||||
});
|
||||
131
mixly/common/modules/mixly-modules/electron/ampy.js
Normal file
131
mixly/common/modules/mixly-modules/electron/ampy.js
Normal 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: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 ls "{{&folderPath}}"',
|
||||
get: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 get "{{&filePath}}"',
|
||||
mkdir: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 mkdir "{{&folderPath}}"',
|
||||
mkfile: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 mkfile "{{&filePath}}"',
|
||||
isdir: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 isdir "{{&folderPath}}"',
|
||||
isfile: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 isfile "{{&filePath}}"',
|
||||
put: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 put "{{&startPath}}" "{{&endPath}}"',
|
||||
rm: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 rm "{{&filePath}}"',
|
||||
rmdir: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 rmdir "{{&folderPath}}"',
|
||||
rename: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 rename "{{&oldPath}}" "{{&newPath}}"',
|
||||
cpdir: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 cpdir "{{&startPath}}" "{{&endPath}}"',
|
||||
cpfile: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 cpfile "{{&startPath}}" "{{&endPath}}"',
|
||||
run: '{{&y}} -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}}" "{{&y}}"', {
|
||||
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;
|
||||
|
||||
});
|
||||
598
mixly/common/modules/mixly-modules/electron/arduino-shell.js
Normal file
598
mixly/common/modules/mixly-modules/electron/arduino-shell.js
Normal 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();
|
||||
}
|
||||
|
||||
});
|
||||
701
mixly/common/modules/mixly-modules/electron/burn-upload.js
Normal file
701
mixly/common/modules/mixly-modules/electron/burn-upload.js
Normal 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);
|
||||
}
|
||||
|
||||
});
|
||||
115
mixly/common/modules/mixly-modules/electron/cloud-download.js
Normal file
115
mixly/common/modules/mixly-modules/electron/cloud-download.js
Normal 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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
50
mixly/common/modules/mixly-modules/electron/electron.js
Normal file
50
mixly/common/modules/mixly-modules/electron/electron.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
23
mixly/common/modules/mixly-modules/electron/events.js
Normal file
23
mixly/common/modules/mixly-modules/electron/events.js
Normal 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
Reference in New Issue
Block a user