- 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:
@@ -3,7 +3,7 @@
|
||||
// ============================================================
|
||||
|
||||
import {
|
||||
t, showToast, escapeHtml, closeDialog,
|
||||
t, showToast, showConfirm, escapeHtml, closeDialog,
|
||||
SEARCH_DEBOUNCE_MS, EMPTY_SVG_FILE, EMPTY_SVG_FOLDER, emptyStateHtml,
|
||||
getAuthHeaders, hasCredentials,
|
||||
} from './core.js';
|
||||
@@ -15,6 +15,7 @@ let currentOffset = 0;
|
||||
let itemsPerPage = parseInt(localStorage.getItem('mediaBrowser.itemsPerPage')) || 100;
|
||||
let totalItems = 0;
|
||||
let mediaFolders = {};
|
||||
let managementEnabled = false;
|
||||
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
||||
let cachedItems = null;
|
||||
let browserSearchTerm = '';
|
||||
@@ -33,7 +34,20 @@ export async function loadMediaFolders() {
|
||||
|
||||
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
|
||||
loadLastBrowserPath();
|
||||
@@ -78,32 +92,39 @@ function showRootFolders() {
|
||||
|
||||
Object.entries(mediaFolders).forEach(([id, folder]) => {
|
||||
if (!folder.enabled) return;
|
||||
const unavailable = folder.available === false;
|
||||
const unavailableClass = unavailable ? ' unavailable' : '';
|
||||
|
||||
if (viewMode === 'list') {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'browser-list-item';
|
||||
row.onclick = () => {
|
||||
currentFolderId = id;
|
||||
browsePath(id, '');
|
||||
};
|
||||
row.className = 'browser-list-item' + unavailableClass;
|
||||
if (!unavailable) {
|
||||
row.onclick = () => {
|
||||
currentFolderId = id;
|
||||
browsePath(id, '');
|
||||
};
|
||||
}
|
||||
row.innerHTML = `
|
||||
<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);
|
||||
} else {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'browser-item';
|
||||
card.onclick = () => {
|
||||
currentFolderId = id;
|
||||
browsePath(id, '');
|
||||
};
|
||||
card.className = 'browser-item' + unavailableClass;
|
||||
if (!unavailable) {
|
||||
card.onclick = () => {
|
||||
currentFolderId = id;
|
||||
browsePath(id, '');
|
||||
};
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="browser-thumb-wrapper">
|
||||
<div class="browser-icon">\u{1F4C1}</div>
|
||||
</div>
|
||||
<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>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
@@ -845,10 +866,72 @@ function loadLastBrowserPath() {
|
||||
}
|
||||
}
|
||||
|
||||
// Folder Management
|
||||
export function showManageFoldersDialog() {
|
||||
// TODO: Implement folder management UI
|
||||
showToast(t('browser.manage_folders_hint'), 'info');
|
||||
// Folder Management — Settings table
|
||||
|
||||
export function loadFoldersTable() {
|
||||
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() {
|
||||
@@ -857,5 +940,90 @@ export function closeFolderDialog() {
|
||||
|
||||
export async function saveFolder(event) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user