Files
fileshare-app/src-bak/static/index.html
T
2026-10-22 20:45:13 +03:00

262 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LAN Share — Текст в ОЗУ / Медиа на Диск</title>
<style>
:root { --bg: #0f172a; --card: #1e293b; --border: #334155; --accent: #3b82f6; --green: #10b981; --red: #ef4444; --text: #f8fafc; --muted: #94a3b8; }
* { box-sizing: border-box; font-family: system-ui, -apple-system, sans-serif; }
body { margin: 0; padding: 20px; background: var(--bg); color: var(--text); }
.container { max-width: 1100px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 800px) { .container { grid-template-columns: 1fr; } }
.header { grid-column: 1 / -1; background: var(--card); border: 1px solid var(--border); padding: 16px 20px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
.pin-badge { background: #1d4ed8; padding: 6px 14px; border-radius: 8px; font-weight: 700; letter-spacing: 1px; font-family: monospace; font-size: 1.1rem; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; display: flex; flex-direction: column; gap: 14px; }
h2 { margin: 0; font-size: 1.1rem; display: flex; justify-content: space-between; align-items: center; }
.tag { font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; font-weight: 600; }
.tag-ram { background: rgba(16, 185, 129, 0.2); color: #34d399; }
.tag-disk { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
select, textarea, input[type="file"] { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 10px; font-size: 0.95rem; }
textarea { resize: vertical; min-height: 80px; }
button { background: var(--accent); color: white; border: none; border-radius: 8px; padding: 10px 16px; font-weight: 600; cursor: pointer; transition: opacity 0.15s; }
button:hover { opacity: 0.9; }
button.btn-green { background: var(--green); }
button.btn-red { background: var(--red); }
button.btn-sm { padding: 6px 12px; font-size: 0.82rem; }
.peer-item, .msg-item { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.msg-item { flex-direction: column; align-items: flex-start; }
.msg-meta { width: 100%; display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--muted); }
.msg-text { font-size: 0.95rem; white-space: pre-wrap; word-break: break-word; width: 100%; margin: 4px 0; }
.path-box { font-family: monospace; font-size: 0.78rem; color: #93c5fd; background: rgba(59,130,246,0.1); padding: 4px 8px; border-radius: 4px; word-break: break-all; }
.preview-img { max-width: 100%; max-height: 180px; border-radius: 6px; margin-top: 6px; }
#status { grid-column: 1 / -1; padding: 10px 16px; border-radius: 8px; display: none; font-weight: 500; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<div style="font-size: 1.2rem; font-weight: 700;" id="my-name">Загрузка...</div>
<div style="font-size: 0.85rem; color: var(--muted);" id="my-ip"></div>
</div>
<div>
<span style="font-size: 0.85rem; color: var(--muted); margin-right: 8px;">Код сверки сертификата (PIN):</span>
<span class="pin-badge" id="my-pin">--------</span>
</div>
</div>
<div id="status"></div>
<div class="card">
<h2>1. Устройства в сети (mDNS) <span class="tag tag-disk">TLS Pinning</span></h2>
<div id="peers-list" style="display:flex; flex-direction:column; gap:8px;">
<div style="color:var(--muted); font-size:0.9rem;">Поиск устройств...</div>
</div>
</div>
<div class="card">
<h2>2. Отправка данных</h2>
<label style="font-size:0.85rem; color:var(--muted);">Получатель (должен быть заверен):</label>
<select id="target-peer"><option value="">-- Выберите устройство --</option></select>
<textarea id="text-input" placeholder="Введите текст (Enter — отправить, Shift+Enter — новая строка)..."></textarea>
<button onclick="sendText()">Отправить текст в ОЗУ</button>
<hr style="border:0; border-top:1px solid var(--border); width:100%; margin: 4px 0;">
<input type="file" id="media-input">
<button class="btn-green" onclick="sendMedia()">Отправить медиа на диск получателя</button>
</div>
<div class="card">
<h2>
<span>Входящий текст <span class="tag tag-ram">ТОЛЬКО В ОЗУ</span></span>
<button class="btn-red btn-sm" onclick="clearRam()">Очистить ОЗУ</button>
</h2>
<div id="ram-texts" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div>
</div>
<div class="card">
<h2>Входящие медиафайлы <span class="tag tag-disk">НА ДИСКЕ (./received_media)</span></h2>
<div id="disk-media" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div>
</div>
</div>
<script>
let knownPeersKey = "";
function showStatus(msg, isError = false) {
const el = document.getElementById('status');
el.style.display = 'block';
el.style.background = isError ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)';
el.style.border = `1px solid ${isError ? '#ef4444' : '#10b981'}`;
el.textContent = msg;
setTimeout(() => { el.style.display = 'none'; }, 4000);
}
async function fetchState() {
try {
const res = await fetch('/ui/state');
const data = await res.json();
document.getElementById('my-name').textContent = `Устройство: ${data.device_name}`;
document.getElementById('my-ip').textContent = `HTTPS P2P: ${data.my_ip}:${data.p2p_port} | SHA-256: ${data.my_fingerprint.slice(0, 24)}...`;
document.getElementById('my-pin').textContent = data.my_pin;
const peersContainer = document.getElementById('peers-list');
if (data.peers.length === 0) {
peersContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Устройства в локальной сети пока не обнаружены</div>';
} else {
peersContainer.innerHTML = data.peers.map(p => `
<div class="peer-item">
<div>
<div style="font-weight:600;">${escapeHtml(p.name)} <span style="color:var(--muted); font-size:0.85rem;">(${p.ip}:${p.port})</span></div>
<div style="font-size:0.8rem; color:var(--muted); font-family:monospace;">PIN: <b style="color:#fff;">[${p.pin_code}]</b> | Статус: ${p.verified ? '<span style="color:#34d399;">Заверен</span>' : '<span style="color:#f87171;">Не заверен</span>'}</div>
</div>
<button class="btn-sm ${p.verified ? 'btn-red' : 'btn-green'}" onclick="toggleTrust('${p.ip}', ${!p.verified})">
${p.verified ? 'Отозвать' : 'Заверить'}
</button>
</div>
`).join('');
}
const currentSelectKey = data.peers.map(p => `${p.ip}:${p.verified}`).join('|');
if (currentSelectKey !== knownPeersKey) {
knownPeersKey = currentSelectKey;
const select = document.getElementById('target-peer');
const prevVal = select.value;
select.innerHTML = '<option value="">-- Выберите устройство --</option>' +
data.peers.map(p => `<option value="${p.ip}">${escapeHtml(p.name)} (${p.ip}) ${p.verified ? '✓' : '🔒 [Не заверен]'}</option>`).join('');
if (prevVal) select.value = prevVal;
}
const textsContainer = document.getElementById('ram-texts');
if (data.texts.length === 0) {
textsContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Буфер ОЗУ пуст</div>';
} else {
textsContainer.innerHTML = data.texts.map(t => `
<div class="msg-item">
<div class="msg-meta">
<span>От: <b>${escapeHtml(t.sender)}</b></span>
<button class="btn-sm" onclick="copyText(${JSON.stringify(t.text).replace(/"/g, '&quot;')})">Скопировать</button>
</div>
<div class="msg-text">${escapeHtml(t.text)}</div>
</div>
`).join('');
}
const mediaContainer = document.getElementById('disk-media');
if (data.media.length === 0) {
mediaContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Нет сохраненных медиафайлов</div>';
} else {
mediaContainer.innerHTML = data.media.map(m => {
const isImg = m.mime.startsWith('image/');
const fileUrl = `/ui/files/${encodeURIComponent(m.filename)}`;
return `
<div class="msg-item">
<div class="msg-meta">
<span>От: <b>${escapeHtml(m.sender)}</b></span>
<span>${(m.size / 1024).toFixed(1)} КБ</span>
</div>
<div style="font-weight:600; margin:2px 0;">
<a href="${fileUrl}" target="_blank" style="color:#60a5fa; text-decoration:none;">${escapeHtml(m.filename)} ↗</a>
</div>
<div class="path-box">Диск: ${escapeHtml(m.saved_path)}</div>
${isImg ? `<img src="${fileUrl}" class="preview-img">` : ''}
</div>
`;
}).join('');
}
} catch (e) {}
}
async function toggleTrust(ip, trust) {
const res = await fetch('/ui/trust', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_ip: ip, trust })
});
if (res.ok) {
showStatus(trust ? `Сертификат узла ${ip} заверен в ОЗУ` : `Доверие узлу ${ip} отозвано`);
fetchState();
}
}
async function sendText() {
const target_ip = document.getElementById('target-peer').value;
const input = document.getElementById('text-input');
const text = input.value.trim();
if (!target_ip) return showStatus('Выберите устройство-получатель!', true);
if (!text) return showStatus('Введите текст для отправки!', true);
const res = await fetch('/ui/send-text', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_ip, text })
});
if (res.ok) {
input.value = '';
showStatus(`Текст доставлен в ОЗУ узла ${target_ip}`);
} else {
const err = await res.text();
showStatus(err, true);
}
}
async function sendMedia() {
const target_ip = document.getElementById('target-peer').value;
const fileInput = document.getElementById('media-input');
if (!target_ip) return showStatus('Выберите устройство-получатель!', true);
if (!fileInput.files.length) return showStatus('Выберите медиафайл!', true);
const file = fileInput.files[0];
showStatus(`Отправка файла "${file.name}"...`);
const res = await fetch('/ui/send-media', {
method: 'POST',
headers: {
'X-Target-Ip': target_ip,
'X-Filename': encodeURIComponent(file.name),
'Content-Type': file.type || 'application/octet-stream'
},
body: file
});
if (res.ok) {
fileInput.value = '';
showStatus(`Файл "${file.name}" сохранен на диске узла ${target_ip}`);
} else {
const err = await res.text();
showStatus(err, true);
}
}
async function clearRam() {
await fetch('/ui/clear-ram', { method: 'POST' });
fetchState();
showStatus('Буфер текстовых сообщений в ОЗУ очищен');
}
function copyText(text) {
navigator.clipboard.writeText(text);
showStatus('Текст скопирован в буфер обмена');
}
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[s]));
}
document.getElementById('text-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendText();
}
});
setInterval(fetchState, 1500);
fetchState();
</script>
</body>
</html>