a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: inline wrappers that are no longer necessary

also make alt text/aria-label more consistent

+29 -43
+7 -7
src/index.html
··· 200 200 201 201 <header id="playback"> 202 202 <audio id="player" crossorigin="anonymous"></audio> 203 - <button id="prev-btn" aria-label="previous"> 203 + <button id="prev-btn"> 204 204 <img src="static/famfamfam-silk/control_rewind.png" alt="previous" /> 205 205 </button> 206 - <button id="play-btn" aria-label="play"> 206 + <button id="play-btn"> 207 207 <img src="static/famfamfam-silk/control_play.png" alt="play" /> 208 208 </button> 209 - <button id="next-btn" aria-label="next"> 209 + <button id="next-btn"> 210 210 <img src="static/famfamfam-silk/control_fastforward.png" alt="next" /> 211 211 </button> 212 - <button id="loop-btn" aria-label="loop"> 212 + <button id="loop-btn"> 213 213 <img src="static/famfamfam-silk/control_repeat.png" alt="loop" /> 214 214 </button> 215 215 <input type="range" id="progress" min="0" max="100" value="0" /> ··· 264 264 1.8.6</span 265 265 > 266 266 <div id="actions"> 267 - <button id="clear-btn" aria-label="clear"> 267 + <button id="clear-btn"> 268 268 <img src="static/famfamfam-silk/cross.png" alt="clear" /> 269 269 <span>clear</span> 270 270 </button> 271 - <button id="sort-btn" aria-label="sort"> 271 + <button id="sort-btn"> 272 272 <img src="static/famfamfam-silk/arrow_switch.png" alt="sort" /> 273 273 <span>sort</span> 274 274 </button> 275 - <button id="settings-btn" aria-label="settings"> 275 + <button id="settings-btn"> 276 276 <img src="static/famfamfam-silk/cog.png" alt="settings" /> 277 277 <span>settings</span> 278 278 </button>
+2 -2
src/js/contextmenu.js
··· 271 271 ui.queueList.addEventListener( 272 272 "contextmenu", 273 273 (e) => { 274 - const row = getClosestRow(e.target); 274 + const row = e.target.closest("tr"); 275 275 if (!row) return; 276 276 277 277 e.preventDefault(); 278 278 e.stopPropagation(); 279 279 280 - const idx = getRowIndex(row, DATA_ATTRS.INDEX); 280 + const idx = parseInt(row.getAttribute(DATA_ATTRS.INDEX)); 281 281 if (!selectionManager.isSelected(idx)) { 282 282 selectionManager.select(idx); 283 283 }
+6 -6
src/js/draggable.js
··· 8 8 let lastDragOverRow = null; 9 9 10 10 ui.queueList.addEventListener("dragstart", (e) => { 11 - const row = getClosestRow(e.target); 11 + const row = e.target.closest("tr"); 12 12 if ( 13 13 !row || 14 14 row.classList.contains(CLASSES.DRAGGING) || 15 - selectionManager.isSelected(getRowIndex(row, DATA_ATTRS.INDEX)) 15 + selectionManager.isSelected(parseInt(row.getAttribute(DATA_ATTRS.INDEX))) 16 16 ) 17 17 return; 18 18 19 19 selectionManager.clear(); 20 - selectionManager.select(getRowIndex(row, DATA_ATTRS.INDEX)); 20 + selectionManager.select(parseInt(row.getAttribute(DATA_ATTRS.INDEX))); 21 21 updateRowClass( 22 22 ui.queueList, 23 23 selectionManager.getSelected(), ··· 32 32 ui.queueList.addEventListener("dragover", (e) => { 33 33 e.preventDefault(); 34 34 e.dataTransfer.dropEffect = "move"; 35 - const row = getClosestRow(e.target); 35 + const row = e.target.closest("tr"); 36 36 if (!row || row.classList.contains(CLASSES.DRAGGING)) return; 37 37 38 38 const isBelow = isDropBelowCenter(e, row); ··· 65 65 // handle drop and complete the move 66 66 ui.queueList.addEventListener("drop", (e) => { 67 67 e.preventDefault(); 68 - const row = getClosestRow(e.target); 68 + const row = e.target.closest("tr"); 69 69 if (!row || row.classList.contains(CLASSES.DRAGGING)) return; 70 70 71 71 if (lastDragOverRow) { ··· 78 78 } 79 79 clearRowClasses(ui.queueList, "tr", CLASSES.DRAGGING); 80 80 81 - const draggedIdx = getRowIndex(row, DATA_ATTRS.INDEX); 81 + const draggedIdx = parseInt(row.getAttribute(DATA_ATTRS.INDEX)); 82 82 moveQueueItems( 83 83 state.queue, 84 84 selectionManager.getSelected(),
+7 -6
src/js/events.js
··· 79 79 ui.player.addEventListener("play", () => { 80 80 if ( 81 81 state.settings.scrobbling && 82 - isValidQueueIndex(state.queueIndex, state.queue.length) 82 + state.queueIndex >= 0 && 83 + state.queueIndex < state.queue.length 83 84 ) { 84 85 const songId = state.queue[state.queueIndex]?.id; 85 86 if (songId && songId !== lastScrobbledSongId) { ··· 125 126 const btn = e.target.closest("button"); 126 127 if (!btn) { 127 128 // handle row selection on click 128 - const row = getClosestRow(e.target); 129 + const row = e.target.closest("tr"); 129 130 if (row) { 130 - const idx = getRowIndex(row, DATA_ATTRS.INDEX); 131 + const idx = parseInt(row.getAttribute(DATA_ATTRS.INDEX)); 131 132 selectionManager.select(idx, { 132 133 multi: e.ctrlKey || e.metaKey, 133 134 shift: e.shiftKey, ··· 136 137 return; 137 138 } 138 139 139 - const idx = getRowIndex(getClosestRow(btn), DATA_ATTRS.INDEX); 140 + const idx = parseInt(e.target.closest("tr").getAttribute(DATA_ATTRS.INDEX)); 140 141 for (const [className, handler] of Object.entries(QUEUE_BUTTON_HANDLERS)) { 141 142 if (btn.classList.contains(className)) { 142 143 handler(idx); ··· 147 148 148 149 // double-click to play 149 150 ui.queueList.addEventListener("dblclick", (e) => { 150 - const row = getClosestRow(e.target); 151 + const row = e.target.closest("tr"); 151 152 if (row) { 152 - const idx = getRowIndex(row, DATA_ATTRS.INDEX); 153 + const idx = parseInt(row.getAttribute(DATA_ATTRS.INDEX)); 153 154 playQueueTrack(idx); 154 155 selectionManager.clear(); 155 156 updateQueue();
+3 -2
src/js/player.js
··· 97 97 98 98 // check if queue has a valid current track 99 99 const hasValidTrack = () => 100 - isValidQueueIndex(state.queueIndex, state.queue.length); 100 + state.queueIndex >= 0 && state.queueIndex < state.queue.length; 101 101 102 102 // helper to play a track at given queue index 103 103 const playQueueTrack = (idx) => { ··· 161 161 // move to next track when current song ends 162 162 function handleTrackEnd() { 163 163 if ( 164 - isValidQueueIndex(state.queueIndex, state.queue.length) && 164 + state.queueIndex >= 0 && 165 + state.queueIndex < state.queue.length && 165 166 state.settings.scrobbling 166 167 ) { 167 168 state.api.scrobble(state.queue[state.queueIndex].id).catch(() => {});
+1 -1
src/js/queue.js
··· 186 186 if (!Array.isArray(songs) || songs.length === 0) return false; 187 187 188 188 state.queue = songs; 189 - if (isValidQueueIndex(queueIndex, state.queue.length)) { 189 + if (queueIndex >= 0 && queueIndex < state.queue.length) { 190 190 state.queueIndex = queueIndex; 191 191 } 192 192 return true;
+3 -19
src/js/ui.js
··· 25 25 } 26 26 27 27 // create a button with icon 28 - function createIconButton(className, ariaLabel, iconPath, onCallback) { 28 + function createIconButton(className, alt, iconPath, onCallback) { 29 29 const btn = createElement("button", { 30 30 className, 31 - attributes: { "aria-label": ariaLabel }, 32 - children: [createElement("img", { attributes: { src: iconPath } })], 31 + children: [createElement("img", { attributes: { src: iconPath, alt } })], 33 32 }); 34 33 35 34 if (onCallback) { ··· 47 46 return btn; 48 47 } 49 48 50 - // find closest row element 51 - function getClosestRow(el, rowSelector = "tr") { 52 - return el.closest(rowSelector); 53 - } 54 - 55 49 // format seconds as mm:ss string 56 50 function formatDuration(seconds) { 57 51 if (!seconds || !Number.isFinite(seconds)) return "0:00"; ··· 61 55 .padStart(2, "0")}`; 62 56 } 63 57 64 - // get row index from data attribute 65 - function getRowIndex(rowEl, attrName = "data-index") { 66 - return parseInt(rowEl.getAttribute(attrName)); 67 - } 68 - 69 58 // remove classes from only elements that have them 70 59 function clearRowClasses(container, rowSelector, classNames) { 71 60 const classes = Array.isArray(classNames) ? classNames : [classNames]; ··· 78 67 function updateRowClass(container, indices, className, add = true) { 79 68 const indexSet = new Set(indices); 80 69 container.querySelectorAll("tr").forEach((row) => { 81 - const idx = getRowIndex(row); 70 + const idx = parseInt(row.getAttribute("data-index")); 82 71 row.classList.toggle(className, indexSet.has(idx) === add); 83 72 }); 84 73 } 85 - 86 - // validate queue index 87 - function isValidQueueIndex(index, queueLength) { 88 - return index >= 0 && index < queueLength; 89 - }