SHA256
Каналы: добавить профиль через TEXT_CHANNEL_META
This commit is contained in:
@@ -16,8 +16,10 @@ 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,
|
||||
};
|
||||
@@ -133,6 +135,36 @@ function shortAddress(value) {
|
||||
return `${raw.slice(0, 8)}...${raw.slice(-6)}`;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -140,6 +172,8 @@ export function openArweaveAttachmentManager({
|
||||
onSelect,
|
||||
selectedTxIds = [],
|
||||
historyOnly = false,
|
||||
mode = 'attachment',
|
||||
historyPurpose = '',
|
||||
} = {}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanStoragePwd = String(storagePwd || '').trim();
|
||||
@@ -158,6 +192,8 @@ export function openArweaveAttachmentManager({
|
||||
let priceInfo = null;
|
||||
let balanceInfo = null;
|
||||
let autoOpenedFileDialog = false;
|
||||
const isAvatarMode = String(mode || '') === 'avatar';
|
||||
const purposeFilter = isAvatarMode || String(historyPurpose || '').trim() === 'avatar' ? 'avatar' : '';
|
||||
const selectedTxIdSet = new Set((Array.isArray(selectedTxIds) ? selectedTxIds : []).map((item) => String(item || '').trim()).filter(Boolean));
|
||||
|
||||
function selectedWallet() {
|
||||
@@ -207,18 +243,18 @@ export function openArweaveAttachmentManager({
|
||||
root.innerHTML = `
|
||||
<div class="modal" data-ar-attach-modal="true">
|
||||
<div class="modal-card stack ar-attachment-manager-card">
|
||||
<h3 class="modal-title">${historyOnly ? 'Загрузить файл в блокчейн' : 'Добавить вложение'}</h3>
|
||||
<h3 class="modal-title">${isAvatarMode ? 'Загрузить аватар' : (historyOnly ? 'Загрузить файл в блокчейн' : 'Добавить вложение')}</h3>
|
||||
<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>
|
||||
${historyOnly ? '' : '<p class="meta-muted">Для надёжности лучше сначала загрузить файл в Arweave, дождаться доступности в журнале, а потом добавить его из истории загрузок.</p>'}
|
||||
${isAvatarMode ? '<p class="meta-muted">Выберите изображение. Перед загрузкой оно будет сжато до 512×512 и сохранено в истории как аватар.</p>' : (historyOnly ? '' : '<p class="meta-muted">Для надёжности лучше сначала загрузить файл в Arweave, дождаться доступности в журнале, а потом добавить его из истории загрузок.</p>')}
|
||||
<label class="meta-muted" for="ar-attach-file">Файл для загрузки</label>
|
||||
<input class="input" id="ar-attach-file" type="file" />
|
||||
<input class="input" id="ar-attach-file" type="file" ${isAvatarMode ? 'accept="image/*"' : ''} />
|
||||
<div class="ar-attachment-meta" data-meta="true"></div>
|
||||
<p class="meta-muted inline-error" data-error="true"></p>
|
||||
<div class="form-actions-grid">
|
||||
${historyOnly ? '' : '<button class="secondary-btn" type="button" data-action="existing">Ввести txId</button>'}
|
||||
${historyOnly ? '' : '<button class="secondary-btn" type="button" data-action="history">История загрузок</button>'}
|
||||
${historyOnly || isAvatarMode ? '' : '<button class="secondary-btn" type="button" data-action="existing">Ввести txId</button>'}
|
||||
${historyOnly ? '' : `<button class="secondary-btn" type="button" data-action="history">${isAvatarMode ? 'История аватаров' : 'История загрузок'}</button>`}
|
||||
<button class="primary-btn" type="button" data-action="upload" disabled>${historyOnly ? 'Загрузить в журнал' : 'Загрузить'}</button>
|
||||
</div>
|
||||
<button class="secondary-btn" type="button" data-action="cancel">Отмена</button>
|
||||
@@ -266,13 +302,19 @@ export function openArweaveAttachmentManager({
|
||||
try {
|
||||
const wallet = selectedWallet();
|
||||
if (!wallet?.address) throw new Error('Выберите Arweave-кошелёк.');
|
||||
if (isAvatarMode && !String(selectedFile.type || '').startsWith('image/')) {
|
||||
throw new Error('Для аватара можно выбрать только изображение.');
|
||||
}
|
||||
if (isAvatarMode) {
|
||||
selectedFile = await resizeImageFileToAvatar(selectedFile);
|
||||
}
|
||||
const buffer = await selectedFile.arrayBuffer();
|
||||
selectedSha256 = await sha256HexFromArrayBuffer(buffer);
|
||||
priceInfo = await getArweaveUploadPrice({ gateway: cleanGateway, byteLength: selectedFile.size });
|
||||
balanceInfo = await getArweaveBalance({ gateway: cleanGateway, address: wallet.address });
|
||||
const hasFunds = BigInt(balanceInfo.winston) >= BigInt(priceInfo.winston);
|
||||
metaEl.innerHTML = `
|
||||
<div>Имя: ${escapeHtml(selectedFile.name || 'file')}</div>
|
||||
${isAvatarMode ? '' : `<div>Имя: ${escapeHtml(selectedFile.name || 'file')}</div>`}
|
||||
<div>Размер: ${escapeHtml(formatBytes(selectedFile.size))}</div>
|
||||
<div>SHA-256: ${escapeHtml(selectedSha256)}</div>
|
||||
<div>Цена: ${escapeHtml(Number(priceInfo.ar).toLocaleString('ru-RU', { maximumFractionDigits: 6 }))} AR</div>
|
||||
@@ -305,19 +347,20 @@ export function openArweaveAttachmentManager({
|
||||
gateway: cleanGateway,
|
||||
jwk: wallet.jwk,
|
||||
file: selectedFile,
|
||||
shineType: 'attachment',
|
||||
shineType: isAvatarMode ? 'avatar' : 'attachment',
|
||||
tags: [
|
||||
{ name: 'SHiNE-Login', value: cleanLogin },
|
||||
{ name: 'SHiNE-Attachment-Name', value: selectedFile.name || 'file' },
|
||||
{ name: isAvatarMode ? 'SHiNE-Avatar' : 'SHiNE-Attachment-Name', value: isAvatarMode ? '1' : (selectedFile.name || 'file') },
|
||||
],
|
||||
});
|
||||
finish(resolve, {
|
||||
name: selectedFile.name || 'file',
|
||||
name: isAvatarMode ? 'Аватар' : (selectedFile.name || 'file'),
|
||||
size: selectedFile.size,
|
||||
sha256: selectedSha256,
|
||||
ar: uploaded.id,
|
||||
uploadedAtMs: Date.now(),
|
||||
}, { pendingPlacement: historyOnly });
|
||||
purpose: isAvatarMode ? 'avatar' : 'attachment',
|
||||
}, { pendingPlacement: historyOnly && !isAvatarMode });
|
||||
} catch (error) {
|
||||
uploadBtn.disabled = false;
|
||||
setText(errorEl, error?.message || 'Не удалось загрузить файл в Arweave.');
|
||||
@@ -394,7 +437,10 @@ export function openArweaveAttachmentManager({
|
||||
};
|
||||
|
||||
const showHistory = () => {
|
||||
const rows = readArweaveAttachmentHistory(cleanLogin).slice().reverse();
|
||||
const rows = readArweaveAttachmentHistory(cleanLogin)
|
||||
.filter((item) => !purposeFilter || item.purpose === purposeFilter)
|
||||
.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">
|
||||
@@ -405,7 +451,7 @@ export function openArweaveAttachmentManager({
|
||||
const isSelected = selectedTxIdSet.has(item.ar);
|
||||
return `
|
||||
<article class="ar-attachment-history-tile${isSelected ? ' is-selected' : ''}" data-history-tile="${index}">
|
||||
<strong class="ar-attachment-history-name">${escapeHtml(item.name)}</strong>
|
||||
<strong class="ar-attachment-history-name">${escapeHtml(item.name)}${item.purpose === 'avatar' ? ' · аватар' : ''}</strong>
|
||||
<div class="ar-attachment-history-meta-row">
|
||||
<span>${escapeHtml(formatBytes(item.size))}</span>
|
||||
<span>${escapeHtml(formatArweaveHistoryTime(item.uploadedAtMs))}</span>
|
||||
|
||||
Reference in New Issue
Block a user