a.0.0.3
This commit is contained in:
1 parent
69e2329695
commit
8b954f757b
11 files changed
+563
-528
No files matched your search
+73
-43
@@ -3,17 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LAN Share — Текст в ОЗУ / Медиа на Диск</title>
|
||||
<title>LAN Share — Постоянный PIN | Текст в ОЗУ | Медиа на Диск</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; }
|
||||
.container { max-width: 1120px; 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; }
|
||||
.pin-badge { background: #1d4ed8; padding: 6px 14px; border-radius: 8px; font-weight: 700; letter-spacing: 1px; font-family: monospace; font-size: 1.15rem; }
|
||||
.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; }
|
||||
h2 { margin: 0; font-size: 1.08rem; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
||||
.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; }
|
||||
@@ -30,6 +30,9 @@
|
||||
.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; }
|
||||
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
|
||||
.dot-on { background: #10b981; }
|
||||
.dot-off { background: #64748b; }
|
||||
#status { grid-column: 1 / -1; padding: 10px 16px; border-radius: 8px; display: none; font-weight: 500; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -41,30 +44,33 @@
|
||||
<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 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>
|
||||
|
||||
<!-- 1. Устройства в сети + ручной ввод -->
|
||||
<!-- 1. Устройства в сети -->
|
||||
<div class="card">
|
||||
<h2>1. Устройства в сети <span class="tag tag-disk">UDP Broadcast</span></h2>
|
||||
<h2>
|
||||
<span>1. Устройства в сети</span>
|
||||
<button class="btn-sm" onclick="pingPeers()">Обновить сеть</button>
|
||||
</h2>
|
||||
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<input type="text" id="manual-ip-input" placeholder="IP устройства (напр. 192.168.1.55)">
|
||||
<input type="text" id="manual-ip-input" placeholder="IP:порт (напр. 192.168.1.55 или 192.168.1.55:8443)">
|
||||
<button class="btn-sm" style="white-space: nowrap;" onclick="addManualPeer()">Найти по IP</button>
|
||||
</div>
|
||||
|
||||
<div id="peers-list" style="display:flex; flex-direction:column; gap:8px;">
|
||||
<div style="color:var(--muted); font-size:0.9rem;">Поиск устройств через маяк 255.255.255.255:5354...</div>
|
||||
<div style="color:var(--muted); font-size:0.9rem;">Поиск устройств в локальной сети...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Панель отправки (Только по нажатию) -->
|
||||
<!-- 2. Отправка данных -->
|
||||
<div class="card">
|
||||
<h2>2. Отправка данных</h2>
|
||||
<h2>2. Отправка данных (по нажатию)</h2>
|
||||
<label style="font-size:0.85rem; color:var(--muted);">Получатель (должен быть заверен):</label>
|
||||
<select id="target-peer"><option value="">-- Выберите устройство --</option></select>
|
||||
|
||||
@@ -76,7 +82,7 @@
|
||||
<button class="btn-green" onclick="sendMedia()">Отправить медиа на диск получателя</button>
|
||||
</div>
|
||||
|
||||
<!-- 3. Принятый текст (Строго в ОЗУ) -->
|
||||
<!-- 3. Принятый текст в ОЗУ -->
|
||||
<div class="card">
|
||||
<h2>
|
||||
<span>Входящий текст <span class="tag tag-ram">ТОЛЬКО В ОЗУ</span></span>
|
||||
@@ -85,7 +91,7 @@
|
||||
<div id="ram-texts" style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Принятые медиафайлы (Сохранены на диске) -->
|
||||
<!-- 4. Принятые медиафайлы на диске -->
|
||||
<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>
|
||||
@@ -110,33 +116,43 @@ async function fetchState() {
|
||||
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-ip').textContent = `Наш адрес: ${data.my_ip}:${data.p2p_port} | ID: ${data.my_fingerprint.slice(0, 20)}...`;
|
||||
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;">Устройства пока не найдены. Введите IP вручную, если в сети отключен broadcast.</div>';
|
||||
peersContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Устройства пока не найдены. Если роутер блокирует broadcast, введите IP соседа выше — оба устройства сразу увидят друг друга.</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>
|
||||
peersContainer.innerHTML = data.peers.map(p => {
|
||||
const ipStr = p.ips.join(', ');
|
||||
const dotClass = p.online ? 'dot-on' : 'dot-off';
|
||||
return `
|
||||
<div class="peer-item">
|
||||
<div>
|
||||
<div style="font-weight:600;">
|
||||
<span class="dot ${dotClass}"></span>${escapeHtml(p.name)}
|
||||
<span style="color:var(--muted); font-size:0.85rem;">(${escapeHtml(ipStr)}:${p.port})</span>
|
||||
</div>
|
||||
<div style="font-size:0.8rem; color:var(--muted); font-family:monospace; margin-top:3px;">
|
||||
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.id}', ${!p.verified})">
|
||||
${p.verified ? 'Отозвать' : 'Заверить'}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn-sm ${p.verified ? 'btn-red' : 'btn-green'}" onclick="toggleTrust('${p.ip}', ${!p.verified})">
|
||||
${p.verified ? 'Отозвать' : 'Заверить'}
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const currentSelectKey = data.peers.map(p => `${p.ip}:${p.verified}`).join('|');
|
||||
const currentSelectKey = data.peers.map(p => `${p.id}:${p.verified}:${p.ips[0]}`).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('');
|
||||
data.peers.map(p => `<option value="${p.id}">${escapeHtml(p.name)} [${p.pin_code}] (${p.ips[0] || ''}) ${p.verified ? '✓' : '🔒 [Не заверен]'}</option>`).join('');
|
||||
if (prevVal) select.value = prevVal;
|
||||
}
|
||||
|
||||
@@ -182,19 +198,27 @@ async function fetchState() {
|
||||
|
||||
async function addManualPeer() {
|
||||
const input = document.getElementById('manual-ip-input');
|
||||
const ip = input.value.trim();
|
||||
if (!ip) return showStatus('Введите IP-адрес для поиска', true);
|
||||
const raw = input.value.trim();
|
||||
if (!raw) return showStatus('Введите IP-адрес устройства', true);
|
||||
|
||||
showStatus(`Опрос узла ${ip}...`);
|
||||
let ip = raw;
|
||||
let port = 8443;
|
||||
if (raw.includes(':')) {
|
||||
const parts = raw.split(':');
|
||||
ip = parts[0];
|
||||
port = parseInt(parts[1], 10) || 8443;
|
||||
}
|
||||
|
||||
showStatus(`Связь с узлом ${ip}:${port}...`);
|
||||
const res = await fetch('/ui/manual-peer', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ip, port: 8443 })
|
||||
body: JSON.stringify({ ip, port })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
input.value = '';
|
||||
showStatus(`Узел ${ip} успешно найден!`);
|
||||
showStatus(`Узел ${ip} подключен (оба устройства обменялись PIN-кодами)!`);
|
||||
fetchState();
|
||||
} else {
|
||||
const err = await res.text();
|
||||
@@ -202,34 +226,40 @@ async function addManualPeer() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTrust(ip, trust) {
|
||||
async function pingPeers() {
|
||||
await fetch('/ui/ping', { method: 'POST' });
|
||||
showStatus('Запущен опрос узлов сети');
|
||||
setTimeout(fetchState, 600);
|
||||
}
|
||||
|
||||
async function toggleTrust(peer_id, trust) {
|
||||
const res = await fetch('/ui/trust', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ target_ip: ip, trust })
|
||||
body: JSON.stringify({ peer_id, trust })
|
||||
});
|
||||
if (res.ok) {
|
||||
showStatus(trust ? `Сертификат узла ${ip} заверен в ОЗУ` : `Доверие узлу ${ip} отозвано`);
|
||||
showStatus(trust ? 'Сертификат устройства заверен' : 'Доверие устройству отозвано');
|
||||
fetchState();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendText() {
|
||||
const target_ip = document.getElementById('target-peer').value;
|
||||
const peer_id = document.getElementById('target-peer').value;
|
||||
const input = document.getElementById('text-input');
|
||||
const text = input.value.trim();
|
||||
if (!target_ip) return showStatus('Выберите устройство-получатель!', true);
|
||||
if (!peer_id) 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 })
|
||||
body: JSON.stringify({ peer_id, text })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
input.value = '';
|
||||
showStatus(`Текст доставлен в ОЗУ узла ${target_ip}`);
|
||||
showStatus('Текст успешно доставлен в ОЗУ получателя');
|
||||
} else {
|
||||
const err = await res.text();
|
||||
showStatus(err, true);
|
||||
@@ -237,9 +267,9 @@ async function sendText() {
|
||||
}
|
||||
|
||||
async function sendMedia() {
|
||||
const target_ip = document.getElementById('target-peer').value;
|
||||
const peer_id = document.getElementById('target-peer').value;
|
||||
const fileInput = document.getElementById('media-input');
|
||||
if (!target_ip) return showStatus('Выберите устройство-получатель!', true);
|
||||
if (!peer_id) return showStatus('Выберите устройство-получатель!', true);
|
||||
if (!fileInput.files.length) return showStatus('Выберите медиафайл!', true);
|
||||
|
||||
const file = fileInput.files[0];
|
||||
@@ -248,7 +278,7 @@ async function sendMedia() {
|
||||
const res = await fetch('/ui/send-media', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Target-Ip': target_ip,
|
||||
'X-Target-Id': peer_id,
|
||||
'X-Filename': encodeURIComponent(file.name),
|
||||
'Content-Type': file.type || 'application/octet-stream'
|
||||
},
|
||||
@@ -257,7 +287,7 @@ async function sendMedia() {
|
||||
|
||||
if (res.ok) {
|
||||
fileInput.value = '';
|
||||
showStatus(`Файл "${file.name}" сохранен на диске узла ${target_ip}`);
|
||||
showStatus(`Файл "${file.name}" сохранен на диске получателя`);
|
||||
} else {
|
||||
const err = await res.text();
|
||||
showStatus(err, true);
|
||||
|
||||
Reference in new issue
Block a user