SHA256
UI: добавить журнал загрузок Arweave
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user