feat: Enhance WebDAV file management and UI
- Add functionality to create new collections via API - Implement copy and move operations between collections - Improve image rendering in markdown preview with relative path resolution - Add support for previewing binary files (images, PDFs) - Refactor modal styling to use flat buttons and improve accessibility
This commit is contained in:
		
							
								
								
									
										183
									
								
								static/js/app.js
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								static/js/app.js
									
									
									
									
									
								
							| @@ -208,10 +208,17 @@ async function loadFileFromURL(collection, filePath) { | ||||
|                 await showDirectoryPreview(filePath); | ||||
|                 fileTree.selectAndExpandPath(filePath); | ||||
|             } else if (node) { | ||||
|                 // It's a file, load it | ||||
|                 // It's a file, check if it's binary | ||||
|                 console.log('[loadFileFromURL] Loading file'); | ||||
|                 await editor.loadFile(filePath); | ||||
|                 fileTree.selectAndExpandPath(filePath); | ||||
|  | ||||
|                 // Use the fileTree.onFileSelect callback to handle both text and binary files | ||||
|                 if (fileTree.onFileSelect) { | ||||
|                     fileTree.onFileSelect({ path: filePath, isDirectory: false }); | ||||
|                 } else { | ||||
|                     // Fallback to direct loading | ||||
|                     await editor.loadFile(filePath); | ||||
|                     fileTree.selectAndExpandPath(filePath); | ||||
|                 } | ||||
|             } else { | ||||
|                 console.error(`[loadFileFromURL] Path not found in file tree: ${filePath}`); | ||||
|             } | ||||
| @@ -269,6 +276,37 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|     collectionSelector = new CollectionSelector('collectionSelect', webdavClient); | ||||
|     await collectionSelector.load(); | ||||
|  | ||||
|     // Setup New Collection button | ||||
|     document.getElementById('newCollectionBtn').addEventListener('click', async () => { | ||||
|         try { | ||||
|             const collectionName = await window.ModalManager.prompt( | ||||
|                 'Enter new collection name (lowercase, underscore only):', | ||||
|                 'new_collection' | ||||
|             ); | ||||
|  | ||||
|             if (!collectionName) return; | ||||
|  | ||||
|             // Validate collection name | ||||
|             const validation = ValidationUtils.validateFileName(collectionName, true); | ||||
|             if (!validation.valid) { | ||||
|                 window.showNotification(validation.message, 'warning'); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Create the collection | ||||
|             await webdavClient.createCollection(validation.sanitized); | ||||
|  | ||||
|             // Reload collections and switch to the new one | ||||
|             await collectionSelector.load(); | ||||
|             await collectionSelector.setCollection(validation.sanitized); | ||||
|  | ||||
|             window.showNotification(`Collection "${validation.sanitized}" created`, 'success'); | ||||
|         } catch (error) { | ||||
|             Logger.error('Failed to create collection:', error); | ||||
|             window.showNotification('Failed to create collection', 'error'); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Setup URL routing | ||||
|     setupPopStateListener(); | ||||
|  | ||||
| @@ -281,11 +319,102 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|     fileTree = new FileTree('fileTree', webdavClient); | ||||
|     fileTree.onFileSelect = async (item) => { | ||||
|         try { | ||||
|             const currentCollection = collectionSelector.getCurrentCollection(); | ||||
|  | ||||
|             // Check if the file is a binary/non-editable file | ||||
|             if (PathUtils.isBinaryFile(item.path)) { | ||||
|                 const fileType = PathUtils.getFileType(item.path); | ||||
|                 const fileName = PathUtils.getFileName(item.path); | ||||
|  | ||||
|                 Logger.info(`Previewing binary file: ${item.path}`); | ||||
|  | ||||
|                 // Set flag to prevent auto-update of preview | ||||
|                 editor.isShowingCustomPreview = true; | ||||
|  | ||||
|                 // In edit mode, show a warning notification | ||||
|                 if (isEditMode) { | ||||
|                     if (window.showNotification) { | ||||
|                         window.showNotification( | ||||
|                             `"${fileName}" is read-only. Showing preview only.`, | ||||
|                             'warning' | ||||
|                         ); | ||||
|                     } | ||||
|  | ||||
|                     // Hide the editor pane temporarily | ||||
|                     const editorPane = document.getElementById('editorPane'); | ||||
|                     const resizer1 = document.getElementById('resizer1'); | ||||
|                     if (editorPane) editorPane.style.display = 'none'; | ||||
|                     if (resizer1) resizer1.style.display = 'none'; | ||||
|                 } | ||||
|  | ||||
|                 // Clear the editor (but don't trigger preview update due to flag) | ||||
|                 if (editor.editor) { | ||||
|                     editor.editor.setValue(''); | ||||
|                 } | ||||
|                 editor.filenameInput.value = item.path; | ||||
|                 editor.currentFile = item.path; | ||||
|  | ||||
|                 // Build the file URL using the WebDAV client's method | ||||
|                 const fileUrl = webdavClient.getFullUrl(item.path); | ||||
|                 Logger.debug(`Binary file URL: ${fileUrl}`); | ||||
|  | ||||
|                 // Generate preview HTML based on file type | ||||
|                 let previewHtml = ''; | ||||
|  | ||||
|                 if (fileType === 'Image') { | ||||
|                     // Preview images | ||||
|                     previewHtml = ` | ||||
|                         <div style="padding: 20px; text-align: center;"> | ||||
|                             <h3>${fileName}</h3> | ||||
|                             <p style="color: var(--text-secondary); margin-bottom: 20px;">Image Preview (Read-only)</p> | ||||
|                             <img src="${fileUrl}" alt="${fileName}" style="max-width: 100%; height: auto; border: 1px solid var(--border-color); border-radius: 4px;"> | ||||
|                         </div> | ||||
|                     `; | ||||
|                 } else if (fileType === 'PDF') { | ||||
|                     // Preview PDFs | ||||
|                     previewHtml = ` | ||||
|                         <div style="padding: 20px;"> | ||||
|                             <h3>${fileName}</h3> | ||||
|                             <p style="color: var(--text-secondary); margin-bottom: 20px;">PDF Preview (Read-only)</p> | ||||
|                             <iframe src="${fileUrl}" style="width: 100%; height: 80vh; border: 1px solid var(--border-color); border-radius: 4px;"></iframe> | ||||
|                         </div> | ||||
|                     `; | ||||
|                 } else { | ||||
|                     // For other binary files, show download link | ||||
|                     previewHtml = ` | ||||
|                         <div style="padding: 20px;"> | ||||
|                             <h3>${fileName}</h3> | ||||
|                             <p style="color: var(--text-secondary); margin-bottom: 20px;">${fileType} File (Read-only)</p> | ||||
|                             <p>This file cannot be previewed in the browser.</p> | ||||
|                             <a href="${fileUrl}" download="${fileName}" class="btn btn-primary">Download ${fileName}</a> | ||||
|                         </div> | ||||
|                     `; | ||||
|                 } | ||||
|  | ||||
|                 // Display in preview pane | ||||
|                 editor.previewElement.innerHTML = previewHtml; | ||||
|  | ||||
|                 // Highlight the file in the tree | ||||
|                 fileTree.selectAndExpandPath(item.path); | ||||
|  | ||||
|                 // Update URL to reflect current file | ||||
|                 updateURL(currentCollection, item.path, isEditMode); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // For text files, restore the editor pane if it was hidden | ||||
|             if (isEditMode) { | ||||
|                 const editorPane = document.getElementById('editorPane'); | ||||
|                 const resizer1 = document.getElementById('resizer1'); | ||||
|                 if (editorPane) editorPane.style.display = ''; | ||||
|                 if (resizer1) resizer1.style.display = ''; | ||||
|             } | ||||
|  | ||||
|             await editor.loadFile(item.path); | ||||
|             // Highlight the file in the tree and expand parent directories | ||||
|             fileTree.selectAndExpandPath(item.path); | ||||
|             // Update URL to reflect current file | ||||
|             const currentCollection = collectionSelector.getCurrentCollection(); | ||||
|             updateURL(currentCollection, item.path, isEditMode); | ||||
|         } catch (error) { | ||||
|             Logger.error('Failed to select file:', error); | ||||
| @@ -332,9 +461,7 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|     const { collection: urlCollection, filePath: urlFilePath } = parseURLPath(); | ||||
|     console.log('[URL PARSE]', { urlCollection, urlFilePath }); | ||||
|  | ||||
|     if (urlCollection && urlFilePath) { | ||||
|         console.log('[URL LOAD] Loading from URL:', urlCollection, urlFilePath); | ||||
|  | ||||
|     if (urlCollection) { | ||||
|         // First ensure the collection is set | ||||
|         const currentCollection = collectionSelector.getCurrentCollection(); | ||||
|         if (currentCollection !== urlCollection) { | ||||
| @@ -343,11 +470,17 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|             await fileTree.load(); | ||||
|         } | ||||
|  | ||||
|         // Now load the file from URL | ||||
|         console.log('[URL LOAD] Calling loadFileFromURL'); | ||||
|         await loadFileFromURL(urlCollection, urlFilePath); | ||||
|         // If there's a file path in the URL, load it | ||||
|         if (urlFilePath) { | ||||
|             console.log('[URL LOAD] Loading file from URL:', urlCollection, urlFilePath); | ||||
|             await loadFileFromURL(urlCollection, urlFilePath); | ||||
|         } else if (!isEditMode) { | ||||
|             // Collection-only URL in view mode: auto-load last viewed page | ||||
|             console.log('[URL LOAD] Collection-only URL, auto-loading page'); | ||||
|             await autoLoadPageInViewMode(); | ||||
|         } | ||||
|     } else if (!isEditMode) { | ||||
|         // In view mode, auto-load last viewed page if no URL file specified | ||||
|         // No URL collection specified, in view mode: auto-load last viewed page | ||||
|         await autoLoadPageInViewMode(); | ||||
|     } | ||||
|  | ||||
| @@ -405,11 +538,34 @@ document.addEventListener('DOMContentLoaded', async () => { | ||||
|  | ||||
|         // Initialize file tree actions manager | ||||
|         window.fileTreeActions = new FileTreeActions(webdavClient, fileTree, editor); | ||||
|  | ||||
|         // Setup Exit Edit Mode button | ||||
|         document.getElementById('exitEditModeBtn').addEventListener('click', () => { | ||||
|             // Switch to view mode by removing edit=true from URL | ||||
|             const url = new URL(window.location.href); | ||||
|             url.searchParams.delete('edit'); | ||||
|             window.location.href = url.toString(); | ||||
|         }); | ||||
|  | ||||
|         // Hide Edit Mode button in edit mode | ||||
|         document.getElementById('editModeBtn').style.display = 'none'; | ||||
|     } else { | ||||
|         // In view mode, hide editor buttons | ||||
|         document.getElementById('newBtn').style.display = 'none'; | ||||
|         document.getElementById('saveBtn').style.display = 'none'; | ||||
|         document.getElementById('deleteBtn').style.display = 'none'; | ||||
|         document.getElementById('exitEditModeBtn').style.display = 'none'; | ||||
|  | ||||
|         // Show Edit Mode button in view mode | ||||
|         document.getElementById('editModeBtn').style.display = 'block'; | ||||
|  | ||||
|         // Setup Edit Mode button | ||||
|         document.getElementById('editModeBtn').addEventListener('click', () => { | ||||
|             // Switch to edit mode by adding edit=true to URL | ||||
|             const url = new URL(window.location.href); | ||||
|             url.searchParams.set('edit', 'true'); | ||||
|             window.location.href = url.toString(); | ||||
|         }); | ||||
|  | ||||
|         // Auto-load last viewed page or first file | ||||
|         await autoLoadPageInViewMode(); | ||||
| @@ -498,10 +654,11 @@ async function handleEditorFileDrop(file) { | ||||
|         const uploadedPath = await fileTree.uploadFile(targetDir, file); | ||||
|  | ||||
|         // Insert markdown link at cursor | ||||
|         // Use relative path (without collection name) so the image renderer can resolve it correctly | ||||
|         const isImage = file.type.startsWith('image/'); | ||||
|         const link = isImage | ||||
|             ? `` | ||||
|             : `[${file.name}](/${webdavClient.currentCollection}/${uploadedPath})`; | ||||
|             ? `` | ||||
|             : `[${file.name}](${uploadedPath})`; | ||||
|  | ||||
|         editor.insertAtCursor(link); | ||||
|         showNotification(`Uploaded and inserted link`, 'success'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user