- Add media_folders_management config flag (enabled by default) - Guard folder CRUD endpoints with 403 when management disabled - Wire up frontend folder add/edit/delete in Settings tab - Add per-folder availability check (for network shares) - Show unavailable badge on offline folders in browser view - Expose management flag via /api/health endpoint - Add EN/RU locale keys for folder management UI
This commit is contained in:
@@ -56,6 +56,11 @@ scripts:
|
|||||||
timeout: 10
|
timeout: 10
|
||||||
shell: true
|
shell: true
|
||||||
|
|
||||||
|
# Media folder management from Web UI (default: true)
|
||||||
|
# When enabled, media folders can be added, edited, and deleted from the Settings tab.
|
||||||
|
# Set to false to disable folder management from the UI.
|
||||||
|
# media_folders_management: false
|
||||||
|
|
||||||
# Callback scripts (executed after media actions)
|
# Callback scripts (executed after media actions)
|
||||||
# All callbacks are optional - if not defined, the action runs without callback
|
# All callbacks are optional - if not defined, the action runs without callback
|
||||||
callbacks:
|
callbacks:
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ class Settings(BaseSettings):
|
|||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Media folders available for browsing in the media browser",
|
description="Media folders available for browsing in the media browser",
|
||||||
)
|
)
|
||||||
|
media_folders_management: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Allow adding, editing, and deleting media folders from the Web UI",
|
||||||
|
)
|
||||||
|
|
||||||
# Thumbnail settings
|
# Thumbnail settings
|
||||||
thumbnail_size: str = Field(
|
thumbnail_size: str = Field(
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(prefix="/api/browser", tags=["browser"])
|
router = APIRouter(prefix="/api/browser", tags=["browser"])
|
||||||
|
|
||||||
|
|
||||||
|
def _require_folder_management() -> None:
|
||||||
|
"""Raise 403 if media folder management is disabled in config."""
|
||||||
|
if not settings.media_folders_management:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Media folder management is disabled. Set media_folders_management: true in config.yaml to enable.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _broadcast_after_open(controller, label: str, max_wait: float = 2.0) -> None:
|
async def _broadcast_after_open(controller, label: str, max_wait: float = 2.0) -> None:
|
||||||
"""Poll until media session registers, then broadcast status update.
|
"""Poll until media session registers, then broadcast status update.
|
||||||
|
|
||||||
@@ -83,17 +92,22 @@ async def list_folders(_: str = Depends(verify_token)):
|
|||||||
"""List all configured media folders.
|
"""List all configured media folders.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary of folder configurations.
|
Dictionary with folder configurations and management flag.
|
||||||
"""
|
"""
|
||||||
folders = {}
|
folders = {}
|
||||||
for folder_id, config in settings.media_folders.items():
|
for folder_id, config in settings.media_folders.items():
|
||||||
|
folder_path = Path(config.path)
|
||||||
folders[folder_id] = {
|
folders[folder_id] = {
|
||||||
"id": folder_id,
|
"id": folder_id,
|
||||||
"label": config.label,
|
"label": config.label,
|
||||||
"path": config.path,
|
"path": config.path,
|
||||||
"enabled": config.enabled,
|
"enabled": config.enabled,
|
||||||
|
"available": folder_path.is_dir(),
|
||||||
}
|
}
|
||||||
return folders
|
return {
|
||||||
|
"folders": folders,
|
||||||
|
"management_enabled": settings.media_folders_management,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/folders/create")
|
@router.post("/folders/create")
|
||||||
@@ -112,6 +126,7 @@ async def create_folder(
|
|||||||
Raises:
|
Raises:
|
||||||
HTTPException: If folder already exists or validation fails.
|
HTTPException: If folder already exists or validation fails.
|
||||||
"""
|
"""
|
||||||
|
_require_folder_management()
|
||||||
try:
|
try:
|
||||||
# Validate folder_id format (alphanumeric and underscore only)
|
# Validate folder_id format (alphanumeric and underscore only)
|
||||||
if not request.folder_id.replace("_", "").isalnum():
|
if not request.folder_id.replace("_", "").isalnum():
|
||||||
@@ -169,6 +184,7 @@ async def update_folder(
|
|||||||
Raises:
|
Raises:
|
||||||
HTTPException: If folder doesn't exist or validation fails.
|
HTTPException: If folder doesn't exist or validation fails.
|
||||||
"""
|
"""
|
||||||
|
_require_folder_management()
|
||||||
try:
|
try:
|
||||||
# Validate path exists
|
# Validate path exists
|
||||||
path = Path(request.path)
|
path = Path(request.path)
|
||||||
@@ -217,6 +233,7 @@ async def delete_folder(
|
|||||||
Raises:
|
Raises:
|
||||||
HTTPException: If folder doesn't exist.
|
HTTPException: If folder doesn't exist.
|
||||||
"""
|
"""
|
||||||
|
_require_folder_management()
|
||||||
try:
|
try:
|
||||||
config_manager.delete_media_folder(folder_id)
|
config_manager.delete_media_folder(folder_id)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Request
|
|||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..auth import auth_enabled
|
from ..auth import auth_enabled
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["health"])
|
router = APIRouter(prefix="/api", tags=["health"])
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ async def health_check(request: Request) -> dict[str, Any]:
|
|||||||
"platform": platform.system(),
|
"platform": platform.system(),
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"auth_required": auth_enabled(),
|
"auth_required": auth_enabled(),
|
||||||
|
"media_folders_management": settings.media_folders_management,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Include cached update info if available
|
# Include cached update info if available
|
||||||
|
|||||||
@@ -199,10 +199,48 @@ h1 {
|
|||||||
transition: background 0.3s;
|
transition: background 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.connected {
|
.status-dot.connected,
|
||||||
|
.status-dot.status-online {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-dot.status-offline {
|
||||||
|
background: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Folder management */
|
||||||
|
.folder-unavailable-badge,
|
||||||
|
.folder-disabled-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-unavailable-badge {
|
||||||
|
background: color-mix(in srgb, var(--error) 20%, transparent);
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-disabled-badge {
|
||||||
|
background: color-mix(in srgb, var(--text-secondary) 20%, transparent);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-item.unavailable,
|
||||||
|
.browser-list-item.unavailable {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-cell {
|
||||||
|
max-width: 250px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.header-toolbar {
|
.header-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -323,6 +323,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details class="settings-section" open id="mediaFoldersSection" style="display: none;">
|
||||||
|
<summary data-i18n="settings.section.media_folders">Media Folders</summary>
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<p class="settings-section-description" data-i18n="browser.folders_description">
|
||||||
|
Media folders available for browsing. Folders on network shares show availability status.
|
||||||
|
</p>
|
||||||
|
<table class="scripts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-i18n="browser.folders_table.id">ID</th>
|
||||||
|
<th data-i18n="browser.folders_table.label">Label</th>
|
||||||
|
<th data-i18n="browser.folders_table.path">Path</th>
|
||||||
|
<th data-i18n="browser.folders_table.status">Status</th>
|
||||||
|
<th data-i18n="browser.folders_table.actions">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="foldersTableBody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="empty-state">
|
||||||
|
<div class="empty-state-illustration">
|
||||||
|
<svg viewBox="0 0 64 64"><path d="M8 16h20l4-6h20a4 4 0 014 4v36a4 4 0 01-4 4H8a4 4 0 01-4-4V20a4 4 0 014-4z"/><path d="M4 24h56" stroke-dasharray="4 3" opacity="0.4"/></svg>
|
||||||
|
<p data-i18n="browser.folders_empty">No media folders configured. Click "+" to add one.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="add-card" onclick="showAddFolderDialog()">
|
||||||
|
<span class="add-card-icon">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details class="settings-section" open>
|
<details class="settings-section" open>
|
||||||
<summary data-i18n="settings.section.scripts">Scripts</summary>
|
<summary data-i18n="settings.section.scripts">Scripts</summary>
|
||||||
<div class="settings-section-content">
|
<div class="settings-section-content">
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
|
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
|
||||||
downloadFile, closeFolderDialog, saveFolder,
|
downloadFile, closeFolderDialog, saveFolder,
|
||||||
showManageFoldersDialog,
|
showManageFoldersDialog,
|
||||||
|
showAddFolderDialog, showEditFolderDialog, deleteFolderConfirm,
|
||||||
} from './browser.js';
|
} from './browser.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -117,6 +118,7 @@ Object.assign(window, {
|
|||||||
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
|
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
|
||||||
downloadFile, closeFolderDialog, saveFolder,
|
downloadFile, closeFolderDialog, saveFolder,
|
||||||
showManageFoldersDialog,
|
showManageFoldersDialog,
|
||||||
|
showAddFolderDialog, showEditFolderDialog, deleteFolderConfirm,
|
||||||
// Links
|
// Links
|
||||||
showAddLinkDialog, showEditLinkDialog, closeLinkDialog,
|
showAddLinkDialog, showEditLinkDialog, closeLinkDialog,
|
||||||
saveLink, deleteLinkConfirm,
|
saveLink, deleteLinkConfirm,
|
||||||
@@ -323,6 +325,24 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
else if (action === 'delete') deleteCallbackConfirm(name);
|
else if (action === 'delete') deleteCallbackConfirm(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Folder dialog backdrop click to close
|
||||||
|
const folderDialog = document.getElementById('folderDialog');
|
||||||
|
folderDialog.addEventListener('click', (e) => {
|
||||||
|
if (e.target === folderDialog) {
|
||||||
|
closeFolderDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delegated click handlers for folder table actions
|
||||||
|
document.getElementById('foldersTableBody').addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('[data-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
const action = btn.dataset.action;
|
||||||
|
const folderId = btn.dataset.folderId;
|
||||||
|
if (action === 'edit') showEditFolderDialog(folderId);
|
||||||
|
else if (action === 'delete') deleteFolderConfirm(folderId);
|
||||||
|
});
|
||||||
|
|
||||||
// Link dialog backdrop click to close
|
// Link dialog backdrop click to close
|
||||||
const linkDialog = document.getElementById('linkDialog');
|
const linkDialog = document.getElementById('linkDialog');
|
||||||
linkDialog.addEventListener('click', (e) => {
|
linkDialog.addEventListener('click', (e) => {
|
||||||
@@ -352,7 +372,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
// Initialize browser toolbar and load folders
|
// Initialize browser toolbar and load folders
|
||||||
initBrowserToolbar();
|
initBrowserToolbar();
|
||||||
if (token) {
|
if (!authReq || token) {
|
||||||
loadMediaFolders();
|
loadMediaFolders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import {
|
import {
|
||||||
t, showToast, escapeHtml, closeDialog,
|
t, showToast, showConfirm, escapeHtml, closeDialog,
|
||||||
SEARCH_DEBOUNCE_MS, EMPTY_SVG_FILE, EMPTY_SVG_FOLDER, emptyStateHtml,
|
SEARCH_DEBOUNCE_MS, EMPTY_SVG_FILE, EMPTY_SVG_FOLDER, emptyStateHtml,
|
||||||
getAuthHeaders, hasCredentials,
|
getAuthHeaders, hasCredentials,
|
||||||
} from './core.js';
|
} from './core.js';
|
||||||
@@ -15,6 +15,7 @@ let currentOffset = 0;
|
|||||||
let itemsPerPage = parseInt(localStorage.getItem('mediaBrowser.itemsPerPage')) || 100;
|
let itemsPerPage = parseInt(localStorage.getItem('mediaBrowser.itemsPerPage')) || 100;
|
||||||
let totalItems = 0;
|
let totalItems = 0;
|
||||||
let mediaFolders = {};
|
let mediaFolders = {};
|
||||||
|
let managementEnabled = false;
|
||||||
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
||||||
let cachedItems = null;
|
let cachedItems = null;
|
||||||
let browserSearchTerm = '';
|
let browserSearchTerm = '';
|
||||||
@@ -33,7 +34,20 @@ export 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();
|
const data = await response.json();
|
||||||
|
mediaFolders = data.folders || {};
|
||||||
|
managementEnabled = data.management_enabled || false;
|
||||||
|
|
||||||
|
// Show/hide the media folders settings section
|
||||||
|
const section = document.getElementById('mediaFoldersSection');
|
||||||
|
if (section) {
|
||||||
|
section.style.display = managementEnabled ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render folders table in settings if management is enabled
|
||||||
|
if (managementEnabled) {
|
||||||
|
loadFoldersTable();
|
||||||
|
}
|
||||||
|
|
||||||
// Load last browsed path or show root folder list
|
// Load last browsed path or show root folder list
|
||||||
loadLastBrowserPath();
|
loadLastBrowserPath();
|
||||||
@@ -78,32 +92,39 @@ function showRootFolders() {
|
|||||||
|
|
||||||
Object.entries(mediaFolders).forEach(([id, folder]) => {
|
Object.entries(mediaFolders).forEach(([id, folder]) => {
|
||||||
if (!folder.enabled) return;
|
if (!folder.enabled) return;
|
||||||
|
const unavailable = folder.available === false;
|
||||||
|
const unavailableClass = unavailable ? ' unavailable' : '';
|
||||||
|
|
||||||
if (viewMode === 'list') {
|
if (viewMode === 'list') {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'browser-list-item';
|
row.className = 'browser-list-item' + unavailableClass;
|
||||||
row.onclick = () => {
|
if (!unavailable) {
|
||||||
currentFolderId = id;
|
row.onclick = () => {
|
||||||
browsePath(id, '');
|
currentFolderId = id;
|
||||||
};
|
browsePath(id, '');
|
||||||
|
};
|
||||||
|
}
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="browser-list-icon">\u{1F4C1}</div>
|
<div class="browser-list-icon">\u{1F4C1}</div>
|
||||||
<div class="browser-list-name">${folder.label}</div>
|
<div class="browser-list-name">${escapeHtml(folder.label)}${unavailable ? ' <span class="folder-unavailable-badge">' + t('browser.unavailable') + '</span>' : ''}</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(row);
|
container.appendChild(row);
|
||||||
} else {
|
} else {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'browser-item';
|
card.className = 'browser-item' + unavailableClass;
|
||||||
card.onclick = () => {
|
if (!unavailable) {
|
||||||
currentFolderId = id;
|
card.onclick = () => {
|
||||||
browsePath(id, '');
|
currentFolderId = id;
|
||||||
};
|
browsePath(id, '');
|
||||||
|
};
|
||||||
|
}
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="browser-thumb-wrapper">
|
<div class="browser-thumb-wrapper">
|
||||||
<div class="browser-icon">\u{1F4C1}</div>
|
<div class="browser-icon">\u{1F4C1}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="browser-item-info">
|
<div class="browser-item-info">
|
||||||
<div class="browser-item-name">${folder.label}</div>
|
<div class="browser-item-name">${escapeHtml(folder.label)}</div>
|
||||||
|
${unavailable ? '<div class="browser-item-meta folder-unavailable-badge">' + t('browser.unavailable') + '</div>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(card);
|
container.appendChild(card);
|
||||||
@@ -845,10 +866,72 @@ function loadLastBrowserPath() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Folder Management
|
// Folder Management — Settings table
|
||||||
export function showManageFoldersDialog() {
|
|
||||||
// TODO: Implement folder management UI
|
export function loadFoldersTable() {
|
||||||
showToast(t('browser.manage_folders_hint'), 'info');
|
const tbody = document.getElementById('foldersTableBody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
const entries = Object.entries(mediaFolders);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="5" class="empty-state">
|
||||||
|
<div class="empty-state-illustration">
|
||||||
|
<svg viewBox="0 0 64 64"><path d="M8 16h20l4-6h20a4 4 0 014 4v36a4 4 0 01-4 4H8a4 4 0 01-4-4V20a4 4 0 014-4z"/><path d="M4 24h56" stroke-dasharray="4 3" opacity="0.4"/></svg>
|
||||||
|
<p data-i18n="browser.folders_empty">${t('browser.folders_empty')}</p>
|
||||||
|
</div></td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = entries.map(([id, folder]) => {
|
||||||
|
const available = folder.available !== false;
|
||||||
|
const statusIcon = available
|
||||||
|
? '<span class="status-dot status-online" title="' + t('browser.folder_available') + '"></span>'
|
||||||
|
: '<span class="status-dot status-offline" title="' + t('browser.folder_unavailable') + '"></span>';
|
||||||
|
const enabledBadge = folder.enabled
|
||||||
|
? ''
|
||||||
|
: ' <span class="folder-disabled-badge">' + t('browser.folder_disabled') + '</span>';
|
||||||
|
return `<tr>
|
||||||
|
<td>${escapeHtml(id)}${enabledBadge}</td>
|
||||||
|
<td>${escapeHtml(folder.label)}</td>
|
||||||
|
<td class="path-cell" title="${escapeHtml(folder.path)}">${escapeHtml(folder.path)}</td>
|
||||||
|
<td>${statusIcon}</td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<button class="btn-icon" data-action="edit" data-folder-id="${escapeHtml(id)}" title="${t('browser.folder_edit')}">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon btn-danger-icon" data-action="delete" data-folder-id="${escapeHtml(id)}" title="${t('browser.folder_delete')}">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showAddFolderDialog() {
|
||||||
|
document.getElementById('folderDialogTitle').textContent = t('browser.folder_dialog.title_add');
|
||||||
|
document.getElementById('folderIsEdit').value = '';
|
||||||
|
document.getElementById('folderOriginalId').value = '';
|
||||||
|
document.getElementById('folderId').value = '';
|
||||||
|
document.getElementById('folderId').disabled = false;
|
||||||
|
document.getElementById('folderLabel').value = '';
|
||||||
|
document.getElementById('folderPath').value = '';
|
||||||
|
document.getElementById('folderEnabled').checked = true;
|
||||||
|
document.getElementById('folderDialog').showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showEditFolderDialog(folderId) {
|
||||||
|
const folder = mediaFolders[folderId];
|
||||||
|
if (!folder) return;
|
||||||
|
|
||||||
|
document.getElementById('folderDialogTitle').textContent = t('browser.folder_dialog.title_edit');
|
||||||
|
document.getElementById('folderIsEdit').value = '1';
|
||||||
|
document.getElementById('folderOriginalId').value = folderId;
|
||||||
|
document.getElementById('folderId').value = folderId;
|
||||||
|
document.getElementById('folderId').disabled = true;
|
||||||
|
document.getElementById('folderLabel').value = folder.label;
|
||||||
|
document.getElementById('folderPath').value = folder.path;
|
||||||
|
document.getElementById('folderEnabled').checked = folder.enabled;
|
||||||
|
document.getElementById('folderDialog').showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeFolderDialog() {
|
export function closeFolderDialog() {
|
||||||
@@ -857,5 +940,90 @@ export function closeFolderDialog() {
|
|||||||
|
|
||||||
export async function saveFolder(event) {
|
export async function saveFolder(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
closeFolderDialog();
|
|
||||||
|
const isEdit = document.getElementById('folderIsEdit').value === '1';
|
||||||
|
const folderId = isEdit
|
||||||
|
? document.getElementById('folderOriginalId').value
|
||||||
|
: document.getElementById('folderId').value.trim();
|
||||||
|
const label = document.getElementById('folderLabel').value.trim();
|
||||||
|
const path = document.getElementById('folderPath').value.trim();
|
||||||
|
const enabled = document.getElementById('folderEnabled').checked;
|
||||||
|
|
||||||
|
if (!folderId || !label || !path) return;
|
||||||
|
|
||||||
|
const submitBtn = document.querySelector('#folderForm button[type="submit"]');
|
||||||
|
if (submitBtn) submitBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
if (isEdit) {
|
||||||
|
response = await fetch(`/api/browser/folders/update/${encodeURIComponent(folderId)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
||||||
|
body: JSON.stringify({ label, path, enabled }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response = await fetch('/api/browser/folders/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
||||||
|
body: JSON.stringify({ folder_id: folderId, label, path, enabled }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
closeFolderDialog();
|
||||||
|
showToast(t(isEdit ? 'browser.folder_updated' : 'browser.folder_created'), 'success');
|
||||||
|
await loadMediaFolders();
|
||||||
|
} else {
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
showToast(result.detail || t('browser.folder_save_error'), 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving folder:', error);
|
||||||
|
showToast(t('browser.folder_save_error'), 'error');
|
||||||
|
} finally {
|
||||||
|
if (submitBtn) submitBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFolderConfirm(folderId) {
|
||||||
|
if (!await showConfirm(t('browser.folder_confirm_delete', { name: folderId }))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/browser/folders/delete/${encodeURIComponent(folderId)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showToast(t('browser.folder_deleted'), 'success');
|
||||||
|
await loadMediaFolders();
|
||||||
|
} else {
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
showToast(result.detail || t('browser.folder_delete_error'), 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting folder:', error);
|
||||||
|
showToast(t('browser.folder_delete_error'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy stub — now handled via settings table
|
||||||
|
export function showManageFoldersDialog() {
|
||||||
|
if (managementEnabled) {
|
||||||
|
// Switch to settings tab and scroll to the folders section
|
||||||
|
const switchTabFn = window.switchTab;
|
||||||
|
if (switchTabFn) switchTabFn('settings');
|
||||||
|
setTimeout(() => {
|
||||||
|
const section = document.getElementById('mediaFoldersSection');
|
||||||
|
if (section) {
|
||||||
|
section.setAttribute('open', '');
|
||||||
|
section.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
showToast(t('browser.manage_folders_hint'), 'info');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,27 @@
|
|||||||
"browser.play_all_error": "Failed to play folder",
|
"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 is disabled. Set media_folders_management: true in config.yaml to enable.",
|
||||||
|
"browser.unavailable": "Unavailable",
|
||||||
|
"browser.folder_available": "Available",
|
||||||
|
"browser.folder_unavailable": "Unavailable (path not reachable)",
|
||||||
|
"browser.folder_disabled": "disabled",
|
||||||
|
"browser.folder_edit": "Edit folder",
|
||||||
|
"browser.folder_delete": "Delete folder",
|
||||||
|
"browser.folder_created": "Media folder created successfully",
|
||||||
|
"browser.folder_updated": "Media folder updated successfully",
|
||||||
|
"browser.folder_deleted": "Media folder deleted successfully",
|
||||||
|
"browser.folder_save_error": "Failed to save media folder",
|
||||||
|
"browser.folder_delete_error": "Failed to delete media folder",
|
||||||
|
"browser.folder_confirm_delete": "Are you sure you want to delete the folder \"{name}\"?",
|
||||||
|
"browser.folders_description": "Media folders available for browsing. Folders on network shares show availability status.",
|
||||||
|
"browser.folders_empty": "No media folders configured. Click \"+\" to add one.",
|
||||||
|
"browser.folders_table.id": "ID",
|
||||||
|
"browser.folders_table.label": "Label",
|
||||||
|
"browser.folders_table.path": "Path",
|
||||||
|
"browser.folders_table.status": "Status",
|
||||||
|
"browser.folders_table.actions": "Actions",
|
||||||
|
"settings.section.media_folders": "Media Folders",
|
||||||
"browser.folder_dialog.title_add": "Add Media Folder",
|
"browser.folder_dialog.title_add": "Add Media Folder",
|
||||||
"browser.folder_dialog.title_edit": "Edit Media Folder",
|
"browser.folder_dialog.title_edit": "Edit Media Folder",
|
||||||
"browser.folder_dialog.folder_id": "Folder ID *",
|
"browser.folder_dialog.folder_id": "Folder ID *",
|
||||||
|
|||||||
@@ -173,7 +173,27 @@
|
|||||||
"browser.play_all_error": "Не удалось воспроизвести папку",
|
"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": "Управление папками отключено. Установите media_folders_management: true в config.yaml для включения.",
|
||||||
|
"browser.unavailable": "Недоступна",
|
||||||
|
"browser.folder_available": "Доступна",
|
||||||
|
"browser.folder_unavailable": "Недоступна (путь не найден)",
|
||||||
|
"browser.folder_disabled": "отключена",
|
||||||
|
"browser.folder_edit": "Редактировать папку",
|
||||||
|
"browser.folder_delete": "Удалить папку",
|
||||||
|
"browser.folder_created": "Медиа папка успешно создана",
|
||||||
|
"browser.folder_updated": "Медиа папка успешно обновлена",
|
||||||
|
"browser.folder_deleted": "Медиа папка успешно удалена",
|
||||||
|
"browser.folder_save_error": "Не удалось сохранить медиа папку",
|
||||||
|
"browser.folder_delete_error": "Не удалось удалить медиа папку",
|
||||||
|
"browser.folder_confirm_delete": "Вы уверены, что хотите удалить папку \"{name}\"?",
|
||||||
|
"browser.folders_description": "Медиа папки для просмотра. Для сетевых ресурсов показан статус доступности.",
|
||||||
|
"browser.folders_empty": "Медиа папки не настроены. Нажмите \"+\" для добавления.",
|
||||||
|
"browser.folders_table.id": "ID",
|
||||||
|
"browser.folders_table.label": "Метка",
|
||||||
|
"browser.folders_table.path": "Путь",
|
||||||
|
"browser.folders_table.status": "Статус",
|
||||||
|
"browser.folders_table.actions": "Действия",
|
||||||
|
"settings.section.media_folders": "Медиа папки",
|
||||||
"browser.folder_dialog.title_add": "Добавить медиа папку",
|
"browser.folder_dialog.title_add": "Добавить медиа папку",
|
||||||
"browser.folder_dialog.title_edit": "Редактировать медиа папку",
|
"browser.folder_dialog.title_edit": "Редактировать медиа папку",
|
||||||
"browser.folder_dialog.folder_id": "ID папки *",
|
"browser.folder_dialog.folder_id": "ID папки *",
|
||||||
|
|||||||
Reference in New Issue
Block a user