pixiv Sort by Popularity

Show images sorted by popularity without pixiv Premium.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         pixiv Sort by Popularity
// @namespace    https://pixiv.net/
// @version      1.0.0
// @description  Show images sorted by popularity without pixiv Premium.
// @author       Yukari Kaname
// @license      MIT
// @icon         https://www.pixiv.net/favicon.ico
// @homepageURL  https://github.com/yukarikaname/pixiv-popularity
// @match        https://www.pixiv.net/*tags*
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    // Configuration constants
    const HIJACK_FLAG = 'ppapiPopularityHijacked';
    const MODAL_ID = 'pixiv-popularity-modal';
    const POLL_INTERVAL = 2000;
    const POPULARITY_ORDER = 'popular_d';
    const POPULARITY_ENTITY_ID = 'search-option/popular_d';

    // Extract tag name from URL
    const getTagFromUrl = () => {
        const m = location.pathname.match(/\/tags\/([^/?]+)/);
        return m ? decodeURIComponent(m[1]) : null;
    };

    // Create modal dialog element
    const createModalDialog = () => {
        const dialog = document.createElement('div');
        dialog.style.cssText = `
            background: white;
            width: 90%;
            max-width: 1200px;
            height: 80vh;
            border-radius: 8px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
        `;
        return dialog;
    };

    // Create modal header with title and close button
    const createModalHeader = (illustCount, onClose) => {
        const header = document.createElement('div');
        header.style.cssText = `
            padding: 16px 20px;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: #fafafa;
        `;

        const title = document.createElement('h2');
        title.textContent = `Popular Works (${illustCount} results)`;
        title.style.cssText = `
            margin: 0;
            font-size: 18px;
            font-weight: 600;
            color: #333;
        `;
        header.appendChild(title);

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '✕';
        closeBtn.style.cssText = `
            background: none;
            border: none;
            font-size: 24px;
            color: #999;
            cursor: pointer;
            padding: 0;
            width: 32px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
        `;
        closeBtn.onclick = onClose;
        header.appendChild(closeBtn);

        return header;
    };

    // Create image grid item
    const createGridItem = (illust) => {
        const item = document.createElement('div');
        item.style.cssText = `
            position: relative;
            overflow: hidden;
            aspect-ratio: 1;
            border-radius: 4px;
            background: #ddd;
            cursor: pointer;
            transition: transform 0.2s;
        `;

        item.addEventListener('mouseenter', () => {
            item.style.transform = 'scale(1.05)';
        });
        item.addEventListener('mouseleave', () => {
            item.style.transform = '';
        });

        const link = document.createElement('a');
        link.href = 'https://www.pixiv.net/artworks/' + illust.id;
        link.target = '_blank';
        link.style.cssText = `
            display: block;
            width: 100%;
            height: 100%;
            text-decoration: none;
        `;

        const img = document.createElement('img');
        img.src = illust.image_urls.medium;
        img.alt = illust.title || '';
        img.style.cssText = `
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
        `;

        link.appendChild(img);
        item.appendChild(link);
        return item;
    };

    // Create grid with all illustrations
    const createImageGrid = (illusts) => {
        const grid = document.createElement('div');
        grid.style.cssText = `
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
            gap: 12px;
        `;

        illusts.forEach((illust) => {
            if (illust.id && illust.image_urls && illust.image_urls.medium) {
                grid.appendChild(createGridItem(illust));
            }
        });

        return grid;
    };

    // Display popular works in a modal popup
    const showPopularModal = (illusts) => {
        // Remove any existing modal
        const existing = document.getElementById(MODAL_ID);
        if (existing) existing.remove();

        // Create modal overlay
        const modal = document.createElement('div');
        modal.id = MODAL_ID;
        modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.7);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 99999;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        `;

        const dialog = createModalDialog();
        const header = createModalHeader(illusts.length, () => modal.remove());

        const content = document.createElement('div');
        content.style.cssText = `
            flex: 1;
            overflow-y: auto;
            padding: 16px;
        `;
        content.appendChild(createImageGrid(illusts));

        dialog.appendChild(header);
        dialog.appendChild(content);
        modal.appendChild(dialog);
        document.body.appendChild(modal);

        // Close when clicking background
        modal.onclick = (e) => {
            if (e.target === modal) modal.remove();
        };
    };

    // Normalize API response to consistent format
    const normalizeWebIllusts = (data) => {
        return (data || [])
            .map((it) => {
                const id = it.id || it.illustId || it.workId;
                if (!id) return null;

                // Extract image URL - try multiple possible field names
                let imageUrl = it.url || '';
                if (!imageUrl && it.urls) {
                    imageUrl =
                        it.urls.regular ||
                        it.urls.small ||
                        it.urls.thumb_mini ||
                        it.urls.px_128x128_90 ||
                        it.urls.px_480mw ||
                        '';
                }
                if (!imageUrl && it.image_urls) {
                    imageUrl =
                        it.image_urls.medium ||
                        it.image_urls.square_medium ||
                        it.image_urls.large ||
                        '';
                }

                if (!imageUrl) return null;

                return {
                    id: id,
                    title: it.title || '',
                    image_urls: {
                        medium: imageUrl
                    }
                };
            })
            .filter(Boolean);
    };

    const pickWebPopularSource = (body) => {
        const popular = body && body.popular;
        const popularList = [];

        if (popular) {
            if (typeof popular === 'object') {
                // Extract from nested object structure
                if (!Array.isArray(popular)) {
                    const possibleKeys = ['permanent', 'recent', 'illusts', 'data', 'items'];
                    for (let key of possibleKeys) {
                        if (Array.isArray(popular[key]) && popular[key].length > 0) {
                            popularList.push(...popular[key]);
                        }
                    }
                }
            }

            if (Array.isArray(popular) && popular.length > 0) {
                popularList.push(...popular);
            }
        }

        if (popularList.length > 0) return popularList;

        // Fallback chain when popular data is not available
        const illustMangaData = body?.illustManga?.data;
        if (Array.isArray(illustMangaData) && illustMangaData.length > 0) {
            return illustMangaData;
        }

        const illustData = body?.illust?.data;
        if (Array.isArray(illustData) && illustData.length > 0) {
            return illustData;
        }

        const bodyData = body?.data;
        if (Array.isArray(bodyData) && bodyData.length > 0) {
            return bodyData;
        }

        return [];
    };

    // Fetch popular works via Pixiv API
    const fetchWebPopularByCookie = (tag) => {
        const encodedTagPath = encodeURIComponent(tag);
        const params = new URLSearchParams({
            word: tag,
            order: POPULARITY_ORDER,
            mode: 'all',
            p: '1',
            s_mode: 's_tag',
            type: 'all',
            lang: document.documentElement.lang || 'en'
        });

        const url = '/ajax/search/artworks/' + encodedTagPath + '?' + params.toString();

        return fetch(url, {
            method: 'GET',
            credentials: 'include',
            headers: {
                'x-requested-with': 'XMLHttpRequest'
            }
        })
            .then((res) => {
                if (!res.ok) throw new Error('Web API HTTP ' + res.status);
                return res.json();
            })
            .then((json) => {
                if (json.error) throw new Error(json.message || 'Pixiv web API returned error');
                const body = json.body || {};
                const source = pickWebPopularSource(body);
                return { illusts: normalizeWebIllusts(source) };
            });
    };

    const getElementText = (el) => {
        return (el.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
    };

    const isPopularityControl = (el) => {
        const ga4Label = el.getAttribute('data-ga4-label');
        if (ga4Label === 'open_dropdown_button' || ga4Label === 'suggest_chip') {
            return false;
        }

        const entityId = el.getAttribute('data-ga4-entity-id');
        if (entityId === POPULARITY_ENTITY_ID) {
            return true;
        }

        const t = getElementText(el);
        const isPopularityText =
            (t.includes('sort by popularity') || t.includes('人気順')) &&
            !t.includes('male') &&
            !t.includes('female');

        if (isPopularityText) {
            const isClickable =
                el.tagName === 'BUTTON' ||
                el.tagName === 'A' ||
                el.getAttribute('role') === 'button';
            return isClickable;
        }

        return false;
    };

    // Handle popularity button clicks
    const onPopularityClick = (e) => {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation?.();

        const tag = getTagFromUrl();
        if (!tag) {
            alert('Unable to detect tag from URL.');
            return;
        }

        fetchWebPopularByCookie(tag)
            .then((data) => {
                const illusts = data.illusts || [];
                if (illusts.length > 0) {
                    showPopularModal(illusts);
                } else {
                    alert('No popular results found.');
                }
            })
            .catch((err) => {
                alert('Error loading popular results: ' + (err.message || String(err)));
            });
    };

    // Scan DOM and hijack popularity sort buttons
    const bindHijack = () => {
        const hasPopularBanner = Array.from(document.querySelectorAll('h3')).some((h3) =>
            h3.textContent.includes('Popular works')
        );

        const candidates = document.querySelectorAll(
            'button, a[role="button"], div[role="button"]'
        );
        candidates.forEach((el) => {
            const isPopular = isPopularityControl(el);

            if (isPopular) {
                if (!hasPopularBanner) {
                    el.remove();
                    return;
                }
                if (el.dataset[HIJACK_FLAG] === '1') return;
                el.dataset[HIJACK_FLAG] = '1';
                el.addEventListener('click', onPopularityClick, true);
            }
        });
    };

    // Monitor DOM changes for SPA navigation
    const installSpaHooks = () => {
        new MutationObserver(bindHijack).observe(document.documentElement, {
            childList: true,
            subtree: true
        });
        setInterval(bindHijack, POLL_INTERVAL);
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', bindHijack, { once: true });
    } else {
        bindHijack();
    }
    installSpaHooks();
})();