import { browseFolder, thumbURL } from "./api.mjs"; import { readState, sortFiles, writeState } from "./state.mjs"; const FOLDER_SVG = ''; function escapeHtml(value) { return String(value).replace(/[&<>"]/g, (char) => ( { "&": "&", "<": "<", ">": ">", '"': """ }[char] )); } export function injectCSS() { if (document.getElementById("dumas-lif-css")) return; const style = document.createElement("style"); style.id = "dumas-lif-css"; style.textContent = ` .dlf-root { display:flex; flex-direction:column; gap:8px; padding:8px 10px; box-sizing:border-box; font-family:inherit; } .dlf-folderrow { display:flex; gap:6px; } .dlf-folder { flex:1; min-width:0; background:#141414; border:1px solid #3a3a3a; border-radius:5px; padding:7px 8px; color:#cfcfcf; font-size:11px; box-sizing:border-box; } .dlf-folder:focus { outline:none; border-color:#e66a2c; } .dlf-browse { display:flex; align-items:center; gap:5px; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.16); border-radius:5px; color:#ddd; font-size:11px; padding:0 9px; cursor:pointer; white-space:nowrap; } .dlf-browse:hover { border-color:#e66a2c; color:#fff; } .dlf-browse svg { width:13px; height:13px; fill:currentColor; } .dlf-pick { background:#e66a2c; border:1px solid #e66a2c; border-radius:6px; padding:8px; font-size:12px; color:#fff; text-align:center; font-weight:500; cursor:pointer; } .dlf-pick:hover { filter:brightness(1.08); } .dlf-pick.empty { background:rgba(255,255,255,0.05); border-color:rgba(255,255,255,0.16); color:#9a9a9a; } .dlf-msg { font-size:11px; color:#e0a33e; line-height:1.4; } .dlf-msg:empty { display:none; } .dlf-menu, .dlf-gallery, .dlf-browse-pop { position:fixed; z-index:99999; background:#191919; box-shadow:0 14px 40px rgba(0,0,0,0.6); } .dlf-menu { border:1px solid #3a3a3a; border-radius:6px; overflow:hidden; min-width:150px; } .dlf-menu .it { padding:7px 11px; font-size:12px; color:#cfcfcf; cursor:pointer; display:flex; justify-content:space-between; gap:14px; } .dlf-menu .it:hover { background:#2a2a2a; } .dlf-menu .it.on { color:#e66a2c; } .dlf-gallery, .dlf-browse-pop { border:1px solid #e66a2c; border-radius:9px; display:flex; flex-direction:column; } .dlf-gallery { max-height:80vh; } .dlf-gal-head { padding:9px 12px; border-bottom:1px solid #333; display:flex; align-items:center; gap:8px; flex-wrap:wrap; } .dlf-tbtn { background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.16); border-radius:5px; padding:5px 10px; font-size:11px; color:#ddd; cursor:pointer; user-select:none; } .dlf-tbtn:hover { border-color:#e66a2c; color:#fff; } .dlf-tbtn.active { border-color:#e66a2c; color:#fff; background:rgba(230,106,44,0.18); } .dlf-firstwrap { display:flex; align-items:center; } .dlf-firstwrap .dlf-tbtn { border-radius:5px 0 0 5px; } .dlf-firstn { width:46px; background:#141414; border:1px solid rgba(255,255,255,0.16); border-left:none; border-radius:0 5px 5px 0; color:#e66a2c; font-size:11px; padding:5px 4px; text-align:center; box-sizing:border-box; } .dlf-firstn:focus { outline:none; border-color:#e66a2c; } .dlf-count { margin-left:auto; font-size:11px; color:#9a9a9a; white-space:nowrap; } .dlf-count b { color:#e66a2c; } .dlf-gal-body { padding:10px 12px; overflow:auto; } .dlf-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(84px,1fr)); gap:7px; } .dlf-thumb { position:relative; aspect-ratio:1; border-radius:5px; border:2px solid transparent; cursor:pointer; overflow:hidden; background:#0f0f0f; } .dlf-thumb img { width:100%; height:100%; object-fit:cover; display:block; } .dlf-thumb .veil { position:absolute; inset:0; background:rgba(0,0,0,0.45); } .dlf-thumb.sel { border-color:#e66a2c; } .dlf-thumb.sel .veil { opacity:0; } .dlf-thumb .chk { position:absolute; top:3px; right:3px; width:16px; height:16px; border-radius:50%; background:#e66a2c; color:#fff; font-size:11px; display:none; align-items:center; justify-content:center; } .dlf-thumb.sel .chk { display:flex; } .dlf-thumb .nm { position:absolute; bottom:0; left:0; right:0; padding:2px 4px; font-size:9px; color:#eee; background:linear-gradient(transparent, rgba(0,0,0,0.75)); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .dlf-gal-empty { padding:30px; text-align:center; color:#888; font-size:12px; grid-column:1/-1; } .dlf-gal-foot { padding:9px 12px; border-top:1px solid #333; display:flex; gap:10px; align-items:center; } .dlf-subf { display:flex; align-items:center; gap:6px; font-size:11px; color:#bbb; cursor:pointer; user-select:none; } .dlf-subf .box { width:12px; height:12px; border:1px solid #555; border-radius:3px; } .dlf-subf.on .box { background:#e66a2c; border-color:#e66a2c; } .dlf-done { margin-left:auto; background:#e66a2c; border:1px solid #e66a2c; border-radius:6px; padding:6px 16px; font-size:12px; color:#fff; cursor:pointer; } .dlf-done:hover { filter:brightness(1.08); } .dlf-bp-head { padding:9px 12px; border-bottom:1px solid #333; font-size:12px; color:#e66a2c; font-weight:600; } .dlf-bp-crumb { padding:7px 12px 4px; font-size:11px; color:#999; word-break:break-all; } .dlf-bp-list { padding:6px 10px 10px; overflow:auto; display:flex; flex-direction:column; gap:4px; } .dlf-bp-item { display:flex; align-items:center; gap:8px; padding:7px 9px; background:#141414; border:1px solid #2c2c2c; border-radius:6px; cursor:pointer; font-size:12px; color:#ddd; } .dlf-bp-item:hover { border-color:#e66a2c; background:#1c1c1c; } .dlf-bp-item svg { width:13px; height:13px; fill:#e66a2c; flex:0 0 auto; } .dlf-bp-item .nm { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .dlf-bp-item .cnt { color:#777; font-size:11px; white-space:nowrap; } .dlf-bp-item.up { color:#9a9a9a; } .dlf-bp-empty { padding:14px; text-align:center; color:#777; font-size:12px; } .dlf-bp-foot { padding:9px 12px; border-top:1px solid #333; display:flex; gap:8px; } `; document.head.appendChild(style); } function positionBelow(popup, anchorEl, width) { const rect = anchorEl.getBoundingClientRect(); popup.style.width = `${width}px`; popup.style.left = `${Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))}px`; popup.style.top = `${rect.bottom + 4}px`; requestAnimationFrame(() => { const popupRect = popup.getBoundingClientRect(); if (popupRect.bottom > window.innerHeight - 8) { popup.style.top = `${Math.max(8, window.innerHeight - 8 - popupRect.height)}px`; } }); } function attachClosePopup(popup, onClose, ignoreSelector) { const close = () => { if (popup._dlfClosed) return; popup._dlfClosed = true; document.removeEventListener("mousedown", onDown, true); document.removeEventListener("pointerdown", onDown, true); document.removeEventListener("wheel", onWheel, true); document.removeEventListener("keydown", onKey, true); popup.remove(); onClose?.(); }; const inside = (target) => popup.contains(target) || (ignoreSelector && target.closest && target.closest(ignoreSelector)); const onDown = (event) => { if (!inside(event.target)) close(); }; const onWheel = (event) => { if (!inside(event.target)) close(); }; const onKey = (event) => { if (event.key === "Escape") close(); }; popup._dlfClose = close; setTimeout(() => { if (popup._dlfClosed) return; document.addEventListener("mousedown", onDown, true); document.addEventListener("pointerdown", onDown, true); document.addEventListener("wheel", onWheel, true); document.addEventListener("keydown", onKey, true); }, 0); return close; } export function openMiniMenu(anchorEl, items, currentValue, onPick) { document.querySelectorAll(".dlf-menu").forEach((menu) => menu._dlfClose?.()); const menu = document.createElement("div"); menu.className = "dlf-menu"; for (const item of items) { const row = document.createElement("div"); row.className = `it${item.value === currentValue ? " on" : ""}`; row.innerHTML = `${escapeHtml(item.label)}`; row.addEventListener("click", () => { menu._dlfClose?.(); onPick(item.value); }); menu.appendChild(row); } document.body.appendChild(menu); positionBelow(menu, anchorEl, Math.max(150, anchorEl.getBoundingClientRect().width)); attachClosePopup(menu); } export function buildRoot() { const root = document.createElement("div"); root.className = "dlf-root"; root.innerHTML = `
` + `` + `` + `
` + `` + `
`; return { root, folderInput: root.querySelector(".dlf-folder"), browseBtn: root.querySelector(".dlf-browse"), browseLbl: root.querySelector(".dlf-browse-lbl"), pickBtn: root.querySelector(".dlf-pick"), msgEl: root.querySelector(".dlf-msg"), }; } const SORTS = [ { value: "name|asc", label: "Name ↑" }, { value: "name|desc", label: "Name ↓" }, { value: "date|asc", label: "Date ↑" }, { value: "date|desc", label: "Date ↓" }, ]; function randomPreviewFile(files) { if (!files.length) return ""; return files[Math.floor(Math.random() * files.length)]?.file || ""; } export function openPickGallery(node, anchorEl, ctx) { document.querySelectorAll(".dlf-gallery").forEach((gallery) => gallery._dlfClose?.()); const gallery = document.createElement("div"); gallery.className = "dlf-gallery"; gallery.innerHTML = `
` + `
Select all
` + `
None
` + `
First
` + `
` + `
Select random
` + `
0 / 0 active
` + `
` + `
` + `
` + `
Include subfolders
` + `
Sort: Name ↑
` + `
Done
` + `
`; document.body.appendChild(gallery); const grid = gallery.querySelector(".dlf-grid"); const countCurrent = gallery.querySelector(".dlf-cn"); const countTotal = gallery.querySelector(".dlf-ct"); const firstInput = gallery.querySelector(".dlf-firstn"); const recursiveToggle = gallery.querySelector(".dlf-subf"); const sortButton = gallery.querySelector('[data-act="sort"]'); const randomButton = gallery.querySelector('[data-act="random"]'); const allButton = gallery.querySelector('[data-act="all"]'); const firstButton = gallery.querySelector('[data-act="first"]'); let state = readState(node); const manualSelection = new Set(state.selected || []); let randomPreview = ""; function activeFiles() { const files = sortFiles(node._dlfFiles || [], state.sort, state.sort_dir); if (state.selection_mode === "all") return new Set(files.map((file) => file.file)); if (state.selection_mode === "first_n") { const count = Math.max(0, Math.min(parseInt(state.first_n, 10) || 0, files.length)); return new Set(files.slice(0, count).map((file) => file.file)); } if (state.selection_mode === "random") { if (!randomPreview || !files.some((file) => file.file === randomPreview)) { randomPreview = randomPreviewFile(files); } return randomPreview ? new Set([randomPreview]) : new Set(); } return manualSelection; } function commit() { const fresh = readState(node); fresh.selected = [...manualSelection]; fresh.sort = state.sort; fresh.sort_dir = state.sort_dir; fresh.recursive = state.recursive; fresh.selection_mode = state.selection_mode; fresh.first_n = Math.max(1, parseInt(firstInput.value, 10) || 1); writeState(node, fresh); state = fresh; ctx.onChange?.(node); } function renderGrid() { grid.innerHTML = ""; const files = sortFiles(node._dlfFiles || [], state.sort, state.sort_dir); const active = activeFiles(); sortButton.textContent = `Sort: ${SORTS.find((item) => item.value === `${state.sort}|${state.sort_dir}`)?.label || "Name ↑"}`; recursiveToggle.classList.toggle("on", !!state.recursive); allButton.classList.toggle("active", state.selection_mode === "all"); firstButton.classList.toggle("active", state.selection_mode === "first_n"); randomButton.classList.toggle("active", state.selection_mode === "random"); countCurrent.textContent = active.size; countTotal.textContent = files.length; firstInput.value = String(Math.max(1, parseInt(state.first_n, 10) || 1)); firstInput.max = String(files.length || 1); if (!files.length) { const empty = document.createElement("div"); empty.className = "dlf-gal-empty"; empty.textContent = node._dlfListError || "No images in this folder."; grid.appendChild(empty); return; } for (const file of files) { const cell = document.createElement("div"); cell.className = `dlf-thumb${active.has(file.file) ? " sel" : ""}`; cell.innerHTML = `` + `
` + `
${escapeHtml(file.name)}
`; cell.addEventListener("click", () => { state.selection_mode = "selected"; randomPreview = ""; if (manualSelection.has(file.file)) manualSelection.delete(file.file); else manualSelection.add(file.file); commit(); renderGrid(); }); grid.appendChild(cell); } } gallery.querySelector('[data-act="all"]').addEventListener("click", () => { state.selection_mode = "all"; randomPreview = ""; commit(); renderGrid(); }); gallery.querySelector('[data-act="none"]').addEventListener("click", () => { state.selection_mode = "selected"; manualSelection.clear(); randomPreview = ""; commit(); renderGrid(); }); gallery.querySelector('[data-act="first"]').addEventListener("click", () => { state.selection_mode = "first_n"; randomPreview = ""; commit(); renderGrid(); }); gallery.querySelector('[data-act="random"]').addEventListener("click", () => { state.selection_mode = "random"; randomPreview = randomPreviewFile(sortFiles(node._dlfFiles || [], state.sort, state.sort_dir)); commit(); renderGrid(); }); firstInput.addEventListener("input", () => { if (state.selection_mode !== "first_n") return; commit(); renderGrid(); }); firstInput.addEventListener("keydown", (event) => { event.stopImmediatePropagation(); if (event.key === "Enter") { event.preventDefault(); commit(); renderGrid(); } }); sortButton.addEventListener("click", () => { openMiniMenu(sortButton, SORTS, `${state.sort}|${state.sort_dir}`, (value) => { const [sort, sortDir] = value.split("|"); state.sort = sort; state.sort_dir = sortDir; if (state.selection_mode === "random") { randomPreview = randomPreviewFile(sortFiles(node._dlfFiles || [], state.sort, state.sort_dir)); } commit(); renderGrid(); }); }); recursiveToggle.addEventListener("click", async () => { state.recursive = !state.recursive; writeState(node, state); grid.innerHTML = '
Loading…
'; await ctx.refreshListing(node); if (gallery._dlfClosed) return; state = readState(node); manualSelection.clear(); (state.selected || []).forEach((file) => manualSelection.add(file)); randomPreview = ""; renderGrid(); }); gallery.querySelector('[data-act="done"]').addEventListener("click", () => gallery._dlfClose?.()); node._dlfGallery = gallery; attachClosePopup(gallery, () => { document.querySelectorAll(".dlf-menu").forEach((menu) => menu._dlfClose?.()); if (node._dlfGallery === gallery) node._dlfGallery = null; }, ".dlf-menu"); positionBelow(gallery, anchorEl, Math.min(560, window.innerWidth - 16)); renderGrid(); } export function openBrowsePopup(node, anchorEl, ctx) { document.querySelectorAll(".dlf-browse-pop").forEach((popup) => popup._dlfClose?.()); const popup = document.createElement("div"); popup.className = "dlf-browse-pop"; popup.innerHTML = `
Choose a folder
` + `
` + `
` + `
` + `
Cancel
` + `
Use this folder
` + `
`; document.body.appendChild(popup); const crumb = popup.querySelector(".dlf-bp-crumb"); const list = popup.querySelector(".dlf-bp-list"); const useButton = popup.querySelector('[data-act="use"]'); let current = ctx.startPath || ""; async function nav(path) { list.innerHTML = '
Loading…
'; const response = await browseFolder(path); if (popup._dlfClosed) return; if (!response.ok) { list.innerHTML = `
${escapeHtml(response.message || "Could not open this folder.")}
`; return; } current = response.path || ""; useButton.style.opacity = current ? "" : "0.4"; useButton.style.pointerEvents = current ? "" : "none"; crumb.innerHTML = current ? `Location: ${escapeHtml(current)}` : "This PC"; list.innerHTML = ""; if (response.parent !== null && response.parent !== undefined) { const up = document.createElement("div"); up.className = "dlf-bp-item up"; up.innerHTML = ' .. (up one level)'; up.addEventListener("click", () => nav(response.parent || "")); list.appendChild(up); } if (!response.dirs.length) { const empty = document.createElement("div"); empty.className = "dlf-bp-empty"; empty.textContent = current ? "No sub-folders here. Use this folder." : "No drives found."; list.appendChild(empty); } for (const dir of response.dirs) { const item = document.createElement("div"); item.className = "dlf-bp-item"; const count = dir.images >= 0 ? `${dir.images} image${dir.images === 1 ? "" : "s"}` : ""; item.innerHTML = `${FOLDER_SVG}${escapeHtml(dir.name)}${count}`; item.addEventListener("click", () => nav(dir.path)); list.appendChild(item); } } popup.querySelector('[data-act="cancel"]').addEventListener("click", () => popup._dlfClose?.()); popup.querySelector('[data-act="use"]').addEventListener("click", () => { if (current) ctx.onPick(current); popup._dlfClose?.(); }); node._dlfBrowsePopup = popup; attachClosePopup(popup, () => { if (node._dlfBrowsePopup === popup) node._dlfBrowsePopup = null; }); positionBelow(popup, anchorEl, Math.min(440, window.innerWidth - 16)); nav(current); }