Skip to content

Commit

Permalink
Merge pull request #1506 from bartbutenaers/ui-audio-widget
Browse files Browse the repository at this point in the history
New Widget: Audio
  • Loading branch information
joepavitt authored Dec 13, 2024
2 parents 9d52ea9 + 0a51568 commit a0c4941
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export default ({ mode }) => {
text: 'Widgets',
collapsed: false,
items: [
{ text: 'ui-audio', link: '/nodes/widgets/ui-audio' },
{ text: 'ui-button', link: '/nodes/widgets/ui-button' },
{ text: 'ui-button-group', link: '/nodes/widgets/ui-button-group' },
{ text: 'ui-control', link: '/nodes/widgets/ui-control' },
Expand Down
7 changes: 6 additions & 1 deletion docs/nodes/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ description: Explore the wide range of widgets available in Node-RED Dashboard 2
import WidgetGrid from '../components/WidgetGrid.vue'

const general = [{
name: 'Audio',
widget: 'ui-audio',
image: '/images/node-examples/ui-audio.png',
description: 'Adds a audio player to your dashboard.'
}, {
name: 'Button',
widget: 'ui-button',
image: '/images/node-examples/ui-button.png',
Expand Down Expand Up @@ -148,4 +153,4 @@ Here is a list of the third-party widgets we're aware of to make it easier to fi

The following are a list of nodes that we've been made aware of, are in active development, but have not yet been published to the Node-RED Palette Manager.

- [@bartbutenaers/ui-svg](https://github.com/bartbutenaers/node-red-dashboard-2-ui-svg/tree/master): Adds an SVG widget to your Dashboard, with dynamic controls over plotting and styling.
- [@bartbutenaers/ui-svg](https://github.com/bartbutenaers/node-red-dashboard-2-ui-svg/tree/master): Adds an SVG widget to your Dashboard, with dynamic controls over plotting and styling.
76 changes: 76 additions & 0 deletions docs/nodes/widgets/ui-audio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
description: "Play dynamically audio files with ui-audio in Node-RED Dashboard 2.0."
props:
Group: Defines which group of the UI Dashboard this widget will render in.
Size: Controls the width of the button with respect to the parent group. Maximum value is the width of the group.
Source:
description: The source is the url where the audio file can be fetched..
dynamic: true
Autoplay:
description: Specify whether the audio file will start playing automatically.
dynamic: true
Loop:
description: Specify whether the audio should be looping, i.e. start playing automatically again when ended.
dynamic: true
Muted:
description: Specify whether the audio should be muted.
dynamic: true
controls:
enabled:
example: true | false
description: Allow control over whether or not the button is clickable.
dynamic:
Source:
payload: msg.ui_update.source
structure: ["String"]
Autoplay:
payload: msg.ui_update.autoplay
structure: ["'on' | 'off'"]
Loop:
payload: msg.ui_update.loop
structure: ["'on' | 'off'"]
Muted:
payload: msg.ui_update.muted
structure: ["'on' | 'off'"]
---

<script setup>
import { ref } from 'vue'

import ExampleButtonHold from '../../examples/ui-button-hold.json'

import TryDemo from "./../../components/TryDemo.vue"
import FlowViewer from '../../components/FlowViewer.vue'

const examples = ref({
'hold': ExampleButtonHold
})
</script>


<TryDemo href="button-example">

# Audio `ui-audio`

</TryDemo>

Adds a clickable button to your dashboard.

## Properties

<PropsTable/>

## Dynamic Properties

<DynamicPropsTable/>

## Controls

<ControlsTable/>

## Example

### Simple Button

![Example of a Button](/images/node-examples/ui-button.png "Example of a Button"){data-zoomable}
*Example of a rendered button in a Dashboard.*
31 changes: 31 additions & 0 deletions nodes/widgets/locales/en-US/ui_audio.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script type="text/html" data-help-name="ui-audio">
<p>
Plays an audio file in the dashboard.
</p>
<p>
Each received <code>msg.payload</code> will contain a new source, i.e. a new audio file url.
</p>
<h3>Properties</h3>
<dl class="message-properties">
<dt>Source <span class="property-type">string</span></dt>
<dd>The source is the url where the audio file can be fetched.</dd>
<dt>Autoplay <span class="property-type">list</span></dt>
<dd>Specify whether the audio file will start playing automatically.</dd>
<dt>Loop <span class="property-type">list</span></dt>
<dd>Specify whether the audio should be looping, i.e. start playing automatically again when ended.</dd>
<dt>Muted <span class="property-type">list</span></dt>
<dd>Specify whether the audio should be muted.</dd>
</dl>
<h3>Dynamic Properties (Inputs)</h3>
<p>Any of the following can be appended to a <code>msg.ui_update</code> in order to override or set properties on this node at runtime.</p>
<dl class="message-properties">
<dt class="optional">src<span class="property-type">string</span></dt>
<dd>Override the configured audio source.</dd>
<dt class="optional">autoplay<span class="property-type">'on' | 'off'</span></dt>
<dd>Override the configured autoplay setting .</dd>
<dt class="optional">loop<span class="property-type">'on' | 'off'</span></dt>
<dd>Override the configured looping behaviour.</dd>
<dt class="optional">muted<span class="property-type">'on' | 'off'</span></dt>
<dd>Override the configured muted setting.</dd>
</dl>
</script>
17 changes: 17 additions & 0 deletions nodes/widgets/locales/en-US/ui_audio.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"ui-audio": {
"label": {
"group": "Group",
"size": "Size",
"icon": "Icon",
"source": "Source",
"autoplay": "Autoplay",
"loop": "Loop",
"muted": "Muted"
},
"option": {
"on": "On",
"off": "Off"
}
}
}
113 changes: 113 additions & 0 deletions nodes/widgets/ui_audio.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<script type="text/javascript">
(function () {
RED.nodes.registerType('ui-audio', {
category: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.label.category'),
color: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.colors.medium'),
defaults: {
group: { type: 'ui-group', required: true },
name: { value: '' },
order: { value: 0 },
width: {
value: 0,
validate: function (v) {
const width = v || 0
const currentGroup = $('#node-input-group').val() || this.group
const groupNode = RED.nodes.node(currentGroup)
const valid = !groupNode || +width >= 0
$('#node-input-size').toggleClass('input-error', !valid)
return valid
}
},
height: { value: 0 },
src: { value: ''},
autoplay: { value: 'off' },
loop: { value: 'off' },
muted: { value: 'off' }
},
inputs: 1,
outputs: 1,
align: 'right',
icon: 'font-awesome/fa-volume-up',
paletteLabel: 'audio',
label: function () { return this.name },
labelStyle: function () { return this.name ? 'node_label_italic' : '' },
oneditprepare: function () {
// if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up
// as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
if (RED.nodes.subflow(this.z)) {
// change inputs from hidden to text & display them
$('#node-input-width').attr('type', 'text')
$('#node-input-height').attr('type', 'text')
$('div.form-row.nr-db-ui-element-sizer-row').hide()
$('div.form-row.nr-db-ui-manual-size-row').show()
} else {
// not in a subflow, use the elementSizer
$('div.form-row.nr-db-ui-element-sizer-row').show()
$('div.form-row.nr-db-ui-manual-size-row').hide()
$('#node-input-size').elementSizer({
width: '#node-input-width',
height: '#node-input-height',
group: '#node-input-group'
})
}

// use jQuery UI tooltip to convert the plain old title attribute to a nice tooltip
$('.ui-node-popover-title').tooltip({
show: {
effect: 'slideDown',
delay: 150
}
})
}
})
})()
</script>

<script type="text/html" data-template-name="ui-audio">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui-audio.label.group"></label>
<input type="text" id="node-input-group">
</div>
<div class="form-row nr-db-ui-element-sizer-row">
<label><i class="fa fa-object-group"></i> <span data-i18n="ui-audio.label.size"></label>
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row nr-db-ui-manual-size-row">
<label><i class="fa fa-arrows-h"></i> <span data-i18n="ui-audio.label.width">Width</label>
<input type="hidden" id="node-input-width">
</div>
<div class="form-row nr-db-ui-manual-size-row">
<label><i class="fa fa-arrows-v"></i> <span data-i18n="ui-audio.label.height">Height</label>
<input type="hidden" id="node-input-height">
</div>
<div class="form-row">
<label for="node-input-src"><i class="fa fa-globe"></i> <span data-i18n="ui-audio.label.source"></label>
<input type="text" id="node-input-src">
</div>
<div class="form-row">
<label for="node-input-autoplay"><i class="fa fa-play-circle"></i> <span data-i18n="ui-audio.label.autoplay"></label>
<select id="node-input-autoplay" style="width:70%;">
<option value="on" data-i18n="ui-audio.option.on"></option>
<option value="off" data-i18n="ui-audio.option.off"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-loop"><i class="fa fa-retweet"></i> <span data-i18n="ui-audio.label.loop"></label>
<select id="node-input-loop" style="width:70%;">
<option value="on" data-i18n="ui-audio.option.on"></option>
<option value="off" data-i18n="ui-audio.option.off"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-muted"><i class="fa fa-volume-up"></i> <span data-i18n="ui-audio.label.muted"></label>
<select id="node-input-muted" style="width:70%;">
<option value="on" data-i18n="ui-audio.option.on"></option>
<option value="off" data-i18n="ui-audio.option.off"></option>
</select>
</div>
<div class="form-tips"><b>Note</b>: Autoplay will only work after a user gesture (e.g. click on the dashboard).</span></div>
</script>
81 changes: 81 additions & 0 deletions nodes/widgets/ui_audio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const datastore = require('../store/data.js')
const statestore = require('../store/state.js')

module.exports = function (RED) {
function AudioNode (config) {
const node = this

RED.nodes.createNode(this, config)

// which group are we rendering this widget
const group = RED.nodes.getNode(config.group)

const evts = {
onAction: true,
onInput: function (msg, send) {
// store the latest msg passed to node, only if a source is supplied in the payload
if (typeof msg.payload === 'string') {
datastore.save(group.getBase(), node, msg)
}
// only send msg on if we have passthru enabled
if (config.passthru) {
send(msg)
}
},
beforeSend: function (msg) {
if (msg.playback === 'play') {
const lastMsg = datastore.get(node.id)
// TODO zou eigenlijk de last message met een payload moeten zijn.
const src = lastMsg?.payload || config.src
if (typeof src !== 'string' || src.trim() === '') {
node.warn('Cannot play audio when no source has been specified')
}
}

if (msg.ui_update) {
const updates = msg.ui_update

if (updates) {
if (typeof updates.src !== 'undefined') {
// dynamically set "src" property
statestore.set(group.getBase(), node, msg, 'src', updates.src)
}
if (typeof updates.autoplay !== 'undefined') {
if (['on', 'off'].includes(updates.autoplay)) {
// dynamically set "autoplay" property
statestore.set(group.getBase(), node, msg, 'autoplay', updates.autoplay)
} else {
node.error('Property msg.ui_update.autoplay should be "on" or "off"')
}
}
if (typeof updates.loop !== 'undefined') {
if (['on', 'off'].includes(updates.loop)) {
// dynamically set "loop" property
statestore.set(group.getBase(), node, msg, 'loop', updates.loop)
} else {
node.error('Property msg.ui_update.loop should be "on" or "off"')
}
}
if (typeof updates.muted !== 'undefined') {
if (['on', 'off'].includes(updates.muted)) {
// dynamically set "muted" property
statestore.set(group.getBase(), node, msg, 'muted', updates.muted)
} else {
node.error('Property msg.ui_update.muted should be "on" or "off"')
}
}
}
}
return msg
}
}

// inform the dashboard UI that we are adding this node
if (group) {
group.register(node, config, evts)
} else {
node.error('No group configured')
}
}
RED.nodes.registerType('ui-audio', AudioNode)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"ui-chart": "nodes/widgets/ui_chart.js",
"ui-gauge": "nodes/widgets/ui_gauge.js",
"ui-notification": "nodes/widgets/ui_notification.js",
"ui-audio": "nodes/widgets/ui_audio.js",
"ui-markdown": "nodes/widgets/ui_markdown.js",
"ui-template": "nodes/widgets/ui_template.js",
"ui-event": "nodes/widgets/ui_event.js",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/widgets/index.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import UIAudio from './ui-audio/UIAudio.vue'
import UIButton from './ui-button/UIButton.vue'
import UIButtonGroup from './ui-button-group/UIButtonGroup.vue'
import UIChart from './ui-chart/UIChart.vue'
Expand All @@ -21,6 +22,7 @@ import UITextInput from './ui-text-input/UITextInput.vue'

// Named exports for use in other components
export {
UIAudio,
UIButton,
UIButtonGroup,
UIChart,
Expand Down Expand Up @@ -48,6 +50,7 @@ export { useDataTracker } from './data-tracker.mjs'

// Exported as an object for look up by widget name
export default {
'ui-audio': UIAudio,
'ui-button': UIButton,
'ui-button-group': UIButtonGroup,
'ui-chart': UIChart,
Expand Down
Loading

0 comments on commit a0c4941

Please sign in to comment.