// ===== Globals ===== let bulkDeleteMode = false; // ===== Bulk Delete State ===== setBulkMode(false); const selectedForDeletion = new Set(); // keys as `${service}||${identifier||'default'}` function selKey(service, identifier) { return `${service}||${identifier || "default"}`; } function getSelectedCount() { return selectedForDeletion.size; } function clearSelected() { selectedForDeletion.clear(); updateBulkControlsUI(); } let _userPublicKey = ""; // reserved for future publish/auth flows let userAddress = ""; let userName = ""; let isAuthenticated = false; let allNames = []; let authStatus = "idle"; let _namesStatus = "idle"; // future UI state for name loading let _allResults = []; // planned for search results caching let metadataArray = []; // ===== Search State ===== const LS_LAST_SEARCH_KEY = "qedit:lastSearch"; let searchState = { params: null, results: [], offset: 0, limit: 100, hasMore: false, inFlight: false, }; // Pagination let currentPage = 1; let itemsPerPage = 25; let totalResults = 0; // Render helper bridge (safe fallback if render helper isn't loaded yet) function renderIntoCompat(el, htmlString, mode) { if (!el) { return; } try { if (window.QEditRender && typeof window.QEditRender.renderInto === "function") { window.QEditRender.renderInto(el, htmlString, mode || "replace"); return; } } catch {} if (mode === "append") { el.insertAdjacentHTML("beforeend", htmlString); } else if (mode === "prepend") { el.insertAdjacentHTML("afterbegin", htmlString); } else { el.innerHTML = htmlString; } } // Pagination preferences (storage + hash) const LS_ITEMS_PER_PAGE_KEY = "qedit_itemsPerPage"; function getPageFromHash() { const h = (location.hash || "").toLowerCase(); const m = h.match(/page=(\d+)/); if (m) { const p = parseInt(m[1], 10); if (!isNaN(p) && p >= 1) { return p; } } return null; } function setPageHash(p) { try { const newHash = `#page=${p}`; if (location.hash !== newHash) { location.hash = newHash; } } catch {} } function initPaginationPrefs() { // itemsPerPage from localStorage try { const stored = localStorage.getItem(LS_ITEMS_PER_PAGE_KEY); const sel = document.getElementById("items-per-page-dropdown"); if (stored) { const v = parseInt(stored, 10); if (!isNaN(v) && v > 0) { itemsPerPage = v; if (sel) { sel.value = String(v); } } } else { if (sel) { sel.value = String(itemsPerPage); } } } catch {} // page from hash const hp = getPageFromHash(); if (hp) { currentPage = hp; } // react to browser hash navigation window.addEventListener("hashchange", async () => { const hp2 = getPageFromHash(); if (hp2 && hp2 !== currentPage) { currentPage = hp2; await fetchPage(); } }); } let totalSize = 0; let _currentServiceFilter = "ALL"; // reserved: legacy filter placeholder const infoDetails = ` Click the identifier to "delete" content.
(This will replace it with a blank file.)

