diff --git a/server_webdav.py b/server_webdav.py index 10307e7..bb955cc 100755 --- a/server_webdav.py +++ b/server_webdav.py @@ -187,6 +187,7 @@ def main(): try: server.start() + server.wait() except KeyboardInterrupt: print("\n\nShutting down...") server.stop() diff --git a/static/css/components.css b/static/css/components.css index 545770d..1caaf93 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -158,3 +158,51 @@ body.dark-mode .context-menu { } } + +/* Modal Dialogs */ +.modal { + z-index: 10000; +} + +.modal-backdrop { + z-index: 9999; + background-color: rgba(0, 0, 0, 0.5); +} + +body.dark-mode .modal-content { + background-color: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +body.dark-mode .modal-header { + border-bottom-color: var(--border-color); + background-color: var(--bg-tertiary); +} + +body.dark-mode .modal-footer { + border-top-color: var(--border-color); + background-color: var(--bg-tertiary); +} + +.modal-header.border-danger { + border-bottom: 2px solid var(--danger-color) !important; +} + +.modal-open { + overflow: hidden; +} + +/* Input in modal */ +.modal-body input.form-control { + background-color: var(--bg-primary); + color: var(--text-primary); + border-color: var(--border-color); +} + +.modal-body input.form-control:focus { + background-color: var(--bg-primary); + color: var(--text-primary); + border-color: var(--link-color); + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} \ No newline at end of file diff --git a/static/css/layout.css b/static/css/layout.css index 96bedc8..8004e0c 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -11,18 +11,25 @@ html, body { /* Column Resizer */ .column-resizer { - width: 4px; + width: 1px; background-color: var(--border-color); cursor: col-resize; - transition: background-color 0.2s ease; + transition: background-color 0.2s ease, width 0.2s ease, box-shadow 0.2s ease; user-select: none; flex-shrink: 0; + padding: 0 3px; /* Add invisible padding for easier grab */ + margin: 0 -3px; /* Compensate for padding */ } .column-resizer:hover { background-color: var(--link-color); - width: 6px; - margin: 0 -1px; + width: 1px; + box-shadow: 0 0 6px rgba(13, 110, 253, 0.3); /* Visual feedback instead of width change */ +} + +.column-resizer.dragging { + background-color: var(--link-color); + box-shadow: 0 0 8px rgba(13, 110, 253, 0.5); } .column-resizer.dragging { diff --git a/static/js/app.js b/static/js/app.js index d090301..ed2c8e2 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -38,7 +38,7 @@ document.addEventListener('DOMContentLoaded', async () => { await fileTree.load(); // Initialize editor - editor = new MarkdownEditor('editor', 'preview'); + editor = new MarkdownEditor('editor', 'preview', 'filenameInput'); // Setup editor drop handler const editorDropHandler = new EditorDropHandler( @@ -66,6 +66,9 @@ document.addEventListener('DOMContentLoaded', async () => { // Initialize mermaid mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' }); + + // Initialize file tree actions manager + window.fileTreeActions = new FileTreeActions(webdavClient, fileTree, editor); }); // Listen for column resize events to refresh editor diff --git a/static/js/editor.js b/static/js/editor.js index 4ab9fbd..32a92ae 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -143,7 +143,7 @@ class MarkdownEditor { this.currentFile = null; this.filenameInput.value = ''; this.filenameInput.focus(); - this.editor.setValue(''); + this.editor.setValue('# New File\n\nStart typing...\n'); this.updatePreview(); if (window.showNotification) { @@ -192,7 +192,7 @@ class MarkdownEditor { */ updatePreview() { const markdown = this.editor.getValue(); - let html = this.marked.parse(markdown); + let html = window.marked.parse(markdown); // Process mermaid diagrams html = html.replace(/
([\s\S]*?)<\/code><\/pre>/g, (match, code) => {
diff --git a/static/js/file-tree-actions.js b/static/js/file-tree-actions.js
new file mode 100644
index 0000000..2ba0422
--- /dev/null
+++ b/static/js/file-tree-actions.js
@@ -0,0 +1,276 @@
+/**
+ * File Tree Actions Manager
+ * Centralized handling of all tree operations
+ */
+
+class FileTreeActions {
+    constructor(webdavClient, fileTree, editor) {
+        this.webdavClient = webdavClient;
+        this.fileTree = fileTree;
+        this.editor = editor;
+        this.clipboard = null;
+    }
+
+    async execute(action, targetPath, isDirectory) {
+        const handler = this.actions[action];
+        if (!handler) {
+            console.error(`Unknown action: ${action}`);
+            return;
+        }
+        
+        try {
+            await handler.call(this, targetPath, isDirectory);
+        } catch (error) {
+            console.error(`Action failed: ${action}`, error);
+            showNotification(`Failed to ${action}`, 'error');
+        }
+    }
+
+    actions = {
+        open: async function(path, isDir) {
+            if (!isDir) {
+                await this.editor.loadFile(path);
+            }
+        },
+
+        newFile: async function(path, isDir) {
+            if (!isDir) return;
+            
+            const filename = await this.showInputDialog('Enter filename:', 'new-file.md');
+            if (filename) {
+                const fullPath = `${path}/${filename}`.replace(/\/+/g, '/');
+                await this.webdavClient.put(fullPath, '# New File\n\n');
+                await this.fileTree.load();
+                showNotification(`Created ${filename}`, 'success');
+            }
+        },
+
+        newFolder: async function(path, isDir) {
+            if (!isDir) return;
+            
+            const foldername = await this.showInputDialog('Enter folder name:', 'new-folder');
+            if (foldername) {
+                const fullPath = `${path}/${foldername}`.replace(/\/+/g, '/');
+                await this.webdavClient.mkcol(fullPath);
+                await this.fileTree.load();
+                showNotification(`Created folder ${foldername}`, 'success');
+            }
+        },
+
+        rename: async function(path, isDir) {
+            const oldName = path.split('/').pop();
+            const newName = await this.showInputDialog('Rename to:', oldName);
+            
+            if (newName && newName !== oldName) {
+                const parentPath = path.substring(0, path.lastIndexOf('/'));
+                const newPath = parentPath ? `${parentPath}/${newName}` : newName;
+                
+                await this.webdavClient.move(path, newPath);
+                await this.fileTree.load();
+                showNotification('Renamed', 'success');
+            }
+        },
+
+        copy: async function(path, isDir) {
+            this.clipboard = { path, operation: 'copy', isDirectory: isDir };
+            showNotification(`Copied: ${path.split('/').pop()}`, 'info');
+            this.updatePasteMenuItem();
+        },
+
+        cut: async function(path, isDir) {
+            this.clipboard = { path, operation: 'cut', isDirectory: isDir };
+            showNotification(`Cut: ${path.split('/').pop()}`, 'warning');
+            this.updatePasteMenuItem();
+        },
+
+        paste: async function(targetPath, isDir) {
+            if (!this.clipboard || !isDir) return;
+            
+            const itemName = this.clipboard.path.split('/').pop();
+            const destPath = `${targetPath}/${itemName}`.replace(/\/+/g, '/');
+            
+            if (this.clipboard.operation === 'copy') {
+                await this.webdavClient.copy(this.clipboard.path, destPath);
+                showNotification('Copied', 'success');
+            } else {
+                await this.webdavClient.move(this.clipboard.path, destPath);
+                this.clipboard = null;
+                this.updatePasteMenuItem();
+                showNotification('Moved', 'success');
+            }
+            
+            await this.fileTree.load();
+        },
+
+        delete: async function(path, isDir) {
+            const name = path.split('/').pop();
+            const type = isDir ? 'folder' : 'file';
+            
+            if (!await this.showConfirmDialog(`Delete this ${type}?`, `${name}`)) {
+                return;
+            }
+            
+            await this.webdavClient.delete(path);
+            await this.fileTree.load();
+            showNotification(`Deleted ${name}`, 'success');
+        },
+
+        download: async function(path, isDir) {
+            showNotification('Downloading...', 'info');
+            // Implementation here
+        },
+
+        upload: async function(path, isDir) {
+            if (!isDir) return;
+            
+            const input = document.createElement('input');
+            input.type = 'file';
+            input.multiple = true;
+            
+            input.onchange = async (e) => {
+                const files = Array.from(e.target.files);
+                for (const file of files) {
+                    const fullPath = `${path}/${file.name}`.replace(/\/+/g, '/');
+                    const content = await file.arrayBuffer();
+                    await this.webdavClient.putBinary(fullPath, content);
+                    showNotification(`Uploaded ${file.name}`, 'success');
+                }
+                await this.fileTree.load();
+            };
+            
+            input.click();
+        }
+    };
+
+    // Modern dialog implementations
+    async showInputDialog(title, placeholder = '') {
+        return new Promise((resolve) => {
+            const dialog = this.createInputDialog(title, placeholder);
+            const input = dialog.querySelector('input');
+            const confirmBtn = dialog.querySelector('.btn-primary');
+            const cancelBtn = dialog.querySelector('.btn-secondary');
+            
+            const cleanup = () => {
+                dialog.remove();
+                document.body.classList.remove('modal-open');
+            };
+            
+            confirmBtn.onclick = () => {
+                resolve(input.value.trim());
+                cleanup();
+            };
+            
+            cancelBtn.onclick = () => {
+                resolve(null);
+                cleanup();
+            };
+            
+            input.onkeypress = (e) => {
+                if (e.key === 'Enter') confirmBtn.click();
+                if (e.key === 'Escape') cancelBtn.click();
+            };
+            
+            document.body.appendChild(dialog);
+            document.body.classList.add('modal-open');
+            input.focus();
+            input.select();
+        });
+    }
+
+    async showConfirmDialog(title, message = '') {
+        return new Promise((resolve) => {
+            const dialog = this.createConfirmDialog(title, message);
+            const confirmBtn = dialog.querySelector('.btn-danger');
+            const cancelBtn = dialog.querySelector('.btn-secondary');
+            
+            const cleanup = () => {
+                dialog.remove();
+                document.body.classList.remove('modal-open');
+            };
+            
+            confirmBtn.onclick = () => {
+                resolve(true);
+                cleanup();
+            };
+            
+            cancelBtn.onclick = () => {
+                resolve(false);
+                cleanup();
+            };
+            
+            document.body.appendChild(dialog);
+            document.body.classList.add('modal-open');
+            confirmBtn.focus();
+        });
+    }
+
+    createInputDialog(title, placeholder) {
+        const backdrop = document.createElement('div');
+        backdrop.className = 'modal-backdrop fade show';
+        
+        const dialog = document.createElement('div');
+        dialog.className = 'modal fade show d-block';
+        dialog.setAttribute('tabindex', '-1');
+        dialog.style.display = 'block';
+        
+        dialog.innerHTML = `
+            
+        `;
+        
+        document.body.appendChild(backdrop);
+        return dialog;
+    }
+
+    createConfirmDialog(title, message) {
+        const backdrop = document.createElement('div');
+        backdrop.className = 'modal-backdrop fade show';
+        
+        const dialog = document.createElement('div');
+        dialog.className = 'modal fade show d-block';
+        dialog.setAttribute('tabindex', '-1');
+        dialog.style.display = 'block';
+        
+        dialog.innerHTML = `
+            
+        `;
+        
+        document.body.appendChild(backdrop);
+        return dialog;
+    }
+
+    updatePasteMenuItem() {
+        const pasteItem = document.getElementById('pasteMenuItem');
+        if (pasteItem) {
+            pasteItem.style.display = this.clipboard ? 'flex' : 'none';
+        }
+    }
+}
\ No newline at end of file
diff --git a/static/js/file-tree.js b/static/js/file-tree.js
index 10e9ec8..7774253 100644
--- a/static/js/file-tree.js
+++ b/static/js/file-tree.js
@@ -18,17 +18,15 @@ class FileTree {
     setupEventListeners() {
         // Click handler for tree nodes
         this.container.addEventListener('click', (e) => {
+            console.log('Container clicked', e.target);
             const node = e.target.closest('.tree-node');
             if (!node) return;
             
+            console.log('Node found', node);
             const path = node.dataset.path;
             const isDir = node.dataset.isdir === 'true';
             
-            // Toggle folder
-            if (e.target.closest('.tree-node-toggle')) {
-                this.toggleFolder(node);
-                return;
-            }
+            // The toggle is handled inside renderNodes now
             
             // Select node
             if (isDir) {
@@ -69,87 +67,46 @@ class FileTree {
     
     renderNodes(nodes, parentElement, level) {
         nodes.forEach(node => {
+            const nodeWrapper = document.createElement('div');
+            nodeWrapper.className = 'tree-node-wrapper';
+
+            // Create node element
             const nodeElement = this.createNodeElement(node, level);
-            parentElement.appendChild(nodeElement);
-            
+            nodeWrapper.appendChild(nodeElement);
+
+            // Create children container ONLY if has children
             if (node.children && node.children.length > 0) {
                 const childContainer = document.createElement('div');
                 childContainer.className = 'tree-children';
                 childContainer.style.display = 'none';
-                nodeElement.appendChild(childContainer);
+                childContainer.dataset.parent = node.path;
+                childContainer.style.marginLeft = `${(level + 1) * 12}px`;
+
+                // Recursively render children
                 this.renderNodes(node.children, childContainer, level + 1);
+                nodeWrapper.appendChild(childContainer);
+
+                // Make toggle functional
+                const toggle = nodeElement.querySelector('.tree-node-toggle');
+                if (toggle) {
+                    toggle.addEventListener('click', (e) => {
+                        console.log('Toggle clicked', e.target);
+                        e.stopPropagation();
+                        const isHidden = childContainer.style.display === 'none';
+                        console.log('Is hidden?', isHidden);
+                        childContainer.style.display = isHidden ? 'block' : 'none';
+                        toggle.innerHTML = isHidden ? '▼' : '▶';
+                        toggle.classList.toggle('expanded');
+                    });
+                }
             }
+
+            parentElement.appendChild(nodeWrapper);
         });
     }
     
-    createNodeElement(node, level) {
-        const nodeWrapper = document.createElement('div');
-        nodeWrapper.className = 'tree-node-wrapper';
-        nodeWrapper.style.marginLeft = `${level * 12}px`;
-        
-        const div = document.createElement('div');
-        div.className = 'tree-node';
-        div.dataset.path = node.path;
-        div.dataset.isdir = node.isDirectory;
-        
-        // Toggle arrow for folders
-        if (node.isDirectory) {
-            const toggle = document.createElement('span');
-            toggle.className = 'tree-node-toggle';
-            toggle.innerHTML = '▶';
-            div.appendChild(toggle);
-        } else {
-            const spacer = document.createElement('span');
-            spacer.style.width = '16px';
-            spacer.style.display = 'inline-block';
-            div.appendChild(spacer);
-        }
-        
-        // Icon
-        const icon = document.createElement('i');
-        icon.className = `bi ${node.isDirectory ? 'bi-folder-fill' : 'bi-file-earmark-text'} tree-node-icon`;
-        div.appendChild(icon);
-        
-        // Content wrapper
-        const contentWrapper = document.createElement('div');
-        contentWrapper.className = 'tree-node-content';
-        
-        // Name
-        const name = document.createElement('span');
-        name.className = 'tree-node-name';
-        name.textContent = node.name;
-        name.title = node.name; // Tooltip on hover
-        contentWrapper.appendChild(name);
-        
-        // Size for files
-        if (!node.isDirectory && node.size) {
-            const size = document.createElement('span');
-            size.className = 'file-size-badge';
-            size.textContent = this.formatSize(node.size);
-            contentWrapper.appendChild(size);
-        }
-        
-        div.appendChild(contentWrapper);
-        nodeWrapper.appendChild(div);
-        
-        return nodeWrapper;
-    }
     
-    toggleFolder(nodeElement) {
-        const childContainer = nodeElement.querySelector('.tree-children');
-        if (!childContainer) return;
-        
-        const toggle = nodeElement.querySelector('.tree-node-toggle');
-        const isExpanded = childContainer.style.display !== 'none';
-        
-        if (isExpanded) {
-            childContainer.style.display = 'none';
-            toggle.innerHTML = '▶';
-        } else {
-            childContainer.style.display = 'block';
-            toggle.innerHTML = '▼';
-        }
-    }
+    // toggleFolder is no longer needed as the event listener is added in renderNodes.
     
     selectFile(path) {
         this.selectedPath = path;
@@ -181,6 +138,32 @@ class FileTree {
             }
         }
     }
+
+    createNodeElement(node, level) {
+        const nodeElement = document.createElement('div');
+        nodeElement.className = 'tree-node';
+        nodeElement.dataset.path = node.path;
+        nodeElement.dataset.isdir = node.isDirectory;
+        nodeElement.style.paddingLeft = `${level * 12}px`;
+
+        const icon = document.createElement('span');
+        icon.className = 'tree-node-icon';
+        if (node.isDirectory) {
+            icon.innerHTML = '▶'; // Collapsed by default
+            icon.classList.add('tree-node-toggle');
+        } else {
+            icon.innerHTML = '●'; // File icon
+        }
+
+        const title = document.createElement('span');
+        title.className = 'tree-node-title';
+        title.textContent = node.name;
+
+        nodeElement.appendChild(icon);
+        nodeElement.appendChild(title);
+
+        return nodeElement;
+    }
     
     formatSize(bytes) {
         if (bytes === 0) return '0 B';
@@ -190,11 +173,21 @@ class FileTree {
         return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i];
     }
     
+    newFile() {
+        this.selectedPath = null;
+        this.updateSelection();
+        // Potentially clear editor via callback
+        if (this.onFileSelect) {
+            this.onFileSelect({ path: null, isDirectory: false });
+        }
+    }
+
     async createFile(parentPath, filename) {
         try {
             const fullPath = parentPath ? `${parentPath}/${filename}` : filename;
             await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n');
             await this.load();
+            this.selectFile(fullPath); // Select the new file
             showNotification('File created', 'success');
             return fullPath;
         } catch (error) {
diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js
index b076afa..d083c47 100644
--- a/static/js/ui-utils.js
+++ b/static/js/ui-utils.js
@@ -47,29 +47,26 @@ function showContextMenu(x, y, target) {
     const menu = document.getElementById('contextMenu');
     if (!menu) return;
     
-    // Store target
+    // Store target data
     menu.dataset.targetPath = target.path;
     menu.dataset.targetIsDir = target.isDir;
     
     // Show/hide menu items based on target type
-    const newFileItem = menu.querySelector('[data-action="new-file"]');
-    const newFolderItem = menu.querySelector('[data-action="new-folder"]');
-    const uploadItem = menu.querySelector('[data-action="upload"]');
-    const downloadItem = menu.querySelector('[data-action="download"]');
+    const items = {
+        'new-file': target.isDir,
+        'new-folder': target.isDir,
+        'upload': target.isDir,
+        'download': true,
+        'paste': target.isDir && window.fileTreeActions?.clipboard,
+        'open': !target.isDir
+    };
     
-    if (target.isDir) {
-        // Folder context menu
-        if (newFileItem) newFileItem.style.display = 'block';
-        if (newFolderItem) newFolderItem.style.display = 'block';
-        if (uploadItem) uploadItem.style.display = 'block';
-        if (downloadItem) downloadItem.style.display = 'block';
-    } else {
-        // File context menu
-        if (newFileItem) newFileItem.style.display = 'none';
-        if (newFolderItem) newFolderItem.style.display = 'none';
-        if (uploadItem) uploadItem.style.display = 'none';
-        if (downloadItem) downloadItem.style.display = 'block';
-    }
+    Object.entries(items).forEach(([action, show]) => {
+        const item = menu.querySelector(`[data-action="${action}"]`);
+        if (item) {
+            item.style.display = show ? 'flex' : 'none';
+        }
+    });
     
     // Position menu
     menu.style.display = 'block';
@@ -77,13 +74,15 @@ function showContextMenu(x, y, target) {
     menu.style.top = y + 'px';
     
     // Adjust if off-screen
-    const rect = menu.getBoundingClientRect();
-    if (rect.right > window.innerWidth) {
-        menu.style.left = (window.innerWidth - rect.width - 10) + 'px';
-    }
-    if (rect.bottom > window.innerHeight) {
-        menu.style.top = (window.innerHeight - rect.height - 10) + 'px';
-    }
+    setTimeout(() => {
+        const rect = menu.getBoundingClientRect();
+        if (rect.right > window.innerWidth) {
+            menu.style.left = (window.innerWidth - rect.width - 10) + 'px';
+        }
+        if (rect.bottom > window.innerHeight) {
+            menu.style.top = (window.innerHeight - rect.height - 10) + 'px';
+        }
+    }, 0);
 }
 
 function hideContextMenu() {
@@ -93,9 +92,29 @@ function hideContextMenu() {
     }
 }
 
-// Hide context menu on click outside
+// Context menu item click handler
+document.addEventListener('click', async (e) => {
+    const menuItem = e.target.closest('.context-menu-item');
+    if (!menuItem) {
+        hideContextMenu();
+        return;
+    }
+    
+    const action = menuItem.dataset.action;
+    const menu = document.getElementById('contextMenu');
+    const targetPath = menu.dataset.targetPath;
+    const isDir = menu.dataset.targetIsDir === 'true';
+    
+    hideContextMenu();
+    
+    if (window.fileTreeActions) {
+        await window.fileTreeActions.execute(action, targetPath, isDir);
+    }
+});
+
+// Hide on outside click
 document.addEventListener('click', (e) => {
-    if (!e.target.closest('#contextMenu')) {
+    if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
         hideContextMenu();
     }
 });
diff --git a/templates/index.html b/templates/index.html
index 14e0242..9f02fbd 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -158,7 +158,7 @@
     
     
 
-    
+