diff --git a/media_server/static/js/app.js b/media_server/static/js/app.js
index 7a65037..65617a1 100644
--- a/media_server/static/js/app.js
+++ b/media_server/static/js/app.js
@@ -57,6 +57,7 @@ import {
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
downloadFile, closeFolderDialog, saveFolder,
showManageFoldersDialog,
+ showAddFolderDialog, showEditFolderDialog, deleteFolderConfirm,
} from './browser.js';
import {
@@ -117,6 +118,7 @@ Object.assign(window, {
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
downloadFile, closeFolderDialog, saveFolder,
showManageFoldersDialog,
+ showAddFolderDialog, showEditFolderDialog, deleteFolderConfirm,
// Links
showAddLinkDialog, showEditLinkDialog, closeLinkDialog,
saveLink, deleteLinkConfirm,
@@ -323,6 +325,24 @@ window.addEventListener('DOMContentLoaded', async () => {
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
const linkDialog = document.getElementById('linkDialog');
linkDialog.addEventListener('click', (e) => {
@@ -352,7 +372,7 @@ window.addEventListener('DOMContentLoaded', async () => {
// Initialize browser toolbar and load folders
initBrowserToolbar();
- if (token) {
+ if (!authReq || token) {
loadMediaFolders();
}
diff --git a/media_server/static/js/browser.js b/media_server/static/js/browser.js
index d115a3d..5b06045 100644
--- a/media_server/static/js/browser.js
+++ b/media_server/static/js/browser.js
@@ -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 = `
\u{1F4C1}
-
${folder.label}
+
${escapeHtml(folder.label)}${unavailable ? ' ' + t('browser.unavailable') + '' : ''}
`;
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 = `
-
${folder.label}
+
${escapeHtml(folder.label)}
+ ${unavailable ? '
' + t('browser.unavailable') + '
' : ''}
`;
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 = `
+
+
+ ${t('browser.folders_empty')}
+ |
`;
+ return;
+ }
+
+ tbody.innerHTML = entries.map(([id, folder]) => {
+ const available = folder.available !== false;
+ const statusIcon = available
+ ? '
'
+ : '
';
+ const enabledBadge = folder.enabled
+ ? ''
+ : '
' + t('browser.folder_disabled') + '';
+ return `
+ | ${escapeHtml(id)}${enabledBadge} |
+ ${escapeHtml(folder.label)} |
+ ${escapeHtml(folder.path)} |
+ ${statusIcon} |
+
+
+
+ |
+
`;
+ }).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');
+ }
}
diff --git a/media_server/static/locales/en.json b/media_server/static/locales/en.json
index 88876d6..4d5eb40 100644
--- a/media_server/static/locales/en.json
+++ b/media_server/static/locales/en.json
@@ -173,7 +173,27 @@
"browser.play_all_error": "Failed to play folder",
"browser.error_loading": "Error loading directory",
"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_edit": "Edit Media Folder",
"browser.folder_dialog.folder_id": "Folder ID *",
diff --git a/media_server/static/locales/ru.json b/media_server/static/locales/ru.json
index 844c330..6ff9e8e 100644
--- a/media_server/static/locales/ru.json
+++ b/media_server/static/locales/ru.json
@@ -173,7 +173,27 @@
"browser.play_all_error": "Не удалось воспроизвести папку",
"browser.error_loading": "Ошибка загрузки каталога",
"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_edit": "Редактировать медиа папку",
"browser.folder_dialog.folder_id": "ID папки *",