Click the file select icon to "edit" content.
(This will replace it with a selected file.)`; // Data sets for chips-based filtering let masterResults = []; // full unfiltered list for current name let filteredResults = []; // results after chips selection let selectedServices = new Set(); // inclusion set; empty => show all let serviceCounts = {}; // { service: count } // === Tree-driven filters === let currentPrefixFilter = null; // e.g., "qtube" let currentIdentifierFilter = null; // exact identifier; if set, we will show inline preview function clearTreeFilters() { currentPrefixFilter = null; currentIdentifierFilter = null; } // === Deleted-content filter (default ON) === const TINY_SIZE_THRESHOLD = 128; // bytes; QDN may report ~32B for 1B newline let hideDeleted = localStorage.getItem("hideDeleted") !== "false"; // default true function getBaselineResults() { // When hiding deletions, exclude items flagged as deleted if (!hideDeleted) { return masterResults; } return masterResults.filter((r) => !r.__isDeleted); } function recomputeServiceCounts() { serviceCounts = {}; const base = getBaselineResults(); for (const r of base) { const svc = r.service || "UNKNOWN"; serviceCounts[svc] = (serviceCounts[svc] || 0) + 1; } } // ===== DOM & UI helpers ===== const contentPage = document.getElementById("content-page"); const authButton = document.getElementById("auth-button"); const nameSwitcherEl = document.getElementById("name-switcher"); const nameSelectEl = document.getElementById("name-select"); const loadingOverlay = document.getElementById("loading-overlay"); function showSpinner() { if (loadingOverlay) { loadingOverlay.style.display = "flex"; } } function hideSpinner() { if (loadingOverlay) { loadingOverlay.style.display = "none"; } } // ===== Sidebar toggle fallback (in case module init fails) ===== (function initSidebarToggleFallback() { // Expose a very simple global fallback toggler for inline onclick usage // This ensures the sidebar can always be toggled even if event wiring changes. window.QEditToggleSidebar = function (action) { try { const shell = document.getElementById("app-shell"); const sidebar = document.getElementById("sidebar"); const collapseBtn = document.getElementById("sidebar-collapse"); const revealBtn = document.getElementById("sidebar-reveal"); if (!shell || !sidebar) { return false; } const currentlyHidden = sidebar.style.display === "none" || sidebar.getAttribute("aria-hidden") === "true" || shell.classList.contains("is-collapsed"); let collapse; if (action === "show") { collapse = false; } else if (action === "hide") { collapse = true; } else { collapse = !currentlyHidden; } function apply(collapsed) { if (collapsed) { shell.classList.add("is-collapsed"); sidebar.style.display = "none"; sidebar.setAttribute("aria-hidden", "true"); if (collapseBtn) { collapseBtn.setAttribute("aria-pressed", "true"); collapseBtn.setAttribute("aria-expanded", "false"); } if (revealBtn) { revealBtn.style.display = "inline-block"; } } else { shell.classList.remove("is-collapsed"); sidebar.style.display = ""; sidebar.removeAttribute("aria-hidden"); if (collapseBtn) { collapseBtn.setAttribute("aria-pressed", "false"); collapseBtn.setAttribute("aria-expanded", "true"); } if (revealBtn) { revealBtn.style.display = "none"; } } try { window.localStorage.setItem("qedit:sidebarCollapsed", String(!!collapsed)); } catch {} } apply(collapse); } catch {} return false; }; function getStoredCollapsed() { try { const v = window.localStorage.getItem("qedit:sidebarCollapsed"); return v === "true"; } catch { return false; } } function setStoredCollapsed(val) { try { window.localStorage.setItem("qedit:sidebarCollapsed", String(!!val)); } catch {} } function applyCollapsed(collapsed) { const shell = document.getElementById("app-shell"); const sidebar = document.getElementById("sidebar"); const collapseBtn = document.getElementById("sidebar-collapse"); const revealBtn = document.getElementById("sidebar-reveal"); if (!shell || !sidebar) { return; } if (collapsed) { // Simple, robust: hide the sidebar directly shell.classList.add("is-collapsed"); sidebar.style.display = "none"; sidebar.setAttribute("aria-hidden", "true"); if (collapseBtn) { collapseBtn.setAttribute("aria-pressed", "true"); collapseBtn.setAttribute("aria-expanded", "false"); } if (revealBtn) { revealBtn.style.display = "inline-block"; } } else { shell.classList.remove("is-collapsed"); sidebar.style.display = ""; sidebar.removeAttribute("aria-hidden"); if (collapseBtn) { collapseBtn.setAttribute("aria-pressed", "false"); collapseBtn.setAttribute("aria-expanded", "true"); } if (revealBtn) { revealBtn.style.display = "none"; } } } function bind() { const shell = document.getElementById("app-shell"); if (!shell || shell.dataset.sidebarInitialized === "1") { return; // module already bound } shell.dataset.sidebarInitialized = "1"; applyCollapsed(getStoredCollapsed()); const collapseBtn = document.getElementById("sidebar-collapse"); const revealBtn = document.getElementById("sidebar-reveal"); if (collapseBtn) { collapseBtn.addEventListener("click", () => { const nowCollapsed = !shell.classList.contains("is-collapsed"); applyCollapsed(nowCollapsed); setStoredCollapsed(nowCollapsed); }); } if (revealBtn) { revealBtn.addEventListener("click", () => { applyCollapsed(false); setStoredCollapsed(false); }); } document.addEventListener("click", (e) => { // Robustly handle clicks that originate on Text/SVG nodes by finding a nearby Element const targetEl = e.target && e.target instanceof Element ? e.target : e.target && /** @type {any} */ (e.target).parentElement ? /** @type {HTMLElement} */ (/** @type {any} */ (e.target).parentElement) : null; const el = targetEl ? targetEl.closest("#sidebar-collapse, #sidebar-reveal") : null; if (!el) { return; } e.preventDefault(); if (el.id === "sidebar-collapse") { const nowCollapsed = !shell.classList.contains("is-collapsed"); applyCollapsed(nowCollapsed); setStoredCollapsed(nowCollapsed); } else if (el.id === "sidebar-reveal") { applyCollapsed(false); setStoredCollapsed(false); } }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bind, { once: true }); } else { bind(); } })(); // ===== View Controller: show only one main section at a time ===== const Sections = { search: document.getElementById("search-page"), info: document.getElementById("info-page"), content: document.getElementById("content-page"), preview: document.getElementById("preview-page"), compose: document.getElementById("compose-page"), }; function showSection(which) { // Promote any currently playing media before switching away try { const prev = Object.entries(Sections).find(([_k, v]) => v && v.style.display !== "none"); const prevKey = prev ? prev[0] : null; if (prevKey && prevKey !== which) { const from = Sections[prevKey]; if (from) { const av = from.querySelector("video, audio"); const shouldPromote = av && (av.paused === false || ((av.currentTime || 0) > 0 && av.ended === false)); if (shouldPromote) { promoteMedia(av, { service: av.getAttribute("data-service") || "", identifier: av.getAttribute("data-identifier") || "default", name: av.getAttribute("data-name") || userName, }); } } } } catch {} for (const [key, el] of Object.entries(Sections)) { if (!el) { continue; } el.style.display = key === which ? "block" : "none"; } } // ===== Search mode state ===== let searchModeActive = false; function updateSearchButtonUI() { const btn = document.getElementById("search-button"); if (!btn) { return; } btn.setAttribute("aria-pressed", String(!!searchModeActive)); const isOn = !!searchModeActive; btn.setAttribute("title", isOn ? "Search (open)" : "Search"); btn.setAttribute("aria-label", isOn ? "Search is open" : "Search"); } function setSearchMode(on) { searchModeActive = !!on; updateSidebarBanner(); updateSearchButtonUI(); showSection(on ? "search" : "content"); if (searchModeActive) { try { initSearchUI(); } catch {} } } // ===== Sidebar tree ===== function buildSidebarTree() { const tree = document.getElementById("file-tree"); if (!tree) { return; } tree.innerHTML = ""; const base = getBaselineResults(); // Build service -> [items] const byService = {}; for (const r of base) { const svc = r.service || "UNKNOWN"; (byService[svc] = byService[svc] || []).push(r); } const services = Object.keys(byService).sort(); for (const svc of services) { const items = byService[svc]; if (!items || items.length === 0) { continue; } const svcNode = document.createElement("div"); svcNode.className = "tree-node"; const line = document.createElement("div"); line.className = "tree-line"; line.setAttribute("role", "treeitem"); line.setAttribute("aria-expanded", "false"); const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "tree-toggle"; toggle.textContent = "▶"; toggle.addEventListener("click", (e) => { e.stopPropagation(); const open = children.classList.toggle("expanded"); line.setAttribute("aria-expanded", String(open)); toggle.textContent = open ? "▼" : "▶"; }); const label = document.createElement("span"); label.className = "tree-label"; label.textContent = svc; label.addEventListener("click", async () => { // Filter to this service selectedServices = new Set([svc]); clearTreeFilters(); applyServiceFilter(); showSpinner(); contentPage.style.display = "none"; try { await fetchPage(); } finally { showSection("content"); hideSpinner(); } }); const count = document.createElement("span"); count.className = "tree-count"; count.textContent = String(items.length); const children = document.createElement("div"); children.className = "tree-children"; children.setAttribute("role", "group"); // Group by identifier prefix (support '_' and '-') with 'q' special-case const byPrefix = {}; function firstSepIndexAny(str) { const u = str.indexOf("_"); const d = str.indexOf("-"); if (u === -1) { return d; } if (d === -1) { return u; } return Math.min(u, d); } function computePrefix(id) { if (!id) { return ""; } const idx1 = firstSepIndexAny(id); if (idx1 <= 0) { return ""; } let pfx = id.slice(0, idx1); if (pfx === "q") { const rest = id.slice(idx1 + 1); const idx2 = firstSepIndexAny(rest); if (idx2 > 0) { return id.slice(0, idx1 + 1 + idx2); } // No second separator: treat as single-item group (will render as leaf) return id; } return pfx; } for (const r of items) { const id = r.identifier || ""; const pfx = computePrefix(id); (byPrefix[pfx] = byPrefix[pfx] || []).push(r); } const prefixes = Object.keys(byPrefix).sort(); const groups = prefixes.filter((p) => p && byPrefix[p].length > 1); const singlePrefixCoversAll = groups.length === 1 && byPrefix[groups[0]].length === items.length; if (singlePrefixCoversAll) { // Flatten: render all items directly items.slice(0, 2000).forEach((r) => { const leaf = document.createElement("div"); leaf.className = "tree-node"; const lline = document.createElement("div"); lline.className = "tree-line"; lline.setAttribute("role", "treeitem"); const llabel = document.createElement("span"); llabel.className = "tree-label"; llabel.textContent = r.identifier === undefined || r.identifier === null || r.identifier === "" ? "default" : r.identifier; llabel.title = r.identifier || ""; llabel.addEventListener("click", () => { openPreviewPage({ service: r.service, identifier: r.identifier, name: r.name || userName, }); }); lline.appendChild(document.createElement("span")); lline.appendChild(llabel); children.appendChild(lline); }); } else { // Render leaves (no prefix or single-item groups) first const leaves = []; for (const pfx of prefixes) { const arr = byPrefix[pfx]; if (!pfx || arr.length === 1) { leaves.push(...arr); } } leaves.slice(0, 2000).forEach((r) => { const leaf = document.createElement("div"); leaf.className = "tree-node"; const lline = document.createElement("div"); lline.className = "tree-line"; lline.setAttribute("role", "treeitem"); const llabel = document.createElement("span"); llabel.className = "tree-label"; llabel.textContent = r.identifier === undefined || r.identifier === null || r.identifier === "" ? "default" : r.identifier; llabel.title = r.identifier || ""; llabel.addEventListener("click", () => { openPreviewPage({ service: r.service, identifier: r.identifier, name: r.name || userName, }); }); lline.appendChild(document.createElement("span")); lline.appendChild(llabel); children.appendChild(lline); }); // Then render multi-item groups for (const pfx of groups) { const arr = byPrefix[pfx]; const pNode = document.createElement("div"); pNode.className = "tree-node"; const pline = document.createElement("div"); pline.className = "tree-line"; pline.setAttribute("role", "treeitem"); pline.setAttribute("aria-expanded", "false"); const ptoggle = document.createElement("button"); ptoggle.type = "button"; ptoggle.className = "tree-toggle"; ptoggle.textContent = "▶"; ptoggle.addEventListener("click", (e) => { e.stopPropagation(); const open = pchildren.classList.toggle("expanded"); pline.setAttribute("aria-expanded", String(open)); ptoggle.textContent = open ? "▼" : "▶"; }); const plabel = document.createElement("span"); plabel.className = "tree-label"; plabel.textContent = pfx; plabel.addEventListener("click", async () => { // Apply service + prefix filter selectedServices = new Set([svc]); currentPrefixFilter = pfx; currentIdentifierFilter = null; applyServiceFilter(); showSpinner(); contentPage.style.display = "none"; try { await fetchPage(); } finally { showSection("content"); hideSpinner(); } }); const pcount = document.createElement("span"); pcount.className = "tree-count"; pcount.textContent = String(arr.length); const pchildren = document.createElement("div"); pchildren.className = "tree-children"; pchildren.setAttribute("role", "group"); arr.slice(0, 2000).forEach((r) => { const leaf = document.createElement("div"); leaf.className = "tree-node"; const lline = document.createElement("div"); lline.className = "tree-line"; lline.setAttribute("role", "treeitem"); const llabel = document.createElement("span"); llabel.className = "tree-label"; llabel.textContent = r.identifier === undefined || r.identifier === null || r.identifier === "" ? "default" : r.identifier; llabel.title = r.identifier || ""; llabel.addEventListener("click", () => { openPreviewPage({ service: r.service, identifier: r.identifier, name: r.name || userName, }); }); lline.appendChild(document.createElement("span")); lline.appendChild(llabel); pchildren.appendChild(lline); }); pline.appendChild(ptoggle); pline.appendChild(plabel); pline.appendChild(pcount); pNode.appendChild(pline); pNode.appendChild(pchildren); children.appendChild(pNode); } } line.appendChild(toggle); line.appendChild(label); line.appendChild(count); svcNode.appendChild(line); svcNode.appendChild(children); tree.appendChild(svcNode); } // Update banner const nm = document.getElementById("sidebar-name"); if (nm) { nm.textContent = userName || "(not authenticated)"; } } // Show inline preview on the content page (used when desired; currently using full Preview page) async function _showInlinePreview(ctx) { try { const container = document.getElementById("inline-viewer"); if (!container) { return; } container.style.display = "block"; await loadPreviewInto(container, ctx); try { container.scrollIntoView({ behavior: "smooth", block: "start" }); } catch {} } catch (e) { console.error("Inline preview failed", e); const container = document.getElementById("inline-viewer"); if (container) { container.textContent = "Preview failed: " + ((e && e.message) || e); } } } // Auth UI updater (visibility + labels) function updateAuthUI() { if (isAuthenticated) { authButton.style.display = "none"; nameSwitcherEl.style.display = "flex"; nameSelectEl.innerHTML = ""; for (const n of allNames) { const opt = document.createElement("option"); opt.value = n.name; opt.textContent = n.name || "(no name)"; if (n.name === userName) { opt.selected = true; } nameSelectEl.appendChild(opt); } } else { authButton.style.display = "inline-block"; nameSwitcherEl.style.display = "none"; } authButton.disabled = authStatus === "loading"; authButton.textContent = authStatus === "loading" ? "Authenticating…" : "Authenticate"; try { updatePublishMenuUI(); } catch {} } // Wire listeners authButton.addEventListener("click", () => { if (authStatus === "loading") { return; } accountLogin(); }); nameSelectEl.addEventListener("change", (e) => { switchActiveName(e.target.value); }); document.getElementById("items-per-page-dropdown").addEventListener("change", async function () { showSpinner(); contentPage.style.display = "none"; try { itemsPerPage = parseInt(this.value, 10); try { localStorage.setItem(LS_ITEMS_PER_PAGE_KEY, String(itemsPerPage)); } catch {} currentPage = 1; setPageHash(currentPage); await fetchPage(); } finally { showSection("content"); hideSpinner(); } }); // === Auto-auth on load (runs once) === (function autoAuthOnce() { // Ensure DOM is ready before trying to auth if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", autoAuthOnce, { once: true }); return; } // Guard: don't double-trigger if (!isAuthenticated && authStatus !== "loading") { accountLogin().catch((err) => console.error("Auto-auth failed:", err)); } })(); initPaginationPrefs(); updateAuthUI(); updateSidebarBanner(); updateSearchButtonUI(); initHideDeletedUI(); initPublishMenuUI(); updatePublishMenuUI(); function initHideDeletedUI() { const btn = document.getElementById("toggle-deleted"); if (!btn) { return; } function updateToggleUI() { btn.setAttribute("aria-pressed", String(hideDeleted)); const on = hideDeleted; btn.setAttribute("title", on ? "Hiding deleted content" : "Showing deleted content"); btn.setAttribute("aria-label", on ? "Hide deleted content: ON" : "Hide deleted content: OFF"); } // Set initial state each time updateToggleUI(); // Bind once if (btn.dataset.bound === "1") { return; } btn.dataset.bound = "1"; btn.addEventListener("click", async () => { hideDeleted = !hideDeleted; try { localStorage.setItem("hideDeleted", String(hideDeleted)); } catch {} updateToggleUI(); showSpinner(); contentPage.style.display = "none"; try { recomputeServiceCounts(); buildServiceChips(); buildSidebarTree(); applyServiceFilter(); await fetchPage(); } finally { contentPage.style.display = "block"; hideSpinner(); } }); } // ===== Publish menu (auth-gated) ===== function updatePublishMenuUI() { const wrap = document.getElementById("publish-menu"); const btn = document.getElementById("publish-button"); if (!wrap || !btn) { return; } wrap.style.display = isAuthenticated ? "inline-block" : "none"; btn.setAttribute("aria-expanded", "false"); wrap.classList.remove("open"); } function closePublishDropdown() { const wrap = document.getElementById("publish-menu"); const btn = document.getElementById("publish-button"); const dd = document.getElementById("publish-dropdown"); if (wrap && btn && dd) { wrap.classList.remove("open"); btn.setAttribute("aria-expanded", "false"); dd.setAttribute("aria-hidden", "true"); } } function togglePublishDropdown() { const wrap = document.getElementById("publish-menu"); const btn = document.getElementById("publish-button"); const dd = document.getElementById("publish-dropdown"); if (!wrap || !btn || !dd) { return; } const open = !wrap.classList.contains("open"); if (open) { wrap.classList.add("open"); btn.setAttribute("aria-expanded", "true"); dd.setAttribute("aria-hidden", "false"); } else { closePublishDropdown(); } } function initPublishMenuUI() { const wrap = document.getElementById("publish-menu"); const btn = document.getElementById("publish-button"); const itemFile = document.getElementById("publish-add-file"); const itemFolder = document.getElementById("publish-add-folder"); const itemText = document.getElementById("publish-new-text"); if (!wrap || !btn || !itemFile || !itemFolder || !itemText) { return; } if (wrap.dataset.bound === "1") { return; } wrap.dataset.bound = "1"; btn.addEventListener("click", (e) => { e.stopPropagation(); if (!isAuthenticated) { return; } togglePublishDropdown(); }); document.addEventListener("click", (e) => { const t = e.target instanceof Element ? e.target : null; if (t && t.closest && t.closest("#publish-menu")) { return; } closePublishDropdown(); }); itemFile.addEventListener("click", async () => { closePublishDropdown(); await handlePublishAddFile(); }); itemFolder.addEventListener("click", async () => { closePublishDropdown(); await handlePublishAddFolder(); }); itemText.addEventListener("click", async () => { closePublishDropdown(); await openComposePage({}); }); } // ===== QDN Preview helpers ===== async function openPreviewPage(ctx) { showSection("preview"); currentPreviewCtx = ctx; const titleEl = document.getElementById("preview-title"); if (titleEl) { const ident = ctx.identifier || "default"; titleEl.textContent = `${ctx.service} — ${ident} — ${ctx.name || ""}`; } // Inject a simple filepath control for multi-file services (WEBSITE/APP/etc.) (function () { const host = document.getElementById("preview-actions"); if (!host) { return; } // Remove any prior controls to avoid duplicates const old = host.querySelector(".preview-path-controls"); if (old) { try { host.removeChild(old); } catch {} } if (!isMultiFileService(ctx.service)) { return; } const wrap = document.createElement("div"); wrap.className = "preview-path-controls"; wrap.style.display = "inline-flex"; wrap.style.gap = "6px"; wrap.style.marginLeft = "12px"; const label = document.createElement("label"); label.textContent = "Path:"; label.style.alignSelf = "center"; label.setAttribute("for", "preview-filepath-input"); const input = document.createElement("input"); input.type = "text"; input.id = "preview-filepath-input"; input.placeholder = "index.html"; input.style.minWidth = "220px"; input.value = (ctx && ctx.filepath) || ""; const go = document.createElement("button"); go.type = "button"; go.textContent = "Open"; go.addEventListener("click", async () => { const path = input.value.trim(); currentPreviewCtx = { ...currentPreviewCtx, filepath: path }; const container = document.getElementById("preview-container"); if (container) { container.innerHTML = "Loading preview…"; await loadPreviewInto(container, currentPreviewCtx); } }); input.addEventListener("keydown", async (e) => { if (e.key === "Enter") { e.preventDefault(); go.click(); } }); wrap.appendChild(label); wrap.appendChild(input); wrap.appendChild(go); host.appendChild(wrap); })(); // Preview header actions wiring (bind once to avoid duplicates across navigations) (function () { const previewEditBtn = document.getElementById("preview-edit"); const previewReplaceBtn = document.getElementById("preview-replace"); const previewDeleteBtn = document.getElementById("preview-delete"); function hasCtx() { return !!(typeof currentPreviewCtx !== "undefined" && currentPreviewCtx); } function identOrDefault(x) { return x && x.identifier ? x.identifier : "default"; } function refresh() { if (typeof updatePreviewActionsState === "function") { updatePreviewActionsState(); } } // Bind each button at most once; handlers read currentPreviewCtx dynamically. if (previewEditBtn && previewEditBtn.dataset.bound !== "1") { previewEditBtn.dataset.bound = "1"; previewEditBtn.addEventListener("click", async () => { if (!hasCtx()) { return; } await editContent(currentPreviewCtx.service, identOrDefault(currentPreviewCtx)); refresh(); }); } if (previewReplaceBtn && previewReplaceBtn.dataset.bound !== "1") { previewReplaceBtn.dataset.bound = "1"; previewReplaceBtn.addEventListener("click", async () => { if (!hasCtx()) { return; } await replaceContent(currentPreviewCtx.service, identOrDefault(currentPreviewCtx)); refresh(); }); } if (previewDeleteBtn && previewDeleteBtn.dataset.bound !== "1") { previewDeleteBtn.dataset.bound = "1"; previewDeleteBtn.addEventListener("click", async () => { if (!hasCtx()) { return; } await deleteContent(currentPreviewCtx.service, identOrDefault(currentPreviewCtx)); refresh(); }); } })(); const container = document.getElementById("preview-container"); if (container) { // If a media element is already playing in the preview container, promote it try { const av = container.querySelector("video, audio"); const shouldPromote = av && (av.paused === false || ((av.currentTime || 0) > 0 && av.ended === false)); if (shouldPromote) { console.debug("[Q-Edit] promoting media before new preview", { service: av.getAttribute("data-service"), identifier: av.getAttribute("data-identifier"), name: av.getAttribute("data-name"), }); promoteMedia(av, { service: av.getAttribute("data-service") || "", identifier: av.getAttribute("data-identifier") || "default", name: av.getAttribute("data-name") || userName, }); } } catch {} container.innerHTML = "Loading preview…"; await loadPreviewInto(container, ctx); } updatePreviewActionsState(); } function buildQdnParams(base) { const p = { ...base }; if (!p.identifier || p.identifier === "" || p.identifier === "default") { delete p.identifier; } return p; } function b64ToBytes(b64) { const bin = atob(b64); const len = bin.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = bin.charCodeAt(i); } return bytes; } function detectMimeFromBytes(bytes) { const h = bytes; const h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3], h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7], h8 = h[8], h9 = h[9], h10 = h[10], h11 = h[11]; // Images if (h0 === 0xff && h1 === 0xd8 && h2 === 0xff) { return "image/jpeg"; } if (h0 === 0x89 && h1 === 0x50 && h2 === 0x4e && h3 === 0x47) { return "image/png"; } if (h0 === 0x47 && h1 === 0x49 && h2 === 0x46 && h3 === 0x38) { return "image/gif"; } if ( h0 === 0x52 && h1 === 0x49 && h2 === 0x46 && h3 === 0x46 && h8 === 0x57 && h9 === 0x45 && h10 === 0x42 && h11 === 0x50 ) { return "image/webp"; } // PDF if (h0 === 0x25 && h1 === 0x50 && h2 === 0x44 && h3 === 0x46) { return "application/pdf"; } // ZIP/OOXML/EPUB containers (PK\x03\x04) if (h0 === 0x50 && h1 === 0x4b && h2 === 0x03 && h3 === 0x04) { return "application/zip"; } // Audio if (h0 === 0x49 && h1 === 0x44 && h2 === 0x33) { return "audio/mpeg"; } if (h0 === 0xff && (h1 & 0xe0) === 0xe0) { return "audio/mpeg"; } if (h0 === 0x4f && h1 === 0x67 && h2 === 0x67 && h3 === 0x53) { return "audio/ogg"; } if ( h0 === 0x52 && h1 === 0x49 && h2 === 0x46 && h3 === 0x46 && h8 === 0x57 && h9 === 0x41 && h10 === 0x56 && h11 === 0x45 ) { return "audio/wav"; } // Video if (h0 === 0x1a && h1 === 0x45 && h2 === 0xdf && h3 === 0xa3) { return "video/webm"; } if ( (h4 === 0x66 && h5 === 0x74 && h6 === 0x79 && h7 === 0x70) || (h0 === 0x00 && h1 === 0x00 && h2 === 0x00 && (h3 === 0x18 || h3 === 0x20) && h4 === 0x66 && h5 === 0x74 && h6 === 0x79 && h7 === 0x70) ) { return "video/mp4"; } if (h0 === 0x4f && h1 === 0x67 && h2 === 0x67 && h3 === 0x53) { return "video/ogg"; } return null; } function guessMimeFromName(name, fallback) { const lower = (name || "").toLowerCase(); if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) { return "image/jpeg"; } if (lower.endsWith(".png")) { return "image/png"; } if (lower.endsWith(".gif")) { return "image/gif"; } if (lower.endsWith(".webp")) { return "image/webp"; } if (lower.endsWith(".mp4") || lower.endsWith(".m4v")) { return "video/mp4"; } if (lower.endsWith(".webm")) { return "video/webm"; } if (lower.endsWith(".ogg") || lower.endsWith(".ogv")) { return "video/ogg"; } if (lower.endsWith(".mp3")) { return "audio/mpeg"; } if (lower.endsWith(".wav")) { return "audio/wav"; } if (lower.endsWith(".m4a") || lower.endsWith(".aac")) { return "audio/aac"; } if (lower.endsWith(".pdf")) { return "application/pdf"; } if ( lower.endsWith(".txt") || lower.endsWith(".log") || lower.endsWith(".csv") || lower.endsWith(".md") ) { return lower.endsWith(".md") ? "text/markdown" : "text/plain"; } if (lower.endsWith(".json")) { return "application/json"; } if (lower.endsWith(".zip")) { return "application/zip"; } if (lower.endsWith(".epub")) { return "application/epub+zip"; } if (lower.endsWith(".docx")) { return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; } if (lower.endsWith(".xlsx")) { return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; } if (lower.endsWith(".pptx")) { return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; } if (lower.endsWith(".doc")) { return "application/msword"; } if (lower.endsWith(".xls")) { return "application/vnd.ms-excel"; } if (lower.endsWith(".ppt")) { return "application/vnd.ms-powerpoint"; } if (lower.endsWith(".odt")) { return "application/vnd.oasis.opendocument.text"; } if (lower.endsWith(".ods")) { return "application/vnd.oasis.opendocument.spreadsheet"; } return fallback || "application/octet-stream"; } // Heuristic: decide if bytes look like decodable UTF-8 text function isLikelyText(bytes) { if (!bytes || bytes.length === 0) { return false; } let nonAscii = 0; let zeroes = 0; const len = Math.min(bytes.length, 4096); for (let i = 0; i < len; i++) { const b = bytes[i]; if (b === 0x00) { zeroes++; } if (b < 0x09 || (b > 0x0d && b < 0x20)) { nonAscii++; } } if (zeroes > 0) { return false; } // binary if (nonAscii / len > 0.3) { return false; } try { const dec = new TextDecoder("utf-8", { fatal: false }).decode(bytes.slice(0, len)); // If decoding yields lots of replacement chars, not text if ((dec.match(/\uFFFD/g) || []).length > 3) { return false; } } catch { return false; } return true; } function isMultiFileService(service) { const s = (service || "").toUpperCase(); return ( s === "WEBSITE" || s === "APP" || s === "FILES" || s === "GIT_REPOSITORY" || s === "GIF_REPOSITORY" || s === "GAME" || s === "DATABASE" || s === "SNAPSHOT" ); } function buildArbitraryUrl({ service, name, identifier, filepath }) { // Always include identifier; default to 'default' when empty const id = identifier && identifier !== "" ? identifier : "default"; const base = `/arbitrary/${service}/${encodeURIComponent(name)}/${encodeURIComponent(id)}`; if (filepath && filepath !== "/") { return `${base}/${filepath.replace(/^\/+/, "")}`; } // No implicit index.html — let QDN route to index for APP/WEBSITE return base; } function buildQortalEmbedUrl({ service, name, identifier, filepath }) { const svc = String(service || "").toUpperCase(); let url = `qortal://${svc}/${encodeURIComponent(name || "")}`; // Omit identifier when default/blank if (identifier && identifier !== "default") { url += `/${encodeURIComponent(identifier)}`; } if (filepath && filepath !== "/") { url += `/${String(filepath).replace(/^\/+/, "")}`; } return url; } function openImageOverlayFromDataUrl(dataUrl) { let overlay = document.getElementById("image-viewer-overlay"); if (!overlay) { overlay = document.createElement("div"); overlay.id = "image-viewer-overlay"; const img = document.createElement("img"); overlay.appendChild(img); const hint = document.createElement("div"); hint.className = "close-hint"; hint.textContent = "Click anywhere or press Esc to close"; overlay.appendChild(hint); overlay.addEventListener("click", () => { overlay.style.display = "none"; }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { overlay.style.display = "none"; } }); document.body.appendChild(overlay); } overlay.querySelector("img").src = dataUrl; overlay.style.display = "flex"; } // ===== Auth & Names ===== async function accountLogin() { try { showSpinner(); authStatus = "loading"; updateAuthUI(); updateSidebarBanner(); initHideDeletedUI(); const account = await qortalRequest({ action: "GET_USER_ACCOUNT" }); contentPage.style.display = "none"; document.getElementById("account-details").innerHTML = "Loading..."; userAddress = account.address ? account.address : "Address unavailable"; _userPublicKey = account.publicKey ? account.publicKey : "Public key unavailable"; let names = []; if (userAddress && userAddress !== "Address unavailable") { try { _namesStatus = "loading"; const res = await qortalRequest({ action: "GET_ACCOUNT_NAMES", address: userAddress }); names = Array.isArray(res) ? res : []; allNames = names.map((n) => ({ name: typeof n?.name === "string" ? n.name : "", owner: typeof n?.owner === "string" ? n.owner : userAddress, })); let primary = ""; try { const maybePrimary = await qortalRequest({ action: "GET_PRIMARY_NAME", address: userAddress, }); primary = typeof maybePrimary === "string" ? maybePrimary : ""; } catch {} if (primary) { userName = primary; } else if (allNames.length > 0 && allNames[0].name) { userName = allNames[0].name; } else { userName = "Name unavailable"; } _namesStatus = "succeeded"; } catch { allNames = [{ name: "", owner: userAddress }]; userName = "Name unavailable"; _namesStatus = "failed"; } } else { allNames = [{ name: "", owner: userAddress }]; userName = "Name unavailable"; _namesStatus = "failed"; } isAuthenticated = true; document.getElementById("info-details").innerHTML = infoDetails; document.getElementById("account-details").innerHTML = `${userAddress}
${userName}`; updateAuthUI(); updateSidebarBanner(); initHideDeletedUI(); currentServiceFilter = "ALL"; await loadAllResults(); buildSidebarTree(); await fetchPage(); showSection("content"); authStatus = "succeeded"; updateAuthUI(); updateSidebarBanner(); initHideDeletedUI(); hideSpinner(); } catch (error) { console.error("Error fetching account details:", error); authStatus = "failed"; updateAuthUI(); updateSidebarBanner(); initHideDeletedUI(); document.getElementById("account-details").innerHTML = `Error fetching account details: ${error}`; hideSpinner(); } } async function switchActiveName(newName) { if (!newName || newName === userName) { return; } showSpinner(); contentPage.style.display = "none"; userName = newName; currentServiceFilter = "ALL"; currentPage = 1; document.getElementById("account-details").innerHTML = `${userAddress}
${userName}`; updateAuthUI(); updateSidebarBanner(); initHideDeletedUI(); try { await loadAllResults(); buildSidebarTree(); await fetchPage(); } finally { showSection("content"); hideSpinner(); } } function tinyThresholdFor(service) { // Bigger threshold for image-like services to account for overhead const s = (service || "").toUpperCase(); if (s === "IMAGE" || s === "QCHAT_IMAGE" || s.includes("THUMBNAIL")) { return 8192; } return typeof TINY_SIZE_THRESHOLD !== "undefined" ? TINY_SIZE_THRESHOLD : 256; } async function markDeletedEntries(results) { // Initialize flag for (const r of results) { r.__isDeleted = false; } // Legacy 0-byte deletions for (const r of results) { if (isExcludedFromDeletionCheck(r.service)) { continue; } const sz = Number(r.size || 0); if (sz === 0) { r.__isDeleted = true; } } // Tiny-file check (1..4 bytes), verify bytes are only whitespace/BOM const tiny = results.filter((r) => { if (isExcludedFromDeletionCheck(r.service)) { return false; } if (r.__isDeleted) { return false; } const sizeVal = Number(r.size); const unknown = !Number.isFinite(sizeVal); const thr = tinyThresholdFor(r.service); return unknown || sizeVal <= thr; }); for (const r of tiny) { try { const name = r.name || userName; const service = r.service; const identifier = r.identifier === undefined ? "default" : r.identifier; let b64 = await qortalRequest( buildQdnParams({ action: "FETCH_QDN_RESOURCE", name, service, identifier, encoding: "base64", rebuild: false, }) ); if (isPrivateService(service)) { b64 = await qortalRequest({ action: "DECRYPT_DATA", encryptedData: b64 }); } const bytes = b64ToBytes(b64); // Strip UTF-8 BOM let i = 0; if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) { i = 3; } let hasNonWhitespace = false; for (; i < bytes.length; i++) { // Handle UTF-8 NBSP (0xC2 0xA0) as whitespace pair if (bytes[i] === 0xc2 && i + 1 < bytes.length && bytes[i + 1] === 0xa0) { i++; continue; } const b = bytes[i]; if (b === 0x00 || b === 0x09 || b === 0x0a || b === 0x0d || b === 0x20) { continue; } else { hasNonWhitespace = true; break; } } if (!hasNonWhitespace) { r.__isDeleted = true; } } catch (e) { // If we cannot fetch/decrypt, be conservative: do not mark deleted console.warn("Tiny-file check failed for", r.service, r.identifier, e); } } // Enforce service exclusions for (const r of results) { if (isExcludedFromDeletionCheck(r.service)) { r.__isDeleted = false; } } } async function loadAllResults() { // Fetch all resources for the active name, including metadata, unfiltered const resp = await fetch( `/arbitrary/resources/search?name=${encodeURIComponent(userName)}&includemetadata=true&exactmatchnames=true&mode=ALL` ); if (!resp.ok) { throw new Error("Failed loading resources"); } masterResults = await resp.json(); // Sort newest first (created/updated) masterResults.sort((a, b) => (b.updated || b.created || 0) - (a.updated || a.created || 0)); await markDeletedEntries(masterResults); // Build counts (respect hideDeleted) recomputeServiceCounts(); // Reset selection (none selected => show all) selectedServices = new Set(); clearTreeFilters(); applyServiceFilter(); buildServiceChips(); } function buildServiceChips() { const container = document.getElementById("service-chips-container"); if (!container) { return; } container.innerHTML = ""; const services = Object.keys(serviceCounts).sort(); services.forEach((svc) => { const chip = document.createElement("div"); chip.className = "chip" + (selectedServices.has(svc) ? " selected" : ""); chip.setAttribute("data-service", svc); const label = document.createElement("span"); label.className = "label"; label.textContent = svc; const count = document.createElement("span"); count.className = "count"; count.textContent = serviceCounts[svc]; chip.appendChild(label); chip.appendChild(count); chip.addEventListener("click", async () => { // Toggle selection if (selectedServices.has(svc)) { selectedServices.delete(svc); } else { selectedServices.add(svc); } // Visual state chip.classList.toggle("selected"); // Apply filter + re-render showSpinner(); contentPage.style.display = "none"; try { applyServiceFilter(); currentPage = 1; await fetchPage(); } finally { contentPage.style.display = "block"; hideSpinner(); } }); container.appendChild(chip); }); } function applyServiceFilter() { const base = getBaselineResults(); // Service filter let tmp = selectedServices.size === 0 ? base.slice() : base.filter((r) => selectedServices.has(r.service)); // Identifier prefix filter (if set) if (currentPrefixFilter) { const pfx = currentPrefixFilter; tmp = tmp.filter((r) => { const id = r.identifier || ""; return id.startsWith(pfx + "_") || id.startsWith(pfx + "-"); }); } // Exact identifier filter (if set) if (currentIdentifierFilter) { tmp = tmp.filter((r) => (r.identifier || "") === currentIdentifierFilter); } filteredResults = tmp; totalResults = filteredResults.length; totalSize = filteredResults.reduce((acc, r) => acc + (r.size || 0), 0); } // ===== Data fetchers ===== async function fetchPage() { try { if (!userName || userName === "Name unavailable") { return; } // Client-side pagination using filteredResults const contentDetails = document.getElementById("content-details"); contentDetails.innerHTML = "

Loading...

"; const start = (currentPage - 1) * itemsPerPage; const pageItems = filteredResults.slice(start, start + itemsPerPage); buildContentTable(pageItems); } catch (error) { console.error("Error fetching page:", error); document.getElementById("content-details").innerHTML = `

