// event listener setup and DOM event binding let queueSelection; // setup media control handlers for hardware buttons and lock screen function setupMediaSessionHandlers() { if (!navigator.mediaSession) return; Object.entries({ play: () => ui.player.play(), pause: () => ui.player.pause(), previoustrack: () => skipTrack(-1), nexttrack: () => skipTrack(1), seekto: (e) => (ui.player.currentTime = e.seekTime), }).forEach(([action, handler]) => navigator.mediaSession.setActionHandler(action, handler), ); } document.addEventListener("DOMContentLoaded", async () => { // lock tab order initially and watch for new elements lockTabOrder(); tabOrderObserver = setupTabOrderObserver(); // initialize queueselection queueSelection = new QueueSelection(ui.queueList, { rowSelector: "tr", indexAttribute: "data-index", selectedClass: CLASSES.SELECTED, }); // make only #queue main and #library div tabbable const mainEl = document.getElementById(DOM_IDS.QUEUE); const libraryEl = document.getElementById(DOM_IDS.LIBRARY); if (mainEl) { mainEl.tabIndex = 0; mainEl.addEventListener("click", () => mainEl.focus()); } if (libraryEl) { libraryEl.tabIndex = 0; libraryEl.addEventListener("click", (e) => { if (e.target.closest(`#${DOM_IDS.LIBRARY_SEARCH}`)) return; libraryEl.focus(); }); } // setup settings modal setupSettings(); // login/logout ui.loginForm.addEventListener("submit", (e) => { e.preventDefault(); handleLogin(); }); // audio playback controls and info display ["play", "pause"].forEach((event) => ui.player.addEventListener(event, updatePlayIcon), ); ui.player.addEventListener("ended", handleTrackEnd); ui.player.addEventListener("loadedmetadata", () => { ui.progress.max = ui.player.duration; }); ui.player.addEventListener("timeupdate", () => { if (!document.hidden) { ui.progress.value = ui.player.currentTime; const current = formatDuration(ui.player.currentTime); const total = formatDuration(ui.player.duration || 0); ui.timeDisplay.textContent = `${current} / ${total}`; const lyric = getCurrentLyric(ui.player.currentTime); ui.trackLyric.textContent = lyric ? `♪ ${lyric.text}` : ""; } }); let lastScrobbledSongId = null; ui.player.addEventListener("play", () => { if ( state.settings.scrobbling && state.queueIndex >= 0 && state.queueIndex < state.queue.length ) { const songId = state.queue[state.queueIndex]?.id; if (songId && songId !== lastScrobbledSongId) { lastScrobbledSongId = songId; state.api.nowPlaying(songId).catch(() => {}); } } }); // playback controls ui.playBtn.addEventListener("click", togglePlayback); ui.prevBtn.addEventListener("click", () => skipTrack(-1)); ui.nextBtn.addEventListener("click", () => skipTrack(1)); // progress slider const seekHandler = (e) => { ui.player.currentTime = parseFloat(e.target.value); }; ui.progress.addEventListener("input", seekHandler); ui.progress.addEventListener("change", seekHandler); // loop toggle ui.loopBtn.addEventListener("click", () => { state.loop = !state.loop; ui.loopBtn.classList.toggle(CLASSES.ACTIVE, state.loop); }); // sort queue ui.sortBtn.addEventListener("click", (e) => { const rect = e.target.getBoundingClientRect(); showContextMenu(rect.left, rect.bottom, getSortMenuItems()); }); // clear queue ui.clearBtn.addEventListener("click", () => { queueSelection?.getSelected()?.length > 0 ? clearSelectedRows() : clearQueue(); }); // queue table: row selection and action button handlers ui.queueList.addEventListener("click", (e) => { const btn = e.target.closest("button"); if (!btn) { // handle row selection on click const row = e.target.closest("tr"); if (row) { const idx = parseInt(row.getAttribute("data-index")); queueSelection.select(idx, { multi: e.ctrlKey || e.metaKey, shift: e.shiftKey, }); } return; } const idx = parseInt(e.target.closest("tr").getAttribute("data-index")); for (const [className, handler] of Object.entries(QUEUE_BUTTON_HANDLERS)) { if (btn.classList.contains(className)) { handler(idx); break; } } }); // double-click to play ui.queueList.addEventListener("dblclick", (e) => { const row = e.target.closest("tr"); if (row) { const idx = parseInt(row.getAttribute("data-index")); state.queueIndex = idx; saveQueue(); playTrack(state.queue[idx]); queueSelection.clear(); updateQueue(); } }); // deselect selections when clicking empty space in their respective containers document.addEventListener("click", (e) => { const isInQueueTable = e.target.closest(`#${DOM_IDS.QUEUE_TABLE}`); const isInContextMenu = e.target.closest(`#${DOM_IDS.CONTEXT_MENU}`); const isButton = e.target.closest("button"); const isLink = e.target.closest("a"); // clear queue selection when clicking empty space in queue if ( mainEl?.contains(e.target) && !isInQueueTable && !isButton && !isLink && !isInContextMenu ) { queueSelection.clear(); } // clear library selection when clicking empty space in library (not on item/button/link) if ( libraryEl?.contains(e.target) && !isLink && !isButton && !isInContextMenu ) { librarySelection.focusItem(null); } }); // library section toggles ui.sectionToggles.forEach((toggle) => toggle.addEventListener("click", handleSectionToggle), ); // setup event handlers setupQueueDragAndDrop(); setupQueueContextMenu(); initVirtualScroller(); setupKeyboardShortcuts(); setupSearch(); setupMediaSessionHandlers(); });