Adding `output` function to buttons designed in `template` node

No. Not really. More like a class name.

It actually uses the topic you provide in data-topic so you don't need to set an id

Ok. Interesting. So: the more buttons, the more work is needed in the template node to parse the messages - yes?

No. That should never change. Just add another button with correct attributes and it will automatically start working. Code never changes.

That's the whole point of the option I provided.

Again, it's only one of several ways this could be achieved.

My personal (strong) preference is write the code once and don't touch it again (unless there is a bug - in which case you only fix it once)

Edit. Sorry I see you point now.

In the node-red side, you still have to route the message to where it needs to go yes.

Ok. Again, I think brain is not working as good as I would like.

The extra stuff is more for...... I will really have to sit down and look at the code.
I only just got a handle on BASIC back in the 80's. Touched on Z80 machine code and never really did any of these languages at all. I really am a fish learning to ride a bike when it comes to this.

Though I do have a bit of an appreciation of what you are meaning how I can just go on adding more buttons.

You have big project.
Couple of rules:
Think more, code less, go step by step.
Collect info by doing little tests / examples to make or decide the best strategy.
Start doing the actual thing when you have no missing bits of what it will be and how to do it.

Doing so, you don't find yourself deleting whole thing you have worked on couple of last days.

1 Like

I'd recommend firing up a separate instance for Dev & playing/testing.

(Going over the code as best I can at this time)

I am really going to mess up the words/terminology, but I hope I can get my take on how it works - and from what you said.

Each button (template node) sends a payload and topic.
The topic is used to identify who sent the message.

On entry to the node:
The topic is used to define who sent the message.
The payload is stored in an array indexed by the topic.

Because of this methodology, I can keep adding buttons ad infinitum.

Each time it receives a new message, it can toggle the value it has (from internal settings if an attribute is set on the incoming message).
Other things - like button colour - are sent to the button, maybe as well as this template node. Just with a bit of filtering.

Ok. Is that sounding vaguely right?

Yes, but I am obliged to this because it is global and so don't know how to make an isolated instance of it.

Short of on a whole other machine.

You don't need to really. Just the bits you're playing with.

I don't have all you code or nodes but I managed to develop that button thing.

Mostly right yes.

Absolutely.

I don't even think I know the complete picture because it is dynamic in some regards to what I want, what can be done and how easy it is.

I definitely will be keeping all the stuff I have done.
It is good and is useful.

I guess I am also partly guilty of not having an exact target of the end game (goal) and so it is hard to achieve this.
But as I go along, I am discovering new things which will enhance what I want and so change the whole workings.

I am both a focused and irrational person so in this environment I am excited with all the new toys with which I can play, and wanting to get something built.

I also guess I need to accept that Rome wasn't built in a day and accept that this sort of thing is going to take time to be done. And a lot more than I can understand because I still don't know the mechanics of what I am doing.

All lots of fun.

What you have shown me does go a long way to what I want.

As said: tomorrow I will have to sit down and see if I can get it playing nicely (working) in a way I understand and can control to get the things I want.

I think it may save a lot of work with a lot of buttons. I think as it is, I have about 100 and that will increase to ...... maybe 200.

If I have to give each of them a unique name using the original method, your way is definitely going to save a lot of name creation and time.

I think I had better call it a night.

@Steve-Mcl, @hotNipi:
Thanks very much again.

Until the next time.......

All the best.

Andrew, some home work for you tomorrow...

I have updated the code to also handle repeat buttons - try to implement them.

PS, backup your flows first :smiley:

Dont worry, the idea is exactly the same (add some attributes to a button then magic happens automatically)

Visual Demo

note...
holding VOL buttons sends repeat messages
the lock button is font awesome (the rest are material design)

The flow

