VNDB Site Search Buttons

Adds Google exact-phrase search buttons on VNDB visual novel pages for f95zone.to, www.ryuugames.com, www.anime-sharing.com, and store.steampowered.com. Separate buttons for main title and original title (if different), with original title buttons on the top row and main title on the bottom row for clean alignment.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         VNDB Site Search Buttons
// @namespace    https://greasyfork.org/users/1071569
// @version      1.5.0
// @description  Adds Google exact-phrase search buttons on VNDB visual novel pages for f95zone.to, www.ryuugames.com, www.anime-sharing.com, and store.steampowered.com. Separate buttons for main title and original title (if different), with original title buttons on the top row and main title on the bottom row for clean alignment.
// @author       FunkyJustin
// @license      MIT
// @match        https://vndb.org/v*
// @grant        none
// @run-at       document-end
// ==/UserScript==

/*
Update History:
- v1.5.0: Reverted to separate buttons per title; original title buttons now appear on the top row, main title on the bottom row (grid layout for perfect 4-column alignment); improved spacing and responsiveness. Total lines ~318.
- v1.4.0: Reduced to one button per site (4 total); each searched both titles with OR when available; tightened layout for single row. Total lines ~278.
- v1.3.0: Added store.steampowered.com (Steam Store). Total lines ~292.
- v1.2.0: Added automatic light/dark theme detection; buttons and header adapt to VNDB's current theme. Total lines ~282.
- v1.1.1: Redesigned button styling to match VNDB's default light theme. Total lines ~232.
- v1.1.0: Added www.anime-sharing.com to site searches. Total lines ~255.
- v1.0.3: More robust title detection using 'article h1' and 'article .alttitle' selectors. Total lines ~238.
- v1.0.2: Fixed visibility on pages using <h2 class="alttitle">; added prominent header and better container styling. Total lines ~215.
- v1.0.1: Fixed main title detection; wrapped titles in quotes for exact Google searches. Total lines ~168.
- v1.0.0: Initial release. Total lines ~142.
*/

(function () {
    'use strict';

    // Run only on actual VN pages (/v12345)
    if (!/^\/v\d+$/.test(location.pathname)) return;

    // Specific selectors to guarantee correct elements
    const mainTitleEl = document.querySelector('article h1');
    if (!mainTitleEl) return;

    const mainTitle = mainTitleEl.textContent.trim();
    if (!mainTitle) return;

    let originalTitle = '';

    // Primary: .alttitle inside article (covers both <h2> and older <p>)
    const altEl = document.querySelector('article .alttitle');
    if (altEl) {
        originalTitle = altEl.textContent.trim();
    }

    // Fallback: extract original (usually Japanese) from titles table/details
    if (!originalTitle || originalTitle === mainTitle) {
        const titlesTd = document.querySelector('article td.titles');
        if (titlesTd) {
            const details = titlesTd.querySelector('details');
            if (details) {
                const jaSpan = details.querySelector('span[lang="ja"]');
                if (jaSpan) {
                    originalTitle = jaSpan.textContent.trim();
                } else {
                    const jaRow = details.querySelector('abbr.icon-lang-ja')?.closest('tr');
                    if (jaRow) {
                        const titleCell = jaRow.querySelector('td:nth-child(2)');
                        if (titleCell) {
                            originalTitle = titleCell.textContent.trim().split('\n')[0].trim();
                        }
                    }
                }
            }
        }
    }

    // Detect current VNDB theme (light or dark) via background luminance
    function isDarkMode() {
        const style = getComputedStyle(document.body);
        const bg = style.backgroundColor;
        const match = bg.match(/\d+/g);
        if (!match || match.length < 3) return false;
        const [r, g, b] = match.map(Number);
        const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
        return luminance < 0.5;
    }

    const dark = isDarkMode();

    // Theme-aware colors
    const headerColor = dark ? '#dddddd' : '#212529';

    const buttonBg = dark ? '#2c3e50' : '#f8f9fa';
    const buttonText = dark ? '#ecf0f1' : '#212529';
    const buttonBorder = dark ? '#34495e' : '#dee2e6';
    const buttonHoverBg = dark ? '#34495e' : '#e9ecef';
    const buttonHoverBorder = dark ? '#3e5c76' : '#ced4da';
    const buttonShadow = dark ? '0 2px 6px rgba(0,0,0,0.4)' : '0 2px 4px rgba(0,0,0,0.05)';

    // Configurable sites
    const sites = [
        { domain: 'f95zone.to',          name: 'F95Zone' },
        { domain: 'www.ryuugames.com',   name: 'RyuuGames' },
        { domain: 'www.anime-sharing.com', name: 'Anime-Sharing' },
        { domain: 'store.steampowered.com', name: 'Steam' }
    ];

    // Main container (column direction for header + grid)
    const container = document.createElement('div');
    container.style.cssText = `
        margin: 24px 0;
        display: flex;
        flex-direction: column;
        gap: 16px;
        font-size: 15px;
    `;

    // Header
    const header = document.createElement('strong');
    header.textContent = 'Quick Site Searches:';
    header.style.cssText = `color: ${headerColor}; align-self: flex-start;`;
    container.appendChild(header);

    // Grid for buttons – 4 columns, perfect alignment
    const grid = document.createElement('div');
    grid.style.cssText = `
        display: grid;
        grid-template-columns: repeat(4, minmax(140px, 1fr));
        gap: 12px 16px;
        width: 100%;
    `;

    // Button creator
    function createButton(title, site, labelSuffix) {
        if (!title) return null;

        const query = encodeURIComponent(`"${title}" site:${site.domain}`);
        const url = 'https://www.google.com/search?q=' + query;

        const a = document.createElement('a');
        a.href = url;
        a.target = '_blank';
        a.rel = 'noopener noreferrer';
        a.textContent = `${site.name} ${labelSuffix}`;
        a.title = `Google exact search: "${title}" on ${site.domain}`;
        a.style.cssText = `
            padding: 10px 12px;
            background: ${buttonBg};
            color: ${buttonText};
            text-decoration: none;
            border-radius: 6px;
            border: 1px solid ${buttonBorder};
            font-weight: 600;
            text-align: center;
            transition: all 0.2s ease;
            box-shadow: ${buttonShadow};
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        `;

        a.onmouseover = () => {
            a.style.background = buttonHoverBg;
            a.style.borderColor = buttonHoverBorder;
            a.style.boxShadow = dark ? '0 4px 10px rgba(0,0,0,0.5)' : '0 4px 8px rgba(0,0,0,0.1)';
            a.style.transform = 'translateY(-1px)';
        };
        a.onmouseout = () => {
            a.style.background = buttonBg;
            a.style.borderColor = buttonBorder;
            a.style.boxShadow = buttonShadow;
            a.style.transform = 'none';
        };

        return a;
    }

    // Add original title buttons first (top row) if available and different
    if (originalTitle && originalTitle !== mainTitle) {
        sites.forEach(site => {
            const btn = createButton(originalTitle, site, '(Original Title)');
            if (btn) grid.appendChild(btn);
        });
    }

    // Always add main title buttons (bottom row, or only row if no original)
    sites.forEach(site => {
        const btn = createButton(mainTitle, site, '(Main Title)');
        if (btn) grid.appendChild(btn);
    });

    container.appendChild(grid);

    // Insert right after the last visible title element
    const insertPoint = altEl || mainTitleEl;
    if (insertPoint && insertPoint.parentNode) {
        insertPoint.after(container);
    }
})();