import { buildArweaveDataUrl, getArweaveUploadPrice, loadArweaveFileMetadata, sha256HexFromArrayBuffer, uploadArweaveFile, validateArweaveTxId, } from '../services/arweave-file-service.js'; import { addArweaveWalletSecret, getArweaveBalance, getArweaveWalletChoices } from '../services/arweave-wallet-service.js'; import { escapeHtml, formatBytes, normalizeAttachment } from '../services/attachment-format.js'; const HISTORY_KEY = 'shine-ui-arweave-attachment-history-v1'; const LAST_WALLET_KEY_PREFIX = 'shine-ui-arweave-last-wallet-v1:'; const MAX_HISTORY_ITEMS = 100; const RECENT_UPLOAD_MS = 20 * 60 * 1000; function normalizeHistoryItem(input = {}) { const item = normalizeAttachment(input); const placedInShineAtMs = Number(input.placedInShineAtMs || 0) || 0; const purpose = String(input.purpose || '').trim() === 'avatar' ? 'avatar' : 'attachment'; return { ...item, purpose, pendingPlacement: input.pendingPlacement === true && !placedInShineAtMs, placedInShineAtMs, }; } export function readArweaveAttachmentHistory(login) { try { const raw = sessionStorage.getItem(HISTORY_KEY); const parsed = raw ? JSON.parse(raw) : {}; const key = String(login || '').trim().toLowerCase(); const rows = Array.isArray(parsed?.[key]) ? parsed[key] : []; return rows.map((item) => normalizeHistoryItem(item)).filter(Boolean); } catch { return []; } } export function saveArweaveAttachmentHistory(login, rows) { try { const key = String(login || '').trim().toLowerCase(); const raw = sessionStorage.getItem(HISTORY_KEY); const parsed = raw ? JSON.parse(raw) : {}; parsed[key] = (Array.isArray(rows) ? rows : []) .map((item) => normalizeHistoryItem(item)) .slice(0, MAX_HISTORY_ITEMS); sessionStorage.setItem(HISTORY_KEY, JSON.stringify(parsed)); } catch { // ignore sessionStorage errors } } export function clearArweaveAttachmentHistory(login) { saveArweaveAttachmentHistory(login, []); } export function addArweaveAttachmentToHistory(login, attachment, { pendingPlacement = undefined, markPlaced = false } = {}) { const item = normalizeHistoryItem(attachment); const rows = readArweaveAttachmentHistory(login); const existing = rows.find((row) => row.ar === item.ar) || {}; const placedInShineAtMs = markPlaced ? Date.now() : Number(existing.placedInShineAtMs || item.placedInShineAtMs || 0) || 0; const nextPendingPlacement = pendingPlacement === undefined ? (existing.pendingPlacement === true && !placedInShineAtMs) : (!!pendingPlacement && !placedInShineAtMs); const nextItem = { ...item, pendingPlacement: nextPendingPlacement, placedInShineAtMs, }; const nextRows = rows.filter((row) => row.ar !== item.ar); nextRows.unshift(nextItem); saveArweaveAttachmentHistory(login, nextRows); return nextItem; } export function markArweaveAttachmentPlaced(login, attachment) { const item = normalizeHistoryItem(attachment); const rows = readArweaveAttachmentHistory(login); const existing = rows.find((row) => row.ar === item.ar) || {}; const nextItem = { ...item, uploadedAtMs: item.uploadedAtMs || existing.uploadedAtMs || Date.now(), pendingPlacement: false, placedInShineAtMs: Number(existing.placedInShineAtMs || 0) || Date.now(), }; const nextRows = rows.filter((row) => row.ar !== item.ar); nextRows.unshift(nextItem); saveArweaveAttachmentHistory(login, nextRows); return nextItem; } export function formatArweaveHistoryTime(ms) { const value = Number(ms || 0); if (!Number.isFinite(value) || value <= 0) return '—'; const diffMs = Date.now() - value; if (diffMs >= 0 && diffMs < RECENT_UPLOAD_MS) { const minutes = Math.max(1, Math.floor(diffMs / 60000)); return `${minutes} мин назад`; } return new Date(value).toLocaleString('ru-RU'); } export function formatArweaveHistoryDateTime(ms) { const value = Number(ms || 0); if (!Number.isFinite(value) || value <= 0) return '—'; return new Date(value).toLocaleString('ru-RU'); } export async function getArweaveAttachmentAvailability({ gateway, attachment } = {}) { const item = normalizeAttachment(attachment); const url = buildArweaveDataUrl({ gateway, txId: item.ar }); try { const response = await fetch(url, { method: 'HEAD', cache: 'no-store' }); if (response.ok) return { status: 'available', label: '✓ Доступен', url }; } catch { // gateway может временно не отвечать или блокировать HEAD; для UI это всё равно недоступно. } const ageMs = Date.now() - Number(item.uploadedAtMs || 0); if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < RECENT_UPLOAD_MS) { return { status: 'pending', label: '… Ещё обновляется', url }; } return { status: 'unavailable', label: '✕ Недоступен', url }; } function setText(node, text) { if (node) node.textContent = String(text || ''); } function shortAddress(value) { const raw = String(value || '').trim(); if (raw.length <= 16) return raw; return `${raw.slice(0, 8)}...${raw.slice(-6)}`; } function lastWalletStorageKey(login) { return `${LAST_WALLET_KEY_PREFIX}${String(login || '').trim().toLowerCase() || 'anonymous'}`; } function readLastWalletId(login) { try { return sessionStorage.getItem(lastWalletStorageKey(login)) || ''; } catch { return ''; } } function writeLastWalletId(login, walletId) { try { const clean = String(walletId || '').trim(); if (clean) sessionStorage.setItem(lastWalletStorageKey(login), clean); } catch { // Если sessionStorage недоступен, выбор сохраняется хотя бы в текущем окне менеджера. } } async function resizeImageFileToAvatar(file) { const source = file instanceof File ? file : null; if (!source) throw new Error('Выберите изображение.'); const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(new Error('Не удалось прочитать изображение.')); reader.readAsDataURL(source); }); const image = await new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('Не удалось открыть изображение.')); img.src = dataUrl; }); const side = Math.min(image.naturalWidth || image.width, image.naturalHeight || image.height); if (!side || side <= 0) throw new Error('Некорректный размер изображения.'); const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Canvas недоступен.'); const sx = Math.max(0, ((image.naturalWidth || image.width) - side) / 2); const sy = Math.max(0, ((image.naturalHeight || image.height) - side) / 2); ctx.drawImage(image, sx, sy, side, side, 0, 0, 512, 512); const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.9)); if (!blob) throw new Error('Не удалось сжать аватар.'); return new File([blob], 'avatar.jpg', { type: 'image/jpeg', lastModified: Date.now() }); } export function openArweaveAttachmentManager({ login, storagePwd, gateway, onSelect, selectedTxIds = [], historyOnly = false, mode = 'attachment', historyPurpose = '', } = {}) { const cleanLogin = String(login || '').trim(); const cleanStoragePwd = String(storagePwd || '').trim(); const cleanGateway = String(gateway || 'https://arweave.net').trim(); if (!cleanLogin || !cleanStoragePwd) return Promise.reject(new Error('Нет активной сессии.')); const root = document.createElement('div'); root.className = 'ar-attachment-manager-root'; document.body.append(root); let closed = false; let wallets = []; let selectedWalletId = readLastWalletId(cleanLogin) || 'derived-client-key'; let selectedFile = null; let selectedSha256 = ''; let priceInfo = null; let balanceInfo = null; let autoOpenedFileDialog = false; const isAvatarMode = String(mode || '') === 'avatar'; const historyPurposeMode = String(historyPurpose || '').trim(); const purposeFilter = isAvatarMode || historyPurposeMode === 'avatar' ? 'avatar' : 'attachment'; const selectedTxIdSet = new Set((Array.isArray(selectedTxIds) ? selectedTxIds : []).map((item) => String(item || '').trim()).filter(Boolean)); function selectedWallet() { return wallets.find((item) => String(item.id) === String(selectedWalletId)) || wallets[0] || null; } function close(resolve, result = null) { if (closed) return; closed = true; root.remove(); resolve(result); } function finish(resolve, attachment, { pendingPlacement = undefined } = {}) { const item = addArweaveAttachmentToHistory(cleanLogin, attachment, { pendingPlacement, markPlaced: false, }); if (!pendingPlacement && typeof onSelect === 'function') onSelect(item); close(resolve, item); } return new Promise((resolve) => { const bindBackdrop = () => { const modal = root.querySelector('[data-ar-attach-modal="true"]'); modal?.addEventListener('click', (event) => { if (event.target === modal) close(resolve, null); }); }; const renderWalletOptions = () => wallets.map((wallet) => ( `` )).join(''); const loadWallets = async (errorEl = null) => { try { wallets = await getArweaveWalletChoices({ login: cleanLogin, storagePwd: cleanStoragePwd }); if (!wallets.some((item) => String(item.id) === String(selectedWalletId))) { selectedWalletId = String(wallets[0]?.id || 'derived-client-key'); } writeLastWalletId(cleanLogin, selectedWalletId); } catch (error) { setText(errorEl, error?.message || 'Не удалось загрузить Arweave-кошельки.'); } }; const showUpload = async () => { root.innerHTML = `