This commit is contained in:
2026-04-30 17:45:16 +03:00
parent 77592c9a55
commit 88bf301b60
6 changed files with 477 additions and 28 deletions

View File

@@ -50,22 +50,53 @@
.pulse-dot { width:8px; height:8px; border-radius:50%; background:#3b82f6; animation:pulse 1.5s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:#0f1117; } ::-webkit-scrollbar-thumb { background:#2d3148; border-radius:3px; }
/* Login screen */
#login-screen { position:fixed; inset:0; z-index:9999; background:#0f1117; display:flex; align-items:center; justify-content:center; }
#login-screen.hidden { display:none; }
.login-card { background:#1a1d2e; border:1px solid #2d3148; border-radius:16px; padding:40px; width:100%; max-width:380px; }
</style>
</head>
<body class="min-h-screen">
<!-- 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>
<div class="flex flex-col gap-4">
<div>
<label class="text-xs text-gray-400 mb-1 block">Логин</label>
<input id="login-input" type="text" autocomplete="username"
class="w-full px-3 py-2 rounded-lg text-sm text-white border border-gray-700 focus:border-indigo-500 outline-none"
style="background:#0f1117" placeholder="Логин">
</div>
<div>
<label class="text-xs text-gray-400 mb-1 block">Пароль</label>
<input id="password-input" type="password" autocomplete="current-password"
class="w-full px-3 py-2 rounded-lg text-sm text-white border border-gray-700 focus:border-indigo-500 outline-none"
style="background:#0f1117" placeholder="Пароль">
</div>
<div id="login-error" class="text-sm text-red-400 hidden"></div>
<button id="login-btn" onclick="doLogin()" class="btn-primary w-full mt-2">Войти</button>
</div>
</div>
</div>
<!-- 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>
</div>
<div class="flex items-center gap-4">
<div id="ws-status" class="flex items-center gap-2 text-sm text-gray-400">
<div class="w-2 h-2 rounded-full bg-gray-600" id="ws-dot"></div>
<span id="ws-text">Подключение...</span>
<div class="flex items-center gap-4">
<div id="ws-status" class="flex items-center gap-2 text-sm text-gray-400">
<div class="w-2 h-2 rounded-full bg-gray-600" id="ws-dot"></div>
<span id="ws-text">Подключение...</span>
</div>
<button id="logout-btn" onclick="doLogout()" class="hidden text-xs text-gray-500 hover:text-gray-300 px-3 py-1 rounded-lg transition-colors" style="background:#1e293b">Выйти</button>
</div>
</div>
</header>
<div class="max-w-7xl mx-auto px-4 py-6">
@@ -252,6 +283,87 @@ const state = {
filter: 'all',
};
// ── Auth ─────────────────────────────────────
function showLoginScreen() {
document.getElementById('login-screen').classList.remove('hidden');
document.getElementById('logout-btn').classList.add('hidden');
// Закрываем WS если открыт
if(ws) { try { ws.close(); } catch(_){} ws = null; }
}
function hideLoginScreen() {
document.getElementById('login-screen').classList.add('hidden');
document.getElementById('logout-btn').classList.remove('hidden');
}
async function checkAuth() {
try {
const r = await fetch('/api/auth/check');
const data = await r.json();
if(!data.auth_enabled) { hideLoginScreen(); return true; }
if(data.authenticated) { hideLoginScreen(); return true; }
showLoginScreen();
return false;
} catch(e) {
showLoginScreen();
return false;
}
}
async function doLogin() {
const btn = document.getElementById('login-btn');
const err = document.getElementById('login-error');
const login = document.getElementById('login-input').value.trim();
const password = document.getElementById('password-input').value;
err.classList.add('hidden');
btn.disabled = true; btn.textContent = 'Входим...';
try {
const r = await fetch('/api/login', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({login, password}),
});
if(!r.ok) {
const d = await r.json().catch(()=>({}));
err.textContent = d.detail || 'Неверный логин или пароль';
err.classList.remove('hidden');
return;
}
hideLoginScreen();
await initApp();
} catch(e) {
err.textContent = 'Ошибка сети';
err.classList.remove('hidden');
} finally {
btn.disabled = false; btn.textContent = 'Войти';
}
}
async function doLogout() {
await fetch('/api/logout', {method:'POST'}).catch(()=>{});
showLoginScreen();
document.getElementById('login-input').value = '';
document.getElementById('password-input').value = '';
// Сбрасываем состояние
Object.keys(state.mangas).forEach(k => delete state.mangas[k]);
Object.keys(state.chapters).forEach(k => delete state.chapters[k]);
document.getElementById('stats-row').innerHTML = '';
document.getElementById('manga-list').innerHTML = '';
}
// Глобальный перехват 401
const _origFetch = window.fetch;
window.fetch = async function(...args) {
const r = await _origFetch.apply(this, args);
if(r.status === 401) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
if(!url.includes('/api/auth/check') && !url.includes('/api/login')) {
showLoginScreen();
}
}
return r;
};
// ── WebSocket ────────────────────────────────
let ws, wsReconnectTimer;
@@ -264,11 +376,17 @@ function connectWS() {
document.getElementById('ws-text').textContent = 'Подключено';
clearTimeout(wsReconnectTimer);
// Keepalive
setInterval(() => { if(ws.readyState===1) ws.send('ping'); }, 20000);
setInterval(() => { if(ws && ws.readyState===1) ws.send('ping'); }, 20000);
};
ws.onclose = () => {
ws.onclose = (e) => {
document.getElementById('ws-dot').className = 'w-2 h-2 rounded-full bg-red-500';
if(e.code === 4401) {
// Сессия истекла или не авторизован
document.getElementById('ws-text').textContent = 'Нет доступа';
showLoginScreen();
return;
}
document.getElementById('ws-text').textContent = 'Переподключение...';
wsReconnectTimer = setTimeout(connectWS, 3000);
};
@@ -625,6 +743,36 @@ async function checkNow(url) {
await fetch('/api/mangas/check_now?url='+encodeURIComponent(url), {method:'POST'});
}
async function checkNowBtn(btn, url) {
if(btn.disabled) return;
btn.disabled = true;
const orig = btn.textContent;
btn.textContent = '⏳';
btn.style.color = '#fbbf24';
try {
const r = await fetch('/api/mangas/check_now?url='+encodeURIComponent(url), {method:'POST'});
if(r.ok) {
btn.textContent = '✓';
btn.style.color = '#4ade80';
setTimeout(() => {
btn.textContent = orig;
btn.style.color = '';
btn.disabled = false;
}, 2500);
} else {
throw new Error();
}
} catch {
btn.textContent = '✕';
btn.style.color = '#f87171';
setTimeout(() => {
btn.textContent = orig;
btn.style.color = '';
btn.disabled = false;
}, 2000);
}
}
// ── API ──────────────────────────────────────
async function loadStats() {
try {
@@ -1046,7 +1194,10 @@ function _rowAuto(m) {
<span class="toggle-slider"></span>
</label>
<span>Авто</span>
${autoOn ? `<button onclick="checkNow('${u}')" class="text-indigo-400 hover:text-indigo-300 text-xs">↻</button>` : ''}
<button onclick="event.stopPropagation(); checkNowBtn(this, '${u}')"
title="Проверить новые главы сейчас"
class="text-indigo-400 hover:text-white transition-colors px-1 rounded"
style="line-height:1">↻</button>
</div>`;
}
@@ -1507,21 +1658,36 @@ async function saveRenameFolder() {
}
// ── Init ─────────────────────────────────────
async function init() {
async function initApp() {
_initDeleteModal();
await loadStats();
connectWS();
// Загружаем список манги
try {
const r = await fetch('/api/mangas');
const mangas = await r.json();
mangas.forEach(m => { state.mangas[m.url] = m; });
renderList();
if(r.ok) {
const mangas = await r.json();
mangas.forEach(m => { state.mangas[m.url] = m; });
renderList();
}
} catch(e) {}
setInterval(loadStats, 15000);
}
document.addEventListener('DOMContentLoaded', init);
async function init() {
const ok = await checkAuth();
if(ok) await initApp();
}
document.addEventListener('DOMContentLoaded', () => {
init();
// Enter в полях логина
['login-input','password-input'].forEach(id => {
document.getElementById(id).addEventListener('keydown', e => {
if(e.key === 'Enter') doLogin();
});
});
});
// Закрытие модалки по клику снаружи
document.getElementById('modal').addEventListener('click', function(e) {