☰

Multi-Platform Search Buttons for DLsite

Advanced search buttons for DLsite (maniax/pro) with settings dialog (gear icon). Supports RJ/BJ/VJ/RE/VE product IDs. Search F95Zone, Google, OtomiGames, Ryuugames + copy helpers.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Multi-Platform Search Buttons for DLsite
// @namespace    http://tampermonkey.net/
// @version      4.12.1
// @description  Advanced search buttons for DLsite (maniax/pro) with settings dialog (gear icon). Supports RJ/BJ/VJ/RE/VE product IDs. Search F95Zone, Google, OtomiGames, Ryuugames + copy helpers.
// @author       FunkyJustin
// @match        https://www.dlsite.com/maniax/work/=/product_id/*
// @match        https://www.dlsite.com/pro/work/=/product_id/*
// @match        https://www.dlsite.com/maniax/announce/=/product_id/*
// @match        https://www.dlsite.com/pro/announce/=/product_id/*
// @match        https://f95zone.to/*
// @grant        GM_openInTab
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==
/*
Update History:
- v4.12.1: Fixed product ID extraction to support VJ/BJ/RE/VE (was hard-coded to RJ only). Regex now matches any two-letter prefix + digits. Verified on pro/work VJ* pages. Total lines ~320.
- v4.12.0: Previous release (RJ-only extraction, settings dialog, multi-group buttons).
*/
(function () {
    'use strict';

    // ==================== F95Zone Handler ====================
    if (window.location.href.includes('f95zone.to')) {
        const urlParams = new URLSearchParams(window.location.search);
        const query = urlParams.get('query');
        if (query) {
            const form = document.createElement('form');
            form.method = 'POST';
            form.action = 'https://f95zone.to/search/search';
            form.style.display = 'none';
            const inputs = {
                'keywords': query,
                'order': 'relevance',
                '_xfToken': document.querySelector('input[name="_xfToken"]')?.value || ''
            };
            for (const [name, value] of Object.entries(inputs)) {
                const input = document.createElement('input');
                input.type = 'hidden';
                input.name = name;
                input.value = value;
                form.appendChild(input);
            }
            document.body.appendChild(form);
            fetch(form.action, {
                method: 'POST',
                body: new FormData(form),
                redirect: 'follow'
            })
            .then(response => {
                const match = response.url.match(/\/search\/(\d+)\//);
                window.location.href = match
                    ? `https://f95zone.to/search/${match[1]}/?q=${encodeURIComponent(query)}&o=relevance`
                    : `https://f95zone.to/search/?q=${encodeURIComponent(query)}&o=relevance`;
            })
            .catch(() => {
                window.location.href = `https://f95zone.to/search/?q=${encodeURIComponent(query)}&o=relevance`;
            })
            .finally(() => {
                if (form.parentNode) document.body.removeChild(form);
            });
        }
        return;
    }

    // ==================== SETTINGS ====================
    const SETTINGS = {
        get defaultOpen() {
            return GM_getValue('dropdown_default_open', true);
        },
        set defaultOpen(value) {
            GM_setValue('dropdown_default_open', value);
        }
    };

    // ==================== CONFIGURATION ====================
    // Easy to extend: just add a new group or button object.
    const CONFIG = {
        groups: [
            {
                name: 'F95Zone',
                buttons: [
                    { text: 'Search on F95Zone', url: (d) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/search=${encodeURIComponent(d.gameTitle)}`, icon: 'f95zone' },
                    { text: 'Search on F95Zone Forums', query: (d) => d.gameTitle, isF95Forum: true, icon: 'f95zone' },
                    { text: 'Search Product ID on F95Zone Forums', query: (d) => d.productId, isF95Forum: true, icon: 'f95zone' },
                ]
            },
            {
                name: 'Utilities',
                buttons: [
                    { text: 'Copy Product ID', action: 'copyPID' },
                    { text: 'Search Product ID on Google', url: (d) => `https://www.google.com/search?q="${d.productId}"`, icon: 'google' },
                    { text: 'Search Title on Google', url: (d) => `https://www.google.com/search?q=${encodeURIComponent(d.gameTitle)}`, icon: 'google' },
                    { text: 'Copy Title', action: 'copyTitle' },
                ]
            },
            {
                name: 'OtomiGames',
                buttons: [
                    { text: 'Search on OtomiGames', url: (d) => `https://otomi-games.com/?s=${encodeURIComponent(d.gameTitle)}`, icon: 'otomi' },
                    { text: 'Search Product ID on OtomiGames', url: (d) => `https://otomi-games.com/?s=${encodeURIComponent(d.productId)}`, icon: 'otomi' },
                ]
            },
            {
                name: 'Ryuugames',
                buttons: [
                    { text: 'Search on Ryuugames', url: (d) => `https://www.ryuugames.com/?s=${encodeURIComponent(d.gameTitle)}`, icon: 'ryuugames' },
                    { text: 'Search Product ID on Ryuugames', url: (d) => `https://www.ryuugames.com/?s=${encodeURIComponent(d.productId)}`, icon: 'ryuugames' },
                ]
            }
        ]
    };

    // ==================== CSS ====================
    const style = document.createElement('style');
    style.innerHTML = `
        :root {
            --button-bg-start: #D6BCFA;
            --button-bg-end: #FBB6CE;
            --button-shadow: rgba(0, 0, 0, 0.1);
            --button-border: #B794F4;
            --dropdown-bg: #f9f9f9;
        }
        .search-btn {
            display: inline-flex;
            align-items: center;
            padding: 5px 9px;
            margin: 3px;
            background: linear-gradient(45deg, var(--button-bg-start), var(--button-bg-end));
            color: #800080 !important;
            text-decoration: none;
            border-radius: 12px;
            border: 1px solid var(--button-border);
            box-shadow: 0 2px 4px var(--button-shadow);
            font-family: Arial, sans-serif;
            font-size: 14.5px;
            font-weight: bold;
            transition: all 0.2s;
            cursor: pointer;
            user-select: auto !important;
            white-space: nowrap;
        }
        .search-btn:hover {
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
            opacity: 0.95;
        }
        .search-btn img {
            width: 18px;
            height: 18px;
            margin-right: 6px;
        }
        .dropdown-container {
            max-width: 680px;
            margin: 12px 0 20px 0;
            position: relative;
        }
        .dropdown-header {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .dropdown-toggle {
            display: flex;
            align-items: center;
            padding: 6px 12px;
            background-color: #E91E63;
            color: #fff;
            border: none;
            border-radius: 12px;
            font-family: Arial, sans-serif;
            font-size: 14px;
            font-weight: bold;
            cursor: pointer;
            flex: 1;
        }
        .dropdown-toggle::after {
            content: ' ▼';
            margin-left: 8px;
            transition: transform 0.3s;
        }
        .dropdown-toggle.collapsed::after {
            transform: rotate(180deg);
        }
        .gear-btn {
            background: none;
            border: none;
            font-size: 18px;
            cursor: pointer;
            padding: 6px;
            border-radius: 50%;
            color: #fff;
            opacity: 0.9;
        }
        .gear-btn:hover {
            background: rgba(255,255,255,0.25);
        }
        .dropdown-content {
            display: block;
            background-color: var(--dropdown-bg);
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            padding: 12px;
            margin-top: 6px;
        }
        .dropdown-content.hidden {
            display: none;
        }
        .version-label {
            position: absolute;
            bottom: 6px;
            right: 12px;
            font-size: 11px;
            color: #888;
            pointer-events: none;
        }
        .button-group { margin-bottom: 12px; }
        .group-label {
            font-family: Arial, sans-serif;
            font-size: 14px;
            font-weight: bold;
            color: #333;
            margin-bottom: 6px;
        }
        .button-group-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
            gap: 6px;
        }
        .circle-btn-wrapper {
            display: flex;
            align-items: center;
            margin-top: 6px;
            gap: 6px;
            flex-wrap: wrap;
        }
        /* Settings Modal */
        .settings-modal {
            position: fixed;
            top: 0; left: 0;
            width: 100%; height: 100%;
            background: rgba(0,0,0,0.65);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 99999;
        }
        .modal-content {
            background: white;
            padding: 24px;
            border-radius: 12px;
            width: 340px;
            box-shadow: 0 8px 30px rgba(0,0,0,0.3);
        }
        .modal-content h3 { margin: 0 0 16px 0; color: #333; }
        .modal-content label { display: block; margin: 14px 0; font-size: 15px; cursor: pointer; }
        .modal-buttons { margin-top: 24px; text-align: right; }
        .modal-buttons button {
            padding: 8px 18px;
            margin-left: 10px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
        }
        .modal-buttons .cancel { background: #ddd; color: #333; }
        .modal-buttons .save { background: #E91E63; color: white; }
    `;
    document.head.appendChild(style);

    const icons = {
        f95zone: 'https://f95zone.to/favicon.ico',
        ryuugames: 'https://www.ryuugames.com/wp-content/uploads/2020/05/cropped-ryuugames_logo-1-32x32.png',
        otomi: 'https://otomi-games.com/favicon.ico',
        google: 'https://www.google.com/favicon.ico'
    };

    // ==================== HELPERS ====================
    function createButton(btnConfig, data) {
        let element;
        if (btnConfig.isF95Forum) {
            element = document.createElement('button');
            element.type = 'button';
            element.addEventListener('mousedown', (e) => {
                if (e.button === 0 || e.button === 1) {
                    e.preventDefault();
                    GM_openInTab(`https://f95zone.to/?query=${encodeURIComponent(btnConfig.query(data))}`, {
                        active: e.button === 0,
                        setParent: true
                    });
                }
            });
        } else {
            element = document.createElement('a');
            element.href = btnConfig.url ? btnConfig.url(data) : '#';
            element.target = '_blank';
        }
        element.className = 'search-btn';
        element.title = btnConfig.text;
        if (btnConfig.icon && icons[btnConfig.icon]) {
            const img = document.createElement('img');
            img.src = icons[btnConfig.icon];
            element.appendChild(img);
        }
        element.appendChild(document.createTextNode(btnConfig.text));
        return element;
    }

    function createGroup(groupConfig, data) {
        const group = document.createElement('div');
        group.className = 'button-group';
        const label = document.createElement('div');
        label.className = 'group-label';
        label.textContent = groupConfig.name;
        group.appendChild(label);
        const grid = document.createElement('div');
        grid.className = 'button-group-grid';
        groupConfig.buttons.forEach(btnConfig => {
            let btn;
            if (btnConfig.action === 'copyPID') {
                btn = createButton({text: btnConfig.text}, data);
                btn.addEventListener('click', (e) => {
                    e.preventDefault();
                    navigator.clipboard.writeText(data.productId).then(() => {
                        const orig = btn.textContent;
                        btn.textContent = '✓ Copied!';
                        setTimeout(() => btn.textContent = orig, 1800);
                    }).catch(() => {
                        // Fallback for rare clipboard failures
                        const ta = document.createElement('textarea');
                        ta.value = data.productId;
                        document.body.appendChild(ta);
                        ta.select();
                        document.execCommand('copy');
                        document.body.removeChild(ta);
                        const orig = btn.textContent;
                        btn.textContent = '✓ Copied!';
                        setTimeout(() => btn.textContent = orig, 1800);
                    });
                });
            } else if (btnConfig.action === 'copyTitle') {
                btn = createButton({text: btnConfig.text}, data);
                btn.addEventListener('click', (e) => {
                    e.preventDefault();
                    navigator.clipboard.writeText(data.gameTitle).then(() => {
                        const orig = btn.textContent;
                        btn.textContent = '✓ Copied!';
                        setTimeout(() => btn.textContent = orig, 1800);
                    }).catch(() => {
                        const ta = document.createElement('textarea');
                        ta.value = data.gameTitle;
                        document.body.appendChild(ta);
                        ta.select();
                        document.execCommand('copy');
                        document.body.removeChild(ta);
                        const orig = btn.textContent;
                        btn.textContent = '✓ Copied!';
                        setTimeout(() => btn.textContent = orig, 1800);
                    });
                });
            } else {
                btn = createButton(btnConfig, data);
            }
            grid.appendChild(btn);
        });
        group.appendChild(grid);
        return group;
    }

    function getProductData() {
        const titleEl = document.querySelector('h1#work_name') ||
                       document.querySelector('h1[itemprop="name"]') ||
                       document.querySelector('h1');
        let gameTitle = titleEl ? titleEl.innerText.trim() : '';
        // Strip trailing [tags] that sometimes appear
        gameTitle = gameTitle.replace(/\s*\[.*?\]\s*$/, '').trim();

        const circleSelectors = ['span.maker_name a', 'span[itemprop="brand"] a', 'a[href*="maker_id"]'];
        let circleName = '';
        let circleLink = null;
        for (const sel of circleSelectors) {
            const el = document.querySelector(sel);
            if (el) {
                circleName = el.innerText.trim();
                circleLink = el;
                break;
            }
        }

        // FIXED: support RJ / BJ / VJ / RE / VE / any two-letter + digits
        const pidMatch = window.location.href.match(/product_id\/([A-Z]{2}\d+)/i);
        const productId = pidMatch ? pidMatch[1].toUpperCase() : '';

        return { gameTitle, circleName, productId, circleLink };
    }

    // ==================== SETTINGS DIALOG ====================
    function showSettings() {
        let modal = document.getElementById('dlsite-settings-modal');
        if (!modal) {
            modal = document.createElement('div');
            modal.id = 'dlsite-settings-modal';
            modal.className = 'settings-modal';
            modal.innerHTML = `
                <div class="modal-content">
                    <h3>⚙️ Script Settings</h3>
                    <label>
                        <input type="checkbox" id="default-open">
                        Open "Search Options" dropdown by default
                    </label>
                    <div class="modal-buttons">
                        <button class="cancel" id="modal-cancel">Cancel</button>
                        <button class="save" id="modal-save">Save & Reload</button>
                    </div>
                </div>
            `;
            document.body.appendChild(modal);
            document.getElementById('modal-cancel').onclick = () => modal.style.display = 'none';
            document.getElementById('modal-save').onclick = () => {
                SETTINGS.defaultOpen = document.getElementById('default-open').checked;
                modal.style.display = 'none';
                location.reload();
            };
            // Close on backdrop click
            modal.addEventListener('click', (e) => {
                if (e.target === modal) modal.style.display = 'none';
            });
        }
        document.getElementById('default-open').checked = SETTINGS.defaultOpen;
        modal.style.display = 'flex';
    }

    // ==================== MAIN FUNCTION ====================
    function addSearchButtons() {
        // Prevent double-injection (multiple observers / SPA navigation)
        if (document.querySelector('.dropdown-container[data-dlsite-search-buttons]')) {
            return;
        }

        const data = getProductData();
        if (!data.gameTitle || !data.productId) {
            console.warn('[DLsite Search Buttons] Could not extract required data.', {
                title: data.gameTitle,
                productId: data.productId,
                url: location.href
            });
            return;
        }

        const titleContainer = document.querySelector('.base_title_br') ||
                              document.querySelector('h1')?.closest('div, section, header');
        if (titleContainer) {
            const dropdownContainer = document.createElement('div');
            dropdownContainer.className = 'dropdown-container';
            dropdownContainer.dataset.dlsiteSearchButtons = '1';

            const header = document.createElement('div');
            header.className = 'dropdown-header';

            const toggle = document.createElement('button');
            toggle.className = 'dropdown-toggle';
            toggle.textContent = 'Search Options';

            const gear = document.createElement('button');
            gear.className = 'gear-btn';
            gear.innerHTML = '⚙️';
            gear.title = 'Script Settings';
            gear.onclick = (e) => {
                e.stopImmediatePropagation();
                showSettings();
            };

            header.append(toggle, gear);

            const content = document.createElement('div');
            content.className = 'dropdown-content';
            if (!SETTINGS.defaultOpen) {
                content.classList.add('hidden');
                toggle.classList.add('collapsed');
            }

            toggle.addEventListener('click', () => {
                content.classList.toggle('hidden');
                toggle.classList.toggle('collapsed');
            });

            const version = document.createElement('div');
            version.className = 'version-label';
            version.textContent = 'v4.12.1';

            dropdownContainer.append(header, content, version);

            CONFIG.groups.forEach(group => {
                content.appendChild(createGroup(group, data));
            });

            titleContainer.parentNode.insertBefore(dropdownContainer, titleContainer.nextSibling);
        }

        // Circle / Maker buttons
        if (data.circleName && data.circleLink) {
            // Avoid double-injection
            if (document.querySelector('.circle-btn-wrapper[data-dlsite-circle-buttons]')) return;

            const wrapper = document.createElement('span');
            wrapper.className = 'circle-btn-wrapper';
            wrapper.dataset.dlsiteCircleButtons = '1';

            const circleConfigs = [
                { text: 'Search Circle on F95Zone', url: (d) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/creator=${encodeURIComponent(d.circleName)}`, icon: 'f95zone' },
                { text: 'Search Circle on F95Zone Forums', query: (d) => d.circleName, isF95Forum: true, icon: 'f95zone' },
                { text: 'Search Circle on Ryuugames', url: (d) => `https://www.ryuugames.com/?s=${encodeURIComponent(d.circleName)}`, icon: 'ryuugames' },
                { text: 'Search Circle on OtomiGames', url: (d) => `https://otomi-games.com/?s=${encodeURIComponent(d.circleName)}`, icon: 'otomi' },
            ];
            circleConfigs.forEach(cfg => wrapper.appendChild(createButton(cfg, data)));

            const makerContainer = data.circleLink.closest('span.maker_name') ||
                                   data.circleLink.closest('span[itemprop="brand"]') ||
                                   data.circleLink.parentNode;
            makerContainer.parentNode.insertBefore(wrapper, makerContainer.nextSibling);
        }
    }

    // ==================== RUN ====================
    if (window.location.href.includes('dlsite.com')) {
        // Immediate try (in case page is already ready)
        if (document.querySelector('h1#work_name, h1[itemprop="name"]')) {
            addSearchButtons();
        }

        // Observer for dynamic content / late hydration
        const observer = new MutationObserver((_, obs) => {
            if (document.querySelector('h1#work_name, h1[itemprop="name"]')) {
                addSearchButtons();
                obs.disconnect();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });

        // Safety timeout – disconnect after 15 s even if nothing found
        setTimeout(() => observer.disconnect(), 15000);
    }
})();