Tabbed UI, browse caching, and bottom mini player
- Convert stacked sections to tabbed interface (Player, Browser, Actions, Scripts, Callbacks) with localStorage persistence - Add in-memory directory listing cache (5-min TTL) with nocache bypass for refresh - Defer stat()/duration calls to paginated items only for faster browse - Move mini player from top to bottom with footer padding fix - Always show scrollbar to prevent layout shift between tabs - Add tab localization keys (en/ru) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,52 @@
|
||||
// Tab management
|
||||
let activeTab = 'player';
|
||||
|
||||
function setMiniPlayerVisible(visible) {
|
||||
const miniPlayer = document.getElementById('mini-player');
|
||||
if (visible) {
|
||||
miniPlayer.classList.remove('hidden');
|
||||
document.body.classList.add('mini-player-visible');
|
||||
} else {
|
||||
miniPlayer.classList.add('hidden');
|
||||
document.body.classList.remove('mini-player-visible');
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(tabName) {
|
||||
activeTab = tabName;
|
||||
|
||||
// Hide all tab content
|
||||
document.querySelectorAll('[data-tab-content]').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
el.style.display = '';
|
||||
});
|
||||
|
||||
// Show selected tab content
|
||||
const target = document.querySelector(`[data-tab-content="${tabName}"]`);
|
||||
if (target) {
|
||||
target.classList.add('active');
|
||||
}
|
||||
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
const activeBtn = document.querySelector(`.tab-btn[data-tab="${tabName}"]`);
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('activeTab', tabName);
|
||||
|
||||
// Mini-player: show when not on player tab
|
||||
if (tabName !== 'player') {
|
||||
setMiniPlayerVisible(true);
|
||||
} else {
|
||||
// Restore scroll-based behavior: check if player is in view
|
||||
const playerContainer = document.querySelector('.player-container');
|
||||
const rect = playerContainer.getBoundingClientRect();
|
||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
||||
setMiniPlayerVisible(!inView);
|
||||
}
|
||||
}
|
||||
|
||||
// Theme management
|
||||
function initTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
@@ -259,6 +308,10 @@
|
||||
setTimeout(() => { isUserAdjustingVolume = false; }, 500);
|
||||
});
|
||||
|
||||
// Restore saved tab
|
||||
const savedTab = localStorage.getItem('activeTab') || 'player';
|
||||
switchTab(savedTab);
|
||||
|
||||
// Mini Player: Intersection Observer to show/hide when main player scrolls out of view
|
||||
const playerContainer = document.querySelector('.player-container');
|
||||
const miniPlayer = document.getElementById('mini-player');
|
||||
@@ -271,13 +324,10 @@
|
||||
|
||||
const observerCallback = (entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
// Main player is visible - hide mini player
|
||||
miniPlayer.classList.add('hidden');
|
||||
} else {
|
||||
// Main player is scrolled out of view - show mini player
|
||||
miniPlayer.classList.remove('hidden');
|
||||
}
|
||||
// Only use scroll-based logic when on the player tab
|
||||
if (activeTab !== 'player') return;
|
||||
|
||||
setMiniPlayerVisible(!entry.isIntersecting);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -738,11 +788,10 @@
|
||||
const grid = document.getElementById('scripts-grid');
|
||||
|
||||
if (scripts.length === 0) {
|
||||
container.style.display = 'none';
|
||||
grid.innerHTML = `<div class="scripts-empty">${t('scripts.no_scripts')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.style.display = 'block';
|
||||
grid.innerHTML = '';
|
||||
|
||||
scripts.forEach(script => {
|
||||
@@ -1500,7 +1549,7 @@ function showRootFolders() {
|
||||
});
|
||||
}
|
||||
|
||||
async function browsePath(folderId, path, offset = 0) {
|
||||
async function browsePath(folderId, path, offset = 0, nocache = false) {
|
||||
try {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (!token) {
|
||||
@@ -1514,8 +1563,10 @@ async function browsePath(folderId, path, offset = 0) {
|
||||
container.innerHTML = '<div class="browser-loading"><div class="loading-spinner"></div></div>';
|
||||
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
let url = `/api/browser/browse?folder_id=${folderId}&path=${encodedPath}&offset=${offset}&limit=${itemsPerPage}`;
|
||||
if (nocache) url += '&nocache=true';
|
||||
const response = await fetch(
|
||||
`/api/browser/browse?folder_id=${folderId}&path=${encodedPath}&offset=${offset}&limit=${itemsPerPage}`,
|
||||
url,
|
||||
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
@@ -2015,7 +2066,7 @@ function nextPage() {
|
||||
|
||||
function refreshBrowser() {
|
||||
if (currentFolderId) {
|
||||
browsePath(currentFolderId, currentPath, currentOffset);
|
||||
browsePath(currentFolderId, currentPath, currentOffset, true);
|
||||
} else {
|
||||
loadMediaFolders();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user