///
///
class UlanziStreamDeck {
constructor() {
this.key = "";
this.uuid = "";
this.actionid = "";
this.websocket = null;
this.language = "en";
this.localization = null;
this.on = EventEmitter.on;
this.emit = EventEmitter.emit;
this.isMain = false;
}
connect(uuid) {
this.port = Utils.getQueryParams("port") || 3906;
this.address = Utils.getQueryParams("address") || "127.0.0.1";
this.actionid = Utils.getQueryParams("actionid") || "";
this.key = Utils.getQueryParams("key") || "";
this.language =
Utils.getQueryParams("language") || Utils.getLanguage() || "en";
this.language = Utils.adaptLanguage(this.language);
this.uuid = Utils.getQueryParams("uuid") || uuid;
this.controller = Utils.getQueryParams("controller") || "Keypad"; //Keypad 按键 ,Encoder 旋钮
this.device = Utils.getQueryParams("device") || "";
this.mode = Utils.getQueryParams("mode") || "";
if (this.mode == "simulate") {
document.documentElement.style.backgroundColor = "#1E1F22";
document.body.style.backgroundColor = "#1E1F22";
}
if (this.websocket) {
this.websocket.close();
this.websocket = null;
}
//判断是否为主服务,约定主服务 uuid 为4位,action应大于4位
const isMain = this.uuid.split(".").length == 4;
this.isMain = isMain;
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET CONNECT:${
this.uuid
}`
);
this.websocket = new WebSocket(`ws://${this.address}:${this.port}`);
this.websocket.onopen = () => {
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET OPEN:${
this.uuid
}`
);
const json = {
code: 0,
cmd: Events.CONNECTED,
actionid: this.actionid,
key: this.key,
uuid: this.uuid,
};
this.websocket.send(JSON.stringify(json));
this.emit(Events.CONNECTED, {});
//如果是主服务,则不进行本地化
if (!isMain) {
this.localizeUI();
}
};
this.websocket.onerror = (evt) => {
const error = `[ULANZIDECK] ${
this.isMain ? "MAIN" : "CLIENT"
} WEBSOCKET ERROR: ${evt}, ${evt.data}, ${SocketErrors["DEFAULT"]}`;
Utils.warn(error);
this.emit(Events.ERROR, error);
};
this.websocket.onclose = (evt) => {
Utils.warn(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET CLOSED:${
SocketErrors["DEFAULT"]
}`
);
this.emit(Events.CLOSE);
};
this.websocket.onmessage = (evt) => {
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET MESSGE `
);
const data = evt && evt.data ? JSON.parse(evt.data) : null;
Utils.log(
`[ULANZIDECK] ${
this.isMain ? "MAIN" : "CLIENT"
} WEBSOCKET MESSGE DATA:${JSON.stringify(data)}`
);
//没有数据或者有data.code属性,且cmdType不等于REQUEST,则返回
if (
!data ||
(typeof data.code !== "undefined" && data.cmdType !== "REQUEST")
)
return;
Utils.log(
`[ULANZIDECK] ${this.isMain ? "MAIN" : "CLIENT"} WEBSOCKET MESSGE IN`
);
//没有key时,保存key
if (!this.key && data.uuid == this.uuid && data.key) {
this.key = data.key;
}
//没有actionid时,保存actionid
if (!this.actionid && data.uuid == this.uuid && data.actionid) {
this.actionid = data.actionid;
}
if (isMain) {
//主服务回应上位机
this.send(data.cmd, {
code: 0,
...data,
});
}
//特殊处理clear,因为clear事件变量是数组形式
if (data.cmd == "clear") {
if (data.param) {
for (let i = 0; i < data.param.length; i++) {
const context = this.encodeContext(data.param[i]);
data.param[i].context = context;
}
}
} else {
//拼接唯一id给功能页
const context = this.encodeContext(data);
data.context = context;
}
//引发事件
this.emit(data.cmd, data);
};
}
/**
* 本地化
*/
async localizeUI() {
const el = document.querySelector(".uspi-wrapper");
if (!el) return Utils.warn("No element found to localize");
// this.language = Utils.getLanguage() || 'en';
if (!this.localization) {
try {
const localJson = await Utils.readJson(
`${Utils.getPluginPath()}/${this.language}.json`
);
this.localization = localJson["Localization"]
? localJson["Localization"]
: null;
} catch (e) {
Utils.log(`${Utils.getPluginPath()}/${this.language}.json`);
Utils.warn(`No FILE found to localize: ${this.language}`);
}
}
if (this.localization){
const selectorsList = "[data-localize]";
el.querySelectorAll(selectorsList).forEach((e) => {
const s = e.innerText.trim();
let dl = e.dataset.localize;
if (e.placeholder && e.placeholder.length) {
// console.log('e.placeholder:',e.placeholder)
e.placeholder =
this.localization[dl ? dl : e.placeholder] || e.placeholder;
}
if (e.title && e.title.length) {
// console.log('e.title:',e.title)
e.title = this.localization[dl ? dl : e.title] || e.title;
}
if (e.label) {
// console.log('e.label:',e.label)
e.label = this.localization[dl ? dl : e.label] || e.label;
}
if (e.textContent) {
// console.log('e.textContent:',e.textContent)
e.textContent = this.localization[dl ? dl : e.textContent] || e.textContent;
}
if (s) {
// console.log('s:',s)
e.innerHTML = this.localization[dl ? dl : s] || e.innerHTML;
}
});
}
}
t(key) {
return (this.localization && this.localization[key]) || key;
}
/**
* 创建唯一值
*/
encodeContext(jsn) {
return jsn.uuid + "___" + jsn.key + "___" + jsn.actionid;
}
/**
* 解构唯一值
*/
decodeContext(context) {
const de_ctx = context.split("___");
return {
uuid: de_ctx[0],
key: de_ctx[1],
actionid: de_ctx[2],
};
}
/**
* Send JSON params to StreamDeck
* @param {string} cmd
* @param {object} params
*/
send(cmd, params) {
this.websocket &&
this.websocket.send(
JSON.stringify({
cmd,
uuid: this.uuid,
key: this.key,
actionid: this.actionid,
...params,
})
);
}
/**
* 向上位机发送配置参数
* @param {object} settings 必传 | 配置参数
* @param {object} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
sendParamFromPlugin(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.PARAMFROMPLUGIN, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
param: settings,
});
}
/**
* 请求上位机使⽤浏览器打开url
* @param {string} url 必传 | 直接远程地址和本地地址,⽀持打开插件根⽬录下的url链接(以/ ./ 起始的链接)。
* 只能是基本路径,不能带参数,需要带参数请设置在param值里面
* @param {local} boolean 可选 | 若为本地地址为true
* @param {object} param 可选 | 路径的参数值
*/
openUrl(url, local, param) {
this.send(Events.OPENURL, {
url,
local: local ? true : false,
param: param ? param : null,
});
}
/**
* 请求上位机机显⽰弹窗;弹窗后,test.html需要主动关闭,测试到window.close()可以通知弹窗关闭
* @param {string} url 必传 | 本地html路径,只能是基本路径,不能带参数,需要带参数请设置在param值里面
* @param {string} width 可选 | 窗口宽度,默认200
* @param {string} height 可选 | 窗口高度,默认200
* @param {string} x 可选 | 窗口x坐标,不传值默认居中
* @param {string} y 可选 | 窗口y坐标,不传值默认居中
* @param {object} param 可选 | 路径的参数值
*/
openView(url, width = 200, height = 200, x, y, param) {
const params = {
url,
width,
height,
};
if (x) {
params.x = x;
}
if (y) {
params.y = y;
}
if (param) {
params.param = param;
}
this.send(Events.OPENVIEW, params);
}
/**
* 请求上位机弹出Toast消息提⽰
* @param {string} msg 必传 | 窗口级消息提示
*/
toast(msg) {
this.send(Events.TOAST, {
msg,
});
}
/**
* 请求上位机弹出快捷键
* @param {string} key 必传 | 快捷键
*/
hotkey(key) {
this.send(Events.HOTKEY, {
keylist: key,
});
}
/**
* 请求上位机弹出日志消息提⽰
* @param {string} msg 必传 | 保存到插件UUID.txt中
* @param {string} level 可选 | 日志级别 info|debug|warn|error
*/
logMessage(msg, level) {
this.send(Events.LOGMESSAGE, {
message: msg,
level: level || "info",
});
}
/**
* 主服务发出,上位机透传参数到action页面,此透传参数上位机不保存
* @param {object} settings 必传 | 设置
* @param {string} context 必传 | 唯一id,需要指定发送到哪个action
*/
sendToPropertyInspector(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SENDTOPROPERTYINSPECTOR, {
uuid: uuid,
key: key,
actionid: actionid,
payload: settings,
});
}
/**
* action页面发出,上位机透传参数到主服务,此透传参数上位机不保存
* @param {object} settings 必传 | 设置
*/
sendToPlugin(settings) {
this.send(Events.SENDTOPLUGIN, {
uuid: this.uuid,
key: this.key,
actionid: this.actionid,
payload: settings,
});
}
/**
* 请求上位机在按键上显示错误提示
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
showAlert(context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SHOWALERT, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
});
}
/**
* 请求上位机发送已保存的参数,上位机接收后会触发didReceiveSettings事件转发至另一端
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
getSettings(context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.GETSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
});
}
/**
* 主动向上位机保存参数,上位机接收后会触发didReceiveSettings事件转发至另一端
* @param {object} settings 必传 | 配置参数
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
setSettings(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SETSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
settings,
});
}
/**
* 请求上位机发送已保存的全局参数,上位机接收后会触发didReceiveGlobalSettings事件转发至另一端
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
getGlobalSettings(context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.GETGLOBALSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
});
}
/**
* 主动向上位机保存参数,上位机接收后会触发didReceiveGlobalSettings事件转发至另一端
* @param {object} settings 必传 | 配置参数
* @param {string} context 可选 | 唯一id。非必传,由action页面发出时可以不传,由主服务发出必传
*/
setGlobalSettings(settings, context) {
const { uuid, key, actionid } = context ? this.decodeContext(context) : {};
this.send(Events.SETGLOBALSETTINGS, {
uuid: uuid || this.uuid,
key: key || this.key,
actionid: actionid || this.actionid,
settings,
});
}
/**
* 请求上位机弹出选择对话框:选择文件
* @param {string} filter 可选 | 文件过滤器。筛选文件的类型,例如 "filter": "image(*.jpg *.png *.gif)" 或者 筛选文件 file(*.txt *.json) 等
* 该请求的选择结果请通过 onSelectdialog 事件接收
*/
selectFileDialog(filter) {
this.send(Events.SELECTDIALOG, {
type: "file",
filter,
});
}
/**
* 请求上位机弹出选择对话框:选择文件夹
* 该请求的选择结果请通过 onSelectdialog 事件接收
*/
selectFolderDialog() {
this.send(Events.SELECTDIALOG, {
type: "folder",
});
}
/**
* 设置图标-使⽤配置⾥的图标列表编号,请对照manifest.json
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {number} state 必传 | 图标列表编号,
* @param {string} text 可选 | icon是否显示文字
*/
setStateIcon(context, state, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 0,
state,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤⾃定义图标
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {string} data 必传 | base64格式的icon
* @param {string} text 可选 | icon是否显示文字
*/
setBaseDataIcon(context, data, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 1,
data,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤本地图片文件
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {string} path 必传 | 本地图片路径,⽀持打开插件根⽬录下的url链接(以/ ./ 起始的链接)
* @param {string} text 可选 | icon是否显示文字
*/
setPathIcon(context, path, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 2,
path,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤⾃定义的动图
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出
* @param {string} gifdata 必传 | ⾃定义gif的base64编码数据
* @param {string} text 可选 | icon是否显示文字
*/
setGifDataIcon(context, gifdata, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 3,
gifdata,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 设置图标-使⽤本地gif⽂件
* @param {string} context 必传 |唯一id,每个message里面common库会自动拼接给出,
* @param {string} gifdata 必传 | 本地gif图片路径,⽀持打开插件根⽬录下的url链接(以/ ./ 起始的链接)
* @param {string} text 可选 | icon是否显示文字
*/
setGifPathIcon(context, gifpath, text) {
const { uuid, key, actionid } = this.decodeContext(context);
this.send(Events.STATE, {
param: {
statelist: [
{
uuid,
key,
actionid,
type: 4,
gifpath,
textData: text || "",
showtext: text ? true : false,
},
],
},
});
}
/**
* 监听socket连接事件
*/
onConnected(fn) {
if (!fn) {
Utils.error(
"A callback function for the connected event is required for onConnected."
);
}
this.on(Events.CONNECTED, (jsn) => fn(jsn));
return this;
}
/**
* 监听socket断开事件
*/
onClose(fn) {
if (!fn) {
Utils.error(
"A callback function for the close event is required for onClose."
);
}
this.on(Events.CLOSE, (jsn) => fn(jsn));
return this;
}
/**
* 监听socket错误事件
*/
onError(fn) {
if (!fn) {
Utils.error(
"A callback function for the error event is required for onError."
);
}
this.on(Events.ERROR, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:add
*/
onAdd(fn) {
if (!fn) {
Utils.error(
"A callback function for the add event is required for onAdd."
);
}
this.on(Events.ADD, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:paramfromapp
*/
onParamFromApp(fn) {
if (!fn) {
Utils.error(
"A callback function for the paramfromapp event is required for onParamFromApp."
);
}
this.on(Events.PARAMFROMAPP, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:paramfromplugin
*/
onParamFromPlugin(fn) {
if (!fn) {
Utils.error(
"A callback function for the paramfromplugin event is required for onParamFromPlugin."
);
}
this.on(Events.PARAMFROMPLUGIN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:run
*/
onRun(fn) {
if (!fn) {
Utils.error(
"A callback function for the run event is required for onRun."
);
}
this.on(Events.RUN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:setactive
*/
onSetActive(fn) {
if (!fn) {
Utils.error(
"A callback function for the setactive event is required for onSetActive."
);
}
this.on(Events.SETACTIVE, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:clear
*/
onClear(fn) {
if (!fn) {
Utils.error(
"A callback function for the clear event is required for onClear."
);
}
this.on(Events.CLEAR, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:返回选择弹窗结果
*/
onSelectdialog(fn) {
if (!fn) {
Utils.error(
"A callback function for the selectdialog event is required for onSelectdialog."
);
}
this.on(Events.SELECTDIALOG, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:didReceiveSettings, 接受上位机保存的参数
*/
onDidReceiveSettings(fn) {
if (!fn) {
Utils.error(
"A callback function for the didReceiveSettings event is required for onDidReceiveSettings."
);
}
this.on(Events.DIDRECEIVESETTINGS, (jsn) => fn(jsn));
return this;
}
/**
* didReceiveGlobalSettings, 接受全局设置的参数
*/
onDidReceiveGlobalSettings(fn) {
if (!fn) {
Utils.error(
"A callback function for the didReceiveGlobalSettings event is required for onDidReceiveGlobalSettings."
);
}
this.on(Events.DIDRECEIVEGLOBALSETTINGS, (jsn) => fn(jsn));
return this;
}
/**
*
* 接收 主服务发给功能页的透传参数事件
*/
onSendToPropertyInspector(fn) {
if (!fn) {
Utils.error(
"A callback function for the sendToPropertyInspector event is required for onSendToPropertyInspector."
);
}
this.on(Events.SENDTOPROPERTYINSPECTOR, (jsn) => fn(jsn));
return this;
}
/**
*
* 接收 功能页发给主服务的透传参数事件
*/
onSendToPlugin(fn) {
if (!fn) {
Utils.error(
"A callback function for the sendToPlugin event is required for onSendToPlugin."
);
}
this.on(Events.SENDTOPLUGIN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:keydown, 接收上位机按键按下事件
*/
onKeyDown(fn) {
if (!fn) {
Utils.error(
"A callback function for the keydown event is required for onKeyDown."
);
}
this.on(Events.KEYDOWN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:keyup, 接收上位机按键松开事件
*/
onKeyUp(fn) {
if (!fn) {
Utils.error(
"A callback function for the keyup event is required for onKeyUp."
);
}
this.on(Events.KEYUP, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:dialdown, 接收上位机旋钮按下事件
*/
onDialDown(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialdown event is required for onDialDown."
);
}
this.on(Events.DIALEDOWN, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:dialup, 接收上位机旋钮松开事件
*/
onDialUp(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialup event is required for onDialUp."
);
}
this.on(Events.DIALEUP, (jsn) => fn(jsn));
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮向左旋转事件
*/
onDialRotateLeft(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate left event is required for onDialRotateLeft."
);
}
this.on(Events.DIALROTATE, (jsn) => {
if (jsn.rotateEvent === "left") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮向右旋转事件
*/
onDialRotateRight(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate right event is required for onDialRotateRight."
);
}
this.on(Events.DIALROTATE, (jsn) => {
if (jsn.rotateEvent === "right") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮按住向左旋转事件
*/
onDialRotateHoldLeft(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate hold-left event is required for onDialRotateHoldLeft."
);
}
this.on(Events.DIALROTATE, (jsn) => {
if (jsn.rotateEvent === "hold-left") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮按住向右旋转事件
*/
onDialRotateHoldRight(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate hold-right event is required for onDialRotateHoldRight."
);
}
this.on(Events.DIALROTATE, (jsn) => {
// 注意:原数据中有个拼写错误"hold—right",这里使用正确的连字符
if (jsn.rotateEvent === "hold-right") {
fn(jsn);
}
});
return this;
}
/**
* 接收上位机事件:dialrotate, 接收上位机旋钮旋转事件
*/
onDialRotate(fn) {
if (!fn) {
Utils.error(
"A callback function for the dialrotate event is required for onDialRotate."
);
}
this.on(Events.DIALROTATE, (jsn) => fn(jsn));
return this;
}
}
const $UD = new UlanziStreamDeck();