Playlist PH Pagination Enhanced

Разбивает плейлист на страницы с поддержкой динамической подгрузки, URL-параметрами и сохранением настроек

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name          Playlist PH Pagination Enhanced
// @namespace     https://www.ph.com/
// @version       1.4
// @description   Разбивает плейлист на страницы с поддержкой динамической подгрузки, URL-параметрами и сохранением настроек
// @match         *://*.pornhub.com/playlist/*
// @match         *://*.pornhubpremium.com/playlist/*
// @run-at        document-end
// @grant         GM.getValue
// @grant         GM.setValue
// @license       MIT
// ==/UserScript==

(async function() {
    'use strict';

    const DEFAULT_ITEMS_PER_PAGE = 20;
    const MAX_PAGE_BUTTONS = 7;
    const REFRESH_INTERVAL = 1000;
    const SESSION_STORAGE_PREFIX = 'ph_pagination_';

    async function getStoredValue(key, defaultValue) {
        const sessionValue = sessionStorage.getItem(SESSION_STORAGE_PREFIX + key);
        if (sessionValue !== null) {
            return JSON.parse(sessionValue);
        }
        return await GM.getValue(key, defaultValue);
    }

    async function storeValue(key, value) {
        sessionStorage.setItem(SESSION_STORAGE_PREFIX + key, JSON.stringify(value));
        await GM.setValue(key, value);
    }

    let itemsPerPage = await getStoredValue('ph_items_per_page', DEFAULT_ITEMS_PER_PAGE);
    let currentPage = 1;
    let items = [];
    let totalPages = 1;
    let refreshTimer = null;
    let lastItemCount = 0;

    const playlistId = window.location.pathname.split('/').pop();
    const itemsCountKey = 'ph_items_count_' + playlistId;

    function getUrlParams() {
        const urlParams = new URLSearchParams(window.location.search);
        return { page: parseInt(urlParams.get('page')) || 1 };
    }

    function updateUrlWithPage(page) {
        const url = new URL(window.location);
        url.searchParams.set('page', page);
        history.replaceState({}, '', url);
    }

    const urlParams = getUrlParams();
    if (urlParams.page > 1) {
        currentPage = urlParams.page;
    } else if (playlistId) {
        currentPage = await getStoredValue('ph_current_page_' + playlistId, 1);
    }

    function getContainer() {
        return document.querySelector('.js-playlistWrapper');
    }

    function waitForContainer() {
        if (document.getElementById('ph-pager')) return;
        const container = getContainer();
        if (!container) return setTimeout(waitForContainer, 300);
        setupPagination(container);
    }

    async function refreshItemsList() {
        const container = getContainer();
        if (!container) return;
        const newItems = Array.from(container.querySelectorAll('.pcVideoListItem, .js-playlistWrapper'));
        lastItemCount = newItems.length;
        items = newItems;
        totalPages = Math.ceil(items.length / itemsPerPage);
        if (playlistId) {
            await storeValue(itemsCountKey, items.length);
        }
        createPagination();
        const counter = document.getElementById('ph-counter');
        if (counter) {
            const start = (currentPage - 1) * itemsPerPage;
            const end = start + itemsPerPage;
            counter.textContent = `Видео ${start + 1}-${Math.min(end, items.length)} из ${items.length}`;
        }
        applyCurrentPage();
    }

    function setupScrollHandler() {
        window.addEventListener('scroll', function() {
            if (refreshTimer) clearTimeout(refreshTimer);
            refreshTimer = setTimeout(refreshItemsList, REFRESH_INTERVAL);
        });
    }

    async function setupPagination(container) {
        items = Array.from(container.querySelectorAll('.pcVideoListItem, .js-playlistWrapper'));
        const savedItemCount = await getStoredValue(itemsCountKey, 0);
        if (!items.length && savedItemCount > 0) {
            lastItemCount = savedItemCount;
            setTimeout(refreshItemsList, 1000);
        } else {
            if (!items.length) return;
            lastItemCount = items.length;
            if (playlistId) {
                await storeValue(itemsCountKey, items.length);
            }
        }
        totalPages = Math.ceil(lastItemCount / itemsPerPage);

        const controls = document.createElement('div');
        controls.id = 'ph-controls';
        controls.style = 'display: flex; justify-content: space-between; margin: 10px 0; padding: 10px; background: #1b1b1b; border-radius: 5px; position: sticky; top: 0; z-index: 100;';

        const settingsDiv = document.createElement('div');
        settingsDiv.style = 'display: flex; align-items: center;';

        const label = document.createElement('label');
        label.textContent = 'Видео на странице: ';
        label.style = 'margin-right: 10px; color: #fff;';

        const select = document.createElement('select');
        select.style = 'padding: 3px; background: #333; color: #fff; border: 1px solid #555;';
        [10, 20, 30, 50, 100].forEach(num => {
            const option = document.createElement('option');
            option.value = num;
            option.textContent = num;
            option.selected = (num === itemsPerPage);
            select.appendChild(option);
        });

        select.onchange = async function() {
            itemsPerPage = parseInt(this.value);
            await storeValue('ph_items_per_page', itemsPerPage);
            totalPages = Math.ceil(items.length / itemsPerPage);
            createPagination();
            showPage(1);
        };

        settingsDiv.appendChild(label);
        settingsDiv.appendChild(select);

        const counter = document.createElement('div');
        counter.id = 'ph-counter';
        counter.style = 'color: #fff;';
        counter.textContent = `Всего видео: ${items.length}`;

        const refreshBtn = document.createElement('button');
        refreshBtn.textContent = '⟳ Обновить';
        refreshBtn.style = 'margin-left: 10px; padding: 3px 8px; background: #333; color: #fff; border: 1px solid #555; cursor: pointer;';
        refreshBtn.onclick = refreshItemsList;

        settingsDiv.appendChild(refreshBtn);
        controls.appendChild(settingsDiv);
        controls.appendChild(counter);

        container.parentNode.insertBefore(controls, container);

        const pager = document.createElement('div');
        pager.id = 'ph-pager';
        pager.style = 'text-align: center; margin: 10px 0; padding: 10px;';
        container.parentNode.insertBefore(pager, container);

        const bottomPager = document.createElement('div');
        bottomPager.id = 'ph-pager-bottom';
        bottomPager.style = 'text-align: center; margin: 10px 0; padding: 10px;';
        if (container.nextSibling) {
            container.parentNode.insertBefore(bottomPager, container.nextSibling);
        } else {
            container.parentNode.appendChild(bottomPager);
        }

        createPagination();
        const urlParams = getUrlParams();
        if (urlParams.page > 1 && urlParams.page <= totalPages) {
            showPage(urlParams.page);
        } else {
            showPage(currentPage);
        }
        setupScrollHandler();
    }

    function createPagination() {
        updatePaginationPanel(document.getElementById('ph-pager'));
        updatePaginationPanel(document.getElementById('ph-pager-bottom'));
    }

    function updatePaginationPanel(pager) {
        if (!pager) return;
        pager.innerHTML = '';
        const buttonStyle = 'margin: 0 3px; padding: 5px 10px; background: #ff9000; color: #000; border: none; border-radius: 3px; cursor: pointer; transition: background 0.3s;';
        const disabledStyle = 'background: #444; color: #aaa; cursor: not-allowed;';
        const activeStyle = 'background: #ff5400; font-weight: bold;';
        const btnFirst = document.createElement('button');
        btnFirst.textContent = '«';
        btnFirst.style = buttonStyle;
        btnFirst.onclick = () => showPage(1);
        pager.appendChild(btnFirst);
        const btnPrev = document.createElement('button');
        btnPrev.textContent = '‹';
        btnPrev.style = buttonStyle;
        btnPrev.onclick = () => showPage(currentPage - 1);
        pager.appendChild(btnPrev);
        let startPage = Math.max(1, currentPage - Math.floor(MAX_PAGE_BUTTONS / 2));
        let endPage = Math.min(totalPages, startPage + MAX_PAGE_BUTTONS - 1);
        if (endPage - startPage + 1 < MAX_PAGE_BUTTONS) {
            startPage = Math.max(1, endPage - MAX_PAGE_BUTTONS + 1);
        }
        if (startPage > 1) {
            const ellipsis = document.createElement('span');
            ellipsis.textContent = '...';
            ellipsis.style = 'margin: 0 10px; color: #fff;';
            pager.appendChild(ellipsis);
        }
        for (let i = startPage; i <= endPage; i++) {
            const btn = document.createElement('button');
            btn.textContent = i;
            btn.style = buttonStyle + (i === currentPage ? activeStyle : '');
            btn.onclick = () => showPage(i);
            pager.appendChild(btn);
        }
        if (endPage < totalPages) {
            const ellipsis = document.createElement('span');
            ellipsis.textContent = '...';
            ellipsis.style = 'margin: 0 10px; color: #fff;';
            pager.appendChild(ellipsis);
        }
        const btnNext = document.createElement('button');
        btnNext.textContent = '›';
        btnNext.style = buttonStyle + (currentPage === totalPages ? disabledStyle : '');
        btnNext.onclick = () => showPage(currentPage + 1);
        btnNext.disabled = (currentPage === totalPages);
        pager.appendChild(btnNext);
        const btnLast = document.createElement('button');
        btnLast.textContent = '»';
        btnLast.style = buttonStyle + (currentPage === totalPages ? disabledStyle : '');
        btnLast.onclick = () => showPage(totalPages);
        btnLast.disabled = (currentPage === totalPages);
        pager.appendChild(btnLast);
        const pageInfo = document.createElement('span');
        pageInfo.style = 'margin-left: 15px; color: #fff;';
        pageInfo.textContent = `Страница ${currentPage} из ${totalPages}`;
        pager.appendChild(pageInfo);
    }

    function showPage(page) {
        if (page < 1 || page > totalPages) return;
        currentPage = page;
        updateUrlWithPage(currentPage);
        if (playlistId) storeValue('ph_current_page_' + playlistId, currentPage);
        applyCurrentPage();
        createPagination();
        const pageJumpInputs = document.querySelectorAll('input[type="number"]');
        pageJumpInputs.forEach(input => {
            input.value = currentPage;
            input.max = totalPages;
        });
        window.scrollTo(0, 0);
    }

    function applyCurrentPage() {
        const start = (currentPage - 1) * itemsPerPage;
        const end = start + itemsPerPage;
        items.forEach((el, idx) => {
            el.style.display = (idx >= start && idx < end) ? '' : 'none';
        });
        const counter = document.getElementById('ph-counter');
        if (counter) {
            counter.textContent = `Видео ${start + 1}-${Math.min(end, items.length)} из ${items.length}`;
        }
    }

    function setupMutationObserver() {
        const container = getContainer();
        if (!container) return;
        const observer = new MutationObserver(() => refreshItemsList());
        observer.observe(container, { childList: true, subtree: true });
    }

    window.addEventListener('popstate', function() {
        const urlParams = getUrlParams();
        if (urlParams.page && urlParams.page !== currentPage && urlParams.page <= totalPages) {
            showPage(urlParams.page);
        }
    });

    function restoreStoredData() {
        if (playlistId) {
            getStoredValue(itemsCountKey, 0).then(savedItemCount => {
                if (savedItemCount > 0) {
                    lastItemCount = savedItemCount;
                    totalPages = Math.ceil(savedItemCount / itemsPerPage);
                    console.log(`[Playlist Pagination] Восстановлены данные: ${savedItemCount} видео, ${totalPages} страниц`);
                }
            });
        }
    }

    window.addEventListener('load', function() {
        restoreStoredData();
        setTimeout(() => {
            waitForContainer();
            setupMutationObserver();
        }, 1000);
    });

    window.addEventListener('beforeunload', function() {
        console.log('[Playlist Pagination] Страница закрывается, данные сессии сохранены');
    });

    let lastUrl = location.href;
    const urlObserver = new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            setTimeout(() => {
                waitForContainer();
                setupMutationObserver();
            }, 1000);
        }
    });
    urlObserver.observe(document, { subtree: true, childList: true });

    setTimeout(() => {
        waitForContainer();
        setupMutationObserver();
    }, 1000);

    const style = document.createElement('style');
    style.textContent = `#ph-controls { display: flex; justify-content: space-between; margin: 10px 0; padding: 10px; background: #1b1b1b; border-radius: 5px; position: sticky; top: 0; z-index: 100; } #ph-pager, #ph-pager-bottom { text-align: center; margin: 10px 0; padding: 10px; } #ph-pager button, #ph-pager-bottom button { margin: 0 3px; padding: 5px 10px; background: #ff9000; color: #000; border: none; border-radius: 3px; cursor: pointer; transition: background 0.3s; } #ph-pager button:hover, #ph-pager-bottom button:hover { background: #ffb244; } #ph-pager button:disabled, #ph-pager-bottom button:disabled { background: #444; color: #aaa; cursor: not-allowed; } #ph-pager button.active, #ph-pager-bottom button.active { background: #ff5400; font-weight: bold; } input[type="number"] { width: 60px; padding: 3px; background: #333; color: #fff; border: 1px solid #555; } input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { opacity: 1; }`;
    document.head.appendChild(style);
})();