From 583eb0bbe02970212a6ae257baa3b62a73f71e0c Mon Sep 17 00:00:00 2001 From: Alexander Garzon Date: Sun, 14 Jun 2026 23:28:43 -0400 Subject: [PATCH 1/4] feat(web): add resize.js for drag-resizable overlay panes --- web/resize.js | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 web/resize.js diff --git a/web/resize.js b/web/resize.js new file mode 100644 index 0000000..c339c1e --- /dev/null +++ b/web/resize.js @@ -0,0 +1,109 @@ +// Drag-to-resize for the FileManaty overlay's tree and preview panes. +// Mirrors ComfyUI/PrimeVue Splitter conventions (localStorage persistence, +// min-size clamps, col-resize gutters) without a Vue dependency. The middle +// grid track stays `1fr` and absorbs the difference, so the thumbnail grid +// keeps auto-reflowing and keyboard nav's computeGridColumns() stays correct. + +const LS_TREE = "filemanaty.layout.treeWidth"; +const LS_PREVIEW = "filemanaty.layout.previewWidth"; + +const DEFAULT_TREE = "200px"; +const DEFAULT_PREVIEW = "34%"; + +const MIN_TREE = 120; // px +const MIN_PREVIEW = 160; // px +const MIN_GRID = 200; // px — the middle pane is never crushed below this +const GUTTER = 4; // px — matches ComfyUI/PrimeVue's default gutter + +function applyColumns(bodyEl, treeCol, previewCol) { + bodyEl.style.gridTemplateColumns = + `${treeCol} ${GUTTER}px 1fr ${GUTTER}px ${previewCol}`; +} + +function readStored(key) { + const v = parseInt(localStorage.getItem(key), 10); + return Number.isFinite(v) && v > 0 ? v : null; +} + +export function initPaneResize(bodyEl) { + const treeEl = bodyEl.querySelector("#fm-tree"); + const previewEl = bodyEl.querySelector("#fm-preview"); + + // Two gutter elements become grid items. DOM order must end up: + // tree, gutTree, grid, gutPreview, preview. + const gutTree = document.createElement("div"); + const gutPreview = document.createElement("div"); + gutTree.className = "fm-gutter"; + gutPreview.className = "fm-gutter"; + treeEl.after(gutTree); + previewEl.before(gutPreview); + + // Current track values, applied to the grid. Only the dragged side is + // converted to px; the other keeps its default string until touched. + const tStored = readStored(LS_TREE); + const pStored = readStored(LS_PREVIEW); + let treeCol = tStored != null ? `${tStored}px` : DEFAULT_TREE; + let previewCol = pStored != null ? `${pStored}px` : DEFAULT_PREVIEW; + applyColumns(bodyEl, treeCol, previewCol); + + function startDrag(which, e) { + e.preventDefault(); + const rect = bodyEl.getBoundingClientRect(); + const prevUserSelect = document.body.style.userSelect; + document.body.style.userSelect = "none"; + document.body.style.cursor = "col-resize"; + let moved = false; + + function onMove(ev) { + moved = true; + const total = rect.width; + if (which === "tree") { + const maxTree = + total - GUTTER * 2 - MIN_GRID - previewEl.getBoundingClientRect().width; + let w = ev.clientX - rect.left; + w = Math.max(MIN_TREE, Math.min(w, maxTree)); + treeCol = `${Math.round(w)}px`; + } else { + const maxPreview = + total - GUTTER * 2 - MIN_GRID - treeEl.getBoundingClientRect().width; + let w = rect.right - ev.clientX; + w = Math.max(MIN_PREVIEW, Math.min(w, maxPreview)); + previewCol = `${Math.round(w)}px`; + } + applyColumns(bodyEl, treeCol, previewCol); + } + + function onUp() { + document.removeEventListener("pointermove", onMove); + document.removeEventListener("pointerup", onUp); + document.body.style.userSelect = prevUserSelect; + document.body.style.cursor = ""; + if (!moved) return; // a plain click should not freeze a % default to px + if (which === "tree") { + localStorage.setItem( + LS_TREE, String(Math.round(treeEl.getBoundingClientRect().width))); + } else { + localStorage.setItem( + LS_PREVIEW, String(Math.round(previewEl.getBoundingClientRect().width))); + } + } + + document.addEventListener("pointermove", onMove); + document.addEventListener("pointerup", onUp); + } + + gutTree.addEventListener("pointerdown", (e) => startDrag("tree", e)); + gutPreview.addEventListener("pointerdown", (e) => startDrag("preview", e)); + + // Double-click a gutter resets that column to its CSS default. + gutTree.addEventListener("dblclick", () => { + localStorage.removeItem(LS_TREE); + treeCol = DEFAULT_TREE; + applyColumns(bodyEl, treeCol, previewCol); + }); + gutPreview.addEventListener("dblclick", () => { + localStorage.removeItem(LS_PREVIEW); + previewCol = DEFAULT_PREVIEW; + applyColumns(bodyEl, treeCol, previewCol); + }); +} From 50394f46053514bf513448fa90e25950be25d84b Mon Sep 17 00:00:00 2001 From: Alexander Garzon Date: Sun, 14 Jun 2026 23:32:27 -0400 Subject: [PATCH 2/4] feat(web): wire resizable panes into the overlay --- web/filemanaty.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/filemanaty.js b/web/filemanaty.js index 120679d..f071b4b 100644 --- a/web/filemanaty.js +++ b/web/filemanaty.js @@ -5,6 +5,7 @@ import { clickSelect, selectAll } from "./selection.js"; import { promptText, confirmDialog, toast, trashView, isDialogOpen } from "./dialogs.js"; import { attachContextMenu } from "./contextmenu.js"; import { renderTree } from "./tree.js"; +import { initPaneResize } from "./resize.js"; import { makeDraggable, makeDropTarget } from "./dnd.js"; import * as settings from "./settings.js"; import { buildSettingsDefinitions, KEYS as SETTINGS_KEYS } from "./settings.js"; @@ -194,9 +195,9 @@ function buildOverlay() {
-
+
-
+
`; const style = document.createElement("style"); @@ -215,7 +216,9 @@ function buildOverlay() { #filemanaty-overlay .fm-head-right{display:flex;align-items:center;gap:16px} #filemanaty-overlay .fm-gh{display:inline-flex;align-items:center;gap:6px;color:var(--fm-text-muted);text-decoration:none;font-size:12px;transition:color .15s} #filemanaty-overlay .fm-gh:hover{color:var(--fm-text)} -#filemanaty-overlay .fm-gh svg{width:15px;height:15px}`; +#filemanaty-overlay .fm-gh svg{width:15px;height:15px} +#filemanaty-overlay .fm-gutter{background:var(--fm-border);cursor:col-resize;touch-action:none;transition:background .12s} +#filemanaty-overlay .fm-gutter:hover{background:var(--fm-accent)}`; root.appendChild(style); return root; } @@ -233,6 +236,7 @@ async function loadVersion() { async function initOverlay() { document.getElementById("fm-close").addEventListener("click", closeOverlay); + initPaneResize(document.getElementById("fm-body")); loadVersion(); document.addEventListener("keydown", (e) => { if (!STATE.open) return; From fb260a8cc5f6c2ca3710eefc6761dc19dc3f8d96 Mon Sep 17 00:00:00 2001 From: Alexander Garzon Date: Sun, 14 Jun 2026 23:45:56 -0400 Subject: [PATCH 3/4] feat(web): capture pointer on gutter drag for touch/stylus robustness --- web/resize.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/resize.js b/web/resize.js index c339c1e..2643655 100644 --- a/web/resize.js +++ b/web/resize.js @@ -92,8 +92,16 @@ export function initPaneResize(bodyEl) { document.addEventListener("pointerup", onUp); } - gutTree.addEventListener("pointerdown", (e) => startDrag("tree", e)); - gutPreview.addEventListener("pointerdown", (e) => startDrag("preview", e)); + // Capture the pointer on the gutter so drags stay tracked on touch/stylus + // (events still bubble to the document listeners below). + gutTree.addEventListener("pointerdown", (e) => { + gutTree.setPointerCapture?.(e.pointerId); + startDrag("tree", e); + }); + gutPreview.addEventListener("pointerdown", (e) => { + gutPreview.setPointerCapture?.(e.pointerId); + startDrag("preview", e); + }); // Double-click a gutter resets that column to its CSS default. gutTree.addEventListener("dblclick", () => { From 0e3298ab58d3f87c6d8d2ba32183bf15c4b1585a Mon Sep 17 00:00:00 2001 From: Alexander Garzon Date: Sun, 14 Jun 2026 23:49:25 -0400 Subject: [PATCH 4/4] docs: release resizable panes as v0.10.0 (CHANGELOG, README, version bump) --- CHANGELOG.md | 12 ++++++++++++ README.md | 4 ++-- filemanaty/__init__.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd6dc0..89abffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v0.10.0 — 2026-06-14 + +### Added + +- **Resizable overlay panes.** The folder tree and preview panes are now drag-resizable + via thin gutters on either side of the file grid — widen the tree for long folder names, + or trade preview width for grid space. Sizes persist per browser in `localStorage`, and + **double-clicking a gutter resets that pane** to its default (tree `200px` / preview `34%`). + Min-width clamps keep every pane usable (tree ≥120px, preview ≥160px, grid ≥200px) so + nothing collapses. The middle grid stays fluid and auto-reflows its thumbnail columns. + Frontend-only — a focused `web/resize.js` module, no backend changes, no new settings. + ## v0.9.0 — 2026-06-02 ### Added diff --git a/README.md b/README.md index 0e0ba25..994ed9a 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ## ✨ Features -- 🗂️ **Explorer-style file manager** — a folder tree, a thumbnail grid, and a live preview pane, all in one fullscreen overlay. +- 🗂️ **Explorer-style file manager** — a folder tree, a thumbnail grid, and a live preview pane, all in one fullscreen overlay with **drag-resizable panes** (double-click a divider to reset). - 🖼️ **Rich previews** — inline images, an HTML5 **video** player, and an **audio** player. Generated files show their **resolution** (`1024 × 1024`), size, and date at a glance. - 🧠 **See the generation behind the file** — embedded ComfyUI metadata (positive/negative prompt, seed, model, LoRAs) surfaced in the preview, with one-click **Copy JSON** and **Load on canvas** to drop the workflow straight onto your graph. - 📤 **Full write operations** — create folders, rename, upload (button or drag from your desktop), copy/cut/paste, and move — within and across roots. @@ -107,7 +107,7 @@ FileManaty can write to your filesystem, so please read this. ## 🗺️ Roadmap -Shipped recently: auto-mounted Workflows root, in-folder name + type filter, rich video + audio preview, embedded-metadata cards, Load-on-canvas, and a native theme-following UI. Coming next: +Shipped recently: drag-resizable overlay panes, auto-mounted Workflows root, in-folder name + type filter, rich video + audio preview, embedded-metadata cards, Load-on-canvas, and a native theme-following UI. Coming next: - 🔍 **Server-side & metadata search** — search across a whole root (past the listing cap) and find files by the **prompt / model / seed** that made them. *(In-folder name + type filtering shipped in v0.8.0.)* - 🔐 **Optional built-in authentication** — a lightweight password mode for small deployments. diff --git a/filemanaty/__init__.py b/filemanaty/__init__.py index e334469..d572a9f 100644 --- a/filemanaty/__init__.py +++ b/filemanaty/__init__.py @@ -1,3 +1,3 @@ """filemanaty package — config, security, thumbnails, HTTP routes.""" -__version__ = "0.9.0" +__version__ = "0.10.0"