648 lines
22 KiB
JavaScript
648 lines
22 KiB
JavaScript
/**
|
|
* TaskMate Config Flow Sound Preview
|
|
* Plays sound preview when the completion_sound dropdown value changes
|
|
* in the config flow (add/edit chore screens).
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Debug logging gate. Set window.__taskmate_config_sounds_debug = true in the
|
|
// browser console to enable the chatty console.debug/warn output.
|
|
const debugLog = (...args) => {
|
|
if (window.__taskmate_config_sounds_debug) console.debug(...args);
|
|
};
|
|
const debugWarn = (...args) => {
|
|
if (window.__taskmate_config_sounds_debug) console.warn(...args);
|
|
};
|
|
|
|
// Audio context for sound preview (lazy initialized)
|
|
let audioContext = null;
|
|
|
|
/**
|
|
* Get or create AudioContext
|
|
*/
|
|
function getAudioContext() {
|
|
if (!audioContext) {
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
if (audioContext.state === 'suspended') {
|
|
audioContext.resume();
|
|
}
|
|
return audioContext;
|
|
}
|
|
|
|
// ============== SOUND IMPLEMENTATIONS ==============
|
|
// These are copies from taskmate-child-card.js to keep this file standalone
|
|
|
|
function playCoinSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.3;
|
|
|
|
const osc1 = ctx.createOscillator();
|
|
const gain1 = ctx.createGain();
|
|
osc1.connect(gain1);
|
|
gain1.connect(masterGain);
|
|
osc1.frequency.value = 1318.5;
|
|
osc1.type = 'square';
|
|
gain1.gain.setValueAtTime(0.5, startTime);
|
|
gain1.gain.exponentialRampToValueAtTime(0.01, startTime + 0.1);
|
|
osc1.start(startTime);
|
|
osc1.stop(startTime + 0.1);
|
|
|
|
const osc2 = ctx.createOscillator();
|
|
const gain2 = ctx.createGain();
|
|
osc2.connect(gain2);
|
|
gain2.connect(masterGain);
|
|
osc2.frequency.value = 1975.5;
|
|
osc2.type = 'square';
|
|
gain2.gain.setValueAtTime(0.5, startTime + 0.08);
|
|
gain2.gain.exponentialRampToValueAtTime(0.01, startTime + 0.25);
|
|
osc2.start(startTime + 0.08);
|
|
osc2.stop(startTime + 0.25);
|
|
}
|
|
|
|
function playLevelUpSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.25;
|
|
|
|
const notes = [523.25, 659.25, 783.99, 1046.5];
|
|
const duration = 0.12;
|
|
|
|
notes.forEach((freq, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(masterGain);
|
|
osc.frequency.value = freq;
|
|
osc.type = 'square';
|
|
|
|
const noteStart = startTime + i * duration;
|
|
gain.gain.setValueAtTime(0.6, noteStart);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, noteStart + duration + 0.1);
|
|
osc.start(noteStart);
|
|
osc.stop(noteStart + duration + 0.15);
|
|
});
|
|
|
|
const chordNotes = [523.25, 659.25, 783.99];
|
|
const chordStart = startTime + notes.length * duration;
|
|
chordNotes.forEach((freq) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(masterGain);
|
|
osc.frequency.value = freq;
|
|
osc.type = 'triangle';
|
|
gain.gain.setValueAtTime(0.3, chordStart);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, chordStart + 0.5);
|
|
osc.start(chordStart);
|
|
osc.stop(chordStart + 0.55);
|
|
});
|
|
}
|
|
|
|
function playFanfareSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.2;
|
|
|
|
const pattern = [
|
|
{ freq: 392.00, duration: 0.1, delay: 0 },
|
|
{ freq: 392.00, duration: 0.1, delay: 0.12 },
|
|
{ freq: 392.00, duration: 0.15, delay: 0.24 },
|
|
{ freq: 329.63, duration: 0.15, delay: 0.42 },
|
|
{ freq: 392.00, duration: 0.15, delay: 0.6 },
|
|
{ freq: 523.25, duration: 0.4, delay: 0.78 },
|
|
];
|
|
|
|
pattern.forEach(({ freq, duration, delay }) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(masterGain);
|
|
osc.frequency.value = freq;
|
|
osc.type = 'sawtooth';
|
|
|
|
const noteStart = startTime + delay;
|
|
gain.gain.setValueAtTime(0.5, noteStart);
|
|
gain.gain.setValueAtTime(0.5, noteStart + duration * 0.8);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, noteStart + duration);
|
|
osc.start(noteStart);
|
|
osc.stop(noteStart + duration + 0.05);
|
|
});
|
|
}
|
|
|
|
function playChimeSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.3;
|
|
|
|
const fundamental = 880;
|
|
const harmonics = [1, 2, 3, 4.2];
|
|
|
|
harmonics.forEach((harmonic, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(masterGain);
|
|
osc.frequency.value = fundamental * harmonic;
|
|
osc.type = 'sine';
|
|
|
|
const amplitude = 0.5 / (i + 1);
|
|
const decayTime = 0.8 / (i + 1);
|
|
|
|
gain.gain.setValueAtTime(amplitude, startTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, startTime + decayTime);
|
|
osc.start(startTime);
|
|
osc.stop(startTime + decayTime + 0.1);
|
|
});
|
|
}
|
|
|
|
function playPowerUpSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.25;
|
|
|
|
const osc1 = ctx.createOscillator();
|
|
const gain1 = ctx.createGain();
|
|
osc1.connect(gain1);
|
|
gain1.connect(masterGain);
|
|
osc1.type = 'sawtooth';
|
|
osc1.frequency.setValueAtTime(200, startTime);
|
|
osc1.frequency.exponentialRampToValueAtTime(1200, startTime + 0.3);
|
|
gain1.gain.setValueAtTime(0.4, startTime);
|
|
gain1.gain.exponentialRampToValueAtTime(0.01, startTime + 0.35);
|
|
osc1.start(startTime);
|
|
osc1.stop(startTime + 0.4);
|
|
|
|
const sparkleNotes = [1318.5, 1567.98, 1975.5];
|
|
sparkleNotes.forEach((freq, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(masterGain);
|
|
osc.frequency.value = freq;
|
|
osc.type = 'sine';
|
|
|
|
const noteStart = startTime + 0.25 + i * 0.05;
|
|
gain.gain.setValueAtTime(0.3, noteStart);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, noteStart + 0.2);
|
|
osc.start(noteStart);
|
|
osc.stop(noteStart + 0.25);
|
|
});
|
|
}
|
|
|
|
function playUndoSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.25;
|
|
|
|
const osc1 = ctx.createOscillator();
|
|
const gain1 = ctx.createGain();
|
|
osc1.connect(gain1);
|
|
gain1.connect(masterGain);
|
|
osc1.type = 'triangle';
|
|
osc1.frequency.setValueAtTime(311.13, startTime);
|
|
osc1.frequency.exponentialRampToValueAtTime(233.08, startTime + 0.25);
|
|
gain1.gain.setValueAtTime(0.6, startTime);
|
|
gain1.gain.exponentialRampToValueAtTime(0.3, startTime + 0.2);
|
|
gain1.gain.exponentialRampToValueAtTime(0.01, startTime + 0.3);
|
|
osc1.start(startTime);
|
|
osc1.stop(startTime + 0.35);
|
|
|
|
const osc2 = ctx.createOscillator();
|
|
const gain2 = ctx.createGain();
|
|
osc2.connect(gain2);
|
|
gain2.connect(masterGain);
|
|
osc2.type = 'triangle';
|
|
osc2.frequency.setValueAtTime(233.08, startTime + 0.3);
|
|
osc2.frequency.exponentialRampToValueAtTime(155.56, startTime + 0.7);
|
|
gain2.gain.setValueAtTime(0.5, startTime + 0.3);
|
|
gain2.gain.exponentialRampToValueAtTime(0.25, startTime + 0.55);
|
|
gain2.gain.exponentialRampToValueAtTime(0.01, startTime + 0.75);
|
|
osc2.start(startTime + 0.3);
|
|
osc2.stop(startTime + 0.8);
|
|
|
|
const osc3 = ctx.createOscillator();
|
|
const gain3 = ctx.createGain();
|
|
osc3.connect(gain3);
|
|
gain3.connect(masterGain);
|
|
osc3.type = 'sine';
|
|
osc3.frequency.setValueAtTime(116.54, startTime + 0.5);
|
|
gain3.gain.setValueAtTime(0.15, startTime + 0.5);
|
|
gain3.gain.exponentialRampToValueAtTime(0.01, startTime + 0.8);
|
|
osc3.start(startTime + 0.5);
|
|
osc3.stop(startTime + 0.85);
|
|
}
|
|
|
|
function playFartSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.9;
|
|
|
|
const noiseLength = ctx.sampleRate * 0.5;
|
|
const noiseBuffer = ctx.createBuffer(1, noiseLength, ctx.sampleRate);
|
|
const noiseData = noiseBuffer.getChannelData(0);
|
|
|
|
let lastOut = 0;
|
|
for (let i = 0; i < noiseLength; i++) {
|
|
const white = Math.random() * 2 - 1;
|
|
noiseData[i] = (lastOut + (0.04 * white)) / 1.04;
|
|
lastOut = noiseData[i];
|
|
noiseData[i] *= 8;
|
|
}
|
|
|
|
const noiseSource = ctx.createBufferSource();
|
|
noiseSource.buffer = noiseBuffer;
|
|
|
|
const filter = ctx.createBiquadFilter();
|
|
filter.type = 'bandpass';
|
|
filter.frequency.setValueAtTime(200, startTime);
|
|
filter.frequency.exponentialRampToValueAtTime(80, startTime + 0.4);
|
|
filter.Q.value = 2;
|
|
|
|
const noiseGain = ctx.createGain();
|
|
noiseGain.gain.setValueAtTime(0, startTime);
|
|
noiseGain.gain.linearRampToValueAtTime(1.0, startTime + 0.01);
|
|
noiseGain.gain.setValueAtTime(1.0, startTime + 0.15);
|
|
noiseGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.45);
|
|
|
|
noiseSource.connect(filter);
|
|
filter.connect(noiseGain);
|
|
noiseGain.connect(masterGain);
|
|
noiseSource.start(startTime);
|
|
noiseSource.stop(startTime + 0.5);
|
|
|
|
const bass = ctx.createOscillator();
|
|
const bassGain = ctx.createGain();
|
|
bass.type = 'sawtooth';
|
|
bass.frequency.setValueAtTime(80, startTime);
|
|
bass.frequency.exponentialRampToValueAtTime(40, startTime + 0.4);
|
|
bassGain.gain.setValueAtTime(0, startTime);
|
|
bassGain.gain.linearRampToValueAtTime(0.8, startTime + 0.02);
|
|
bassGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.45);
|
|
bass.connect(bassGain);
|
|
bassGain.connect(masterGain);
|
|
bass.start(startTime);
|
|
bass.stop(startTime + 0.5);
|
|
|
|
const flap = ctx.createOscillator();
|
|
const flapGain = ctx.createGain();
|
|
flap.type = 'square';
|
|
flap.frequency.setValueAtTime(25, startTime);
|
|
flap.frequency.exponentialRampToValueAtTime(12, startTime + 0.4);
|
|
flapGain.gain.setValueAtTime(0.6, startTime);
|
|
flapGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.4);
|
|
flap.connect(flapGain);
|
|
flapGain.connect(masterGain);
|
|
flap.start(startTime);
|
|
flap.stop(startTime + 0.45);
|
|
|
|
const sub = ctx.createOscillator();
|
|
const subGain = ctx.createGain();
|
|
sub.type = 'sine';
|
|
sub.frequency.setValueAtTime(50, startTime);
|
|
sub.frequency.exponentialRampToValueAtTime(25, startTime + 0.3);
|
|
subGain.gain.setValueAtTime(0.7, startTime);
|
|
subGain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.35);
|
|
sub.connect(subGain);
|
|
subGain.connect(masterGain);
|
|
sub.start(startTime);
|
|
sub.stop(startTime + 0.4);
|
|
}
|
|
|
|
function playFartLongSound(ctx, startTime) {
|
|
const masterGain = ctx.createGain();
|
|
masterGain.connect(ctx.destination);
|
|
masterGain.gain.value = 0.85;
|
|
|
|
const noiseLength = ctx.sampleRate * 2.0;
|
|
const noiseBuffer = ctx.createBuffer(1, noiseLength, ctx.sampleRate);
|
|
const noiseData = noiseBuffer.getChannelData(0);
|
|
|
|
let lastOut = 0;
|
|
for (let i = 0; i < noiseLength; i++) {
|
|
const white = Math.random() * 2 - 1;
|
|
noiseData[i] = (lastOut + (0.05 * white)) / 1.05;
|
|
lastOut = noiseData[i];
|
|
noiseData[i] *= 7;
|
|
}
|
|
|
|
const noiseSource = ctx.createBufferSource();
|
|
noiseSource.buffer = noiseBuffer;
|
|
|
|
const filter = ctx.createBiquadFilter();
|
|
filter.type = 'bandpass';
|
|
filter.frequency.setValueAtTime(250, startTime);
|
|
filter.frequency.linearRampToValueAtTime(180, startTime + 0.3);
|
|
filter.frequency.linearRampToValueAtTime(280, startTime + 0.5);
|
|
filter.frequency.linearRampToValueAtTime(150, startTime + 0.9);
|
|
filter.frequency.linearRampToValueAtTime(220, startTime + 1.2);
|
|
filter.frequency.exponentialRampToValueAtTime(60, startTime + 1.8);
|
|
filter.Q.value = 3;
|
|
|
|
const noiseGain = ctx.createGain();
|
|
noiseGain.gain.setValueAtTime(0, startTime);
|
|
noiseGain.gain.linearRampToValueAtTime(1.0, startTime + 0.02);
|
|
noiseGain.gain.linearRampToValueAtTime(0.7, startTime + 0.4);
|
|
noiseGain.gain.linearRampToValueAtTime(1.0, startTime + 0.6);
|
|
noiseGain.gain.linearRampToValueAtTime(0.5, startTime + 1.0);
|
|
noiseGain.gain.linearRampToValueAtTime(0.9, startTime + 1.3);
|
|
noiseGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.9);
|
|
|
|
noiseSource.connect(filter);
|
|
filter.connect(noiseGain);
|
|
noiseGain.connect(masterGain);
|
|
noiseSource.start(startTime);
|
|
noiseSource.stop(startTime + 2.0);
|
|
|
|
const bass = ctx.createOscillator();
|
|
const bassGain = ctx.createGain();
|
|
bass.type = 'sawtooth';
|
|
bass.frequency.setValueAtTime(90, startTime);
|
|
bass.frequency.linearRampToValueAtTime(70, startTime + 0.5);
|
|
bass.frequency.linearRampToValueAtTime(100, startTime + 0.8);
|
|
bass.frequency.linearRampToValueAtTime(50, startTime + 1.3);
|
|
bass.frequency.exponentialRampToValueAtTime(30, startTime + 1.8);
|
|
bassGain.gain.setValueAtTime(0.8, startTime);
|
|
bassGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.85);
|
|
bass.connect(bassGain);
|
|
bassGain.connect(masterGain);
|
|
bass.start(startTime);
|
|
bass.stop(startTime + 1.9);
|
|
|
|
const flutter = ctx.createOscillator();
|
|
const flutterGain = ctx.createGain();
|
|
flutter.type = 'square';
|
|
flutter.frequency.setValueAtTime(35, startTime);
|
|
flutter.frequency.linearRampToValueAtTime(20, startTime + 0.8);
|
|
flutter.frequency.linearRampToValueAtTime(28, startTime + 1.0);
|
|
flutter.frequency.exponentialRampToValueAtTime(6, startTime + 1.8);
|
|
flutterGain.gain.setValueAtTime(0.7, startTime);
|
|
flutterGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.8);
|
|
flutter.connect(flutterGain);
|
|
flutterGain.connect(masterGain);
|
|
flutter.start(startTime);
|
|
flutter.stop(startTime + 1.85);
|
|
|
|
const sub = ctx.createOscillator();
|
|
const subGain = ctx.createGain();
|
|
sub.type = 'sine';
|
|
sub.frequency.setValueAtTime(45, startTime);
|
|
sub.frequency.exponentialRampToValueAtTime(20, startTime + 1.7);
|
|
subGain.gain.setValueAtTime(0.6, startTime);
|
|
subGain.gain.exponentialRampToValueAtTime(0.01, startTime + 1.75);
|
|
sub.connect(subGain);
|
|
subGain.connect(masterGain);
|
|
sub.start(startTime);
|
|
sub.stop(startTime + 1.8);
|
|
|
|
const squeakTimes = [0.5, 1.2];
|
|
squeakTimes.forEach((delay) => {
|
|
const squeak = ctx.createOscillator();
|
|
const squeakGain = ctx.createGain();
|
|
squeak.type = 'triangle';
|
|
squeak.frequency.setValueAtTime(180, startTime + delay);
|
|
squeak.frequency.exponentialRampToValueAtTime(120, startTime + delay + 0.15);
|
|
squeakGain.gain.setValueAtTime(0.4, startTime + delay);
|
|
squeakGain.gain.exponentialRampToValueAtTime(0.01, startTime + delay + 0.15);
|
|
squeak.connect(squeakGain);
|
|
squeakGain.connect(masterGain);
|
|
squeak.start(startTime + delay);
|
|
squeak.stop(startTime + delay + 0.2);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Play a sound by name
|
|
*/
|
|
function playSound(soundName) {
|
|
if (!soundName || soundName === 'none') {
|
|
debugLog('[TaskMate Config] No sound to play');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const ctx = getAudioContext();
|
|
const now = ctx.currentTime;
|
|
|
|
switch (soundName) {
|
|
case 'coin': playCoinSound(ctx, now); break;
|
|
case 'levelup': playLevelUpSound(ctx, now); break;
|
|
case 'fanfare': playFanfareSound(ctx, now); break;
|
|
case 'chime': playChimeSound(ctx, now); break;
|
|
case 'powerup': playPowerUpSound(ctx, now); break;
|
|
case 'undo': playUndoSound(ctx, now); break;
|
|
case 'fart': playFartSound(ctx, now); break;
|
|
case 'fart_long': playFartLongSound(ctx, now); break;
|
|
default:
|
|
debugWarn(`[TaskMate Config] Unknown sound: ${soundName}`);
|
|
playCoinSound(ctx, now);
|
|
}
|
|
} catch (e) {
|
|
debugWarn('[TaskMate Config] Error playing sound:', e);
|
|
}
|
|
}
|
|
|
|
// Valid sound names
|
|
const VALID_SOUNDS = ['coin', 'levelup', 'fanfare', 'chime', 'powerup', 'undo', 'fart', 'fart_long', 'none'];
|
|
|
|
/**
|
|
* Map display text to sound value
|
|
*/
|
|
function textToSoundValue(text) {
|
|
if (!text) return null;
|
|
const normalized = text.toLowerCase().trim().replace(/\s+/g, '_');
|
|
const map = {
|
|
'no_sound': 'none',
|
|
'no sound': 'none',
|
|
'coin': 'coin',
|
|
'levelup': 'levelup',
|
|
'level_up': 'levelup',
|
|
'fanfare': 'fanfare',
|
|
'chime': 'chime',
|
|
'powerup': 'powerup',
|
|
'power_up': 'powerup',
|
|
'undo': 'undo',
|
|
'fart': 'fart',
|
|
'fart_long': 'fart_long',
|
|
};
|
|
return map[normalized] || (VALID_SOUNDS.includes(normalized) ? normalized : null);
|
|
}
|
|
|
|
/**
|
|
* Attach change listener to a select element
|
|
*/
|
|
function attachSoundChangeListener(element) {
|
|
if (element.dataset.taskmateSoundListener) return;
|
|
element.dataset.taskmateSoundListener = 'true';
|
|
|
|
// Listen for various change events
|
|
const handleChange = (e) => {
|
|
const value = e.target?.value || e.detail?.value;
|
|
const soundValue = textToSoundValue(value) || value;
|
|
|
|
debugLog('[TaskMate Config] Sound changed to:', soundValue);
|
|
|
|
if (soundValue && soundValue !== 'none' && VALID_SOUNDS.includes(soundValue)) {
|
|
playSound(soundValue);
|
|
}
|
|
};
|
|
|
|
element.addEventListener('change', handleChange);
|
|
element.addEventListener('value-changed', handleChange);
|
|
element.addEventListener('selected', handleChange);
|
|
element.addEventListener('click', (e) => {
|
|
// For dropdown items being clicked
|
|
const item = e.target.closest('mwc-list-item, ha-list-item, [role="option"]');
|
|
if (item) {
|
|
const value = item.value || item.getAttribute('value') || item.textContent;
|
|
const soundValue = textToSoundValue(value);
|
|
if (soundValue && soundValue !== 'none') {
|
|
setTimeout(() => playSound(soundValue), 50);
|
|
}
|
|
}
|
|
});
|
|
|
|
debugLog('[TaskMate Config] Attached sound change listener to element');
|
|
}
|
|
|
|
/**
|
|
* Find sound selectors and attach listeners
|
|
*/
|
|
function findAndEnhanceSoundSelectors() {
|
|
// Look for any select/dropdown that might be for sounds
|
|
const selectors = document.querySelectorAll('ha-select, mwc-select, ha-combo-box, select');
|
|
|
|
selectors.forEach(selector => {
|
|
// Check if this selector has sound-related options
|
|
const options = selector.querySelectorAll('mwc-list-item, ha-list-item, option, [role="option"]');
|
|
let isSoundSelector = false;
|
|
|
|
options.forEach(opt => {
|
|
const text = (opt.value || opt.getAttribute('value') || opt.textContent || '').toLowerCase();
|
|
if (VALID_SOUNDS.some(s => text.includes(s))) {
|
|
isSoundSelector = true;
|
|
}
|
|
});
|
|
|
|
// Also check the selector's current value
|
|
const currentValue = (selector.value || '').toLowerCase();
|
|
if (VALID_SOUNDS.some(s => currentValue.includes(s))) {
|
|
isSoundSelector = true;
|
|
}
|
|
|
|
if (isSoundSelector) {
|
|
attachSoundChangeListener(selector);
|
|
|
|
// Also attach to inner elements if present
|
|
const innerSelect = selector.shadowRoot?.querySelector('select, mwc-menu, mwc-list');
|
|
if (innerSelect) {
|
|
attachSoundChangeListener(innerSelect);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Also look for ha-selector elements (HA's custom form selectors)
|
|
document.querySelectorAll('ha-selector').forEach(haSelector => {
|
|
const innerSelect = haSelector.shadowRoot?.querySelector('ha-select, mwc-select, select') ||
|
|
haSelector.querySelector('ha-select, mwc-select, select');
|
|
if (innerSelect) {
|
|
const options = innerSelect.querySelectorAll('mwc-list-item, [role="option"]');
|
|
let isSoundSelector = false;
|
|
options.forEach(opt => {
|
|
const text = (opt.value || opt.textContent || '').toLowerCase();
|
|
if (VALID_SOUNDS.some(s => text.includes(s))) {
|
|
isSoundSelector = true;
|
|
}
|
|
});
|
|
if (isSoundSelector) {
|
|
attachSoundChangeListener(innerSelect);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Watch for DOM changes and enhance new sound selectors
|
|
*/
|
|
function startObserver() {
|
|
const observer = new MutationObserver((mutations) => {
|
|
let shouldScan = false;
|
|
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.addedNodes.length > 0) {
|
|
shouldScan = true;
|
|
}
|
|
});
|
|
|
|
if (shouldScan) {
|
|
// Debounce scanning
|
|
clearTimeout(window._taskmateScanTimeout);
|
|
window._taskmateScanTimeout = setTimeout(findAndEnhanceSoundSelectors, 100);
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
window._taskmateSoundObserver = observer;
|
|
|
|
debugLog('[TaskMate Config] Sound change observer started');
|
|
}
|
|
|
|
// Initialize
|
|
function init() {
|
|
if (window._taskmateConfigSoundsInit) return;
|
|
window._taskmateConfigSoundsInit = true;
|
|
findAndEnhanceSoundSelectors();
|
|
startObserver();
|
|
|
|
// Slow periodic scan as a safety net for dynamically loaded content the
|
|
// MutationObserver doesn't catch (e.g. dialogs rendered outside body's light DOM)
|
|
window._taskmateConfigSoundsPoll = setInterval(findAndEnhanceSoundSelectors, 10000);
|
|
|
|
const cleanup = () => {
|
|
if (window._taskmateConfigSoundsPoll) {
|
|
clearInterval(window._taskmateConfigSoundsPoll);
|
|
window._taskmateConfigSoundsPoll = null;
|
|
}
|
|
if (window._taskmateScanTimeout) {
|
|
clearTimeout(window._taskmateScanTimeout);
|
|
window._taskmateScanTimeout = null;
|
|
}
|
|
if (window._taskmateSoundObserver) {
|
|
window._taskmateSoundObserver.disconnect();
|
|
window._taskmateSoundObserver = null;
|
|
}
|
|
};
|
|
// pagehide covers SPA navigation in HA; beforeunload covers full reloads.
|
|
window.addEventListener('pagehide', cleanup, { once: true });
|
|
window.addEventListener('beforeunload', cleanup, { once: true });
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
window.addEventListener('load', () => setTimeout(init, 500));
|
|
|
|
// Listen for taskmate_preview_sound events fired by the preview_sound service
|
|
document.addEventListener('taskmate_preview_sound', (e) => {
|
|
const sound = e.detail?.sound || e.detail?.data?.sound;
|
|
if (sound) playSound(sound);
|
|
});
|
|
|
|
// Also listen via window for HA event bus forwarding
|
|
window.addEventListener('taskmate_preview_sound', (e) => {
|
|
const sound = e.detail?.sound || e.detail?.data?.sound;
|
|
if (sound) playSound(sound);
|
|
});
|
|
|
|
console.info('[TaskMate] Config sound preview module loaded - sounds will play on selection change');
|
|
})();
|