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:
2026-02-09 02:34:29 +03:00
parent 8db40d3ee9
commit 98a33bca54
7 changed files with 237 additions and 55 deletions

View File

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