"use strict"; // DAO_master — Folder File Pro UI (v4.0) // - Folder icon from web/ico_dossier.png (non déformée) // - Grid/List, scroll fiable, double-clic, type-to-select, Explorer import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; /* ---------- Lightbox (image/vidéo/audio) ---------- */ function ensureLightbox() { let el = document.getElementById("ffp-lightbox"); if (el) return; el = document.createElement("div"); el.id = "ffp-lightbox"; el.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;z-index:10000;"; el.innerHTML = '' + '' + '' + ''; document.body.appendChild(el); const close = () => { const i = el.querySelector("img"), v = el.querySelector("video"), a = el.querySelector("audio"); if (i) i.src = ""; if (v) { v.pause(); v.src = ""; } if (a) { a.pause(); a.src = ""; } el.style.display = "none"; }; el.querySelector(".ffp-close").onclick = close; el.addEventListener("click", (e) => { if (e.target === el) close(); }); } /* ---------- Icône dossier PNG depuis /web ---------- */ const FFP_FOLDER_URL = new URL("./ico_dossier.png", import.meta.url).href; /* ---------- Badge d'extension pour non-images ---------- */ function extBadge(ext) { if (!ext) ext = ".file"; if (ext[0] !== ".") ext = "." + ext; return '
[File' + ext + "]
"; } app.registerExtension({ name: "DAO_master.FolderFilePro.UI.v4_0", async beforeRegisterNodeDef(nodeType, nodeData) { if (!nodeData || nodeData.name !== "Folder File Pro") return; const onCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { if (onCreated) onCreated.apply(this, arguments); ensureLightbox(); /* --------- Widgets Comfy --------- */ const wDir = this.widgets.find((w) => w.name === "directory"); const wExt = this.widgets.find((w) => w.name === "extensions"); const wRe = this.widgets.find((w) => w.name === "name_regex"); const wReM = this.widgets.find((w) => w.name === "regex_mode"); const wReIC = this.widgets.find((w) => w.name === "regex_ignore_case"); const wSort = this.widgets.find((w) => w.name === "sort_by"); const wDesc = this.widgets.find((w) => w.name === "descending"); const wIdx = this.widgets.find((w) => w.name === "index"); if (wIdx) wIdx.hidden = true; /* --------- UI DOM --------- */ const root = document.createElement("div"); const css = ` `; root.innerHTML = ` ${css}

Saisir un dossier puis Refresh.

`; this.addDOMWidget("folder_file_pro", "div", root, {}); this.size = [860, 660]; /* --------- Refs DOM --------- */ const grid = root.querySelector(".ffp-grid"); const pathInput = root.querySelector(".ffp-path"); const upBtn = root.querySelector(".ffp-up"); const goBtn = root.querySelector(".ffp-go"); const exploreBtn = root.querySelector(".ffp-explore"); const viewSel = root.querySelector(".ffp-view"); const countLbl = root.querySelector(".ffp-count"); /* --------- État --------- */ let parentDir = null; let isLoading = false; let selectedPath = null; let lastDir = ""; let lastView = "grid"; let forceResetScroll = false; let typeBuffer = ""; let typeTimer = null; const TYPE_TIMEOUT = 800; /* --------- Helpers --------- */ function cardsArray() { return Array.from(grid.querySelectorAll(".ffp-card")); } function getNameFromCard(card) { const info = card.querySelector(".ffp-info"); return (info ? info.textContent : "").trim(); } function selectCard(card, { scroll = true, updateIndex = true } = {}) { if (!card) return; cardsArray().forEach((c) => c.classList.remove("selected")); card.classList.add("selected"); selectedPath = card.dataset.path || null; if (scroll) card.scrollIntoView({ block: "nearest" }); if (updateIndex && card.dataset.type === "file") resolveAndSelect.call(this, selectedPath); } function selectByIndex(idx) { const list = cardsArray(); if (!list.length) return; if (idx < 0) idx = 0; if (idx >= list.length) idx = list.length - 1; selectCard.call(this, list[idx]); } function currentIndex() { const list = cardsArray(); const cur = grid.querySelector(".ffp-card.selected"); return Math.max(0, list.indexOf(cur || list[0] || null)); } function folderHTML(d) { return ( '
folder
' + '
' + d.name + "
" ); } function fileHTML(f) { if (f.type === "image") { const src = "/folder_file_pro/thumbnail?filepath=" + encodeURIComponent(f.path); return ( '
' + f.name + "
" ); } else if (f.type === "svg") { const src = "/folder_file_pro/view?filepath=" + encodeURIComponent(f.path); return ( '
' + f.name + "
" ); } else { return ( '
' + extBadge(f.ext || ".file") + '
' + f.name + "
" ); } } /* --------- Récupération liste --------- */ async function fetchList() { if (isLoading) return; isLoading = true; const prevScroll = grid.scrollTop; const prevDir = lastDir; const prevView = lastView; grid.style.opacity = "0.6"; grid.innerHTML = ""; selectedPath = null; try { const params = new URLSearchParams({ directory: String(wDir ? wDir.value : ""), exts: String(wExt ? wExt.value : ""), sort_by: String(wSort ? wSort.value : "name"), descending: String(!!(wDesc && wDesc.value)), regex: String(wRe ? wRe.value : ""), regex_mode: String(wReM ? wReM.value : "include"), regex_ic: String(!!(wReIC && wReIC.value)), }).toString(); const res = await api.fetchApi("/folder_file_pro/list?" + params); if (!res.ok) throw new Error("HTTP " + res.status); const data = await res.json(); const curDir = data.current_directory || ""; const curView = viewSel.value; pathInput.value = curDir; parentDir = data.parent_directory || null; upBtn.disabled = !parentDir; countLbl.textContent = data.total_count ? String(data.total_count) + " fichier(s)" : ""; grid.classList.toggle("ffp-list", curView === "list"); (data.dirs || []).forEach((d) => { const card = document.createElement("div"); card.className = "ffp-card"; card.dataset.type = "dir"; card.dataset.path = d.path; card.innerHTML = folderHTML(d); grid.appendChild(card); }); (data.files || []).forEach((f) => { const card = document.createElement("div"); card.className = "ffp-card"; card.dataset.type = "file"; card.dataset.path = f.path; card.innerHTML = fileHTML(f); grid.appendChild(card); }); requestAnimationFrame(() => { const sameDir = curDir === prevDir; const sameView = curView === prevView; if (!sameDir || !sameView || forceResetScroll) grid.scrollTop = 0; else { const maxScroll = Math.max( 0, grid.scrollHeight - grid.clientHeight ); grid.scrollTop = Math.min(prevScroll, maxScroll); } lastDir = curDir; lastView = curView; forceResetScroll = false; grid.focus(); }); } catch (e) { grid.innerHTML = '

' + (e.message || String(e)) + "

"; lastDir = String(wDir ? wDir.value : ""); lastView = viewSel.value; forceResetScroll = false; } finally { isLoading = false; grid.style.opacity = "1"; } } /* --------- Résolution index pour sortie --------- */ async function resolveAndSelect(targetPath) { if (!targetPath) return; try { const params = new URLSearchParams({ directory: String(wDir ? wDir.value : ""), exts: String(wExt ? wExt.value : ""), sort_by: String(wSort ? wSort.value : "name"), descending: String(!!(wDesc && wDesc.value)), regex: String(wRe ? wRe.value : ""), regex_mode: String(wReM ? wReM.value : "include"), regex_ic: String(!!(wReIC && wReIC.value)), path: String(targetPath), }).toString(); const res = await api.fetchApi( "/folder_file_pro/resolve_index?" + params ); const data = await res.json(); const idxW = this.widgets.find((w) => w.name === "index"); if (idxW && data.index >= 0) { idxW.value = data.index; this.setDirtyCanvas(true, true); } } catch (e) { console.warn("[FolderFilePro] resolve_index error", e); } } /* --------- Interactions --------- */ grid.addEventListener("click", (e) => { const card = e.target.closest(".ffp-card"); if (!card) return; selectCard.call(this, card, { scroll: false }); grid.focus(); }); grid.addEventListener("dblclick", (e) => { const card = e.target.closest(".ffp-card"); if (!card) return; const p = card.dataset.path; if (card.dataset.type === "dir") { if (wDir) { wDir.value = p; forceResetScroll = true; fetchList(); } return; } const isImg = /\.(png|jpg|jpeg|bmp|gif|webp|svg)$/i.test(p); const lb = document.getElementById("ffp-lightbox"); const i = lb.querySelector("img"), v = lb.querySelector("video"), a = lb.querySelector("audio"); i.style.display = v.style.display = a.style.display = "none"; v.pause(); a.pause(); if (isImg) { i.src = "/folder_file_pro/view?filepath=" + encodeURIComponent(p); i.style.display = "block"; lb.style.display = "flex"; } else { window.open( "/folder_file_pro/view?filepath=" + encodeURIComponent(p), "_blank" ); } }); // tape-pour-sélectionner function handleType(char) { if (!char) return; typeBuffer += char.toLowerCase(); if (typeTimer) clearTimeout(typeTimer); typeTimer = setTimeout(() => (typeBuffer = ""), TYPE_TIMEOUT); const list = cardsArray(); if (!list.length) return; let start = currentIndex.call(this) + 1; const N = list.length; for (let k = 0; k < N; k++) { const idx = (start + k) % N; const nm = getNameFromCard(list[idx]).toLowerCase(); if (nm.startsWith(typeBuffer)) { selectByIndex.call(this, idx); return; } } } grid.addEventListener("keydown", (e) => { const tag = (document.activeElement && document.activeElement.tagName) || ""; if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return; if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { handleType.call(this, e.key); e.preventDefault(); return; } if (e.key === "Backspace") { typeBuffer = typeBuffer.slice(0, -1); e.preventDefault(); return; } if (e.key === "Escape") { typeBuffer = ""; return; } }); // relier widgets aux refresh function rewire(w) { if (!w) return; const prev = w.callback; w.callback = function () { if (prev) prev.apply(w, arguments); fetchList(); }; } [wDir, wExt, wRe, wReM, wReIC, wSort, wDesc].forEach(rewire); // barre d’actions upBtn.onclick = () => { if (parentDir && wDir) { wDir.value = parentDir; forceResetScroll = true; fetchList(); } }; goBtn.onclick = () => { if (wDir) { wDir.value = pathInput.value; forceResetScroll = true; fetchList(); } }; viewSel.onchange = () => { forceResetScroll = true; fetchList(); }; exploreBtn.onclick = async () => { const toOpen = (selectedPath || (wDir ? wDir.value : "") || "").toString(); if (!toOpen) return; try { await api.fetchApi("/folder_file_pro/open_explorer", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: toOpen }), }); } catch (e) { console.warn("open_explorer failed", e); } }; // init (async () => { try { const r = await api.fetchApi("/folder_file_pro/get_last_path"); const d = await r.json(); const last = d.last_path || (wDir ? wDir.value : ""); if (last) { if (wDir) wDir.value = last; pathInput.value = last; } lastDir = last || ""; lastView = viewSel.value; } catch (e) { /* ignore */ } forceResetScroll = true; fetchList(); })(); }; }, }); console.log("[DAO_master] Folder File Pro UI v4.0 loaded");