// ===== Globals ===== // ===== 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 = ''; let userAddress = ''; let userName = ''; let isAuthenticated = false; let allNames = []; let authStatus = 'idle'; let namesStatus = 'idle'; let allResults = []; let metadataArray = []; // Pagination let currentPage = 1; let itemsPerPage = 25; let totalResults = 0; // 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'; // 'ALL' means no service filter 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 } // === Deleted-content filter (default ON) === const TINY_SIZE_THRESHOLD = 128; // bytes; QDN may report ~32B for 1B newline let hideDeleted = (localStorage.getItem('hideDeleted') !== 'false'); // default true function getBaselineResults() { // When hiding deletions, exclude items flagged as deleted if (!hideDeleted) return masterResults; return masterResults.filter(r => !r.__isDeleted); } function recomputeServiceCounts() { serviceCounts = {}; const base = getBaselineResults(); for (const r of base) { const svc = r.service || 'UNKNOWN'; serviceCounts[svc] = (serviceCounts[svc] || 0) + 1; } } // ===== DOM & UI helpers ===== const contentPage = document.getElementById('content-page'); const authButton = document.getElementById('auth-button'); const nameSwitcherEl = document.getElementById('name-switcher'); const nameSelectEl = document.getElementById('name-select'); const loadingOverlay = document.getElementById('loading-overlay'); function showSpinner(){ if (loadingOverlay) loadingOverlay.style.display = 'flex'; } function hideSpinner(){ if (loadingOverlay) loadingOverlay.style.display = 'none'; } 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'; } // 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 { contentPage.style.display = "block"; 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(); initHideDeletedUI(); function initHideDeletedUI() { const filterDiv = document.getElementById('filter-options'); if (!filterDiv) return; // Avoid duplicate if (document.getElementById('hide-deleted-checkbox')) return; const label = document.createElement('label'); label.style.marginRight = '10px'; label.style.cursor = 'pointer'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.id = 'hide-deleted-checkbox'; cb.checked = hideDeleted; cb.style.marginRight = '6px'; label.appendChild(cb); label.appendChild(document.createTextNode('Hide Deleted Content')); // Prefer placing it before the service chips filterDiv.insertBefore(label, filterDiv.firstChild); cb.addEventListener('change', async () => { hideDeleted = cb.checked; localStorage.setItem('hideDeleted', String(hideDeleted)); showSpinner(); contentPage.style.display = "none"; try { recomputeServiceCounts(); buildServiceChips(); applyServiceFilter(); await fetchPage(); } finally { contentPage.style.display = "block"; hideSpinner(); } }); } // ===== QDN Preview helpers ===== 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 { 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(); 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(); initHideDeletedUI(); currentServiceFilter = 'ALL'; await loadAllResults(); await fetchPage(); contentPage.style.display = "block"; authStatus = 'succeeded'; updateAuthUI(); initHideDeletedUI(); hideSpinner(); } catch (error) { console.error('Error fetching account details:', error); authStatus = 'failed'; updateAuthUI(); 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(); initHideDeletedUI(); try { await loadAllResults(); await fetchPage(); } finally { contentPage.style.display = "block"; 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; try { await waitForResourceReady({ name, service, identifier, initialBuild: true, timeoutMs: 20000, intervalMs: 500 }); } catch (e) { /* ignore */ } 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(); 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(); if (selectedServices.size === 0) { filteredResults = base.slice(); } else { filteredResults = base.filter(r => selectedServices.has(r.service)); } totalResults = filteredResults.length; totalSize = filteredResults.reduce((acc, r) => acc + (r.size || 0), 0); } // ===== Data fetchers ===== async function fetchPage() { try { if (!userName || userName === 'Name unavailable') return; // Client-side pagination using filteredResults const contentDetails = document.getElementById('content-details'); contentDetails.innerHTML = '

Loading...

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

Error: ${error.message}

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

No results found.

