Allow sidebar menu reorder

This commit is contained in:
BlackSigkill 2026-02-09 20:34:00 +01:00
parent 62b83ea9e9
commit 72c32fe702
3 changed files with 240 additions and 12 deletions

View file

@ -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) {

View file

@ -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() },

View file

@ -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;