···11-// subsonic api client for server communication
11+// subsonic api client
22+23class SubsonicAPI {
34 constructor(serverUrl, username, password) {
45 this.serverUrl = serverUrl.replace(/\/$/, "");
···11+// drag and drop support for reordering queue items
22+13// setup drag and drop for queue reordering
24function setupDragAndDrop() {
35 const clearDragOver = () =>
+2-166
src/js/events.js
···11-let selectionManager;
22-33-// callbacks for queue operations
44-const queueCallbacks = {
55- onSelectionChange: (newIndices) => selectionManager.setSelection(newIndices),
66- onQueueChange: () => updateQueueDisplay(),
77-};
88-99-// toggle playback or start queue if nothing playing
1010-const togglePlayback = () => {
1111- if (hasValidTrack()) {
1212- if (ui.player.src) {
1313- ui.player.paused ? ui.player.play() : ui.player.pause();
1414- } else {
1515- playTrack(state.queue[state.queueIndex]);
1616- }
1717- } else if (state.queue.length > 0) {
1818- state.queueIndex = 0;
1919- playTrack(state.queue[0]);
2020- updateQueue();
2121- }
2222-};
2323-2424-// check if queue has a valid current track
2525-const hasValidTrack = () =>
2626- isValidQueueIndex(state.queueIndex, state.queue.length);
2727-2828-// helper to play a track at given queue index
2929-const playQueueTrack = (idx) => {
3030- state.queueIndex = idx;
3131- saveQueue();
3232- playTrack(state.queue[idx]);
3333-};
3434-3535-// map button classes to handler functions
3636-const QUEUE_BUTTON_HANDLERS = {
3737- // play the selected track
3838- [CLASSES.QUEUE_PLAY]: (idx) => {
3939- playQueueTrack(idx);
4040- updateQueue();
4141- },
4242- // insert selected track after current track
4343- [CLASSES.QUEUE_PLAY_NEXT]: (idx) => {
4444- const insertPos = state.queueIndex >= 0 ? state.queueIndex + 1 : 0;
4545- moveQueueItems(state.queue, [idx], insertPos, queueCallbacks);
4646- },
4747- // move up one position
4848- [CLASSES.QUEUE_MOVE_UP]: (idx) => {
4949- if (idx > 0) {
5050- moveQueueItems(state.queue, [idx], idx - 1, queueCallbacks);
5151- }
5252- },
5353- // move down one position
5454- [CLASSES.QUEUE_MOVE_DOWN]: (idx) => {
5555- if (idx < state.queue.length - 1) {
5656- moveQueueItems(state.queue, [idx], idx + 2, queueCallbacks);
5757- }
5858- },
5959- // clear from queue
6060- [CLASSES.QUEUE_CLEAR]: (idx) => {
6161- const isCurrentTrack = removeFromQueue(idx);
6262-6363- if (isCurrentTrack) {
6464- handleCurrentTrackDeleted();
6565- highlightCurrentTrack();
6666- }
11+// event listener setup and DOM event binding
6726868- updateQueue();
6969- },
7070- // toggle favorite status
7171- [CLASSES.QUEUE_FAVORITE]: async (idx) => {
7272- const song = state.queue[idx];
7373- if (song) {
7474- await setFavoriteSong(song);
7575- updateQueueDisplay();
7676- }
7777- },
7878-};
33+let selectionManager;
794805// setup media control handlers for hardware buttons and lock screen
816function setupMediaSessionHandlers() {
···9015 }).forEach(([action, handler]) =>
9116 navigator.mediaSession.setActionHandler(action, handler),
9217 );
9393-}
9494-9595-// navigate queue selection
9696-const navigateSelection = (offset, extend = false) => {
9797- const currentIdx = selectionManager.lastSelected ?? 0;
9898- const nextIdx =
9999- offset < 0
100100- ? Math.max(0, currentIdx - 1)
101101- : Math.min(state.queue.length - 1, currentIdx + 1);
102102-103103- if (extend) {
104104- selectionManager.select(nextIdx, { shift: true });
105105- } else {
106106- selectionManager.select(nextIdx);
107107- }
108108-109109- ui.queueList
110110- .querySelector(`tr[${DATA_ATTRS.INDEX}="${nextIdx}"]`)
111111- ?.scrollIntoView({ block: "nearest" });
112112-};
113113-114114-// keyboard shortcuts
115115-function setupKeyboardShortcuts() {
116116- document.addEventListener("keydown", (e) => {
117117- // skip if user is typing in an input field
118118- if (document.activeElement.matches("input, textarea")) return;
119119-120120- switch (e.code) {
121121- case "Space": {
122122- e.preventDefault();
123123- togglePlayback();
124124- break;
125125- }
126126-127127- case "Delete":
128128- case "Backspace": {
129129- e.preventDefault();
130130- if (selectionManager.count() > 0) {
131131- const selected = selectionManager.getSelected();
132132- const nextIdx = Math.min(
133133- selected[0],
134134- state.queue.length - selected.length - 1,
135135- );
136136- clearSelectedRows();
137137- if (nextIdx >= 0 && state.queue.length > 0) {
138138- selectionManager.select(nextIdx);
139139- }
140140- }
141141- break;
142142- }
143143-144144- case "ArrowUp": {
145145- e.preventDefault();
146146- navigateSelection(-1, e.shiftKey);
147147- break;
148148- }
149149-150150- case "ArrowDown": {
151151- e.preventDefault();
152152- navigateSelection(1, e.shiftKey);
153153- break;
154154- }
155155-156156- case "KeyA": {
157157- if (!(e.ctrlKey || e.metaKey)) return;
158158- e.preventDefault();
159159- if (state.queue.length > 0) {
160160- selectionManager.setSelection(
161161- Array.from({ length: state.queue.length }, (_, i) => i),
162162- );
163163- }
164164- break;
165165- }
166166-167167- case "Enter": {
168168- e.preventDefault();
169169- const selectedIndices = Array.from(selectionManager.getSelected());
170170- if (selectedIndices.length > 0) playQueueTrack(selectedIndices[0]);
171171- break;
172172- }
173173-174174- case "Escape": {
175175- e.preventDefault();
176176- cleanupContextMenu();
177177- clearSelection();
178178- break;
179179- }
180180- }
181181- });
18218}
1831918420document.addEventListener("DOMContentLoaded", async () => {
···11+// settings
22+13// load settings from localStorage and apply to state
24function loadSettings() {
35 const saved = localStorage.getItem("tinysub_settings");