Add Play All, home navigation, and UI improvements
- Add Play All button with M3U playlist generation (local temp file with absolute paths) - Replace folder combobox with root folder cards and home icon breadcrumb - Fix compact grid card sizing (64x64 thumbnails, align-items: start) - Add loading spinner when browsing folders - Cache browse items to avoid re-fetching on view mode switch - Remove unused browser-controls CSS - Add localization keys for Play All and Home (en/ru) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1410,6 +1410,7 @@ let itemsPerPage = parseInt(localStorage.getItem('mediaBrowser.itemsPerPage')) |
|
||||
let totalItems = 0;
|
||||
let mediaFolders = {};
|
||||
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
||||
let cachedItems = null;
|
||||
|
||||
// Load media folders on page load
|
||||
async function loadMediaFolders() {
|
||||
@@ -1427,9 +1428,8 @@ async function loadMediaFolders() {
|
||||
if (!response.ok) throw new Error('Failed to load folders');
|
||||
|
||||
mediaFolders = await response.json();
|
||||
renderFolderSelect();
|
||||
|
||||
// Load last browsed path
|
||||
// Load last browsed path or show root folder list
|
||||
loadLastBrowserPath();
|
||||
} catch (error) {
|
||||
console.error('Error loading media folders:', error);
|
||||
@@ -1437,33 +1437,69 @@ async function loadMediaFolders() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderFolderSelect() {
|
||||
const select = document.getElementById('folderSelect');
|
||||
select.innerHTML = `<option value="" data-i18n="browser.select_folder_option">${t('browser.select_folder_option')}</option>`;
|
||||
function showRootFolders() {
|
||||
currentFolderId = '';
|
||||
currentPath = '';
|
||||
currentOffset = 0;
|
||||
|
||||
// Render breadcrumb with just "Home" (not clickable at root)
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
breadcrumb.innerHTML = '';
|
||||
const root = document.createElement('span');
|
||||
root.className = 'breadcrumb-item breadcrumb-home';
|
||||
root.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>';
|
||||
breadcrumb.appendChild(root);
|
||||
|
||||
// Hide play all button and pagination
|
||||
document.getElementById('playAllBtn').style.display = 'none';
|
||||
document.getElementById('browserPagination').style.display = 'none';
|
||||
|
||||
// Render folders as grid cards
|
||||
const container = document.getElementById('browserGrid');
|
||||
if (viewMode === 'list') {
|
||||
container.className = 'browser-list';
|
||||
} else if (viewMode === 'compact') {
|
||||
container.className = 'browser-grid browser-grid-compact';
|
||||
} else {
|
||||
container.className = 'browser-grid';
|
||||
}
|
||||
container.innerHTML = '';
|
||||
|
||||
Object.entries(mediaFolders).forEach(([id, folder]) => {
|
||||
if (folder.enabled) {
|
||||
const option = document.createElement('option');
|
||||
option.value = id;
|
||||
option.textContent = folder.label;
|
||||
select.appendChild(option);
|
||||
if (!folder.enabled) return;
|
||||
|
||||
if (viewMode === 'list') {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'browser-list-item';
|
||||
row.onclick = () => {
|
||||
currentFolderId = id;
|
||||
browsePath(id, '');
|
||||
};
|
||||
row.innerHTML = `
|
||||
<div class="browser-list-icon">📁</div>
|
||||
<div class="browser-list-name">${folder.label}</div>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
} else {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'browser-item';
|
||||
card.onclick = () => {
|
||||
currentFolderId = id;
|
||||
browsePath(id, '');
|
||||
};
|
||||
card.innerHTML = `
|
||||
<div class="browser-thumb-wrapper">
|
||||
<div class="browser-icon">📁</div>
|
||||
</div>
|
||||
<div class="browser-item-info">
|
||||
<div class="browser-item-name">${folder.label}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onFolderSelected() {
|
||||
const select = document.getElementById('folderSelect');
|
||||
currentFolderId = select.value;
|
||||
|
||||
if (currentFolderId) {
|
||||
currentPath = '';
|
||||
currentOffset = 0;
|
||||
browsePath(currentFolderId, currentPath);
|
||||
} else {
|
||||
clearBrowserGrid();
|
||||
}
|
||||
}
|
||||
|
||||
async function browsePath(folderId, path, offset = 0) {
|
||||
try {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
@@ -1472,6 +1508,11 @@ async function browsePath(folderId, path, offset = 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner
|
||||
const container = document.getElementById('browserGrid');
|
||||
container.className = 'browser-grid';
|
||||
container.innerHTML = '<div class="browser-loading"><div class="loading-spinner"></div></div>';
|
||||
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
const response = await fetch(
|
||||
`/api/browser/browse?folder_id=${folderId}&path=${encodedPath}&offset=${offset}&limit=${itemsPerPage}`,
|
||||
@@ -1485,10 +1526,15 @@ async function browsePath(folderId, path, offset = 0) {
|
||||
currentOffset = offset;
|
||||
totalItems = data.total;
|
||||
|
||||
cachedItems = data.items;
|
||||
renderBreadcrumbs(data.current_path, data.parent_path);
|
||||
renderBrowserItems(data.items);
|
||||
renderBrowserItems(cachedItems);
|
||||
renderPagination();
|
||||
|
||||
// Show/hide Play All button based on whether media items exist
|
||||
const hasMedia = data.items.some(item => item.is_media);
|
||||
document.getElementById('playAllBtn').style.display = hasMedia ? '' : 'none';
|
||||
|
||||
// Save last path
|
||||
saveLastBrowserPath(folderId, currentPath);
|
||||
} catch (error) {
|
||||
@@ -1502,17 +1548,29 @@ function renderBreadcrumbs(currentPath, parentPath) {
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
breadcrumb.innerHTML = '';
|
||||
|
||||
if (!currentPath || currentPath === '/') return;
|
||||
|
||||
const parts = currentPath.split('/').filter(p => p);
|
||||
const parts = (currentPath || '').split('/').filter(p => p);
|
||||
let path = '/';
|
||||
|
||||
// Root
|
||||
const root = document.createElement('span');
|
||||
root.className = 'breadcrumb-item';
|
||||
root.textContent = mediaFolders[currentFolderId]?.label || 'Root';
|
||||
root.onclick = () => browsePath(currentFolderId, '');
|
||||
breadcrumb.appendChild(root);
|
||||
// Home link (back to folder list)
|
||||
const home = document.createElement('span');
|
||||
home.className = 'breadcrumb-item breadcrumb-home';
|
||||
home.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>';
|
||||
home.onclick = () => showRootFolders();
|
||||
breadcrumb.appendChild(home);
|
||||
|
||||
// Separator + Folder name
|
||||
const sep = document.createElement('span');
|
||||
sep.className = 'breadcrumb-separator';
|
||||
sep.textContent = '›';
|
||||
breadcrumb.appendChild(sep);
|
||||
|
||||
const folderItem = document.createElement('span');
|
||||
folderItem.className = 'breadcrumb-item';
|
||||
folderItem.textContent = mediaFolders[currentFolderId]?.label || 'Root';
|
||||
if (parts.length > 0) {
|
||||
folderItem.onclick = () => browsePath(currentFolderId, '');
|
||||
}
|
||||
breadcrumb.appendChild(folderItem);
|
||||
|
||||
// Path parts
|
||||
parts.forEach((part, index) => {
|
||||
@@ -1859,6 +1917,33 @@ async function playMediaFile(fileName) {
|
||||
}
|
||||
}
|
||||
|
||||
async function playAllFolder() {
|
||||
try {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (!token || !currentFolderId) return;
|
||||
|
||||
const response = await fetch('/api/browser/play-folder', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ folder_id: currentFolderId, path: currentPath })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Failed to play folder');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
showToast(t('browser.play_all_success', { count: data.count }), 'success');
|
||||
} catch (error) {
|
||||
console.error('Error playing folder:', error);
|
||||
showToast(t('browser.play_all_error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(fileName, event) {
|
||||
if (event) event.stopPropagation();
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
@@ -1932,9 +2017,11 @@ function setViewMode(mode) {
|
||||
const btnId = mode === 'list' ? 'viewListBtn' : mode === 'compact' ? 'viewCompactBtn' : 'viewGridBtn';
|
||||
document.getElementById(btnId).classList.add('active');
|
||||
|
||||
// Re-render current items if we have a folder selected
|
||||
if (currentFolderId) {
|
||||
browsePath(currentFolderId, currentPath, currentOffset);
|
||||
// Re-render current view from cache (no network request)
|
||||
if (currentFolderId && cachedItems) {
|
||||
renderBrowserItems(cachedItems);
|
||||
} else {
|
||||
showRootFolders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1986,6 +2073,7 @@ function clearBrowserGrid() {
|
||||
grid.innerHTML = `<div class="browser-empty" data-i18n="browser.no_folder_selected">${t('browser.no_folder_selected')}</div>`;
|
||||
document.getElementById('breadcrumb').innerHTML = '';
|
||||
document.getElementById('browserPagination').style.display = 'none';
|
||||
document.getElementById('playAllBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
// LocalStorage for last path
|
||||
@@ -2004,12 +2092,14 @@ function loadLastBrowserPath() {
|
||||
const lastPath = localStorage.getItem('mediaBrowser.lastPath');
|
||||
|
||||
if (lastFolderId && mediaFolders[lastFolderId]) {
|
||||
document.getElementById('folderSelect').value = lastFolderId;
|
||||
currentFolderId = lastFolderId;
|
||||
browsePath(lastFolderId, lastPath || '');
|
||||
} else {
|
||||
showRootFolders();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load last browser path:', e);
|
||||
showRootFolders();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user