Error: ${error.message}

`; } } // ===== Table & pagination ===== function buildIdentifierCellHTML(result, identifier) { const svc = result.service; const key = selKey(svc, identifier); const checkbox = ``; const deleteIcon = `Delete`; const editIcon = `Edit`; const embedIcon = isEmbedService(svc) ? ` ` : ""; return `${checkbox}${deleteIcon}${editIcon}${identifier}${embedIcon}`; } function setBulkMode(on) { bulkDeleteMode = !!on; document.body.classList.toggle("bulk-mode", bulkDeleteMode); updateBulkControlsUI(); } // ===== Bulk delete UI wiring ===== function updateBulkControlsUI() { const btn = document.getElementById("bulk-delete-toggle"); if (btn) { const count = getSelectedCount(); if (!bulkDeleteMode) { btn.textContent = "Delete Files"; } else { btn.textContent = count > 0 ? `Delete ${count} Files` : "Deleting Files"; } } const selAll = document.getElementById("select-all-checkbox"); if (selAll) { // Determine if all visible are checked const boxes = Array.from(document.querySelectorAll(".bulk-select")); const allChecked = boxes.length > 0 && boxes.every((cb) => cb.checked); selAll.checked = allChecked; selAll.indeterminate = boxes.some((cb) => cb.checked) && !allChecked; } } async function bulkDeleteSelected() { try { if (!userName || userName === "Name unavailable") { return; } const entries = Array.from(selectedForDeletion).map((k) => { const [svc, ident] = k.split("||"); return { service: svc, identifier: ident === "default" ? undefined : ident }; }); if (entries.length === 0) { return; } showPublishModal("Deleting selected files…"); // Build a tiny non-empty file to mark as deleted const emptyFile = new Blob(["\n"], { type: "application/octet-stream" }); const resourceArray = entries.map((e) => ({ name: userName, service: e.service, identifier: e.identifier || "default", file: emptyFile, })); const response = await qortalRequest({ action: "PUBLISH_MULTIPLE_QDN_RESOURCES", resources: resourceArray, }); console.log("Bulk delete response:", response); // Remove deleted from masterResults const keySet = new Set(resourceArray.map((r) => selKey(r.service, r.identifier))); masterResults = masterResults.filter((r) => !keySet.has(selKey(r.service, r.identifier))); // Recompute derived collections recomputeServiceCounts(); buildSidebarTree(); applyServiceFilter(); selectedForDeletion.clear(); setBulkMode(false); await fetchPage(); } catch (err) { alert("Bulk delete failed: " + (err?.message || err)); } finally { closePublishModal(); } } function buildContentTable(results) { const contentDetailsDiv = document.getElementById("content-details"); const contentSummaryDiv = document.getElementById("content-summary"); if (results.length === 0) { contentDetailsDiv.innerHTML = "

No results found.

"; contentSummaryDiv.innerHTML = ""; document.getElementById("pagination-top").innerHTML = ""; document.getElementById("pagination-bottom").innerHTML = ""; return; } results.sort((a, b) => (b.updated || b.created) - (a.updated || a.created)); let tableHtml = ""; tableHtml += ``; metadataArray = []; for (const result of results) { const identifier = result.identifier === undefined ? "default" : result.identifier; let createdString = new Date(result.created).toLocaleString(); if (isNaN(new Date(result.created))) { createdString = "Unknown"; } let updatedString = new Date(result.updated).toLocaleString(); if (isNaN(new Date(result.updated))) { updatedString = "Never"; } const sizeString = formatSize(result.size || 0); let metadataKeys = ""; let metadataIndex = -1; if (result.metadata) { metadataIndex = metadataArray.length; metadataArray.push(result.metadata); metadataKeys = Object.keys(result.metadata).join(", "); } tableHtml += ``; } tableHtml += `
Service Identifier ${bulkDeleteMode ? '' : ""} Metadata Preview Size Created / Updated
${result.service} ${buildIdentifierCellHTML(result, identifier)} ${generatePreviewHTML(result, userName, identifier)} ${sizeString} ${createdString}
${updatedString}
`; const startItem = (currentPage - 1) * itemsPerPage + 1; const endItem = Math.min(startItem + itemsPerPage - 1, totalResults); const selectedCount = getSelectedCount(); contentSummaryDiv.innerHTML = `
${startItem}-${endItem} of ${totalResults} resultsTotal Size: ${formatSize(totalSize)}${selectedCount > 0 ? '' : ""}
`; const paginationHTML = buildPaginationControls(); renderIntoCompat(document.getElementById("pagination-top"), paginationHTML, "replace"); renderIntoCompat(document.getElementById("pagination-bottom"), paginationHTML, "replace"); /* promote media before rerender */ const playing = contentDetailsDiv.querySelector("video, audio"); if (playing && !playing.paused) { promoteMedia(playing, { service: playing.getAttribute("data-service") || "", identifier: playing.getAttribute("data-identifier") || "default", name: playing.getAttribute("data-name") || userName, }); } contentDetailsDiv.innerHTML = tableHtml; // Wire bulk selection checkboxes if (bulkDeleteMode) { document.querySelectorAll(".bulk-select").forEach((cb) => { cb.addEventListener("change", (_e) => { const svc = cb.getAttribute("data-service"); const ident = cb.getAttribute("data-identifier") || "default"; const key = selKey(svc, ident); if (cb.checked) { selectedForDeletion.add(key); } else { selectedForDeletion.delete(key); } updateBulkControlsUI(); }); }); const selAll = document.getElementById("select-all-checkbox"); if (selAll) { selAll.addEventListener("change", () => { const boxes = Array.from(document.querySelectorAll(".bulk-select")); boxes.forEach((cb) => { cb.checked = selAll.checked; const svc = cb.getAttribute("data-service"); const ident = cb.getAttribute("data-identifier") || "default"; const key = selKey(svc, ident); if (cb.checked) { selectedForDeletion.add(key); } else { selectedForDeletion.delete(key); } }); updateBulkControlsUI(); }); } } // Wire summary bar buttons const bulkBtn = document.getElementById("bulk-delete-toggle"); if (bulkBtn) { bulkBtn.addEventListener("click", async () => { if (!bulkDeleteMode) { setBulkMode(true); await fetchPage(); return; } const count = getSelectedCount(); if (count === 0) { setBulkMode(false); await fetchPage(); } else { await bulkDeleteSelected(); } }); } const clearBtn = document.getElementById("clear-selected-btn"); if (clearBtn) { clearBtn.addEventListener("click", () => { clearSelected(); fetchPage(); }); } // Initialize bulk UI state updateBulkControlsUI(); initPreviews(); document.querySelectorAll(".copy-embed-icon").forEach((el) => { el.addEventListener("click", async function () { const nm = this.getAttribute("data-name") || ""; const svc = this.getAttribute("data-service") || ""; const ident = this.getAttribute("data-identifier") || "default"; const url = `qortal://use-embed/IMAGE?name=${encodeURIComponent(nm)}&identifier=${encodeURIComponent(ident)}&service=${encodeURIComponent(svc)}`; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(url); } else { const ta = document.createElement("textarea"); ta.value = url; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } // Remove prior toast (if any) to avoid stacking const prev = this.parentElement.querySelector(".copy-embed-toast"); if (prev) { prev.remove(); } const msg = document.createElement("span"); msg.className = "copy-embed-toast"; msg.textContent = "Embed link copied!"; msg.style.marginLeft = "6px"; msg.style.fontSize = "12px"; msg.style.color = "#1f6feb"; this.parentElement.appendChild(msg); setTimeout(() => { if (msg && msg.parentElement) { msg.parentElement.removeChild(msg); } }, 1800); } catch (e) { alert("Could not copy link: " + (e?.message || e)); } }); }); document.querySelectorAll(".clickable-delete").forEach((el) => { el.addEventListener("click", function () { deleteContent(this.getAttribute("data-service"), this.getAttribute("data-identifier")); }); }); document.querySelectorAll(".clickable-edit").forEach((el) => { el.addEventListener("click", function () { editContent(this.getAttribute("data-service"), this.getAttribute("data-identifier")); }); }); document.querySelectorAll(".clickable-metadata").forEach((el) => { el.addEventListener("click", function () { const idx = parseInt(this.getAttribute("data-metadata-index"), 10); if (!isNaN(idx) && idx >= 0) { openMetadataDialog(metadataArray[idx]); } else { alert("No metadata available."); } }); }); addPaginationEventHandlers(); document.querySelectorAll(".identifier-text").forEach((el) => { el.addEventListener("click", () => { const svc = el.getAttribute("data-service"); const ident = el.getAttribute("data-identifier") || "default"; const nm = el.getAttribute("data-name") || userName; openPreviewPage({ service: svc, identifier: ident, name: nm }); }); }); } function buildPaginationControls() { if (window.QEditPagination && typeof window.QEditPagination.build === "function") { return window.QEditPagination.build({ currentPage, itemsPerPage, totalResults }); } const totalPages = Math.ceil(totalResults / itemsPerPage); if (totalPages <= 1) { return ""; } let html = '"; return html; } function addPaginationEventHandlers() { document.querySelectorAll(".pagination-link").forEach((link) => { link.addEventListener("click", async function () { const newPage = parseInt(this.getAttribute("data-page"), 10); if (!isNaN(newPage)) { showSpinner(); contentPage.style.display = "none"; try { currentPage = newPage; setPageHash(currentPage); await fetchPage(); } finally { showSection("content"); hideSpinner(); } } }); }); document.querySelectorAll(".jump-btn").forEach((btn) => { btn.addEventListener("click", async function () { const container = this.closest(".pagination-controls") || document; const inp = container.querySelector(".jump-input"); if (!inp) { return; } const v = parseInt(inp.value, 10); if (!isNaN(v)) { const totalPages = Math.ceil(totalResults / itemsPerPage); const newPage = Math.min(Math.max(1, v), totalPages); showSpinner(); contentPage.style.display = "none"; try { currentPage = newPage; setPageHash(currentPage); await fetchPage(); } finally { showSection("content"); hideSpinner(); } } }); }); document.querySelectorAll(".jump-input").forEach((inp) => { inp.addEventListener("keydown", async function (e) { if (e.key === "Enter") { e.preventDefault(); const totalPages = Math.ceil(totalResults / itemsPerPage); const v = parseInt(this.value, 10); if (!isNaN(v)) { const newPage = Math.min(Math.max(1, v), totalPages); showSpinner(); contentPage.style.display = "none"; try { currentPage = newPage; setPageHash(currentPage); await fetchPage(); } finally { showSection("content"); hideSpinner(); } } } }); }); } function formatSize(size) { if (size > 1024 * 1024 * 1024 * 1024) { return (size / (1024 * 1024 * 1024 * 1024)).toFixed(2) + " TB"; } if (size > 1024 * 1024 * 1024) { return (size / (1024 * 1024 * 1024)).toFixed(2) + " GB"; } if (size > 1024 * 1024) { return (size / (1024 * 1024)).toFixed(2) + " MB"; } if (size > 1024) { return (size / 1024).toFixed(2) + " KB"; } return (size || 0) + " B"; } // ===== Preview rendering ===== function generatePreviewHTML(result, userName, identifier) { const safeName = (result.name || userName || "").replace(/"/g, """); const safeService = (result.service || "").replace(/"/g, """); const safeIdent = (identifier || "default").replace(/"/g, """); return `
0% Loaded
`; } function initPreviews(root) { const scope = root && root.querySelectorAll ? root : document; const holders = scope.querySelectorAll(".preview-holder"); holders.forEach((el) => { const svc = (el.getAttribute("data-service") || "").toUpperCase(); const ident = el.getAttribute("data-identifier") || "default"; const nm = el.getAttribute("data-name") || userName; loadPreviewInto(el, { service: svc, identifier: ident, name: nm }); }); } function isPrivateService(service) { const s = (service || "").toUpperCase(); return ( s.endsWith("_PRIVATE") || [ "QCHAT_ATTACHMENT_PRIVATE", "ATTACHMENT_PRIVATE", "FILE_PRIVATE", "IMAGE_PRIVATE", "VIDEO_PRIVATE", "AUDIO_PRIVATE", "VOICE_PRIVATE", "DOCUMENT_PRIVATE", "MAIL_PRIVATE", "MESSAGE_PRIVATE", ].includes(s) ); } function isExcludedFromDeletionCheck(service) { const s = (service || "").toUpperCase(); return s === "CHAIN_DATA" || s === "CHAIN_COMMENT"; } function isEmbedService(service) { const s = (service || "").toUpperCase(); return s === "IMAGE" || s === "QCHAT_IMAGE" || s.includes("THUMBNAIL"); } function getBaseServiceKind(service) { const s = (service || "").toUpperCase(); if (s.includes("IMAGE") || s.includes("THUMBNAIL")) { return "image"; } if (s.includes("VIDEO")) { return "video"; } if (s.includes("AUDIO") || s.includes("VOICE") || s.includes("PODCAST")) { return "audio"; } if ( s.includes("DOCUMENT") || s.includes("BLOG") || s.includes("COMMENT") || s.includes("JSON") || s.includes("CODE") ) { return "text"; } if (s.includes("FILE") || s.includes("ATTACHMENT")) { return "file"; } return "file"; } async function waitForResourceReady({ name, service, identifier, initialBuild = true, timeoutMs = 60000, intervalMs = 800, onProgress, }) { const start = Date.now(); while (true) { try { const status = await qortalRequest( buildQdnParams({ action: "GET_QDN_RESOURCE_STATUS", name, service, identifier }) ); let percent = 0; if (typeof status?.percentLoaded === "number") { percent = status.percentLoaded; } else if (status?.localChunkCount && status?.totalChunkCount) { percent = Math.floor((status.localChunkCount / status.totalChunkCount) * 100); } if (onProgress && Number.isFinite(percent)) { onProgress(Math.max(0, Math.min(100, Math.floor(percent)))); } const ready = status && (status.status === "READY" || percent >= 100 || (status.localChunkCount && status.totalChunkCount && status.localChunkCount >= status.totalChunkCount)); if (ready) { return status; } } catch (_e) { /* ignore transient */ } if (Date.now() - start > timeoutMs) { throw new Error("Resource not ready (timeout)"); } await new Promise((r) => setTimeout(r, intervalMs)); } } async function loadPreviewInto(container, ctx) { const isFullPreview = container && container.id === "preview-container"; const set = (el) => { // Revoke previous blob URL if present const prev = container.firstElementChild; if (prev && prev.dataset && prev.dataset.bloburl) { try { URL.revokeObjectURL(prev.dataset.bloburl); } catch {} } container.innerHTML = ""; container.appendChild(el); try { container.dataset.loading = "0"; } catch {} try { // Heuristic: mark whether preview is showing text content for inline editing eligibility let isTextual = false; if (el && el.classList && el.classList.contains("inline-text-editor")) { isTextual = true; } else if (el && el.tagName === "PRE") { const ws = el.style.whiteSpace || (window.getComputedStyle ? getComputedStyle(el).whiteSpace : ""); isTextual = String(ws).toLowerCase().includes("pre-wrap"); } else if (el && el.tagName === "IFRAME") { isTextual = el.getAttribute("srcdoc") != null; // srcdoc implies HTML text, not PDF/file blob } else if (el && el.tagName === "DIV") { const pre = el.querySelector("pre"); if (pre) { const ws = pre.style.whiteSpace || (window.getComputedStyle ? getComputedStyle(pre).whiteSpace : ""); isTextual = String(ws).toLowerCase().includes("pre-wrap"); } } container.dataset.textual = isTextual ? "1" : "0"; } catch {} try { console.debug("[Q-Edit] set(): content rendered", { tag: el.tagName, id: container.id }); } catch {} }; const looksHtml = (txt) => /<(?:!doctype|html|head|body|div|p|span|img|video|audio|iframe|section|article)/i.test(txt); const tryParseJson = (txt) => { try { return JSON.parse(txt); } catch { return null; } }; try { // Mark container as loading for progress updates; cleared when set() renders content try { container.dataset.loading = "1"; } catch {} const service = ctx.service; const identifier = ctx.identifier; const name = ctx.name; const lower = (service || "").toLowerCase(); const baseKind = getBaseServiceKind(service); const isPriv = isPrivateService(service); const filepath = (ctx && ctx.filepath) || ""; const textServices = ["blog", "blog_post", "blog_comment", "document", "game", "json", "code"]; const isText = textServices.some((t) => lower.includes(t)) || baseKind === "text"; // Start background status polling (no build), updates percent while loading let stopProgress = false; const statusWait = waitForResourceReady({ name, service, identifier, initialBuild: false, onProgress: (pct) => { try { if (stopProgress) { return; } if (container.dataset && container.dataset.loading !== "1") { return; } const p = Math.max(0, Math.min(100, Math.floor(pct))); container.textContent = `${p}% Loaded`; console.debug("[Q-Edit] progress", { p, service, identifier, container: container.id }); } catch {} }, }).catch(() => {}); // Embedded WEBSITE/APP (public) — use qortal:// URL to leverage QDN routing const sUp = (service || "").toUpperCase(); if (!isPriv && (sUp === "WEBSITE" || sUp === "APP")) { const url = buildQortalEmbedUrl({ service: sUp, name, identifier, filepath }); const iframe = document.createElement("iframe"); iframe.src = url; if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } set(iframe); stopProgress = true; // do not overwrite iframe with progress text // While it loads, update percent in the preview title if (isFullPreview) { const titleEl = document.getElementById("preview-title"); const baseTitle = titleEl ? titleEl.textContent : ""; (async () => { const start = Date.now(); while (Date.now() - start < 60000) { try { const status = await qortalRequest( buildQdnParams({ action: "GET_QDN_RESOURCE_STATUS", name, service, identifier }) ); let percent = 0; if (typeof status?.percentLoaded === "number") { percent = status.percentLoaded; } else if (status?.localChunkCount && status?.totalChunkCount) { percent = Math.floor((status.localChunkCount / status.totalChunkCount) * 100); } const ready = status && (status.status === "READY" || percent >= 100 || (status.localChunkCount && status.totalChunkCount && status.localChunkCount >= status.totalChunkCount)); if (titleEl && Number.isFinite(percent)) { titleEl.textContent = `${baseTitle} — ${Math.max(0, Math.min(100, Math.floor(percent)))}% Loaded`; } if (ready) { break; } } catch {} await new Promise((r) => setTimeout(r, 800)); } if (titleEl) { titleEl.textContent = baseTitle; } })(); } return; } if (isPriv) { // Private: fetch base64 and decrypt const encB64 = await qortalRequest( buildQdnParams({ action: "FETCH_QDN_RESOURCE", name, service, identifier, encoding: "base64", rebuild: false, ...(filepath ? { filepath } : {}), }) ); const decB64 = await qortalRequest({ action: "DECRYPT_DATA", encryptedData: encB64 }); if (isText) { const bytes = b64ToBytes(decB64); const text = new TextDecoder("utf-8").decode(bytes); const maybeJson = tryParseJson(text.trim()); if (maybeJson) { const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = JSON.stringify(maybeJson, null, 2); stopProgress = true; set(pre); return; } if (looksHtml(text)) { const iframe = document.createElement("iframe"); iframe.setAttribute("sandbox", ""); if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } iframe.srcdoc = text; stopProgress = true; set(iframe); return; } const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = isFullPreview ? text : text.slice(0, 5000); stopProgress = true; set(pre); } else { // Media / file const bytes = b64ToBytes(decB64); // If content looks like text despite service label, render as text if (isLikelyText(bytes)) { const text = new TextDecoder("utf-8").decode(bytes); const maybeJson = tryParseJson(text.trim()); if (maybeJson) { const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = JSON.stringify(maybeJson, null, 2); stopProgress = true; set(pre); return; } if (looksHtml(text)) { const iframe = document.createElement("iframe"); iframe.setAttribute("sandbox", ""); if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } iframe.srcdoc = text; stopProgress = true; set(iframe); return; } const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = isFullPreview ? text : text.slice(0, 5000); stopProgress = true; set(pre); return; } const mime = detectMimeFromBytes(bytes) || (baseKind === "image" ? "image/png" : baseKind === "video" ? "video/mp4" : baseKind === "audio" ? "audio/mpeg" : "application/octet-stream"); const blob = new Blob([bytes], { type: mime }); const url = URL.createObjectURL(blob); if (mime === "application/pdf") { const iframe = document.createElement("iframe"); iframe.type = "application/pdf"; iframe.src = url; iframe.dataset.bloburl = url; if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } stopProgress = true; set(iframe); return; } if (baseKind === "image") { const img = document.createElement("img"); img.src = url; img.alt = identifier; img.dataset.bloburl = url; img.className = isFullPreview ? "preview-image full" : "preview-image"; img.setAttribute("data-service", service); img.setAttribute("data-identifier", identifier); img.setAttribute("data-name", name); img.addEventListener("click", () => openImageOverlayFromDataUrl(url)); stopProgress = true; set(img); return; } if (baseKind === "video") { const video = document.createElement("video"); video.dataset.bloburl = url; video.controls = true; video.className = isFullPreview ? "preview-video full" : "preview-video"; video.src = url; video.setAttribute("data-service", service); video.setAttribute("data-identifier", identifier); video.setAttribute("data-name", name); stopProgress = true; set(video); return; } if (baseKind === "audio") { const audio = document.createElement("audio"); audio.dataset.bloburl = url; audio.controls = true; audio.className = isFullPreview ? "preview-audio full" : "preview-audio"; audio.src = url; audio.setAttribute("data-service", service); audio.setAttribute("data-identifier", identifier); audio.setAttribute("data-name", name); stopProgress = true; set(audio); return; } // Fallback file link const a = document.createElement("a"); a.textContent = "Open"; a.href = url; a.target = "_blank"; a.rel = "noopener"; a.dataset.bloburl = url; stopProgress = true; set(a); } } else { // Public if (isText) { let resp; try { resp = await qortalRequest( buildQdnParams({ action: "FETCH_QDN_RESOURCE", name, service, identifier, rebuild: false, ...(filepath ? { filepath } : {}), }) ); } catch (_e) { const b64 = await qortalRequest( buildQdnParams({ action: "FETCH_QDN_RESOURCE", name, service, identifier, encoding: "base64", rebuild: false, ...(filepath ? { filepath } : {}), }) ); const bytes = b64ToBytes(b64); const dec = new TextDecoder("utf-8").decode(bytes); resp = dec || b64; } if (typeof resp === "string") { const maybeJson = tryParseJson(resp.trim()); if (maybeJson) { const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = JSON.stringify(maybeJson, null, 2); stopProgress = true; set(pre); return; } if (looksHtml(resp)) { const iframe = document.createElement("iframe"); iframe.setAttribute("sandbox", ""); if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } iframe.srcdoc = resp; stopProgress = true; set(iframe); return; } const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = isFullPreview ? resp : resp.slice(0, 5000); stopProgress = true; set(pre); return; } else if (resp && resp.type === "Buffer" && Array.isArray(resp.data)) { const bytes = new Uint8Array(resp.data); const text = new TextDecoder("utf-8").decode(bytes); const maybeJson = tryParseJson(text.trim()); if (maybeJson) { const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = JSON.stringify(maybeJson, null, 2); stopProgress = true; set(pre); return; } if (looksHtml(text)) { const iframe = document.createElement("iframe"); iframe.setAttribute("sandbox", ""); if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } iframe.srcdoc = text; stopProgress = true; set(iframe); return; } const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = isFullPreview ? text : text.slice(0, 5000); set(pre); return; } } // Non-text: fetch as base64 and sniff const b64 = await qortalRequest( buildQdnParams({ action: "FETCH_QDN_RESOURCE", name, service, identifier, encoding: "base64", rebuild: false, ...(filepath ? { filepath } : {}), }) ); const bytes = b64ToBytes(b64); // Text-like payload under non-text service? Render as text if (isLikelyText(bytes)) { const text = new TextDecoder("utf-8").decode(bytes); const maybeJson = tryParseJson(text.trim()); if (maybeJson) { const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; pre.style.overflow = "auto"; } pre.textContent = JSON.stringify(maybeJson, null, 2); set(pre); return; } if (looksHtml(text)) { const iframe = document.createElement("iframe"); iframe.setAttribute("sandbox", ""); if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } iframe.srcdoc = text; set(iframe); return; } const pre = document.createElement("pre"); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; } pre.style.overflow = "auto"; pre.style.whiteSpace = "pre-wrap"; pre.textContent = isFullPreview ? text : text.slice(0, 5000); stopProgress = true; set(pre); return; } const mime = detectMimeFromBytes(bytes) || guessMimeFromName(identifier, "application/octet-stream"); const blob = new Blob([bytes], { type: mime }); const url = URL.createObjectURL(blob); if (mime === "application/pdf") { const iframe = document.createElement("iframe"); iframe.type = "application/pdf"; iframe.src = url; iframe.dataset.bloburl = url; if (isFullPreview) { iframe.style.width = "100%"; iframe.style.height = "70vh"; } else { iframe.style.width = "240px"; iframe.style.height = "160px"; } stopProgress = true; set(iframe); return; } if (mime.startsWith("image/")) { const img = document.createElement("img"); img.src = url; img.alt = identifier; img.className = isFullPreview ? "preview-image full" : "preview-image"; img.dataset.bloburl = url; img.setAttribute("data-service", service); img.setAttribute("data-identifier", identifier); img.setAttribute("data-name", name); img.addEventListener("click", () => openImageOverlayFromDataUrl(url)); stopProgress = true; set(img); return; } if (mime.startsWith("video/")) { const video = document.createElement("video"); video.dataset.bloburl = url; video.controls = true; video.className = isFullPreview ? "preview-video full" : "preview-video"; video.src = url; video.setAttribute("data-service", service); video.setAttribute("data-identifier", identifier); video.setAttribute("data-name", name); stopProgress = true; set(video); return; } if (mime.startsWith("audio/")) { const audio = document.createElement("audio"); audio.dataset.bloburl = url; audio.controls = true; audio.className = isFullPreview ? "preview-audio full" : "preview-audio"; audio.src = url; audio.setAttribute("data-service", service); audio.setAttribute("data-identifier", identifier); audio.setAttribute("data-name", name); stopProgress = true; set(audio); return; } // Fallback: show a small hex/ASCII preview plus an Open link const wrap = document.createElement("div"); const pre = document.createElement("pre"); const viewLen = Math.min(bytes.length, 2048); let out = ""; for (let i = 0; i < viewLen; i += 16) { let hex = ""; let ascii = ""; for (let j = 0; j < 16 && i + j < viewLen; j++) { const b = bytes[i + j]; hex += b.toString(16).padStart(2, "0") + " "; ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : "."; } out += hex.padEnd(16 * 3, " ") + " " + ascii + "\n"; } pre.textContent = out + (bytes.length > viewLen ? "\n… (truncated)" : ""); if (isFullPreview) { pre.style.maxWidth = "100%"; pre.style.maxHeight = "70vh"; } else { pre.style.maxWidth = "240px"; pre.style.maxHeight = "160px"; } pre.style.overflow = "auto"; pre.style.whiteSpace = "pre"; const a = document.createElement("a"); a.textContent = "Open"; a.href = url; a.target = "_blank"; a.rel = "noopener"; a.dataset.bloburl = url; a.style.display = "inline-block"; a.style.marginTop = "6px"; wrap.appendChild(pre); wrap.appendChild(a); stopProgress = true; set(wrap); } } catch (e) { container.textContent = "Failed to load preview: " + (e?.message || e); console.error("Preview failed", e); } } // ===== Delete/Edit/Metadata/Publish (from user's file, preserved) ===== async function deleteContent(service, identifier) { try { if (!userName || userName === "Name unavailable") { return; } showPublishModal("Please wait..."); // Fetch existing metadata let existingMetadata = {}; try { const metadataResponse = await fetch( `/arbitrary/resources/search?name=${userName}&service=${service}&identifier=${identifier}&includemetadata=true&exactmatchnames=true&mode=ALL` ); if (metadataResponse.ok) { const metadataResults = await metadataResponse.json(); if (metadataResults.length > 0 && metadataResults[0].metadata) { existingMetadata = metadataResults[0].metadata; } } } catch (err) { console.error("Error fetching existing metadata:", err); } // Use a minimal 1-byte file to avoid hub check rejecting size=0 // A zero-byte Blob triggers the hub's `/arbitrary/check/tmp` to fail, // which surfaces as a misleading "Not enough space" error. const emptyFile = new Blob(["\n"], { type: "application/octet-stream" }); const deleteIdent = identifier === "default" ? "" : identifier; // Prepare the publish parameters, including existing metadata if available const publishParams = { action: "PUBLISH_QDN_RESOURCE", name: userName, service: service, identifier: deleteIdent, file: emptyFile, }; // List of metadata fields to delete const metadataFields = ["filename", "title", "description"]; // Add existing metadata fields to publishParams if they exist for (const field of metadataFields) { if (existingMetadata[field]) { publishParams[field] = "deleted"; } } if (existingMetadata["category"]) { publishParams["category"] = "UNCATEGORIZED"; } if (existingMetadata["tags"]) { publishParams["tag1"] = "deleted"; } // Proceed with publishing using publishWithFeedback try { await publishWithFeedback(publishParams); console.log("Content deleted successfully"); } catch (error) { console.error("Error deleting content:", error); } } catch (error) { console.error("Error deleting content:", error); } } async function editContent(service, identifier) { try { if (!userName || userName === "Name unavailable") { return; } // Fetch existing metadata let existingMetadata = {}; try { const metadataResponse = await fetch( `/arbitrary/resources/search?name=${userName}&service=${service}&identifier=${identifier}&includemetadata=true&exactmatchnames=true&mode=ALL` ); if (metadataResponse.ok) { const metadataResults = await metadataResponse.json(); if (metadataResults.length > 0 && metadataResults[0].metadata) { existingMetadata = metadataResults[0].metadata; } } } catch (err) { console.error("Error fetching existing metadata:", err); } const editIdent = identifier === "default" ? "" : identifier; // Prepare the publish parameters (for non-text flow; text flow handled inline on save) const publishParams = { action: "PUBLISH_QDN_RESOURCE", name: userName, service: service, identifier: editIdent, // 'file' will be added below after obtaining the edited or selected file }; const textualInPreview = isTextDisplayedInPreview(); if (textualInPreview) { // Fetch the current content and enable inline editing in the preview panel let contentUrl = `/arbitrary/${service}/${userName}/${identifier}`; let content = ""; try { const contentResponse = await fetch(contentUrl); if (contentResponse.ok) { content = await contentResponse.text(); } } catch (_err) { // ignore; we'll try to read from current preview DOM below } if (!content) { // Fallback: read currently displayed preview content (handles private resources) try { const container = document.getElementById("preview-container"); const el = container && container.firstElementChild; if (el) { if (el.tagName === "PRE") { content = el.textContent || ""; } else if (el.tagName === "IFRAME" && el.getAttribute("srcdoc") != null) { content = el.srcdoc || ""; } else if (el.tagName === "DIV") { const pre = el.querySelector("pre"); if (pre) { content = pre.textContent || ""; } } } } catch {} } enableInlineTextEditInPreview({ content, service, identifier, existingMetadata, }); return; // Inline editor will manage the rest (metadata + publish or cancel) } else { // For other types, prompt the user to select a new file const input = document.createElement("input"); input.type = "file"; input.click(); const selectedFilePromise = new Promise((resolve, reject) => { input.onchange = (event) => { const file = event.target.files[0]; resolve(file); }; input.onerror = reject; }); const selectedFile = await selectedFilePromise; publishParams.file = selectedFile; // Add the selected file to publishParams } // Open metadata editor dialog let updatedMetadata = await openMetadataEditorDialog(existingMetadata); if (updatedMetadata === null) { // User cancelled return; } // Update 'publishParams' with 'updatedMetadata' const metadataFields = ["filename", "title", "description", "category"]; for (const field of metadataFields) { if (updatedMetadata[field]) { publishParams[field] = updatedMetadata[field]; } else { delete publishParams[field]; } } // Handle tags if (updatedMetadata["tags"]) { const tagsArray = updatedMetadata["tags"] .split(",") .map((tag) => tag.trim()) .filter((tag) => tag); for (let i = 1; i <= 5; i++) { if (tagsArray[i - 1]) { publishParams[`tag${i}`] = tagsArray[i - 1]; } else { delete publishParams[`tag${i}`]; } } } else { // Remove tags if none provided for (let i = 1; i <= 5; i++) { delete publishParams[`tag${i}`]; } } // Proceed with publishing using publishWithFeedback try { await publishWithFeedback(publishParams); console.log("Content edited successfully"); // Optionally, refresh the content display // fetchContent(); } catch (error) { console.error("Error editing content:", error); } } catch (error) { console.error("Error editing content:", error); } } // Enable inline text editing inside the main preview panel for text services function enableInlineTextEditInPreview({ content, service, identifier, existingMetadata }) { const container = document.getElementById("preview-container"); if (!container) { return; } // If already in inline editing, just refresh content and focus const existing = container.querySelector(".inline-text-editor"); if (existing) { const ta = existing.querySelector("textarea"); if (ta) { ta.value = content || ""; ta.focus(); } return; } const wrapper = document.createElement("div"); wrapper.className = "inline-text-editor"; wrapper.style.display = "flex"; wrapper.style.flexDirection = "column"; wrapper.style.width = "100%"; wrapper.style.height = "70vh"; const toolbar = document.createElement("div"); toolbar.style.display = "flex"; toolbar.style.justifyContent = "flex-end"; toolbar.style.gap = "8px"; toolbar.style.marginBottom = "8px"; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancel"; const saveBtn = document.createElement("button"); saveBtn.textContent = "Save"; toolbar.appendChild(cancelBtn); toolbar.appendChild(saveBtn); const textarea = document.createElement("textarea"); textarea.style.flex = "1"; textarea.style.width = "100%"; textarea.style.resize = "vertical"; textarea.style.backgroundColor = "#3d4452"; textarea.style.color = "#c9d2d9"; textarea.style.border = "1px solid #445063"; textarea.style.borderRadius = "8px"; textarea.style.padding = "10px"; textarea.value = content || ""; wrapper.appendChild(toolbar); wrapper.appendChild(textarea); container.replaceChildren(wrapper); try { container.dataset.textual = "1"; } catch {} cancelBtn.addEventListener("click", async () => { // Restore the normal preview content container.textContent = "Loading preview…"; try { await loadPreviewInto(container, currentPreviewCtx); } catch {} }); saveBtn.addEventListener("click", async () => { try { const editedContent = textarea.value; // Prepare publish params const editIdent = identifier === "default" ? "" : identifier; const publishParams = { action: "PUBLISH_QDN_RESOURCE", name: userName, service, identifier: editIdent, file: new Blob([editedContent], { type: "text/plain" }), }; // Let user edit metadata next const updatedMetadata = await openMetadataEditorDialog(existingMetadata || {}); if (updatedMetadata === null) { // Stay in edit mode return; } const metadataFields = ["filename", "title", "description", "category"]; for (const field of metadataFields) { if (updatedMetadata[field]) { publishParams[field] = updatedMetadata[field]; } else { delete publishParams[field]; } } if (updatedMetadata["tags"]) { const tagsArray = updatedMetadata["tags"] .split(",") .map((tag) => tag.trim()) .filter((tag) => tag); for (let i = 1; i <= 5; i++) { if (tagsArray[i - 1]) { publishParams[`tag${i}`] = tagsArray[i - 1]; } else { delete publishParams[`tag${i}`]; } } } else { for (let i = 1; i <= 5; i++) { delete publishParams[`tag${i}`]; } } // Publish and then reload preview await publishWithFeedback(publishParams); container.textContent = "Loading preview…"; try { await loadPreviewInto(container, currentPreviewCtx); } catch {} } catch (e) { console.error("Inline edit save failed:", e); } }); } // Replace: always use a file picker, even for text services async function replaceContent(service, identifier) { try { if (!userName || userName === "Name unavailable") { return; } showPublishModal("Please wait..."); // Fetch existing metadata let existingMetadata = {}; try { const metadataResponse = await fetch( `/arbitrary/resources/search?name=${userName}&service=${service}&identifier=${identifier}&includemetadata=true&exactmatchnames=true&mode=ALL` ); if (metadataResponse.ok) { const metadataResults = await metadataResponse.json(); if (metadataResults.length > 0 && metadataResults[0].metadata) { existingMetadata = metadataResults[0].metadata; } } } catch (err) { console.error("Error fetching existing metadata:", err); } const repIdent = identifier === "default" ? "" : identifier; // Prepare the publish parameters const publishParams = { action: "PUBLISH_QDN_RESOURCE", name: userName, service: service, identifier: repIdent, // 'file' will be added below after user selects a file }; // Always pick a file for replacement closePublishModal(); const input = document.createElement("input"); input.type = "file"; input.click(); const selectedFilePromise = new Promise((resolve, reject) => { input.onchange = (event) => { const file = event.target.files[0]; resolve(file); }; input.onerror = reject; }); const selectedFile = await selectedFilePromise; publishParams.file = selectedFile; // Open metadata editor dialog let updatedMetadata = await openMetadataEditorDialog(existingMetadata); if (updatedMetadata === null) { // User cancelled return; } // Update 'publishParams' with 'updatedMetadata' const metadataFields = ["filename", "title", "description", "category"]; for (const field of metadataFields) { if (updatedMetadata[field]) { publishParams[field] = updatedMetadata[field]; } else { delete publishParams[field]; } } // Handle tags if (updatedMetadata["tags"]) { const tagsArray = updatedMetadata["tags"] .split(",") .map((tag) => tag.trim()) .filter((tag) => tag); for (let i = 1; i <= 5; i++) { if (tagsArray[i - 1]) { publishParams[`tag${i}`] = tagsArray[i - 1]; } else { delete publishParams[`tag${i}`]; } } } else { // Remove tags if none provided for (let i = 1; i <= 5; i++) { delete publishParams[`tag${i}`]; } } // Proceed with publishing using publishWithFeedback try { await publishWithFeedback(publishParams); console.log("Content replaced successfully"); } catch (error) { console.error("Error replacing content:", error); } } catch (error) { console.error("Error replacing content:", error); } } function openTextEditorDialog(content) { return new Promise((resolve) => { // Create the modal overlay const modalOverlay = document.createElement("div"); modalOverlay.style.position = "fixed"; modalOverlay.style.top = "0"; modalOverlay.style.left = "0"; modalOverlay.style.width = "100%"; modalOverlay.style.height = "100%"; modalOverlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; modalOverlay.style.display = "flex"; modalOverlay.style.justifyContent = "center"; modalOverlay.style.alignItems = "center"; modalOverlay.style.zIndex = "1000"; // Create the modal content container const modalContent = document.createElement("div"); modalContent.style.backgroundColor = "#2d3749"; // Use background color from main content modalContent.style.color = "#c9d2d9"; // Use text color from your CSS modalContent.style.padding = "20px"; modalContent.style.borderRadius = "25px"; // Match border radius from your CSS modalContent.style.maxWidth = "600px"; modalContent.style.width = "90%"; modalContent.style.fontFamily = "'Lexend', sans-serif"; // Use the same font modalContent.style.lineHeight = "1.6"; // Consistent line height // Create the textarea for editing const textarea = document.createElement("textarea"); textarea.style.width = "100%"; textarea.style.height = "300px"; textarea.style.backgroundColor = "#3d4452"; // Use background color from main content textarea.style.color = "#c9d2d9"; // Use text color from your CSS textarea.value = content; // Create the button container const buttonContainer = document.createElement("div"); buttonContainer.style.textAlign = "right"; buttonContainer.style.marginTop = "10px"; // Create the Save and Cancel buttons const saveButton = document.createElement("button"); saveButton.textContent = "Save"; const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.style.marginRight = "10px"; buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(saveButton); modalContent.appendChild(textarea); modalContent.appendChild(buttonContainer); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); // Event listeners for the buttons cancelButton.addEventListener("click", () => { document.body.removeChild(modalOverlay); closePublishModal(); resolve(null); }); saveButton.addEventListener("click", () => { const editedContent = textarea.value; document.body.removeChild(modalOverlay); resolve(editedContent); }); }); } function openMetadataDialog(metadata) { // Create the modal overlay const modalOverlay = document.createElement("div"); modalOverlay.style.position = "fixed"; modalOverlay.style.top = "0"; modalOverlay.style.left = "0"; modalOverlay.style.width = "100%"; modalOverlay.style.height = "100%"; modalOverlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; modalOverlay.style.display = "flex"; modalOverlay.style.justifyContent = "center"; modalOverlay.style.alignItems = "center"; modalOverlay.style.zIndex = "1000"; // Create the modal content container const modalContent = document.createElement("div"); modalContent.style.backgroundColor = "#2d3749"; // Use background color from main content modalContent.style.color = "#c9d2d9"; // Use text color from your CSS modalContent.style.padding = "20px"; modalContent.style.borderRadius = "25px"; // Match border radius from your CSS modalContent.style.maxWidth = "600px"; modalContent.style.width = "90%"; modalContent.style.fontFamily = "'Lexend', sans-serif"; // Use the same font modalContent.style.lineHeight = "1.6"; // Consistent line height // Create the content display const contentDiv = document.createElement("div"); contentDiv.style.maxHeight = "400px"; contentDiv.style.overflowY = "auto"; // Build the metadata display for (let key in metadata) { const keyElement = document.createElement("strong"); keyElement.textContent = key + ": "; keyElement.style.color = "#ffffff"; // Make keys stand out const valueElement = document.createElement("span"); valueElement.textContent = metadata[key]; const lineBreak = document.createElement("br"); contentDiv.appendChild(keyElement); contentDiv.appendChild(valueElement); contentDiv.appendChild(lineBreak); } // Create the Close button const closeButton = document.createElement("button"); closeButton.textContent = "Close"; closeButton.style.marginTop = "10px"; modalContent.appendChild(contentDiv); modalContent.appendChild(closeButton); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); // Event listener for the Close button closeButton.addEventListener("click", () => { document.body.removeChild(modalOverlay); }); } function openMetadataEditorDialog(existingMetadata) { return new Promise((resolve) => { // Create the modal overlay const modalOverlay = document.createElement("div"); modalOverlay.style.position = "fixed"; modalOverlay.style.top = "0"; modalOverlay.style.left = "0"; modalOverlay.style.width = "100%"; modalOverlay.style.height = "100%"; modalOverlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; modalOverlay.style.display = "flex"; modalOverlay.style.justifyContent = "center"; modalOverlay.style.alignItems = "center"; modalOverlay.style.zIndex = "1000"; // Create the modal content container const modalContent = document.createElement("div"); modalContent.style.backgroundColor = "#2d3749"; modalContent.style.color = "#c9d2d9"; modalContent.style.padding = "20px"; modalContent.style.borderRadius = "25px"; modalContent.style.maxWidth = "600px"; modalContent.style.width = "90%"; modalContent.style.fontFamily = "'Lexend', sans-serif"; modalContent.style.lineHeight = "1.6"; // Create the form const form = document.createElement("form"); const fields = ["filename", "title", "description", "category", "tags"]; // Define the categories const categories = [ { value: "", display: "" }, { value: "ART", display: "Art and Design" }, { value: "AUTOMOTIVE", display: "Automotive" }, { value: "BEAUTY", display: "Beauty" }, { value: "BOOKS", display: "Books and Reference" }, { value: "BUSINESS", display: "Business" }, { value: "COMMUNICATIONS", display: "Communications" }, { value: "CRYPTOCURRENCY", display: "Cryptocurrency and Blockchain" }, { value: "CULTURE", display: "Culture" }, { value: "DATING", display: "Dating" }, { value: "DESIGN", display: "Design" }, { value: "ENTERTAINMENT", display: "Entertainment" }, { value: "EVENTS", display: "Events" }, { value: "FAITH", display: "Faith and Religion" }, { value: "FASHION", display: "Fashion" }, { value: "FINANCE", display: "Finance" }, { value: "FOOD", display: "Food and Drink" }, { value: "GAMING", display: "Gaming" }, { value: "GEOGRAPHY", display: "Geography" }, { value: "HEALTH", display: "Health" }, { value: "HISTORY", display: "History" }, { value: "HOME", display: "Home" }, { value: "KNOWLEDGE", display: "Knowledge Share" }, { value: "LANGUAGE", display: "Language" }, { value: "LIFESTYLE", display: "Lifestyle" }, { value: "MANUFACTURING", display: "Manufacturing" }, { value: "MAPS", display: "Maps and Navigation" }, { value: "MUSIC", display: "Music" }, { value: "NEWS", display: "News" }, { value: "OTHER", display: "Other" }, { value: "PETS", display: "Pets" }, { value: "PHILOSOPHY", display: "Philosophy" }, { value: "PHOTOGRAPHY", display: "Photography" }, { value: "POLITICS", display: "Politics" }, { value: "PRODUCE", display: "Products and Services" }, { value: "PRODUCTIVITY", display: "Productivity" }, { value: "PSYCHOLOGY", display: "Psychology" }, { value: "QORTAL", display: "Qortal" }, { value: "SCIENCE", display: "Science" }, { value: "SELF_CARE", display: "Self Care" }, { value: "SELF_SUFFICIENCY", display: "Self-Sufficiency and Homesteading" }, { value: "SHOPPING", display: "Shopping" }, { value: "SOCIAL", display: "Social" }, { value: "SOFTWARE", display: "Software" }, { value: "SPIRITUALITY", display: "Spirituality" }, { value: "SPORTS", display: "Sports" }, { value: "STORYTELLING", display: "Storytelling" }, { value: "TECHNOLOGY", display: "Technology" }, { value: "TOOLS", display: "Tools" }, { value: "TRAVEL", display: "Travel" }, { value: "UNCATEGORIZED", display: "Uncategorized" }, { value: "VIDEO", display: "Video" }, { value: "WEATHER", display: "Weather" }, ]; fields.forEach((field) => { const label = document.createElement("label"); label.textContent = field.charAt(0).toUpperCase() + field.slice(1) + ":"; label.style.display = "block"; label.style.marginTop = "10px"; let input; if (field === "category") { // Create a select element for category input = document.createElement("select"); input.name = field; input.style.width = "100%"; input.style.padding = "5px"; input.style.marginTop = "5px"; // Add options to the select element categories.forEach((category) => { const option = document.createElement("option"); option.value = category.value; option.textContent = category.display; input.appendChild(option); }); // Set the selected value if it exists in existingMetadata if (existingMetadata[field]) { input.value = existingMetadata[field]; } else { input.value = ""; // Default to blank line } } else { // Create an input element for other fields input = document.createElement("input"); input.type = "text"; input.name = field; input.style.width = "100%"; input.style.padding = "5px"; input.style.marginTop = "5px"; if (existingMetadata[field]) { if (field === "tags" && Array.isArray(existingMetadata[field])) { input.value = existingMetadata[field].join(", "); } else { input.value = existingMetadata[field]; } } else { input.placeholder = field.charAt(0).toUpperCase() + field.slice(1); } } label.appendChild(input); form.appendChild(label); }); // Create the button container const buttonContainer = document.createElement("div"); buttonContainer.style.textAlign = "right"; buttonContainer.style.marginTop = "20px"; // Create the Save and Cancel buttons const saveButton = document.createElement("button"); saveButton.textContent = "Save"; saveButton.type = "submit"; saveButton.style.marginLeft = "10px"; const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.type = "button"; buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(saveButton); form.appendChild(buttonContainer); modalContent.appendChild(form); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); // Event listeners cancelButton.addEventListener("click", (event) => { event.preventDefault(); document.body.removeChild(modalOverlay); closePublishModal(); resolve(null); }); form.addEventListener("submit", (event) => { event.preventDefault(); // Collect the metadata const formData = new FormData(form); const updatedMetadata = {}; fields.forEach((field) => { const value = formData.get(field); if (value) { updatedMetadata[field] = value; } }); document.body.removeChild(modalOverlay); resolve(updatedMetadata); }); }); } let publishModal = null; function showPublishModal(message) { if (!publishModal) { // Create the modal publishModal = document.createElement("div"); publishModal.style.position = "fixed"; publishModal.style.top = "0"; publishModal.style.left = "0"; publishModal.style.width = "100%"; publishModal.style.height = "100%"; publishModal.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; publishModal.style.display = "flex"; publishModal.style.justifyContent = "center"; publishModal.style.alignItems = "center"; publishModal.style.zIndex = "1000"; // Create the modal content container const modalContent = document.createElement("div"); modalContent.style.backgroundColor = "#2d3749"; // Use background color from main content modalContent.style.padding = "20px"; modalContent.style.borderRadius = "5px"; modalContent.style.maxWidth = "400px"; modalContent.style.width = "90%"; modalContent.style.textAlign = "center"; // Create the message element const messageElement = document.createElement("p"); messageElement.id = "publish-modal-message"; messageElement.textContent = message; modalContent.appendChild(messageElement); publishModal.appendChild(modalContent); document.body.appendChild(publishModal); } else { // Update the message const messageElement = publishModal.querySelector("#publish-modal-message"); messageElement.textContent = message; // Remove any buttons (Retry/Cancel) if they exist const buttons = publishModal.querySelector("#publish-modal-buttons"); if (buttons) { buttons.remove(); } } if (message === "Publish TX submitted! Confirmation needed.") { // Create buttons container const buttonsContainer = document.createElement("div"); buttonsContainer.id = "publish-modal-buttons"; buttonsContainer.style.marginTop = "20px"; // Create Close button const closeButton = document.createElement("button"); closeButton.textContent = "Close"; closeButton.addEventListener("click", () => { closePublishModal(); }); buttonsContainer.appendChild(closeButton); // Append button to modal content const modalContent = publishModal.firstChild; modalContent.appendChild(buttonsContainer); } } function closePublishModal() { if (publishModal) { document.body.removeChild(publishModal); publishModal = null; } } function showPublishErrorModal(errorMessage, onRetry, onCancel) { if (publishModal) { // Update the message const messageElement = publishModal.querySelector("#publish-modal-message"); messageElement.textContent = errorMessage; // Remove any existing buttons const existingButtons = publishModal.querySelector("#publish-modal-buttons"); if (existingButtons) { existingButtons.remove(); } // Create buttons container const buttonsContainer = document.createElement("div"); buttonsContainer.id = "publish-modal-buttons"; buttonsContainer.style.marginTop = "20px"; // Create Retry button const retryButton = document.createElement("button"); retryButton.textContent = "Retry"; retryButton.style.marginRight = "10px"; retryButton.addEventListener("click", () => { onRetry(); }); // Create Cancel button const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.addEventListener("click", () => { onCancel(); }); buttonsContainer.appendChild(retryButton); buttonsContainer.appendChild(cancelButton); // Append buttons to modal content const modalContent = publishModal.firstChild; modalContent.appendChild(buttonsContainer); } } async function publishWithFeedback(publishParams) { return new Promise(async (resolve, reject) => { async function attemptPublish() { try { // Show modal with "Attempting to publish, please wait..." showPublishModal("Attempting to publish, please wait..."); const response = await qortalRequest(publishParams); // Close modal resolve(response); showPublishModal("Publish TX submitted! Confirmation needed."); } catch (error) { // Update modal to show error message and Retry/Cancel buttons showPublishErrorModal( `Publishing failed: ${error.message}`, () => { // On Retry attemptPublish(); }, () => { // On Cancel closePublishModal(); reject(error); } ); } } await attemptPublish(); }); } // ===== Publish helpers: new actions (Add file/folder/New text) ===== function guessServiceFromFilename(filename) { const lower = String(filename || "").toLowerCase(); if (/(\.jpg|\.jpeg|\.png|\.gif|\.webp)$/.test(lower)) { return "IMAGE"; } if (/(\.mp4|\.m4v|\.webm|\.ogv)$/.test(lower)) { return "VIDEO"; } if (/(\.mp3|\.wav|\.ogg|\.m4a|\.aac)$/.test(lower)) { return "AUDIO"; } if (/\.json$/.test(lower)) { return "JSON"; } if (/(\.txt|\.md|\.csv|\.log)$/.test(lower)) { return "DOCUMENT"; } if (/\.pdf$/.test(lower)) { return "DOCUMENT"; } return "FILE"; } function defaultIdentifierFromFileName(filename) { const base = String(filename || "") .split("/") .pop() || ""; const i = base.lastIndexOf("."); return i > 0 ? base.slice(0, i) : base; } function safeIdentifierFromPath(path) { const p = String(path || "").replace(/^\.+\/?/, ""); return p.replace(/[\\/]+/g, "_"); } function applyMetadataToParams(target, md) { if (!md) { return; } const fields = ["filename", "title", "description", "category"]; for (const k of fields) { if (md[k]) { target[k] = md[k]; } else { delete target[k]; } } if (md["tags"]) { const tagsArray = String(md["tags"]) .split(",") .map((t) => t.trim()) .filter(Boolean); for (let i = 1; i <= 5; i++) { if (tagsArray[i - 1]) { target[`tag${i}`] = tagsArray[i - 1]; } else { delete target[`tag${i}`]; } } } else { for (let i = 1; i <= 5; i++) { delete target[`tag${i}`]; } } } function openPublishDetailsDialog(defaults) { return new Promise((resolve) => { const modalOverlay = document.createElement("div"); Object.assign(modalOverlay.style, { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", backgroundColor: "rgba(0, 0, 0, 0.5)", display: "flex", justifyContent: "center", alignItems: "center", zIndex: "1000", }); const modalContent = document.createElement("div"); Object.assign(modalContent.style, { backgroundColor: "#2d3749", color: "#c9d2d9", padding: "20px", borderRadius: "12px", width: "90%", maxWidth: "520px", fontFamily: "'Lexend', sans-serif", lineHeight: "1.6", }); const title = document.createElement("div"); title.textContent = "Publish details"; title.style.fontWeight = "600"; title.style.marginBottom = "10px"; const form = document.createElement("form"); form.innerHTML = `
`; const svcInput = form.querySelector('input[name="service"]'); const idInput = form.querySelector('input[name="identifier"]'); if (svcInput) { svcInput.value = defaults?.service || ""; } if (idInput) { idInput.value = defaults?.identifier || ""; } form.querySelector('[data-act="cancel"]').addEventListener("click", () => { document.body.removeChild(modalOverlay); resolve(null); }); form.addEventListener("submit", (e) => { e.preventDefault(); const svc = String(svcInput.value || "") .trim() .toUpperCase(); const ident = String(idInput.value || "").trim(); document.body.removeChild(modalOverlay); resolve({ service: svc, identifier: ident }); }); modalContent.appendChild(title); modalContent.appendChild(form); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); }); } async function handlePublishAddFile() { try { if (!isAuthenticated || !userName || userName === "Name unavailable") { alert("Authenticate and select a name first."); return; } // Pick a file const inp = document.createElement("input"); inp.type = "file"; const file = await new Promise((resolve, reject) => { inp.onchange = (e) => { const f = /** @type {HTMLInputElement} */ (e.target).files?.[0] || null; resolve(f); }; inp.onerror = reject; inp.click(); }); if (!file) { return; } const svcGuess = guessServiceFromFilename(file.name); const identGuess = defaultIdentifierFromFileName(file.name); const details = await openPublishDetailsDialog({ service: svcGuess, identifier: identGuess }); if (!details) { return; } const md = await openMetadataEditorDialog({ filename: file.name }); if (md === null) { return; } const params = { action: "PUBLISH_QDN_RESOURCE", name: userName, service: details.service, identifier: details.identifier || "", file, }; applyMetadataToParams(params, md); await publishWithFeedback(params); // Refresh data showSpinner(); try { await loadAllResults(); buildSidebarTree(); applyServiceFilter(); await fetchPage(); } finally { hideSpinner(); } } catch (e) { alert("Publish failed: " + (e?.message || e)); } } async function handlePublishAddFolder() { try { if (!isAuthenticated || !userName || userName === "Name unavailable") { alert("Authenticate and select a name first."); return; } const inp = document.createElement("input"); inp.type = "file"; // @ts-ignore - webkitdirectory is widely supported in Chromium-based browsers inp.webkitdirectory = true; inp.multiple = true; const files = await new Promise((resolve, reject) => { inp.onchange = (e) => { const list = /** @type {HTMLInputElement} */ (e.target).files; resolve(list ? Array.from(list) : []); }; inp.onerror = reject; inp.click(); }); if (!files || files.length === 0) { return; } // Suggest service FILE for mixed folders const details = await openPublishDetailsDialog({ service: "FILE", identifier: "" }); if (!details) { return; } const md = await openMetadataEditorDialog({}); if (md === null) { return; } showPublishModal(`Publishing ${files.length} files…`); try { const resources = files.map((f) => { const rel = /** @type {any} */ (f).webkitRelativePath || f.name; const ident = safeIdentifierFromPath(rel); const r = { name: userName, service: details.service, identifier: ident || "default", file: f, filename: f.name, }; applyMetadataToParams(r, md); return r; }); const response = await qortalRequest({ action: "PUBLISH_MULTIPLE_QDN_RESOURCES", resources, }); console.log("Folder publish response:", response); // Reload data await loadAllResults(); buildSidebarTree(); applyServiceFilter(); await fetchPage(); } finally { closePublishModal(); } } catch (e) { closePublishModal(); alert("Folder publish failed: " + (e?.message || e)); } } // ===== Compose (New text) page ===== let composeMetadata = {}; async function openComposePage({ service, identifier, content } = {}) { composeMetadata = {}; const svcEl = document.getElementById("compose-service"); const idEl = document.getElementById("compose-identifier"); const txt = document.getElementById("compose-text"); const sum = document.getElementById("compose-metadata-summary"); if (svcEl) { /** @type {HTMLInputElement} */ (svcEl).value = (service || "DOCUMENT").toString(); } if (idEl) { /** @type {HTMLInputElement} */ (idEl).value = (identifier || "").toString(); } if (txt) { /** @type {HTMLTextAreaElement} */ (txt).value = (content || "").toString(); } if (sum) { sum.textContent = ""; } showSection("compose"); // Wire one-time handlers const editBtn = document.getElementById("compose-edit-metadata"); if (editBtn && editBtn.dataset.bound !== "1") { editBtn.dataset.bound = "1"; editBtn.addEventListener("click", async () => { const updated = await openMetadataEditorDialog(composeMetadata || {}); if (updated) { composeMetadata = updated; const keys = Object.keys(updated).filter( (k) => updated[k] && String(updated[k]).trim() !== "" ); const sumEl = document.getElementById("compose-metadata-summary"); if (sumEl) { sumEl.textContent = keys.length > 0 ? `${keys.length} field(s) set` : ""; } } }); } const backBtn = document.getElementById("compose-back-btn"); if (backBtn && backBtn.dataset.bound !== "1") { backBtn.dataset.bound = "1"; backBtn.addEventListener("click", async () => { showSection(searchModeActive ? "search" : "content"); }); } const pubBtn = document.getElementById("compose-publish"); if (pubBtn && pubBtn.dataset.bound !== "1") { pubBtn.dataset.bound = "1"; pubBtn.addEventListener("click", async () => { try { if (!isAuthenticated || !userName || userName === "Name unavailable") { alert("Authenticate and select a name first."); return; } const svc = /** @type {HTMLInputElement} */ ( document.getElementById("compose-service") ).value .trim() .toUpperCase(); const ident = /** @type {HTMLInputElement} */ ( document.getElementById("compose-identifier") ).value.trim(); const text = /** @type {HTMLTextAreaElement} */ (document.getElementById("compose-text")) .value; if (!svc) { alert("Enter a service (e.g. DOCUMENT, JSON)"); return; } const params = { action: "PUBLISH_QDN_RESOURCE", name: userName, service: svc, identifier: ident || "", file: new Blob([text || ""], { type: svc === "JSON" ? "application/json" : "text/plain", }), }; applyMetadataToParams(params, composeMetadata); await publishWithFeedback(params); // Refresh view showSpinner(); try { await loadAllResults(); buildSidebarTree(); applyServiceFilter(); await fetchPage(); } finally { hideSpinner(); } showSection("content"); } catch (e) { alert("Publish failed: " + (e?.message || e)); } }); } } window.addEventListener("beforeunload", () => { const el = document.querySelector("#inline-viewer > *[data-bloburl]"); if (el && el.dataset.bloburl) { try { URL.revokeObjectURL(el.dataset.bloburl); } catch {} } }); const searchBtn = document.getElementById("search-button"); if (searchBtn) { searchBtn.addEventListener("click", async () => { if (searchModeActive) { // Toggle off: same behavior as sidebar "Back to My Files" setSearchMode(false); showSpinner(); try { await fetchPage(); } finally { hideSpinner(); } } else { setSearchMode(true); const sp = document.getElementById("search-page"); const ph = sp ? sp.querySelector(".search-placeholder") : null; if (ph) { ph.textContent = "Search is coming soon…"; } } }); } function updateSidebarBanner() { const nm = document.getElementById("sidebar-name"); const ctxBtn = document.getElementById("sidebar-context"); const exitBtn = document.getElementById("sidebar-exit-search"); if (searchModeActive) { if (ctxBtn) { ctxBtn.disabled = false; ctxBtn.textContent = "Search Results"; } if (exitBtn) { exitBtn.style.display = "inline-block"; } } else { if (ctxBtn) { ctxBtn.disabled = true; ctxBtn.innerHTML = 'My Files — ' + (userName || "(not authenticated)") + ""; } if (exitBtn) { exitBtn.style.display = "none"; } } if (nm) { nm.textContent = userName || "(not authenticated)"; } } const previewBackBtn = document.getElementById("preview-back-btn"); if (previewBackBtn) { previewBackBtn.addEventListener("click", () => { // Return to Search if we came from Search mode; otherwise back to My Files showSection(searchModeActive ? "search" : "content"); }); } // Sidebar banner button wiring const sidebarContextBtn = document.getElementById("sidebar-context"); if (sidebarContextBtn) { sidebarContextBtn.addEventListener("click", () => { if (searchModeActive) { showSection("search"); } }); } const sidebarExitBtn = document.getElementById("sidebar-exit-search"); if (sidebarExitBtn) { sidebarExitBtn.addEventListener("click", async () => { setSearchMode(false); showSpinner(); try { await fetchPage(); } finally { hideSpinner(); } }); } // ===== Search UI (form + results) ===== function initSearchUI() { const form = document.getElementById("search-form"); const resultsHost = document.getElementById("search-results"); const moreWrap = document.getElementById("search-more"); const moreBtn = document.getElementById("search-load-more"); const resetBtn = document.getElementById("search-reset"); const summary = document.getElementById("search-summary"); if (!form || !resultsHost || !moreBtn || !summary) { return; } if (form.dataset.bound === "1") { // Ensure summary reflects current state when toggling back renderSearchResults(); return; } form.dataset.bound = "1"; // Restore last query try { const raw = localStorage.getItem(LS_LAST_SEARCH_KEY); if (raw) { const p = JSON.parse(raw); applySearchParamsToForm(form, p); searchState.params = p; searchState.limit = Number(p.limit) || 100; } } catch {} form.addEventListener("submit", async (e) => { e.preventDefault(); await performSearch({ reset: true }); }); if (resetBtn) { resetBtn.addEventListener("click", () => { form.reset(); searchState = { params: null, results: [], offset: 0, limit: 100, hasMore: false, inFlight: false, }; resultsHost.innerHTML = ""; summary.textContent = ""; if (moreWrap) { moreWrap.style.display = "none"; } try { localStorage.removeItem(LS_LAST_SEARCH_KEY); } catch {} }); } moreBtn.addEventListener("click", async () => { await performSearch({ reset: false }); }); } function readSearchParamsFromForm() { const form = document.getElementById("search-form"); if (!form) { return null; } const get = (id) => { const el = document.getElementById(id); return el ? el.value.trim() : ""; }; const getBool = (id) => { const el = document.getElementById(id); return !!(el && /** @type {HTMLInputElement} */ (el).checked); }; const getNum = (id, d) => { const el = document.getElementById(id); const v = el ? parseInt(/** @type {HTMLSelectElement} */ (el).value, 10) : d; return Number.isFinite(v) && v > 0 ? v : d; }; // Normalize service to uppercase as QDN expects exact case (e.g., APP) let service = get("search-service"); if (service) { service = service.toUpperCase(); } const p = { query: get("search-query"), name: get("search-name"), identifier: get("search-identifier"), service, prefix: getBool("search-prefix"), includeMetadata: getBool("search-include-metadata"), exactMatchNames: getBool("search-exact-names"), reverse: getBool("search-reverse"), limit: getNum("search-limit", 100), }; return p; } function applySearchParamsToForm(form, p) { const setVal = (id, v) => { const el = /** @type {HTMLInputElement|HTMLSelectElement|null} */ (document.getElementById(id)); if (el != null && typeof v !== "undefined") { el.value = String(v); } }; const setChk = (id, v) => { const el = /** @type {HTMLInputElement|null} */ (document.getElementById(id)); if (el != null) { el.checked = !!v; } }; setVal("search-query", p.query || ""); setVal("search-name", p.name || ""); setVal("search-identifier", p.identifier || ""); setVal("search-service", p.service || ""); setChk("search-prefix", !!p.prefix); setChk("search-include-metadata", !!p.includeMetadata); setChk("search-exact-names", !!p.exactMatchNames); setChk("search-reverse", p.reverse !== false); setVal("search-limit", String(p.limit || 100)); } function buildSearchUrl(params, offset) { const u = new URL(location.origin + "/arbitrary/resources/search"); const add = (k, v) => { if (v === undefined || v === null) { return; } const s = String(v).trim(); if (s.length === 0) { return; } u.searchParams.append(k, s); }; add("query", params.query); add("identifier", params.identifier); add("name", params.name); add("service", params.service); if (params.prefix) { add("prefix", true); } if (params.includeMetadata) { add("includemetadata", true); } if (params.exactMatchNames) { add("exactmatchnames", true); } add("reverse", params.reverse !== false); add("limit", params.limit || 100); add("offset", offset || 0); // Return newest-first results by default add("mode", "ALL"); return u.toString(); } async function performSearch({ reset }) { if (searchState.inFlight) { return; } const form = document.getElementById("search-form"); const resultsHost = document.getElementById("search-results"); const summary = document.getElementById("search-summary"); const moreWrap = document.getElementById("search-more"); if (!form || !resultsHost || !summary) { return; } const params = readSearchParamsFromForm(); if (!params || (!params.query && !params.identifier && !params.name && !params.service)) { summary.textContent = "Enter at least one field to search."; return; } searchState.params = params; searchState.limit = Number(params.limit) || 100; if (reset) { searchState.results = []; searchState.offset = 0; searchState.hasMore = false; resultsHost.innerHTML = ""; } const url = buildSearchUrl(params, searchState.offset); try { searchState.inFlight = true; summary.textContent = "Searching…"; const resp = await fetch(url); if (!resp.ok) { throw new Error("Search failed"); } const items = await resp.json(); // Normalize/sort by updated if reverse not handled if (!params.reverse) { items.sort((a, b) => (a.updated || a.created || 0) - (b.updated || b.created || 0)); } searchState.results.push(...items); searchState.offset += items.length; searchState.hasMore = items.length >= searchState.limit; try { localStorage.setItem(LS_LAST_SEARCH_KEY, JSON.stringify(params)); } catch {} renderSearchResults(); if (moreWrap) { moreWrap.style.display = searchState.hasMore ? "block" : "none"; } } catch (e) { summary.textContent = "Search error: " + (e?.message || e); } finally { searchState.inFlight = false; } } function renderSearchResults() { const resultsHost = document.getElementById("search-results"); const summary = document.getElementById("search-summary"); const moreWrap = document.getElementById("search-more"); if (!resultsHost || !summary) { return; } const items = searchState.results || []; const total = items.length; if (total === 0) { resultsHost.innerHTML = ""; summary.textContent = searchState.inFlight ? "Searching…" : "No results yet."; if (moreWrap) { moreWrap.style.display = "none"; } return; } summary.textContent = `${total} result${total === 1 ? "" : "s"}${searchState.hasMore ? " (more available)" : ""}`; // Build table with inline previews let html = ""; for (const r of items) { const name = r.name || ""; const svc = r.service || ""; const ident = r.identifier === undefined || r.identifier === null || r.identifier === "" ? "default" : r.identifier; const title = r.metadata && r.metadata.title ? r.metadata.title : ""; const size = Number(r.size || 0).toLocaleString(); const upd = new Date(r.updated || r.created || 0); const updated = isNaN(upd) ? "" : upd.toLocaleString(); const previewCell = generatePreviewHTML(r, name, ident); const row = ` `; html += row; } html += "
NameServiceIdentifierTitlePreviewSizeUpdatedOpen
${escapeHtml(name)} ${escapeHtml(svc)} ${escapeHtml(ident)} ${escapeHtml(title)} ${previewCell} ${size} ${escapeHtml(updated)} Open
"; resultsHost.innerHTML = html; // Initialize inline preview holders within search results only initPreviews(resultsHost); // Wire up preview links resultsHost.querySelectorAll(".open-preview").forEach((el) => { el.addEventListener("click", () => { const nm = el.getAttribute("data-name") || ""; const svc = el.getAttribute("data-service") || ""; const ident = el.getAttribute("data-identifier") || "default"; openPreviewPage({ name: nm, service: svc, identifier: ident }); }); }); } function escapeHtml(s) { return String(s || "").replace( /[&<>\"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch] ); } function escapeAttr(s) { return escapeHtml(s).replace(/"/g, """); } // ===== Global media controller ===== const mediaControls = document.getElementById("media-controls"); const mediaPlayPause = document.getElementById("media-play-pause"); const mediaStop = document.getElementById("media-stop"); const mediaTitle = document.getElementById("media-title"); const globalMediaHost = document.createElement("div"); globalMediaHost.id = "global-media-host"; globalMediaHost.style.display = "none"; document.body.appendChild(globalMediaHost); let globalMedia = { el: null, ctx: null, blobUrl: null }; function setMediaTitle(ctx) { if (!mediaTitle) { return; } if (!ctx) { mediaTitle.textContent = ""; mediaTitle.title = ""; return; } const t = `${ctx.service || ""} — ${ctx.identifier || "default"} — ${ctx.name || ""}`.trim(); mediaTitle.textContent = t; mediaTitle.title = t; } function showMediaControls(show) { if (!mediaControls) { return; } mediaControls.style.display = show ? "inline-flex" : "none"; if (!show) { setMediaTitle(null); } } function promoteMedia(el, ctx) { if (!el) { return; } try { globalMediaHost.appendChild(el); } catch {} globalMedia.el = el; globalMedia.ctx = ctx || null; globalMedia.blobUrl = el.dataset && el.dataset.bloburl ? el.dataset.bloburl : null; setMediaTitle(ctx); showMediaControls(true); } function releaseGlobalMedia() { if (globalMedia.el && globalMedia.el.parentElement === globalMediaHost) { try { globalMediaHost.removeChild(globalMedia.el); } catch {} } globalMedia.el = null; globalMedia.ctx = null; globalMedia.blobUrl = null; showMediaControls(false); } if (mediaPlayPause) { mediaPlayPause.addEventListener("click", () => { const el = globalMedia.el; if (!el) { return; } if (el.paused) { el.play().catch(() => {}); } else { el.pause(); } }); } if (mediaStop) { mediaStop.addEventListener("click", () => { const el = globalMedia.el; if (!el) { return; } try { el.pause(); el.currentTime = 0; } catch {} if (globalMedia.blobUrl) { try { URL.revokeObjectURL(globalMedia.blobUrl); } catch {} } releaseGlobalMedia(); }); } let currentPreviewCtx = null; function isTextDisplayedInPreview() { const container = document.getElementById("preview-container"); if (!container) { return false; } if (container.dataset && container.dataset.textual != null) { return container.dataset.textual === "1"; } const el = container.firstElementChild; if (!el) { return false; } if (el.classList && el.classList.contains("inline-text-editor")) { return true; } if (el.tagName === "IFRAME" && el.getAttribute("srcdoc") != null) { return true; } if (el.tagName === "PRE") { const ws = el.style.whiteSpace || (window.getComputedStyle ? getComputedStyle(el).whiteSpace : ""); return String(ws).toLowerCase().includes("pre-wrap"); } const pre = container.querySelector(":scope > div pre"); if (pre) { const ws = pre.style.whiteSpace || (window.getComputedStyle ? getComputedStyle(pre).whiteSpace : ""); return String(ws).toLowerCase().includes("pre-wrap"); } return false; } function updatePreviewActionsState() { const editBtn = document.getElementById("preview-edit"); const replaceBtn = document.getElementById("preview-replace"); const deleteBtn = document.getElementById("preview-delete"); if (!editBtn || !replaceBtn || !deleteBtn) { return; } const textual = isTextDisplayedInPreview(); // Update button tooltips to reflect behavior; labels remain stable editBtn.title = textual ? "Edit inline" : "Edit (choose file)"; replaceBtn.title = "Replace (choose file)"; // Ownership: show actions only if previewing an item published by the current selected name const isOwner = !!( currentPreviewCtx && currentPreviewCtx.name && userName && currentPreviewCtx.name === userName ); [editBtn, replaceBtn, deleteBtn].forEach((btn) => { btn.style.display = isOwner ? "inline-block" : "none"; btn.disabled = !isOwner; }); }