From fbf40f75b2f951c7e096069ff85180116c38516f Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Thu, 9 Apr 2026 20:42:31 +0200 Subject: [PATCH] first plugins for Ulanzi D200H - petrol watch - copilot usage Signed-off-by: Peter Siegmund --- .gitignore | 1 + assets/icons/copilot.png | Bin 0 -> 73102 bytes assets/icons/petrol.svg | 13 + de_DE.json | 15 + en.json | 15 + libs/assets/u_active.svg | 3 + libs/assets/u_active_none.svg | 3 + libs/assets/u_check_checkbox.svg | 3 + libs/assets/u_check_none.svg | 3 + libs/assets/u_check_radio.svg | 4 + libs/assets/u_down.svg | 3 + libs/assets/u_file.svg | 3 + libs/assets/u_folder.svg | 4 + libs/assets/u_tip_error.svg | 3 + libs/assets/u_tip_info.svg | 3 + libs/assets/u_tip_success.svg | 3 + libs/assets/u_tip_warn.svg | 3 + libs/css/uspi.css | 352 +++++++++ libs/js/constants.js | 46 ++ libs/js/eventEmitter.js | 47 ++ libs/js/timers.js | 87 +++ libs/js/ulanzideckApi.js | 904 ++++++++++++++++++++++ libs/js/utils.js | 549 +++++++++++++ manifest.json | 59 ++ plugin/actions/CopilotAction.js | 175 +++++ plugin/actions/PetrolAction.js | 151 ++++ plugin/app.html | 18 + plugin/app.js | 64 ++ property-inspector/copilot/inspector.html | 37 + property-inspector/copilot/inspector.js | 31 + property-inspector/petrol/inspector.html | 45 ++ property-inspector/petrol/inspector.js | 31 + 32 files changed, 2678 insertions(+) create mode 100644 .gitignore create mode 100644 assets/icons/copilot.png create mode 100644 assets/icons/petrol.svg create mode 100644 de_DE.json create mode 100644 en.json create mode 100644 libs/assets/u_active.svg create mode 100644 libs/assets/u_active_none.svg create mode 100644 libs/assets/u_check_checkbox.svg create mode 100644 libs/assets/u_check_none.svg create mode 100644 libs/assets/u_check_radio.svg create mode 100644 libs/assets/u_down.svg create mode 100644 libs/assets/u_file.svg create mode 100644 libs/assets/u_folder.svg create mode 100644 libs/assets/u_tip_error.svg create mode 100644 libs/assets/u_tip_info.svg create mode 100644 libs/assets/u_tip_success.svg create mode 100644 libs/assets/u_tip_warn.svg create mode 100644 libs/css/uspi.css create mode 100644 libs/js/constants.js create mode 100644 libs/js/eventEmitter.js create mode 100644 libs/js/timers.js create mode 100644 libs/js/ulanzideckApi.js create mode 100644 libs/js/utils.js create mode 100644 manifest.json create mode 100644 plugin/actions/CopilotAction.js create mode 100644 plugin/actions/PetrolAction.js create mode 100644 plugin/app.html create mode 100644 plugin/app.js create mode 100644 property-inspector/copilot/inspector.html create mode 100644 property-inspector/copilot/inspector.js create mode 100644 property-inspector/petrol/inspector.html create mode 100644 property-inspector/petrol/inspector.js 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 0000000000000000000000000000000000000000..38b810fb9a09e0b56893f8c72b89f47b4a383e52 GIT binary patch literal 73102 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�uFuy7|Pr#zk?@)0$$I7mZiB}RG=>ymZ-nY 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

b}{n2ZP|a`S4A2&!edvN=CPg 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 literal 0 HcmV?d00001 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); +}