@andreydk1981/node-red-dashboard-2-ui-projector 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Node-RED Dashboard 2.0 - Projector Widget
2
+
3
+ A simple Node-RED Dashboard 2.0 widget that displays a projector icon.
4
+
5
+ ## Installation
6
+
7
+ 1. Install dependencies:
8
+ ```bash
9
+ npm install
10
+ ```
11
+
12
+ 2. Build the widget:
13
+ ```bash
14
+ npm run build
15
+ ```
16
+
17
+ 3. Link to your Node-RED installation (for development):
18
+ ```bash
19
+ npm link
20
+ cd ~/.node-red
21
+ npm link @andreydk1981/node-red-dashboard-2-ui-projector
22
+ ```
23
+
24
+ 4. Restart Node-RED
25
+
26
+ ## Development
27
+
28
+ To develop the widget with hot-reload:
29
+ ```bash
30
+ npm run dev
31
+ ```
32
+
33
+ Then open http://localhost:5173 in your browser.
34
+
35
+ ## Usage
36
+
37
+ After installation, you'll find the "ui-projector" node in the Node-RED palette under the Dashboard category. Drag it onto your flow, configure it to be part of a Dashboard 2.0 group, and deploy.
38
+
39
+ The widget will display a projector icon on your dashboard.
40
+
41
+ ## Structure
42
+
43
+ - `/nodes` - Node-RED node definition files
44
+ - `/ui` - Vue.js component files
45
+ - `/resources` - Built widget files (generated)
46
+ - `vite.config.mjs` - Vite build configuration
47
+ - `package.json` - Package definition
48
+
49
+ ## License
50
+
51
+ Apache-2.0
@@ -0,0 +1,76 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('ui-projector', {
3
+ category: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.label.category'),
4
+ color: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.colors.light'),
5
+ defaults: {
6
+ name: { value: "" },
7
+ group: { type: 'ui-group', required: true },
8
+ order: { value: 0 },
9
+ width: {
10
+ value: 0,
11
+ validate: function (v) {
12
+ const width = v || 0
13
+ const currentGroup = $('#node-input-group').val() || this.group
14
+ const groupNode = RED.nodes.node(currentGroup)
15
+ const valid = !groupNode || +width <= +groupNode.width
16
+ $('#node-input-size').toggleClass('input-error', !valid)
17
+ return valid
18
+ }
19
+ },
20
+ height: { value: 0 }
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ icon: "font-awesome/fa-video-camera",
25
+ label: function() {
26
+ return this.name || "ui-projector";
27
+ },
28
+ oneditprepare: function () {
29
+ $('#node-input-size').elementSizer({
30
+ width: '#node-input-width',
31
+ height: '#node-input-height',
32
+ group: '#node-input-group'
33
+ });
34
+ }
35
+ });
36
+ </script>
37
+
38
+ <script type="text/html" data-template-name="ui-projector">
39
+ <div class="form-row">
40
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
41
+ <input type="text" id="node-input-name" placeholder="Name">
42
+ </div>
43
+ <div class="form-row">
44
+ <label><i class="fa fa-object-group"></i> <span data-i18n="ui-projector.label.size"></label>
45
+ <input type="hidden" id="node-input-width">
46
+ <input type="hidden" id="node-input-height">
47
+ <button class="editor-button" id="node-input-size"></button>
48
+ </div>
49
+ </script>
50
+
51
+ <script type="text/html" data-help-name="ui-projector">
52
+ <p>Виджет Dashboard 2.0 с иконкой проектора.</p>
53
+
54
+ <h3>Свойства</h3>
55
+ <dl class="message-properties">
56
+ <dt>Name <span class="property-type">string</span></dt>
57
+ <dd>Имя виджета. Отображается над иконкой. Можно изменить через входящее сообщение msg.name</dd>
58
+ </dl>
59
+
60
+ <h3>Входящие сообщения</h3>
61
+ <dl class="message-properties">
62
+ <dt>msg.name <span class="property-type">string</span></dt>
63
+ <dd>Новое имя виджета. Будет сохранено и использовано для отображения и topic</dd>
64
+ <dt>msg.color <span class="property-type">string</span></dt>
65
+ <dd>Цвет иконки проектора. Можно использовать любой CSS цвет: <code>"red"</code>, <code>"#00ff00"</code>, <code>"rgb(255, 0, 0)"</code>.
66
+ Если не указан, используется цвет темы Dashboard. Чтобы сбросить цвет, отправьте <code>msg.color = null</code></dd>
67
+
68
+ <dt>msg.ires <span class="property-type">string</span></dt>
69
+ <dd>Разрешение, отображаемое под иконкой проектора. Например: <code>"1920x1080"</code>, <code>"4K"</code>, <code>"HD"</code></dd>
70
+ </dl>
71
+
72
+ <h3>Подробности</h3>
73
+ <p>Виджет сохраняет последний установленный цвет и разрешение при получении других сообщений.</p>
74
+ <p>При обновлении страницы цвет и разрешение восстанавливаются из последнего сохраненного сообщения.</p>
75
+ <p>Размер иконки и текста автоматически адаптируются под размер виджета (1x1, 2x2 и т.д.)</p>
76
+ </script>
@@ -0,0 +1,56 @@
1
+ module.exports = function (RED) {
2
+ function UIProjectorNode (config) {
3
+ RED.nodes.createNode(this, config)
4
+
5
+ const node = this
6
+
7
+ // which group are we rendering this widget
8
+ const group = RED.nodes.getNode(config.group)
9
+
10
+ const base = group.getBase()
11
+
12
+ // server-side event handlers
13
+ const evts = {
14
+ onAction: function (msg, send, done) {
15
+ // get current name
16
+ const currentName = node.context().get('name') || config.name || 'projector'
17
+ // set topic to current name
18
+ msg.topic = currentName
19
+ send(msg)
20
+ },
21
+ beforeSend: function (msg) {
22
+ // ensure topic exists: prefer existing, then saved name, then configured name, then default
23
+ const currentName = node.context().get('name') || config.name || 'projector'
24
+ if (!msg.topic) {
25
+ msg.topic = currentName
26
+ }
27
+ return msg
28
+ },
29
+ onInput: function (msg, send, done) {
30
+ // handle name change
31
+ if (msg.name) {
32
+ node.context().set('name', msg.name)
33
+ }
34
+ // determine current name
35
+ const currentName = node.context().get('name') || config.name || 'projector'
36
+ // for action messages from widget (ON/OFF) set topic to current name
37
+ if (msg.payload === 'ON' || msg.payload === 'OFF') {
38
+ msg.topic = currentName
39
+ }
40
+ // store the latest value
41
+ base.stores.data.save(base, node, msg)
42
+ // send to connected nodes
43
+ send(msg)
44
+ }
45
+ }
46
+
47
+ // inform the dashboard UI that we are adding this node
48
+ if (group) {
49
+ group.register(node, config, evts)
50
+ } else {
51
+ node.error('No group configured')
52
+ }
53
+ }
54
+
55
+ RED.nodes.registerType('ui-projector', UIProjectorNode)
56
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@andreydk1981/node-red-dashboard-2-ui-projector",
3
+ "version": "0.1.2",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "A Node-RED Dashboard 2.0 widget with a projector icon",
8
+ "keywords": [
9
+ "node-red",
10
+ "dashboard",
11
+ "projector"
12
+ ],
13
+ "author": "andre",
14
+ "license": "Apache-2.0",
15
+ "node-red": {
16
+ "version": ">=3.0.0",
17
+ "nodes": {
18
+ "ui-projector": "nodes/ui-projector.js"
19
+ }
20
+ },
21
+ "node-red-dashboard-2": {
22
+ "widgets": {
23
+ "ui-projector": {
24
+ "output": "ui-projector.umd.js",
25
+ "component": "UIProjector"
26
+ }
27
+ }
28
+ },
29
+ "scripts": {
30
+ "dev": "vite",
31
+ "build": "vite build"
32
+ },
33
+ "files": [
34
+ "nodes/*",
35
+ "resources/*",
36
+ "ui/dist/*"
37
+ ],
38
+ "peerDependencies": {
39
+ "@flowfuse/node-red-dashboard": ">=1.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@vitejs/plugin-vue": "^4.4.0",
43
+ "vite": "^4.5.0",
44
+ "vite-plugin-css-injected-by-js": "^3.3.0",
45
+ "vite-plugin-static-copy": "^0.17.0",
46
+ "vue": "^3.3.8"
47
+ }
48
+ }
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ui-projector-wrapper[data-v-a2ab9879]{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1px;gap:0;box-sizing:border-box;overflow:hidden}.projector-name[data-v-a2ab9879]{font-size:clamp(6px,1.5vw,12px);font-weight:500;color:var(--v-theme-on-surface);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;line-height:1.1;flex-shrink:0}.projector-icon[data-v-a2ab9879]{width:100%;height:auto;max-width:100%;color:var(--v-theme-on-surface);flex:1;min-height:0;cursor:pointer;transition:opacity .2s}.projector-icon[data-v-a2ab9879]:hover{opacity:.7}.projector-resolution[data-v-a2ab9879]{font-size:clamp(5px,1.2vw,10px);color:var(--v-theme-on-surface);opacity:.7;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;line-height:1.1;flex-shrink:0}.dialog-buttons[data-v-a2ab9879]{display:flex;flex-direction:column;gap:12px;padding:8px 0}")),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
2
+ (function(r,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("vuex"),require("vue")):typeof define=="function"&&define.amd?define(["exports","vuex","vue"],i):(r=typeof globalThis<"u"?globalThis:r||self,i(r["ui-projector"]={},r.vuex,r.Vue))})(this,function(r,i,e){"use strict";const j="",m=(t,o)=>{const l=t.__vccOpts||t;for(const[d,n]of o)l[d]=n;return l},f={name:"UIProjector",inject:["$socket","$dataTracker"],props:{id:{type:String,required:!0},props:{type:Object,default:()=>({})},state:{type:Object,default:()=>({enabled:!1,visible:!1})}},data(){return{currentColor:null,resolution:null,dialog:!1,name:null}},computed:{...i.mapState("data",["messages"])},created(){this.name=this.props.name||"projector",this.$dataTracker(this.id,this.onInput,this.onLoad)},methods:{send(t){this.$socket.emit("widget-action",this.id,t)},sendCommand(t){this.send({payload:t,topic:this.name}),this.dialog=!1},openDialog(){this.dialog=!0},onInput(t){t.name&&(this.name=t.name),t.color&&(this.currentColor=t.color,t.ires&&(this.resolution=t.ires)),this.$store.commit("data/bind",{widgetId:this.id,msg:t})},onLoad(t,o){t&&t.name&&(this.name=t.name),t&&t.ires&&(this.resolution=t.ires),t&&t.color&&(this.currentColor=t.color)}}},_={class:"ui-projector-wrapper"},h={key:0,class:"projector-name"},x={key:1,class:"projector-resolution"},C={class:"dialog-buttons"};function u(t,o,l,d,n,s){const k=e.resolveComponent("v-card-title"),c=e.resolveComponent("v-icon"),p=e.resolveComponent("v-btn"),w=e.resolveComponent("v-card-text"),N=e.resolveComponent("v-card"),y=e.resolveComponent("v-dialog");return e.openBlock(),e.createElementBlock("div",_,[n.name?(e.openBlock(),e.createElementBlock("div",h,e.toDisplayString(n.name),1)):e.createCommentVNode("",!0),(e.openBlock(),e.createElementBlock("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",class:"projector-icon",style:e.normalizeStyle({color:n.currentColor}),onClick:o[0]||(o[0]=(...a)=>s.openDialog&&s.openDialog(...a))},[...o[4]||(o[4]=[e.createElementVNode("path",{d:"M16 6c-1.13 0-2.23.35-3.16 1H4c-1.11 0-2 .89-2 2v6c0 1.11.89 2 2 2h1v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-1h6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-1h1c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-.85c-.92-.65-2.02-1-3.15-1m0 1.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5a3.5 3.5 0 0 1-3.5-3.5A3.5 3.5 0 0 1 16 7.5M4 9h4v1H4zm12 0a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2M4 11h4v1H4zm0 2h4v1H4z",fill:"currentColor"},null,-1)])],4)),n.resolution?(e.openBlock(),e.createElementBlock("div",x,e.toDisplayString(n.resolution),1)):e.createCommentVNode("",!0),e.createVNode(y,{modelValue:n.dialog,"onUpdate:modelValue":o[3]||(o[3]=a=>n.dialog=a),"max-width":"300"},{default:e.withCtx(()=>[e.createVNode(N,null,{default:e.withCtx(()=>[e.createVNode(k,{class:"text-h6 text-center"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(l.props.label||l.props.name||"Проектор"),1)]),_:1}),e.createVNode(w,null,{default:e.withCtx(()=>[e.createElementVNode("div",C,[e.createVNode(p,{color:"success",size:"large",block:"",onClick:o[1]||(o[1]=a=>s.sendCommand("ON"))},{default:e.withCtx(()=>[e.createVNode(c,{left:""},{default:e.withCtx(()=>[...o[5]||(o[5]=[e.createTextVNode("mdi-power",-1)])]),_:1}),o[6]||(o[6]=e.createTextVNode(" ON ",-1))]),_:1}),e.createVNode(p,{color:"error",size:"large",block:"",onClick:o[2]||(o[2]=a=>s.sendCommand("OFF"))},{default:e.withCtx(()=>[e.createVNode(c,{left:""},{default:e.withCtx(()=>[...o[7]||(o[7]=[e.createTextVNode("mdi-power-off",-1)])]),_:1}),o[8]||(o[8]=e.createTextVNode(" OFF ",-1))]),_:1})])]),_:1})]),_:1})]),_:1},8,["modelValue"])])}const V=m(f,[["render",u],["__scopeId","data-v-a2ab9879"]]);r.UIProjector=V,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ui-projector-wrapper[data-v-a2ab9879]{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1px;gap:0;box-sizing:border-box;overflow:hidden}.projector-name[data-v-a2ab9879]{font-size:clamp(6px,1.5vw,12px);font-weight:500;color:var(--v-theme-on-surface);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;line-height:1.1;flex-shrink:0}.projector-icon[data-v-a2ab9879]{width:100%;height:auto;max-width:100%;color:var(--v-theme-on-surface);flex:1;min-height:0;cursor:pointer;transition:opacity .2s}.projector-icon[data-v-a2ab9879]:hover{opacity:.7}.projector-resolution[data-v-a2ab9879]{font-size:clamp(5px,1.2vw,10px);color:var(--v-theme-on-surface);opacity:.7;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;line-height:1.1;flex-shrink:0}.dialog-buttons[data-v-a2ab9879]{display:flex;flex-direction:column;gap:12px;padding:8px 0}")),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
2
+ (function(r,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("vuex"),require("vue")):typeof define=="function"&&define.amd?define(["exports","vuex","vue"],i):(r=typeof globalThis<"u"?globalThis:r||self,i(r["ui-projector"]={},r.vuex,r.Vue))})(this,function(r,i,e){"use strict";const j="",m=(t,o)=>{const l=t.__vccOpts||t;for(const[d,n]of o)l[d]=n;return l},f={name:"UIProjector",inject:["$socket","$dataTracker"],props:{id:{type:String,required:!0},props:{type:Object,default:()=>({})},state:{type:Object,default:()=>({enabled:!1,visible:!1})}},data(){return{currentColor:null,resolution:null,dialog:!1,name:null}},computed:{...i.mapState("data",["messages"])},created(){this.name=this.props.name||"projector",this.$dataTracker(this.id,this.onInput,this.onLoad)},methods:{send(t){this.$socket.emit("widget-action",this.id,t)},sendCommand(t){this.send({payload:t,topic:this.name}),this.dialog=!1},openDialog(){this.dialog=!0},onInput(t){t.name&&(this.name=t.name),t.color&&(this.currentColor=t.color,t.ires&&(this.resolution=t.ires)),this.$store.commit("data/bind",{widgetId:this.id,msg:t})},onLoad(t,o){t&&t.name&&(this.name=t.name),t&&t.ires&&(this.resolution=t.ires),t&&t.color&&(this.currentColor=t.color)}}},_={class:"ui-projector-wrapper"},h={key:0,class:"projector-name"},x={key:1,class:"projector-resolution"},C={class:"dialog-buttons"};function u(t,o,l,d,n,s){const k=e.resolveComponent("v-card-title"),c=e.resolveComponent("v-icon"),p=e.resolveComponent("v-btn"),w=e.resolveComponent("v-card-text"),N=e.resolveComponent("v-card"),y=e.resolveComponent("v-dialog");return e.openBlock(),e.createElementBlock("div",_,[n.name?(e.openBlock(),e.createElementBlock("div",h,e.toDisplayString(n.name),1)):e.createCommentVNode("",!0),(e.openBlock(),e.createElementBlock("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",class:"projector-icon",style:e.normalizeStyle({color:n.currentColor}),onClick:o[0]||(o[0]=(...a)=>s.openDialog&&s.openDialog(...a))},[...o[4]||(o[4]=[e.createElementVNode("path",{d:"M16 6c-1.13 0-2.23.35-3.16 1H4c-1.11 0-2 .89-2 2v6c0 1.11.89 2 2 2h1v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-1h6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-1h1c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-.85c-.92-.65-2.02-1-3.15-1m0 1.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5a3.5 3.5 0 0 1-3.5-3.5A3.5 3.5 0 0 1 16 7.5M4 9h4v1H4zm12 0a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2M4 11h4v1H4zm0 2h4v1H4z",fill:"currentColor"},null,-1)])],4)),n.resolution?(e.openBlock(),e.createElementBlock("div",x,e.toDisplayString(n.resolution),1)):e.createCommentVNode("",!0),e.createVNode(y,{modelValue:n.dialog,"onUpdate:modelValue":o[3]||(o[3]=a=>n.dialog=a),"max-width":"300"},{default:e.withCtx(()=>[e.createVNode(N,null,{default:e.withCtx(()=>[e.createVNode(k,{class:"text-h6 text-center"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(l.props.label||l.props.name||"Проектор"),1)]),_:1}),e.createVNode(w,null,{default:e.withCtx(()=>[e.createElementVNode("div",C,[e.createVNode(p,{color:"success",size:"large",block:"",onClick:o[1]||(o[1]=a=>s.sendCommand("ON"))},{default:e.withCtx(()=>[e.createVNode(c,{left:""},{default:e.withCtx(()=>[...o[5]||(o[5]=[e.createTextVNode("mdi-power",-1)])]),_:1}),o[6]||(o[6]=e.createTextVNode(" ON ",-1))]),_:1}),e.createVNode(p,{color:"error",size:"large",block:"",onClick:o[2]||(o[2]=a=>s.sendCommand("OFF"))},{default:e.withCtx(()=>[e.createVNode(c,{left:""},{default:e.withCtx(()=>[...o[7]||(o[7]=[e.createTextVNode("mdi-power-off",-1)])]),_:1}),o[8]||(o[8]=e.createTextVNode(" OFF ",-1))]),_:1})])]),_:1})]),_:1})]),_:1},8,["modelValue"])])}const V=m(f,[["render",u],["__scopeId","data-v-a2ab9879"]]);r.UIProjector=V,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});