1643 lines
68 KiB
JavaScript
1643 lines
68 KiB
JavaScript
// ===== 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 =
|
|
`<img src="red-x.svg" style="width:15px;height:15px;"> Click the identifier to "delete" content.<br>(This will replace it with a blank file.)<br><br>
|
|
<img src="file-up.png" style="width:15px;height:15px;"> Click the file select icon to "edit" content.<br>(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<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';
|
|
// 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';
|
|
return fallback || 'application/octet-stream';
|
|
}
|
|
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();
|
|
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}<br>${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}<br>${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 = '<p>Loading...</p>';
|
|
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 = `<p>Error: ${error.message}</p>`;
|
|
}
|
|
}
|
|
|
|
// ===== Table & pagination =====
|
|
function buildIdentifierCellHTML(result, identifier) {
|
|
const svc = result.service;
|
|
const key = selKey(svc, identifier);
|
|
const checkbox = `<input type="checkbox" class="bulk-select" data-service="${svc}" data-identifier="${identifier}" ${selectedForDeletion.has(key) ? 'checked' : ''} />`;
|
|
const deleteIcon = `<img src="red-x.svg" class="clickable-delete action-icon" title="Delete" alt="Delete" data-service="${svc}" data-identifier="${identifier}"/>`;
|
|
const editIcon = `<img src="file-up.png" class="clickable-edit action-icon" title="Edit" alt="Edit" data-service="${svc}" data-identifier="${identifier}"/>`;
|
|
const embedIcon = isEmbedService(svc) ? (
|
|
`<svg class="action-icon copy-embed-icon" data-name="${userName}" data-service="${svc}" data-identifier="${identifier}" width="15" height="15" viewBox="0 0 24 24" style="margin-left:6px;">
|
|
<circle cx="12" cy="12" r="11" fill="#ffffff"></circle>
|
|
<path fill="#0f1a2e" d="M10.59 13.41a1 1 0 0 0 1.41 1.41l4.95-4.95a3 3 0 1 0-4.24-4.24l-2.12 2.12a1 1 0 1 0 1.41 1.41l2.12-2.12a1 1 0 1 1 1.41 1.41l-4.95 4.95zM13.41 10.59a1 1 0 0 0-1.41-1.41L7.05 14.13a3 3 0 1 0 4.24 4.24l2.12-2.12a1 1 0 1 0-1.41-1.41l-2.12 2.12a1 1 0 1 1-1.41-1.41l4.95-4.95z"/>
|
|
</svg>`
|
|
) : '';
|
|
return `${checkbox}${deleteIcon}${editIcon}<span class="identifier-text">${identifier}</span>${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 = '<p>No results found.</p>';
|
|
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 = '<table>';
|
|
tableHtml += `<tr>
|
|
<th>Service</th>
|
|
<th>Identifier ${ bulkDeleteMode ? '<input type="checkbox" id="select-all-checkbox" title="Select all on this page"><label class="select-all-label" for="select-all-checkbox">Select All</label>' : '' }</th>
|
|
<th>Metadata</th>
|
|
<th>Preview</th>
|
|
<th>Size</th>
|
|
<th>Created / Updated</th>
|
|
</tr>`;
|
|
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 += `<tr>
|
|
<td>${result.service}</td>
|
|
<td>${buildIdentifierCellHTML(result, identifier)}</td>
|
|
<td><span class="clickable-metadata" data-metadata-index='${metadataIndex}'>${metadataKeys}</span></td>
|
|
<td>${generatePreviewHTML(result, userName, identifier)}</td>
|
|
<td>${sizeString}</td>
|
|
<td>${createdString}<br>${updatedString}</td>
|
|
</tr>`;
|
|
}
|
|
tableHtml += `</table>`;
|
|
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
|
const endItem = Math.min(startItem + itemsPerPage - 1, totalResults);
|
|
const selectedCount = getSelectedCount();
|
|
contentSummaryDiv.innerHTML = `<div class="summary-bar"><span>${startItem}-${endItem} of ${totalResults} results</span><span style=\"margin-left:12px;\">Total Size: ${formatSize(totalSize)}</span><span style=\"margin-left:18px;\"><button id=\"bulk-delete-toggle\">${ !bulkDeleteMode ? 'Delete Files' : (selectedCount > 0 ? `Delete ${selectedCount} Files` : 'Deleting Files') }</button>${ selectedCount > 0 ? '<button id=\"clear-selected-btn\" style=\"margin-left:8px;\">Clear Selected</button>' : '' }</span></div>`;
|
|
|
|
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 = '<nav class="pagination-controls" aria-label="Pagination">';
|
|
// First/Prev
|
|
if (currentPage > 1) {
|
|
html += `<span class="pagination-link" data-page="1" aria-label="First">««</span>`;
|
|
html += `<span class="pagination-link" data-page="${currentPage - 1}" aria-label="Previous">«</span>`;
|
|
} else {
|
|
html += `<span class="disabled" aria-disabled="true">««</span><span class="disabled" aria-disabled="true">«</span>`;
|
|
}
|
|
|
|
// Windowed pages
|
|
const windowSize = 2;
|
|
const pages = new Set([1, 2, totalPages - 1, totalPages]);
|
|
for (let p = currentPage - windowSize; p <= currentPage + windowSize; p++) {
|
|
if (p >= 1 && p <= totalPages) pages.add(p);
|
|
}
|
|
const sorted = Array.from(pages).sort((a,b)=>a-b);
|
|
let last = 0;
|
|
for (const p of sorted) {
|
|
if (p - last > 1) html += `<span class="ellipsis" aria-hidden="true">…</span>`;
|
|
if (p === currentPage) html += `<span class="current-page" aria-current="page">${p}</span>`;
|
|
else html += `<span class="pagination-link" data-page="${p}">${p}</span>`;
|
|
last = p;
|
|
}
|
|
|
|
// Next/Last
|
|
if (currentPage < totalPages) {
|
|
html += `<span class="pagination-link" data-page="${currentPage + 1}" aria-label="Next">»</span>`;
|
|
html += `<span class="pagination-link" data-page="${totalPages}" aria-label="Last">»»</span>`;
|
|
} else {
|
|
html += `<span class="disabled" aria-disabled="true">»</span><span class="disabled" aria-disabled="true">»»</span>`;
|
|
}
|
|
|
|
// Jump control
|
|
html += `<span class="jump-to-page">
|
|
<label style="margin-right:4px;">Jump:</label>
|
|
<input type="number" class="jump-input" min="1" max="${totalPages}" value="${currentPage}" aria-label="Jump to page">
|
|
<button type="button" class="jump-btn">Go</button>
|
|
</span>`;
|
|
|
|
html += '</nav>';
|
|
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 `<div class="preview-holder" data-name="${safeName}" data-service="${safeService}" data-identifier="${safeIdent}">0% Loaded</div>`;
|
|
}
|
|
|
|
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<bin.length;i++) bytes[i]=bin.charCodeAt(i);
|
|
const dec = new TextDecoder('utf-8'); const text = dec.decode(bytes);
|
|
if (looksHtml(text)) {
|
|
const iframe = document.createElement('iframe');
|
|
iframe.setAttribute('sandbox','');
|
|
iframe.style.width = '240px'; iframe.style.height = '160px';
|
|
iframe.srcdoc = text; set(iframe);
|
|
} else {
|
|
const pre = document.createElement('pre');
|
|
pre.style.maxWidth = '240px'; pre.style.maxHeight = '160px'; pre.style.overflow = 'auto';
|
|
pre.textContent = text.slice(0, 5000); set(pre);
|
|
}
|
|
return;
|
|
} else {
|
|
// Media / file: make Blob URL from decrypted bytes
|
|
const bytes = b64ToBytes(decB64);
|
|
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;
|
|
}
|
|
} 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<bin.length;i++) bytes[i]=bin.charCodeAt(i);
|
|
const dec = new TextDecoder('utf-8'); const text = dec.decode(bytes);
|
|
resp = text || b64;
|
|
}
|
|
if (typeof resp === 'string') {
|
|
if (looksHtml(resp)) {
|
|
const iframe = document.createElement('iframe');
|
|
iframe.setAttribute('sandbox','');
|
|
iframe.style.width = '240px'; iframe.style.height = '160px';
|
|
iframe.srcdoc = resp; set(iframe);
|
|
} else {
|
|
const pre = document.createElement('pre');
|
|
pre.style.maxWidth = '240px'; pre.style.maxHeight = '160px'; pre.style.overflow = 'auto';
|
|
pre.textContent = resp.slice(0, 5000); set(pre);
|
|
}
|
|
} else if (resp && typeof resp === 'object') {
|
|
const pre = document.createElement('pre');
|
|
pre.style.maxWidth = '240px'; pre.style.maxHeight = '160px'; pre.style.overflow = 'auto';
|
|
pre.textContent = JSON.stringify(resp, null, 2).slice(0, 5000); set(pre);
|
|
} else { container.textContent = '(unavailable)'; }
|
|
return;
|
|
}
|
|
// Media/file via base64 -> 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();
|
|
});
|
|
}
|