commit fbf40f75b2f951c7e096069ff85180116c38516f Author: Peter Siegmund Date: Thu Apr 9 20:42:31 2026 +0200 first plugins for Ulanzi D200H - petrol watch - copilot usage Signed-off-by: Peter Siegmund diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6b130c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude/settings.local.json diff --git a/assets/icons/copilot.png b/assets/icons/copilot.png new file mode 100644 index 0000000..38b810f Binary files /dev/null and b/assets/icons/copilot.png differ diff --git a/assets/icons/petrol.svg b/assets/icons/petrol.svg new file mode 100644 index 0000000..28bccef --- /dev/null +++ b/assets/icons/petrol.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/de_DE.json b/de_DE.json new file mode 100644 index 0000000..23b7c29 --- /dev/null +++ b/de_DE.json @@ -0,0 +1,15 @@ +{ + "Localization": { + "URL": "URL", + "Station UUID": "Stations-UUID", + "Fuel Type": "Kraftstofftyp", + "Refresh Rate": "Aktualisierungsrate", + "On Press": "Bei Tastendruck", + "Every 1 min": "Jede Minute", + "Every 2 min": "Alle 2 Minuten", + "Every 5 min": "Alle 5 Minuten", + "Every 10 min": "Alle 10 Minuten", + "Every 30 min": "Alle 30 Minuten", + "Every Hour": "Jede Stunde" + } +} diff --git a/en.json b/en.json new file mode 100644 index 0000000..531370a --- /dev/null +++ b/en.json @@ -0,0 +1,15 @@ +{ + "Localization": { + "URL": "URL", + "Station UUID": "Station UUID", + "Fuel Type": "Fuel Type", + "Refresh Rate": "Refresh Rate", + "On Press": "On Press", + "Every 1 min": "Every 1 min", + "Every 2 min": "Every 2 min", + "Every 5 min": "Every 5 min", + "Every 10 min": "Every 10 min", + "Every 30 min": "Every 30 min", + "Every Hour": "Every Hour" + } +} diff --git a/libs/assets/u_active.svg b/libs/assets/u_active.svg new file mode 100644 index 0000000..2c10a0d --- /dev/null +++ b/libs/assets/u_active.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_active_none.svg b/libs/assets/u_active_none.svg new file mode 100644 index 0000000..05f3a2f --- /dev/null +++ b/libs/assets/u_active_none.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_check_checkbox.svg b/libs/assets/u_check_checkbox.svg new file mode 100644 index 0000000..8e2dc47 --- /dev/null +++ b/libs/assets/u_check_checkbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_check_none.svg b/libs/assets/u_check_none.svg new file mode 100644 index 0000000..2211655 --- /dev/null +++ b/libs/assets/u_check_none.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_check_radio.svg b/libs/assets/u_check_radio.svg new file mode 100644 index 0000000..c398ccb --- /dev/null +++ b/libs/assets/u_check_radio.svg @@ -0,0 +1,4 @@ + + + + diff --git a/libs/assets/u_down.svg b/libs/assets/u_down.svg new file mode 100644 index 0000000..b4e52ce --- /dev/null +++ b/libs/assets/u_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_file.svg b/libs/assets/u_file.svg new file mode 100644 index 0000000..3997d17 --- /dev/null +++ b/libs/assets/u_file.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_folder.svg b/libs/assets/u_folder.svg new file mode 100644 index 0000000..659a662 --- /dev/null +++ b/libs/assets/u_folder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/libs/assets/u_tip_error.svg b/libs/assets/u_tip_error.svg new file mode 100644 index 0000000..1a6d5f5 --- /dev/null +++ b/libs/assets/u_tip_error.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_tip_info.svg b/libs/assets/u_tip_info.svg new file mode 100644 index 0000000..3feec80 --- /dev/null +++ b/libs/assets/u_tip_info.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_tip_success.svg b/libs/assets/u_tip_success.svg new file mode 100644 index 0000000..2258bf1 --- /dev/null +++ b/libs/assets/u_tip_success.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/assets/u_tip_warn.svg b/libs/assets/u_tip_warn.svg new file mode 100644 index 0000000..12a9878 --- /dev/null +++ b/libs/assets/u_tip_warn.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/css/uspi.css b/libs/css/uspi.css new file mode 100644 index 0000000..8f8548d --- /dev/null +++ b/libs/css/uspi.css @@ -0,0 +1,352 @@ +:root { + --uspi-bodybg: #1e1f22; /* body背景色 */ + --uspi-inputbg: #18191B; /* 输入框背景色 */ + --uspi-textcolor: #fff; /* 文字颜色 */ + --uspi-unitcolor: #A6A6A6; /* 单位字体颜色 */ + --uspi-bordercolor: #3a3a3a; /* 边框颜色 */ + --uspi-borderradius: 4px; /* 边框圆角 */ + --uspi-width: 320px; /* 宽度 */ + --uspi-height: 32px; /* 高度 */ + --uspi-theme: #00FFE6; /* 主题颜色 */ + --uspi-label-width: 112px; /* 标签宽度 */ +} + + +*{ + box-sizing: border-box; +} + +html { + width: 100%; + padding: 0; + margin: 0; + min-height: 100vh; +} + +html,body{ + font-family: 'Source Han Sans SC', system-ui, -apple-system, Segoe UI, Roboto, Arial, "PingFang SC", "Microsoft Yahei", sans-serif; + font-size: 14px; + line-height: 20px; + color: var(--uspi-textcolor); +} + +body { + min-height: 100%; + padding: 0; + margin: 0; +} + +a{ + color: var(--uspi-theme); + cursor: pointer; +} + + +input, +textarea { + color: var(--uspi-textcolor); /* 改变可编辑区域内文字的颜色 */ + caret-color: #4AA3FF; /* 改变可编辑区域光标的颜色 */ +} + +input::placeholder, +textarea::placeholder, +select::placeholder { + color: #65686D; +} +button:focus, +textarea:focus, +input:focus, +select:focus, +option:focus, +details:focus, +summary:focus{ + outline: none; +} + +.uspi-item{ + display: flex; + align-items: center; + margin: 10px; +} +.uspi-item-label{ + width: var(--uspi-label-width); + color: var(--uspi-textcolor); + text-align: right; + margin-right: 10px; + +} +.uspi-item-label:after { + content: ": "; +} +.uspi-item-label.empty:after { + content: ""; +} +.uspi-item-value{ + width: var(--uspi-width); + display: flex; + align-items: center; + justify-content: space-between; +} +.uspi-item-value.no-label{ + width: auto; + margin-left: var(--uspi-label-width); + display: inline-block; +} + +select.uspi-item-value{ + padding: 0 6px; + color: var(--uspi-textcolor); + height: var(--uspi-height); + background-color: var(--uspi-inputbg); + border: 1px solid var(--uspi-inputbg); + border-radius: var(--uspi-borderradius); + line-height: var(--uspi-height); +} + +input.uspi-item-value{ + padding: 0 10px; + height: var(--uspi-height); + background-color: var(--uspi-inputbg); + border: 1px solid var(--uspi-inputbg); + border-radius: var(--uspi-borderradius); +} +textarea.uspi-item-value { + resize: none; + height: 68px; + max-height: 132px; + background-color: var(--uspi-inputbg); + border: 1px solid var(--uspi-inputbg); + border-radius: var(--uspi-borderradius); + padding: 8px; + color: var(--uspi-textcolor); +} +[type="file"] .uspi-item-value{ + /* padding: 0; */ + height: var(--uspi-height); + background-color: var(--uspi-inputbg); + border: 1px solid var(--uspi-inputbg); + border-radius: var(--uspi-borderradius); +} +[type="file"] .uspi-file-info{ + flex: 1; + padding: 0 10px; + color: var(--uspi-textcolor); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + background-color: var(--uspi-inputbg); + border: 1px solid var(--uspi-inputbg); +} +[type="file"] .uspi-file-label{ + display: flex; + justify-content: center; + cursor: pointer; + margin-right: 8px; +} + +input[type="radio"], +input[type="checkbox"]{ + display: none; +} + +input[type="radio"]+label, +input[type="checkbox"]+label{ + padding-left: 24px; + background: url(../assets/u_check_none.svg) no-repeat left center; + color: var(--uspi-textcolor); +} + +input[type="radio"]:checked+label{ + background: url(../assets/u_check_radio.svg) no-repeat left center; +} +input[type="checkbox"]:checked+label { + background: url(../assets/u_check_checkbox.svg) no-repeat left center; +} +input[type="range"]{ + flex: 1; + height: 4px; + background: var(--uspi-inputbg); + border-radius: 8px; + outline: none; +} +input[type="range"]+span{ + text-align: right; + min-width: 25px; + padding-left: 8px; +} + +.uspi-heading { + display: flex; + flex-basis: 100%; + align-items: center; + color: inherit; + font-size: 14px; + margin: 8px 0px; +} + +.uspi-heading::before, +.uspi-heading::after { + content: ""; + flex-grow: 1; + background: var(--uspi-bordercolor); + height: 1px; + font-size: 0px; + line-height: 0px; + margin: 0px 16px; +} + + +button{ + cursor: pointer; + padding: 0 16px; + height: var(--uspi-height); + background: none; + border: 1px solid var(--uspi-theme); + border-radius: 8px; + color: var(--uspi-theme); + width: 124px; +} +button.primary{ + background-color: var(--uspi-theme); + border: 1px solid var(--uspi-theme); + color: var(--uspi-bodybg); +} +button.uspi-item-value{ + display: inline-block; + margin-left: 108px; + width: 124px; +} + +button.default{ + border: 1px solid #fff; + background-color: #fff; + color: var(--uspi-bodybg); +} + +button.default-border{ + border: 1px solid #fff; + color: #fff; +} + + +button.disabled{ + cursor: not-allowed; + /* opacity: 0.6; */ + border: 1px solid #65686D; + color: #65686D; +} + + +hr{ + margin: 12px 16px; + border-style: none; + background: var(--uspi-bordercolor); + height: 1px; +} + +.tip{ + font-size: 12px; + color: var(--uspi-unitcolor); + margin: 0 10px; + line-height: 16px; + padding-left: 20px; +} +.tip.info{ + background: url(../assets/u_tip_info.svg) no-repeat left center; + background-size: 16px 16px; +} +.tip.success{ + background: url(../assets/u_tip_success.svg) no-repeat left center; + background-size: 16px 16px; +} +.tip.error{ + background: url(../assets/u_tip_error.svg) no-repeat left center; + background-size: 16px 16px; +} +.tip.warn{ + background: url(../assets/u_tip_warn.svg) no-repeat left center; + background-size: 16px 16px; +} + +details{ + color: var(--uspi-unitcolor); +} + +.uspi-label-placeholder{ + margin-left: var(--uspi-label-width); +} +.spinner { + width: 30px; + height: 30px; + border: 4px solid #555; + border-top: 4px solid rgba(255, 255, 255, 1); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 6px; +} +.loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 999; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.hidden{ + display: none; +} + + /* 滚动条整体样式 */ +::-webkit-scrollbar { + width: 4px; /* 垂直滚动条宽度 */ + height: 4px; /* 水平滚动条高度 */ +} + +/* 滚动条轨道 */ +::-webkit-scrollbar-track { + background: var(--uspi-bodybg); /* 轨道背景色 */ + border-radius: 4px; /* 轨道圆角 */ +} + +/* 滚动条滑块 */ +::-webkit-scrollbar-thumb { + background: #727476; /* 滑块背景色 */ + border-radius: 4px; /* 滑块圆角 */ + transition: background 0.3s; /* 过渡效果 */ +} + +/* 滚动条滑块悬停状态 */ +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; /* 悬停时的滑块颜色 */ +} + +/* 滚动条滑块激活状态(点击时) */ +::-webkit-scrollbar-thumb:active { + background: #888888; /* 激活时的滑块颜色 */ +} + +/* 滚动条角落(垂直和水平滚动条交汇处) */ +::-webkit-scrollbar-corner { + background: #f1f1f1; /* 角落背景色 */ +} + + + + + + diff --git a/libs/js/constants.js b/libs/js/constants.js new file mode 100644 index 0000000..081f6d4 --- /dev/null +++ b/libs/js/constants.js @@ -0,0 +1,46 @@ + + +/** + * Events used for communicating with Ulanzi Stream Deck + */ +const Events = Object.freeze({ + CONNECTED: 'connected', + CLOSE: 'close', + ERROR: 'error', + ADD: 'add', + RUN: 'run', + PARAMFROMAPP: 'paramfromapp', + PARAMFROMPLUGIN: 'paramfromplugin', + SETACTIVE: 'setactive', + CLEAR: 'clear', + TOAST:'toast', + STATE:'state', + OPENURL:'openurl', + OPENVIEW:'openview', + SELECTDIALOG:'selectdialog', + LOGMESSAGE:'logMessage', + HOTKEY:'hotkey', + SENDTOPROPERTYINSPECTOR:'sendToPropertyInspector', + SENDTOPLUGIN:'sendToPlugin', + SHOWALERT:'showAlert', + GETSETTINGS:'getSettings', + SETSETTINGS:'setSettings', + DIDRECEIVESETTINGS:'didReceiveSettings', + SETGLOBALSETTINGS:'setGlobalSettings', + GETGLOBALSETTINGS:'getGlobalSettings', + DIDRECEIVEGLOBALSETTINGS:'didReceiveGlobalSettings', + KEYDOWN:'keydown', + KEYUP:'keyup', + DIALEDOWN:'dialdown', + DIALEUP:'dialup', + DIALROTATE:'dialrotate' +}); + +/** + * Errors received from WebSocket + */ +const SocketErrors = { + DEFAULT:'closed *****' +}; + + diff --git a/libs/js/eventEmitter.js b/libs/js/eventEmitter.js new file mode 100644 index 0000000..0e807e4 --- /dev/null +++ b/libs/js/eventEmitter.js @@ -0,0 +1,47 @@ +class ULANZIEventEmitter { + constructor (id, debug = false) { + + const eventList = new Map(); + const ALLEVENTS = "*"; + + eventList.hasWildcard = function(name, data) { + for(const [key, value] of this) { + if(key !== ALLEVENTS && key.includes(ALLEVENTS) && new RegExp(`^${key.split(/\*+/).join('.*')}$`).test(name)) { + if(data) value.pub(data, name); + else return true; + } + } + }; + + this.on = (name, fn) => { + if(!eventList.has(name)) eventList.set(name, ULANZIEventEmitter.pubSub()); + return eventList.get(name).sub(fn); + }; + + this.has = name => eventList.has(name); + this.hasMatch = name => eventList.has(name) || eventList.hasWildcard(name); + this.emit = (name, data) => { + eventList.has(name) && eventList.get(name).pub(data, name); + eventList.has(ALLEVENTS) && eventList.get(ALLEVENTS).pub(data, name); + eventList.hasWildcard(name, data); + }; + + return this; + } + + static pubSub() { + const subscribers = new Set(); + + const sub = fn => { + subscribers.add(fn); + return () => { + subscribers.delete(fn); + }; + }; + + const pub = (data, name) => subscribers.forEach(fn => fn(data, name)); + return Object.freeze({pub, sub}); + } +} + +const EventEmitter = new ULANZIEventEmitter(); \ No newline at end of file diff --git a/libs/js/timers.js b/libs/js/timers.js new file mode 100644 index 0000000..8d10cf6 --- /dev/null +++ b/libs/js/timers.js @@ -0,0 +1,87 @@ +/* global USDTimerWorker */ + +let USDTimerWorker = new Worker(URL.createObjectURL( + new Blob([timerFn.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '')], {type: 'text/javascript'}) +)); +USDTimerWorker.timerId = 1; +USDTimerWorker.timers = {}; +const USDDefaultTimeouts = { + timeout: 0, + interval: 10 +}; + +Object.freeze(USDDefaultTimeouts); + +function _setTimer(callback, delay, type, params) { + const id = USDTimerWorker.timerId++; + USDTimerWorker.timers[id] = {callback, params}; + USDTimerWorker.onmessage = (e) => { + if(USDTimerWorker.timers[e.data.id]) { + if(e.data.type === 'clearTimer') { + delete USDTimerWorker.timers[e.data.id]; + } else { + const cb = USDTimerWorker.timers[e.data.id].callback; + if(cb && typeof cb === 'function') cb(...USDTimerWorker.timers[e.data.id].params); + } + } + }; + USDTimerWorker.postMessage({type, id, delay}); + return id; +} + +function _setTimeoutUSD(...args) { + let [callback, delay = 0, ...params] = [...args]; + return _setTimer(callback, delay, 'setTimeout', params); +} + +function _setIntervalUSD(...args) { + let [callback, delay = 0, ...params] = [...args]; + return _setTimer(callback, delay, 'setInterval', params); +} + +function _clearTimeoutUSD(id) { + USDTimerWorker.postMessage({type: 'clearTimeout', id}); // USDTimerWorker.postMessage({type: 'clearInterval', id}); = same thing + delete USDTimerWorker.timers[id]; +} + +window.setTimeout = _setTimeoutUSD; +window.setInterval = _setIntervalUSD; +window.clearTimeout = _clearTimeoutUSD; //timeout and interval share the same timer-pool +window.clearInterval = _clearTimeoutUSD; + + + +function timerFn() { + + let timers = {}; + let debug = false; + let supportedCommands = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval']; + + function log(e) {console.log('Worker-Info::Timers', timers);} + + function clearTimerAndRemove(id) { + if(timers[id]) { + if(debug) console.log('clearTimerAndRemove', id, timers[id], timers); + clearTimeout(timers[id]); + delete timers[id]; + postMessage({type: 'clearTimer', id: id}); + if(debug) log(); + } + } + + onmessage = function(e) { + // first see, if we have a timer with this id and remove it + // this automatically fulfils clearTimeout and clearInterval + supportedCommands.includes(e.data.type) && timers[e.data.id] && clearTimerAndRemove(e.data.id); + if(e.data.type === 'setTimeout') { + timers[e.data.id] = setTimeout(() => { + postMessage({id: e.data.id}); + clearTimerAndRemove(e.data.id); //cleaning up + }, Math.max(e.data.delay || 0)); + } else if(e.data.type === 'setInterval') { + timers[e.data.id] = setInterval(() => { + postMessage({id: e.data.id}); + }, Math.max(e.data.delay || USDDefaultTimeouts.interval)); + } + }; +} diff --git a/libs/js/ulanzideckApi.js b/libs/js/ulanzideckApi.js new file mode 100644 index 0000000..d69578d --- /dev/null +++ b/libs/js/ulanzideckApi.js @@ -0,0 +1,904 @@ +/// +/// + +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(); diff --git a/libs/js/utils.js b/libs/js/utils.js new file mode 100644 index 0000000..b317e6e --- /dev/null +++ b/libs/js/utils.js @@ -0,0 +1,549 @@ +class UlanziUtils { + + /** + * 获取表单数据 + * Returns the value from a form using the form controls name property + * @param {Element | string} form + * @returns + */ + getFormValue(form) { + if (typeof form === 'string') { + form = document.querySelector(form); + } + + const elements = form ? form.elements : ''; + + if (!elements) { + console.error('Could not find form!'); + } + + const formData = new FormData(form); + let formValue = {}; + + formData.forEach((value, key) => { + if (!Reflect.has(formValue, key)) { + formValue[key] = value; + return; + } + if (!Array.isArray(formValue[key])) { + formValue[key] = [formValue[key]]; + } + formValue[key].push(value); + }); + + return formValue; + } + + /** + * 重载表单数据 + * Sets the value of form controls using their name attribute and the jsn object key + * @param {*} jsn + * @param {Element | string} form + */ + setFormValue(jsn, form) { + if (!jsn) { + return; + } + + if (typeof form === 'string') { + form = document.querySelector(form); + } + + const elements = form ? form.elements : ''; + + if (!elements) { + console.error('Could not find form!'); + } + + Array.from(elements) + .filter((element) => element ? element.name : null) + .forEach((element) => { + const { name, type } = element; + const value = name in jsn ? jsn[name] : null; + const isCheckOrRadio = type === 'checkbox' || type === 'radio'; + + if (value === null) return; + + if (isCheckOrRadio) { + const isSingle = value === element.value; + if (isSingle || (Array.isArray(value) && value.includes(element.value))) { + element.checked = true; + } + } else { + element.value = value ? value : ''; + } + }); + } + + /** + * 延迟触发 + * This provides a slight delay before processing rapid events + * @param {function} fn + * @param {number} wait - delay before processing function (recommended time 150ms) + * @returns + */ + debounce(fn, wait = 150) { + let timeoutId = null + return (...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + fn.apply(null, args); + }, wait); + }; + } + + /** + * 返回url的查询参数 + */ + getQueryParams(param) { + const searchParams = new URLSearchParams(window.location.search); + return searchParams.get(param); + } + + /** + * 获取浏览器语言 + * Returns the user language + */ + getLanguage() { + let userLanguage = navigator.languages && navigator.languages.length ? navigator.languages[0] : (navigator.language || navigator.userLanguage); + if (userLanguage == 'zh') { + userLanguage = 'zh_CN' + } else if (userLanguage.indexOf('zh-') >= 0) { + userLanguage = userLanguage.split('-').join('_') + } else if (userLanguage.indexOf('-') !== -1) { + userLanguage = userLanguage.replace(/-/g, '_'); + } + return this.adaptLanguage(userLanguage); + } + + /** + * 适配语言环境 + */ + adaptLanguage(ln) { + let userLanguage = ln; + if (ln.indexOf('zh') == 0) { + if(ln.indexOf('CN') > -1){ + userLanguage = 'zh_CN' + }else{ + userLanguage = 'zh_HK' + } + } else if (ln.indexOf('en') == 0) { + userLanguage = 'en' + } else if (userLanguage.indexOf('-') !== -1) { + userLanguage = userLanguage.replace(/-/g, '_'); + } + + return userLanguage + } + + /** + * JSON.parse优化 + * parse json + * @param {string} jsonString + * @returns {object} json + */ + parseJson(jsonString) { + if (typeof jsonString === 'object') return jsonString; + try { + const o = JSON.parse(jsonString); + if (o && typeof o === 'object') { + return o; + } + } catch (e) { } + + return false; + } + + /** + * 读取json文件 + * Reads a json file + * @param {string} path + * @returns {Promise} json + */ + async readJson(path) { + if (!path) { + console.error('A path is required to readJson.'); + } + + return new Promise((resolve, reject) => { + try { + const req = new XMLHttpRequest(); + req.onerror = reject; + req.overrideMimeType('application/json'); + req.open('GET', path, true); + req.onreadystatechange = (response) => { + if (req.readyState === 4) { + const jsonString = response && response.target && response.target.response || ''; + if (jsonString) { + try { + resolve(JSON.parse(jsonString)); + } catch (e) { + reject(); + } + } else { + reject(); + } + } + }; + + req.send(); + + } catch (e) { + reject(); + } + }); + } + + + /** + * 完整图片转base64 + * @param {string} url 图片地址 + * @param {number} width canvas宽度,默认196 + * @param {number} height canvas宽度,默认196 + * @param {HTMLCanvasElement} inCanvas canvas元素,默认创建 + * @param {boolean} returnCanvas 是否返回canvas,默认false。默认返回base64的图片路径,有些时候需要接着画布添加元素,所以我们添加这个变量 + * @return { string | HTMLCanvasElement } 默认返回base64的图片路径,returnCanvas为true返回画布 + */ + async drawImage(url, width = 196, height = 196, inCanvas, returnCanvas) { + const canvas = inCanvas && inCanvas instanceof HTMLCanvasElement ? inCanvas : document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + + const imgData = await this.loadImagePromise(url) + if (imgData.status == 'ok') { + ctx.drawImage(imgData.img, 0, 0, canvas.width, canvas.height); + } + return returnCanvas ? canvas : canvas.toDataURL('image/png'); //需要是否需要返回画布或者直接返回base64 + } + + /** + * 裁剪图片转base64 + * @param {string} url 图片地址 + * @param {number} offsetX 裁剪x的位置 + * @param {number} offsetY 裁剪y的位置 + * @param {number} width canvas宽度,默认196 + * @param {number} height canvas宽度,默认196 + * @param {HTMLCanvasElement} inCanvas canvas元素,默认创建 + * @param {boolean} returnCanvas 是否返回canvas,默认false。默认返回base64的图片路径,有些时候需要接着画布添加元素,所以我们添加这个变量 + * @return { string | HTMLCanvasElement } 默认返回base64的图片路径,returnCanvas为true返回画布 + */ + async cropImage(url, offsetX, offsetY, width = 196, height = 196, inCanvas, returnCanvas) { + const canvas = inCanvas && inCanvas instanceof HTMLCanvasElement ? inCanvas : document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = width; + canvas.height = height; + + + const imgData = await this.loadImagePromise(url) + if (imgData.status == 'ok') { + ctx.drawImage(imgData.img, offsetX, offsetY, width, height, 0, 0, canvas.width, canvas.height); + } + + return returnCanvas ? canvas : canvas.toDataURL('image/png'); //需要是否需要返回画布或者直接返回base64 + + }; + + /** + * 获取图片数据 + * @param {string} url 图片地址 + * @return {object} {url, status: 'ok', img} or {url, status: 'error'} + */ + loadImagePromise(url) { + return new Promise(resolve => { + const img = new Image(); + img.onload = () => resolve({ url, status: 'ok', img }); + img.onerror = () => resolve({ url, status: 'error' }); + img.src = url; + }); + } + + + getData(url, param) { + + param = Object.assign(param || {}, Utils.joinTimestamp()); + + //若参数有数组,进行特殊拼接 + url = url + '?' + Object.keys(param).map(e => { + let str = '' + //判断数组拼接 + if (param[e] instanceof Array) { + str = param[e].map((item) => { + return `${e}=${item}` + }).join('&') + } else { + str = `${e}=${param[e]}` + } + return str + }).join('&'); + // console.warn('=====getData url:', url) + return new Promise(function (resolve, reject) { + var req = new XMLHttpRequest(); + + req.timeout = 1500; // 设置超时时间为 5 秒 + + req.ontimeout = function () { + console.error('Request timed out'); + }; + + req.onload = function () { + // console.warn('=====getData onload:') + if (req.status === 200) { + // console.warn('=====getData success:') + resolve(req.response); + } else { + // console.warn('=====getData not 200:') + reject(Error(req.statusText)); + } + }; + + req.onerror = function () { + // console.warn('=====getData error:') + reject(Error('Network Error')); + }; + + req.open('GET', url, true); + req.send(); + }); + }; + + /** + * 获取接口数据 + * @param {string} url 接口地址 + * @param {object} param 接口参数 + * @param {string} method 请求方式:GET/POST/PUT/DELETE + * @param {object} headers 请求头 + */ + fetchData(url, param, method = 'GET', headers = {}) { + + if (method.toUpperCase() === 'GET') { + param = Object.assign(param || {}, Utils.joinTimestamp()); + + const tag = url.indexOf('?') >= 0 ? '&':'?' + + //若参数有数组,进行特殊拼接 + url = url + tag + Object.keys(param).map(e => { + let str = '' + //判断数组拼接 + if (param[e] instanceof Array) { + str = param[e].map((item) => { + return `${e}=${item}` + }).join('&') + } else { + str = `${e}=${param[e]}` + } + return str + }).join('&'); + } + + const opts = { + cache: 'no-cache', + headers, + method: method, + body: ['GET', 'HEAD'].includes(method) + ? undefined + : param, + }; + return new Promise(function (resolve, reject) { + Utils.fetchWithTimeout(url, opts) + .then(async (resp) => { + // console.warn('==fetch success:', url) + if (!resp) { + reject(new Error('No Resp')); + } + if (!resp.ok) { + const errData = await resp.json(); + if (errData) { + reject(errData); + } else { + reject(new Error(`{${resp.status}: ${await resp.text()}}`)); + } + + } else { + resolve(await resp.json()); + } + }) + .catch((err) => { + // console.warn('==fetch error:', JSON.stringify(err)) + reject(err); + }) + }); + } + + /** + * 封装fetch请求,设置超时时间 + */ + fetchWithTimeout(url, options = {}) { + const { timeout = 15000 } = options; // 设置默认超时时间为8000ms + // console.warn('====fetchWithTimeout timeout:', timeout) + + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + + + // console.warn('==fetchWithTimeout:', url, JSON.stringify(options)) + const response = fetch(url, { + ...options, + signal: controller.signal + }).then((response) => { + // console.warn('==fetchWithTimeout success:', JSON.stringify(response)) + clearTimeout(id); + return response; + }).catch((error) => { + // console.warn('==fetchWithTimeout error:', JSON.stringify(error)) + clearTimeout(id); + throw error; + }); + + return response; + + } + + /** + * 获取随机时间戳 + */ + joinTimestamp() { + const now = new Date().getTime(); + return { _t: now }; + } + + + //判断是否为文件类型 + isFile(variable) { + return variable instanceof File; + } + + /** + * 浏览器file转base64 + */ + htmlFileToBase64(file) { + if (!this.isFile(file)) { + return Promise.reject(new Error('Not a file')); + } + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + }); + } + + drawText(text, stroke = "#fff", background = "#000", wh = 196, textLabel, inCanvas) { + // console.log('==drawText:', text, textLabel) + const canvas = inCanvas ? inCanvas : document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if(!inCanvas){ + canvas.width = wh; + canvas.height = wh; + if (background == "transparent") { + ctx.clearRect(0, 0, canvas.width, canvas.height); + } else { + ctx.fillStyle = background; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + } + + + + const fSize = text.length > 8 ? 30 - text.length / 2 : 40; + + + ctx.strokeStyle = "#000"; + ctx.lineWidth = 4; + + ctx.fillStyle = stroke; + ctx.font = `bold ${fSize}px "Source Han Sans SC"`; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + + ctx.strokeText(text, ctx.canvas.width / 2, ctx.canvas.height / 2); + ctx.fillText(text, ctx.canvas.width / 2, ctx.canvas.height / 2 ); + + if(textLabel){ + ctx.font = `bold 24px "Source Han Sans SC"`; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'left'; + ctx.fillText(textLabel, 10, 20); + } + + + return canvas.toDataURL('image/png') + } + + getProperty(obj, dotSeparatedKeys, defaultValue) { + if (arguments.length > 1 && typeof dotSeparatedKeys !== 'string') return undefined; + if (typeof obj !== 'undefined' && typeof dotSeparatedKeys === 'string') { + const pathArr = dotSeparatedKeys.split('.'); + pathArr.forEach((key, idx, arr) => { + if (typeof key === 'string' && key.includes('[')) { + try { + // extract the array index as string + const pos = /\[([^)]+)\]/.exec(key)[1]; + // get the index string length (i.e. '21'.length === 2) + const posLen = pos.length; + arr.splice(idx + 1, 0, Number(pos)); + + // keep the key (array name) without the index comprehension: + // (i.e. key without [] (string of length 2) + // and the length of the index (posLen)) + arr[idx] = key.slice(0, -2 - posLen); // eslint-disable-line no-param-reassign + } catch (e) { + // do nothing + } + } + }); + // eslint-disable-next-line no-param-reassign, no-confusing-arrow + obj = pathArr.reduce((o, key) => (o && o[key] !== 'undefined' ? o[key] : undefined), obj); + } + return obj === undefined ? defaultValue : obj; + }; + + getProp(jsn, str, defaultValue = {}, sep = '.') { + const arr = str.split(sep); + return arr.reduce((obj, key) => (obj && obj.hasOwnProperty(key) ? obj[key] : defaultValue), jsn); + }; + + /** + * 获取插件根目录路径 + */ + getPluginPath(){ + const currentFilePath = location.pathname; + let split_tag = '/' + if(currentFilePath.indexOf('\\') > -1){ + split_tag = '\\' + } + const pathArr = currentFilePath.split(split_tag); + const idx = pathArr.findIndex(f => f.endsWith('ulanziPlugin')); + const __folderpath = `${pathArr.slice(0, idx + 1).join("/")}`; + + return __folderpath; + + } + + /** + * Logs a message + * @param {any} msg + */ + log(...msg) { + console.warn(`[${new Date().toLocaleString('zh-CN', { hour12: false })}]`, ...msg); + // this.getQueryParams('debug') && console.log(`[${new Date().toLocaleString('zh-CN', {hour12: false})}]`, ...msg); + } + + /** + * Logs a warning message + */ + warn(...msg) { + console.warn(`[${new Date().toLocaleString('zh-CN', { hour12: false })}]`, ...msg); + } + + /** + * Logs an error message + */ + error(...msg) { + console.error(`[${new Date().toLocaleString('zh-CN', { hour12: false })}]`, ...msg); + } +} + +const Utils = new UlanziUtils() \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..572736c --- /dev/null +++ b/manifest.json @@ -0,0 +1,59 @@ +{ + "Version": "1.0.0", + "Author": "mars3142", + "Name": "mars3142 Collection", + "Description": "A collection of plugins by mars3142.", + "Icon": "assets/icons/icon.svg", + "Category": "mars3142", + "CategoryIcon": "assets/icons/icon.svg", + "CodePath": "plugin/app.html", + "Type": "JavaScript", + "SupportedInMultiActions": false, + "PrivateAPI": true, + "UUID": "dev.mars3142.ulanzideck.collection", + "Actions": [ + { + "Name": "Petrol Watch", + "Icon": "assets/icons/petrol.svg", + "PropertyInspectorPath": "property-inspector/petrol/inspector.html", + "state": 0, + "States": [ + { + "Name": "Default", + "Image": "assets/icons/petrol.svg" + } + ], + "Tooltip": "Monitors petrol prices from a configured URL", + "UUID": "dev.mars3142.ulanzideck.collection.petrol", + "SupportedInMultiActions": false + }, + { + "Name": "Copilot Usage", + "Icon": "assets/icons/copilot.png", + "PropertyInspectorPath": "property-inspector/copilot/inspector.html", + "state": 0, + "States": [ + { + "Name": "Default", + "Image": "assets/icons/copilot.png" + } + ], + "Tooltip": "Displays GitHub Copilot usage percentage as a gauge", + "UUID": "dev.mars3142.ulanzideck.collection.copilot", + "SupportedInMultiActions": false + } + ], + "OS": [ + { + "Platform": "mac", + "MinimumVersion": "10.11" + }, + { + "Platform": "windows", + "MinimumVersion": "10" + } + ], + "Software": { + "MinimumVersion": "2.1.18" + } +} \ No newline at end of file diff --git a/plugin/actions/CopilotAction.js b/plugin/actions/CopilotAction.js new file mode 100644 index 0000000..2196fd0 --- /dev/null +++ b/plugin/actions/CopilotAction.js @@ -0,0 +1,175 @@ +class CopilotAction { + constructor($UD, context) { + this.$UD = $UD; + this.context = context; + this.config = { + url: '', + refreshRate: '4', + }; + this.refreshTimer = null; + this.debounceTimer = 0; + } + + setActive() {} + + onClear() { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + this.debounceTimer = 0; + } + } + + setParams(jsn) { + this.config = Object.assign(this.config, (jsn && jsn.param) || {}); + this.start(); + } + + onRun() { + this.fetchData(); + } + + start() { + this.onClear(); + this.fetchData(); + const duration = this.getRefreshDuration(); + if (duration > 0) { + this.refreshTimer = setInterval(() => this.fetchData(), duration * 60 * 1000); + } + } + + async fetchData() { + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(async () => { + if (!this.config.url) { + this.renderGauge(null); + return; + } + try { + const response = await fetch(this.config.url); + const text = await response.text(); + const value = this.parsePrometheus(text); + this.renderGauge(value !== null ? parseFloat(value) : null); + } catch (e) { + this.renderGauge(null); + } + }, 300); + } + + parsePrometheus(text) { + for (const line of text.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + if (trimmed.startsWith('github_copilot_usage_percentage')) { + const closingBrace = trimmed.indexOf('}'); + if (closingBrace !== -1) return trimmed.slice(closingBrace + 1).trim(); + // no labels + const parts = trimmed.split(/\s+/); + if (parts.length >= 2) return parts[1]; + } + } + return null; + } + + renderGauge(value) { + const canvas = document.createElement('canvas'); + canvas.width = 196; + canvas.height = 196; + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, 196, 196); + + const cx = 98, cy = 100; + const r = 68; + const lineWidth = 14; + + // Gauge runs from 225° to 315° clockwise (270° sweep) + const startAngle = 135 * Math.PI / 180; + const totalSweep = 270 * Math.PI / 180; + const greenEnd = startAngle + 0.50 * totalSweep; // 50% + const yellowEnd = startAngle + 0.80 * totalSweep; // 80% + const endAngle = startAngle + totalSweep; // 100% + + // Background track + ctx.beginPath(); + ctx.arc(cx, cy, r, startAngle, endAngle, false); + ctx.strokeStyle = '#333333'; + ctx.lineWidth = lineWidth; + ctx.lineCap = 'round'; + ctx.stroke(); + + if (value !== null) { + const pct = Math.max(0, Math.min(100, value)) / 100; + const valueAngle = startAngle + pct * totalSweep; + + // Green segment (0–50%) + if (pct > 0) { + ctx.beginPath(); + ctx.arc(cx, cy, r, startAngle, Math.min(valueAngle, greenEnd), false); + ctx.strokeStyle = '#4caf50'; + ctx.lineWidth = lineWidth; + ctx.lineCap = 'round'; + ctx.stroke(); + } + + // Yellow segment (50–80%) + if (pct > 0.5) { + ctx.beginPath(); + ctx.arc(cx, cy, r, greenEnd, Math.min(valueAngle, yellowEnd), false); + ctx.strokeStyle = '#ffeb3b'; + ctx.lineWidth = lineWidth; + ctx.lineCap = 'butt'; + ctx.stroke(); + } + + // Red segment (80–100%) + if (pct > 0.8) { + ctx.beginPath(); + ctx.arc(cx, cy, r, yellowEnd, valueAngle, false); + ctx.strokeStyle = '#f44336'; + ctx.lineWidth = lineWidth; + ctx.lineCap = 'butt'; + ctx.stroke(); + } + + // Value text — centered, colored + const color = pct <= 0.5 ? '#4caf50' : pct <= 0.8 ? '#ffeb3b' : '#f44336'; + const displayText = value.toFixed(1) + '%'; + const fSize = displayText.length > 6 ? 28 : 34; + ctx.fillStyle = color; + ctx.font = `bold ${fSize}px "Source Han Sans SC"`; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.fillText(displayText, cx, cy + 8); + } else { + ctx.fillStyle = '#666666'; + ctx.font = 'bold 28px "Source Han Sans SC"'; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.fillText('N/A', cx, cy + 8); + } + + // Label — bottom + ctx.fillStyle = '#7ec8e3'; + ctx.font = 'bold 20px "Source Han Sans SC"'; + ctx.fillText('Copilot', cx, 165); + + this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png')); + } + + getRefreshDuration() { + switch (this.config.refreshRate) { + case '1': return 1; + case '2': return 2; + case '3': return 5; + case '4': return 10; + case '5': return 30; + case '6': return 60; + default: return 0; + } + } +} diff --git a/plugin/actions/PetrolAction.js b/plugin/actions/PetrolAction.js new file mode 100644 index 0000000..84b9f9c --- /dev/null +++ b/plugin/actions/PetrolAction.js @@ -0,0 +1,151 @@ +class PetrolAction { + constructor($UD, context) { + this.$UD = $UD; + this.context = context; + this.config = { + url: '', + stationUuid: '', + fuelType: '', + refreshRate: '1', + }; + this.refreshTimer = null; + this.debounceTimer = 0; + this.previousPrice = null; + this.lastDelta = null; + + this.$UD.onDidReceiveSettings(jsn => { + if (jsn.context !== this.context) return; + const s = jsn.settings || {}; + this.config = Object.assign(this.config, s); + if (s.previousPrice != null) this.previousPrice = s.previousPrice; + if (s.lastDelta != null) this.lastDelta = s.lastDelta; + }); + } + + setActive() {} + + onClear() { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + this.debounceTimer = 0; + } + } + + setParams(jsn) { + const params = (jsn && jsn.param) || {}; + this.config = Object.assign(this.config, params); + this.$UD.getSettings(this.context); + this.start(); + } + + onRun() { + this.fetchPrice(); + } + + start() { + this.onClear(); + this.fetchPrice(); + const duration = this.getRefreshDuration(); + if (duration > 0) { + this.refreshTimer = setInterval(() => this.fetchPrice(), duration * 60 * 1000); + } + } + + async fetchPrice() { + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(async () => { + if (!this.config.url || !this.config.stationUuid || !this.config.fuelType) { + this.renderButton('?', null); + return; + } + try { + const response = await fetch(this.config.url); + const text = await response.text(); + const raw = this.parsePrometheus(text); + if (raw) { + const current = parseFloat(raw); + if (this.previousPrice !== null) { + const delta = current - this.previousPrice; + if (delta !== 0) this.lastDelta = delta; + } + this.previousPrice = current; + this.renderButton(`${raw}€`, this.lastDelta); + this.$UD.setSettings({ ...this.config, previousPrice: this.previousPrice, lastDelta: this.lastDelta }, this.context); + } else { + this.renderButton('N/A', null); + } + } catch (e) { + this.renderButton('ERR', null); + } + }, 300); + } + + parsePrometheus(text) { + for (const line of text.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + if (trimmed.startsWith('petrol_price_euro') && trimmed.includes(this.config.stationUuid) && trimmed.includes(this.config.fuelType)) { + const closingBrace = trimmed.indexOf('}'); + if (closingBrace !== -1) return trimmed.slice(closingBrace + 1).trim(); + } + } + return null; + } + + renderButton(priceText, delta) { + const canvas = document.createElement('canvas'); + canvas.width = 196; + canvas.height = 196; + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, 196, 196); + + ctx.strokeStyle = '#000'; + ctx.lineWidth = 3; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + + // Fuel type — top, light blue + ctx.fillStyle = '#7ec8e3'; + ctx.font = 'bold 28px "Source Han Sans SC"'; + ctx.strokeText(this.config.fuelType, 98, 45); + ctx.fillText(this.config.fuelType, 98, 45); + + // Price — middle, yellow + const fSize = priceText.length > 6 ? 34 - priceText.length : 40; + ctx.fillStyle = '#f0c040'; + ctx.font = `bold ${fSize}px "Source Han Sans SC"`; + ctx.strokeText(priceText, 98, 105); + ctx.fillText(priceText, 98, 105); + + // Delta — bottom + if (delta !== null) { + const arrow = delta > 0 ? '▲' : '▼'; + const color = delta > 0 ? '#ff6b6b' : '#6bff6b'; + const deltaText = `${arrow} ${Math.abs(delta).toFixed(3)}`; + ctx.fillStyle = color; + ctx.font = '18px "Source Han Sans SC"'; + ctx.strokeText(deltaText, 98, 142); + ctx.fillText(deltaText, 98, 142); + } + + this.$UD.setBaseDataIcon(this.context, canvas.toDataURL('image/png')); + } + + getRefreshDuration() { + switch (this.config.refreshRate) { + case '1': return 1; + case '2': return 2; + case '3': return 5; + case '4': return 10; + case '5': return 30; + case '6': return 60; + default: return 0; + } + } +} diff --git a/plugin/app.html b/plugin/app.html new file mode 100644 index 0000000..47b7e81 --- /dev/null +++ b/plugin/app.html @@ -0,0 +1,18 @@ + + + + + mars3142 Collection + + + + + + + + + + + + + diff --git a/plugin/app.js b/plugin/app.js new file mode 100644 index 0000000..71264e4 --- /dev/null +++ b/plugin/app.js @@ -0,0 +1,64 @@ +const ACTION_CACHES = {}; + +$UD.connect('dev.mars3142.ulanzideck.collection'); +$UD.onConnected(() => { + console.log('app.js onConnected'); +}); + +$UD.onAdd(jsn => { + const context = jsn.context; + console.log('app.js onAdd:', JSON.stringify(jsn)); + + if (!ACTION_CACHES[context]) { + const name = jsn.uuid.split('.').pop().toLowerCase(); + if (name === 'petrol') { + ACTION_CACHES[context] = new PetrolAction($UD, context); + } else if (name === 'copilot') { + ACTION_CACHES[context] = new CopilotAction($UD, context); + } + } + + if (ACTION_CACHES[context]) ACTION_CACHES[context].setParams(jsn); +}); + +$UD.onSetActive(jsn => { + const instance = ACTION_CACHES[jsn.context]; + if (instance) instance.setActive(jsn.active); +}); + +$UD.onRun(jsn => { + const context = jsn.context; + if (!ACTION_CACHES[context]) { + $UD.emit('add', jsn); + } else { + ACTION_CACHES[context].onRun(); + } +}); + +$UD.onClear(jsn => { + if (jsn.param) { + for (let i = 0; i < jsn.param.length; i++) { + const context = jsn.param[i].context; + if (ACTION_CACHES[context]) { + ACTION_CACHES[context].onClear(); + delete ACTION_CACHES[context]; + } + } + } +}); + +$UD.onParamFromApp(jsn => { + console.log('app.js onParamFromApp:', JSON.stringify(jsn.param)); + onSetSettings(jsn); +}); + +$UD.onParamFromPlugin(jsn => { + console.log('app.js onParamFromPlugin:', JSON.stringify(jsn.param)); + onSetSettings(jsn); +}); + +function onSetSettings(jsn) { + const action = ACTION_CACHES[jsn.context]; + if (!action) return; + action.setParams(jsn); +} diff --git a/property-inspector/copilot/inspector.html b/property-inspector/copilot/inspector.html new file mode 100644 index 0000000..63a4a59 --- /dev/null +++ b/property-inspector/copilot/inspector.html @@ -0,0 +1,37 @@ + + + + + + Copilot Usage + + + +
+
+
+
URL
+ +
+
+
Refresh Rate
+ +
+
+
+ + + + + + + + diff --git a/property-inspector/copilot/inspector.js b/property-inspector/copilot/inspector.js new file mode 100644 index 0000000..0757488 --- /dev/null +++ b/property-inspector/copilot/inspector.js @@ -0,0 +1,31 @@ +let ACTION_SETTING = {}; +let form = ''; + +$UD.connect('dev.mars3142.ulanzideck.collection.copilot'); + +$UD.onConnected(() => { + form = document.querySelector('#property-inspector'); + + form.addEventListener('input', Utils.debounce(() => { + const value = Utils.getFormValue(form); + ACTION_SETTING = { ...ACTION_SETTING, ...value }; + $UD.sendParamFromPlugin(ACTION_SETTING); + })); +}); + +$UD.onAdd(jsn => { + if (jsn && jsn.param) settingSaveParam(jsn.param); +}); + +$UD.onParamFromApp(jsn => { + settingSaveParam((jsn && jsn.param) || {}); +}); + +$UD.onParamFromPlugin(jsn => { + settingSaveParam((jsn && jsn.param) || {}); +}); + +function settingSaveParam(params) { + ACTION_SETTING = { ...ACTION_SETTING, ...params }; + if (form) Utils.setFormValue(ACTION_SETTING, form); +} diff --git a/property-inspector/petrol/inspector.html b/property-inspector/petrol/inspector.html new file mode 100644 index 0000000..d567627 --- /dev/null +++ b/property-inspector/petrol/inspector.html @@ -0,0 +1,45 @@ + + + + + + Petrol Watch + + + +
+
+
+
URL
+ +
+
+
Station UUID
+ +
+
+
Fuel Type
+ +
+
+
Refresh Rate
+ +
+
+
+ + + + + + + + diff --git a/property-inspector/petrol/inspector.js b/property-inspector/petrol/inspector.js new file mode 100644 index 0000000..e0e3c51 --- /dev/null +++ b/property-inspector/petrol/inspector.js @@ -0,0 +1,31 @@ +let ACTION_SETTING = {}; +let form = ''; + +$UD.connect('dev.mars3142.ulanzideck.collection.petrol'); + +$UD.onConnected(() => { + form = document.querySelector('#property-inspector'); + + form.addEventListener('input', Utils.debounce(() => { + const value = Utils.getFormValue(form); + ACTION_SETTING = { ...ACTION_SETTING, ...value }; + $UD.sendParamFromPlugin(ACTION_SETTING); + })); +}); + +$UD.onAdd(jsn => { + if (jsn && jsn.param) settingSaveParam(jsn.param); +}); + +$UD.onParamFromApp(jsn => { + settingSaveParam((jsn && jsn.param) || {}); +}); + +$UD.onParamFromPlugin(jsn => { + settingSaveParam((jsn && jsn.param) || {}); +}); + +function settingSaveParam(params) { + ACTION_SETTING = { ...ACTION_SETTING, ...params }; + if (form) Utils.setFormValue(ACTION_SETTING, form); +}