Consolidate tabs, Quick Access links, mini player nav, link descriptions
- Merge Scripts/Callbacks/Links tabs into single Settings tab with collapsible sections - Rename Actions tab to Quick Access showing both scripts and configured links - Add prev/next buttons to mini (secondary) player - Add optional description field to links (backend + frontend) - Add CSS chevron indicators on collapsible settings sections - Persist section collapse/expand state in localStorage - Fix race condition in Quick Access rendering with generation counter - Order settings sections: Scripts, Links, Callbacks Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -400,12 +400,13 @@
|
||||
document.getElementById('source').textContent = lastStatus.source || t('player.unknown_source');
|
||||
}
|
||||
|
||||
// Reload tables to get translated content
|
||||
// Reload tables and quick access to get translated content
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (token) {
|
||||
loadScriptsTable();
|
||||
loadCallbacksTable();
|
||||
loadLinksTable();
|
||||
displayQuickAccess();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,8 +565,9 @@
|
||||
setupVolumeSlider('volume-slider');
|
||||
setupVolumeSlider('mini-volume-slider');
|
||||
|
||||
// Restore saved tab
|
||||
const savedTab = localStorage.getItem('activeTab') || 'player';
|
||||
// Restore saved tab (migrate old tab names)
|
||||
let savedTab = localStorage.getItem('activeTab') || 'player';
|
||||
if (['scripts', 'callbacks', 'links'].includes(savedTab)) savedTab = 'settings';
|
||||
switchTab(savedTab);
|
||||
// Snap indicator to initial position without animation
|
||||
const initialActiveBtn = document.querySelector('.tab-btn.active');
|
||||
@@ -700,6 +702,17 @@
|
||||
setupIconPreview('scriptIcon', 'scriptIconPreview');
|
||||
setupIconPreview('linkIcon', 'linkIconPreview');
|
||||
|
||||
// Settings sections: restore collapse state and persist on toggle
|
||||
document.querySelectorAll('.settings-section').forEach(details => {
|
||||
const key = `settings_section_${details.querySelector('summary')?.getAttribute('data-i18n') || ''}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved === 'closed') details.removeAttribute('open');
|
||||
else if (saved === 'open') details.setAttribute('open', '');
|
||||
details.addEventListener('toggle', () => {
|
||||
localStorage.setItem(key, details.open ? 'open' : 'closed');
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup blob URLs on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
thumbnailCache.forEach(url => URL.revokeObjectURL(url));
|
||||
@@ -840,6 +853,7 @@
|
||||
console.log('Links changed, reloading...');
|
||||
loadHeaderLinks();
|
||||
loadLinksTable();
|
||||
displayQuickAccess();
|
||||
} else if (msg.type === 'error') {
|
||||
console.error('WebSocket error:', msg.message);
|
||||
}
|
||||
@@ -1192,57 +1206,108 @@
|
||||
|
||||
if (response.ok) {
|
||||
scripts = await response.json();
|
||||
displayScripts();
|
||||
displayQuickAccess();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading scripts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayScripts() {
|
||||
const container = document.getElementById('panel-quick-actions');
|
||||
let _quickAccessGen = 0;
|
||||
async function displayQuickAccess() {
|
||||
const gen = ++_quickAccessGen;
|
||||
const grid = document.getElementById('scripts-grid');
|
||||
|
||||
grid.innerHTML = '';
|
||||
// Build everything into a fragment before touching the DOM
|
||||
const fragment = document.createDocumentFragment();
|
||||
const hasScripts = scripts.length > 0;
|
||||
let hasLinks = false;
|
||||
|
||||
if (scripts.length === 0) {
|
||||
grid.innerHTML = `<div class="scripts-empty empty-state-illustration"><svg viewBox="0 0 64 64"><path d="M20 8l-8 48"/><path d="M44 8l8 48"/><path d="M10 24h44"/><path d="M8 40h44"/></svg><p>${t('scripts.no_scripts')}</p></div>`;
|
||||
} else {
|
||||
scripts.forEach(script => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'script-btn';
|
||||
button.onclick = () => executeScript(script.name, button);
|
||||
// Render script buttons
|
||||
scripts.forEach(script => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'script-btn';
|
||||
button.onclick = () => executeScript(script.name, button);
|
||||
|
||||
if (script.icon) {
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.className = 'script-icon';
|
||||
iconEl.setAttribute('data-mdi-icon', script.icon);
|
||||
button.appendChild(iconEl);
|
||||
if (script.icon) {
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.className = 'script-icon';
|
||||
iconEl.setAttribute('data-mdi-icon', script.icon);
|
||||
button.appendChild(iconEl);
|
||||
}
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'script-label';
|
||||
label.textContent = script.label || script.name;
|
||||
button.appendChild(label);
|
||||
|
||||
if (script.description) {
|
||||
const description = document.createElement('div');
|
||||
description.className = 'script-description';
|
||||
description.textContent = script.description;
|
||||
button.appendChild(description);
|
||||
}
|
||||
|
||||
fragment.appendChild(button);
|
||||
});
|
||||
|
||||
// Fetch link cards
|
||||
try {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (token) {
|
||||
const response = await fetch('/api/links/list', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (gen !== _quickAccessGen) return; // stale call, discard
|
||||
if (response.ok) {
|
||||
const links = await response.json();
|
||||
hasLinks = links.length > 0;
|
||||
links.forEach(link => {
|
||||
const card = document.createElement('a');
|
||||
card.className = 'script-btn link-card';
|
||||
card.href = link.url;
|
||||
card.target = '_blank';
|
||||
card.rel = 'noopener noreferrer';
|
||||
|
||||
if (link.icon) {
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.className = 'script-icon';
|
||||
iconEl.setAttribute('data-mdi-icon', link.icon);
|
||||
card.appendChild(iconEl);
|
||||
}
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'script-label';
|
||||
label.textContent = link.label || link.name;
|
||||
card.appendChild(label);
|
||||
|
||||
if (link.description) {
|
||||
const desc = document.createElement('div');
|
||||
desc.className = 'script-description';
|
||||
desc.textContent = link.description;
|
||||
card.appendChild(desc);
|
||||
}
|
||||
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'script-label';
|
||||
label.textContent = script.label || script.name;
|
||||
|
||||
button.appendChild(label);
|
||||
|
||||
if (script.description) {
|
||||
const description = document.createElement('div');
|
||||
description.className = 'script-description';
|
||||
description.textContent = script.description;
|
||||
button.appendChild(description);
|
||||
}
|
||||
|
||||
grid.appendChild(button);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (gen !== _quickAccessGen) return;
|
||||
console.warn('Failed to load links for quick access:', e);
|
||||
}
|
||||
|
||||
// Add "+" card at the end
|
||||
const addCard = document.createElement('div');
|
||||
addCard.className = 'script-btn add-card-grid';
|
||||
addCard.onclick = () => showAddScriptDialog();
|
||||
addCard.innerHTML = '<span class="add-card-icon">+</span>';
|
||||
grid.appendChild(addCard);
|
||||
// Show empty state if nothing
|
||||
if (!hasScripts && !hasLinks) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'scripts-empty empty-state-illustration';
|
||||
empty.innerHTML = `<svg viewBox="0 0 64 64"><path d="M20 8l-8 48"/><path d="M44 8l8 48"/><path d="M10 24h44"/><path d="M8 40h44"/></svg><p>${t('quick_access.no_items')}</p>`;
|
||||
fragment.prepend(empty);
|
||||
}
|
||||
|
||||
// Replace grid content atomically
|
||||
grid.innerHTML = '';
|
||||
grid.appendChild(fragment);
|
||||
|
||||
// Resolve MDI icons
|
||||
resolveMdiIcons(grid);
|
||||
@@ -3192,6 +3257,7 @@ async function showEditLinkDialog(linkName) {
|
||||
document.getElementById('linkUrl').value = link.url;
|
||||
document.getElementById('linkIcon').value = link.icon || '';
|
||||
document.getElementById('linkLabel').value = link.label || '';
|
||||
document.getElementById('linkDescription').value = link.description || '';
|
||||
|
||||
// Update icon preview
|
||||
const preview = document.getElementById('linkIconPreview');
|
||||
@@ -3241,7 +3307,8 @@ async function saveLink(event) {
|
||||
const data = {
|
||||
url: document.getElementById('linkUrl').value,
|
||||
icon: document.getElementById('linkIcon').value || 'mdi:link',
|
||||
label: document.getElementById('linkLabel').value || ''
|
||||
label: document.getElementById('linkLabel').value || '',
|
||||
description: document.getElementById('linkDescription').value || ''
|
||||
};
|
||||
|
||||
const endpoint = isEdit ?
|
||||
|
||||
Reference in New Issue
Block a user