Читалка для Litnet в стиле Author.Today с обходом защиты от копирования и встроенным экспортом в FB2.
// ==UserScript==
// @name Litnet Reader & FB2 Exporter
// @description Читалка для Litnet в стиле Author.Today с обходом защиты от копирования и встроенным экспортом в FB2.
// @match https://litnet.com/*/reader/*
// @noframes
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect *
// @require https://update.greasyfork.org/scripts/468831/1575776/HTML2FB2Lib.js
// @run-at document-idle
// @version 0.5
// @namespace https://greasyfork.org/users/789838
// ==/UserScript==
(function() {
'use strict';
if (window.top !== window.self) return;
const defaultSettings = {
themeStep: 1,
fontSize: 22,
lineHeight: 1.6,
textWidth: 900,
fontFamily: 'Roboto',
textColor: '#333333',
bgColor: '#ffffff',
hyphens: false
};
let userSettings = JSON.parse(localStorage.getItem('at-reader-settings')) || defaultSettings;
if (userSettings.themeStep === undefined) userSettings.themeStep = 1;
if (userSettings.hyphens === undefined) userSettings.hyphens = false;
const urlPathParts = window.location.pathname.split('?')[0].split('/');
const bookSlug = urlPathParts[urlPathParts.length - 1];
const fallbackIdMatch = bookSlug.match(/-b(\d+)$/);
let bookMeta = {
bookSlug: bookSlug,
idBook: fallbackIdMatch ? fallbackIdMatch[1] : null,
inLibrary: false,
isLiked: false,
likeCount: 0,
tags: [],
genres: [],
rawStats: [],
title: document.title,
headerTitle: document.title,
author: 'Автор неизвестен',
authorLink: '#',
annotation: '',
cover: '',
url: window.location.origin + window.location.pathname.replace(/\/reader\//, '/book/').split('?')[0]
};
let tocHtml = '';
let currentChapterTitle = '';
let nextChapterUrl = '';
let prevChapterUrl = '';
let nextChapterTitle = '';
let prevChapterTitle = '';
let availableChapters = [];
if (typeof FB2Image !== 'undefined') {
FB2Image.prototype._load = async function(url, params) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url.toString(),
responseType: "blob",
onload: (r) => {
if (r.status >= 200 && r.status < 300) resolve(r.response);
else reject(new Error("HTTP " + r.status));
},
onerror: reject
});
});
};
}
function getTodayString() {
return new Date().toLocaleDateString('ru-RU');
}
function cleanOldCaches() {
const today = getTodayString();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('at-chap-')) {
try {
const data = JSON.parse(localStorage.getItem(key));
if (data.date !== today) {
localStorage.removeItem(key);
}
} catch(e) {}
}
}
}
function loadChapterCache(chapterId, currentLastEdit) {
cleanOldCaches();
try {
const data = JSON.parse(localStorage.getItem(`at-chap-${chapterId}`));
if (data && data.date === getTodayString()) {
if (currentLastEdit && data.lastEdit && currentLastEdit !== data.lastEdit) {
console.log("Litnet Reader: Обнаружено обновление главы, сброс кэша.");
localStorage.removeItem(`at-chap-${chapterId}`);
return null;
}
if (data.totalPages && data.totalPages > 1 && window.atTotalChapterPages === 1) {
window.atTotalChapterPages = data.totalPages;
}
return data.pages;
}
} catch(e) {}
return null;
}
function saveChapterCache(chapterId, pagesObj) {
try {
localStorage.setItem(`at-chap-${chapterId}`, JSON.stringify({
date: getTodayString(),
lastEdit: window.atCurrentLastEdit || '',
totalPages: window.atTotalChapterPages || 1,
pages: pagesObj
}));
} catch(e) {
console.warn('Litnet Reader: LocalStorage переполнен, очистка старых данных');
}
}
function fixNativeModals() {
const modals = ['complaint-modal', 'reward-author-modal-dialog', 'modal_user_no_book', 'modal_user_no_lib', 'modal_no_user'];
modals.forEach(id => {
const el = document.getElementById(id);
if (el && el.parentElement !== document.body) {
document.body.appendChild(el);
}
});
GM_addStyle(`
.modal-backdrop { z-index: 2147483640 !important; visibility: visible !important; pointer-events: auto !important; display: block !important; }
.modal { z-index: 2147483645 !important; visibility: visible !important; pointer-events: auto !important; }
.bootbox { z-index: 2147483646 !important; visibility: visible !important; pointer-events: auto !important; }
`);
}
function killAntiCopy() {
const stopProp = (e) => {
if (e.target && e.target.closest && (e.target.closest('.modal') || e.target.closest('.modal-backdrop') || e.target.closest('.bootbox') || e.target.closest('#reward-author-modal-dialog') || e.target.closest('#complaint-modal'))) {
return;
}
e.stopPropagation();
};
['selectstart', 'mousedown', 'mouseup', 'copy', 'contextmenu', 'dragstart'].forEach(evt => {
window.addEventListener(evt, stopProp, true);
document.addEventListener(evt, stopProp, true);
});
GM_addStyle(`
* {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
}
`);
}
function cleanChapterHtml(rawHtml, expectedTitle) {
if (!rawHtml) return '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = rawHtml;
tempDiv.querySelectorAll('.reader-pagination, .pagination, .js-pagination, .clearfix, script, style, .reader-btn-w, .audio_btn_prompt').forEach(el => el.remove());
let contentEl = tempDiv.querySelector('.jsReaderText, [data-test-id="reader-text"]');
let contentHtml = contentEl ? contentEl.innerHTML : tempDiv.innerHTML;
const cleanDiv = document.createElement('div');
cleanDiv.innerHTML = contentHtml;
let headings = cleanDiv.querySelectorAll('h1, h2, h3');
headings.forEach(heading => {
if (expectedTitle && heading.textContent.trim().toLowerCase() === expectedTitle.toLowerCase()) {
heading.remove();
}
});
return cleanDiv.innerHTML.replace(/onmousedown="[^"]*"/gi, '').trim();
}
function extractData() {
let foundTotalPages = false;
window.atTotalChapterPages = 1;
let serverPage = 1;
const idBookEl = document.querySelector('.book-id');
if (idBookEl) {
bookMeta.idBook = idBookEl.textContent.trim();
}
const rightBlockGenres = document.querySelectorAll('.jsAddTargetBlank a[href*="/top/"]');
if (rightBlockGenres.length > 0) {
bookMeta.genres = Array.from(rightBlockGenres).map(a => a.textContent.trim());
}
const likeBtn = document.querySelector('.likes-btn, .not-likes-btn');
if (likeBtn) {
bookMeta.isLiked = likeBtn.classList.contains('likes-btn');
const countEl = likeBtn.querySelector('.count');
if (countEl) bookMeta.likeCount = parseInt(countEl.textContent.trim(), 10) || 0;
}
const libIn = document.querySelector('.to_lib.in-lib');
bookMeta.inLibrary = !!libIn;
window.atCurrentLastEdit = '';
const editEl = document.querySelector('.last-edit');
if (editEl) {
window.atCurrentLastEdit = editEl.textContent.trim();
} else {
const htmlMatch = document.body.innerHTML.match(/Отредактировано:\s*[\d\.]+/i);
if (htmlMatch) window.atCurrentLastEdit = htmlMatch[0];
}
const urlParams = new URLSearchParams(window.location.search);
let urlPageParam = parseInt(urlParams.get('p'), 10);
if (isNaN(urlPageParam)) urlPageParam = null;
window.atCurrentPage = urlPageParam || 1;
window.atCurrentChapterId = urlParams.get('c');
window.atBaseUrl = window.location.pathname.split('?')[0];
let chapterHtml = '';
try {
const ngStateScript = document.getElementById('ng-state');
if (ngStateScript) {
const stateStr = ngStateScript.textContent;
const tpMatch = stateStr.match(/"totalPages"\s*:\s*(\d+)/);
if (tpMatch) {
window.atTotalChapterPages = parseInt(tpMatch[1], 10);
foundTotalPages = true;
}
const stateData = JSON.parse(stateStr);
for (const key in stateData) {
const obj = stateData[key]?.body || stateData[key]?.data || stateData[key];
if (obj && obj.idBook) bookMeta.idBook = obj.idBook;
if (obj && obj.bookId) bookMeta.idBook = obj.bookId;
if (obj && obj.bookInLib !== undefined) bookMeta.inLibrary = obj.bookInLib;
if (obj && obj.totalPages !== undefined) {
window.atTotalChapterPages = parseInt(obj.totalPages, 10);
foundTotalPages = true;
if (obj.data && typeof obj.data === 'string') {
chapterHtml = cleanChapterHtml(obj.data, obj.chapterTitle);
}
if (obj.page !== undefined) serverPage = parseInt(obj.page, 10);
if (obj.chapterTitle) currentChapterTitle = obj.chapterTitle;
}
if (obj && obj.author) {
if (obj.author.name) bookMeta.author = obj.author.name;
}
if (obj && obj.title) bookMeta.title = obj.title;
if (obj && obj.chapters && Array.isArray(obj.chapters) && availableChapters.length === 0) {
obj.chapters.forEach((ch, idx) => {
availableChapters.push({
idx: idx + 1,
id: ch.id.toString(),
title: ch.title,
url: window.atBaseUrl + '?c=' + ch.id
});
});
}
}
}
} catch (e) {}
if (!chapterHtml) {
const textContainer = document.querySelector('.jsReaderText, [data-test-id="reader-text"]');
if (textContainer) {
chapterHtml = cleanChapterHtml(textContainer.parentElement.innerHTML, currentChapterTitle);
}
}
if (!foundTotalPages) {
let maxPage = window.atTotalChapterPages || 1;
document.querySelectorAll('.pagination a, .js-pagination a, a[href*="&p="], a[href*="?p="]').forEach(a => {
const match = a.href.match(/[?&]p=(\d+)/);
if (match) maxPage = Math.max(maxPage, parseInt(match[1], 10));
});
document.querySelectorAll('[onclick*="Reader.goTo"]').forEach(el => {
const match = el.getAttribute('onclick').match(/Reader\.goTo\((\d+)\)/);
if (match) maxPage = Math.max(maxPage, parseInt(match[1], 10));
});
window.atTotalChapterPages = maxPage;
}
if (!chapterHtml || chapterHtml.length < 50) return false;
const authorDomLinks = document.querySelectorAll('.book-page-author__about, .sa-name, a[href*="-u"]');
for (let a of authorDomLinks) {
if (a.href && !a.href.includes('search')) {
bookMeta.authorLink = a.href;
bookMeta.author = a.textContent.replace(/Автор\s*•.*$/i, '').replace(/Автор\s*книг.*$/i, '').trim();
break;
}
}
if (!bookMeta.authorLink || bookMeta.authorLink === '#') {
const langPrefix = window.location.pathname.split('/')[1] || 'ru';
bookMeta.authorLink = `/${langPrefix}/search?q=${encodeURIComponent(bookMeta.author)}`;
}
let rawTitle = bookMeta.title || document.title;
rawTitle = rawTitle.replace(/^Книга\s+/i, '').split(/,(?:\s*глава)?/i)[0].split(/\s*—/)[0].split(/\|/)[0].trim();
const h1BookTitle = document.querySelector('h1.book-heading a, h1.book-heading');
if (h1BookTitle) {
rawTitle = h1BookTitle.textContent.trim();
}
bookMeta.title = rawTitle;
bookMeta.headerTitle = rawTitle;
window.atRequestedPage = urlPageParam || serverPage || 1;
window.atCurrentPage = window.atRequestedPage;
window.atChapterPagesCache = loadChapterCache(window.atCurrentChapterId, window.atCurrentLastEdit) || {};
if (serverPage === window.atRequestedPage && !window.atChapterPagesCache[serverPage]) {
window.atChapterPagesCache[serverPage] = chapterHtml;
}
if (availableChapters.length === 0) {
const chapterSelect = document.querySelector('select.js-chapter-change');
if (chapterSelect) {
if (!window.atCurrentChapterId) window.atCurrentChapterId = chapterSelect.value;
const options = Array.from(chapterSelect.options);
options.forEach((opt, index) => {
availableChapters.push({
idx: index + 1,
id: opt.value,
title: opt.text,
url: window.atBaseUrl + '?c=' + opt.value
});
});
}
}
availableChapters.forEach((ch, index) => {
const isActive = ch.id === window.atCurrentChapterId;
tocHtml += `<a href="${ch.url}" class="at-toc-item ${isActive ? 'active' : ''}">${ch.title}</a>`;
if (isActive) {
currentChapterTitle = currentChapterTitle || ch.title;
if (index > 0) {
prevChapterUrl = availableChapters[index - 1].url;
prevChapterTitle = availableChapters[index - 1].title;
}
if (index < availableChapters.length - 1) {
nextChapterUrl = availableChapters[index + 1].url;
nextChapterTitle = availableChapters[index + 1].title;
}
}
});
return true;
}
async function fetchBookExtraInfo() {
const cacheKey = `at-book-meta-${bookMeta.bookSlug}`;
const cachedDataStr = localStorage.getItem(cacheKey);
if (cachedDataStr) {
try {
const cachedData = JSON.parse(cachedDataStr);
let cacheValid = true;
if (cachedData.genres && cachedData.genres.some(g => /\d/.test(g) || g.includes('из'))) {
cacheValid = false;
}
if (cacheValid && cachedData.cover && cachedData.author && cachedData.author !== 'Автор неизвестен') {
Object.assign(bookMeta, cachedData);
if (!bookMeta.genres) bookMeta.genres = [];
updateSidebarUI();
return;
}
} catch (e) {}
}
try {
const response = await fetch(bookMeta.url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const tagEls = doc.querySelectorAll('.view-book-page-header-desktop__tags-tag span, .lib-tag span');
if (tagEls.length > 0) bookMeta.tags = Array.from(tagEls).map(el => el.textContent.trim()).filter((v, i, a) => a.indexOf(v) === i);
if (!bookMeta.genres || bookMeta.genres.length === 0) {
let genreEls = doc.querySelectorAll('.book-info a[href*="/top/"], .view-book-page-header-desktop__details a[href*="/top/"], .jsAddTargetBlank a[href*="/top/"]');
if (genreEls.length === 0) {
genreEls = doc.querySelectorAll('a[href*="/top/"]');
}
let parsedGenres = [];
Array.from(genreEls).forEach(el => {
if (el.closest('.dropdown, .menu, nav, header, footer, .lib-dropdown__content')) return;
let text = el.textContent.replace(/^\s*\d+\s*(?:[-–—]\s*)?/, '').replace(/\s*\(из.*?\)\s*/i, '').trim();
if (text) parsedGenres.push(text);
});
if (parsedGenres.length > 0) {
bookMeta.genres = Array.from(new Set(parsedGenres)).slice(0, 5);
}
}
const descEl = doc.querySelector('.lib-description__description');
if (descEl) bookMeta.annotation = descEl.innerHTML.trim();
const statEls = doc.querySelectorAll('.view-book-page-header-desktop__labels .lib-label');
if (statEls.length > 0) {
bookMeta.rawStats = Array.from(statEls).map(el => {
const iconEl = el.querySelector('img.lib-icon__img');
let icon = '';
if (iconEl && iconEl.src) {
if (iconEl.src.includes('star')) icon = '⭐';
else if (iconEl.src.includes('eye')) icon = '👁️';
else if (iconEl.src.includes('like')) icon = '❤️';
}
return icon ? `${icon} ${el.querySelector('.lib-label__text')?.textContent?.trim() || ''}` : '';
}).filter(Boolean);
}
const authorEl = doc.querySelector('.book-page-author__about, .sa-name, a.view-book-page-header-desktop__author, a.lib-author__name, a[href*="-u"]');
if (authorEl && authorEl.href && !authorEl.href.includes('search')) {
bookMeta.authorLink = authorEl.href;
const nameEl = authorEl.querySelector('.book-page-author__about-info-name');
if (nameEl) {
bookMeta.author = nameEl.textContent.trim();
} else {
bookMeta.author = authorEl.textContent.replace(/Автор\s*•.*$/i, '').replace(/Автор\s*книг.*$/i, '').trim();
}
}
const ogImage = doc.querySelector('meta[property="og:image"]');
if (ogImage && ogImage.content) bookMeta.cover = ogImage.content;
localStorage.setItem(cacheKey, JSON.stringify({
tags: bookMeta.tags, genres: bookMeta.genres, annotation: bookMeta.annotation, rawStats: bookMeta.rawStats,
author: bookMeta.author, authorLink: bookMeta.authorLink, cover: bookMeta.cover
}));
updateSidebarUI();
} catch (e) {}
}
async function fetchChapterPage(chapterId, pageNum, expectedTitle) {
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
const bodyParams = new URLSearchParams();
bodyParams.append('chapterId', chapterId);
bodyParams.append('page', pageNum);
if (csrfToken) bodyParams.append('_csrf', csrfToken);
const apiRes = await fetch('/reader/get-page', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: bodyParams.toString()
});
if (apiRes.ok) {
const json = await apiRes.json();
if (json && json.status === 1 && json.data) {
if (json.idBook) bookMeta.idBook = json.idBook;
if (json.bookInLib !== undefined) bookMeta.inLibrary = json.bookInLib;
let html = cleanChapterHtml(json.data, expectedTitle);
if (html.includes('<p>')) {
return { html: html, totalPages: json.totalPages || 1 };
}
}
}
} catch (e) {
console.warn("API fetch error, using fallback", e);
}
const url = window.atBaseUrl + (window.atBaseUrl.includes('?') ? '&' : '?') + 'c=' + chapterId + '&p=' + pageNum;
return new Promise((resolve, reject) => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
let checkInterval;
let timeout = setTimeout(() => {
clearInterval(checkInterval);
iframe.remove();
reject(new Error("Таймаут загрузки страницы"));
}, 10000);
iframe.src = url;
checkInterval = setInterval(() => {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
if (doc) {
const textContainer = doc.querySelector('.jsReaderText, [data-test-id="reader-text"]');
if (textContainer && textContainer.querySelectorAll('p').length > 0) {
clearInterval(checkInterval);
clearTimeout(timeout);
let maxPage = 1;
doc.querySelectorAll('.pagination a, .js-pagination a, a[href*="&p="], a[href*="?p="]').forEach(a => {
const m = a.href.match(/[?&]p=(\d+)/);
if (m) maxPage = Math.max(maxPage, parseInt(m[1], 10));
});
doc.querySelectorAll('[onclick*="Reader.goTo"]').forEach(el => {
const m = el.getAttribute('onclick').match(/Reader\.goTo\((\d+)\)/);
if (m) maxPage = Math.max(maxPage, parseInt(m[1], 10));
});
let html = cleanChapterHtml(textContainer.parentElement.innerHTML, expectedTitle);
iframe.remove();
resolve({ html: html, totalPages: maxPage });
}
}
} catch (e) {}
}, 100);
});
}
function injectStyles() {
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Alegreya:wght@400;500;700&family=Roboto:wght@400;500;700&family=Comfortaa:wght@400;700&display=swap');
:root {
--at-bg-color: ${userSettings.bgColor};
--at-text-color: ${userSettings.textColor};
--at-font-size: ${userSettings.fontSize}px;
--at-line-height: ${userSettings.lineHeight};
--at-text-width: ${userSettings.textWidth === '100%' ? '100%' : userSettings.textWidth + 'px'};
--at-font-family: '${userSettings.fontFamily}', sans-serif;
--at-hyphens: ${userSettings.hyphens ? 'auto' : 'none'};
}
.ln_topbar, .wrap, footer, .main_footer, .cookies-w, .pwa-banner, .pwa-banner-wrapper {
position: fixed !important;
top: -9999px !important;
left: -9999px !important;
visibility: hidden !important;
pointer-events: none !important;
z-index: -1 !important;
}
html, body { background-color: var(--at-bg-color) !important; margin: 0 !important; padding: 0 !important; width: 100% !important; min-height: 100vh !important; }
#at-app { display: flex; flex-direction: column; min-height: 100vh; font-family: 'Roboto', sans-serif; background-color: var(--at-bg-color); transition: background-color 0.2s ease; }
#at-header { position: fixed; top: 0; left: 0; right: 0; min-height: 50px; background: #ffffff; border-bottom: 1px solid #e9ecef; display: flex; align-items: center; justify-content: space-between; padding: 5px 20px; z-index: 1030; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: transform 0.3s ease !important; }
#at-header.hidden { transform: translateY(-100%) !important; }
.at-header-group { display: flex; gap: 10px; align-items: center; }
.at-btn { background: #f8f9fa; border: 1px solid #ced4da; border-radius: 4px; padding: 6px 12px; cursor: pointer; color: #495057; font-size: 14px; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 6px; transition: all 0.2s; font-weight: 500; outline: none; white-space: nowrap; }
.at-btn:hover { background: #e2e6ea; color: #212529; text-decoration: none; }
.at-btn.btn-action { background: #e0f2fe; color: #0284c7; border-color: #bae6fd; }
.at-btn.btn-action:hover { background: #bae6fd; }
.at-btn-icon { padding: 6px 8px; font-size: 0; line-height: 0; }
.at-btn-icon svg { width: 18px; height: 18px; }
.at-btn-icon.active { color: #e11d48; border-color: #f43f5e; background: #ffe4e6; }
.at-btn-icon.lib-active { color: #0284c7; border-color: #bae6fd; background: #e0f2fe; }
.at-divider { width: 1px; height: 24px; background: #ced4da; margin: 0 5px; }
.at-header-title { font-weight: 700; color: #333; font-size: 15px; text-align: center; max-width: 100%; line-height:1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
#at-sidebar-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 1034; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; }
#at-sidebar-overlay.visible { opacity: 1; pointer-events: auto; }
#at-sidebar { position: fixed; top: 0; left: -340px; bottom: 0; width: 340px; background: #fff; box-shadow: 2px 0 12px rgba(0,0,0,0.15); transition: left 0.3s ease; z-index: 1035; overflow-y: auto; }
#at-sidebar.open { left: 0; }
.at-sidebar-close { position: absolute; top: 15px; right: 15px; background: none; border: none; font-size: 28px; color: #999; cursor: pointer; line-height: 1; padding: 0; outline: none; transition: color 0.2s; }
.at-sidebar-close:hover { color: #333; }
.at-sidebar-inner { padding: 40px 20px 20px 20px; display: flex; flex-direction: column; gap: 15px; }
.at-meta-cover { width: 100%; max-width: 200px; margin: 0 auto; border-radius: 6px; display: block; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.at-meta-title { font-size: 18px; font-weight: 700; color: #333; text-align: center; margin: 0; line-height: 1.3;}
.at-meta-author { font-size: 14px; font-weight: bold; color: #4582af; text-align: center; display: block; text-decoration: none; transition: color 0.2s; }
.at-meta-author:hover { color: #0284c7; text-decoration: underline; }
.at-meta-stats { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; font-size: 12px; color: #555; background: #f8f9fa; padding: 10px; border-radius: 6px; }
.at-meta-stats span { background: #e9ecef; padding: 3px 8px; border-radius: 12px; font-weight: 500; }
.at-meta-tags { display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
.at-meta-tag { background: #e0f2fe; color: #0284c7; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: bold;}
.at-meta-genres { display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; color: #d9534f; font-size: 12px; font-weight: bold; }
.at-meta-anno { font-size: 13px; color: #555; line-height: 1.5; background: #fdfdfd; padding: 10px; border: 1px dashed #eee; border-radius: 4px; max-height: 250px; overflow-y: auto; text-align: justify; }
.at-toc-title { font-size: 15px; font-weight: 700; color: #555; text-transform: uppercase; border-bottom: 2px solid #f0f0f0; padding-bottom: 5px; margin-top: 10px; }
.at-toc-list { display: flex; flex-direction: column; gap: 2px; }
.at-toc-item { display: block; padding: 8px 12px; color: #333; text-decoration: none; border-radius: 4px; font-size: 14px; transition: background 0.2s;}
.at-toc-item.active { background: #e9ecef; font-weight: 700; border-left: 3px solid #4582af; padding-left: 9px; }
#at-settings-panel { position: fixed; top: 50px; right: 20px; width: 320px; background: #fff; border: 1px solid #ced4da; border-top: none; border-radius: 0 0 6px 6px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); padding: 20px; display: none; flex-direction: column; gap: 15px; z-index: 1035; }
#at-settings-panel.open { display: flex; }
.at-setting-row { display: flex; flex-direction: column; gap: 5px; }
.at-setting-row label { font-size: 13px; font-weight: 600; color: #555; display: flex; justify-content: space-between; align-items: center;}
.at-slider { width: 100%; cursor: pointer; accent-color: #4582af; }
.at-select, .at-color { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; outline: none; }
.at-checkbox { width: 18px; height: 18px; cursor: pointer; accent-color: #4582af; }
#at-main { flex: 1; margin-top: 50px; padding: 60px 20px 80px 20px; display: flex; flex-direction: column; align-items: center; }
#at-text-content { width: 100%; max-width: var(--at-text-width); box-sizing: border-box; font-family: var(--at-font-family); font-size: var(--at-font-size); line-height: var(--at-line-height); color: var(--at-text-color); text-align: justify; -webkit-hyphens: var(--at-hyphens); hyphens: var(--at-hyphens); transition: color 0.2s ease; }
.at-chapter-heading { text-align: center; font-weight: 400; font-size: 1.6em; margin-top: 0; margin-bottom: 1.5em; color: var(--at-text-color); }
#at-text-content p { text-indent: 1.5em; margin-top: 0; margin-bottom: 0.5em; }
.at-page-block { position: relative; display: block; }
.at-page-separator { display: none; }
@keyframes at-spin { 100% { transform: rotate(360deg); } }
.at-spinner { width: 40px; height: 40px; margin: 0 auto 15px auto; border: 4px solid #f3f3f3; border-top: 4px solid #4582af; border-radius: 50%; animation: at-spin 1s linear infinite; }
.at-loader-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100px 20px; color: #777; font-size: 16px; font-weight: 500; font-family: 'Roboto', sans-serif;}
.at-pagination { width: 100%; max-width: var(--at-text-width); display: flex; justify-content: space-between; margin-top: 60px; padding-top: 20px; border-top: 1px solid rgba(128,128,128,0.3); }
.at-pagination a { color: #4582af; padding: 8px 16px; border: 1px solid #4582af; border-radius: 4px; text-decoration: none; font-family: 'Roboto', sans-serif;}
.ate-dlg-overlay { position:fixed; top:0; left:0; bottom:0; right:0; background-color:rgba(0,0,0,0.5); z-index:1055; display:flex; align-items:center; justify-content:center; opacity:0; pointer-events:none; transition: opacity 0.2s;}
.ate-dlg-overlay.open { opacity:1; pointer-events:auto; }
.ate-dialog { background:#fff; width: 90%; max-width: 550px; height: 80vh; max-height: 600px; border-radius: 6px; display:flex; flex-direction:column; box-shadow:0 5px 15px rgba(0,0,0,0.5); overflow:hidden; font-family: 'Roboto', sans-serif; }
.ate-title { flex: 0 0 auto; display:flex; align-items:center; justify-content:space-between; padding: 12px 15px; background: #edf1f2; border-bottom: 1px solid #e5e5e5; color: #66757f; font-weight:bold; font-size: 15px;}
.ate-close-btn { cursor:pointer; background:none; border:none; font-size:24px; line-height:1; color:#000; opacity:0.4; padding:0; outline:none; transition: opacity 0.2s; }
.ate-close-btn:hover { opacity:0.9; }
.ate-form-body { display:flex; flex-direction:column; flex:1; overflow:hidden; padding: 15px; }
.ate-page { display:flex; flex-direction:column; flex:1; overflow:hidden; gap: 15px; }
.ate-page.hidden { display: none !important; }
.ate-fieldset { border: 1px solid #bbb; border-radius: 6px; padding: 10px; margin: 0; display:flex; flex-direction:column; flex:1; overflow:hidden;}
.ate-legend { font-size: 13px; font-weight:bold; color: #333; margin: 0; padding: 0 5px; width:auto; border:none; line-height:1;}
.ate-note { font-size: 12px; color: #66757f; margin-bottom: 10px; line-height:1.4; }
.ate-chapter-list { flex:1; overflow-y:auto; border: 1px solid #eee; padding: 5px; border-radius:4px; display:flex; flex-direction:column; gap:4px; }
.ate-toolbar { display:flex; align-items:center; justify-content:space-between; padding-top:10px; border-top:1px solid #bbb; margin-top:10px; font-size: 13px; color:#333; }
.ate-group-select { background:none; border:1px solid #ccc; border-radius:4px; padding:4px 8px; cursor:pointer; font-size:12px; color:#555; }
.ate-settings-list { display:flex; flex-direction:column; gap: 8px; margin-top: 10px; }
.ate-checkbox-wrap { display:flex; align-items:center; font-size: 13px; color: #333; cursor:pointer; gap:8px;}
.ate-checkbox-wrap input { width: 16px; height: 16px; cursor:pointer; accent-color: #4582af; }
.ate-log { flex:1; overflow-y:auto; background:#1e1e1e; color:#ccc; font-family:monospace; font-size:13px; padding:10px; border-radius:4px; line-height:1.5; white-space:pre-wrap; border: 1px solid #333; }
.ate-buttons { display:flex; gap: 10px; justify-content:flex-end; border-top: 1px solid #eee; padding-top: 15px; margin-top: auto; }
.ate-btn-main { background: #5cb85c; color: #fff; border: 1px solid #4cae4c; border-radius:4px; padding: 8px 16px; cursor:pointer; font-weight:500; font-size:14px; outline:none; transition:0.2s;}
.ate-btn-main:hover { background: #449d44; }
.ate-btn-main:disabled { background: #a5d8a5; cursor:not-allowed; }
.ate-btn-alt { background: #fff; color: #333; border: 1px solid #ccc; border-radius:4px; padding: 8px 16px; cursor:pointer; font-size:14px; outline:none; transition:0.2s;}
`);
}
function updateSidebarUI() {
const metaContainer = document.getElementById('at-meta-container');
if (!metaContainer) return;
let statsHtml = '';
if (bookMeta.rawStats && bookMeta.rawStats.length > 0) {
statsHtml = `<div class="at-meta-stats">${bookMeta.rawStats.map(s => `<span>${s}</span>`).join('')}</div>`;
}
let genresHtml = '';
if (bookMeta.genres && bookMeta.genres.length > 0) {
genresHtml = `<div class="at-meta-genres">` + bookMeta.genres.join(', ') + `</div>`;
}
let tagsHtml = '';
if (bookMeta.tags && bookMeta.tags.length > 0) {
tagsHtml = `<div class="at-meta-tags">` + bookMeta.tags.map(t => `<span class="at-meta-tag">${t}</span>`).join('') + `</div>`;
}
metaContainer.innerHTML = `
${bookMeta.cover ? `<img src="${bookMeta.cover}" class="at-meta-cover">` : ''}
<h2 class="at-meta-title">${bookMeta.title}</h2>
<a href="${bookMeta.authorLink}" target="_self" class="at-meta-author" id="at-author-link">${bookMeta.author}</a>
${genresHtml}
${statsHtml}
${tagsHtml}
${bookMeta.annotation ? `<div class="at-meta-anno">${bookMeta.annotation}</div>` : ''}
`;
const authorLinkEl = document.getElementById('at-author-link');
if (authorLinkEl) {
authorLinkEl.onclick = (e) => {
e.stopPropagation();
window.location.href = bookMeta.authorLink;
};
}
}
function renderInterface() {
document.body.style.background = 'none';
let themeName = 'Белая';
switch(userSettings.themeStep) {
case 1: themeName = 'Белая'; break;
case 2: themeName = 'Сепия'; break;
case 3: themeName = 'Серая'; break;
case 4: themeName = 'Темная'; break;
case 5: themeName = 'Черная'; break;
}
const app = document.createElement('div');
app.id = 'at-app';
app.innerHTML = `
<header id="at-header">
<div class="at-header-group">
<button type="button" id="at-btn-toc" class="at-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line></svg>
Оглавление
</button>
<a href="${bookMeta.url}" class="at-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>
К книге
</a>
<button type="button" id="at-btn-download" class="at-btn btn-action">Скачать FB2</button>
</div>
<div class="at-header-group" style="flex: 1; justify-content: center; overflow: hidden; padding: 0 15px;">
<span class="at-header-title" title="${bookMeta.headerTitle}">${bookMeta.headerTitle}</span>
</div>
<div class="at-header-group">
<button type="button" id="at-btn-like" class="at-btn at-btn-icon ${bookMeta.isLiked ? 'active' : ''}" title="Мне нравится">
<svg width="18" height="18" viewBox="0 0 24 24" fill="${bookMeta.isLiked ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
<span id="at-like-count" style="margin-left:5px; font-size:13px; font-weight:bold;">${bookMeta.likeCount > 0 ? bookMeta.likeCount : ''}</span>
</button>
<button type="button" id="at-btn-library" class="at-btn at-btn-icon" title="Добавить в библиотеку"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg></button>
<button type="button" id="at-btn-reward" class="at-btn at-btn-icon" title="Наградить автора"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 12 20 22 4 22 4 12"></polyline><rect x="2" y="7" width="20" height="5"></rect><line x1="12" y1="22" x2="12" y2="7"></line><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path></svg></button>
<button type="button" id="at-btn-complain" class="at-btn at-btn-icon" title="Пожаловаться"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg></button>
<div class="at-divider"></div>
<button type="button" id="at-btn-refresh" class="at-btn" title="Очистить кэш и обновить главу">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.59-9.21L21.5 8"></path></svg>
</button>
<button type="button" id="at-btn-settings" class="at-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</button>
</div>
</header>
<div id="at-sidebar-overlay"></div>
<aside id="at-sidebar">
<button type="button" id="at-btn-close-sidebar" class="at-sidebar-close" title="Закрыть">×</button>
<div class="at-sidebar-inner">
<div id="at-meta-container">
<h2 class="at-meta-title">${bookMeta.title}</h2>
<span style="color:#777; font-size:12px; text-align:center; display:block;">Загрузка информации...</span>
</div>
<div class="at-toc-title">Оглавление</div>
<div class="at-toc-list">${tocHtml}</div>
</div>
</aside>
<div id="at-settings-panel">
<div class="at-setting-row">
<label>Тема (Фон) <span id="at-val-theme">${themeName}</span></label>
<input type="range" class="at-slider" id="at-inp-theme" min="1" max="5" step="1" value="${userSettings.themeStep}">
</div>
<div class="at-setting-row">
<label>Размер шрифта <span id="at-val-fs">${userSettings.fontSize}px</span></label>
<input type="range" class="at-slider" id="at-inp-fs" min="14" max="36" value="${userSettings.fontSize}">
</div>
<div class="at-setting-row">
<label>Ширина текста <span id="at-val-tw">${userSettings.textWidth === '100%' ? 'Макс' : userSettings.textWidth + 'px'}</span></label>
<input type="range" class="at-slider" id="at-inp-tw" min="500" max="1400" step="50" value="${userSettings.textWidth === '100%' ? 1400 : userSettings.textWidth}">
</div>
<div class="at-setting-row">
<label>Высота строк <span id="at-val-lh">${userSettings.lineHeight}</span></label>
<input type="range" class="at-slider" id="at-inp-lh" min="1.0" max="2.5" step="0.1" value="${userSettings.lineHeight}">
</div>
<div class="at-setting-row">
<label>Перенос слов
<input type="checkbox" class="at-checkbox" id="at-inp-hy" ${userSettings.hyphens ? 'checked' : ''}>
</label>
</div>
<div class="at-setting-row">
<label>Шрифт</label>
<select class="at-select" id="at-inp-ff">
<option value="Roboto" ${userSettings.fontFamily === 'Roboto' ? 'selected' : ''}>Roboto</option>
<option value="Alegreya" ${userSettings.fontFamily === 'Alegreya' ? 'selected' : ''}>Alegreya</option>
<option value="Comfortaa" ${userSettings.fontFamily === 'Comfortaa' ? 'selected' : ''}>Comfortaa</option>
<option value="Arial" ${userSettings.fontFamily === 'Arial' ? 'selected' : ''}>Arial</option>
<option value="Georgia" ${userSettings.fontFamily === 'Georgia' ? 'selected' : ''}>Georgia</option>
</select>
</div>
<div class="at-flex-row" style="display:flex; gap:10px;">
<div class="at-setting-row" style="flex:1;">
<label>Текст</label>
<input type="color" class="at-color" id="at-inp-tc" value="${userSettings.textColor}">
</div>
<div class="at-setting-row" style="flex:1;">
<label>Фон</label>
<input type="color" class="at-color" id="at-inp-bc" value="${userSettings.bgColor}">
</div>
</div>
</div>
<main id="at-main">
<div id="at-text-content" lang="ru"></div>
<div class="at-pagination">
${prevChapterUrl ? `<a href="${prevChapterUrl}">← <span>${prevChapterTitle}</span></a>` : '<div></div>'}
${nextChapterUrl ? `<a href="${nextChapterUrl}"><span>${nextChapterTitle}</span> →</a>` : '<div></div>'}
</div>
</main>
<div id="ate-overlay" class="ate-dlg-overlay">
<div class="ate-dialog">
<div class="ate-title"><span>Формирование файла FB2</span><button id="ate-close" class="ate-close-btn">×</button></div>
<div class="ate-form-body">
<div id="ate-page-1" class="ate-page">
<fieldset class="ate-fieldset">
<legend class="ate-legend">Главы для выгрузки</legend>
<div class="ate-note">Выберите главы для скачивания. Все внутренние страницы глав будут загружены автоматически.</div>
<div class="ate-chapter-list" id="ate-chapter-container"></div>
<div class="ate-toolbar">
<span>Выбрано глав: <strong id="ate-selected-count">0</strong> из <strong id="ate-total-count">0</strong></span>
<button id="ate-select-all" class="ate-group-select">✔ Выделить всё/ничего</button>
</div>
</fieldset>
<div class="ate-settings-list">
<label class="ate-checkbox-wrap"><input type="checkbox" id="ate-chk-notes" checked> Добавить аннотацию книги</label>
<label class="ate-checkbox-wrap"><input type="checkbox" id="ate-chk-cover" checked> Вшить обложку книги</label>
<label class="ate-checkbox-wrap"><input type="checkbox" id="ate-chk-img" checked> Загружать иллюстрации в главах</label>
</div>
</div>
<div id="ate-page-2" class="ate-page hidden"><div id="ate-log-container" class="ate-log"></div></div>
<div class="ate-buttons">
<button id="ate-btn-cancel" class="ate-btn-alt">Закрыть</button>
<button id="ate-btn-action" class="ate-btn-main">Продолжить</button>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(app);
bindEvents();
}
async function toggleLibraryState() {
if (!bookMeta.idBook) {
alert('ID книги не найден. Попробуйте обновить страницу.');
return;
}
bookMeta.inLibrary = !bookMeta.inLibrary;
updateLibraryIcon();
const langPrefix = window.location.pathname.split('/')[1] || 'ru';
const url = bookMeta.inLibrary ? `/${langPrefix}/book/to-library` : `/${langPrefix}/book/from-library`;
const bodyParams = new URLSearchParams();
bodyParams.append('bookId', bookMeta.idBook);
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
if (csrfToken) bodyParams.append('_csrf', csrfToken);
try {
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: bodyParams.toString()
});
} catch (e) {
console.error(e);
bookMeta.inLibrary = !bookMeta.inLibrary;
updateLibraryIcon();
}
}
function updateLibraryIcon() {
const btn = document.getElementById('at-btn-library');
if (!btn) return;
if (bookMeta.inLibrary) {
btn.classList.add('lib-active');
btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>`;
btn.title = 'В библиотеке (Нажмите, чтобы удалить)';
} else {
btn.classList.remove('lib-active');
btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>`;
btn.title = 'Добавить в библиотеку';
}
}
function updateLikeIcon() {
const btnLike = document.getElementById('at-btn-like');
const countSpan = document.getElementById('at-like-count');
if (!btnLike) return;
if (bookMeta.isLiked) {
btnLike.classList.add('active');
btnLike.querySelector('svg').setAttribute('fill', 'currentColor');
} else {
btnLike.classList.remove('active');
btnLike.querySelector('svg').setAttribute('fill', 'none');
}
if (countSpan) countSpan.textContent = bookMeta.likeCount > 0 ? bookMeta.likeCount : '';
}
function bindDownloadModalEvents() {
const overlay = document.getElementById('ate-overlay');
const btnOpen = document.getElementById('at-btn-download');
const btnCloseX = document.getElementById('ate-close');
const btnCancel = document.getElementById('ate-btn-cancel');
const btnAction = document.getElementById('ate-btn-action');
const page1 = document.getElementById('ate-page-1');
const page2 = document.getElementById('ate-page-2');
const chapterContainer = document.getElementById('ate-chapter-container');
const selectedCount = document.getElementById('ate-selected-count');
const totalCount = document.getElementById('ate-total-count');
const btnSelectAll = document.getElementById('ate-select-all');
const logContainer = document.getElementById('ate-log-container');
let isDownloading = false;
let generatedBlobUrl = null;
let generatedFilename = '';
totalCount.textContent = availableChapters.length;
availableChapters.forEach((ch) => {
const row = document.createElement('label');
row.className = 'ate-checkbox-wrap';
row.style.padding = '4px';
row.style.borderBottom = '1px solid #f5f5f5';
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.className = 'ate-chapter-chk';
chk.checked = true;
chk.dataset.id = ch.id;
chk.dataset.idx = ch.idx;
row.appendChild(chk);
row.appendChild(document.createTextNode(` ${ch.idx}. ${ch.title}`));
chapterContainer.appendChild(row);
});
const updateCount = () => {
const checked = document.querySelectorAll('.ate-chapter-chk:checked').length;
selectedCount.textContent = checked;
btnAction.disabled = checked === 0;
};
chapterContainer.addEventListener('change', updateCount);
updateCount();
btnSelectAll.onclick = () => {
const checkboxes = document.querySelectorAll('.ate-chapter-chk');
const allChecked = Array.from(checkboxes).every(c => c.checked);
checkboxes.forEach(c => c.checked = !allChecked);
updateCount();
};
const closeModal = () => {
if (isDownloading) return;
overlay.classList.remove('open');
setTimeout(() => {
page1.classList.remove('hidden');
page2.classList.add('hidden');
logContainer.innerHTML = '';
btnAction.textContent = 'Продолжить';
btnAction.style.background = '';
btnCancel.style.display = '';
}, 300);
};
btnOpen.onclick = () => overlay.classList.add('open');
btnCloseX.onclick = closeModal;
btnCancel.onclick = closeModal;
overlay.onclick = (e) => { if (e.target === overlay) closeModal(); };
function writeLog(text, color = '#ccc') {
const line = document.createElement('div');
line.style.color = color;
line.textContent = text;
logContainer.appendChild(line);
logContainer.scrollTop = logContainer.scrollHeight;
return line;
}
btnAction.onclick = async () => {
if (btnAction.textContent === 'Продолжить') {
if (typeof FB2Document === 'undefined') {
alert('Библиотека конвертации FB2 не загрузилась. Проверьте @require.');
return;
}
page1.classList.add('hidden');
page2.classList.remove('hidden');
btnCancel.style.display = 'none';
btnAction.textContent = 'Прервать';
btnAction.style.background = '#d9534f';
isDownloading = true;
writeLog('Подготовка структуры FB2...', '#5bc0de');
let safeAuthor = 'Автор';
try {
const doc = new FB2Document();
doc.bookTitle = bookMeta.title || 'Неизвестная книга';
if (typeof FB2GenreList !== 'undefined' && Array.isArray(FB2GenreList._keys)) {
const litnetKeys = [
[ "sf_fantasy", "бояръ-аниме", ["бояръ"] ],
[ "fairy_fantasy", "уся", ["wuxia", "азиатское"] ],
[ "sf_fantasy", "бытовое фэнтези", ["бытовое"] ],
[ "sf_fantasy", "магическая академия", ["академия"] ],
[ "sf_fantasy", "темное фэнтези", ["темное"] ],
[ "love_sf", "любовное фэнтези", ["мистический любовный", "мистический любовный роман"] ],
[ "love_erotica", "эротическое фэнтези", ["эротическая фантастика"] ],
[ "love_contemporary", "служебный роман", ["служебный"] ],
[ "love_contemporary", "студенческий роман", ["студенческий"] ],
[ "love_hard", "жесткая эротика", ["жесткая"] ],
[ "love_detective", "остросюжетный любовный", ["криминальный любовный"] ],
[ "sf_detective", "магический детектив", ["магический"] ],
[ "child_prose", "молодежная проза", ["молодежная", "подростковая"] ],
[ "popadanec", "попаданцы в другие миры", ["попаданцы во времени", "попаданцы"] ],
[ "fanfiction", "аниме фанфики", ["фанфики по", "манга фанфики"] ],
[ "thriller", "мистический триллер", ["психологический триллер", "политический триллер", "криминальный триллер"] ],
[ "sf_horror", "хоррор", ["готика", "фолк-хоррор", "научно-фантастический хоррор"] ],
[ "sf_mystic", "мистика", ["паранормальное"] ],
[ "sf_fantasy_city", "городское фэнтези", ["городское"] ]
];
litnetKeys.forEach(lk => {
if (!FB2GenreList._keys.some(k => k[1] === lk[1])) {
FB2GenreList._keys.push(lk);
}
});
}
if (bookMeta.genres && bookMeta.genres.length > 0) {
doc.genres = [ bookMeta.genres.join(", ") ];
} else {
doc.genres = ["novel"];
}
safeAuthor = (bookMeta.author || 'Автор').replace(/[\/\\?%*:|"<>]/g, '').trim();
doc.bookAuthors.push(new FB2Author(safeAuthor));
doc.sourceURL = bookMeta.url;
if (bookMeta.annotation && document.getElementById('ate-chk-notes').checked) {
const li = writeLog('Парсинг аннотации... ');
try {
doc.bindParser("a", new FB2AnnotationParser());
const dom = new DOMParser().parseFromString(bookMeta.annotation, "text/html");
doc.parse("a", dom.body);
li.innerHTML += '<span style="color:#5cb85c">ok</span>';
} catch(e) {
li.innerHTML += `<span style="color:#d9534f">ошибка (${e.message})</span>`;
} finally {
doc.bindParser();
}
}
if (bookMeta.cover && document.getElementById('ate-chk-cover').checked) {
const li = writeLog('Скачивание обложки... ');
try {
const img = new FB2Image(bookMeta.cover);
await img.load();
img.id = "cover" + img.suffix();
doc.coverpage = img;
doc.binaries.push(img);
li.innerHTML += '<span style="color:#5cb85c">ok</span>';
} catch(e) {
li.innerHTML += '<span style="color:#d9534f">ошибка</span>';
}
}
writeLog('---');
doc.bindParser("c", new FB2ChapterParser());
const selectedChks = document.querySelectorAll('.ate-chapter-chk:checked');
for (let i = 0; i < selectedChks.length; i++) {
if (!isDownloading) break;
const chk = selectedChks[i];
const ch = availableChapters.find(c => c.id === chk.dataset.id);
const li = writeLog(`Загрузка ${i+1}/${selectedChks.length}: ${ch.title}... `);
try {
let chapterFullHtml = '';
let chTotalPages = 1;
const cachePages = loadChapterCache(ch.id) || {};
if (ch.id === window.atCurrentChapterId && cachePages[1] && cachePages[1] !== 'loading') {
chapterFullHtml += cachePages[1];
chTotalPages = window.atTotalChapterPages;
} else {
const data1 = await fetchChapterPage(ch.id, 1, ch.title);
chapterFullHtml += data1.html;
chTotalPages = data1.totalPages;
}
for (let p = 2; p <= chTotalPages; p++) {
if (!isDownloading) break;
li.innerHTML = `Загрузка ${i+1}/${selectedChks.length}: ${ch.title} (стр ${p}/${chTotalPages})... `;
if (ch.id === window.atCurrentChapterId && cachePages[p] && cachePages[p] !== 'loading') {
chapterFullHtml += cachePages[p];
} else {
const pageData = await fetchChapterPage(ch.id, p, null);
chapterFullHtml += pageData.html;
await new Promise(r => setTimeout(r, 50));
}
}
if (chapterFullHtml && chapterFullHtml.trim().length > 0) {
const chDoc = new DOMParser().parseFromString(chapterFullHtml, "text/html");
const tempDiv = chDoc.createElement('div');
tempDiv.innerHTML = chapterFullHtml;
doc.parse("c", tempDiv, ch.title);
li.innerHTML = `Загрузка ${i+1}/${selectedChks.length}: ${ch.title}... <span style="color:#5cb85c">ok</span>`;
} else {
li.innerHTML = `Загрузка ${i+1}/${selectedChks.length}: ${ch.title}... <span style="color:#d9534f">ошибка (пусто)</span>`;
}
} catch (e) {
li.innerHTML = `Загрузка ${i+1}/${selectedChks.length}: ${ch.title}... <span style="color:#d9534f">ошибка (${e.message})</span>`;
}
}
doc.bindParser();
if (isDownloading && document.getElementById('ate-chk-img').checked) {
const chapterImages = doc.binaries.filter(bin => bin instanceof FB2Image && bin !== doc.coverpage && !bin.value);
if (chapterImages.length > 0) {
const li = writeLog(`Скачивание иллюстраций (${chapterImages.length} шт)... `);
let loadedCount = 0;
for (let img of chapterImages) {
if (!isDownloading) break;
try {
await img.load();
loadedCount++;
} catch(e) {}
}
li.innerHTML += `<span style="color:#5cb85c">ok (${loadedCount}/${chapterImages.length})</span>`;
}
}
if (isDownloading) {
writeLog('---');
writeLog('Сборка и очистка FB2 файла...', '#f0ad4e');
let xmlString = doc.toString();
xmlString = xmlString.replace(/<program-used>.*?<\/program-used>/gi, '');
xmlString = xmlString.replace(/<first-name>Ox90<\/first-name>/gi, `<first-name>${safeAuthor}</first-name>`);
const blob = new Blob([xmlString], { type: 'application/x-fictionbook+xml;charset=utf-8' });
generatedBlobUrl = URL.createObjectURL(blob);
let safeTitle = (bookMeta.title || 'Книга').replace(/[\/\\?%*:|"<>]/g, '').trim();
generatedFilename = `${safeAuthor}. ${safeTitle}`;
if (selectedChks.length === 1) {
generatedFilename += `. Глава ${selectedChks[0].dataset.idx}`;
} else if (selectedChks.length > 1 && selectedChks.length < availableChapters.length) {
generatedFilename += `. Главы ${selectedChks[0].dataset.idx}-${selectedChks[selectedChks.length - 1].dataset.idx}`;
}
writeLog('Готово! Файл можно скачивать.', '#5cb85c');
btnAction.textContent = 'Сохранить в файл';
btnAction.style.background = '#5cb85c';
isDownloading = false;
}
} catch (fatalErr) {
writeLog(`Критическая ошибка: ${fatalErr.message}`, '#d9534f');
console.error(fatalErr);
isDownloading = false;
btnAction.textContent = 'Закрыть';
btnAction.style.background = '';
}
}
else if (btnAction.textContent === 'Прервать') {
isDownloading = false;
writeLog('Операция прервана!', '#d9534f');
btnAction.textContent = 'Закрыть';
btnAction.style.background = '';
}
else if (btnAction.textContent === 'Сохранить в файл') {
if (generatedBlobUrl) {
const downloadLink = document.createElement('a');
downloadLink.href = generatedBlobUrl;
downloadLink.download = `${generatedFilename}.fb2`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
closeModal();
}
else if (btnAction.textContent === 'Закрыть') {
closeModal();
}
};
}
async function buildFullChapter(forceRefresh = false) {
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
const contentContainer = document.getElementById('at-text-content');
contentContainer.innerHTML = `
<div class="at-loader-container">
<div class="at-spinner"></div>
<div id="at-loader-text">Подготовка главы (всего страниц: ${window.atTotalChapterPages})...</div>
</div>
`;
if (forceRefresh) {
localStorage.removeItem(`at-chap-${window.atCurrentChapterId}`);
window.atChapterPagesCache = {};
}
let results = [];
for (let p = 1; p <= window.atTotalChapterPages; p++) {
if (window.atChapterPagesCache[p] && window.atChapterPagesCache[p] !== 'loading') {
results.push({ p: p, html: window.atChapterPagesCache[p] });
} else {
const loaderText = document.getElementById('at-loader-text');
if (loaderText) loaderText.textContent = `Загрузка страницы ${p} из ${window.atTotalChapterPages}...`;
try {
const data = await fetchChapterPage(window.atCurrentChapterId, p, currentChapterTitle);
if (data.totalPages && data.totalPages > window.atTotalChapterPages) {
window.atTotalChapterPages = data.totalPages;
}
results.push({ p: p, html: data.html });
window.atChapterPagesCache[p] = data.html;
if (p < window.atTotalChapterPages) {
await new Promise(r => setTimeout(r, 50));
}
} catch (e) {
results.push({ p: p, html: `<div style="color:red; text-align:center;">Ошибка загрузки страницы ${p}</div>` });
}
}
}
saveChapterCache(window.atCurrentChapterId, window.atChapterPagesCache);
let pagesHtml = '';
results.forEach(res => {
let resHtml = res.html;
pagesHtml += `<div class="at-page-block" data-page="${res.p}" id="at-page-${res.p}">${resHtml}</div>`;
});
contentContainer.innerHTML = (currentChapterTitle ? `<h1 class="at-chapter-heading">${currentChapterTitle}</h1>` : '') + `<div id="at-pages-container">${pagesHtml}</div>`;
pingServerBookmark(window.atCurrentChapterId, window.atRequestedPage);
let isUserScrolling = false;
const stopScrollFix = () => { isUserScrolling = true; };
window.addEventListener('wheel', stopScrollFix, { once: true });
window.addEventListener('touchstart', stopScrollFix, { once: true });
window.addEventListener('mousedown', stopScrollFix, { once: true });
const scrollToTarget = () => {
if (isUserScrolling) return;
let restored = false;
try {
const bmStr = localStorage.getItem(`at-bookmark-${bookMeta.bookSlug}`);
if (bmStr) {
const bm = JSON.parse(bmStr);
if (bm.chapterId === window.atCurrentChapterId && bm.percent !== undefined) {
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
if (scrollHeight > 0) {
window.scrollTo(0, scrollHeight * bm.percent);
restored = true;
}
}
}
} catch(e) {}
if (!restored) {
const targetPageEl = document.getElementById(`at-page-${window.atRequestedPage}`);
if (targetPageEl && window.atRequestedPage > 1) {
const y = targetPageEl.getBoundingClientRect().top + window.scrollY - 60;
window.scrollTo(0, y);
} else if (window.atRequestedPage === 1) {
window.scrollTo(0, 0);
}
}
};
[50, 150, 300, 600, 1000].forEach(delay => setTimeout(scrollToTarget, delay));
setTimeout(initPageObserver, 1200);
}
function pingServerBookmark(chapterId, pageNum) {
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
const bodyParams = new URLSearchParams();
bodyParams.append('chapterId', chapterId);
bodyParams.append('page', pageNum);
if (csrfToken) bodyParams.append('_csrf', csrfToken);
fetch('/reader/get-page', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: bodyParams.toString()
}).catch(() => {});
} catch (e) {}
}
function initPageObserver() {
let scrollTimeout;
window.addEventListener('scroll', () => {
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const scrollY = window.scrollY || document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
if (scrollHeight > 0) {
const percent = Math.max(0, Math.min(1, scrollY / scrollHeight));
try {
localStorage.setItem(`at-bookmark-${bookMeta.bookSlug}`, JSON.stringify({
chapterId: window.atCurrentChapterId,
percent: percent,
time: Date.now()
}));
} catch(e) {}
}
const blocks = document.querySelectorAll('.at-page-block');
let activePage = window.atCurrentPage;
const triggerLine = window.innerHeight * 0.4;
for (let block of blocks) {
const rect = block.getBoundingClientRect();
if (rect.top <= triggerLine && rect.bottom >= triggerLine) {
activePage = parseInt(block.dataset.page);
break;
}
}
if (activePage && activePage !== window.atCurrentPage) {
window.atCurrentPage = activePage;
const url = new URL(window.location);
url.searchParams.set('p', activePage);
window.history.replaceState(null, '', url.toString());
pingServerBookmark(window.atCurrentChapterId, activePage);
}
}, 100);
}, { passive: true });
}
function bindEvents() {
bindDownloadModalEvents();
const btnRefresh = document.getElementById('at-btn-refresh');
const btnToc = document.getElementById('at-btn-toc');
const sidebar = document.getElementById('at-sidebar');
const sidebarOverlay = document.getElementById('at-sidebar-overlay');
const settingsPanel = document.getElementById('at-settings-panel');
const btnSettings = document.getElementById('at-btn-settings');
const headerEl = document.getElementById('at-header');
const btnLike = document.getElementById('at-btn-like');
const btnLibrary = document.getElementById('at-btn-library');
const btnReward = document.getElementById('at-btn-reward');
const btnComplain = document.getElementById('at-btn-complain');
btnLibrary.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!bookMeta.idBook) {
alert('ID книги не найден. Попробуйте обновить страницу.');
return;
}
bookMeta.inLibrary = !bookMeta.inLibrary;
updateLibraryIcon();
const langPrefix = window.location.pathname.split('/')[1] || 'ru';
const url = bookMeta.inLibrary ? `/${langPrefix}/book/to-library` : `/${langPrefix}/book/from-library`;
const bodyParams = new URLSearchParams();
bodyParams.append('bookId', bookMeta.idBook);
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
if (csrfToken) bodyParams.append('_csrf', csrfToken);
try {
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: bodyParams.toString()
});
} catch (err) {
console.error(err);
bookMeta.inLibrary = !bookMeta.inLibrary;
updateLibraryIcon();
}
};
btnLike.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!bookMeta.idBook) return;
bookMeta.isLiked = !bookMeta.isLiked;
bookMeta.likeCount += bookMeta.isLiked ? 1 : -1;
updateLikeIcon();
try {
const langPrefix = window.location.pathname.split('/')[1] || 'ru';
const action = bookMeta.isLiked ? 'like' : 'dislike';
await fetch(`/${langPrefix}/book/${action}?id=${bookMeta.idBook}`, {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
} catch(err) {
bookMeta.isLiked = !bookMeta.isLiked;
bookMeta.likeCount += bookMeta.isLiked ? 1 : -1;
updateLikeIcon();
}
};
btnReward.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const nativeBtn = document.querySelector('[onclick*="rewardAuthor"]');
if (nativeBtn) nativeBtn.click();
else alert('Функция награды недоступна для данной книги.');
};
btnComplain.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const nativeBtn = document.querySelector('#common-complaint, .complain-btn');
if (nativeBtn) nativeBtn.click();
else alert('Функция жалобы недоступна.');
};
btnRefresh.onclick = () => {
buildFullChapter(true);
};
function closeSidebar() { sidebar.classList.remove('open'); sidebarOverlay.classList.remove('visible'); }
btnToc.onclick = () => { sidebar.classList.toggle('open'); sidebarOverlay.classList.toggle('visible'); settingsPanel.classList.remove('open'); };
sidebarOverlay.onclick = closeSidebar;
document.addEventListener('click', (e) => {
if (!settingsPanel.contains(e.target) && e.target !== btnSettings) settingsPanel.classList.remove('open');
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { closeSidebar(); settingsPanel.classList.remove('open'); }
});
btnSettings.onclick = (e) => { e.stopPropagation(); settingsPanel.classList.toggle('open'); closeSidebar(); };
function applySettings() {
const themeStep = parseInt(document.getElementById('at-inp-theme').value, 10);
userSettings.themeStep = themeStep;
let themeName = 'Белая';
switch(themeStep) {
case 1: userSettings.bgColor = '#ffffff'; userSettings.textColor = '#333333'; themeName = 'Белая'; break;
case 2: userSettings.bgColor = '#f4ecd8'; userSettings.textColor = '#333333'; themeName = 'Сепия'; break;
case 3: userSettings.bgColor = '#e0e0e0'; userSettings.textColor = '#333333'; themeName = 'Серая'; break;
case 4: userSettings.bgColor = '#222222'; userSettings.textColor = '#eeeeee'; themeName = 'Темная'; break;
case 5: userSettings.bgColor = '#000000'; userSettings.textColor = '#808080'; themeName = 'Черная'; break;
}
userSettings.fontSize = parseInt(document.getElementById('at-inp-fs').value, 10);
userSettings.lineHeight = parseFloat(document.getElementById('at-inp-lh').value);
let twVal = parseInt(document.getElementById('at-inp-tw').value, 10);
userSettings.textWidth = twVal >= 1400 ? '100%' : twVal;
userSettings.fontFamily = document.getElementById('at-inp-ff').value;
userSettings.textColor = document.getElementById('at-inp-tc').value;
userSettings.bgColor = document.getElementById('at-inp-bc').value;
userSettings.hyphens = document.getElementById('at-inp-hy').checked;
document.getElementById('at-val-theme').textContent = themeName;
document.getElementById('at-inp-tc').value = userSettings.textColor;
document.getElementById('at-inp-bc').value = userSettings.bgColor;
document.getElementById('at-val-fs').textContent = userSettings.fontSize + 'px';
document.getElementById('at-val-lh').textContent = userSettings.lineHeight;
document.getElementById('at-val-tw').textContent = userSettings.textWidth === '100%' ? 'Макс' : userSettings.textWidth + 'px';
const root = document.documentElement;
root.style.setProperty('--at-bg-color', userSettings.bgColor);
root.style.setProperty('--at-text-color', userSettings.textColor);
root.style.setProperty('--at-font-size', userSettings.fontSize + 'px');
root.style.setProperty('--at-line-height', userSettings.lineHeight);
root.style.setProperty('--at-text-width', userSettings.textWidth === '100%' ? '100%' : userSettings.textWidth + 'px');
root.style.setProperty('--at-font-family', `'${userSettings.fontFamily}', sans-serif`);
root.style.setProperty('--at-hyphens', userSettings.hyphens ? 'auto' : 'none');
localStorage.setItem('at-reader-settings', JSON.stringify(userSettings));
}
['theme', 'fs', 'lh', 'tw', 'ff', 'tc', 'bc', 'hy'].forEach(id => {
const el = document.getElementById(`at-inp-${id}`);
if (el) {
el.addEventListener('input', applySettings);
el.addEventListener('change', applySettings);
}
});
document.getElementById('at-inp-theme').addEventListener('input', (e) => {
let tc = '#333', bc = '#fff';
switch(parseInt(e.target.value, 10)) {
case 1: bc = '#ffffff'; tc = '#333333'; break;
case 2: bc = '#f4ecd8'; tc = '#333333'; break;
case 3: bc = '#e0e0e0'; tc = '#333333'; break;
case 4: bc = '#222222'; tc = '#eeeeee'; break;
case 5: bc = '#000000'; tc = '#808080'; break;
}
document.getElementById('at-inp-tc').value = tc;
document.getElementById('at-inp-bc').value = bc;
applySettings();
});
let lastScroll = 0, isHeaderHidden = false;
window.addEventListener('scroll', () => {
const currentScroll = window.scrollY || document.documentElement.scrollTop;
if (currentScroll > lastScroll + 10 && currentScroll > 60) {
if (!isHeaderHidden) { headerEl.classList.add('hidden'); isHeaderHidden = true; }
} else if (currentScroll < lastScroll - 10 || currentScroll <= 60) {
if (isHeaderHidden) { headerEl.classList.remove('hidden'); isHeaderHidden = false; }
}
lastScroll = currentScroll <= 0 ? 0 : currentScroll;
}, { passive: true });
document.addEventListener('mousemove', (e) => {
if (e.clientY <= 60 && isHeaderHidden) { headerEl.classList.remove('hidden'); isHeaderHidden = false; }
});
}
function init() {
if (extractData()) {
fixNativeModals();
killAntiCopy();
injectStyles();
renderInterface();
updateLibraryIcon();
updateLikeIcon();
fetchBookExtraInfo();
buildFullChapter();
return true;
}
return false;
}
let initAttempts = 0;
const maxAttempts = 150;
const initTimer = setInterval(() => {
initAttempts++;
if (document.title.includes('Один момент') || document.querySelector('#challenge-error-text') || document.querySelector('.cf-browser-verification')) {
console.log("Litnet Reader: Обнаружена проверка Cloudflare. Ожидание...");
return;
}
const hasState = document.getElementById('ng-state');
const hasText = document.querySelector('.jsReaderText, [data-test-id="reader-text"]');
if (hasState || hasText) {
clearInterval(initTimer);
if (!window.litnetReaderInitialized) {
window.litnetReaderInitialized = true;
setTimeout(init, 50);
}
} else if (initAttempts >= maxAttempts) {
clearInterval(initTimer);
console.warn("Litnet Reader: Превышено время ожидания загрузки страницы.");
}
}, 100);
})();