This commit is contained in:
2026-04-30 19:32:13 +03:00
parent 87b692ba49
commit b4e4a51ae5
4 changed files with 391 additions and 25 deletions

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Manga Downloader</title>
<link rel="icon" type="image/png" href="/static/favicon.png"/>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { background: #0f1117; color: #e2e8f0; font-family: 'Segoe UI', system-ui, sans-serif; }
@@ -61,9 +62,9 @@
<!-- Login screen -->
<div id="login-screen">
<div class="login-card">
<div class="flex items-center gap-3 mb-8 justify-center">
<span class="text-3xl">📚</span>
<h1 class="text-xl font-bold text-white">Manga Downloader</h1>
<div class="flex flex-col items-center gap-2 mb-8 justify-center">
<img src="/static/logo.png" alt="Manga Downloader" class="h-16 w-auto">
<span class="text-white font-bold text-xl tracking-wide">Manga Downloader</span>
</div>
<div class="flex flex-col gap-4">
<div>
@@ -87,8 +88,8 @@
<!-- Header -->
<header class="border-b border-gray-800 px-6 py-4 flex items-center justify-between sticky top-0 z-50" style="background:#0f1117ee;backdrop-filter:blur(12px)">
<div class="flex items-center gap-3">
<span class="text-2xl">📚</span>
<h1 class="text-xl font-bold text-white">Manga Downloader</h1>
<img src="/static/logo.png" alt="Manga Downloader" class="h-10 w-auto">
<span class="text-white font-semibold text-lg tracking-wide">Manga Downloader</span>
</div>
<div class="flex items-center gap-4">
<div id="ws-status" class="flex items-center gap-2 text-sm text-gray-400">
@@ -412,6 +413,7 @@ window.fetch = async function(...args) {
// ── WebSocket ────────────────────────────────
let ws, wsReconnectTimer;
let _pingInterval = null;
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
@@ -421,14 +423,15 @@ function connectWS() {
document.getElementById('ws-dot').className = 'w-2 h-2 rounded-full bg-green-400';
document.getElementById('ws-text').textContent = 'Подключено';
clearTimeout(wsReconnectTimer);
// Keepalive
setInterval(() => { if(ws && ws.readyState===1) ws.send('ping'); }, 20000);
// Keepalive — один интервал, предыдущий убираем
if(_pingInterval) clearInterval(_pingInterval);
_pingInterval = setInterval(() => { if(ws && ws.readyState === 1) ws.send('ping'); }, 20000);
};
ws.onclose = (e) => {
document.getElementById('ws-dot').className = 'w-2 h-2 rounded-full bg-red-500';
if(_pingInterval) { clearInterval(_pingInterval); _pingInterval = null; }
if(e.code === 4401) {
// Сессия истекла или не авторизован
document.getElementById('ws-text').textContent = 'Нет доступа';
showLoginScreen();
return;
@@ -449,6 +452,9 @@ function handleEvent(msg) {
msg.mangas.forEach(m => { state.mangas[m.url] = m; });
renderList();
loadStats();
// Дополнительно запрашиваем свежие данные с сервера — на случай если
// пока WS был отключён, статусы изменились и события были потеряны
_refreshMangaList();
break;
case 'manga_queued':
@@ -476,11 +482,15 @@ function handleEvent(msg) {
break;
case 'manga_start':
if(state.mangas[msg.url]) {
if(!state.mangas[msg.url]) {
// Манга не в state — берём свежие данные с сервера
_refreshMangaList();
} else {
state.mangas[msg.url].status = 'downloading';
if(msg.started_at) state.mangas[msg.url].started_at = msg.started_at;
state.mangas[msg.url].finished_at = null;
renderList();
}
renderList();
break;
case 'manga_info':
@@ -685,7 +695,6 @@ function switchTab(tab) {
if(tab === 'news') { newsUnreadCount = 0; updateNewsBadge(); loadNews(); }
if(tab === 'settings') loadSources();
}
}
function updateNewsBadge() {
const badge = document.getElementById('news-unread-badge');
@@ -1513,11 +1522,6 @@ function _rowButtons(m) {
title="${m.errors_count} проблем при загрузке"
style="background:#450a0a;color:#fca5a5;padding:4px 8px;border-radius:6px;font-size:0.75rem;cursor:pointer">⚠️ ${m.errors_count}</button>`
: ''}
${!isActive
? `<button onclick="event.stopPropagation(); openSwitchSourceModal('${u}')"
title="Сменить источник"
style="background:#1e3a2e;color:#6ee7b7;padding:4px 8px;border-radius:6px;font-size:0.75rem;cursor:pointer">↔ Источник</button>`
: ''}
${isActive
? `<button onclick="stopManga('${u}')" class="btn-danger" title="Остановить" style="background:#7c2d12;color:#fdba74">⏸</button>`
: ''}
@@ -1788,6 +1792,12 @@ function renderModalBody(data) {
style="background:#0c1a2e;color:#93c5fd;border:1px solid #1e3a5f">
↺ Скачать заново
</button>` : ''}
${data.status !== 'downloading' ? `
<button onclick="openSwitchSourceModal('${escHtml(data.url)}')"
class="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors"
style="background:#0f2a1e;color:#6ee7b7;border:1px solid #1e3a2e">
↔ Сменить источник
</button>` : ''}
</div>
</div>
@@ -2007,20 +2017,24 @@ async function saveRenameFolder() {
}
// ── Init ─────────────────────────────────────
async function _refreshMangaList() {
try {
const r = await fetch('/api/mangas');
if(!r.ok) return;
const mangas = await r.json();
mangas.forEach(m => { state.mangas[m.url] = m; });
renderList();
} catch(e) {}
}
async function initApp() {
_initDeleteModal();
await loadStats();
await loadSources();
connectWS();
// Загружаем список манги
try {
const r = await fetch('/api/mangas');
if(r.ok) {
const mangas = await r.json();
mangas.forEach(m => { state.mangas[m.url] = m; });
renderList();
}
} catch(e) {}
await _refreshMangaList();
// Периодически синхронизируем список манг — подстраховка от потерянных WS событий
setInterval(_refreshMangaList, 20000);
setInterval(loadStats, 15000);
}