// ===== 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, }; // Search-mode filters (mirror of My Files filters, but scoped to search) let searchSelectedServices = new Set(); let searchCurrentPrefixFilter = null; let searchCurrentIdentifierFilter = null; let searchCurrentNameFilter = null; let searchFilteredResults = []; let searchGroupByName = (function () { try { return window.localStorage.getItem("qedit:searchGroupByName") === "true"; } catch { return false; } })(); // ===== Timestamp display mode (Updated column) ===== // Modes: 'ago' (default), 'full', 'raw' const LS_TIMESTAMP_MODE_KEY = "qedit:timestampMode"; let timestampDisplayMode = (function () { try { const v = window.localStorage.getItem(LS_TIMESTAMP_MODE_KEY); return v === "full" || v === "raw" ? v : "ago"; } catch { return "ago"; } })(); function setTimestampDisplayMode(mode) { timestampDisplayMode = mode === "full" || mode === "raw" ? mode : "ago"; try { window.localStorage.setItem(LS_TIMESTAMP_MODE_KEY, timestampDisplayMode); } catch {} } function cycleTimestampDisplayMode() { const next = timestampDisplayMode === "ago" ? "full" : timestampDisplayMode === "full" ? "raw" : "ago"; setTimestampDisplayMode(next); // Re-render current tables to reflect the change try { // My Files table fetchPage(); } catch {} try { // Search results table (if visible/loaded) renderSearchResults(); } catch {} } function getTimestampModeLabel(mode) { switch (mode || timestampDisplayMode) { case "full": return "Full date"; case "raw": return "Raw"; default: return "Relative"; } } function pad2(n) { return String(n).padStart(2, "0"); } function formatTimestampFull(ts) { const t = Number(ts); if (!Number.isFinite(t) || t <= 0) { return "Unknown"; } const d = new Date(t); if (isNaN(d.getTime())) { return "Unknown"; } return ( d.getFullYear() + "/" + pad2(d.getMonth() + 1) + "/" + pad2(d.getDate()) + " " + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()) ); } function formatTimestampDisplay(ts) { switch (timestampDisplayMode) { case "full": return formatTimestampFull(ts); case "raw": return String(Number(ts) || 0); case "ago": default: return formatTimeAgo(ts); } } // ===== Column sorting (My Files + Search) ===== const LS_SORT_MY = "qedit:sort:my"; const LS_SORT_SEARCH = "qedit:sort:search"; function loadSort(context) { const key = context === "search" ? LS_SORT_SEARCH : LS_SORT_MY; try { const raw = window.localStorage.getItem(key); if (raw) { const obj = JSON.parse(raw); if (obj && (obj.dir === "asc" || obj.dir === "desc") && typeof obj.key === "string") { return { key: obj.key, dir: obj.dir }; } } } catch {} // Default: newest first return { key: "updated", dir: "desc" }; } function saveSort(context, state) { const key = context === "search" ? LS_SORT_SEARCH : LS_SORT_MY; try { window.localStorage.setItem(key, JSON.stringify({ key: state.key, dir: state.dir })); } catch {} } let myFilesSort = loadSort("my"); let searchSort = loadSort("search"); function getSortState(context) { return context === "search" ? searchSort : myFilesSort; } function setSortState(context, state) { if (context === "search") { searchSort = state; saveSort("search", searchSort); } else { myFilesSort = state; saveSort("my", myFilesSort); } } function applySortToggle(context, key) { const st = { ...getSortState(context) }; if (st.key === key) { st.dir = st.dir === "asc" ? "desc" : "asc"; } else { st.key = key; st.dir = "asc"; } setSortState(context, st); // Re-render appropriate view if (context === "search") { try { renderSearchResults(); } catch {} } else { try { fetchPage(); } catch {} } } function buildSortHeader(label, key, context) { const st = getSortState(context); const active = st.key === key; const dir = active ? st.dir : null; const arrow = !active ? "" : dir === "asc" ? " ▲" : " ▼"; const aria = active ? (dir === "asc" ? "ascending" : "descending") : "none"; const title = `Sort by ${label}${active ? ` (${dir})` : ""}`; return ( '" ); } function getSortValue(item, key) { switch (key) { case "name": return String(item.name || "").toLowerCase(); case "service": return String(item.service || "").toLowerCase(); case "identifier": { const id = item.identifier; return String(id === undefined || id === null || id === "" ? "default" : id).toLowerCase(); } case "metadata": { try { return Object.keys(item.metadata || {}) .join(", ") .toLowerCase(); } catch { return ""; } } case "preview": { const s = String(item.service || "").toLowerCase(); const id = String(item.identifier || "default").toLowerCase(); return s + "::" + id; } case "size": return Number(item.size) || 0; case "updated": return Number(item.updated || item.created || 0); default: return ""; } } function compareResultsGeneric(a, b, sortState) { const key = sortState.key || "updated"; const dir = sortState.dir === "asc" ? 1 : -1; // multiply by dir const av = getSortValue(a, key); const bv = getSortValue(b, key); let cmp = 0; if (typeof av === "number" || typeof bv === "number") { const an = Number(av) || 0; const bn = Number(bv) || 0; cmp = an === bn ? 0 : an < bn ? -1 : 1; } else { const as = String(av || ""); const bs = String(bv || ""); cmp = as.localeCompare(bs); } if (cmp === 0) { // Tiebreaker: newest updated/created first const au = Number(a.updated || a.created || 0) || 0; const bu = Number(b.updated || b.created || 0) || 0; cmp = bu === au ? 0 : bu < au ? -1 : 1; // desc } return cmp * dir; } const compareResultsMyFiles = (a, b) => compareResultsGeneric(a, b, myFilesSort); const compareResultsSearch = (a, b) => compareResultsGeneric(a, b, searchSort); function buildUpdatedHeaderHTML(context) { const label = getTimestampModeLabel(); const title = `Timestamp: ${label} (click to change)`; // Simple inline clock icon const icon = '"; return ( '' + buildSortHeader("Updated", "updated", context) + ' " ); } function clearSearchTreeFilters() { searchSelectedServices = new Set(); searchCurrentPrefixFilter = null; searchCurrentIdentifierFilter = null; searchCurrentNameFilter = null; } function applySearchFilter() { const base = getSearchBaselineResults(); let tmp = base.slice(); if (searchSelectedServices.size > 0) { tmp = tmp.filter((r) => searchSelectedServices.has(r.service)); } if (searchCurrentNameFilter) { const nm = searchCurrentNameFilter; tmp = tmp.filter((r) => (r.name || "") === nm); } if (searchCurrentPrefixFilter) { const pfx = searchCurrentPrefixFilter; tmp = tmp.filter((r) => { const id = r.identifier || ""; return id.startsWith(pfx + "_") || id.startsWith(pfx + "-"); }); } if (searchCurrentIdentifierFilter) { const ident = searchCurrentIdentifierFilter; tmp = tmp.filter((r) => (r.identifier || "") === ident); } searchFilteredResults = tmp; } // 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 } // ===== All QDN services (public + private) ===== // Used for service input combobox suggestions in Search and Publish flows. const ALL_QDN_SERVICES = [ "AUTO_UPDATE", "ARBITRARY_DATA", "QCHAT_ATTACHMENT", "QCHAT_ATTACHMENT_PRIVATE", "ATTACHMENT", "ATTACHMENT_PRIVATE", "FILE", "FILE_PRIVATE", "FILES", "CHAIN_DATA", "WEBSITE", "GIT_REPOSITORY", "IMAGE", "IMAGE_PRIVATE", "THUMBNAIL", "QCHAT_IMAGE", "VIDEO", "VIDEO_PRIVATE", "AUDIO", "AUDIO_PRIVATE", "QCHAT_AUDIO", "QCHAT_VOICE", "VOICE", "VOICE_PRIVATE", "PODCAST", "BLOG", "BLOG_POST", "BLOG_COMMENT", "DOCUMENT", "DOCUMENT_PRIVATE", "LIST", "PLAYLIST", "APP", "METADATA", "JSON", "GIF_REPOSITORY", "STORE", "PRODUCT", "OFFER", "COUPON", "CODE", "PLUGIN", "EXTENSION", "GAME", "ITEM", "NFT", "DATABASE", "SNAPSHOT", "COMMENT", "CHAIN_COMMENT", "MAIL", "MAIL_PRIVATE", "MESSAGE", "MESSAGE_PRIVATE", ]; // === 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); } // Baseline for Search mode results function getSearchBaselineResults() { const arr = searchState && Array.isArray(searchState.results) ? searchState.results : []; if (!hideDeleted) { return arr; } return arr.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(); } })(); // ===== Service Combobox (shared) ===== /** * Enhance a text input into a combobox with a dropdown of all QDN services and * type-ahead filtering. Matches anywhere in the service string (case-insensitive). * @param {HTMLInputElement} input */ function createServiceCombobox(input) { if (!input || input.dataset.comboInit === "1") { return; } input.dataset.comboInit = "1"; // Wrap input and add toggle + list const wrap = document.createElement("div"); wrap.className = "service-combobox"; input.parentElement?.insertBefore(wrap, input); wrap.appendChild(input); const btn = document.createElement("button"); btn.type = "button"; btn.className = "service-combobox-button"; btn.title = "Show services"; btn.setAttribute("aria-label", "Show services"); btn.innerHTML = ''; wrap.appendChild(btn); const list = document.createElement("div"); list.className = "service-combobox-list"; list.setAttribute("role", "listbox"); wrap.appendChild(list); let open = false; let items = []; let activeIndex = -1; function closeList() { open = false; activeIndex = -1; wrap.classList.remove("open"); Array.from(list.children).forEach((c) => c.removeAttribute("aria-selected")); } function openList() { if (!open) { open = true; wrap.classList.add("open"); } } function rank(service, q) { if (!q) { return 3; } if (service === q) { return 0; } if (service.startsWith(q)) { return 1; } if (service.endsWith(q)) { return 2; } if (service.includes(q)) { return 3; } return 4; } function renderList(q) { const query = String(q || "").toUpperCase(); let matches = ALL_QDN_SERVICES.filter((s) => s.includes(query)); matches.sort((a, b) => { const ra = rank(a, query); const rb = rank(b, query); if (ra !== rb) { return ra - rb; } // Prefer shorter then alphabetical for equal rank if (a.length !== b.length) { return a.length - b.length; } return a.localeCompare(b); }); // Rebuild DOM items list.innerHTML = ""; items = matches.map((svc) => { const it = document.createElement("div"); it.className = "service-combobox-item"; it.setAttribute("role", "option"); it.textContent = svc; it.addEventListener("mousedown", (e) => { e.preventDefault(); input.value = svc; closeList(); input.dispatchEvent(new Event("change", { bubbles: true })); }); list.appendChild(it); return it; }); } function moveActive(delta) { if (!open || items.length === 0) { return; } const n = items.length; activeIndex = (activeIndex + delta + n) % n; items.forEach((el, i) => { if (i === activeIndex) { el.setAttribute("aria-selected", "true"); el.scrollIntoView({ block: "nearest" }); } else { el.removeAttribute("aria-selected"); } }); } input.addEventListener("input", () => { renderList(input.value); openList(); }); input.addEventListener("focus", () => { renderList(input.value); openList(); }); input.addEventListener("keydown", (e) => { if (!open && (e.key === "ArrowDown" || e.key === "Enter")) { renderList(input.value); openList(); e.preventDefault(); return; } if (e.key === "ArrowDown") { moveActive(1); e.preventDefault(); } else if (e.key === "ArrowUp") { moveActive(-1); e.preventDefault(); } else if (e.key === "Enter") { if (open && activeIndex >= 0 && activeIndex < items.length) { const el = items[activeIndex]; el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); e.preventDefault(); } } else if (e.key === "Escape") { closeList(); e.preventDefault(); } }); btn.addEventListener("click", () => { if (open) { closeList(); } else { renderList(""); openList(); input.focus(); } }); // Click outside closes document.addEventListener("click", (e) => { if (!wrap.contains(e.target)) { closeList(); } }); } /** * Name autosuggest for the Search form. Reuses the dropdown styles but without a toggle button. * Fetches suggestions via qortalRequest(SEARCH_NAMES) with prefix matching as the user types. * @param {HTMLInputElement} input */ function createNameAutosuggest(input) { if (!input || input.dataset.nameSuggestInit === "1") { return; } input.dataset.nameSuggestInit = "1"; const wrap = document.createElement("div"); wrap.className = "service-combobox"; // reuse positioning/styles input.parentElement?.insertBefore(wrap, input); wrap.appendChild(input); const list = document.createElement("div"); list.className = "service-combobox-list"; // reuse dropdown styles list.setAttribute("role", "listbox"); wrap.appendChild(list); let open = false; let items = []; let activeIndex = -1; let debounceTimer = null; let seq = 0; function closeList() { open = false; activeIndex = -1; wrap.classList.remove("open"); Array.from(list.children).forEach((c) => c.removeAttribute("aria-selected")); } function openList() { if (!open) { open = true; wrap.classList.add("open"); } } function setItems(names) { list.innerHTML = ""; items = names.map((nm) => { const it = document.createElement("div"); it.className = "service-combobox-item"; it.setAttribute("role", "option"); it.textContent = nm; it.addEventListener("mousedown", (e) => { e.preventDefault(); input.value = nm; closeList(); input.dispatchEvent(new Event("change", { bubbles: true })); }); list.appendChild(it); return it; }); if (items.length > 0) { openList(); } else { closeList(); } } function moveActive(delta) { if (!open || items.length === 0) { return; } const n = items.length; activeIndex = (activeIndex + delta + n) % n; items.forEach((el, i) => { if (i === activeIndex) { el.setAttribute("aria-selected", "true"); el.scrollIntoView({ block: "nearest" }); } else { el.removeAttribute("aria-selected"); } }); } async function fetchNames(q, requestId) { try { const res = await qortalRequest({ action: "SEARCH_NAMES", query: q, limit: 10, offset: 0, reverse: false, prefix: true, }); if (requestId !== seq) { return; } // stale const names = Array.isArray(res) ? res.map((x) => x?.name).filter(Boolean) : []; setItems(names); } catch (_e) { if (requestId !== seq) { return; } setItems([]); } } function scheduleFetch() { const q = (input.value || "").trim(); if (!q) { setItems([]); return; } if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(() => { seq++; const id = seq; fetchNames(q, id); }, 200); } input.addEventListener("input", scheduleFetch); input.addEventListener("focus", scheduleFetch); input.addEventListener("keydown", (e) => { if (!open && (e.key === "ArrowDown" || e.key === "Enter")) { scheduleFetch(); e.preventDefault(); return; } if (e.key === "ArrowDown") { moveActive(1); e.preventDefault(); } else if (e.key === "ArrowUp") { moveActive(-1); e.preventDefault(); } else if (e.key === "Enter") { if (open && activeIndex >= 0 && activeIndex < items.length) { const el = items[activeIndex]; el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); e.preventDefault(); } } else if (e.key === "Escape") { closeList(); e.preventDefault(); } }); document.addEventListener("click", (e) => { if (!wrap.contains(e.target)) { closeList(); } }); } // ===== 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"; } try { updateSidebarBanner(); } catch {} } // ===== 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"); } // Initialize service comboboxes for static inputs (function initServiceCombos() { function bind() { const ss = /** @type {HTMLInputElement|null} */ (document.getElementById("search-service")); if (ss) { createServiceCombobox(ss); } const cs = /** @type {HTMLInputElement|null} */ (document.getElementById("compose-service")); if (cs) { createServiceCombobox(cs); } const sn = /** @type {HTMLInputElement|null} */ (document.getElementById("search-name")); if (sn) { createNameAutosuggest(sn); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bind, { once: true }); } else { bind(); } })(); // Global event delegation for Updated column toggle in tables document.addEventListener("click", (ev) => { const btn = ev.target && typeof ev.target.closest === "function" ? ev.target.closest(".updated-toggle-btn") : null; if (btn) { cycleTimestampDisplayMode(); } }); // Global event delegation for column sort toggles document.addEventListener("click", (ev) => { const b = ev.target && typeof ev.target.closest === "function" ? ev.target.closest(".table-sort") : null; if (!b) { return; } const key = b.getAttribute("data-sort") || ""; const ctx = b.getAttribute("data-context") || ""; if (!key || !ctx) { return; } applySortToggle(ctx, key); }); function setSearchMode(on) { searchModeActive = !!on; updateSidebarBanner(); updateSearchButtonUI(); showSection(on ? "search" : "content"); if (searchModeActive) { try { initSearchUI(); } catch {} } // Refresh sidebar to reflect current context (Search vs My Files) try { buildSidebarTree(); } catch {} // In search mode ensure filters apply to table if (searchModeActive) { try { applySearchFilter(); renderSearchResults(); } catch {} } } // ===== Sidebar tree ===== function buildSidebarTree() { const tree = document.getElementById("file-tree"); if (!tree) { return; } tree.innerHTML = ""; // Choose data source based on context /** @type {Array} */ const base = searchModeActive ? getSearchBaselineResults() : getBaselineResults(); // Empty/placeholder state if (!base || base.length === 0) { const empty = document.createElement("div"); empty.className = "tree-empty"; empty.textContent = searchModeActive ? "No search results yet." : "No files to show."; tree.appendChild(empty); // Update banner name for My Files context const nm = document.getElementById("sidebar-name"); if (nm && !searchModeActive) { nm.textContent = userName || "(not authenticated)"; } return; } // Helper: build a leaf line (with avatar in Search mode) function createLeafLine(r, parentEl) { const leaf = document.createElement("div"); leaf.className = "tree-node"; const lline = document.createElement("div"); lline.className = "tree-line"; lline.setAttribute("role", "treeitem"); if (searchModeActive) { const av = document.createElement("span"); av.className = "tree-avatar"; av.title = r.name || ""; attachAvatarInto(av, r.name || ""); lline.appendChild(av); } else { lline.appendChild(document.createElement("span")); } 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(llabel); parentEl.appendChild(lline); } // Helper: prefix computation for default (service-first) layout 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); } return id; } return pfx; } // If grouping by Name (Search mode), render name -> service -> prefixes/leaves if (searchModeActive && searchGroupByName) { const byName = {}; for (const r of base) { const nm = r.name || ""; (byName[nm] = byName[nm] || []).push(r); } const names = Object.keys(byName).sort(); for (const nmKey of names) { const itemsForName = byName[nmKey]; if (!itemsForName || itemsForName.length === 0) { continue; } const nameNode = document.createElement("div"); nameNode.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.innerHTML = ''; const children = document.createElement("div"); children.className = "tree-children"; children.setAttribute("role", "group"); toggle.addEventListener("click", (e) => { e.stopPropagation(); const open = children.classList.toggle("expanded"); line.setAttribute("aria-expanded", String(open)); // visual state handled via CSS rotation on [aria-expanded] }); const nAvatar = document.createElement("span"); nAvatar.className = "tree-avatar"; nAvatar.title = nmKey || ""; attachAvatarInto(nAvatar, nmKey || ""); const label = document.createElement("span"); label.className = "tree-label"; label.textContent = nmKey || "(no name)"; label.addEventListener("click", () => { searchCurrentNameFilter = nmKey; searchSelectedServices = new Set(); searchCurrentPrefixFilter = null; searchCurrentIdentifierFilter = null; applySearchFilter(); renderSearchResults(); showSection("search"); try { updateSidebarBanner(); } catch {} }); const count = document.createElement("span"); count.className = "tree-count"; count.textContent = String(itemsForName.length); line.appendChild(toggle); line.appendChild(nAvatar); line.appendChild(label); line.appendChild(count); nameNode.appendChild(line); nameNode.appendChild(children); const bySvc = {}; for (const r of itemsForName) { const svc = r.service || "UNKNOWN"; (bySvc[svc] = bySvc[svc] || []).push(r); } const services = Object.keys(bySvc).sort(); for (const svc of services) { const svcItems = bySvc[svc]; const svcNode = document.createElement("div"); svcNode.className = "tree-node"; const sline = document.createElement("div"); sline.className = "tree-line"; sline.setAttribute("role", "treeitem"); sline.setAttribute("aria-expanded", "false"); const stoggle = document.createElement("button"); stoggle.type = "button"; stoggle.className = "tree-toggle"; stoggle.innerHTML = ''; const schildren = document.createElement("div"); schildren.className = "tree-children"; schildren.setAttribute("role", "group"); stoggle.addEventListener("click", (e) => { e.stopPropagation(); const open = schildren.classList.toggle("expanded"); sline.setAttribute("aria-expanded", String(open)); // visual state handled via CSS rotation on [aria-expanded] }); const slabel = document.createElement("span"); slabel.className = "tree-label"; slabel.textContent = svc; slabel.addEventListener("click", () => { searchCurrentNameFilter = nmKey; searchSelectedServices = new Set([svc]); searchCurrentPrefixFilter = null; searchCurrentIdentifierFilter = null; applySearchFilter(); renderSearchResults(); showSection("search"); try { updateSidebarBanner(); } catch {} }); const scount = document.createElement("span"); scount.className = "tree-count"; scount.textContent = String(svcItems.length); sline.appendChild(stoggle); sline.appendChild(slabel); sline.appendChild(scount); svcNode.appendChild(sline); svcNode.appendChild(schildren); children.appendChild(svcNode); // Prefix grouping under this name+service const byPrefix = {}; for (const r of svcItems) { 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 === svcItems.length; if (singlePrefixCoversAll) { svcItems.slice(0, 2000).forEach((r) => createLeafLine(r, schildren)); } else { 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) => createLeafLine(r, schildren)); 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.innerHTML = ''; const pchildren = document.createElement("div"); pchildren.className = "tree-children"; pchildren.setAttribute("role", "group"); ptoggle.addEventListener("click", (e) => { e.stopPropagation(); const open = pchildren.classList.toggle("expanded"); pline.setAttribute("aria-expanded", String(open)); // visual state handled via CSS rotation on [aria-expanded] }); const plabel = document.createElement("span"); plabel.className = "tree-label"; plabel.textContent = pfx; plabel.addEventListener("click", () => { searchCurrentNameFilter = nmKey; searchSelectedServices = new Set([svc]); searchCurrentPrefixFilter = pfx; searchCurrentIdentifierFilter = null; applySearchFilter(); renderSearchResults(); showSection("search"); try { updateSidebarBanner(); } catch {} }); const pcount = document.createElement("span"); pcount.className = "tree-count"; pcount.textContent = String(arr.length); pline.appendChild(ptoggle); pline.appendChild(plabel); pline.appendChild(pcount); pNode.appendChild(pline); pNode.appendChild(pchildren); schildren.appendChild(pNode); arr.slice(0, 2000).forEach((r) => createLeafLine(r, pchildren)); } } } tree.appendChild(nameNode); } // Update banner name text (auth context still shown) const nmEl = document.getElementById("sidebar-name"); if (nmEl) { nmEl.textContent = userName || "(not authenticated)"; } return; } // 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.innerHTML = ''; toggle.addEventListener("click", (e) => { e.stopPropagation(); const open = children.classList.toggle("expanded"); line.setAttribute("aria-expanded", String(open)); // visual state handled via CSS rotation on [aria-expanded] }); const label = document.createElement("span"); label.className = "tree-label"; label.textContent = svc; label.addEventListener("click", async () => { if (searchModeActive) { // Search mode: filter the search table by service searchSelectedServices = new Set([svc]); searchCurrentNameFilter = null; searchCurrentPrefixFilter = null; searchCurrentIdentifierFilter = null; applySearchFilter(); renderSearchResults(); showSection("search"); try { updateSidebarBanner(); } catch {} return; } // My Files: 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 = {}; 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) => { createLeafLine(r, children); }); } 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) => { createLeafLine(r, children); }); // 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.innerHTML = ''; ptoggle.addEventListener("click", (e) => { e.stopPropagation(); const open = pchildren.classList.toggle("expanded"); pline.setAttribute("aria-expanded", String(open)); // visual state handled via CSS rotation on [aria-expanded] }); const plabel = document.createElement("span"); plabel.className = "tree-label"; plabel.textContent = pfx; plabel.addEventListener("click", async () => { if (searchModeActive) { // Search mode: filter to service + prefix searchSelectedServices = new Set([svc]); searchCurrentPrefixFilter = pfx; searchCurrentIdentifierFilter = null; searchCurrentNameFilter = null; applySearchFilter(); renderSearchResults(); showSection("search"); try { updateSidebarBanner(); } catch {} return; } // My Files: 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) => { createLeafLine(r, pchildren); }); 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 { if (searchModeActive) { // In Search mode, rebuild sidebar and refresh filtered results applySearchFilter(); renderSearchResults(); buildSidebarTree(); } else { // My Files: rebuild chips, sidebar, filters, and current page 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"; const nm = ctx.name || ""; // Build with inline avatar placeholder; will upgrade after fetch titleEl.innerHTML = ``; const svcSpan = document.createElement("span"); svcSpan.textContent = `${ctx.service} - ${ident} - `; const nameWrap = document.createElement("span"); nameWrap.className = "name-with-avatar"; nameWrap.setAttribute("data-name", nm); const av = document.createElement("span"); av.className = "avatar-img"; av.style.display = "inline-block"; av.style.borderRadius = "50%"; av.style.width = "24px"; av.style.height = "24px"; av.style.background = "#1f2c49"; av.style.color = "#c9d2d9"; av.style.textAlign = "center"; av.style.lineHeight = "24px"; av.style.fontSize = "14px"; av.textContent = initialForName(nm); const nmSpan = document.createElement("span"); nmSpan.className = "name-text"; nmSpan.textContent = nm; nameWrap.appendChild(av); nameWrap.appendChild(nmSpan); titleEl.appendChild(svcSpan); titleEl.appendChild(nameWrap); // Attempt to upgrade avatar to image getAvatarForName(nm) .then((res) => { if (!res || !res.url) { return; } const img = document.createElement("img"); img.className = "avatar-img"; img.src = res.url; img.alt = nm; av.replaceWith(img); }) .catch(() => {}); } // 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 sortedAll = filteredResults.slice().sort(compareResultsMyFiles); const pageItems = sortedAll.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 arrive pre-sorted by fetchPage across the full dataset. let tableHtml = ''; tableHtml += ``; metadataArray = []; for (const result of results) { const identifier = result.identifier === undefined ? "default" : result.identifier; const updatedAgo = formatTimestampDisplay(result.updated || result.created || 0); 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 += ``; 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) { const shouldPromote = playing.paused === false || ((playing.currentTime || 0) > 0 && playing.ended === false); if (shouldPromote) { 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"; } function formatTimeAgo(ts) { const t = Number(ts); if (!Number.isFinite(t) || t <= 0) { return "Unknown"; } const now = Date.now(); let diff = Math.max(0, now - t); const sec = diff / 1000; if (sec < 60) { const s = Math.floor(sec); return `${s} sec ago`; } const min = sec / 60; if (min < 60) { const m = Math.floor(min); return `${m} min ago`; } const hr = min / 60; if (hr < 24) { return `${hr.toFixed(1)} hours ago`; } const days = hr / 24; if (days < 365) { return `${days.toFixed(1)} days ago`; } const years = days / 365; return `${years.toFixed(1)} years ago`; } // ===== 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) ); } // Validate a plausible public key string function isValidPublicKey(pk) { if (!pk || typeof pk !== "string") { return false; } const s = pk.trim(); if (!s || s.toLowerCase().includes("unavailable")) { return false; } // Qortal public keys are Base58; length typically ~44-48 chars return s.length >= 30; } // Ensure publish params include encryption to the user's public key when service is private async function ensureEncryptionForPublishParams(publishParams) { try { if (!publishParams || typeof publishParams !== "object") { return publishParams; } const action = String(publishParams.action || ""); // Helper to attach enc fields const attachEnc = async (svc, target) => { if (!isPrivateService(svc)) { return; } if (!isValidPublicKey(_userPublicKey)) { try { const account = await qortalRequest({ action: "GET_USER_ACCOUNT" }); if (account && account.publicKey) { _userPublicKey = account.publicKey; } } catch (e) { // ignore; will fail validation below } } if (!isValidPublicKey(_userPublicKey)) { throw new Error("Unable to determine your public key for private publish"); } target.encrypt = true; target.publicKeys = [_userPublicKey]; }; if (action === "PUBLISH_QDN_RESOURCE") { await attachEnc(publishParams.service, publishParams); } else if ( action === "PUBLISH_MULTIPLE_QDN_RESOURCES" && Array.isArray(publishParams.resources) ) { for (const r of publishParams.resources) { if (r && typeof r === "object") { await attachEnc(r.service, r); } } } } catch (e) { // Surface error to caller throw e; } return publishParams; } 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"; } // ===== Avatars ===== const AVATAR_SERVICE = "THUMBNAIL"; const AVATAR_IDENTIFIER = "qortal_avatar"; const _avatarCache = new Map(); // name -> Promise<{ url: string } | null> function initialForName(nm) { const s = nm || ""; return s.length > 0 ? s[0] : "?"; } async function fetchAvatarUrl(name) { try { const b64 = await qortalRequest( buildQdnParams({ action: "FETCH_QDN_RESOURCE", name, service: AVATAR_SERVICE, identifier: AVATAR_IDENTIFIER, encoding: "base64", rebuild: false, }) ); const bytes = b64ToBytes(b64); const mime = detectMimeFromBytes(bytes) || "image/png"; const blob = new Blob([bytes], { type: mime }); const url = URL.createObjectURL(blob); return { url }; } catch (_e) { return null; } } function getAvatarForName(name) { if (!name) { return Promise.resolve(null); } if (_avatarCache.has(name)) { return _avatarCache.get(name); } const p = fetchAvatarUrl(name); _avatarCache.set(name, p); return p; } function attachAvatarInto(container, name) { if (!container) { return; } // Start with initial container.textContent = initialForName(name); getAvatarForName(name) .then((res) => { if (!res || !res.url) { return; } // Replace text with image try { container.textContent = ""; const img = document.createElement("img"); img.className = container.classList.contains("tree-avatar") ? "tree-avatar-img" : "avatar-img"; img.src = res.url; img.alt = name || ""; container.appendChild(img); } catch {} }) .catch(() => {}); } 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 = "100%"; 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 { // Enforce encryption automatically for private services await ensureEncryptionForPublishParams(publishParams); // Message hint if private service const svc = publishParams.service || (publishParams.resources && publishParams.resources[0]?.service) || ""; const isPriv = isPrivateService(svc); const note = isPriv ? "Private service detected — encrypting to your account only. " : ""; // Show modal with hint (if any) showPublishModal(`${note}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"]'); const noteDiv = form.querySelector("#publish-private-note"); if (svcInput) { svcInput.value = defaults?.service || ""; // Enhance with combobox behavior try { createServiceCombobox(/** @type {HTMLInputElement} */ (svcInput)); } catch {} const updateNote = () => { const s = String(svcInput.value || "") .trim() .toUpperCase(); if (noteDiv) { noteDiv.style.display = isPrivateService(s) ? "block" : "none"; } }; svcInput.addEventListener("input", updateNote); updateNote(); } 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; }); // Auto-attach encryption for private services const multiParams = { action: "PUBLISH_MULTIPLE_QDN_RESOURCES", resources }; await ensureEncryptionForPublishParams(multiParams); // Add brief note for private if (isPrivateService(details.service)) { showPublishModal( `Private service detected - encrypting to your account only. Publishing ${files.length} files...` ); } const response = await qortalRequest(multiParams); 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"); const privNote = document.getElementById("compose-private-note"); if (svcEl) { /** @type {HTMLInputElement} */ (svcEl).value = (service || "DOCUMENT").toString(); // Toggle private service note const updateNote = () => { const s = String(/** @type {HTMLInputElement} */ (svcEl).value || "") .trim() .toUpperCase(); if (privNote) { privNote.style.display = isPrivateService(s) ? "block" : "none"; } }; try { // hook up service combobox to reuse suggestions if available createServiceCombobox(/** @type {HTMLInputElement} */ (svcEl)); } catch {} svcEl.addEventListener("input", updateNote); updateNote(); } 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 { buildSidebarTree(); 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"); const banner = document.getElementById("sidebar-banner"); let sortBtn = document.getElementById("sidebar-sort-by-name"); if (searchModeActive) { if (ctxBtn) { ctxBtn.disabled = false; ctxBtn.textContent = "Search Results"; } if (exitBtn) { exitBtn.style.display = "inline-block"; } // Highlight "Search Results" when full, unfiltered results are shown on Search page try { if (ctxBtn) { const searchVisible = Sections.search && Sections.search.style.display !== "none"; const filtersCleared = (!searchSelectedServices || searchSelectedServices.size === 0) && !searchCurrentPrefixFilter && !searchCurrentIdentifierFilter && !searchCurrentNameFilter; const active = !!(searchVisible && filtersCleared); ctxBtn.setAttribute("data-active", String(active)); } } catch {} // Ensure sort-by-name toggle is present if (banner) { if (!sortBtn) { sortBtn = document.createElement("button"); sortBtn.id = "sidebar-sort-by-name"; sortBtn.className = "icon-button"; sortBtn.style.marginLeft = "6px"; banner.appendChild(sortBtn); sortBtn.addEventListener("click", () => { searchGroupByName = !searchGroupByName; try { window.localStorage.setItem("qedit:searchGroupByName", String(!!searchGroupByName)); } catch {} updateSidebarBanner(); buildSidebarTree(); }); } sortBtn.style.display = "inline-block"; sortBtn.setAttribute("aria-pressed", String(!!searchGroupByName)); sortBtn.textContent = searchGroupByName ? "Group by Name: On" : "Group by Name: Off"; } } else { if (ctxBtn) { ctxBtn.disabled = true; ctxBtn.innerHTML = 'My Files - ' + (userName || "(not authenticated)") + ""; } if (exitBtn) { exitBtn.style.display = "none"; } if (sortBtn) { sortBtn.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) { // Reset search-mode filters and show full results clearSearchTreeFilters(); applySearchFilter(); renderSearchResults(); showSection("search"); } }); } const sidebarExitBtn = document.getElementById("sidebar-exit-search"); if (sidebarExitBtn) { sidebarExitBtn.addEventListener("click", async () => { setSearchMode(false); showSpinner(); try { // Rebuild My Files sidebar immediately buildSidebarTree(); 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", () => { // Preserve current limit selection across reset const limitEl = /** @type {HTMLSelectElement|null} */ ( document.getElementById("search-limit") ); const currentLimitVal = limitEl ? limitEl.value : null; form.reset(); if (limitEl && currentLimitVal !== null) { limitEl.value = currentLimitVal; } searchState = { params: null, results: [], offset: 0, limit: Number(currentLimitVal) || searchState.limit || 100, hasMore: false, inFlight: false, }; // Clear search tree filters clearSearchTreeFilters(); resultsHost.innerHTML = ""; summary.textContent = ""; if (moreWrap) { moreWrap.style.display = "none"; } try { localStorage.removeItem(LS_LAST_SEARCH_KEY); } catch {} try { buildSidebarTree(); } 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 = ""; // Clear any prior tree filters when starting a new search clearSearchTreeFilters(); } 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)); } // Mark deletion flags to respect Hide Deleted in the tree try { await markDeletedEntries(items); } catch (e) { console.warn("Mark deleted for search items failed:", e); } 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 {} applySearchFilter(); renderSearchResults(); // Update sidebar to reflect new results try { buildSidebarTree(); } catch {} 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; } let items = searchFilteredResults && Array.isArray(searchFilteredResults) ? searchFilteredResults : searchState.results || []; // Sort for display according to search sort state const itemsToRender = items.slice().sort(compareResultsSearch); 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)" : ""}`; // Promote any playing media from the current results host before re-rendering const existingAv = resultsHost.querySelector("video, audio"); if (existingAv) { const shouldPromote = existingAv.paused === false || ((existingAv.currentTime || 0) > 0 && existingAv.ended === false); if (shouldPromote) { promoteMedia(existingAv, { service: existingAv.getAttribute("data-service") || "", identifier: existingAv.getAttribute("data-identifier") || "default", name: existingAv.getAttribute("data-name") || userName, }); } } // Build table with inline previews let html = '' + `` + `` + `` + `` + `` + `` + `` + ""; metadataArray = []; for (const r of itemsToRender) { const name = r.name || ""; const svc = r.service || ""; const ident = r.identifier === undefined || r.identifier === null || r.identifier === "" ? "default" : r.identifier; const size = formatSize(r.size || 0); const updTs = r.updated || r.created || 0; const updated = formatTimestampDisplay(updTs); let metadataKeys = ""; let metadataIndex = -1; if (r.metadata) { metadataIndex = metadataArray.length; metadataArray.push(r.metadata); metadataKeys = Object.keys(r.metadata).join(", "); } const embedIcon = isEmbedService(svc) ? `\n \n \n ` : ""; const previewCell = generatePreviewHTML(r, name, ident); const row = ` `; html += row; } html += ""; resultsHost.innerHTML = html; // Initialize inline preview holders within search results only initPreviews(resultsHost); try { updateSidebarBanner(); } catch {} // Upgrade avatars in the Name column resultsHost.querySelectorAll(".name-with-avatar").forEach((wrap) => { const nm = wrap.getAttribute("data-name") || ""; // Replace the placeholder span with a real avatar image if available const place = wrap.querySelector(".avatar-img"); if (place) { getAvatarForName(nm) .then((res) => { if (res && res.url) { const img = document.createElement("img"); img.className = "avatar-img"; img.src = res.url; img.alt = nm; place.replaceWith(img); } else { // Keep initial already rendered } }) .catch(() => {}); } }); // Wire up identifier click to open preview resultsHost.querySelectorAll(".identifier-text").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 }); }); }); // Wire up embed copy buttons resultsHost.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); } 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)); } }); }); // Wire up metadata dialog resultsHost.querySelectorAll(".clickable-metadata").forEach((el) => { el.addEventListener("click", function () { const idx = parseInt(this.getAttribute("data-metadata-index"), 10); if (!isNaN(idx) && idx >= 0 && idx < metadataArray.length) { openMetadataDialog(metadataArray[idx]); } }); }); } 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; }); }