Multi-Platform Search Buttons for DLsite

Advanced search buttons for DLsite with settings dialog (gear icon).

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Multi-Platform Search Buttons for DLsite
// @namespace    http://tampermonkey.net/
// @version      4.12.0
// @description  Advanced search buttons for DLsite with settings dialog (gear icon).
// @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==

(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 ====================
    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);
                    });
                });
            } 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);
                    });
                });
            } 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() : '';
        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;
            }
        }

        const pidMatch = window.location.href.match(/product_id\/(RJ\d+)/i);
        const productId = pidMatch ? pidMatch[1] : '';

        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();
            };
        }

        document.getElementById('default-open').checked = SETTINGS.defaultOpen;
        modal.style.display = 'flex';
    }

    // ==================== MAIN FUNCTION ====================
    function addSearchButtons() {
        const data = getProductData();
        if (!data.gameTitle || !data.productId) {
            console.warn('[DLsite Search Buttons] Could not extract required data.');
            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';

            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.0';

            dropdownContainer.append(header, content, version);

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

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

        // Circle Buttons
        if (data.circleName && data.circleLink) {
            const wrapper = document.createElement('span');
            wrapper.className = 'circle-btn-wrapper';

            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.parentNode;
            makerContainer.parentNode.insertBefore(wrapper, makerContainer.nextSibling);
        }
    }

    // ==================== RUN ====================
    if (window.location.href.includes('dlsite.com')) {
        const observer = new MutationObserver((_, obs) => {
            if (document.querySelector('h1')) {
                addSearchButtons();
                obs.disconnect();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }
})();