a.0.0.4
This commit is contained in:
1 parent
5723bfc532
commit
e88dc07696
5 files changed
+613
-104
No files matched your search
+145
-29
@@ -17,13 +17,14 @@
|
||||
.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; }
|
||||
input[type="text"], 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; }
|
||||
input[type="text"], 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.92rem; }
|
||||
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:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.btn-green { background: var(--green); }
|
||||
button.btn-red { background: var(--red); }
|
||||
button.btn-sm { padding: 6px 12px; font-size: 0.82rem; }
|
||||
button.btn-sm { padding: 7px 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); }
|
||||
@@ -33,6 +34,10 @@
|
||||
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
|
||||
.dot-on { background: #10b981; }
|
||||
.dot-off { background: #64748b; }
|
||||
.progress-wrap { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.progress-bar-bg { width: 100%; height: 10px; background: #0b1120; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
|
||||
.progress-bar-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #10b981); transition: width 0.2s ease; }
|
||||
.sub-box { background: rgba(15, 23, 42, 0.55); border: 1px solid var(--border); border-radius: 8px; padding: 10px; display: flex; flex-direction: column; gap: 8px; }
|
||||
#status { grid-column: 1 / -1; padding: 10px 16px; border-radius: 8px; display: none; font-weight: 500; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -51,16 +56,36 @@
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<!-- 1. Устройства в сети -->
|
||||
<!-- 1. Устройства в сети и сканер диапазона IP -->
|
||||
<div class="card">
|
||||
<h2>
|
||||
<span>1. Устройства в сети</span>
|
||||
<button class="btn-sm" onclick="pingPeers()">Обновить сеть</button>
|
||||
<button class="btn-sm" onclick="pingPeers()">Опросить известных</button>
|
||||
</h2>
|
||||
|
||||
|
||||
<!-- Одиночное добавление по IP -->
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- Сканер диапазона IP -->
|
||||
<div class="sub-box">
|
||||
<div style="font-size: 0.82rem; color: var(--muted); font-weight: 600;">Сканер диапазона IP-адресов:</div>
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<input type="text" id="scan-range-input" style="flex: 1; min-width: 190px;" placeholder="Напр. 192.168.1.1-254 или 192.168.1.10-50">
|
||||
<button class="btn-sm" id="btn-scan-range" onclick="scanIpRange(false)">Сканировать диапазон</button>
|
||||
<button class="btn-sm btn-green" id="btn-scan-all" onclick="scanIpRange(true)">Вся подсеть (.1–254)</button>
|
||||
</div>
|
||||
<div id="scan-progress-box" style="display: none;">
|
||||
<div style="display:flex; justify-content:space-between; font-size:0.78rem; color:var(--muted); margin-bottom:4px;">
|
||||
<span id="scan-label">Сканирование...</span>
|
||||
<span id="scan-numbers">0 / 0</span>
|
||||
</div>
|
||||
<div class="progress-bar-bg">
|
||||
<div class="progress-bar-fill" id="scan-bar-fill" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="peers-list" style="display:flex; flex-direction:column; gap:8px;">
|
||||
@@ -68,7 +93,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Отправка данных -->
|
||||
<!-- 2. Отправка данных и прогресс-бар передачи -->
|
||||
<div class="card">
|
||||
<h2>2. Отправка данных (по нажатию)</h2>
|
||||
<label style="font-size:0.85rem; color:var(--muted);">Получатель (должен быть заверен):</label>
|
||||
@@ -79,7 +104,10 @@
|
||||
|
||||
<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>
|
||||
<button class="btn-green" id="send-media-btn" onclick="sendMedia()">Отправить медиа на диск получателя</button>
|
||||
|
||||
<!-- Контейнер прогресс-баров активных передач (исходящих и входящих) -->
|
||||
<div id="transfers-list" style="display:flex; flex-direction:column; gap:8px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Принятый текст в ОЗУ -->
|
||||
@@ -100,6 +128,7 @@
|
||||
|
||||
<script>
|
||||
let knownPeersKey = "";
|
||||
let defaultRangeInitialized = false;
|
||||
|
||||
function showStatus(msg, isError = false) {
|
||||
const el = document.getElementById('status');
|
||||
@@ -110,6 +139,12 @@ function showStatus(msg, isError = false) {
|
||||
setTimeout(() => { el.style.display = 'none'; }, 4000);
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return `${bytes} Б`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} МБ`;
|
||||
}
|
||||
|
||||
async function fetchState() {
|
||||
try {
|
||||
const res = await fetch('/ui/state');
|
||||
@@ -119,9 +154,61 @@ async function fetchState() {
|
||||
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;
|
||||
|
||||
// Автоподстановка подсети в поле диапазона при первой загрузке
|
||||
if (!defaultRangeInitialized && data.my_ip && data.my_ip.includes('.')) {
|
||||
const parts = data.my_ip.split('.');
|
||||
if (parts.length === 4 && parts[0] !== '127') {
|
||||
document.getElementById('scan-range-input').value = `${parts[0]}.${parts[1]}.${parts[2]}.1-254`;
|
||||
defaultRangeInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение статуса сканирования IP
|
||||
const scanBox = document.getElementById('scan-progress-box');
|
||||
const btnScanRange = document.getElementById('btn-scan-range');
|
||||
const btnScanAll = document.getElementById('btn-scan-all');
|
||||
if (data.scan && (data.scan.active || data.scan.total > 0)) {
|
||||
scanBox.style.display = 'block';
|
||||
const pct = data.scan.total > 0 ? Math.round((data.scan.scanned / data.scan.total) * 100) : 0;
|
||||
document.getElementById('scan-label').textContent = data.scan.active
|
||||
? `Сканирование ${data.scan.range}... (найдено: ${data.scan.found})`
|
||||
: `Сканирование ${data.scan.range} завершено (найдено: ${data.scan.found})`;
|
||||
document.getElementById('scan-numbers').textContent = `${data.scan.scanned} / ${data.scan.total} (${pct}%)`;
|
||||
document.getElementById('scan-bar-fill').style.width = `${pct}%`;
|
||||
btnScanRange.disabled = data.scan.active;
|
||||
btnScanAll.disabled = data.scan.active;
|
||||
}
|
||||
|
||||
// Отображение прогресс-баров передачи файлов
|
||||
const transfersContainer = document.getElementById('transfers-list');
|
||||
if (!data.transfers || data.transfers.length === 0) {
|
||||
transfersContainer.innerHTML = '';
|
||||
} else {
|
||||
transfersContainer.innerHTML = data.transfers.map(tr => {
|
||||
const dirBadge = tr.direction === 'out' ? '⬆️ Отправка на' : '⬇️ Прием от';
|
||||
const statusText = tr.done ? '✓ Завершено' : `${tr.percent}%`;
|
||||
return `
|
||||
<div class="progress-wrap">
|
||||
<div style="display:flex; justify-content:space-between; font-size:0.83rem; font-weight:600;">
|
||||
<span>${dirBadge} ${escapeHtml(tr.peer_name)}: <span style="color:#60a5fa;">${escapeHtml(tr.filename)}</span></span>
|
||||
<span>${statusText}</span>
|
||||
</div>
|
||||
<div class="progress-bar-bg">
|
||||
<div class="progress-bar-fill" style="width: ${tr.percent}%;"></div>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; font-size:0.76rem; color:var(--muted);">
|
||||
<span>${formatBytes(tr.transferred_bytes)} из ${formatBytes(tr.total_bytes)}</span>
|
||||
<span>${tr.done ? 'Сохранено на диске' : 'Передача по TLS...'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Список устройств
|
||||
const peersContainer = document.getElementById('peers-list');
|
||||
if (data.peers.length === 0) {
|
||||
peersContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Устройства пока не найдены. Если роутер блокирует broadcast, введите IP соседа выше — оба устройства сразу увидят друг друга.</div>';
|
||||
peersContainer.innerHTML = '<div style="color:var(--muted); font-size:0.9rem;">Устройства пока не найдены. Нажмите «Вся подсеть (.1–254)» или введите IP вручную.</div>';
|
||||
} else {
|
||||
peersContainer.innerHTML = data.peers.map(p => {
|
||||
const ipStr = p.ips.join(', ');
|
||||
@@ -182,7 +269,7 @@ async function fetchState() {
|
||||
<div class="msg-item">
|
||||
<div class="msg-meta">
|
||||
<span>От: <b>${escapeHtml(m.sender)}</b></span>
|
||||
<span>${(m.size / 1024).toFixed(1)} КБ</span>
|
||||
<span>${formatBytes(m.size)}</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>
|
||||
@@ -196,6 +283,28 @@ async function fetchState() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function scanIpRange(fullSubnet = false) {
|
||||
const input = document.getElementById('scan-range-input');
|
||||
const range = fullSubnet ? 'all' : input.value.trim();
|
||||
if (!fullSubnet && !range) {
|
||||
return showStatus('Введите диапазон IP (например 192.168.1.1-254 или 192.168.1.10-50)', true);
|
||||
}
|
||||
|
||||
const res = await fetch('/ui/scan-range', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ range })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showStatus('Сканирование диапазона запущено...');
|
||||
fetchState();
|
||||
} else {
|
||||
const err = await res.text();
|
||||
showStatus(err, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function addManualPeer() {
|
||||
const input = document.getElementById('manual-ip-input');
|
||||
const raw = input.value.trim();
|
||||
@@ -228,7 +337,7 @@ async function addManualPeer() {
|
||||
|
||||
async function pingPeers() {
|
||||
await fetch('/ui/ping', { method: 'POST' });
|
||||
showStatus('Запущен опрос узлов сети');
|
||||
showStatus('Запущен опрос известных узлов');
|
||||
setTimeout(fetchState, 600);
|
||||
}
|
||||
|
||||
@@ -269,28 +378,34 @@ async function sendText() {
|
||||
async function sendMedia() {
|
||||
const peer_id = document.getElementById('target-peer').value;
|
||||
const fileInput = document.getElementById('media-input');
|
||||
const sendBtn = document.getElementById('send-media-btn');
|
||||
if (!peer_id) return showStatus('Выберите устройство-получатель!', true);
|
||||
if (!fileInput.files.length) return showStatus('Выберите медиафайл!', true);
|
||||
|
||||
const file = fileInput.files[0];
|
||||
showStatus(`Отправка файла "${file.name}"...`);
|
||||
sendBtn.disabled = true;
|
||||
|
||||
const res = await fetch('/ui/send-media', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Target-Id': peer_id,
|
||||
'X-Filename': encodeURIComponent(file.name),
|
||||
'Content-Type': file.type || 'application/octet-stream'
|
||||
},
|
||||
body: file
|
||||
});
|
||||
try {
|
||||
const res = await fetch('/ui/send-media', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Target-Id': peer_id,
|
||||
'X-Filename': encodeURIComponent(file.name),
|
||||
'Content-Type': file.type || 'application/octet-stream'
|
||||
},
|
||||
body: file
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
fileInput.value = '';
|
||||
showStatus(`Файл "${file.name}" сохранен на диске получателя`);
|
||||
} else {
|
||||
const err = await res.text();
|
||||
showStatus(err, true);
|
||||
if (res.ok) {
|
||||
fileInput.value = '';
|
||||
showStatus(`Файл "${file.name}" успешно передан на диск получателя`);
|
||||
} else {
|
||||
const err = await res.text();
|
||||
showStatus(err, true);
|
||||
}
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
fetchState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +431,8 @@ document.getElementById('text-input').addEventListener('keydown', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(fetchState, 1500);
|
||||
// Опрос состояния каждые 500 мс для плавного отображения прогресс-баров
|
||||
setInterval(fetchState, 500);
|
||||
fetchState();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user