您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Пагинация комментариев
当前为
// ==UserScript== // @name Shikimori Comments Pagination // @namespace http://tampermonkey.net/ // @version 1.3 // @description Пагинация комментариев // @author karuma // @license MIT // @match https://shikimori.one/* // @grant GM_addStyle // ==/UserScript== (function () { 'use strict'; const COMMENTS_PER_PAGE = 5; // Число комментариев на странице const CHECK_INTERVAL = 500; // Частота проверки элементов на странице (Не ставить сликшом маленький) const TimeoutToScroll = 200; // Задержка перед скролом к пагинатору. (После нажатия на вперед/назад) const EnableScroll = true; // true/false - после после обновления блока комментариев скролл до пагинатора GM_addStyle(` .shiki-comments-pagination { display: flex; justify-content: center; align-items: center; margin: 20px 0; gap: 10px; padding: 10px; border-radius: 4px; } .shiki-comments-pagination button { background: #579; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer; min-width: 30px; transition: background 0.2s; } .shiki-comments-pagination button:hover { background: #467; } .shiki-comments-pagination button:disabled { background: #ccc; cursor: not-allowed; } .shiki-comments-pagination input { width: 60px; text-align: center; padding: 5px; border: 1px solid #ddd; border-radius: 4px; } .shiki-comments-pagination .page-info { margin: 0 10px; font-size: 14px; } .shiki-comments-loading { opacity: 0.7; pointer-events: none; } .b-spoiler_inline.opened { background-color: #f5f5f5; color: #333; padding: 2px 4px; border-radius: 3px; box-shadow: 0 0 0 1px rgba(0,0,0,0.1); } .b-spoiler_block { cursor: pointer; display: inline; margin: 0 1px; } .b-spoiler_block > span[tabindex="0"] { display: inline; padding: 1px 4px; background-color: #687687; color: #fff; font-size: 12px; font-family: inherit; border-radius: 2px; transition: all 0.15s ease; line-height: 1.3; } .b-spoiler_block:hover > span[tabindex="0"] { background-color: #5a6775; } .b-spoiler_block.is-opened > span[tabindex="0"] { background-color: #f5f5f5; color: #333; box-shadow: 0 0 0 1px rgba(0,0,0,0.1); } .b-spoiler_block > div { display: none; margin-top: 3px; padding: 5px; background: #f5f5f5; border-radius: 2px; border: 1px solid #e0e0e0; } .b-spoiler_block.is-opened > div { display: block; } `); /* ========== ОБРАБОТКА СПОЙЛЕРОВ И УДАЛЕНИЯ ========== */ // Функция для раскрытия/закрытия inline-спойлеров (текстовых) function bindSpoilerDeleteButtons(container) { container.querySelectorAll('.b-spoiler_inline').forEach(spoiler => { // Клонируем элемент (это удалит все предыдущие обработчики) const newSpoiler = spoiler.cloneNode(true); // Добавляем наш обработчик newSpoiler.addEventListener('click', async function(e) { e.stopPropagation(); // Останавливаем всплытие if (this.classList.contains('opened')) { // Закрываем спойлер if (this.dataset.originalContent) { this.innerHTML = this.dataset.originalContent; } this.classList.remove('opened'); } else { // Открываем спойлер this.dataset.originalContent = this.innerHTML; this.innerHTML = this.textContent.trim(); this.classList.add('opened'); } }); // Заменяем оригинальный элемент клоном spoiler.parentNode.replaceChild(newSpoiler, spoiler); }); } // Функция для раскрытия block-спойлеров (с контентом) function bindSpoilerBlockButtons(container) { container.querySelectorAll('.b-spoiler_block').forEach(spoilerBlock => { const newSpoilerBlock = spoilerBlock.cloneNode(true); const spoilerTitle = newSpoilerBlock.querySelector('span[tabindex="0"]'); const contentDiv = newSpoilerBlock.querySelector('div'); if (!spoilerTitle || !contentDiv) return; contentDiv.style.display = 'none'; spoilerTitle.addEventListener('click', (e) => { e.stopPropagation(); if (contentDiv.style.display === 'none') { contentDiv.style.display = 'block'; newSpoilerBlock.classList.add('is-opened'); initImageModalViewer(contentDiv); initVideoModalViewer(contentDiv); } else { contentDiv.style.display = 'none'; newSpoilerBlock.classList.remove('is-opened'); } }); // Заменяем оригинальный элемент клоном spoilerBlock.parentNode.replaceChild(newSpoilerBlock, spoilerBlock); }); } // Кликабельность картинок function initImageModalViewer(container) { const containerEl = typeof container === 'string' ? document.querySelector(container) : container; if (!containerEl) return; containerEl.querySelectorAll('img').forEach(el => { if (!el.src || el.closest('.b-video')) return; const originalStyles = el.getAttribute('style'); const preview = el.cloneNode(true); preview.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); // Создание модального окна при клике const modal = document.createElement('div'); modal.style.cssText = ` display: flex; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); cursor: zoom-out; align-items: center; justify-content: center; overflow: auto; `; const img = document.createElement('img'); img.src = el.src.replace('/thumbnail/', '/original/').replace('/x48/', '/x160/'); img.style.cssText = ` max-width: 90vw; max-height: 90vh; display: block; object-fit: contain; animation: fadeInScale 0.3s ease-out; `; const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` position: fixed; top: 20px; right: 30px; font-size: 40px; font-weight: bold; cursor: pointer; color: white; text-shadow: 0 0 5px rgba(0,0,0,0.8); z-index: 10000; `; const style = document.createElement('style'); style.textContent = ` @keyframes fadeInScale { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } `; document.head.appendChild(style); const close = () => { modal.remove(); style.remove(); document.body.style.overflow = ''; document.removeEventListener('keydown', onKeydown); }; const onKeydown = (e) => { if (e.key === 'Escape') close(); }; modal.addEventListener('click', e => { if (e.target === modal || e.target === img) close(); }); closeBtn.addEventListener('click', close); document.addEventListener('keydown', onKeydown); modal.append(img, closeBtn); document.body.append(modal); document.body.style.overflow = 'hidden'; }); el.parentNode.replaceChild(preview, el); if (originalStyles) { preview.setAttribute('style', originalStyles); } if (!originalStyles || !originalStyles.includes('cursor:')) { preview.style.cursor = 'zoom-in'; } }); } function initVideoModalViewer(container) { const containerEl = typeof container === 'string' ? document.querySelector(container) : container; if (!containerEl) return; containerEl.querySelectorAll('.b-video.youtube .video-link').forEach(link => { if (!link.dataset.href) return; const youtubeUrl = link.dataset.href; const videoId = youtubeUrl.match(/embed\/([^?]+)/)?.[1] || youtubeUrl.match(/youtu\.be\/([^?]+)/)?.[1] || youtubeUrl.match(/v=([^&]+)/)?.[1]; if (!videoId) return; const preview = link.querySelector('img'); if (!preview) return; const originalStyles = preview.getAttribute('style'); link.removeAttribute('href'); link.onclick = function (e) { e.preventDefault(); e.stopPropagation(); const modal = document.createElement('div'); modal.style.cssText = ` display: flex; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); cursor: zoom-out; align-items: center; justify-content: center; overflow: auto; `; const videoContainer = document.createElement('div'); videoContainer.style.cssText = ` position: relative; width: 90vw; max-width: 1200px; height: 0; padding-bottom: 56.25%; animation: fadeInScale 0.3s ease-out; `; const iframe = document.createElement('iframe'); iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`; iframe.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; `; iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allow', 'autoplay'); const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` position: fixed; top: 20px; right: 30px; font-size: 40px; font-weight: bold; cursor: pointer; color: white; text-shadow: 0 0 5px rgba(0,0,0,0.8); z-index: 10000; `; const style = document.createElement('style'); style.textContent = ` @keyframes fadeInScale { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } `; document.head.appendChild(style); const close = () => { modal.remove(); style.remove(); document.body.style.overflow = ''; document.removeEventListener('keydown', onKeydown); }; const onKeydown = (e) => { if (e.key === 'Escape') close(); }; modal.addEventListener('click', e => { if (e.target === modal) close(); }); closeBtn.addEventListener('click', close); document.addEventListener('keydown', onKeydown); videoContainer.appendChild(iframe); modal.append(videoContainer, closeBtn); document.body.append(modal); document.body.style.overflow = 'hidden'; }; if (!originalStyles || !originalStyles.includes('cursor:')) { preview.style.cursor = 'zoom-in'; } }); } /* ========== КЛАСС ДЛЯ РАБОТЫ С БЛОКАМИ КОММЕНТАРИЕВ ========== */ class CommentsBlock { constructor(container) { // Инициализация свойств this.container = container; // DOM-элемент контейнера this.loader = container.querySelector('.comments-loader');// Элемент загрузки this.fetchId = null;// ID для запросов this.entityId = null; // Может быть topicId или userId this.entityType = null; // 'Topic' или 'User' this.currentPage = 1;// Текущая страница this.totalPages = 1;// Всего страниц this.pagination = null;// Элемент пагинации this.dataskip = parseInt(this.loader.getAttribute('data-skip')); this.datacount = parseInt(this.loader.getAttribute('data-count')); this.html this.init(); } // Основная инициализация init() { if (!this.loader) return; const ids = this.getCommentsIDs(); if (!ids) return; this.fetchId = ids.fetchId; this.entityId = ids.entityId; this.entityType = ids.entityType; // Рассчитываем общее количество страниц const commentsCount = this.datacount + this.dataskip || 0; this.totalPages = Math.max(1, Math.ceil(commentsCount / COMMENTS_PER_PAGE)); } // Создание и загрузка блока комментариев async CreateCommentsBlock() { // Проверяем наличие всех необходимых данных перед загрузкой if (!this.hasRequiredAttributes()) { console.error('Cannot create comments block - missing required attributes'); return false; } await this.loadComments(); this.renderPagination(); } // Проверка наличия всех необходимых атрибутов hasRequiredAttributes() { if (!this.loader) { console.error('Missing comments loader element'); return false; } const urlTemplate = this.loader.getAttribute('data-clickloaded-url-template'); if (!urlTemplate) { console.error('Missing data-clickloaded-url-template attribute'); return false; } this.ids = this.getCommentsIDs(); if (!this.ids || !this.ids.fetchId || !this.ids.entityId || !this.ids.entityType) { console.error('Invalid or missing IDs in URL template'); return false; } const count = this.loader.getAttribute('data-count'); if (!count) { console.error('Missing data-count attribute'); return false; } return true; } // Получение идентификаторов топика getCommentsIDs() { try { const urlTemplate = this.loader.getAttribute('data-clickloaded-url-template'); if (!urlTemplate) return null; const matches = urlTemplate.match(/\/fetch\/(\d+)\/(Topic|User)\/(\d+)/); if (!matches || matches.length < 4) return null; return { fetchId: matches[1], entityId: matches[3], entityType: matches[2] // 'Topic' или 'User' }; } catch (error) { console.error('Error parsing comment IDs:', error); return null; } } //Создание Url для запроса buildCommentsUrl(offset) { // Формируем URL в зависимости от типа сущности if (this.entityType === 'User') { return `https://shikimori.one/comments/fetch/${this.fetchId}/User/${this.entityId}/${offset}/${COMMENTS_PER_PAGE}`; } else { // По умолчанию считаем, что это Topic return `https://shikimori.one/comments/fetch/${this.fetchId}/Topic/${this.entityId}/${offset}/${COMMENTS_PER_PAGE}`; } } /** * Загружает комментарии с автоматическим повтором при ошибках * @param {string} url - URL для запроса * @param {number} [maxRetries] - Максимальное количество попыток (по умолчанию: 4) * @param {number} [retryDelay] - Задержка между попытками в миллисекундах (по умолчанию: 2 секунды) * @returns {Promise<string>} HTML-контент комментариев * @throws {Error} Если все попытки завершились ошибкой */ async fetchComments(url, maxRetries = 4, initialRetryDelay = 2000) { let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url); // Обработка HTTP-ошибок if (!response.ok) { // Особый случай: Too Many Requests (429) if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') || initialRetryDelay / 1000, 10); await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); continue; // Повторяем попытку без увеличения счетчика } throw new Error(`HTTP error ${response.status} ${response.statusText}`); } const data = await response.json(); if (!data?.content) throw new Error('Invalid response format: missing content'); return data; } catch (error) { lastError = error; if (attempt < maxRetries) { const currentDelay = initialRetryDelay * Math.pow(2, attempt - 1); await new Promise(resolve => setTimeout(resolve, currentDelay)); } } } throw lastError || new Error('All retry attempts failed'); } // Основная функция загрузки async loadComments() { try { const offset = (this.currentPage - 1) * COMMENTS_PER_PAGE; this.container.classList.add('shiki-comments-loading'); const data = await this.fetchComments(this.buildCommentsUrl(offset)); this.container.innerHTML = data.content; jQuery(this.container).process(data.JS_EXPORTS); bindSpoilerDeleteButtons(this.container); bindSpoilerBlockButtons(this.container); initImageModalViewer(this.container); initVideoModalViewer(this.container); } catch (error) { console.error('Ошибка загрузки комментариев:', error); } finally { this.container.classList.remove('shiki-comments-loading'); } } // Создание интерфейса пагинации renderPagination() { if (this.pagination) { this.pagination.remove(); // Удаляем старую пагинацию } function ScrollToPagination () { this.pagination.scrollIntoView({ behavior: 'smooth', // Плавная прокрутка block: 'start'// Выравнивание по верхнему краю элемента }); } // Создаем новый элемент пагинации this.pagination = document.createElement('div'); this.pagination.className = 'shiki-comments-pagination'; this.pagination.classList.add('l-page'); this.pagination.innerHTML = ` <button class="prev-page">< Назад</button> <span class="page-info">Страница ${this.currentPage} из ${this.totalPages}</span> <input type="number" class="page-input" min="1" max="${this.totalPages}" value="${this.currentPage}"> <button class="next-page">Вперед ></button> `; // Находим editor-container (может быть рядом с контейнером или в другом месте) const editorContainer = this.container.closest('.b-topic')?.querySelector('.editor-container') || document.querySelector('.editor-container'); // Вставляем после editor-container если найден, иначе после контейнера комментариев const insertAfter = editorContainer || this.container; insertAfter.parentNode.insertBefore(this.pagination, insertAfter.nextSibling); function ScrollIntoPagination(container) { if (EnableScroll) { setTimeout(() => { // Получаем позицию элемента относительно документа const elementRect = container.getBoundingClientRect(); const scrollPosition = elementRect.bottom + window.pageYOffset - window.innerHeight; // Добавляем отступ -100px сверху window.scrollTo({ top: scrollPosition - (-80), behavior: 'instant' // или 'smooth' для плавной прокрутки }); },TimeoutToScroll) // Задержка } } // Обработчики событий для кнопок пагинации this.pagination.querySelector('.prev-page').addEventListener('click', async () => { if (this.currentPage > 1) { this.currentPage--; await this.loadComments(); this.renderPagination(); // Обновляем отображение ScrollIntoPagination(this.pagination); } }); // Обработчик для поля ввода страницы this.pagination.querySelector('.next-page').addEventListener('click', async () => { if (this.currentPage < this.totalPages) { this.currentPage++; await this.loadComments(); this.renderPagination(); ScrollIntoPagination(this.pagination); } }); this.pagination.querySelector('.page-input').addEventListener('change', async (e) => { const newPage = parseInt(e.target.value, 10); if (newPage >= 1 && newPage <= this.totalPages) { this.currentPage = newPage; await this.loadComments(); this.renderPagination(); ScrollIntoPagination(this.pagination); } }); } } /* ========== ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ И ФУНКЦИИ ========== */ let initializedBlocks = new WeakMap(); // Хранит инициализированные блоки let isInitializing = false; // Флаг для защиты от повторной инициализации async function init() { if (isInitializing) return; isInitializing = true; console.log('Parallel INIT started'); const updatedBlocks = document.querySelectorAll('.b-comments'); try { // Создаем массив промисов для всех блоков const initializationPromises = Array.from(updatedBlocks).map(async (container) => { try { if (!initializedBlocks.has(container)) { const instance = new CommentsBlock(container); initializedBlocks.set(container, instance); await instance.CreateCommentsBlock(); console.log('Successfully initialized:', container); } else { await initializedBlocks.get(container).CreateCommentsBlock(); } } catch (error) { console.error(`Error processing block ${container}:`, error); // Пробрасываем ошибку дальше, если нужно прервать все операции throw error; } }); // Ожидаем завершения ВСЕХ операций параллельно await Promise.all(initializationPromises); } catch (error) { console.error('Global initialization error:', error); } finally { isInitializing = false; } } let checkInterval = null; let lastKnownBlocks = []; // Функция для проверки новых блоков комментариев на странице и инициализации async function observeNewComments() { if (checkInterval) clearInterval(checkInterval); console.log("Запуск наблюдения за .b-comments"); checkInterval = setInterval(async () => { const currentBlocks = Array.from(document.querySelectorAll('.b-comments')); if (currentBlocks.length !== lastKnownBlocks.length || currentBlocks.some(block => !lastKnownBlocks.includes(block))) { console.log("Обнаружены изменения в .b-comments"); await init(); // Добавлен await для асинхронной init() lastKnownBlocks = currentBlocks; } }, CHECK_INTERVAL); const initialBlocks = Array.from(document.querySelectorAll('.b-comments')); if (initialBlocks.length > 0) { await init(); lastKnownBlocks = initialBlocks; } } function stopObserving() { if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } } window.addEventListener('popstate', (event) => { window.location.reload(); console.log('Сработал popstate!', event.state); }); observeNewComments(); })();