Multi-Platform Search Buttons for DLsite

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

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