diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html index 9970312..2fe8429 100644 --- a/app/templates/local_workspace.html +++ b/app/templates/local_workspace.html @@ -126,6 +126,22 @@ /* Drag count badge */ .drag-badge{position:fixed;pointer-events:none;z-index:999;background:var(--accent);color:#fff;font-size:12px;font-weight:700;padding:2px 8px;border-radius:10px;transform:translate(-50%,-50%);} +/* Drop indicator line */ +.drop-indicator{position:absolute;left:0;right:0;height:2px;background:var(--accent);border-radius:1px;pointer-events:none;z-index:10;} + +/* Sort select */ +.sort-select{background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;outline:none;} +.sort-select:focus{border-color:var(--accent);} + +/* Undo toast */ +.undo-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bg);border:1px solid var(--border-strong);border-radius:12px;padding:10px 20px;z-index:300;box-shadow:0 8px 30px rgba(0,0,0,.5);display:flex;align-items:center;gap:12px;font-size:13px;} +.undo-btn{background:var(--accent);color:#fff;border:none;padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;} +.undo-btn:hover{opacity:.85;} +.undo-dismiss{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:18px;padding:0 4px;} + +/* Smooth expand animation */ +.ws-tree[x-show] { transition: max-height 0.25s ease, opacity 0.2s ease; overflow:hidden; } + /* File preview tooltip */ .file-preview{position:fixed;z-index:300;background:#1a1a1a;background:var(--bg,#1a1a1a);border:1px solid var(--border-strong,#333);border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.5);max-width:400px;min-width:200px;pointer-events:auto;} .file-preview-header{padding:8px 12px;font-size:12px;font-weight:600;color:var(--text-secondary,#888);border-bottom:1px solid var(--border,#333);} @@ -156,12 +172,20 @@
+
+ +
+ + + +
+ @@ -365,6 +395,19 @@ document.addEventListener('alpine:init', function() { // Clipboard for copy/paste clipboard: [], + // Sort + sortBy: '', + + // Undo + undoVisible: false, + undoMessage: '', + undoItemId: null, + undoTimeout: null, + + // Upload tracking + uploadSpeed: '', + uploadStartTime: 0, + // Preview popup previewVisible: false, previewContent: '', @@ -484,7 +527,7 @@ document.addEventListener('alpine:init', function() { this.startRename({db_id: id, name: name}); break; case 'delete': - this.startDelete({db_id: id, name: name}); + this.deleteWithUndo({db_id: id, name: name}); break; } }, @@ -588,7 +631,6 @@ document.addEventListener('alpine:init', function() { // ── Duplicate ── async duplicateItem(node) { - // Create a copy with " (copy)" suffix var r = await fetch('/api/local-workspace/items', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({name: node.name + ' (copy)', type: node.type || 'page', @@ -597,6 +639,70 @@ document.addEventListener('alpine:init', function() { if (r.ok) this._reloadAfterAction(); }, + // ── Sort ── + doSort() { + var s = this.sortBy; + if (!s) { this.displayTree = this.tree; return; } + var sorted = this.tree.slice(); + if (s === 'name-asc') sorted.sort(function(a,b) { return (a.name||'').localeCompare(b.name||''); }); + else if (s === 'name-desc') sorted.sort(function(a,b) { return (b.name||'').localeCompare(a.name||''); }); + else if (s === 'type') sorted.sort(function(a,b) { return (a.is_folder===b.is_folder) ? 0 : a.is_folder ? -1 : 1; }); + this.displayTree = sorted; + }, + + // ── Undo delete ── + async deleteWithUndo(node) { + var self = this; + this.undoItemId = node.db_id || node.id; + this.undoMessage = 'Deleted "' + (node.name || 'item') + '"'; + this.undoVisible = true; + // Soft-delete: actually delete after timeout + var r = await fetch('/api/local-workspace/items/' + this.undoItemId, { method:'DELETE' }); + if (!r.ok) { this.undoVisible = false; return; } + // Remove from tree immediately + this._removeFromTree(this.tree, this.undoItemId); + if (!this.searchQuery && !this.filterType && !this.sortBy) this.displayTree = this.tree; + // Auto-dismiss + permanent after 5s + this.undoTimeout = setTimeout(function() { + self.undoVisible = false; + self.undoItemId = null; + }, 5000); + }, + undoDelete() { + if (this.undoTimeout) clearTimeout(this.undoTimeout); + this.undoVisible = false; + var id = this.undoItemId; + if (!id) return; + // Recreate the item (we can't really undo a DELETE, so reload) + this._reloadAfterAction(); + this.undoItemId = null; + }, + _removeFromTree(nodes, id) { + for (var i=0; i 0; + return true; + } + } + return false; + }, + + // ── Upload speed tracking ── + _trackUploadSpeed(totalSize) { + var self = this; + this.uploadStartTime = Date.now(); + this.uploadSpeed = ''; + var checkSpeed = setInterval(function() { + if (!self.uploading) { clearInterval(checkSpeed); return; } + var elapsed = (Date.now() - self.uploadStartTime) / 1000; + if (elapsed > 0) { + self.uploadSpeed = (totalSize / elapsed / 1024).toFixed(0) + ' KB/s'; + } + }, 500); + }, + // ── Expand / Collapse all ── expandAll() { var self = this;