Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion filemanaty/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""filemanaty package — config, security, thumbnails, HTTP routes."""

__version__ = "0.9.0"
__version__ = "0.10.0"
10 changes: 7 additions & 3 deletions web/filemanaty.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -194,9 +195,9 @@ function buildOverlay() {
<button id="fm-refresh" class="fm-tb">↻ Refresh</button>
</div>
<div id="fm-body" style="flex:1;display:grid;grid-template-columns:200px 1fr 34%;min-height:0;">
<div id="fm-tree" style="overflow:auto;padding:8px;border-right:1px solid var(--fm-border);background:var(--fm-bg);font-size:13px;"></div>
<div id="fm-tree" style="overflow:auto;padding:8px;background:var(--fm-bg);font-size:13px;"></div>
<div id="fm-grid" style="overflow:auto;padding:10px;display:grid;gap:8px;align-content:start;grid-template-columns:repeat(auto-fill, minmax(140px, 1fr));"></div>
<div id="fm-preview" style="border-left:1px solid var(--fm-border);padding:14px;display:flex;flex-direction:column;gap:10px;background:var(--fm-bg);min-height:0;overflow:hidden;"></div>
<div id="fm-preview" style="padding:14px;display:flex;flex-direction:column;gap:10px;background:var(--fm-bg);min-height:0;overflow:hidden;"></div>
</div>
`;
const style = document.createElement("style");
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down
117 changes: 117 additions & 0 deletions web/resize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 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;
}
Comment on lines +23 to +26

export function initPaneResize(bodyEl) {
const treeEl = bodyEl.querySelector("#fm-tree");
const previewEl = bodyEl.querySelector("#fm-preview");

Comment on lines +28 to +31
// 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)));
}
Comment on lines +82 to +88
}

document.addEventListener("pointermove", onMove);
document.addEventListener("pointerup", onUp);
}
Comment on lines +76 to +93

// 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", () => {
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);
});
Comment on lines +107 to +116
}