diff --git a/templates/admin_library.templ b/templates/admin_library.templ index bd64ae2..3bd5b56 100644 --- a/templates/admin_library.templ +++ b/templates/admin_library.templ @@ -122,6 +122,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
+
@@ -146,6 +147,19 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
+ + + diff --git a/web/src/library.ts b/web/src/library.ts index f775155..6ceba84 100644 --- a/web/src/library.ts +++ b/web/src/library.ts @@ -18,6 +18,10 @@ interface LibraryFolder { created_at: string; } +interface DeleteFolderRequest { + folder_path: string; +} + interface User { id: string; username: string; @@ -147,6 +151,9 @@ async function handleCreateLibrarySubmit(event: Event): Promise { const form = event.target as HTMLFormElement; const formData = new FormData(form); + const libraryId = (document.getElementById('library-id') as HTMLInputElement)?.value; + const isEdit = !!libraryId; + const libraryData = { name: formData.get('name') as string, description: formData.get('description') as string, @@ -154,18 +161,32 @@ async function handleCreateLibrarySubmit(event: Event): Promise { }; try { - const response = await (window as any).api.post('/libraries', libraryData); - await (window as any).api.handleResponse(response) as { data: Library }; + const url = isEdit ? `/libraries/${libraryId}` : '/libraries'; + const method = isEdit ? 'put' : 'post'; + + const response = await (window as any).api[method](url, libraryData); + + if (isEdit) { + await (window as any).api.handleVoidResponse(response); + } else { + await (window as any).api.handleResponse(response) as { data: Library }; + } if ((window as any).showToast?.success) { - (window as any).showToast.success('Library created successfully'); + (window as any).showToast.success(isEdit ? 'Library updated successfully' : 'Library created successfully'); } hideCreateLibraryModal(); form.reset(); + + const libraryIdInput = document.getElementById('library-id') as HTMLInputElement; + if (libraryIdInput) { + libraryIdInput.value = ''; + } + void reloadLibraries(); } catch (error) { - (window as any).api.handleError(error, 'Failed to create library'); + (window as any).api.handleError(error, isEdit ? 'Failed to update library' : 'Failed to create library'); } } @@ -174,7 +195,18 @@ async function deleteLibrary(libraryId: string): Promise { const library = libraries.find(l => l.id === libraryId); if (!library) return; - if (!confirm(`Delete library "${library.name}"? This will permanently remove all associated media and cannot be undone.`)) { + const message = `Are you sure you want to delete "${library.name}"? + +This will remove: +• Library metadata from the database +• All folder references +• All book records from the database + +⚠️ Book files on disk will NOT be deleted. + +This action cannot be undone.`; + + if (!confirm(message)) { return; } @@ -197,33 +229,30 @@ async function showLibraryFolders(libraryId: string): Promise { const container = document.getElementById(`library-folders-${libraryId}`); if (!container) return; - if (container.classList.contains('hidden')) { - try { - const response = await (window as any).api.get(`/libraries/${libraryId}/folders`); - const folders = await (window as any).api.handleResponse(response) as LibraryFolder[]; + try { + const response = await (window as any).api.get(`/libraries/${libraryId}/folders`); + const folders = await (window as any).api.handleResponse(response) as LibraryFolder[]; - container.innerHTML = folders.map((folder: LibraryFolder) => - '
' + - `${escapeHtmlLocal(folder.folder_path)}` + - `' + - '
' - ).join(''); + container.innerHTML = folders.map((folder: LibraryFolder) => + '
' + + `${escapeHtmlLocal(folder.folder_path)}` + + `' + + '
' + ).join(''); - // Add folder input - container.innerHTML += '
' + - `' + - `' + - '
'; + container.innerHTML += '
' + + `' + + `' + + `' + + '
'; - container.classList.remove('hidden'); - } catch (error) { - (window as any).api.handleError(error, 'Failed to load folders'); - } - } else { - container.classList.add('hidden'); + container.classList.remove('hidden'); + } catch (error) { + (window as any).api.handleError(error, 'Failed to load folders'); } } @@ -262,7 +291,10 @@ async function removeLibraryFolder(libraryId: string, folderPath: string): Promi } try { - const response = await (window as any).api.delete(`/libraries/${libraryId}/folders`); + const response = await (window as any).api.delete( + `/libraries/${libraryId}/folders`, + { folder_path: folderPath } + ); await (window as any).api.handleVoidResponse(response); if ((window as any).showToast?.success) { @@ -277,10 +309,36 @@ async function removeLibraryFolder(libraryId: string, folderPath: string): Promi // Edit library (placeholder - opens modal or navigates to edit page) function editLibrary(libraryId: string): void { - console.log('Edit library:', libraryId); - if ((window as any).showToast?.info) { - (window as any).showToast.info('Edit library functionality coming soon'); + const library = libraries.find(l => l.id === libraryId); + if (!library) { + if ((window as any).showToast?.error) { + (window as any).showToast.error('Library not found'); + } + return; } + + const form = document.getElementById('create-library-form') as HTMLFormElement; + if (form) { + const nameInput = form.querySelector('[name="name"]') as HTMLInputElement; + const descInput = form.querySelector('[name="description"]') as HTMLTextAreaElement; + const typeInput = form.querySelector('[name="type"]') as HTMLSelectElement; + + if (nameInput) nameInput.value = library.name; + if (descInput) descInput.value = library.description || ''; + if (typeInput) typeInput.value = library.library_type_id; + } + + const modalTitle = document.querySelector('#create-library-modal h2'); + if (modalTitle) { + modalTitle.textContent = 'Edit Library'; + } + + const libraryIdInput = document.getElementById('library-id') as HTMLInputElement; + if (libraryIdInput) { + libraryIdInput.value = libraryId; + } + + showCreateLibraryModal(); } // Modal controls @@ -288,6 +346,11 @@ function showCreateLibraryModal(): void { const modal = document.getElementById('create-library-modal') as HTMLElement; if (modal) { modal.classList.remove('hidden'); + + const modalTitle = document.querySelector('#create-library-modal h2'); + if (modalTitle) { + modalTitle.textContent = 'Create Library'; + } } } @@ -332,6 +395,37 @@ function handleLibraryListClick(event: Event): void { removeLibraryFolder(libraryId, button.dataset.folderPath); } break; + case 'browse-folder': + if (button.dataset.inputId) showFolderBrowser(button.dataset.inputId); + break; + } +} + +function handleFolderBrowserClick(event: Event): void { + const target = event.target as HTMLElement; + const button = target.closest('button') as HTMLElement; + const div = target.closest('div[data-action]') as HTMLElement; + + if (button) { + const action = button.dataset.action; + const path = button.dataset.path; + + switch (action) { + case 'browse-parent': + if (path) navigateFolderBrowser(path); + break; + case 'browse-cancel': + hideFolderBrowser(); + break; + case 'browse-select': + if (path) selectBrowseFolder(path); + break; + } + } + + if (div && div.dataset.action === 'browse-navigate') { + const path = div.dataset.path; + if (path) navigateFolderBrowser(path); } } @@ -352,6 +446,103 @@ function handleGlobalClick(event: Event): void { } } +// Folder browser state +let currentBrowsePath = ''; +let currentBrowseInputId = ''; + +// Show folder browser modal +function showFolderBrowser(inputId: string): void { + currentBrowseInputId = inputId; + currentBrowsePath = '/'; + + const modal = document.getElementById('folder-browser-modal') as HTMLElement; + if (modal) { + modal.classList.remove('hidden'); + void loadBrowseDirectories(currentBrowsePath); + } +} + +// Load directories for browsing +async function loadBrowseDirectories(path: string): Promise { + try { + const response = await (window as any).api.get(`/libraries/browse?path=${encodeURIComponent(path)}`); + const data = await (window as any).api.handleResponse(response) as { + current_path: string; + parent_path: string; + directories: string[]; + }; + + currentBrowsePath = data.current_path; + renderBrowseDirectories(data); + } catch (error) { + (window as any).api.handleError(error, 'Failed to load directories'); + } +} + +// Render browse directories (uses event delegation via data-action attributes) +function renderBrowseDirectories(data: { current_path: string; parent_path: string; directories: string[] }): void { + const container = document.getElementById('folder-browser-content'); + if (!container) return; + + let html = ` +
+ ${data.parent_path ? + `` + : ''} + ${escapeHtmlLocal(data.current_path)} +
+
+ `; + + if (data.directories.length === 0) { + html += '

No subdirectories

'; + } else { + data.directories.forEach(dir => { + const fullPath = data.current_path === '/' ? `/${dir}` : `${data.current_path}/${dir}`; + html += ` +
+ 📁 ${escapeHtmlLocal(dir)} +
+ `; + }); + } + + html += ` +
+
+ + +
+ `; + + container.innerHTML = html; +} + +// Navigate to subdirectory +function navigateFolderBrowser(path: string): void { + void loadBrowseDirectories(path); +} + +// Select folder and close browser +function selectBrowseFolder(path: string): void { + const input = document.getElementById(currentBrowseInputId) as HTMLInputElement; + if (input) { + input.value = path; + } + hideFolderBrowser(); +} + +// Hide folder browser modal +function hideFolderBrowser(): void { + const modal = document.getElementById('folder-browser-modal') as HTMLElement; + if (modal) { + modal.classList.add('hidden'); + } +} + // Initialize page function initializeLibraryAdmin(): void { // Setup event listeners @@ -360,6 +551,11 @@ function initializeLibraryAdmin(): void { librariesList.addEventListener('click', handleLibraryListClick); } + const folderBrowserModal = document.getElementById('folder-browser-modal'); + if (folderBrowserModal) { + folderBrowserModal.addEventListener('click', handleFolderBrowserClick); + } + document.addEventListener('click', handleGlobalClick); // Setup form submission @@ -385,6 +581,10 @@ function initializeLibraryAdmin(): void { (window as any).loadUserVisibility = loadUserVisibility; (window as any).editLibrary = editLibrary; (window as any).handleCreateLibrarySubmit = handleCreateLibrarySubmit; +(window as any).showFolderBrowser = showFolderBrowser; +(window as any).navigateFolderBrowser = navigateFolderBrowser; +(window as any).selectBrowseFolder = selectBrowseFolder; +(window as any).hideFolderBrowser = hideFolderBrowser; // Initialize on DOM ready if (document.readyState === 'loading') {