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 @@
-
+