upd
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user