generated from ztimson/template
This commit is contained in:
267
public/jukebox.js
Normal file
267
public/jukebox.js
Normal file
@@ -0,0 +1,267 @@
|
||||
class JukeboxComponent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
// Use global singleton 🎵
|
||||
this.api = window.netNaviAPI;
|
||||
|
||||
this.playlist = [];
|
||||
this.currentTrackIndex = 0;
|
||||
this.bgMusic = null;
|
||||
this.isMuted = false;
|
||||
this.hasInteracted = false;
|
||||
this.theme = null;
|
||||
this.isPlaylistMode = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.setupAPIListeners();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// Cleanup listeners when component is removed
|
||||
if (this.unsubscribeWorld) this.unsubscribeWorld();
|
||||
}
|
||||
|
||||
setupAPIListeners() {
|
||||
// Listen for world loaded events to auto-load music 🎧
|
||||
this.unsubscribeWorld = this.api.on('world:loaded', (data) => {
|
||||
console.log('🎵 Jukebox detected world loaded:', data.theme.name);
|
||||
if (data.theme?.music) {
|
||||
this.loadMusic(data.theme.music, data.theme);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
.audio-controls {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--dialogue-header-bg, #ffffff);
|
||||
border: 3px solid var(--dialogue-border, #000);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
min-width: 200px;
|
||||
}
|
||||
.track-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--dialogue-bg, #8b5cf6);
|
||||
color: var(--dialogue-text, #ffffff);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin: 0 8px;
|
||||
overflow: hidden;
|
||||
height: 24px;
|
||||
width: 120px;
|
||||
position: relative;
|
||||
}
|
||||
.track-name {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
padding-left: 100%;
|
||||
animation: marquee 10s linear infinite;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
@keyframes marquee {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
.controls-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.control-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--button-bg, #6366f1);
|
||||
border: 2px solid var(--dialogue-border, #000);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 3px 0 var(--button-shadow, #4338ca);
|
||||
transition: transform 0.1s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.control-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 0 var(--button-shadow, #4338ca);
|
||||
}
|
||||
.control-btn:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: 0 1px 0 var(--button-shadow, #4338ca);
|
||||
}
|
||||
.control-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #fff;
|
||||
}
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
|
||||
<button class="control-btn" id="simple-mute-btn">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="audio-controls hidden" id="playlist-controls">
|
||||
<div class="controls-row">
|
||||
<button class="control-btn" id="prev-btn">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="track-info">
|
||||
<span class="track-name" id="track-name">No track loaded</span>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button class="control-btn" id="next-btn">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="control-btn" id="mute-btn">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.shadowRoot.getElementById('simple-mute-btn').addEventListener('click', () => this.toggleMute());
|
||||
this.shadowRoot.getElementById('mute-btn').addEventListener('click', () => this.toggleMute());
|
||||
this.shadowRoot.getElementById('prev-btn').addEventListener('click', () => this.previousTrack());
|
||||
this.shadowRoot.getElementById('next-btn').addEventListener('click', () => this.nextTrack());
|
||||
}
|
||||
|
||||
loadMusic(musicConfig, theme) {
|
||||
if (!musicConfig) return;
|
||||
|
||||
this.theme = theme;
|
||||
this.isPlaylistMode = Array.isArray(musicConfig) && musicConfig.length > 1;
|
||||
this.playlist = Array.isArray(musicConfig) ? musicConfig : [musicConfig];
|
||||
this.currentTrackIndex = 0;
|
||||
|
||||
this.applyThemeColors();
|
||||
|
||||
if (this.isPlaylistMode) {
|
||||
this.shadowRoot.getElementById('simple-mute-btn').classList.add('hidden');
|
||||
this.shadowRoot.getElementById('playlist-controls').classList.remove('hidden');
|
||||
} else {
|
||||
this.shadowRoot.getElementById('simple-mute-btn').classList.remove('hidden');
|
||||
this.shadowRoot.getElementById('playlist-controls').classList.add('hidden');
|
||||
}
|
||||
|
||||
this.loadTrack(this.currentTrackIndex);
|
||||
}
|
||||
|
||||
loadTrack(index) {
|
||||
if (this.bgMusic) {
|
||||
this.bgMusic.pause();
|
||||
this.bgMusic = null;
|
||||
}
|
||||
|
||||
if (index >= 0 && index < this.playlist.length) {
|
||||
this.bgMusic = new Audio(this.playlist[index]);
|
||||
this.bgMusic.volume = 0.5;
|
||||
|
||||
if (this.isPlaylistMode) {
|
||||
this.bgMusic.addEventListener('ended', () => {
|
||||
this.currentTrackIndex = (this.currentTrackIndex + 1) % this.playlist.length;
|
||||
this.loadTrack(this.currentTrackIndex);
|
||||
});
|
||||
this.updateTrackDisplay();
|
||||
} else {
|
||||
this.bgMusic.loop = true;
|
||||
}
|
||||
|
||||
this.setupAutoplayHandler();
|
||||
|
||||
if (this.hasInteracted && !this.isMuted) {
|
||||
this.bgMusic.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTrackDisplay() {
|
||||
const trackName = this.shadowRoot.getElementById('track-name');
|
||||
const fileName = this.playlist[this.currentTrackIndex].split('/').pop();
|
||||
const trackNum = String(this.currentTrackIndex + 1).padStart(2, '0');
|
||||
trackName.textContent = `[${trackNum}] ${fileName}`;
|
||||
}
|
||||
|
||||
applyThemeColors() {
|
||||
if (!this.theme) return;
|
||||
const root = this.shadowRoot.host.style;
|
||||
Object.entries(this.theme.colors).forEach(([key, value]) => {
|
||||
const cssVar = '--' + key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
root.setProperty(cssVar, value);
|
||||
});
|
||||
}
|
||||
|
||||
setupAutoplayHandler() {
|
||||
const startMusic = () => {
|
||||
if (!this.hasInteracted && !this.isMuted && this.bgMusic) {
|
||||
this.hasInteracted = true;
|
||||
this.bgMusic.play();
|
||||
}
|
||||
};
|
||||
const interactionEvents = ['click', 'keydown', 'touchstart'];
|
||||
interactionEvents.forEach(event => {
|
||||
document.addEventListener(event, startMusic, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
previousTrack() {
|
||||
if (!this.isPlaylistMode) return;
|
||||
this.currentTrackIndex = (this.currentTrackIndex - 1 + this.playlist.length) % this.playlist.length;
|
||||
this.loadTrack(this.currentTrackIndex);
|
||||
}
|
||||
|
||||
nextTrack() {
|
||||
if (!this.isPlaylistMode) return;
|
||||
this.currentTrackIndex = (this.currentTrackIndex + 1) % this.playlist.length;
|
||||
this.loadTrack(this.currentTrackIndex);
|
||||
}
|
||||
|
||||
toggleMute() {
|
||||
this.isMuted = !this.isMuted;
|
||||
const simpleMuteBtn = this.shadowRoot.getElementById('simple-mute-btn');
|
||||
const muteBtn = this.shadowRoot.getElementById('mute-btn');
|
||||
|
||||
const mutedSVG = '<svg viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>';
|
||||
const unmutedSVG = '<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>';
|
||||
|
||||
if (this.bgMusic) {
|
||||
if (this.isMuted) {
|
||||
this.bgMusic.pause();
|
||||
simpleMuteBtn.innerHTML = mutedSVG;
|
||||
muteBtn.innerHTML = mutedSVG;
|
||||
} else {
|
||||
if (!this.hasInteracted) this.hasInteracted = true;
|
||||
this.bgMusic.play();
|
||||
simpleMuteBtn.innerHTML = unmutedSVG;
|
||||
muteBtn.innerHTML = unmutedSVG;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('jukebox-component', JukeboxComponent);
|
||||
Reference in New Issue
Block a user