[{"id":"f4fac1e9.e93e4","type":"ui_template","z":"e1dfad68.fee2a","group":"be17d3d4.a92a6","name":"CSS only ","order":7,"width":0,"height":0,"format":"<style id=\"remote-buttons\">\n    :root {\n      --dashboard-unit-width: 48px;\n      --dashboard-unit-height: 48px;\n    }\n    .nr-dashboard-template {\n        padding: 0px;\n    }\n    .remote-button:not([disabled]):hover{\n         background-color: #232323 !important;\n    }\n\n    /*   This is the normal button definition  */\n    .remote-button{\n        background-color: black !important;\n        color: #cccccc !important;\n        height: var(--dashboard-unit-height);\n        width: 100%;\n        border-radius: 10px;\n        font-size:1.0em;\n        font-weight:normal;\n        margin: 0;\n        min-height: 36px;\n        min-width: unset;\n        line-height: unset;\n    }\n    /*  This is a sub-set which is invoked by */\n    /*  <md-button class=\"md-button remote-button bigger\"> */\n    /*  note the (space) \"bigger\" at the end.  */\n    .remote-button.bigger{\n        font-weight:bold;\n        font-size:1.5em;\n    }\n    /*  This is for buttons with a lot of text.  `font-size:0.7em` */\n    /*  makes the font 70% normal size  */\n    .remote-button.small{\n        font-size:0.7em;\n    }\n    /*  This is for buttons with just icons, to upsize the size */\n    /*  of the icon with the line: */\n    /*  <i class=\"fa fa-fw fa-plus remote-icon\"> in the other node  */\n    .remote-icon{\n        font-size:2.0em;\n    }\n    /*  This is the same as the other one, but it makes the icon smaller  */\n    .remote-iconS{\n        font-size:0.5em;\n    }\n\n    .remote-button.black{\n        background-color: black !important;\n        color: #cccccc !important;\n    }\n\n    .remote-button.red{\n        background-color: red !important;\n        color: #cccccc !important;\n    }\n    .remote-button.red:not([disabled]):hover{\n         background-color: orange !important;\n    }\n</style>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"global","x":740,"y":80,"wires":[[]]},{"id":"bc691572.02ce88","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"NetFlix","order":20,"width":"1","height":"1","format":"<div>\n    <md-button \n        class=\"md-button remote-button\"\n        data-topic=\"netflix\"\n        data-payload=\"netflix\"\n        aria-label=\"Netflix\"\n    >\n        <img\n            class=\"remote-icon\"\n            style=\"width: 36px; padding-top: 2px\"\n            src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAE2klEQVR4Xu3bW4hVVRzH8c8ZE/GCtywry5qcarrhpbKLmoVl9FZ0s2zS0DIJrIceSl+EwIgIDCQCp4t2L4guBJUgTmnlXYtUirIpU9EZxyayqWbmxPKcARFn1hKDaW/7v64fM/v3Pf/1X2v/91oFx3kUjnP//gdwLBnQxMB2Hk75G81sreTNFO0CbioyKkX7Nwvnsz9FeyTNMWXAbip78kPKP29n++uMmkNzTL+AlzAtpgvjBSof48cU7b8O4Ckqp7G8B2elPMB2pl7KazFtZgA8wVkTWHI+V8dMhfE/eec0bolpMwWgNxtq6IeeMWM4sIzRU/i2K22mABTZPoUvB3BFAgD7mHcOC3IFoJKNkxmdAqCNTc9y2XxaO9NnLgPCUjKTnys4IwXCNm4Yxye5ARCMXEvduUxMAdBC7TDuyxWAPjTUMBAnJEBo/Jjqu2g4kjZzU6DDxJ2s7s/lCQA0cX8Vi3MFoIoNkxiTAqCV5UOZlCsABYoz2VnBsAQI7VsYM4HNh2szOwWCkUnUVSUWwwM8eQaP5gpAP/ZMZQgqYllQpP4Dqu+l5VBtpjMgGJnK2n5cFgMQxhu4+TzezRWAatZOTATQyttDuT1XACpon8meAqckZEFLPSPGsLNDm/kpEIxcT93ZicXwLx45ladzBWAAu6aUMiDadWpn8xBGh2U0QMhFBgQjd7OuL5cmTIOwMxxfxapcATifNVczNgVAO8+dxOxcAaigbSaNBU5OgNC0lTPH81tupkAwfSN1w9OL4bRTWZorAAPYMYXTEzIgVMDlQ5iUKwDB+D1s7J3WMise4LyXmZeJ7wKhLR6aorFf9yK+HJfYNG3n8cUMzxWAHrTO4NcCJ8Zg4ada6tqoSdB275eh1AwoF8MVw7kmxVQdn2xjcoq2Wz+NHQ2Awbx9G7em7Ax/5Ys3uDJXALBkVqlTdF2Csb+WcuCPUpO1y8hMBgQAD7CsyCsxU2H8az77nAkxbaYA1DC7D79gUMxYK989zzkxXaYAzGV6I89gTsxYGH+fbbuo7kqbOQD7uKh4MMPjsZvP3otMg8wBCLYbS6+9V8URaK6lVxu9OtNmEkAD0wu8mADAWj7f0AWsTALYSZ9epR7ggBiEFjYvYWSuMiCY2ceiIg/GAITxt9jR1MkbZSYzIJhqYmQ7m1IA/Ejdx530FDILoFwMV0tomRXZvZihxSM0WLMOYAZqU7JgOeu/45LDtZkGsId+PdildMqsy2hm9etHOHuQaQDlafAcZsUAoHUpzX8w+FBt5gHsZUwF6xMA+IZPVx52KDPzAMpL4vpiwsmSNr6vZUSuMqAMYFaRMBWi8SFbdnBBhzAXGdBIf6WdYd8Ygb2sfIfxuQJQLoZhOQzLYpdR5PcXqGildxDmIgOCkQbGFggbo2isY9V6xuUKQNjl7WOjLl58Osi08PUSLs4VgHIWPFhgUTQFSi9I9U2cmZspEEyX7yCFYnhwfncV9dR9xMRcASgXw9AomR4DUGRPLUOKjOi2O0NH82EktMVDUzRmrLHU/Tl4UiQWK1j7LbfnCkC5GIam6YUxAL+z5lXuyBWAcjGcUyi1z2PR9hVV13bXtbn5DOyZeHGywKa5h50I7czdfga18VDMfRivYOGg7ro4mfKA/3VN9Azff93AsT7fcQ/gH8HaMl8q26yhAAAAAElFTkSuQmCC\">\n        />\n    </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":730,"y":380,"wires":[[]]},{"id":"57de4e.f4cfb1b4","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"Home","order":21,"width":"1","height":"1","format":"<div>\n   <md-button class=\"md-button remote-button\" \n        data-topic=\"home\"\n        data-payload=\"home\" \n        aria-label=\"home\"\n    >\n    <i class=\"fa fa-home remote-icon\"> </i>\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":380,"wires":[[]]},{"id":"a6df6e36.1d16b","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"Vid","order":22,"width":"1","height":"1","format":"<div>\n   <md-button class=\"md-button remote-button small\" \n        data-topic=\"video\"\n        data-payload=\"video\" \n    >Video\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":970,"y":380,"wires":[[]]},{"id":"2a646eff.3f7cf2","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"1","order":2,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"1\"\n    data-payload=\"1\">1\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":730,"y":140,"wires":[[]]},{"id":"ef0701c1.ddb63","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"2","order":3,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"2\"\n    data-payload=\"2\">2\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":140,"wires":[[]]},{"id":"a8296eda.ec9d6","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"3","order":4,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"3\"\n    data-payload=\"3\">3\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":970,"y":140,"wires":[[]]},{"id":"cf65f510.c80d68","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"4","order":5,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"4\"\n    data-payload=\"4\">4\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":730,"y":180,"wires":[[]]},{"id":"84949bc8.2bd298","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"5","order":6,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"5\"\n    data-payload=\"5\">5\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":180,"wires":[[]]},{"id":"b341d1f1.adf42","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"6","order":7,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"6\"\n    data-payload=\"6\">6\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":970,"y":180,"wires":[[]]},{"id":"76507e83.940e6","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"7","order":8,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"7\"\n    data-payload=\"7\">7\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":730,"y":220,"wires":[[]]},{"id":"fbafce7d.80576","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"8","order":9,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"8\"\n    data-payload=\"8\">8\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":220,"wires":[[]]},{"id":"684b5568.bf5c6c","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"9","order":10,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"9\"\n    data-payload=\"9\">9\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":970,"y":220,"wires":[[]]},{"id":"114192b6.67ea7d","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"0","order":12,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button bigger\"\n    data-topic=\"0\"\n    data-payload=\"0\">0\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":260,"wires":[[]]},{"id":"84c69bfc.13faa8","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"info","order":11,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\"\n    data-topic=\"info\"\n    data-payload=\"info\"\n    aria-label=\"info\"\n   >\n      <i class=\"fa fa-info-circle remote-icon\"></i>\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":730,"y":260,"wires":[[]]},{"id":"25910d61.070c82","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"prev","order":13,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\"\n    data-topic=\"prev\"\n    data-payload=\"prev\"\n    aria-label=\"previous\"\n   >\n      <i class=\"fa fa-rotate-left remote-icon\"></i>\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":970,"y":260,"wires":[[]]},{"id":"787787b2.c80b88","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"Ch +","order":16,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\" \n        data-topic=\"channel/up\"\n        data-payload=\"up\" \n        aria-label=\"channel up\"\n    >\n    <i class=\"fa fa-chevron-up remote-icon\"></i>\n   </md-button>\n</div>\n\n\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":970,"y":300,"wires":[[]]},{"id":"7eb81447.3ea8dc","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"mute","order":15,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\" \n              data-payload=\"1\" \n              data-buttontype=\"toggle\"\n              data-topic=\"mute\"\n              data-icon0=\"volume_off\"\n              data-icon1=\"volume_mute\"\n              aria-label=\"volume mute\"\n              >\n      <i class=\"material-icons md-48\">volume_mute</i>\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":300,"wires":[[]],"info":"  class=\"material-icons\"> volume_off"},{"id":"6eeb00d0.da35b","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"vol +  *","order":14,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\" \n        data-buttontype=\"repeater\"\n        data-interval=\"200\"\n        data-topic=\"volume/plus\"\n        data-payload=\"volume/plus\"\n        aria-label=\"volume plus\"\n    >\n    <span class=\"fa fa-plus remote-icon\"> </span>\n   </md-button>\n</div>\n\n\n","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":730,"y":300,"wires":[[]],"info":"<div id=\"regular_plus\">\n   <md-button class=\"md-button remote-button\">\n      <i class=\"fa fa-plus remote-icon\"></i>\n   </md-button>\n</div>"},{"id":"c58bb5ab.ebc658","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"vol -  *","order":17,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\"\n        data-buttontype=\"repeater\"\n        data-interval=\"200\"\n        data-topic=\"volume/minus\"\n        data-payload=\"volume/minus\"\n        aria-label=\"volume minus\"\n    >\n    <span style=\"color:{{msg.colour}}\" class=\"fa fa-minus remote-icon\"> </span>\n   </md-button>\n</div>\n\n","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":730,"y":340,"wires":[[]],"info":"<div id=\"regular_plus\">\n   <md-button class=\"md-button remote-button\">\n      <i class=\"fa fa-minus remote-icon\"></i>\n   </md-button>\n</div>\n"},{"id":"782351a7.0f3f6","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"ch list","order":18,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\"\n    data-topic=\"channel/list\"\n    data-payload=\"list\"\n    aria-label=\"channel list\"\n   >\n      <i class=\"fa fa-list-alt remote-icon\"></i>\n   </md-button>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":340,"wires":[[]]},{"id":"18ff979.4cf7768","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"Ch -","order":19,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\" \n        data-topic=\"channel/down\"\n        data-payload=\"down\" \n        aria-label=\"channel down\"\n    >\n    <i class=\"fa fa-chevron-down remote-icon\"></i>\n   </md-button>\n</div>\n\n\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":970,"y":340,"wires":[[]]},{"id":"6b43ca3e.3b35a4","type":"inject","z":"e1dfad68.fee2a","name":"","topic":"mute","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":690,"y":440,"wires":[["c322da00.abb3a8"]]},{"id":"54ff0f5c.2703e","type":"inject","z":"e1dfad68.fee2a","name":"","topic":"mute","payload":"0","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":690,"y":480,"wires":[["c322da00.abb3a8"]]},{"id":"c322da00.abb3a8","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"script for all buttons with class remote-button","order":23,"width":"1","height":"1","format":"<div>\n<!--diliberately emtpy - only need the script below -->\n</div>\n\n<script>\n\n(function($scope) {\n//debugger\n\n    //cause a small delay while things load \n    //ideally this would be an init event or on all parts of document loaded\n    //(may not be necessary!)\n    setTimeout(function() {\n        //debugger\n        $scope.init();\n    },100);\n    \n    var BUTTON_CLASS = \".remote-button\";\n\n    /** \n     * Initialise all buttons with class BUTTON_CLASS\n    */\n    $scope.init = function () {\n        //debugger\n        console.log(\"$scope.init called. Adding event handlers to all buttons with class '\" + BUTTON_CLASS + \"'.\");\n        var clickButtons = $(BUTTON_CLASS + \":not([data-buttontype='repeater'])\") \n        clickButtons.click(function(e){\n            //debugger\n            var btn = $(this)\n            var type = btn.data(\"buttontype\");//get the button type from attribute data-buttontype=\"xxxxx\"\n            var topic = btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n            var payload = btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n            \n            if(type == \"toggle\"){\n                var newPayload = payload == 1 ? 0 : 1;\n                setButtonState(btn,newPayload)\n                $scope.send({\"topic\":topic,\"payload\":newPayload})\n            } else {\n                $scope.send({\"topic\":topic,\"payload\":payload})\n            }\n        }); \n\n        var repeatButtons = $(BUTTON_CLASS + \"[data-buttontype='repeater']\") \n        repeatButtons.on('mousedown',function(e) {\n            var btn = $(this);\n            if(this._intervalId) return; //already in operation\n            var topic = btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n            var payload = btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n            var interval = btn.data(\"interval\");//get the desired repeat duration\n            if(isNumeric(interval) == false || interval < 1) interval = 300;//prevent zero & non numeric timeout value\n            this._intervalId = setInterval(function() {\n                $scope.send({\"topic\":topic,\"payload\":payload})\n            },interval); \n        }).on('mouseup blur focusout',function(e) {\n            var btn = $(this);\n            if(this._intervalId){\n                clearInterval(this._intervalId);\n                this._intervalId = null;\n            }\n        });\n           \n    };\n    \n    //watch for node-red msgs\n    $scope.$watch('msg', function(msg) {\n        //debugger\n        if(!msg){ //if no msg \n            console.log(\"$scope.$watch('msg', ...) - msg is empty\");\n            return;\n        }\n        if(!msg.topic){ //if no topic set found\n            console.log(\"msg.topic is empty - cannot match this to any button\")\n            return; //stop processing!\n        }\n        var buttonSelector = \".remote-button[data-topic='\" + msg.topic + \"']\" \n        var $btn = $(buttonSelector);//get the button\n        \n        if(!$btn.length){ //if no button found\n            console.log(buttonSelector + \" not found - cannot set state\")\n            return; //stop processing!\n        }\n        \n        if($btn.length > 1){ //if MORE than one button found\n            console.log(buttonSelector + \" found more than 1 button - is this intended? Do you have the same data-topic set on multiple buttons?\")\n        }\n        \n        if($btn.data(\"buttontype\") === \"toggle\"){\n            if(msg.payload == \"1\"){\n                setButtonState($btn, 1);\n            } else if(msg.payload == \"0\"){\n                setButtonState($btn, 0);\n            } else {\n               console.log(\"Invalid toggle value in msg.payload, cannot set \" + buttonSelector + \". Ensure msg.payload is either 0 or 1\") \n            }\n        }\n\n    }); \n    \n    /** \n    * helper function to set the correct icon & update the \"data-payload\" memory \n    */\n    function setButtonState($btn, state){\n        \n        $btn.data(\"payload\", state);//set data-payload to new state value (used as a memory)\n        \n        //determine the opposite state\n        var oppositeState;\n        if(state == \"1\" || state === 1){\n            state = 1; //normalise to a number\n            oppositeState = 0;\n        } else {\n            state = 0; //normalise to a number\n            oppositeState = 1;\n        }\n        \n        var $icon = $btn.find(\"i\"); //get the <i> element\n        if(!$icon.length){\n            $icon = $btn.find(\"span\"); //get the <span> element instead!\n        }\n        if(!$icon.length){\n            console.log(\"<i> or <span> not found inside button - cant toggle the icon!\")\n            return;//exit this function - nothing to toggle!\n        }\n        \n        //get the old icon and new icon names\n        var oldIcon = $btn.data(\"icon\" + oppositeState); //get icon1 or icon2 depending on oppositeState\n        var newIcon = $btn.data(\"icon\" + state); //get icon1 or icon2 depending on newPayload\n       \n        //if we have newIcon and an actual DOM element ($icon) - update it...\n        if(newIcon && $icon.length){\n            if(newIcon.includes(\"fa-\")){ \n                $icon.removeClass(oldIcon).addClass(newIcon);  // fontawesome\n            } else { \n                $icon.text(newIcon); // MDI\n            }\n        }\n    }\n        \n    /** \n    * helper function to determine a value is REALLY a number \n    */\n    function isNumeric(n){\n        if(n === \"\") return false;\n        if(n === true || n === false) return false;\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n   \n\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":540,"wires":[["a90df8f.7eac108"]]},{"id":"a90df8f.7eac108","type":"debug","z":"e1dfad68.fee2a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":930,"y":480,"wires":[]},{"id":"b5beb822.d8e4d8","type":"ui_template","z":"e1dfad68.fee2a","group":"a381801e.309e1","name":"lock","order":23,"width":"1","height":"1","format":"<div>\n   <md-button class=\"md-button remote-button bigger\" \n              data-payload=\"0\" \n              data-buttontype=\"toggle\"\n              data-topic=\"lock\"\n              data-icon0=\"fa-unlock\"\n              data-icon1=\"fa-lock\"\n              aria-label=\"lock\"\n              >\n      <span class=\"fa fa-unlock\" aria-hidden=\"true\"> </span>\n   </md-button>\n</div>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":420,"wires":[[]]},{"id":"be17d3d4.a92a6","type":"ui_group","z":"","name":"HOME","tab":"6af80cca.442cb4","order":1,"disp":true,"width":3,"collapse":false},{"id":"a381801e.309e1","type":"ui_group","z":"","name":"Full_Remote2","tab":"9e72c753.ebf048","order":3,"disp":false,"width":"3","collapse":false},{"id":"6af80cca.442cb4","type":"ui_tab","z":"","name":"TEST","icon":"dashboard","order":3,"disabled":false,"hidden":false},{"id":"9e72c753.ebf048","type":"ui_tab","z":"","name":"HDMI_TV_control","icon":"dashboard","order":7,"disabled":false,"hidden":false}]

How it works...

NOTE: All of the below code snippets are in the FLOW above (no real need to copy paste this) - these are for you to use as a reference.

A normal / simple button...

<div>
   <md-button class="md-button remote-button bigger"
    data-topic="7"
    data-payload="7">7
   </md-button>
</div>

Toggle buttons

Material Icon toggle...
<div>
 
   <md-button id="mute-button" class="md-button remote-button" 
              data-payload="1" 
              data-buttontype="toggle"
              data-topic="mute"
              data-icon0="volume_off"
              data-icon1="volume_mute"
              aria-label="volume mute"
              >
      <i class="material-icons md-48">volume_mute</i>
   </md-button>
</div>
font awesome Icon toggle...
<div>
   <md-button class="md-button remote-button bigger" 
              data-payload="0" 
              data-buttontype="toggle"
              data-topic="lock"
              data-icon0="fa-unlock"
              data-icon1="fa-lock"
              aria-label="lock"
              >
      <span class="fa fa-unlock" aria-hidden="true"> </span>
   </md-button>
</div>

A repeater button (set to resend every 200ms while pressed)

<div>
   <md-button class="md-button remote-button" 
        data-buttontype="repeater"
        data-interval="200"
        data-topic="volume/plus"
        data-payload="volume/plus"
        aria-label="volume plus"
    >
    <span class="fa fa-plus remote-icon"> </span>
   </md-button>
</div>

the script that handles all the .remote-button buttons...

<div>
<!--diliberately emtpy - only need the script below -->
</div>

<script>

(function($scope) {
//debugger

    //cause a small delay while things load 
    //ideally this would be an init event or on all parts of document loaded
    //(may not be necessary!)
    setTimeout(function() {
        //debugger
        $scope.init();
    },100);
    
    var BUTTON_CLASS = ".remote-button"; // The class we want this code to handle. Code will only work for buttons with this class!

    /** 
     * Initialise event handlers all buttons with class BUTTON_CLASS
    */
    $scope.init = function () {
        //debugger
        console.log("$scope.init called. Adding event handlers to all buttons with class '" + BUTTON_CLASS + "'.");
        var clickButtons = $(BUTTON_CLASS + ":not([data-buttontype='repeater'])") 
        clickButtons.click(function(e){
            //debugger
            var btn = $(this)
            var type = btn.data("buttontype");//get the button type from attribute data-buttontype="xxxxx"
            var topic = btn.data("topic");//get the topic from attribute data-topic="xxxxx"
            var payload = btn.data("payload");//get the payload from attribute data-payload="xxxxx"
            
            if(type == "toggle"){
                var newPayload = payload == 1 ? 0 : 1;
                setButtonState(btn,newPayload)
                $scope.send({"topic":topic,"payload":newPayload})
            } else {
                $scope.send({"topic":topic,"payload":payload})
            }
        }); 

        var repeatButtons = $(BUTTON_CLASS + "[data-buttontype='repeater']") 
        repeatButtons.on('mousedown',function(e) {
            var btn = $(this);
            if(this._intervalId) return; //already in operation
            var topic = btn.data("topic");//get the topic from attribute data-topic="xxxxx"
            var payload = btn.data("payload");//get the payload from attribute data-payload="xxxxx"
            var interval = btn.data("interval");//get the desired repeat duration
            if(isNumeric(interval) == false || interval < 1) interval = 300;//prevent zero & non numeric timeout value
            this._intervalId = setInterval(function() {
                $scope.send({"topic":topic,"payload":payload})
            },interval); 
        }).on('mouseup blur focusout',function(e) {
            var btn = $(this);
            if(this._intervalId){
                clearInterval(this._intervalId);
                this._intervalId = null;
            }
        });
           
    };
    
    //watch for node-red msgs
    $scope.$watch('msg', function(msg) {
        //debugger
        if(!msg){ //if no msg 
            console.log("$scope.$watch('msg', ...) - msg is empty");
            return;
        }
        if(!msg.topic){ //if no topic set found
            console.log("msg.topic is empty - cannot match this to any button")
            return; //stop processing!
        }
        var buttonSelector = ".remote-button[data-topic='" + msg.topic + "']" 
        var $btn = $(buttonSelector);//get the button
        
        if(!$btn.length){ //if no button found
            console.log(buttonSelector + " not found - cannot set state")
            return; //stop processing!
        }
        
        if($btn.length > 1){ //if MORE than one button found
            console.log(buttonSelector + " found more than 1 button - is this intended? Do you have the same data-topic set on multiple buttons?")
        }
        
        if($btn.data("buttontype") === "toggle"){
            if(msg.payload == "1"){
                setButtonState($btn, 1);
            } else if(msg.payload == "0"){
                setButtonState($btn, 0);
            } else {
               console.log("Invalid toggle value in msg.payload, cannot set " + buttonSelector + ". Ensure msg.payload is either 0 or 1") 
            }
        }

    }); 
    
    /** 
    * helper function to set the correct icon & update the "data-payload" memory 
    */
    function setButtonState($btn, state){
        
        $btn.data("payload", state);//set data-payload to new state value (used as a memory)
        
        //determine the opposite state
        var oppositeState;
        if(state == "1" || state === 1){
            state = 1; //normalise to a number
            oppositeState = 0;
        } else {
            state = 0; //normalise to a number
            oppositeState = 1;
        }
        
        var $icon = $btn.find("i"); //get the <i> element
        if(!$icon.length){
            $icon = $btn.find("span"); //get the <span> element instead!
        }
        if(!$icon.length){
            console.log("<i> or <span> not found inside button - cant toggle the icon!")
            return;//exit this function - nothing to toggle!
        }
        
        //get the old icon and new icon names
        var oldIcon = $btn.data("icon" + oppositeState); //get icon1 or icon2 depending on oppositeState
        var newIcon = $btn.data("icon" + state); //get icon1 or icon2 depending on newPayload
       
        //if we have newIcon and an actual DOM element ($icon) - update it...
        if(newIcon && $icon.length){
            if(newIcon.includes("fa-")){ 
                $icon.removeClass(oldIcon).addClass(newIcon);  // fontawesome
            } else { 
                $icon.text(newIcon); // MDI
            }
        }
    }
        
    /** 
    * helper function to determine a value is REALLY a number 
    */
    function isNumeric(n){
        if(n === "") return false;
        if(n === true || n === false) return false;
        return !isNaN(parseFloat(n)) && isFinite(n);
    }
   

})(scope);
</script>
1 Like

Wow! Mind blown!

I have sort of only just got up and am only starting to look at it.

One question from last night I kind of forgot to ask:
With all the other things I have done in Node-Red there needs to be wires between the nodes to get the message from one to the next.

Yet: yours doesn't have any except for the two inject nodes to simulate muting and the output to the debug node, or the next stage in the flow.

How do the messages go from the buttons to the main node?

I am kind of distracted by how that works.
Sure I am trying to black box it for now, and I am kind of succeeding in it, but it is still there.

I think I am going to have a lot of fun today trying to understand it all.

(Slightly off topic, butt.....)
I know CSS is a different beast to JavaScript.
When writing comments in NR the qualifier is // for a comment.
CSS uses the /* */ format. I'm taking it these are only one line and can't be multiple lined like:

/*  comments
more comments
end comments  */

But in your big template node at the bottom there are lines starting with
<!
I'm guessing they are also comments.

I'll shut up now and get back to studying the code.

Html comments...

<!--
Html
Comments
Are
multi
Line
-->

Js comments...

var x; // single line comment
/*
Multi
Line
Comment
*/

CSS comments...

<style>
.class1{
  /* width : 10px  inside a CSS entry */
}
.class2{
  // width : 10px 
}
</style>
<!-- html style comment outside style tag
<style>
.class3{
  width : 10px 
}
</style>-->

Stop fusing over details haha.

Edit...
Made some corrections.

No problems.

:slight_smile:
Um.... Sorry... B-u-u-u-u-t-t-t-t-t-t.

I am getting the hang of the flow.

The volume up button wasn't working correctly. It didn't have the colour acceptance line.

Not a problem. Found, fixed.

There is a couple of kinks though that I am seeing:

The messages are sent on mouse up events for some/most/all the buttons.
As much as it is nice the volume button sends pulses if held, it doesn't clean up when released either.
With the bit of code I posted earlier with the trigger node, when it was done, it would send a definite done message (up) and so the part of the routine that blinked the buttons would reset the colour.

As is, it can be left in the wrong colour. Not a big problem.
Did you say they can send both down and up messages? If so I can fix that problem easily enough.

Back around post 37, I was asking about any problems with button names and conflicts.
You said:

Fair enough. But I think I asked the wrong question.

Not that you are expected to know, but I am making a lot of (I dont' know the correct term) panels for remote controls to work.

It may be just an exercise as I do have the remotes. Just it is interesting to me at this stage.
Making the computer control things is different to actually having buttons, but while I am at it, I may as well add the buttons as well.

So I have 6 devices to control.
1 - TV
2 - Amp
3 - CableTV
4 - DVD
5 - DVR #1
6 - DVR #2

It will grow.
There is a Raspie playing MP3 files Jukebox in there but that is stalled at this point.

Five of the six have IR ports that accept commands.
This is useful in some cases. So all buttons must be independent of one another.

THEN

There is this wonderful world of HDMI CEC.
Sometimes IR signals are annoying. My computer monitor is actually a TV that was thrown out for not working. $95 and it works. (Cheap at twice the price)
Nice 786i wide screen.

Sending the TV IR commands to change channel when the computer is on is not a good thing.
So I use the HDMI signals.

But the IR have their own special reasons to be used.

Bottom line is I need 6 x 2 sets of panels. Each independent of any other.

I quickly doubled your buttons and ...... there is cross talk.
I have to name each button uniquely.

No problem. I was going to have to do it either way.

Anyway, I'll get back to work on getting my head around it all.

Oh, P.S.
This is what I did with the volume button.
Skip the gate node. Just bypass it.

[{"id":"d3c9c78.b59d3b8","type":"link in","z":"e59e599c.c89648","name":"","links":["40c38309.ec44dc"],"x":305,"y":290,"wires":[["be9306b5.e10408"]]},{"id":"be9306b5.e10408","type":"ui_template","z":"e59e599c.c89648","group":"ee64910d.bf557","name":"vol + ","order":14,"width":1,"height":1,"format":"<div>\n   <md-button class=\"md-button remote-button\" \n        data-buttontype=\"repeater\"\n        data-interval=\"200\"\n        data-topic=\"volume\"\n        data-payload=\"up\"\n        aria-label=\"volume plus\"\n    >\n    <span style=\"color:{{msg.colour}}\" class=\"fa fa-plus remote-icon\"> </span>\n   </md-button>\n</div>\n\n\n","storeOutMessages":true,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":410,"y":290,"wires":[[]]},{"id":"c34f0d99.c713b","type":"ui_template","z":"e59e599c.c89648","group":"ee64910d.bf557","name":"script for all buttons with class remote-button","order":23,"width":"1","height":"1","format":"<div>\n<!--diliberately emtpy - only need the script below -->\n</div>\n\n<script>\n\n(function($scope) {\n//debugger\n\n    //cause a small delay while things load \n    //ideally this would be an init event or on all parts of document loaded\n    //(may not be necessary!)\n    setTimeout(function() {\n        //debugger\n        $scope.init();\n    },100);\n    \n    var BUTTON_CLASS = \".remote-button\";\n\n    /** \n     * Initialise all buttons with class BUTTON_CLASS\n    */\n    $scope.init = function () {\n        //debugger\n        console.log(\"$scope.init called. Adding event handlers to all buttons with class '\" + BUTTON_CLASS + \"'.\");\n        var clickButtons = $(BUTTON_CLASS + \":not([data-buttontype='repeater'])\") \n        clickButtons.click(function(e){\n            //debugger\n            var btn = $(this)\n            var type = btn.data(\"buttontype\");//get the button type from attribute data-buttontype=\"xxxxx\"\n            var topic = btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n            var payload = btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n            \n            if(type == \"toggle\"){\n                var newPayload = payload == 1 ? 0 : 1;\n                setButtonState(btn,newPayload)\n                $scope.send({\"topic\":topic,\"payload\":newPayload})\n            } else {\n                $scope.send({\"topic\":topic,\"payload\":payload})\n            }\n        }); \n\n        var repeatButtons = $(BUTTON_CLASS + \"[data-buttontype='repeater']\") \n        repeatButtons.on('mousedown',function(e) {\n            var btn = $(this);\n            if(this._intervalId) return; //already in operation\n            var topic = btn.data(\"topic\");//get the topic from attribute data-topic=\"xxxxx\"\n            var payload = btn.data(\"payload\");//get the payload from attribute data-payload=\"xxxxx\"\n            var interval = btn.data(\"interval\");//get the desired repeat duration\n            if(isNumeric(interval) == false || interval < 1) interval = 300;//prevent zero & non numeric timeout value\n            this._intervalId = setInterval(function() {\n                $scope.send({\"topic\":topic,\"payload\":payload})\n            },interval); \n        }).on('mouseup blur focusout',function(e) {\n            var btn = $(this);\n            if(this._intervalId){\n                clearInterval(this._intervalId);\n                this._intervalId = null;\n            }\n        });\n           \n    };\n    \n    //watch for node-red msgs\n    $scope.$watch('msg', function(msg) {\n        //debugger\n        if(!msg){ //if no msg \n            console.log(\"$scope.$watch('msg', ...) - msg is empty\");\n            return;\n        }\n        if(!msg.topic){ //if no topic set found\n            console.log(\"msg.topic is empty - cannot match this to any button\")\n            return; //stop processing!\n        }\n        var buttonSelector = \".remote-button[data-topic='\" + msg.topic + \"']\" \n        var $btn = $(buttonSelector);//get the button\n        \n        if(!$btn.length){ //if no button found\n            console.log(buttonSelector + \" not found - cannot set state\")\n            return; //stop processing!\n        }\n        \n        if($btn.length > 1){ //if MORE than one button found\n            console.log(buttonSelector + \" found more than 1 button - is this intended? Do you have the same data-topic set on multiple buttons?\")\n        }\n        \n        if($btn.data(\"buttontype\") === \"toggle\"){\n            if(msg.payload == \"1\"){\n                setButtonState($btn, 1);\n            } else if(msg.payload == \"0\"){\n                setButtonState($btn, 0);\n            } else {\n               console.log(\"Invalid toggle value in msg.payload, cannot set \" + buttonSelector + \". Ensure msg.payload is either 0 or 1\") \n            }\n        }\n\n    }); \n    \n    /** \n    * helper function to set the correct icon & update the \"data-payload\" memory \n    */\n    function setButtonState($btn, state){\n        \n        $btn.data(\"payload\", state);//set data-payload to new state value (used as a memory)\n        \n        //determine the opposite state\n        var oppositeState;\n        if(state == \"1\" || state === 1){\n            state = 1; //normalise to a number\n            oppositeState = 0;\n        } else {\n            state = 0; //normalise to a number\n            oppositeState = 1;\n        }\n        \n        var $icon = $btn.find(\"i\"); //get the <i> element\n        if(!$icon.length){\n            $icon = $btn.find(\"span\"); //get the <span> element instead!\n        }\n        if(!$icon.length){\n            console.log(\"<i> or <span> not found inside button - cant toggle the icon!\")\n            return;//exit this function - nothing to toggle!\n        }\n        \n        //get the old icon and new icon names\n        var oldIcon = $btn.data(\"icon\" + oppositeState); //get icon1 or icon2 depending on oppositeState\n        var newIcon = $btn.data(\"icon\" + state); //get icon1 or icon2 depending on newPayload\n       \n        //if we have newIcon and an actual DOM element ($icon) - update it...\n        if(newIcon && $icon.length){\n            if(newIcon.includes(\"fa-\")){ \n                $icon.removeClass(oldIcon).addClass(newIcon);  // fontawesome\n            } else { \n                $icon.text(newIcon); // MDI\n            }\n        }\n    }\n        \n    /** \n    * helper function to determine a value is REALLY a number \n    */\n    function isNumeric(n){\n        if(n === \"\") return false;\n        if(n === true || n === false) return false;\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n   \n\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":530,"y":530,"wires":[["b525eab0.e413b","39b04897.517ec8"]]},{"id":"39b04897.517ec8","type":"switch","z":"e59e599c.c89648","name":"","property":"topic","propertyType":"msg","rules":[{"t":"neq","v":"lock","vt":"str"},{"t":"eq","v":"lock","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":410,"y":570,"wires":[["caafaf69.b9d52"],["bd9f23f4.ace9b"]]},{"id":"caafaf69.b9d52","type":"gate","z":"e59e599c.c89648","name":"","controlTopic":"MASTER","defaultState":"open","openCmd":"0","closeCmd":"1","toggleCmd":"toggle","defaultCmd":"default","persist":false,"x":570,"y":570,"wires":[["923668d0.6f876"]]},{"id":"923668d0.6f876","type":"switch","z":"e59e599c.c89648","name":"Route","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"volume","vt":"str"},{"t":"eq","v":"channel","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":800,"y":570,"wires":[["eedf856e.c91138","a0996ac0.b21f78"],["e3bf21fd.ce125"],["9493345c.011bf"]]},{"id":"a0996ac0.b21f78","type":"switch","z":"e59e599c.c89648","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"up","vt":"str"},{"t":"eq","v":"down","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":900,"y":420,"wires":[["76c22cee.89116c"],["c49454ea.576eb"]]},{"id":"76c22cee.89116c","type":"function","z":"e59e599c.c89648","name":"toggle","func":"var x = context.get(\"counter\") || 0;\nif (msg.payload == \"Up\")\n{\n    context.set(\"counter\",0);\n    msg.colour = \"grey\";\n    return msg;\n}\n\nif (x === 0)\n{\n    msg.colour = \"lime\";\n} else\nif (x === 1)\n{\n    msg.colour = \"grey\";\n}\n\nx = (x + 1) % 2;\n\ncontext.set(\"counter\",x);\n\nreturn msg;","outputs":1,"noerr":0,"x":1060,"y":400,"wires":[["40c38309.ec44dc","338cd3f6.7f6fc4"]]},{"id":"40c38309.ec44dc","type":"link out","z":"e59e599c.c89648","name":"","links":["d3c9c78.b59d3b8"],"x":1155,"y":400,"wires":[]},{"id":"ee64910d.bf557","type":"ui_group","z":"","name":"Full_Remote2","tab":"3b12b4f5.2a8a6c","order":3,"disp":false,"width":"3","collapse":false},{"id":"3b12b4f5.2a8a6c","type":"ui_tab","z":"","name":"HDMI_TV_control","icon":"dashboard","order":7,"disabled":false,"hidden":false}]

But see the problem with the colour setting?

See here...

Instead of loads of wires coming out of every button node, there is one, but everything is identified by the topic.

Its the same but different

to think about....
I would strongly recommend putting all the number buttons topics as "channel/1", "channel/2" etc then using a switch, pass any message stating "channel/" towards your code that sends channel numbers. There's 1 bit of flow to handle all 10 numbers.

We can get to that later.

But keep in mind how you might type these topics as you go through setting up your buttons. If the items belong together, like channel or volume up/down/mute, use topics with common parts like "volume/up", "volume/down" etc.

Bed time for me I'm afraid.

1 Like

There is no need to name buttons. Remove id from all of them.

Just ensure they have individual topic set.

Ps, I deliberately added console logging should you make any mistakes look in the browsers console log (Dev tools). If there are no messages, all is well.

:wink:

I know the feeling frm 8 hours ago.

I think I have worked out a way to fix a problem I mentioned.

talk to you later.

1 Like