UI: добавить журнал загрузок Arweave

This commit is contained in:
AidarKC
2026-07-31 13:13:58 +04:00
parent c530627078
commit 98eba54621
11 changed files with 310 additions and 27 deletions
@@ -1,4 +1,5 @@
import {
buildArweaveDataUrl,
getArweaveUploadPrice,
loadArweaveFileMetadata,
sha256HexFromArrayBuffer,
@@ -10,8 +11,9 @@ import { escapeHtml, formatBytes, normalizeAttachment } from '../services/attach
const HISTORY_KEY = 'shine-ui-arweave-attachment-history-v1';
const MAX_HISTORY_ITEMS = 100;
const RECENT_UPLOAD_MS = 20 * 60 * 1000;
function readHistory(login) {
export function readArweaveAttachmentHistory(login) {
try {
const raw = sessionStorage.getItem(HISTORY_KEY);
const parsed = raw ? JSON.parse(raw) : {};
@@ -23,7 +25,7 @@ function readHistory(login) {
}
}
function saveHistory(login, rows) {
export function saveArweaveAttachmentHistory(login, rows) {
try {
const key = String(login || '').trim().toLowerCase();
const raw = sessionStorage.getItem(HISTORY_KEY);
@@ -35,20 +37,40 @@ function saveHistory(login, rows) {
}
}
function addToHistory(login, attachment) {
export function clearArweaveAttachmentHistory(login) {
saveArweaveAttachmentHistory(login, []);
}
export function addArweaveAttachmentToHistory(login, attachment) {
const item = normalizeAttachment(attachment);
const rows = readHistory(login).filter((row) => row.ar !== item.ar);
const rows = readArweaveAttachmentHistory(login).filter((row) => row.ar !== item.ar);
rows.unshift(item);
saveHistory(login, rows);
saveArweaveAttachmentHistory(login, rows);
return item;
}
function formatDateTime(ms) {
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 || '');
}
@@ -64,6 +86,7 @@ export function openArweaveAttachmentManager({
storagePwd,
gateway,
onSelect,
selectedTxIds = [],
} = {}) {
const cleanLogin = String(login || '').trim();
const cleanStoragePwd = String(storagePwd || '').trim();
@@ -82,6 +105,7 @@ export function openArweaveAttachmentManager({
let priceInfo = null;
let balanceInfo = null;
let autoOpenedFileDialog = false;
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;
@@ -95,7 +119,7 @@ export function openArweaveAttachmentManager({
}
function finish(resolve, attachment) {
const item = addToHistory(cleanLogin, attachment);
const item = addArweaveAttachmentToHistory(cleanLogin, attachment);
if (typeof onSelect === 'function') onSelect(item);
close(resolve, item);
}
@@ -131,13 +155,14 @@ export function openArweaveAttachmentManager({
<label class="meta-muted" for="ar-attach-wallet">Кошелёк оплаты Arweave</label>
<select class="input" id="ar-attach-wallet"></select>
<button class="ghost-btn" type="button" data-action="add-wallet">Добавить кошелёк</button>
<p class="meta-muted">Для надёжности лучше сначала загрузить файл в Arweave, дождаться доступности в журнале, а потом добавить его из истории загрузок.</p>
<label class="meta-muted" for="ar-attach-file">Файл для загрузки</label>
<input class="input" id="ar-attach-file" type="file" />
<div class="ar-attachment-meta" data-meta="true"></div>
<p class="meta-muted inline-error" data-error="true"></p>
<div class="form-actions-grid">
<button class="secondary-btn" type="button" data-action="existing">Ввести txId</button>
<button class="secondary-btn" type="button" data-action="history">История</button>
<button class="secondary-btn" type="button" data-action="history">История загрузок</button>
<button class="primary-btn" type="button" data-action="upload" disabled>Загрузить</button>
</div>
<button class="secondary-btn" type="button" data-action="cancel">Отмена</button>
@@ -313,29 +338,27 @@ export function openArweaveAttachmentManager({
};
const showHistory = () => {
const rows = readHistory(cleanLogin);
const rows = readArweaveAttachmentHistory(cleanLogin).slice().reverse();
root.innerHTML = `
<div class="modal" data-ar-attach-modal="true">
<div class="modal-card stack ar-attachment-manager-card ar-attachment-manager-card--wide">
<h3 class="modal-title">Журнал загруженных файлов</h3>
<p class="meta-muted">Журнал хранится только в этой браузерной сессии.</p>
<div class="ar-attachment-history-table-wrap">
<table class="ar-attachment-history-table">
<thead>
<tr><th>Файл</th><th>txId</th><th>Размер</th><th>Дата</th><th></th></tr>
</thead>
<tbody>
${rows.length ? rows.map((item, index) => `
<tr>
<td>${escapeHtml(item.name)}</td>
<td class="mono-cell">${escapeHtml(item.ar)}</td>
<td>${escapeHtml(formatBytes(item.size))}</td>
<td>${escapeHtml(formatDateTime(item.uploadedAtMs))}</td>
<td><button class="ghost-btn" type="button" data-pick="${index}">Выбрать</button></td>
</tr>
`).join('') : '<tr><td colspan="5">В этой сессии ещё нет загруженных файлов.</td></tr>'}
</tbody>
</table>
<div class="ar-attachment-history-tiles" data-history-scroll="true">
${rows.length ? rows.map((item, index) => {
const isSelected = selectedTxIdSet.has(item.ar);
return `
<article class="ar-attachment-history-tile${isSelected ? ' is-selected' : ''}" data-history-tile="${index}">
<div class="ar-attachment-history-tile-head">
<strong>${escapeHtml(item.name)}</strong>
<span class="ar-attachment-status ar-attachment-status--pending" data-status="${index}">Проверяем...</span>
</div>
<div class="meta-muted">${escapeHtml(formatBytes(item.size))} · ${escapeHtml(formatArweaveHistoryDateTime(item.uploadedAtMs))}</div>
<div class="mono-cell ar-attachment-history-txid">${escapeHtml(item.ar)}</div>
<button class="${isSelected ? 'secondary-btn' : 'ghost-btn'}" type="button" data-pick="${index}" ${isSelected ? 'disabled' : ''}>${isSelected ? 'Уже добавлен ✓' : 'Выбрать'}</button>
</article>
`;
}).join('') : '<div class="card meta-muted">В этой сессии ещё нет загруженных файлов.</div>'}
</div>
<div class="form-actions-grid">
<button class="secondary-btn" type="button" data-action="back">Назад</button>
@@ -344,6 +367,10 @@ export function openArweaveAttachmentManager({
</div>
`;
bindBackdrop();
const scrollEl = root.querySelector('[data-history-scroll="true"]');
window.setTimeout(() => {
if (scrollEl) scrollEl.scrollTop = scrollEl.scrollHeight;
}, 0);
root.querySelector('[data-action="back"]')?.addEventListener('click', showUpload);
root.querySelectorAll('[data-pick]').forEach((button) => {
button.addEventListener('click', () => {
@@ -352,6 +379,20 @@ export function openArweaveAttachmentManager({
if (item) finish(resolve, item);
});
});
rows.forEach((item, index) => {
const statusEl = root.querySelector(`[data-status="${index}"]`);
void getArweaveAttachmentAvailability({ gateway: cleanGateway, attachment: item })
.then((result) => {
if (!statusEl) return;
statusEl.className = `ar-attachment-status ar-attachment-status--${result.status}`;
statusEl.textContent = result.label;
})
.catch(() => {
if (!statusEl) return;
statusEl.className = 'ar-attachment-status ar-attachment-status--unavailable';
statusEl.textContent = 'Недоступен';
});
});
};
const showAddWallet = () => {