☰

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 のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==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);
    }
})();