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:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
@@ -48,6 +49,13 @@ class PlayRequest(BaseModel):
|
|||||||
path: str = Field(..., description="Full path to the media file")
|
path: str = Field(..., description="Full path to the media file")
|
||||||
|
|
||||||
|
|
||||||
|
class PlayFolderRequest(BaseModel):
|
||||||
|
"""Request model for playing all media files in a folder."""
|
||||||
|
|
||||||
|
folder_id: str = Field(..., description="Media folder ID")
|
||||||
|
path: str = Field(default="", description="Path relative to folder root")
|
||||||
|
|
||||||
|
|
||||||
# Folder Management Endpoints
|
# Folder Management Endpoints
|
||||||
@router.get("/folders")
|
@router.get("/folders")
|
||||||
async def list_folders(_: str = Depends(verify_token)):
|
async def list_folders(_: str = Depends(verify_token)):
|
||||||
@@ -423,6 +431,89 @@ async def play_file(
|
|||||||
raise HTTPException(status_code=500, detail="Failed to play file")
|
raise HTTPException(status_code=500, detail="Failed to play file")
|
||||||
|
|
||||||
|
|
||||||
|
# Play Folder Endpoint (M3U playlist)
|
||||||
|
@router.post("/play-folder")
|
||||||
|
async def play_folder(
|
||||||
|
request: PlayFolderRequest,
|
||||||
|
_: str = Depends(verify_token),
|
||||||
|
):
|
||||||
|
"""Play all media files in a folder by generating an M3U playlist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Play folder request with folder_id and path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Success message with file count.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If folder not found or playback fails.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
decoded_path = unquote(request.path)
|
||||||
|
full_path = BrowserService.validate_path(request.folder_id, decoded_path)
|
||||||
|
|
||||||
|
if not full_path.is_dir():
|
||||||
|
raise HTTPException(status_code=400, detail="Path is not a directory")
|
||||||
|
|
||||||
|
# Collect all media files sorted by name
|
||||||
|
media_files = sorted(
|
||||||
|
[f for f in full_path.iterdir() if f.is_file() and BrowserService.is_media_file(f)],
|
||||||
|
key=lambda f: f.name.lower(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not media_files:
|
||||||
|
raise HTTPException(status_code=404, detail="No media files found in this folder")
|
||||||
|
|
||||||
|
# Generate M3U playlist with absolute paths and EXTINF entries
|
||||||
|
# Written to local temp dir to avoid extra SMB file handle on network shares
|
||||||
|
# Uses utf-8-sig (BOM) so players detect encoding properly
|
||||||
|
m3u_content = "#EXTM3U\r\n"
|
||||||
|
for f in media_files:
|
||||||
|
m3u_content += f"#EXTINF:-1,{f.stem}\r\n"
|
||||||
|
m3u_content += f"{f}\r\n"
|
||||||
|
|
||||||
|
playlist_path = Path(tempfile.gettempdir()) / ".media_server_playlist.m3u"
|
||||||
|
playlist_path.write_text(m3u_content, encoding="utf-8-sig")
|
||||||
|
|
||||||
|
# Open playlist with default player
|
||||||
|
controller = get_media_controller()
|
||||||
|
success = await controller.open_file(playlist_path)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to open playlist")
|
||||||
|
|
||||||
|
# Wait for media player to start
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
# Broadcast status update
|
||||||
|
try:
|
||||||
|
status = await controller.get_status()
|
||||||
|
status_dict = status.model_dump()
|
||||||
|
await ws_manager.broadcast({
|
||||||
|
"type": "status",
|
||||||
|
"data": status_dict
|
||||||
|
})
|
||||||
|
logger.info(f"Broadcasted status after opening playlist with {len(media_files)} files")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to broadcast status after opening playlist: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Playing {len(media_files)} files",
|
||||||
|
"count": len(media_files),
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error playing folder: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to play folder")
|
||||||
|
|
||||||
|
|
||||||
# Download Endpoint
|
# Download Endpoint
|
||||||
@router.get("/download")
|
@router.get("/download")
|
||||||
async def download_file(
|
async def download_file(
|
||||||
|
|||||||
@@ -1130,31 +1130,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser-controls {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-controls select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-controls select:hover {
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-controls select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Breadcrumb Navigation */
|
/* Breadcrumb Navigation */
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1184,6 +1159,15 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumb-home {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-home:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.breadcrumb-separator {
|
.breadcrumb-separator {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
@@ -1243,6 +1227,29 @@
|
|||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.browser-play-all-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
background: var(--accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-play-all-btn:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.items-per-page-label {
|
.items-per-page-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1272,6 +1279,7 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact Grid */
|
/* Compact Grid */
|
||||||
@@ -1432,6 +1440,18 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.browser-loading {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-loading .loading-spinner {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.browser-empty {
|
.browser-empty {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -1620,9 +1640,8 @@
|
|||||||
|
|
||||||
/* Compact grid overrides */
|
/* Compact grid overrides */
|
||||||
.browser-grid-compact .browser-thumb-wrapper {
|
.browser-grid-compact .browser-thumb-wrapper {
|
||||||
width: 100%;
|
width: 64px;
|
||||||
height: auto;
|
height: 64px;
|
||||||
aspect-ratio: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser-grid-compact .browser-play-overlay svg {
|
.browser-grid-compact .browser-play-overlay svg {
|
||||||
|
|||||||
@@ -147,13 +147,6 @@
|
|||||||
<div class="browser-container">
|
<div class="browser-container">
|
||||||
<h2 data-i18n="browser.title">Media Browser</h2>
|
<h2 data-i18n="browser.title">Media Browser</h2>
|
||||||
|
|
||||||
<!-- Folder Selection -->
|
|
||||||
<div class="browser-controls">
|
|
||||||
<select id="folderSelect" onchange="onFolderSelected()" data-i18n-empty="browser.select_folder">
|
|
||||||
<option value="" data-i18n="browser.select_folder_option">Select a folder...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Breadcrumb Navigation -->
|
<!-- Breadcrumb Navigation -->
|
||||||
<div class="breadcrumb" id="breadcrumb"></div>
|
<div class="breadcrumb" id="breadcrumb"></div>
|
||||||
|
|
||||||
@@ -171,6 +164,10 @@
|
|||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="browser-play-all-btn" id="playAllBtn" onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
|
||||||
|
<span data-i18n="browser.play_all">Play All</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="browser-toolbar-right">
|
<div class="browser-toolbar-right">
|
||||||
<label class="items-per-page-label">
|
<label class="items-per-page-label">
|
||||||
|
|||||||
@@ -1410,6 +1410,7 @@ let itemsPerPage = parseInt(localStorage.getItem('mediaBrowser.itemsPerPage')) |
|
|||||||
let totalItems = 0;
|
let totalItems = 0;
|
||||||
let mediaFolders = {};
|
let mediaFolders = {};
|
||||||
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
||||||
|
let cachedItems = null;
|
||||||
|
|
||||||
// Load media folders on page load
|
// Load media folders on page load
|
||||||
async function loadMediaFolders() {
|
async function loadMediaFolders() {
|
||||||
@@ -1427,9 +1428,8 @@ async function loadMediaFolders() {
|
|||||||
if (!response.ok) throw new Error('Failed to load folders');
|
if (!response.ok) throw new Error('Failed to load folders');
|
||||||
|
|
||||||
mediaFolders = await response.json();
|
mediaFolders = await response.json();
|
||||||
renderFolderSelect();
|
|
||||||
|
|
||||||
// Load last browsed path
|
// Load last browsed path or show root folder list
|
||||||
loadLastBrowserPath();
|
loadLastBrowserPath();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading media folders:', error);
|
console.error('Error loading media folders:', error);
|
||||||
@@ -1437,33 +1437,69 @@ async function loadMediaFolders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFolderSelect() {
|
function showRootFolders() {
|
||||||
const select = document.getElementById('folderSelect');
|
currentFolderId = '';
|
||||||
select.innerHTML = `<option value="" data-i18n="browser.select_folder_option">${t('browser.select_folder_option')}</option>`;
|
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]) => {
|
Object.entries(mediaFolders).forEach(([id, folder]) => {
|
||||||
if (folder.enabled) {
|
if (!folder.enabled) return;
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = id;
|
if (viewMode === 'list') {
|
||||||
option.textContent = folder.label;
|
const row = document.createElement('div');
|
||||||
select.appendChild(option);
|
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) {
|
async function browsePath(folderId, path, offset = 0) {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
@@ -1472,6 +1508,11 @@ async function browsePath(folderId, path, offset = 0) {
|
|||||||
return;
|
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 encodedPath = encodeURIComponent(path);
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/browser/browse?folder_id=${folderId}&path=${encodedPath}&offset=${offset}&limit=${itemsPerPage}`,
|
`/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;
|
currentOffset = offset;
|
||||||
totalItems = data.total;
|
totalItems = data.total;
|
||||||
|
|
||||||
|
cachedItems = data.items;
|
||||||
renderBreadcrumbs(data.current_path, data.parent_path);
|
renderBreadcrumbs(data.current_path, data.parent_path);
|
||||||
renderBrowserItems(data.items);
|
renderBrowserItems(cachedItems);
|
||||||
renderPagination();
|
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
|
// Save last path
|
||||||
saveLastBrowserPath(folderId, currentPath);
|
saveLastBrowserPath(folderId, currentPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1502,17 +1548,29 @@ function renderBreadcrumbs(currentPath, parentPath) {
|
|||||||
const breadcrumb = document.getElementById('breadcrumb');
|
const breadcrumb = document.getElementById('breadcrumb');
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
|
|
||||||
if (!currentPath || currentPath === '/') return;
|
const parts = (currentPath || '').split('/').filter(p => p);
|
||||||
|
|
||||||
const parts = currentPath.split('/').filter(p => p);
|
|
||||||
let path = '/';
|
let path = '/';
|
||||||
|
|
||||||
// Root
|
// Home link (back to folder list)
|
||||||
const root = document.createElement('span');
|
const home = document.createElement('span');
|
||||||
root.className = 'breadcrumb-item';
|
home.className = 'breadcrumb-item breadcrumb-home';
|
||||||
root.textContent = mediaFolders[currentFolderId]?.label || 'Root';
|
home.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>';
|
||||||
root.onclick = () => browsePath(currentFolderId, '');
|
home.onclick = () => showRootFolders();
|
||||||
breadcrumb.appendChild(root);
|
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
|
// Path parts
|
||||||
parts.forEach((part, index) => {
|
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) {
|
function downloadFile(fileName, event) {
|
||||||
if (event) event.stopPropagation();
|
if (event) event.stopPropagation();
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
@@ -1932,9 +2017,11 @@ function setViewMode(mode) {
|
|||||||
const btnId = mode === 'list' ? 'viewListBtn' : mode === 'compact' ? 'viewCompactBtn' : 'viewGridBtn';
|
const btnId = mode === 'list' ? 'viewListBtn' : mode === 'compact' ? 'viewCompactBtn' : 'viewGridBtn';
|
||||||
document.getElementById(btnId).classList.add('active');
|
document.getElementById(btnId).classList.add('active');
|
||||||
|
|
||||||
// Re-render current items if we have a folder selected
|
// Re-render current view from cache (no network request)
|
||||||
if (currentFolderId) {
|
if (currentFolderId && cachedItems) {
|
||||||
browsePath(currentFolderId, currentPath, currentOffset);
|
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>`;
|
grid.innerHTML = `<div class="browser-empty" data-i18n="browser.no_folder_selected">${t('browser.no_folder_selected')}</div>`;
|
||||||
document.getElementById('breadcrumb').innerHTML = '';
|
document.getElementById('breadcrumb').innerHTML = '';
|
||||||
document.getElementById('browserPagination').style.display = 'none';
|
document.getElementById('browserPagination').style.display = 'none';
|
||||||
|
document.getElementById('playAllBtn').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalStorage for last path
|
// LocalStorage for last path
|
||||||
@@ -2004,12 +2092,14 @@ function loadLastBrowserPath() {
|
|||||||
const lastPath = localStorage.getItem('mediaBrowser.lastPath');
|
const lastPath = localStorage.getItem('mediaBrowser.lastPath');
|
||||||
|
|
||||||
if (lastFolderId && mediaFolders[lastFolderId]) {
|
if (lastFolderId && mediaFolders[lastFolderId]) {
|
||||||
document.getElementById('folderSelect').value = lastFolderId;
|
|
||||||
currentFolderId = lastFolderId;
|
currentFolderId = lastFolderId;
|
||||||
browsePath(lastFolderId, lastPath || '');
|
browsePath(lastFolderId, lastPath || '');
|
||||||
|
} else {
|
||||||
|
showRootFolders();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load last browser path:', e);
|
console.error('Failed to load last browser path:', e);
|
||||||
|
showRootFolders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
"callbacks.confirm.delete": "Are you sure you want to delete the callback \"{name}\"?",
|
"callbacks.confirm.delete": "Are you sure you want to delete the callback \"{name}\"?",
|
||||||
"callbacks.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?",
|
"callbacks.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?",
|
||||||
"browser.title": "Media Browser",
|
"browser.title": "Media Browser",
|
||||||
|
"browser.home": "Home",
|
||||||
"browser.manage_folders": "Manage Folders",
|
"browser.manage_folders": "Manage Folders",
|
||||||
"browser.select_folder": "Select a folder...",
|
"browser.select_folder": "Select a folder...",
|
||||||
"browser.select_folder_option": "Select a folder...",
|
"browser.select_folder_option": "Select a folder...",
|
||||||
@@ -125,6 +126,9 @@
|
|||||||
"browser.download": "Download",
|
"browser.download": "Download",
|
||||||
"browser.play_success": "Playing {filename}",
|
"browser.play_success": "Playing {filename}",
|
||||||
"browser.play_error": "Failed to play file",
|
"browser.play_error": "Failed to play file",
|
||||||
|
"browser.play_all": "Play All",
|
||||||
|
"browser.play_all_success": "Playing {count} files",
|
||||||
|
"browser.play_all_error": "Failed to play folder",
|
||||||
"browser.error_loading": "Error loading directory",
|
"browser.error_loading": "Error loading directory",
|
||||||
"browser.error_loading_folders": "Failed to load media folders",
|
"browser.error_loading_folders": "Failed to load media folders",
|
||||||
"browser.manage_folders_hint": "Folder management coming soon! For now, edit config.yaml to add media folders.",
|
"browser.manage_folders_hint": "Folder management coming soon! For now, edit config.yaml to add media folders.",
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
"callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?",
|
"callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?",
|
||||||
"callbacks.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
"callbacks.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
||||||
"browser.title": "Медиа Браузер",
|
"browser.title": "Медиа Браузер",
|
||||||
|
"browser.home": "Главная",
|
||||||
"browser.manage_folders": "Управление папками",
|
"browser.manage_folders": "Управление папками",
|
||||||
"browser.select_folder": "Выберите папку...",
|
"browser.select_folder": "Выберите папку...",
|
||||||
"browser.select_folder_option": "Выберите папку...",
|
"browser.select_folder_option": "Выберите папку...",
|
||||||
@@ -125,6 +126,9 @@
|
|||||||
"browser.download": "Скачать",
|
"browser.download": "Скачать",
|
||||||
"browser.play_success": "Воспроизведение {filename}",
|
"browser.play_success": "Воспроизведение {filename}",
|
||||||
"browser.play_error": "Не удалось воспроизвести файл",
|
"browser.play_error": "Не удалось воспроизвести файл",
|
||||||
|
"browser.play_all": "Воспроизвести все",
|
||||||
|
"browser.play_all_success": "Воспроизведение {count} файлов",
|
||||||
|
"browser.play_all_error": "Не удалось воспроизвести папку",
|
||||||
"browser.error_loading": "Ошибка загрузки каталога",
|
"browser.error_loading": "Ошибка загрузки каталога",
|
||||||
"browser.error_loading_folders": "Не удалось загрузить медиа папки",
|
"browser.error_loading_folders": "Не удалось загрузить медиа папки",
|
||||||
"browser.manage_folders_hint": "Управление папками скоро появится! Пока редактируйте config.yaml для добавления медиа папок.",
|
"browser.manage_folders_hint": "Управление папками скоро появится! Пока редактируйте config.yaml для добавления медиа папок.",
|
||||||
|
|||||||
Reference in New Issue
Block a user