'; contentSummaryDiv.innerHTML = ''; document.getElementById('pagination-top').innerHTML = ''; document.getElementById('pagination-bottom').innerHTML = ''; return; } results.sort((a, b) => (b.updated || b.created) - (a.updated || a.created)); let tableHtml = ''; tableHtml += ``; metadataArray = []; for (const result of results) { const identifier = (result.identifier === undefined) ? 'default' : result.identifier; const isImageLike = (getBaseServiceKind(result.service) === 'image'); let createdString = new Date(result.created).toLocaleString(); if (isNaN(new Date(result.created))) createdString = 'Unknown'; let updatedString = new Date(result.updated).toLocaleString(); if (isNaN(new Date(result.updated))) updatedString = 'Never'; const sizeString = formatSize(result.size || 0); let metadataKeys = ''; let metadataIndex = -1; if (result.metadata) { metadataIndex = metadataArray.length; metadataArray.push(result.metadata); metadataKeys = Object.keys(result.metadata).join(', '); } tableHtml += ``; } tableHtml += `
Service Identifier ${ bulkDeleteMode ? '' : '' } Metadata Preview Size Created / Updated
${result.service} ${buildIdentifierCellHTML(result, identifier)} ${generatePreviewHTML(result, userName, identifier)} ${sizeString} ${createdString}
${updatedString}
`; const startItem = (currentPage - 1) * itemsPerPage + 1; const endItem = Math.min(startItem + itemsPerPage - 1, totalResults); const selectedCount = getSelectedCount(); contentSummaryDiv.innerHTML = `
${startItem}-${endItem} of ${totalResults} resultsTotal Size: ${formatSize(totalSize)}${ selectedCount > 0 ? '' : '' }
`; const paginationHTML = buildPaginationControls(); document.getElementById('pagination-top').innerHTML = paginationHTML; document.getElementById('pagination-bottom').innerHTML = paginationHTML; 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 { contentPage.style.display = "block"; 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 { contentPage.style.display = "block"; hideSpinner(); } } } }); }); 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(); } function buildPaginationControls() { 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 { contentPage.style.display = "block"; 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 { contentPage.style.display = "block"; 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 { contentPage.style.display = "block"; hideSpinner(); } } } }); }); } function formatSize(size) { if (size > (1024*1024*1024*1024)) return (size / (1024*1024*1024*1024)).toFixed(2) + ' TB'; if (size > (1024*1024*1024)) return (size / (1024*1024*1024)).toFixed(2) + ' GB'; if (size > (1024*1024)) return (size / (1024*1024)).toFixed(2) + ' MB'; if (size > 1024) return (size / 1024).toFixed(2) + ' KB'; return (size || 0) + ' B'; } // ===== Preview rendering ===== function generatePreviewHTML(result, userName, identifier) { const safeName = (result.name || userName || '').replace(/"/g, '"'); const safeService = (result.service || '').replace(/"/g, '"'); const safeIdent = (identifier || 'default').replace(/"/g, '"'); return `
0% Loaded
`; } function initPreviews() { const holders = document.querySelectorAll('.preview-holder'); holders.forEach((el) => { const svc = (el.getAttribute('data-service') || '').toUpperCase(); const ident = el.getAttribute('data-identifier') || 'default'; const nm = el.getAttribute('data-name') || userName; loadPreviewInto(el, { service: svc, identifier: ident, name: nm }); }); } function isPrivateService(service) { const s = (service || '').toUpperCase(); return s.endsWith('_PRIVATE') || ['QCHAT_ATTACHMENT_PRIVATE','ATTACHMENT_PRIVATE','FILE_PRIVATE','IMAGE_PRIVATE','VIDEO_PRIVATE','AUDIO_PRIVATE','VOICE_PRIVATE','DOCUMENT_PRIVATE','MAIL_PRIVATE','MESSAGE_PRIVATE'].includes(s); } function isExcludedFromDeletionCheck(service){ const s = (service||'').toUpperCase(); return s === 'CHAIN_DATA' || s === 'CHAIN_COMMENT'; } function isEmbedService(service){ const s = (service||'').toUpperCase(); return s === 'IMAGE' || s === 'QCHAT_IMAGE' || s.includes('THUMBNAIL'); } function getBaseServiceKind(service) { const s = (service || '').toUpperCase(); if (s.includes('IMAGE') || s.includes('THUMBNAIL')) return 'image'; if (s.includes('VIDEO')) return 'video'; if (s.includes('AUDIO') || s.includes('VOICE') || s.includes('PODCAST')) return 'audio'; if (s.includes('DOCUMENT') || s.includes('BLOG') || s.includes('COMMENT') || s.includes('JSON') || s.includes('CODE')) return 'text'; if (s.includes('FILE') || s.includes('ATTACHMENT')) return 'file'; return 'file'; } async function waitForResourceReady({ name, service, identifier, initialBuild = true, timeoutMs = 60000, intervalMs = 800, onProgress }) { const start = Date.now(); if (initialBuild) { try { await qortalRequest(buildQdnParams({ action: 'GET_QDN_RESOURCE_STATUS', name, service, identifier, build: true })); } catch (e) { /* ignore */ } } 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 set = (el) => { container.innerHTML=''; container.appendChild(el); }; const looksHtml = (txt) => /<(?:!doctype|html|head|body|div|p|span|img|video|audio|iframe|section|article)/i.test(txt); try { // Wait until built; update percent text while polling await waitForResourceReady({ name: ctx.name, service: ctx.service, identifier: ctx.identifier, initialBuild: true, onProgress: (pct) => { container.textContent = `${pct}% Loaded`; } }); const service = ctx.service; const identifier = ctx.identifier; const name = ctx.name; const lower = service.toLowerCase(); const baseKind = getBaseServiceKind(service); const isPriv = isPrivateService(service); // Text-like services (public and private) const textServices = ['blog','blog_post','blog_comment','document','game','json','code']; const isText = textServices.some(t => lower.includes(t)) || baseKind === 'text'; if (isPriv) { // Private path: always fetch base64, then decrypt via DECRYPT_DATA const encB64 = await qortalRequest(buildQdnParams({ action:'FETCH_QDN_RESOURCE', name, service, identifier, encoding:'base64', rebuild:false })); const decB64 = await qortalRequest({ action:'DECRYPT_DATA', encryptedData: encB64 }); if (isText) { // Decode to UTF-8 string const bin = atob(decB64); const bytes = new Uint8Array(bin.length); for (let i=0;i openImageOverlayFromDataUrl(url)); set(img); return; } if (baseKind === 'video') { const video = document.createElement('video'); video.controls = true; video.className = 'preview-video'; video.src = url; set(video); return; } if (baseKind === 'audio') { const audio = document.createElement('audio'); audio.controls = true; audio.className = 'preview-audio'; audio.src = url; set(audio); return; } const a = document.createElement('a'); a.textContent = 'Open'; a.href = url; a.target = '_blank'; a.rel = 'noopener'; set(a); return; } } else { // Public path (existing behavior) if (isText) { let resp; try { resp = await qortalRequest(buildQdnParams({ action:'FETCH_QDN_RESOURCE', name, service, identifier, rebuild: false })); } catch (e) { const b64 = await qortalRequest(buildQdnParams({ action:'FETCH_QDN_RESOURCE', name, service, identifier, encoding:'base64', rebuild:false })); const bin = atob(b64); const bytes = new Uint8Array(bin.length); for (let i=0;i Blob URL const b64 = await qortalRequest(buildQdnParams({ action:'FETCH_QDN_RESOURCE', name, service, identifier, encoding:'base64', rebuild:false })); const bytes = b64ToBytes(b64); const mime = detectMimeFromBytes(bytes) || (baseKind==='image' ? 'image/*' : baseKind==='video' ? 'video/mp4' : baseKind==='audio' ? 'audio/mpeg' : 'application/octet-stream'); const blob = new Blob([bytes], { type: mime }); const url = URL.createObjectURL(blob); if (baseKind === 'image') { const img = document.createElement('img'); img.src = url; img.alt = identifier; img.className = 'preview-image'; img.addEventListener('click', () => openImageOverlayFromDataUrl(url)); set(img); return; } if (baseKind === 'video') { const video = document.createElement('video'); video.controls = true; video.className = 'preview-video'; video.src = url; set(video); return; } if (baseKind === 'audio') { const audio = document.createElement('audio'); audio.controls = true; audio.className = 'preview-audio'; audio.src = url; set(audio); return; } const a = document.createElement('a'); a.textContent = 'Open'; a.href = url; a.target = '_blank'; a.rel = 'noopener'; set(a); return; } } catch (err) { console.error('Preview load error for', ctx, err); container.textContent = 'Preview unavailable'; } } // ===== 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 { const response = 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; } 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 editIdent = (identifier === 'default') ? '' : identifier; // Prepare the publish parameters 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 textServices = ['BLOG', 'BLOG_POST', 'BLOG_COMMENT', 'DOCUMENT']; if (textServices.includes(service)) { // For text types, fetch the current content let contentUrl = `/arbitrary/${service}/${userName}/${identifier}`; let content = ''; try { const contentResponse = await fetch(contentUrl); if (contentResponse.ok) { content = await contentResponse.text(); } else { content = 'Error fetching content'; } } catch (err) { content = 'Error fetching content'; } // Open a modal dialog to edit the content let editedContent = await openTextEditorDialog(content); if (editedContent === null) { // User cancelled return; } // Create a new Blob with the edited content const editedFile = new Blob([editedContent], { type: 'text/plain' }); publishParams.file = editedFile; // Add the edited file to publishParams } else { closePublishModal(); // 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 { const response = 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); } } function openTextEditorDialog(content) { return new Promise((resolve, reject) => { // 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, reject) => { // Create the modal overlay const modalOverlay = document.createElement('div'); modalOverlay.style.position = 'fixed'; modalOverlay.style.top = '0'; modalOverlay.style.left = '0'; modalOverlay.style.width = '100%'; modalOverlay.style.height = '100%'; modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; modalOverlay.style.display = 'flex'; modalOverlay.style.justifyContent = 'center'; modalOverlay.style.alignItems = 'center'; modalOverlay.style.zIndex = '1000'; // Create the modal content container const modalContent = document.createElement('div'); modalContent.style.backgroundColor = '#2d3749'; modalContent.style.color = '#c9d2d9'; modalContent.style.padding = '20px'; modalContent.style.borderRadius = '25px'; modalContent.style.maxWidth = '600px'; modalContent.style.width = '90%'; modalContent.style.fontFamily = "'Lexend', sans-serif"; modalContent.style.lineHeight = '1.6'; // Create the form const form = document.createElement('form'); const fields = ['filename', 'title', 'description', 'category', 'tags']; // Define the categories const categories = [ { value: '', display: '' }, { value: 'ART', display: 'Art and Design' }, { value: 'AUTOMOTIVE', display: 'Automotive' }, { value: 'BEAUTY', display: 'Beauty' }, { value: 'BOOKS', display: 'Books and Reference' }, { value: 'BUSINESS', display: 'Business' }, { value: 'COMMUNICATIONS', display: 'Communications' }, { value: 'CRYPTOCURRENCY', display: 'Cryptocurrency and Blockchain' }, { value: 'CULTURE', display: 'Culture' }, { value: 'DATING', display: 'Dating' }, { value: 'DESIGN', display: 'Design' }, { value: 'ENTERTAINMENT', display: 'Entertainment' }, { value: 'EVENTS', display: 'Events' }, { value: 'FAITH', display: 'Faith and Religion' }, { value: 'FASHION', display: 'Fashion' }, { value: 'FINANCE', display: 'Finance' }, { value: 'FOOD', display: 'Food and Drink' }, { value: 'GAMING', display: 'Gaming' }, { value: 'GEOGRAPHY', display: 'Geography' }, { value: 'HEALTH', display: 'Health' }, { value: 'HISTORY', display: 'History' }, { value: 'HOME', display: 'Home' }, { value: 'KNOWLEDGE', display: 'Knowledge Share' }, { value: 'LANGUAGE', display: 'Language' }, { value: 'LIFESTYLE', display: 'Lifestyle' }, { value: 'MANUFACTURING', display: 'Manufacturing' }, { value: 'MAPS', display: 'Maps and Navigation' }, { value: 'MUSIC', display: 'Music' }, { value: 'NEWS', display: 'News' }, { value: 'OTHER', display: 'Other' }, { value: 'PETS', display: 'Pets' }, { value: 'PHILOSOPHY', display: 'Philosophy' }, { value: 'PHOTOGRAPHY', display: 'Photography' }, { value: 'POLITICS', display: 'Politics' }, { value: 'PRODUCE', display: 'Products and Services' }, { value: 'PRODUCTIVITY', display: 'Productivity' }, { value: 'PSYCHOLOGY', display: 'Psychology' }, { value: 'QORTAL', display: 'Qortal' }, { value: 'SCIENCE', display: 'Science' }, { value: 'SELF_CARE', display: 'Self Care' }, { value: 'SELF_SUFFICIENCY', display: 'Self-Sufficiency and Homesteading' }, { value: 'SHOPPING', display: 'Shopping' }, { value: 'SOCIAL', display: 'Social' }, { value: 'SOFTWARE', display: 'Software' }, { value: 'SPIRITUALITY', display: 'Spirituality' }, { value: 'SPORTS', display: 'Sports' }, { value: 'STORYTELLING', display: 'Storytelling' }, { value: 'TECHNOLOGY', display: 'Technology' }, { value: 'TOOLS', display: 'Tools' }, { value: 'TRAVEL', display: 'Travel' }, { value: 'UNCATEGORIZED', display: 'Uncategorized' }, { value: 'VIDEO', display: 'Video' }, { value: 'WEATHER', display: 'Weather' }, ]; fields.forEach(field => { const label = document.createElement('label'); label.textContent = field.charAt(0).toUpperCase() + field.slice(1) + ':'; label.style.display = 'block'; label.style.marginTop = '10px'; let input; if (field === 'category') { // Create a select element for category input = document.createElement('select'); input.name = field; input.style.width = '100%'; input.style.padding = '5px'; input.style.marginTop = '5px'; // Add options to the select element categories.forEach(category => { const option = document.createElement('option'); option.value = category.value; option.textContent = category.display; input.appendChild(option); }); // Set the selected value if it exists in existingMetadata if (existingMetadata[field]) { input.value = existingMetadata[field]; } else { input.value = ''; // Default to blank line } } else { // Create an input element for other fields input = document.createElement('input'); input.type = 'text'; input.name = field; input.style.width = '100%'; input.style.padding = '5px'; input.style.marginTop = '5px'; if (existingMetadata[field]) { if (field === 'tags' && Array.isArray(existingMetadata[field])) { input.value = existingMetadata[field].join(', '); } else { input.value = existingMetadata[field]; } } else { input.placeholder = field.charAt(0).toUpperCase() + field.slice(1); } } label.appendChild(input); form.appendChild(label); }); // Create the button container const buttonContainer = document.createElement('div'); buttonContainer.style.textAlign = 'right'; buttonContainer.style.marginTop = '20px'; // Create the Save and Cancel buttons const saveButton = document.createElement('button'); saveButton.textContent = 'Save'; saveButton.type = 'submit'; saveButton.style.marginLeft = '10px'; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.type = 'button'; buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(saveButton); form.appendChild(buttonContainer); modalContent.appendChild(form); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); // Event listeners cancelButton.addEventListener('click', (event) => { event.preventDefault(); document.body.removeChild(modalOverlay); closePublishModal(); resolve(null); }); form.addEventListener('submit', (event) => { event.preventDefault(); // Collect the metadata const formData = new FormData(form); const updatedMetadata = {}; fields.forEach(field => { const value = formData.get(field); if (value) { updatedMetadata[field] = value; } }); document.body.removeChild(modalOverlay); resolve(updatedMetadata); }); }); } let publishModal = null; function showPublishModal(message) { if (!publishModal) { // Create the modal publishModal = document.createElement('div'); publishModal.style.position = 'fixed'; publishModal.style.top = '0'; publishModal.style.left = '0'; publishModal.style.width = '100%'; publishModal.style.height = '100%'; publishModal.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; publishModal.style.display = 'flex'; publishModal.style.justifyContent = 'center'; publishModal.style.alignItems = 'center'; publishModal.style.zIndex = '1000'; // Create the modal content container const modalContent = document.createElement('div'); modalContent.style.backgroundColor = '#2d3749'; // Use background color from main content modalContent.style.padding = '20px'; modalContent.style.borderRadius = '5px'; modalContent.style.maxWidth = '400px'; modalContent.style.width = '90%'; modalContent.style.textAlign = 'center'; // Create the message element const messageElement = document.createElement('p'); messageElement.id = 'publish-modal-message'; messageElement.textContent = message; modalContent.appendChild(messageElement); publishModal.appendChild(modalContent); document.body.appendChild(publishModal); } else { // Update the message const messageElement = publishModal.querySelector('#publish-modal-message'); messageElement.textContent = message; // Remove any buttons (Retry/Cancel) if they exist const buttons = publishModal.querySelector('#publish-modal-buttons'); if (buttons) { buttons.remove(); } } if (message == "Publish TX submitted! Confirmation needed.") { // Create buttons container const buttonsContainer = document.createElement('div'); buttonsContainer.id = 'publish-modal-buttons'; buttonsContainer.style.marginTop = '20px'; // Create Close button const closeButton = document.createElement('button'); closeButton.textContent = 'Close'; closeButton.addEventListener('click', () => { closePublishModal(); }); buttonsContainer.appendChild(closeButton); // Append button to modal content const modalContent = publishModal.firstChild; modalContent.appendChild(buttonsContainer); } } function closePublishModal() { if (publishModal) { document.body.removeChild(publishModal); publishModal = null; } } function showPublishErrorModal(errorMessage, onRetry, onCancel) { if (publishModal) { // Update the message const messageElement = publishModal.querySelector('#publish-modal-message'); messageElement.textContent = errorMessage; // Remove any existing buttons const existingButtons = publishModal.querySelector('#publish-modal-buttons'); if (existingButtons) { existingButtons.remove(); } // Create buttons container const buttonsContainer = document.createElement('div'); buttonsContainer.id = 'publish-modal-buttons'; buttonsContainer.style.marginTop = '20px'; // Create Retry button const retryButton = document.createElement('button'); retryButton.textContent = 'Retry'; retryButton.style.marginRight = '10px'; retryButton.addEventListener('click', () => { onRetry(); }); // Create Cancel button const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.addEventListener('click', () => { onCancel(); }); buttonsContainer.appendChild(retryButton); buttonsContainer.appendChild(cancelButton); // Append buttons to modal content const modalContent = publishModal.firstChild; modalContent.appendChild(buttonsContainer); } } async function publishWithFeedback(publishParams) { return new Promise(async (resolve, reject) => { async function attemptPublish() { try { // Show modal with "Attempting to publish, please wait..." showPublishModal("Attempting to publish, please wait..."); const response = await qortalRequest(publishParams); // Close modal resolve(response); showPublishModal("Publish TX submitted! Confirmation needed."); } catch (error) { // Update modal to show error message and Retry/Cancel buttons showPublishErrorModal(`Publishing failed: ${error.message}`, () => { // On Retry attemptPublish(); }, () => { // On Cancel closePublishModal(); reject(error); }); } } await attemptPublish(); }); }