|
1 | 1 | const { combineRgb } = require('@companion-module/base') |
2 | 2 |
|
| 3 | +function parseVolumePercent(self) { |
| 4 | + const raw = self.STATUS.state && self.STATUS.state.volume |
| 5 | + const v = parseInt(raw, 10) |
| 6 | + if (Number.isNaN(v)) return null |
| 7 | + return Math.min(100, Math.max(0, v)) |
| 8 | +} |
| 9 | + |
| 10 | +/** Map prefixed feedback options to advanced feedback style (Companion merges multiple feedbacks). */ |
| 11 | +function buildAdvancedStyleFromOptions(opt, idPrefix) { |
| 12 | + const g = (k) => opt[`${idPrefix}_${k}`] |
| 13 | + const out = {} |
| 14 | + const bg = g('bgcolor') |
| 15 | + const fg = g('color') |
| 16 | + const size = g('size') |
| 17 | + const alignment = g('alignment') |
| 18 | + const showTop = g('show_topbar') |
| 19 | + const text = g('text') |
| 20 | + if (bg !== undefined && bg !== null && bg !== '') out.bgcolor = bg |
| 21 | + if (fg !== undefined && fg !== null && fg !== '') out.color = fg |
| 22 | + if (size !== undefined && size !== null && size !== '') out.size = size |
| 23 | + if (alignment) out.alignment = alignment |
| 24 | + if (showTop !== undefined) out.show_topbar = showTop |
| 25 | + if (text !== undefined && String(text).trim() !== '') { |
| 26 | + out.text = String(text) |
| 27 | + out.textExpression = false |
| 28 | + } |
| 29 | + return out |
| 30 | +} |
| 31 | + |
| 32 | +function alignmentChoices() { |
| 33 | + const positions = [ |
| 34 | + ['left', 'Left'], |
| 35 | + ['center', 'Center'], |
| 36 | + ['right', 'Right'], |
| 37 | + ] |
| 38 | + const vpos = [ |
| 39 | + ['top', 'Top'], |
| 40 | + ['center', 'Middle'], |
| 41 | + ['bottom', 'Bottom'], |
| 42 | + ] |
| 43 | + const choices = [] |
| 44 | + for (const [h, hLabel] of positions) { |
| 45 | + for (const [v, vLabel] of vpos) { |
| 46 | + const id = `${h}:${v}` |
| 47 | + choices.push({ id, label: `${hLabel} / ${vLabel}` }) |
| 48 | + } |
| 49 | + } |
| 50 | + return choices |
| 51 | +} |
| 52 | + |
| 53 | +/** @param {string} idPrefix unique prefix per feedback (Companion option ids) */ |
| 54 | +function volumeStyleOptionFields(idPrefix) { |
| 55 | + return [ |
| 56 | + { |
| 57 | + type: 'static-text', |
| 58 | + id: `${idPrefix}_style_hdr`, |
| 59 | + label: 'Style when this feedback matches (use several feedbacks on one button for multiple zones)', |
| 60 | + }, |
| 61 | + { |
| 62 | + type: 'colorpicker', |
| 63 | + id: `${idPrefix}_bgcolor`, |
| 64 | + label: 'Background color', |
| 65 | + default: combineRgb(0, 0, 0), |
| 66 | + }, |
| 67 | + { |
| 68 | + type: 'colorpicker', |
| 69 | + id: `${idPrefix}_color`, |
| 70 | + label: 'Text color', |
| 71 | + default: combineRgb(255, 255, 255), |
| 72 | + }, |
| 73 | + { |
| 74 | + type: 'dropdown', |
| 75 | + id: `${idPrefix}_size`, |
| 76 | + label: 'Text size', |
| 77 | + default: 'auto', |
| 78 | + choices: [ |
| 79 | + { id: 'auto', label: 'Auto' }, |
| 80 | + { id: '7', label: '7' }, |
| 81 | + { id: '14', label: '14' }, |
| 82 | + { id: '18', label: '18' }, |
| 83 | + { id: '24', label: '24' }, |
| 84 | + { id: '30', label: '30' }, |
| 85 | + { id: '44', label: '44' }, |
| 86 | + ], |
| 87 | + }, |
| 88 | + { |
| 89 | + type: 'dropdown', |
| 90 | + id: `${idPrefix}_alignment`, |
| 91 | + label: 'Text alignment', |
| 92 | + default: 'center:center', |
| 93 | + choices: alignmentChoices(), |
| 94 | + }, |
| 95 | + { |
| 96 | + type: 'checkbox', |
| 97 | + id: `${idPrefix}_show_topbar`, |
| 98 | + label: 'Show topbar', |
| 99 | + default: true, |
| 100 | + }, |
| 101 | + { |
| 102 | + type: 'textinput', |
| 103 | + id: `${idPrefix}_text`, |
| 104 | + label: 'Button text (optional)', |
| 105 | + default: '', |
| 106 | + tooltip: 'Leave empty to keep the button’s existing text; expressions are not used unless you enable expression mode in Companion for this field', |
| 107 | + }, |
| 108 | + ] |
| 109 | +} |
| 110 | + |
3 | 111 | module.exports = { |
4 | 112 | // ########################## |
5 | 113 | // #### Define Feedbacks #### |
@@ -106,6 +214,122 @@ module.exports = { |
106 | 214 | }, |
107 | 215 | } |
108 | 216 |
|
| 217 | + feedbacks.volumeBelow = { |
| 218 | + type: 'advanced', |
| 219 | + name: 'Volume: below', |
| 220 | + description: |
| 221 | + 'When macOS system output volume is strictly below the threshold, apply the style below. Stack with “Volume: between” and “Volume: above” for multi-zone buttons.', |
| 222 | + options: [ |
| 223 | + { |
| 224 | + type: 'static-text', |
| 225 | + id: 'vol_below_match_hdr', |
| 226 | + label: 'Match when volume is less than (strictly below):', |
| 227 | + }, |
| 228 | + { |
| 229 | + type: 'number', |
| 230 | + id: 'threshold', |
| 231 | + label: 'Below (%)', |
| 232 | + default: 50, |
| 233 | + min: 0, |
| 234 | + max: 100, |
| 235 | + range: true, |
| 236 | + }, |
| 237 | + ...volumeStyleOptionFields('vb'), |
| 238 | + ], |
| 239 | + callback: async (event) => { |
| 240 | + const v = parseVolumePercent(this) |
| 241 | + if (v === null) return {} |
| 242 | + let t = Number(event.options.threshold) |
| 243 | + if (Number.isNaN(t)) t = 50 |
| 244 | + t = Math.min(100, Math.max(0, t)) |
| 245 | + if (v >= t) return {} |
| 246 | + return buildAdvancedStyleFromOptions(event.options, 'vb') |
| 247 | + }, |
| 248 | + } |
| 249 | + |
| 250 | + feedbacks.volumeBetween = { |
| 251 | + type: 'advanced', |
| 252 | + name: 'Volume: between', |
| 253 | + description: |
| 254 | + 'When volume is between the low and high values (inclusive on both ends), apply the style. Stack multiple instances for different bands.', |
| 255 | + options: [ |
| 256 | + { |
| 257 | + type: 'static-text', |
| 258 | + id: 'vol_between_match_hdr', |
| 259 | + label: 'Match when volume is in this range (%), inclusive:', |
| 260 | + }, |
| 261 | + { |
| 262 | + type: 'number', |
| 263 | + id: 'low', |
| 264 | + label: 'Low (%)', |
| 265 | + default: 50, |
| 266 | + min: 0, |
| 267 | + max: 100, |
| 268 | + range: true, |
| 269 | + }, |
| 270 | + { |
| 271 | + type: 'number', |
| 272 | + id: 'high', |
| 273 | + label: 'High (%)', |
| 274 | + default: 75, |
| 275 | + min: 0, |
| 276 | + max: 100, |
| 277 | + range: true, |
| 278 | + }, |
| 279 | + ...volumeStyleOptionFields('vbt'), |
| 280 | + ], |
| 281 | + callback: async (event) => { |
| 282 | + const v = parseVolumePercent(this) |
| 283 | + if (v === null) return {} |
| 284 | + let low = Number(event.options.low) |
| 285 | + let high = Number(event.options.high) |
| 286 | + if (Number.isNaN(low)) low = 50 |
| 287 | + if (Number.isNaN(high)) high = 75 |
| 288 | + low = Math.min(100, Math.max(0, low)) |
| 289 | + high = Math.min(100, Math.max(0, high)) |
| 290 | + if (low > high) { |
| 291 | + const x = low |
| 292 | + low = high |
| 293 | + high = x |
| 294 | + } |
| 295 | + if (v < low || v > high) return {} |
| 296 | + return buildAdvancedStyleFromOptions(event.options, 'vbt') |
| 297 | + }, |
| 298 | + } |
| 299 | + |
| 300 | + feedbacks.volumeAbove = { |
| 301 | + type: 'advanced', |
| 302 | + name: 'Volume: above', |
| 303 | + description: |
| 304 | + 'When volume is strictly greater than the threshold, apply the style. Stack with “Volume: below” and “Volume: between”.', |
| 305 | + options: [ |
| 306 | + { |
| 307 | + type: 'static-text', |
| 308 | + id: 'vol_above_match_hdr', |
| 309 | + label: 'Match when volume is greater than (strictly above):', |
| 310 | + }, |
| 311 | + { |
| 312 | + type: 'number', |
| 313 | + id: 'threshold', |
| 314 | + label: 'Above (%)', |
| 315 | + default: 75, |
| 316 | + min: 0, |
| 317 | + max: 100, |
| 318 | + range: true, |
| 319 | + }, |
| 320 | + ...volumeStyleOptionFields('va'), |
| 321 | + ], |
| 322 | + callback: async (event) => { |
| 323 | + const v = parseVolumePercent(this) |
| 324 | + if (v === null) return {} |
| 325 | + let t = Number(event.options.threshold) |
| 326 | + if (Number.isNaN(t)) t = 75 |
| 327 | + t = Math.min(100, Math.max(0, t)) |
| 328 | + if (v <= t) return {} |
| 329 | + return buildAdvancedStyleFromOptions(event.options, 'va') |
| 330 | + }, |
| 331 | + } |
| 332 | + |
109 | 333 | this.setFeedbackDefinitions(feedbacks) |
110 | 334 | }, |
111 | 335 | } |
0 commit comments