import { Controller } from "@hotwired/stimulus" const STORAGE_KEY = "evaluation_music_enabled" const FADE_DURATION = 500 // ms export default class extends Controller { static targets = ["toggleButton", "toggleIcon"] static values = { musicUrls: Array } connect() { this.audio = null this.isFading = false // Load preference from localStorage this.musicEnabled = localStorage.getItem(STORAGE_KEY) === "true" this.updateToggleUI() // Start music if enabled if (this.musicEnabled) { this.startMusic() } // Listen for modal close event (dialog element stays in DOM when closed) this.dialog = this.element.closest("dialog") if (this.dialog) { this.boundHandleDialogClose = this.handleDialogClose.bind(this) this.dialog.addEventListener("close", this.boundHandleDialogClose) } // Also listen for Turbo events that might replace the content this.boundHandleTurboBeforeCache = this.handleTurboBeforeCache.bind(this) document.addEventListener("turbo:before-cache", this.boundHandleTurboBeforeCache) } disconnect() { this.stopMusic() // Clean up event listeners if (this.dialog && this.boundHandleDialogClose) { this.dialog.removeEventListener("close", this.boundHandleDialogClose) } document.removeEventListener("turbo:before-cache", this.boundHandleTurboBeforeCache) } handleDialogClose() { this.fadeOutAndStop() } handleTurboBeforeCache() { this.stopMusic() } toggle() { this.musicEnabled = !this.musicEnabled localStorage.setItem(STORAGE_KEY, this.musicEnabled.toString()) this.updateToggleUI() if (this.musicEnabled) { this.startMusic() } else { this.fadeOutAndStop() } } startMusic() { if (this.audio || this.musicUrlsValue.length === 0) return // Pick a random track const randomIndex = Math.floor(Math.random() * this.musicUrlsValue.length) const musicUrl = this.musicUrlsValue[randomIndex] this.audio = new Audio(musicUrl) this.audio.loop = true this.audio.volume = 0 // Start playing and fade in this.audio.play() .then(() => { this.fadeIn() }) .catch((error) => { console.log("Audio autoplay blocked:", error) // If autoplay is blocked, we'll try again on next user interaction this.audio = null }) } stopMusic() { if (this.audio) { this.audio.pause() this.audio.src = "" this.audio = null } } fadeIn() { if (!this.audio || this.isFading) return this.isFading = true const targetVolume = 0.5 const steps = 20 const stepDuration = FADE_DURATION / steps const volumeStep = targetVolume / steps let currentStep = 0 const fadeInterval = setInterval(() => { currentStep++ if (currentStep >= steps || !this.audio) { if (this.audio) this.audio.volume = targetVolume clearInterval(fadeInterval) this.isFading = false } else { this.audio.volume = Math.min(targetVolume, volumeStep * currentStep) } }, stepDuration) } fadeOutAndStop() { if (!this.audio || this.isFading) { this.stopMusic() return } this.isFading = true const startVolume = this.audio.volume const steps = 20 const stepDuration = FADE_DURATION / steps const volumeStep = startVolume / steps let currentStep = 0 const fadeInterval = setInterval(() => { currentStep++ if (currentStep >= steps || !this.audio) { clearInterval(fadeInterval) this.stopMusic() this.isFading = false } else { this.audio.volume = Math.max(0, startVolume - (volumeStep * currentStep)) } }, stepDuration) } updateToggleUI() { if (this.hasToggleIconTarget) { this.toggleIconTarget.innerHTML = this.musicEnabled ? this.speakerOnIcon() : this.speakerOffIcon() } if (this.hasToggleButtonTarget) { this.toggleButtonTarget.classList.toggle("btn-primary", this.musicEnabled) this.toggleButtonTarget.classList.toggle("btn-ghost", !this.musicEnabled) } } speakerOnIcon() { // Heroicon: speaker-wave (outline) return ` ` } speakerOffIcon() { // Heroicon: speaker-x-mark (outline) return ` ` } }