Bangumi jump to multiple sites

在Bangumi游戏条目上添加实用的按钮

目前為 2025-03-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Bangumi jump to multiple sites
// @namespace    http://tampermonkey.net/
// @version      0.9.5
// @description  在Bangumi游戏条目上添加实用的按钮
// @author       Sedoruee
// @include      /https?:\/\/(bgm\.tv|bangumi\.tv|chii\.in).*/
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 获取条目类型和标题
    const subjectType = document.querySelector('.nameSingle > .grey')?.textContent;
    const gameTitle = document.querySelector('.nameSingle > a')?.textContent;

    if (subjectType === '游戏' && gameTitle) {
        const nameSingle = document.querySelector('.nameSingle');

        // 添加统一样式
        GM_addStyle(`
            .combined-button, .multisearch-select-container .combined-button, .jump-button { /* 统一高度应用到所有按钮 */
                display: inline-flex;
                align-items: center;
                margin-left: 5px;
                border: 1px solid #ccc;
                border-radius: 3px;
                background-color: #f0f0f0;
                color: black;
                font-size: 14px;
                cursor: pointer;
                height: 32px; /* 统一按钮高度 */
                box-sizing: border-box;
                overflow: hidden;
            }

            .button-name {
                padding: 5px 10px;
                border: none;
                background-color: transparent;
                color: inherit;
                font-size: inherit;
                cursor: pointer;
                text-align: center;
                flex-grow: 1;
                flex-shrink: 0;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                min-width: 50px;
            }

            .select-arrow {
                -webkit-appearance: none;
                -moz-appearance: none;
                appearance: none;
                background-color: transparent;
                border: none;
                padding: 5px 10px;
                cursor: pointer;
                font-size: inherit;
                color: inherit;
                position: relative;
                z-index: 1;
                width: 20px;
                flex-shrink: 0;
                /* 使用 background-image 替代 ::after 实现箭头 */
                background-image: url('data:image/svg+xml;utf8,<svg fill="black" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
                background-repeat: no-repeat;
                background-position: center; /* 居中箭头 */
                border-left: 1px solid #ddd; /* 弱化左边框颜色 */
                margin-left: 2px; /* 稍微减小箭头区域左边距,更紧凑 */
            }

            .select-arrow::-ms-expand {
                display: none;
            }


            .combined-button:hover, .multisearch-select-container .combined-button:hover, .jump-button:hover,
            .button-name:hover, .select-arrow:hover {
                background-color: #e0e0e0;
            }
            .combined-button:active, .multisearch-select-container .combined-button:active, .jump-button:active,
            .button-name:active, .select-arrow:active {
                background-color: #d0d0d0;
            }


            .jump-button { /* 确保 jump-button 也应用统一高度,虽然在上面已经统一设置了,这里再次强调 */
                height: 32px;
                line-height: 32px; /* 垂直居中文字,如果需要 */
                padding-top: 0;
                padding-bottom: 0;
                display: inline-flex; /* 使其可以垂直对齐,虽然默认已经是 inline-block */
                align-items: center; /* 垂直居中按钮内的文字 */
                text-align: center; /* 确保文本居中 */
            }


            .multisearch-select-container {
                display: inline-block;
                position: relative;
                margin-left: 5px;
            }


            .multisearch-select-dropdown {
                position: absolute;
                top: 100%;
                left: 0;
                z-index: 10;
                border: 1px solid #ccc;
                border-radius: 3px;
                background-color: white;
                padding: 5px 0;
                min-width: 150px;
                display: none;
            }

            .multisearch-select-dropdown.show {
                display: block;
            }

            .multisearch-select-dropdown label {
                display: block;
                padding: 5px 15px;
                cursor: pointer;
            }

            .multisearch-select-dropdown label:hover {
                background-color: #f0f0f0;
            }
        `);

        // 辅助函数:创建按钮
        const createButton = (text, clickHandler) => {
            const button = document.createElement('button');
            button.textContent = text;
            button.className = 'jump-button';
            if (clickHandler) {
                button.addEventListener('click', clickHandler);
            }
            nameSingle.appendChild(button);
            return button;
        };

        // 辅助函数:创建合并按钮 (名称左侧点击,箭头右侧下拉)
        const createCombinedButton = (defaultSiteValue, siteOptions, storageKey, openAction) => {
            const container = document.createElement('div');
            container.className = 'combined-button'; // 统一class

            const nameButton = document.createElement('button');
            nameButton.className = 'button-name';
            container.appendChild(nameButton);

            const selectArrow = document.createElement('select');
            selectArrow.className = 'select-arrow';
            container.appendChild(selectArrow);

            siteOptions.forEach(site => {
                const option = document.createElement('option');
                option.value = site.value;
                option.text = site.text;
                selectArrow.appendChild(option);
            });

            // 读取上次选择的站点
            const storedSite = GM_getValue(storageKey, defaultSiteValue);
            selectArrow.value = storedSite;
            updateButtonName(nameButton, selectArrow.options[selectArrow.selectedIndex].text); // 初始化按钮名称

            // 监听下拉框变化
            selectArrow.addEventListener('change', function() {
                GM_setValue(storageKey, this.value);
                updateButtonName(nameButton, this.options[this.selectedIndex].text); // 更新按钮名称
            });

            // 初始化点击事件
            nameButton.addEventListener('click', () => {
                openAction(selectArrow.value);
            });
             // selectArrow点击时打开下拉菜单,并阻止事件冒泡,避免触发nameButton的点击
            selectArrow.addEventListener('click', function(event) {
                this.focus(); // 聚焦select元素以打开下拉菜单
                event.stopPropagation(); // 阻止事件冒泡到容器或nameButton
            });
            container.addEventListener('click', function(event) {
                if (!container.contains(event.target)) {
                    selectArrow.blur(); // 当点击容器外部时,移除select焦点,关闭下拉菜单
                }
            });


            function updateButtonName(button, siteName) {
                button.textContent = siteName;
            }


            nameSingle.appendChild(container);
            return container;
        };


        // 辅助函数:创建多搜索下拉选择框 (类似合并按钮样式)
        const createMultiSearchSelect = () => {
            const container = document.createElement('div');
            container.className = 'multisearch-select-container';

            const buttonArea = document.createElement('div');
            buttonArea.className = 'combined-button'; // 统一class
            container.appendChild(buttonArea);


            const nameButton = document.createElement('button');
            nameButton.className = 'button-name';
            nameButton.textContent = '多搜索';
            buttonArea.appendChild(nameButton);

            const selectArrow = document.createElement('button'); // 使用 button 替代 select,用于触发下拉菜单
            selectArrow.className = 'select-arrow';
            buttonArea.appendChild(selectArrow);


            const dropdown = document.createElement('div');
            dropdown.className = 'multisearch-select-dropdown';
            dropdown.id = 'multisearchDropdown';
            container.appendChild(dropdown);


            const sites = [
                { value: 'ai2', text: 'ai2.moe', url: `https://www.ai2.moe/search/?q=${encodeURIComponent(gameTitle)}&updated_after=any&sortby=relevancy&search_in=titles`, checked: true },
                { value: 'moyu', text: 'moyu.moe', url: `https://www.moyu.moe/search?q=${encodeURIComponent(gameTitle)}`, checked: true },
                { value: '2dfan_preview', text: '2dfan(预览)', url: `https://2dfan.com/subjects/search?keyword=${encodeURIComponent(gameTitle)}`, checked: true }
            ];

            // 读取上次选择的站点
            let storedMultiSearchSites = GM_getValue('multiSearchSites', sites.filter(site => site.checked).map(site => site.value).join(','));
            let selectedSitesValues = storedMultiSearchSites ? storedMultiSearchSites.split(',') : sites.filter(site => site.checked).map(site => site.value);


            sites.forEach(site => {
                const label = document.createElement('label');
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.value = site.value;
                checkbox.checked = selectedSitesValues.includes(site.value); // 根据存储状态设置选中

                checkbox.addEventListener('change', function() {
                    let currentSelectedValues = Array.from(dropdown.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
                    GM_setValue('multiSearchSites', currentSelectedValues.join(','));
                });


                label.appendChild(checkbox);
                label.appendChild(document.createTextNode(' ' + site.text));
                dropdown.appendChild(label);
            });


            selectArrow.addEventListener('click', function(event) {
                dropdown.classList.toggle('show');
                event.stopPropagation(); // 防止事件冒泡到 document 导致立即关闭下拉菜单
            });
            nameButton.addEventListener('click', () => {
                // 延迟打开预览窗口,如果需要立即打开,可以直接调用 openPreviewWindows();
                setTimeout(() => {
                    openPreviewWindows();
                }, 200);
            });


            // 点击文档其他地方关闭下拉菜单
            document.addEventListener('click', function(event) {
                if (!container.contains(event.target)) {
                    dropdown.classList.remove('show');
                }
            });


            return container;
        };


        // 添加换行符
        nameSingle.appendChild(document.createElement('br'));

        // VNDB按钮
        createButton('VNDB', () => {
            window.location.href = `https://vndb.org/v?q=${encodeURIComponent(gameTitle)}`;
        });

        // Hitomi按钮
        createButton('Hitomi', () => {
            // 1. Hitomi之前先把所有标题的特殊字符转为空格,只搜索最长一块
            const delimitersRegex = /[~]+/g; // 正则表达式匹配一个或多个 -, ~ 字符
            const titleSegments = gameTitle.split(delimitersRegex);
            let longestSegment = "";
            let maxLength = 0;
            for (const segment of titleSegments) {
                if (segment.length > maxLength) {
                    maxLength = segment.length;
                    longestSegment = segment;
                }
            }
            const hitomiTitle = longestSegment.trim(); // 使用最长文本段, trim去除首尾空格
            window.location.href = `https://hitomi.la/search.html?type%3Agamecg%20${encodeURIComponent(hitomiTitle)}%20orderby%3Apopular%20orderbykey%3Ayear`;
        });

        // 魔皇地狱/zi0.cc 合并按钮
        createCombinedButton(
            'mhdy',
            [
                { value: 'mhdy', text: '魔皇地狱' },
                { value: 'zi0', text: 'zi0.cc' }
            ],
            'selectedSite',
            (selectedSite) => {
                GM_setClipboard(gameTitle);
                let siteUrl;
                if (selectedSite === 'mhdy') {
                    siteUrl = 'https://pan1.mhdy.shop/';
                } else if (selectedSite === 'zi0') {
                    siteUrl = 'https://zi0.cc/';
                } else {
                    siteUrl = 'https://pan1.mhdy.shop/'; // 默认魔皇地狱
                }
                window.open(siteUrl);
            }
        );


        // 2dfan按钮
        createButton('2dfan(网页)', () => {
            GM_setClipboard(gameTitle);
            window.open(`https://2dfan.com/subjects/search?keyword=${encodeURIComponent(gameTitle)}`);
        });

        // “多搜索” 按钮 (合并样式)
        const multiSearchSelectContainer = createMultiSearchSelect();
        nameSingle.appendChild(multiSearchSelectContainer);


        let previewWindows = [];
        let monitorInterval = null;
        let focusMonitorDelayTimer = null;


        // 打开预览窗口
        function openPreviewWindows() {
            // ... (多搜索窗口打开和管理逻辑 - 与之前版本相同,无需修改)
            closePreviewWindows();

            // 获取选中的多搜索站点
            const dropdownElement = multiSearchSelectContainer.querySelector('.multisearch-select-dropdown');
            const selectedCheckboxes = dropdownElement.querySelectorAll('input[type="checkbox"]:checked');
            const selectedSitesValues = Array.from(selectedCheckboxes).map(cb => cb.value);

            const sites = [
                { value: 'ai2', text: 'ai2.moe', url: `https://www.ai2.moe/search/?q=${encodeURIComponent(gameTitle)}&updated_after=any&sortby=relevancy&search_in=titles` },
                { value: 'moyu', text: 'moyu.moe', url: `https://www.moyu.moe/search?q=${encodeURIComponent(gameTitle)}` },
                { value: '2dfan_preview', text: '2dfan(预览)', url: `https://2dfan.com/subjects/search?keyword=${encodeURIComponent(gameTitle)}` }
            ];


            const urls = sites.filter(site => selectedSitesValues.includes(site.value)).map(site => site.url);


            if (urls.length === 0) {
                alert("请选择至少一个多搜索站点。");
                return;
            }


            // 设置窗口尺寸和间隔
            const gap = 10;
            const winWidth = Math.floor(screen.width / urls.length); // 等分屏幕宽度
            const winHeight = 1600;
            const totalWidth = winWidth * urls.length + gap * (urls.length -1 ); // 修正 totalWidth 计算
            // 屏幕正中位置
            const leftStart = Math.floor((screen.width - totalWidth) / 2);
            const topPos = Math.floor((screen.height - winHeight) / 2);


            previewWindows = []; // Reset the array before opening new windows
            urls.forEach((url, index) => {
                const leftPos = leftStart + index * (winWidth + gap);
                const features = `width=${winWidth},height=${winHeight},left=${leftPos},top=${topPos},resizable=yes,scrollbars=yes`;
                const newWin = window.open(url, '_blank', features);
                if (newWin) {
                    previewWindows.push(newWin);
                    newWin.onload = () => {
                        newWin.document.addEventListener('click', function(event) {
                            if (event.target.tagName === 'A') {
                                event.preventDefault(); // 阻止默认链接行为在弹窗中打开
                                const href = event.target.href;
                                closePreviewWindows(); // 关闭所有弹窗
                                window.open(href, '_blank'); // 在新标签页中打开链接
                            }
                        });
                    };
                } else {
                    console.warn("弹窗被拦截,无法打开:", url);
                }
            });

            // 延迟 2 秒启动定时器,监控焦点
            focusMonitorDelayTimer = setTimeout(() => {
                startFocusMonitor();
            }, 2000);
        }

        // 关闭所有预览窗口
        function closePreviewWindows() {
            stopFocusMonitor();
            if (focusMonitorDelayTimer) {
                clearTimeout(focusMonitorDelayTimer);
                focusMonitorDelayTimer = null;
            }
            previewWindows.forEach(win => {
                if (win && !win.closed) {
                    win.close();
                }
            });
            previewWindows = [];
        }

        //  ---  焦点监控逻辑  ---
        function startFocusMonitor() {
            if (!monitorInterval) {
                monitorInterval = setInterval(monitorFocus, 300);
            }
        }

        function stopFocusMonitor() {
            if (monitorInterval) {
                clearInterval(monitorInterval);
                monitorInterval = null;
            }
        }

        function monitorFocus() {
            for (let i = 0; i < previewWindows.length; i++) {
                if (previewWindows[i] && previewWindows[i].closed) {
                    closePreviewWindows();
                    return;
                }
            }
            if (!document.hasFocus()) {
                let previewWindowFocused = false;
                for (let win of previewWindows) {
                    if (win && !win.closed && win.document.hasFocus()) {
                        previewWindowFocused = true;
                        break;
                    }
                }
                if (!previewWindowFocused) {
                    closePreviewWindows();
                }
            }
        }
    }
})();