upd
This commit is contained in:
@@ -50,6 +50,8 @@
|
||||
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
|
||||
.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} }
|
||||
.meta-spinner { display:inline-block; width:12px; height:12px; border:2px solid #4f46e5; border-top-color:transparent; border-radius:50%; animation:spin 0.7s linear infinite; vertical-align:middle; }
|
||||
@keyframes spin { to { transform:rotate(360deg); } }
|
||||
::-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; }
|
||||
@@ -166,6 +168,12 @@
|
||||
|
||||
<!-- Manga List -->
|
||||
<div id="tab-content-mangas">
|
||||
<div class="px-4 py-2 border-b border-gray-800">
|
||||
<input id="manga-search" type="search" placeholder="🔍 Поиск по названию..."
|
||||
oninput="onMangaSearch(this.value)"
|
||||
class="w-full px-3 py-1.5 text-sm rounded-lg"
|
||||
style="background:#0f1117;border:1px solid #2d3148;color:#e2e8f0;outline:none">
|
||||
</div>
|
||||
<div id="manga-list" class="divide-y divide-gray-800">
|
||||
<div class="px-5 py-8 text-center text-gray-500 text-sm">Загрузка...</div>
|
||||
</div>
|
||||
@@ -380,9 +388,11 @@ const state = {
|
||||
mangas: {}, // url → manga object
|
||||
chapters: {}, // manga_url → [chapter, ...]
|
||||
filter: 'all',
|
||||
search: '',
|
||||
sources: [], // [{id, slug, display_name, domains}]
|
||||
currentUser: null, // {id, username, role}
|
||||
authWarnings: {}, // source_slug → {source_slug, source_name}
|
||||
metaUpdating: new Set(), // urls where meta refresh is in progress
|
||||
};
|
||||
|
||||
// ── Auth ─────────────────────────────────────
|
||||
@@ -539,7 +549,7 @@ function handleEvent(msg) {
|
||||
if(!state.mangas[msg.url]) {
|
||||
const srcInfo = msg.source_id ? (state.sources.find(s => s.id === msg.source_id) || null) : null;
|
||||
state.mangas[msg.url] = { url: msg.url, title: msg.url, status: 'queued', format: msg.format,
|
||||
chapters_total: 0, chapters_done: 0, size_human: '—',
|
||||
chapters_total: 0, chapters_done: 0, size_human: '0.0 Б',
|
||||
added_by: msg.added_by || null,
|
||||
added_by_username: msg.added_by_username || null,
|
||||
source: srcInfo ? {id: srcInfo.id, slug: srcInfo.slug, display_name: srcInfo.display_name} : null };
|
||||
@@ -683,8 +693,14 @@ function handleEvent(msg) {
|
||||
loadStats();
|
||||
break;
|
||||
|
||||
case 'meta_refresh_started':
|
||||
state.metaUpdating.add(msg.url);
|
||||
_updateMetaBtn(msg.url);
|
||||
break;
|
||||
|
||||
case 'meta_refreshed':
|
||||
// Ничего не делаем визуально — файлы обновлены на диске
|
||||
state.metaUpdating.delete(msg.url);
|
||||
_updateMetaBtn(msg.url, msg.failed === -1 ? 'error' : 'done');
|
||||
break;
|
||||
|
||||
case 'manga_meta_updated':
|
||||
@@ -1623,34 +1639,56 @@ async function confirmDelete() {
|
||||
loadStats();
|
||||
}
|
||||
|
||||
async function refreshMeta(url) {
|
||||
const r = await fetch('/api/mangas/refresh_meta?url='+encodeURIComponent(url), {method:'POST'});
|
||||
if(r.ok) {
|
||||
const btn = document.querySelector(`[data-refresh-url="${CSS.escape(url)}"]`);
|
||||
if(btn) { btn.textContent = '✓'; btn.disabled = true; setTimeout(() => { btn.textContent = '🏷'; btn.disabled = false; }, 2000); }
|
||||
function _updateMetaBtn(url, result) {
|
||||
const btn = document.getElementById('modal-refresh-meta-btn');
|
||||
if(!btn) return;
|
||||
const inProgress = state.metaUpdating.has(url);
|
||||
if(inProgress) {
|
||||
btn.innerHTML = '<span class="meta-spinner"></span> Обновляем...';
|
||||
btn.disabled = true;
|
||||
btn.style.color = '#94a3b8';
|
||||
btn.style.borderColor = '#334155';
|
||||
} else if(result === 'done') {
|
||||
btn.innerHTML = '✅ Готово';
|
||||
btn.disabled = false;
|
||||
btn.style.color = '#4ade80';
|
||||
btn.style.borderColor = '#166534';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = '🏷 Обновить метатеги';
|
||||
btn.style.color = '#a78bfa';
|
||||
btn.style.borderColor = '#312e81';
|
||||
}, 2500);
|
||||
} else if(result === 'error') {
|
||||
btn.innerHTML = '❌ Ошибка';
|
||||
btn.disabled = false;
|
||||
btn.style.color = '#f87171';
|
||||
btn.style.borderColor = '#7f1d1d';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = '🏷 Обновить метатеги';
|
||||
btn.style.color = '#a78bfa';
|
||||
btn.style.borderColor = '#312e81';
|
||||
}, 3000);
|
||||
} else {
|
||||
btn.innerHTML = '🏷 Обновить метатеги';
|
||||
btn.disabled = false;
|
||||
btn.style.color = '#a78bfa';
|
||||
btn.style.borderColor = '#312e81';
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMetaModal(url) {
|
||||
const btn = document.getElementById('modal-refresh-meta-btn');
|
||||
if(btn) { btn.textContent = '⏳ Обновляем...'; btn.disabled = true; }
|
||||
async function refreshMeta(url) {
|
||||
const r = await fetch('/api/mangas/refresh_meta?url='+encodeURIComponent(url), {method:'POST'});
|
||||
if(btn) {
|
||||
if(r.ok) {
|
||||
btn.textContent = '✅ Метатеги обновлены';
|
||||
btn.style.color = '#4ade80';
|
||||
btn.style.borderColor = '#166534';
|
||||
setTimeout(() => {
|
||||
btn.textContent = '🏷 Обновить метатеги';
|
||||
btn.disabled = false;
|
||||
btn.style.color = '#a78bfa';
|
||||
btn.style.borderColor = '#312e81';
|
||||
}, 2500);
|
||||
} else {
|
||||
btn.textContent = '❌ Ошибка';
|
||||
btn.disabled = false;
|
||||
}
|
||||
if(!r.ok) return;
|
||||
// state будет обновлён через WS meta_refresh_started
|
||||
}
|
||||
|
||||
async function refreshMetaModal(url) {
|
||||
const r = await fetch('/api/mangas/refresh_meta?url='+encodeURIComponent(url), {method:'POST'});
|
||||
if(!r.ok) {
|
||||
const btn = document.getElementById('modal-refresh-meta-btn');
|
||||
if(btn) { btn.innerHTML = '❌ Ошибка'; }
|
||||
}
|
||||
// Спиннер появится через WS meta_refresh_started, исчезнет через meta_refreshed
|
||||
}
|
||||
|
||||
async function forceRedownload(url, closeModalAfter = false) {
|
||||
@@ -1943,6 +1981,12 @@ function _rowAuto(m) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let _searchTimer = null;
|
||||
function onMangaSearch(val) {
|
||||
clearTimeout(_searchTimer);
|
||||
_searchTimer = setTimeout(() => { state.search = val.trim().toLowerCase(); renderList(); }, 120);
|
||||
}
|
||||
|
||||
function _sortedMangas() {
|
||||
let mangas = Object.values(state.mangas);
|
||||
if(state.filter === 'ongoing') {
|
||||
@@ -1950,6 +1994,14 @@ function _sortedMangas() {
|
||||
} else if(state.filter !== 'all') {
|
||||
mangas = mangas.filter(m => m.status === state.filter);
|
||||
}
|
||||
if(state.search) {
|
||||
const q = state.search;
|
||||
mangas = mangas.filter(m =>
|
||||
(m.title || '').toLowerCase().includes(q) ||
|
||||
(m.title_ru || '').toLowerCase().includes(q) ||
|
||||
(m.title_full || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
const order = {downloading: 0, queued: 1, stopped: 2, failed: 3, done: 4};
|
||||
mangas.sort((a, b) => {
|
||||
const oa = order[a.status] ?? 2, ob = order[b.status] ?? 2;
|
||||
|
||||
Reference in New Issue
Block a user