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:
Mahmoud-Emad
2025-10-26 17:29:45 +03:00
parent 0ed6bcf1f2
commit f319f29d4c
20 changed files with 1679 additions and 113 deletions

View File

@@ -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}](/${webdavClient.currentCollection}/${uploadedPath})`;
? `![${file.name}](${uploadedPath})`
: `[${file.name}](${uploadedPath})`;
editor.insertAtCursor(link);
showNotification(`Uploaded and inserted link`, 'success');