upd
This commit is contained in:
@@ -108,7 +108,7 @@
|
||||
<div class="card rounded-xl p-5 mb-6">
|
||||
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">Добавить мангу</h2>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<textarea id="url-input" rows="2" placeholder="Один или несколько URL (каждый с новой строки) https://3.readmanga.ru/manga_slug" class="flex-1 px-3 py-2 text-sm resize-none"></textarea>
|
||||
<textarea id="url-input" rows="2" placeholder="Один или несколько URL (каждый с новой строки) https://3.readmanga.ru/manga_slug" class="flex-1 px-3 py-2 text-sm resize-none" oninput="onUrlInputChange()"></textarea>
|
||||
<div class="flex flex-col gap-2">
|
||||
<select id="fmt-select" class="px-3 py-2 text-sm">
|
||||
<option value="cbz">CBZ</option>
|
||||
@@ -116,7 +116,20 @@
|
||||
<option value="epub">EPUB</option>
|
||||
<option value="all">Все форматы</option>
|
||||
</select>
|
||||
<button onclick="addToQueue()" class="btn-primary text-sm">➕ В очередь</button>
|
||||
<button onclick="addToQueue()" id="add-btn" class="btn-primary text-sm">➕ В очередь</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Source detection hint -->
|
||||
<div id="source-hint" class="mt-2 hidden">
|
||||
<div id="source-hint-found" class="hidden text-xs text-green-400 flex items-center gap-2">
|
||||
<span>🔗 Источник:</span>
|
||||
<span id="source-hint-name" class="font-semibold"></span>
|
||||
</div>
|
||||
<div id="source-hint-unknown" class="hidden flex flex-col gap-2">
|
||||
<div class="text-xs text-yellow-400">⚠ Домен не распознан. Выберите источник вручную:</div>
|
||||
<select id="source-manual-select" class="px-3 py-2 text-sm w-full md:w-72">
|
||||
<option value="">— выберите источник —</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-msg" class="mt-2 text-sm text-green-400 hidden"></div>
|
||||
@@ -132,6 +145,8 @@
|
||||
class="px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-400 hover:text-white">🔔 Новости</button>
|
||||
<button onclick="switchTab('history')" id="tab-history"
|
||||
class="px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-400 hover:text-white">🕒 История</button>
|
||||
<button onclick="switchTab('settings')" id="tab-settings"
|
||||
class="px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-400 hover:text-white">⚙️ Настройки</button>
|
||||
</div>
|
||||
<div id="manga-filters" class="flex gap-2 py-2">
|
||||
<button onclick="filterMangas('all')" id="filter-all" class="text-xs px-3 py-1 rounded-full bg-indigo-600 text-white">Все</button>
|
||||
@@ -177,6 +192,36 @@
|
||||
<div class="px-5 py-8 text-center text-gray-500 text-sm">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div id="tab-content-settings" class="hidden">
|
||||
<div class="px-5 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-1">Источники</h3>
|
||||
<p class="text-xs text-gray-500 mb-4">Источники определяются в коде приложения. Здесь можно управлять доменами для каждого источника.</p>
|
||||
<div id="sources-list" class="flex flex-col gap-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch Source Modal -->
|
||||
<div id="switch-source-modal" class="fixed inset-0 z-[60] hidden items-center justify-center" style="background:rgba(0,0,0,0.75)">
|
||||
<div class="card rounded-2xl w-full max-w-md mx-4 p-6 flex flex-col gap-4">
|
||||
<h3 class="font-semibold text-white text-base">↔ Сменить источник</h3>
|
||||
<div class="text-sm text-gray-400" id="switch-source-current"></div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-gray-400">Новый источник</label>
|
||||
<select id="switch-source-select" class="px-3 py-2 text-sm w-full"></select>
|
||||
<div id="switch-source-warning" class="text-xs text-yellow-400 hidden"></div>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end mt-2">
|
||||
<button onclick="closeSwitchSourceModal()"
|
||||
class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white"
|
||||
style="background:#1e293b">Отмена</button>
|
||||
<button onclick="confirmSwitchSource()"
|
||||
class="px-4 py-2 rounded-lg text-sm font-semibold text-white"
|
||||
style="background:#312e81">Применить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -281,6 +326,7 @@ const state = {
|
||||
mangas: {}, // url → manga object
|
||||
chapters: {}, // manga_url → [chapter, ...]
|
||||
filter: 'all',
|
||||
sources: [], // [{id, slug, display_name, domains}]
|
||||
};
|
||||
|
||||
// ── Auth ─────────────────────────────────────
|
||||
@@ -407,8 +453,12 @@ function handleEvent(msg) {
|
||||
|
||||
case 'manga_queued':
|
||||
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: '—',
|
||||
source: srcInfo ? {id: srcInfo.id, slug: srcInfo.slug, display_name: srcInfo.display_name} : null };
|
||||
} else {
|
||||
state.mangas[msg.url].status = 'queued';
|
||||
}
|
||||
renderList();
|
||||
loadStats();
|
||||
@@ -597,6 +647,25 @@ function handleEvent(msg) {
|
||||
renderList();
|
||||
loadStats();
|
||||
break;
|
||||
|
||||
case 'source_unknown':
|
||||
_showNotification('⚠ Источник не определён для ' + (state.mangas[msg.url]?.title || msg.url) + '. Выберите источник.', 'warn');
|
||||
if(state.mangas[msg.url]) { state.mangas[msg.url].status = 'failed'; renderList(); }
|
||||
break;
|
||||
|
||||
case 'source_domain_added':
|
||||
case 'source_domain_removed':
|
||||
loadSources();
|
||||
break;
|
||||
|
||||
case 'source_switched':
|
||||
if(state.mangas[msg.url]) {
|
||||
// Обновляем source у манги из актуального списка источников
|
||||
const newSrc = state.sources.find(s => s.id === msg.new_source_id);
|
||||
if(newSrc) state.mangas[msg.url].source = {id: newSrc.id, slug: newSrc.slug, display_name: newSrc.display_name};
|
||||
updateMangaRow(msg.url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,7 +673,7 @@ function handleEvent(msg) {
|
||||
let newsUnreadCount = 0;
|
||||
|
||||
function switchTab(tab) {
|
||||
['mangas', 'news', 'history'].forEach(t => {
|
||||
['mangas', 'news', 'history', 'settings'].forEach(t => {
|
||||
document.getElementById('tab-content-'+t).classList.toggle('hidden', t !== tab);
|
||||
const btn = document.getElementById('tab-'+t);
|
||||
btn.className = t === tab
|
||||
@@ -614,6 +683,8 @@ function switchTab(tab) {
|
||||
document.getElementById('manga-filters').classList.toggle('hidden', tab !== 'mangas');
|
||||
if(tab === 'history') loadHistory();
|
||||
if(tab === 'news') { newsUnreadCount = 0; updateNewsBadge(); loadNews(); }
|
||||
if(tab === 'settings') loadSources();
|
||||
}
|
||||
}
|
||||
|
||||
function updateNewsBadge() {
|
||||
@@ -773,6 +844,66 @@ async function checkNowBtn(btn, url) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Source detection ─────────────────────────
|
||||
let _resolveTimer = null;
|
||||
let _resolvedSourceId = null; // null = found via domain, undefined = unknown
|
||||
|
||||
async function onUrlInputChange() {
|
||||
clearTimeout(_resolveTimer);
|
||||
_resolveTimer = setTimeout(_resolveSource, 400);
|
||||
}
|
||||
|
||||
async function _resolveSource() {
|
||||
const raw = document.getElementById('url-input').value.trim();
|
||||
const hint = document.getElementById('source-hint');
|
||||
const hintFound = document.getElementById('source-hint-found');
|
||||
const hintUnknown = document.getElementById('source-hint-unknown');
|
||||
|
||||
// Берём первый непустой URL
|
||||
const url = raw.split('\n').map(u=>u.trim()).filter(Boolean)[0];
|
||||
if(!url) {
|
||||
hint.classList.add('hidden');
|
||||
_resolvedSourceId = null;
|
||||
document.getElementById('add-btn').disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/resolve-source?url=' + encodeURIComponent(url));
|
||||
const data = await r.json();
|
||||
hint.classList.remove('hidden');
|
||||
|
||||
if(data.source) {
|
||||
hintFound.classList.remove('hidden');
|
||||
hintUnknown.classList.add('hidden');
|
||||
document.getElementById('source-hint-name').textContent = data.source.display_name;
|
||||
_resolvedSourceId = data.source.id;
|
||||
document.getElementById('add-btn').disabled = false;
|
||||
} else {
|
||||
hintFound.classList.add('hidden');
|
||||
hintUnknown.classList.remove('hidden');
|
||||
_resolvedSourceId = undefined; // неизвестен — нужен ручной выбор
|
||||
document.getElementById('add-btn').disabled = true;
|
||||
// Заполняем список источников
|
||||
const sel = document.getElementById('source-manual-select');
|
||||
sel.innerHTML = '<option value="">— выберите источник —</option>';
|
||||
(state.sources || []).forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.textContent = s.display_name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.onchange = () => {
|
||||
document.getElementById('add-btn').disabled = !sel.value;
|
||||
};
|
||||
}
|
||||
} catch(e) {
|
||||
hint.classList.add('hidden');
|
||||
_resolvedSourceId = null;
|
||||
document.getElementById('add-btn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── API ──────────────────────────────────────
|
||||
async function loadStats() {
|
||||
try {
|
||||
@@ -788,17 +919,35 @@ async function addToQueue() {
|
||||
const urls = raw.split('\n').map(u=>u.trim()).filter(Boolean);
|
||||
if(!urls.length) return;
|
||||
|
||||
// Определяем source_id
|
||||
let sourceId = null;
|
||||
if(_resolvedSourceId === undefined) {
|
||||
// Неизвестный домен — нужен ручной выбор
|
||||
const manualVal = document.getElementById('source-manual-select').value;
|
||||
if(!manualVal) { alert('Выберите источник для добавления манги'); return; }
|
||||
sourceId = parseInt(manualVal);
|
||||
} else if(_resolvedSourceId !== null) {
|
||||
sourceId = _resolvedSourceId;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = {urls, format: fmt};
|
||||
if(sourceId !== null) body.source_id = sourceId;
|
||||
const r = await fetch('/api/queue', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({urls, format: fmt})
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await r.json();
|
||||
const msg = document.getElementById('add-msg');
|
||||
msg.textContent = `✓ Добавлено: ${data.added.length}, уже есть: ${data.skipped.length}`;
|
||||
msg.classList.remove('hidden');
|
||||
if(data.added.length) document.getElementById('url-input').value = '';
|
||||
if(data.added.length) {
|
||||
document.getElementById('url-input').value = '';
|
||||
document.getElementById('source-hint').classList.add('hidden');
|
||||
_resolvedSourceId = null;
|
||||
document.getElementById('add-btn').disabled = false;
|
||||
}
|
||||
setTimeout(()=>msg.classList.add('hidden'), 4000);
|
||||
} catch(e) {
|
||||
alert('Ошибка: ' + e.message);
|
||||
@@ -825,6 +974,193 @@ async function resumeManga(url) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sources ───────────────────────────────────
|
||||
async function loadSources() {
|
||||
try {
|
||||
const r = await fetch('/api/sources');
|
||||
if(r.ok) {
|
||||
state.sources = await r.json();
|
||||
if(!document.getElementById('tab-content-settings').classList.contains('hidden')) {
|
||||
renderSources();
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function renderSources() {
|
||||
const container = document.getElementById('sources-list');
|
||||
if(!container) return;
|
||||
if(!state.sources.length) {
|
||||
container.innerHTML = '<div class="text-sm text-gray-500">Нет доступных источников</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = state.sources.map(s => `
|
||||
<div class="rounded-lg p-4" style="background:#0f172a;border:1px solid #1e293b">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<span class="text-sm font-semibold text-white">${escHtml(s.display_name)}</span>
|
||||
<span class="ml-2 text-xs text-gray-500">slug: ${escHtml(s.slug)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
${s.domains.map(d => `
|
||||
<span class="flex items-center gap-1 text-xs px-2 py-1 rounded" style="background:#1e293b;color:#94a3b8">
|
||||
${escHtml(d)}
|
||||
<button onclick="removeDomain(${s.id}, '${escHtml(d)}')"
|
||||
title="Удалить домен"
|
||||
style="color:#ef4444;background:none;border:none;cursor:pointer;padding:0 2px;font-size:0.8rem;line-height:1">✕</button>
|
||||
</span>
|
||||
`).join('')}
|
||||
<span id="add-domain-area-${s.id}">
|
||||
<button onclick="showAddDomain(${s.id})"
|
||||
style="font-size:0.7rem;padding:3px 8px;border-radius:4px;background:#1e293b;color:#6ee7b7;border:1px dashed #334155;cursor:pointer">
|
||||
+ домен
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function showAddDomain(sourceId) {
|
||||
const area = document.getElementById('add-domain-area-' + sourceId);
|
||||
if(!area) return;
|
||||
area.innerHTML = `
|
||||
<span class="flex items-center gap-1">
|
||||
<input id="new-domain-input-${sourceId}" type="text" placeholder="example.com"
|
||||
class="text-xs px-2 py-1 rounded" style="background:#1e293b;color:#e2e8f0;border:1px solid #334155;width:140px"
|
||||
onkeydown="if(event.key==='Enter') addDomain(${sourceId}); if(event.key==='Escape') renderSources();">
|
||||
<button onclick="addDomain(${sourceId})"
|
||||
style="font-size:0.75rem;padding:3px 8px;border-radius:4px;background:#166534;color:#86efac;cursor:pointer">✓</button>
|
||||
<button onclick="renderSources()"
|
||||
style="font-size:0.75rem;padding:3px 8px;border-radius:4px;background:#1e293b;color:#94a3b8;cursor:pointer">✕</button>
|
||||
</span>
|
||||
`;
|
||||
setTimeout(() => document.getElementById('new-domain-input-' + sourceId)?.focus(), 50);
|
||||
}
|
||||
|
||||
async function addDomain(sourceId) {
|
||||
const input = document.getElementById('new-domain-input-' + sourceId);
|
||||
if(!input) return;
|
||||
const domain = input.value.trim().toLowerCase();
|
||||
if(!domain) return;
|
||||
try {
|
||||
const r = await fetch(`/api/sources/${sourceId}/domains`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({domain}),
|
||||
});
|
||||
if(!r.ok) {
|
||||
const err = await r.json();
|
||||
_showNotification('Ошибка: ' + (err.detail || 'неизвестная ошибка'), 'error');
|
||||
return;
|
||||
}
|
||||
await loadSources();
|
||||
} catch(e) {
|
||||
_showNotification('Ошибка: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDomain(sourceId, domain) {
|
||||
if(!confirm(`Удалить домен «${domain}»?`)) return;
|
||||
try {
|
||||
const r = await fetch(`/api/sources/${sourceId}/domains/${encodeURIComponent(domain)}`, {method: 'DELETE'});
|
||||
if(!r.ok) {
|
||||
const err = await r.json();
|
||||
_showNotification('Ошибка: ' + (err.detail || 'неизвестная ошибка'), 'error');
|
||||
return;
|
||||
}
|
||||
await loadSources();
|
||||
} catch(e) {
|
||||
_showNotification('Ошибка: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Switch Source Modal ───────────────────────
|
||||
let _switchSourceUrl = null;
|
||||
|
||||
function openSwitchSourceModal(url) {
|
||||
_switchSourceUrl = url;
|
||||
const manga = state.mangas[url];
|
||||
const modal = document.getElementById('switch-source-modal');
|
||||
const sel = document.getElementById('switch-source-select');
|
||||
const warning = document.getElementById('switch-source-warning');
|
||||
|
||||
document.getElementById('switch-source-current').textContent =
|
||||
'Текущий источник: ' + (manga?.source?.display_name || 'не определён');
|
||||
|
||||
sel.innerHTML = '<option value="">— выберите источник —</option>';
|
||||
state.sources.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.textContent = s.display_name;
|
||||
if(manga?.source?.id === s.id) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
|
||||
try {
|
||||
const domain = new URL(url).hostname.replace(/^www\./, '');
|
||||
warning.textContent = `⚠ Домен «${domain}» будет перепривязан к выбранному источнику. Это затронет все манги с этого домена.`;
|
||||
warning.classList.remove('hidden');
|
||||
} catch(e) { warning.classList.add('hidden'); }
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeSwitchSourceModal() {
|
||||
_switchSourceUrl = null;
|
||||
const modal = document.getElementById('switch-source-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
async function confirmSwitchSource() {
|
||||
const url = _switchSourceUrl;
|
||||
const sourceId = parseInt(document.getElementById('switch-source-select').value);
|
||||
if(!url || !sourceId) return;
|
||||
try {
|
||||
const r = await fetch('/api/mangas/switch-source', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({url, source_id: sourceId}),
|
||||
});
|
||||
if(!r.ok) {
|
||||
const err = await r.json();
|
||||
_showNotification('Ошибка: ' + (err.detail || 'неизвестная ошибка'), 'error');
|
||||
return;
|
||||
}
|
||||
const data = await r.json();
|
||||
closeSwitchSourceModal();
|
||||
_showNotification(
|
||||
`✓ Источник изменён на «${data.source_name}»` +
|
||||
(data.chapters_reset ? `. Сброшено глав: ${data.chapters_reset}` : ''), 'ok'
|
||||
);
|
||||
if(state.mangas[url]) {
|
||||
const src = state.sources.find(s => s.id === sourceId);
|
||||
if(src) state.mangas[url].source = {id: src.id, slug: src.slug, display_name: src.display_name};
|
||||
updateMangaRow(url);
|
||||
}
|
||||
} catch(e) {
|
||||
_showNotification('Ошибка: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
const modal = document.getElementById('switch-source-modal');
|
||||
if(modal && !modal.classList.contains('hidden') && e.target === modal) closeSwitchSourceModal();
|
||||
});
|
||||
|
||||
// ── Notification helper ───────────────────────
|
||||
function _showNotification(text, type='ok') {
|
||||
const el = document.getElementById('add-msg');
|
||||
if(!el) return;
|
||||
el.textContent = text;
|
||||
el.style.color = type === 'error' ? '#f87171' : type === 'warn' ? '#fbbf24' : '#4ade80';
|
||||
el.classList.remove('hidden');
|
||||
setTimeout(() => el.classList.add('hidden'), 5000);
|
||||
}
|
||||
|
||||
// ── Delete modal ─────────────────────────────
|
||||
let _deleteUrl = null;
|
||||
let _deleteFilesChecked = false;
|
||||
@@ -1011,6 +1347,12 @@ function pubStatusPill(s) {
|
||||
return `<span class="pill pill-pub-${s}">${map[s]}</span>`;
|
||||
}
|
||||
|
||||
function _sourceBadge(source) {
|
||||
if(!source) return '<span style="font-size:0.65rem;padding:2px 6px;border-radius:4px;background:#1e293b;color:#64748b">Источник неизвестен</span>';
|
||||
if(source.slug === 'unknown') return '<span style="font-size:0.65rem;padding:2px 6px;border-radius:4px;background:#450a0a;color:#fca5a5">' + escHtml(source.display_name) + '</span>';
|
||||
return '<span style="font-size:0.65rem;padding:2px 6px;border-radius:4px;background:#0f2a1e;color:#6ee7b7">' + escHtml(source.display_name) + '</span>';
|
||||
}
|
||||
|
||||
// ── Время загрузки ────────────────────────────
|
||||
// Храним интервал живого таймера: url → intervalId
|
||||
const _timerIntervals = {};
|
||||
@@ -1126,6 +1468,7 @@ function renderMangaRow(m) {
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span data-r="status">${statusPill(m.status)}</span>
|
||||
<span data-r="pubstatus">${pubStatusPill(m.pub_status || 'unknown')}</span>
|
||||
<span data-r="source">${_sourceBadge(m.source)}</span>
|
||||
<span class="text-sm font-medium text-white truncate" data-r="title">${escHtml(m.title || m.url)}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-0.5 flex gap-3 flex-wrap">
|
||||
@@ -1170,6 +1513,11 @@ 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>`
|
||||
: ''}
|
||||
@@ -1282,6 +1630,7 @@ function _patchRow(el, m) {
|
||||
|
||||
set('status', statusPill(m.status));
|
||||
set('pubstatus', pubStatusPill(m.pub_status || 'unknown'));
|
||||
set('source', _sourceBadge(m.source));
|
||||
set('title', escHtml(m.title || m.url));
|
||||
set('chcount', `📖 ${chDone}/${chTotal} глав`);
|
||||
set('size', `💾 ${m.size_human || '—'}`);
|
||||
@@ -1661,6 +2010,7 @@ async function saveRenameFolder() {
|
||||
async function initApp() {
|
||||
_initDeleteModal();
|
||||
await loadStats();
|
||||
await loadSources();
|
||||
connectWS();
|
||||
// Загружаем список манги
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user