From 72c32fe702525d92a3a4b6a28c062642fd27c116 Mon Sep 17 00:00:00 2001 From: BlackSigkill Date: Mon, 9 Feb 2026 20:34:00 +0100 Subject: [PATCH 1/3] Allow sidebar menu reorder --- js/settings.js | 108 ++++++++++++++++++++++++++++++++++++++++++++++--- js/storage.js | 88 ++++++++++++++++++++++++++++++++++++---- styles.css | 56 +++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 12 deletions(-) diff --git a/js/settings.js b/js/settings.js index 210673f..dafb943 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1362,11 +1362,9 @@ export function initializeSettings(scrobbler, player, api, ui) { const sidebarShowSettingsToggle = document.getElementById('sidebar-show-settings-toggle'); if (sidebarShowSettingsToggle) { - sidebarShowSettingsToggle.checked = sidebarSectionSettings.shouldShowSettings(); - sidebarShowSettingsToggle.addEventListener('change', (e) => { - sidebarSectionSettings.setShowSettings(e.target.checked); - sidebarSectionSettings.applySidebarVisibility(); - }); + sidebarShowSettingsToggle.checked = true; + sidebarShowSettingsToggle.disabled = true; + sidebarSectionSettings.setShowSettings(true); } const sidebarShowAccountToggle = document.getElementById('sidebar-show-account-toggle'); @@ -1408,6 +1406,106 @@ export function initializeSettings(scrobbler, player, api, ui) { // Apply sidebar visibility on initialization sidebarSectionSettings.applySidebarVisibility(); + const sidebarOrderConfig = [ + { toggle: sidebarShowHomeToggle, sidebarId: 'sidebar-nav-home' }, + { toggle: sidebarShowLibraryToggle, sidebarId: 'sidebar-nav-library' }, + { toggle: sidebarShowRecentToggle, sidebarId: 'sidebar-nav-recent' }, + { toggle: sidebarShowUnreleasedToggle, sidebarId: 'sidebar-nav-unreleased' }, + { toggle: sidebarShowDonateToggle, sidebarId: 'sidebar-nav-donate' }, + { toggle: sidebarShowSettingsToggle, sidebarId: 'sidebar-nav-settings' }, + { toggle: sidebarShowAccountToggle, sidebarId: 'sidebar-nav-account' }, + { toggle: sidebarShowAboutToggle, sidebarId: 'sidebar-nav-about' }, + { toggle: sidebarShowDownloadToggle, sidebarId: 'sidebar-nav-download' }, + { toggle: sidebarShowDiscordToggle, sidebarId: 'sidebar-nav-discord' }, + ]; + + const sidebarSettingsGroup = sidebarShowHomeToggle?.closest('.settings-group'); + if (sidebarSettingsGroup) { + sidebarOrderConfig.forEach(({ toggle, sidebarId }) => { + const item = toggle?.closest('.setting-item'); + if (!item) return; + item.dataset.sidebarId = sidebarId; + item.classList.add('sidebar-setting-item'); + item.draggable = true; + }); + + const getSidebarItems = () => + Array.from(sidebarSettingsGroup.querySelectorAll('.sidebar-setting-item[data-sidebar-id]')); + + const applySidebarSettingsOrder = () => { + const order = sidebarSectionSettings.getOrder(); + const itemMap = new Map(getSidebarItems().map((item) => [item.dataset.sidebarId, item])); + + order.forEach((id) => { + const item = itemMap.get(id); + if (item) { + sidebarSettingsGroup.appendChild(item); + } + }); + }; + + applySidebarSettingsOrder(); + + let draggedItem = null; + + const saveSidebarOrder = () => { + const order = getSidebarItems().map((item) => item.dataset.sidebarId); + sidebarSectionSettings.setOrder(order); + sidebarSectionSettings.applySidebarVisibility(); + }; + + const handleDragStart = (e) => { + const item = e.target.closest('.sidebar-setting-item'); + if (!item) return; + draggedItem = item; + draggedItem.classList.add('dragging'); + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', item.dataset.sidebarId || ''); + } + }; + + const handleDragEnd = () => { + if (!draggedItem) return; + draggedItem.classList.remove('dragging'); + draggedItem = null; + saveSidebarOrder(); + }; + + const getDragAfterElement = (container, y) => { + const draggableElements = [...container.querySelectorAll('.sidebar-setting-item:not(.dragging)')]; + + return draggableElements.reduce( + (closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset, element: child }; + } + return closest; + }, + { offset: Number.NEGATIVE_INFINITY } + ).element; + }; + + const handleDragOver = (e) => { + e.preventDefault(); + if (!draggedItem) return; + const afterElement = getDragAfterElement(sidebarSettingsGroup, e.clientY); + if (afterElement === draggedItem) return; + if (afterElement) { + sidebarSettingsGroup.insertBefore(draggedItem, afterElement); + } else { + sidebarSettingsGroup.appendChild(draggedItem); + } + }; + + sidebarSettingsGroup.addEventListener('dragstart', handleDragStart); + sidebarSettingsGroup.addEventListener('dragend', handleDragEnd); + sidebarSettingsGroup.addEventListener('dragover', handleDragOver); + sidebarSettingsGroup.addEventListener('drop', (e) => e.preventDefault()); + } + // Filename template setting const filenameTemplate = document.getElementById('filename-template'); if (filenameTemplate) { diff --git a/js/storage.js b/js/storage.js index f3d8938..1d13e16 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1215,6 +1215,19 @@ export const sidebarSectionSettings = { SHOW_ABOUT_KEY: 'sidebar-show-about', SHOW_DOWNLOAD_KEY: 'sidebar-show-download', SHOW_DISCORD_KEY: 'sidebar-show-discord', + ORDER_KEY: 'sidebar-menu-order', + DEFAULT_ORDER: [ + 'sidebar-nav-home', + 'sidebar-nav-library', + 'sidebar-nav-recent', + 'sidebar-nav-unreleased', + 'sidebar-nav-donate', + 'sidebar-nav-settings', + 'sidebar-nav-account', + 'sidebar-nav-about', + 'sidebar-nav-download', + 'sidebar-nav-discord', + ], shouldShowHome() { try { @@ -1282,16 +1295,15 @@ export const sidebarSectionSettings = { }, shouldShowSettings() { - try { - const val = localStorage.getItem(this.SHOW_SETTINGS_KEY); - return val === null ? true : val === 'true'; - } catch { - return true; - } + return true; }, setShowSettings(enabled) { - localStorage.setItem(this.SHOW_SETTINGS_KEY, enabled ? 'true' : 'false'); + if (enabled) { + localStorage.setItem(this.SHOW_SETTINGS_KEY, 'true'); + } else { + localStorage.removeItem(this.SHOW_SETTINGS_KEY); + } }, shouldShowAccount() { @@ -1346,7 +1358,69 @@ export const sidebarSectionSettings = { localStorage.setItem(this.SHOW_DISCORD_KEY, enabled ? 'true' : 'false'); }, + normalizeOrder(order) { + const baseOrder = this.DEFAULT_ORDER; + const safeOrder = Array.isArray(order) ? order.filter((id) => baseOrder.includes(id)) : []; + const uniqueOrder = [...new Set(safeOrder)]; + const missing = baseOrder.filter((id) => !uniqueOrder.includes(id)); + return [...uniqueOrder, ...missing]; + }, + + getOrder() { + try { + const stored = localStorage.getItem(this.ORDER_KEY); + if (stored) { + return this.normalizeOrder(JSON.parse(stored)); + } + } catch { + // ignore + } + return this.normalizeOrder([]); + }, + + setOrder(order) { + const normalized = this.normalizeOrder(order); + localStorage.setItem(this.ORDER_KEY, JSON.stringify(normalized)); + }, + + applySidebarOrder() { + const lists = document.querySelectorAll('.sidebar-nav ul'); + const primaryList = lists[0]; + if (!primaryList) return; + const secondaryList = lists[1]; + + const order = this.getOrder(); + const secondaryCount = secondaryList ? secondaryList.children.length : 0; + const splitIndex = secondaryCount ? Math.max(0, order.length - secondaryCount) : order.length; + const primaryOrder = order.slice(0, splitIndex); + const secondaryOrder = order.slice(splitIndex); + + primaryOrder.forEach((id) => { + const item = document.getElementById(id); + if (item) { + primaryList.appendChild(item); + } + }); + + if (secondaryList) { + secondaryOrder.forEach((id) => { + const item = document.getElementById(id); + if (item) { + secondaryList.appendChild(item); + } + }); + } else { + secondaryOrder.forEach((id) => { + const item = document.getElementById(id); + if (item) { + primaryList.appendChild(item); + } + }); + } + }, + applySidebarVisibility() { + this.applySidebarOrder(); const items = [ { id: 'sidebar-nav-home', check: this.shouldShowHome() }, { id: 'sidebar-nav-library', check: this.shouldShowLibrary() }, diff --git a/styles.css b/styles.css index cff42a0..201c4e7 100644 --- a/styles.css +++ b/styles.css @@ -1742,6 +1742,40 @@ input[type='search']::-webkit-search-cancel-button { gap: var(--spacing-lg); } +.setting-item.sidebar-setting-item { + align-items: center; + gap: var(--spacing-md); + cursor: grab; +} + +.setting-item.sidebar-setting-item::before { + content: ''; + width: 14px; + height: 16px; + border-radius: 4px; + flex-shrink: 0; + display: inline-block; + background-image: radial-gradient(currentColor 1.2px, transparent 1.3px); + background-size: 6px 6px; + background-position: center; + background-repeat: repeat; + color: var(--muted-foreground); +} + +.setting-item.sidebar-setting-item .info { + flex: 1; + min-width: 0; +} + +.setting-item.sidebar-setting-item .toggle-switch { + flex-shrink: 0; +} + +.setting-item.sidebar-setting-item.dragging { + opacity: 0.6; + cursor: grabbing; +} + .setting-item .info { display: flex; flex-direction: column; @@ -1981,6 +2015,28 @@ input:checked + .slider::before { transform: translateX(16px) scale(1.1); } +.toggle-switch input:disabled + .slider { + opacity: 0.5; + cursor: not-allowed; +} + +.toggle-switch input:disabled + .slider::before { + box-shadow: none; + transform: translateX(0); +} + +.toggle-switch input:disabled:checked + .slider::before { + transform: translateX(16px); +} + +.toggle-switch:hover input:disabled + .slider::before { + transform: translateX(0); +} + +.toggle-switch:hover input:disabled:checked + .slider::before { + transform: translateX(16px); +} + .track-info { display: flex; align-items: center; From 667a861b1d3c617fc6d57720ba566ac9cf744042 Mon Sep 17 00:00:00 2001 From: BlackSigkill Date: Mon, 9 Feb 2026 20:40:42 +0100 Subject: [PATCH 2/3] fix settings (with toggles) layout --- js/settings.js | 24 ++++++++++-------------- styles.css | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/js/settings.js b/js/settings.js index dafb943..aeba237 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1406,22 +1406,18 @@ export function initializeSettings(scrobbler, player, api, ui) { // Apply sidebar visibility on initialization sidebarSectionSettings.applySidebarVisibility(); - const sidebarOrderConfig = [ - { toggle: sidebarShowHomeToggle, sidebarId: 'sidebar-nav-home' }, - { toggle: sidebarShowLibraryToggle, sidebarId: 'sidebar-nav-library' }, - { toggle: sidebarShowRecentToggle, sidebarId: 'sidebar-nav-recent' }, - { toggle: sidebarShowUnreleasedToggle, sidebarId: 'sidebar-nav-unreleased' }, - { toggle: sidebarShowDonateToggle, sidebarId: 'sidebar-nav-donate' }, - { toggle: sidebarShowSettingsToggle, sidebarId: 'sidebar-nav-settings' }, - { toggle: sidebarShowAccountToggle, sidebarId: 'sidebar-nav-account' }, - { toggle: sidebarShowAboutToggle, sidebarId: 'sidebar-nav-about' }, - { toggle: sidebarShowDownloadToggle, sidebarId: 'sidebar-nav-download' }, - { toggle: sidebarShowDiscordToggle, sidebarId: 'sidebar-nav-discord' }, - ]; - const sidebarSettingsGroup = sidebarShowHomeToggle?.closest('.settings-group'); if (sidebarSettingsGroup) { - sidebarOrderConfig.forEach(({ toggle, sidebarId }) => { + const toggleIdFromSidebarId = (sidebarId) => + sidebarId ? sidebarId.replace('sidebar-nav-', 'sidebar-show-') + '-toggle' : ''; + + const sidebarOrderConfig = sidebarSectionSettings.DEFAULT_ORDER.map((sidebarId) => ({ + sidebarId, + toggleId: toggleIdFromSidebarId(sidebarId), + })); + + sidebarOrderConfig.forEach(({ toggleId, sidebarId }) => { + const toggle = document.getElementById(toggleId); const item = toggle?.closest('.setting-item'); if (!item) return; item.dataset.sidebarId = sidebarId; diff --git a/styles.css b/styles.css index 201c4e7..1a4879e 100644 --- a/styles.css +++ b/styles.css @@ -4596,6 +4596,35 @@ img[src=''] { gap: var(--spacing-md); } + .setting-item:has(.toggle-switch) { + flex-direction: row; + align-items: center; + width: 100%; + } + + .setting-item:has(.toggle-switch) .info { + width: auto; + flex: 1; + min-width: 0; + } + + .setting-item:has(.toggle-switch) .label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .setting-item:has(.toggle-switch) .description { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + } + + .setting-item:has(.toggle-switch) .toggle-switch { + margin-left: auto; + } + .setting-item .info { width: 100%; } From ed579624e29385445b714048b5c46152ef2dbf72 Mon Sep 17 00:00:00 2001 From: BlackSigkill Date: Mon, 9 Feb 2026 20:48:02 +0100 Subject: [PATCH 3/3] fix linting --- js/audio-context.js | 4 ++-- js/storage.js | 2 +- styles.css | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/audio-context.js b/js/audio-context.js index d16c721..ef30140 100644 --- a/js/audio-context.js +++ b/js/audio-context.js @@ -212,7 +212,7 @@ class AudioContextManager { if (this.monoMergerNode) { try { this.monoMergerNode.disconnect(); - } catch (e) { + } catch { // Ignore if not connected } } @@ -220,7 +220,7 @@ class AudioContextManager { // Only disconnect destination from analyser to preserve other taps (like Butterchurn) try { this.analyser.disconnect(this.audioContext.destination); - } catch (e) { + } catch { // Ignore if not connected } diff --git a/js/storage.js b/js/storage.js index 1d13e16..c9bc7e3 100644 --- a/js/storage.js +++ b/js/storage.js @@ -86,7 +86,7 @@ export const apiSettings = { } }, - async getInstances(type = 'api', sortBySpeed = false) { + async getInstances(type = 'api', _sortBySpeed = false) { let instancesObj; const stored = localStorage.getItem(this.STORAGE_KEY); diff --git a/styles.css b/styles.css index 1a4879e..d11761d 100644 --- a/styles.css +++ b/styles.css @@ -1755,7 +1755,7 @@ input[type='search']::-webkit-search-cancel-button { border-radius: 4px; flex-shrink: 0; display: inline-block; - background-image: radial-gradient(currentColor 1.2px, transparent 1.3px); + background-image: radial-gradient(currentcolor 1.2px, transparent 1.3px); background-size: 6px 6px; background-position: center; background-repeat: repeat;