您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在Bangumi游戏条目上添加实用的按钮
// ==UserScript== // @name Bangumi jump to multiple sites // @namespace http://tampermonkey.net/ // @version 0.9.5.6 // @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: 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 { height: 32px; line-height: 32px; padding-top: 0; padding-bottom: 0; display: inline-flex; 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; } .settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: white; border: 1px solid #ccc; padding: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); z-index: 1000; max-height: 80vh; overflow-y: auto; } .settings-panel h2, .settings-panel h3 { margin-top: 0; } .settings-panel label { display: block; margin-bottom: 5px; } .settings-panel input[type="text"], .settings-panel input[type="url"], .settings-panel select { width: calc(100% - 10px); padding: 8px; margin-bottom: 10px; border: 1px solid #ddd; box-sizing: border-box; } .settings-panel button { padding: 8px 15px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; margin-right: 5px; } .settings-panel button:hover { background-color: #e0e0e0; } .settings-panel-buttons { margin-bottom: 15px; border: 1px solid #eee; padding: 10px; border-radius: 5px; } .settings-panel-button-item { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px dashed #eee; } .settings-panel-button-item:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } .settings-panel-button-actions { margin-top: 5px; } .settings-tutorial { margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px; } .settings-tutorial h3 { margin-bottom: 10px; } .settings-tutorial p { line-height: 1.6; } .hidden { display: none !important; } `); const createButton = (buttonConfig) => { if (buttonConfig.type === 'jump') { return createJumpButton(buttonConfig); } else if (buttonConfig.type === 'combined') { return createCombinedButton(buttonConfig); } else if (buttonConfig.type === 'multisearch') { return createMultiSearchSelect(buttonConfig); } return null; }; const createJumpButton = (buttonConfig) => { const button = document.createElement('button'); button.textContent = buttonConfig.name; button.className = 'jump-button'; button.addEventListener('click', () => { if (buttonConfig.clipboardText) { GM_setClipboard(gameTitle); } window.open(buttonConfig.url.replace('{{gameTitle}}', encodeURIComponent(gameTitle))); }); return button; }; const createCombinedButton = (buttonConfig) => { const container = document.createElement('div'); container.className = 'combined-button'; const nameButton = document.createElement('button'); nameButton.className = 'button-name'; container.appendChild(nameButton); const selectArrow = document.createElement('select'); selectArrow.className = 'select-arrow'; container.appendChild(selectArrow); buttonConfig.options.forEach(site => { const option = document.createElement('option'); option.value = site.value; option.text = site.text; selectArrow.appendChild(option); }); const storedSite = GM_getValue(buttonConfig.storageKey, buttonConfig.defaultOption); selectArrow.value = storedSite; updateButtonName(nameButton, selectArrow.options[selectArrow.selectedIndex].text); selectArrow.addEventListener('change', function() { GM_setValue(buttonConfig.storageKey, this.value); updateButtonName(nameButton, this.options[this.selectedIndex].text); }); nameButton.addEventListener('click', () => { const selectedOption = selectArrow.value; const selectedSiteOption = buttonConfig.options.find(opt => opt.value === selectedOption); if (selectedSiteOption) { if (buttonConfig.clipboardText) { GM_setClipboard(gameTitle); } window.open(selectedSiteOption.url.replace('{{gameTitle}}', encodeURIComponent(gameTitle))); } }); selectArrow.addEventListener('click', function(event) { this.focus(); event.stopPropagation(); }); container.addEventListener('click', function(event) { if (!container.contains(event.target)) { selectArrow.blur(); } }); function updateButtonName(button, siteName) { button.textContent = siteName; } return container; }; const createMultiSearchSelect = (buttonConfig) => { const container = document.createElement('div'); container.className = 'multisearch-select-container'; const buttonArea = document.createElement('div'); buttonArea.className = 'combined-button'; container.appendChild(buttonArea); const nameButton = document.createElement('button'); nameButton.className = 'button-name'; nameButton.textContent = buttonConfig.name; buttonArea.appendChild(nameButton); const selectArrow = document.createElement('button'); 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 = buttonConfig.options; let storedMultiSearchSites = GM_getValue(buttonConfig.storageKey, 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(buttonConfig.storageKey, 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(); }); nameButton.addEventListener('click', () => { openPreviewWindowsWithDelay(buttonConfig, gameTitle, container, 500); // 延迟 500ms 打开 }); document.addEventListener('click', function(event) { if (!container.contains(event.target)) { dropdown.classList.remove('show'); } }); return container; }; // 修改后的延迟打开函数 function openPreviewWindowsWithDelay(buttonConfig, gameTitle, multiSearchSelectContainer, delay) { setTimeout(() => { openPreviewWindows(buttonConfig, gameTitle, multiSearchSelectContainer); }, delay); } function openPreviewWindows(buttonConfig, gameTitle, multiSearchSelectContainer) { 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 = buttonConfig.options.filter(site => selectedSitesValues.includes(site.value)); const urls = sites.map(site => site.url.replace('{{gameTitle}}', encodeURIComponent(gameTitle))); 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 ); const leftStart = Math.floor((screen.width - totalWidth) / 2); const topPos = Math.floor((screen.height - winHeight) / 2); previewWindows = []; 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); } }); focusMonitorDelayTimer = setTimeout(() => { startFocusMonitor(); }, 2000); } let previewWindows = []; let monitorInterval = null; let focusMonitorDelayTimer = null; 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(); } } } function loadButtonSettings() { return GM_getValue('customButtons', [ { type: 'jump', name: 'VNDB', url: 'https://vndb.org/v?q={{gameTitle}}' }, { type: 'jump', name: 'Hitomi', url: 'https://hitomi.la/search.html?type%3Agamecg%20{{gameTitle}}%20orderby%3Apopular%20orderbykey%3Ayear', processTitle: 'hitomi' }, { type: 'combined', name: '魔皇/zi0', storageKey: 'selectedSite', defaultOption: 'mhdy', clipboardText: true, options: [ { value: 'mhdy', text: '魔皇地狱', url: 'https://pan1.mhdy.shop/' }, { value: 'zi0', text: 'zi0.cc', url: 'https://zi0.cc/' } ] }, { type: 'jump', name: '2dfan', clipboardText: true, url: 'https://2dfan.com/subjects/search?keyword={{gameTitle}}' }, { type: 'multisearch', name: '多搜索', storageKey: 'multiSearchSites', options: [ { value: 'ai2', text: 'ai2.moe', url: 'https://www.ai2.moe/search/?q={{gameTitle}}&updated_after=any&sortby=relevancy&search_in=titles', checked: true }, { value: 'moyu', text: 'moyu.moe', url: 'https://www.moyu.moe/search?q={{gameTitle}}', checked: true }, { value: '2dfan_preview', text: '2dfan', url: 'https://2dfan.com/subjects/search?keyword={{gameTitle}}', checked: true } ] } ]); } function saveButtonSettings(settings) { GM_setValue('customButtons', settings); } function renderButtons() { const buttonSettings = loadButtonSettings(); nameSingle.querySelectorAll('.jump-button, .combined-button, .multisearch-select-container').forEach(btn => btn.remove()); nameSingle.appendChild(document.createElement('br')); buttonSettings.forEach(buttonConfig => { const button = createButton(buttonConfig); if (button) { nameSingle.appendChild(button); } }); const settingsButton = document.createElement('button'); settingsButton.textContent = '设置'; settingsButton.className = 'jump-button'; settingsButton.addEventListener('click', openSettingsPanel); nameSingle.appendChild(settingsButton); } renderButtons(); let settingsPanel; function openSettingsPanel() { if (!settingsPanel) { settingsPanel = createSettingsPanel(); document.body.appendChild(settingsPanel); } settingsPanel.classList.remove('hidden'); } function closeSettingsPanel() { if (settingsPanel) { settingsPanel.classList.add('hidden'); } } function createSettingsPanel() { const panel = document.createElement('div'); panel.className = 'settings-panel hidden'; const title = document.createElement('h2'); title.textContent = '网址按钮设置'; panel.appendChild(title); const tutorialSection = createTutorialSection(); panel.appendChild(tutorialSection); const buttonsSection = createButtonsSettingsSection(); panel.appendChild(buttonsSection); const closeButton = document.createElement('button'); closeButton.textContent = '关闭'; closeButton.addEventListener('click', closeSettingsPanel); panel.appendChild(closeButton); return panel; } function createTutorialSection() { const section = document.createElement('div'); section.className = 'settings-tutorial'; const title = document.createElement('h3'); title.textContent = '修改教程'; section.appendChild(title); const tutorialText = document.createElement('p'); tutorialText.innerHTML = ` 1. **添加按钮**: 点击 “添加按钮”,填写按钮名称,类型,URL等信息,点击 “保存按钮” 完成添加。<br> 2. **修改按钮**: 在按钮列表中,点击 “编辑” 按钮,修改按钮信息后,点击 “保存按钮” 完成修改。<br> 3. **删除按钮**: 在按钮列表中,点击 “删除” 按钮即可删除按钮。<br> 4. **按钮类型说明**: <br> - **Jump Button**: 点击直接跳转到设置的URL,URL中可以使用 {{gameTitle}} 占位符代表游戏标题。<br> - **Combined Button**: 合并按钮,左侧为按钮名称,右侧下拉选择不同站点。需要设置多个 options,每个 option 包含 value, text, url。<br> - **MultiSearch Select**: 多搜索按钮,点击按钮显示下拉菜单,用户可以选择多个站点进行多搜索。需要设置多个 options,每个 option 包含 value, text, url, checked (默认选中)。<br> 5. **保存设置**: 修改完成后,点击 “保存设置” 保存所有更改。点击 “关闭” 关闭设置面板。<br> *URL 填写说明*: URL 中可以使用 {{gameTitle}} 作为占位符,脚本运行时会自动替换为当前页面的游戏标题。 `; section.appendChild(tutorialText); return section; } function createButtonsSettingsSection() { const section = document.createElement('div'); section.className = 'settings-panel-buttons'; const title = document.createElement('h3'); title.textContent = '按钮列表'; section.appendChild(title); const buttonsList = document.createElement('div'); section.appendChild(buttonsList); let currentButtonSettings = loadButtonSettings(); function refreshButtonsList() { buttonsList.innerHTML = ''; currentButtonSettings.forEach((buttonConfig, index) => { const buttonItem = createButtonItem(buttonConfig, index); buttonsList.appendChild(buttonItem); }); } function createButtonItem(buttonConfig, index) { const item = document.createElement('div'); item.className = 'settings-panel-button-item'; const nameLabel = document.createElement('label'); nameLabel.textContent = `名称: ${buttonConfig.name}, 类型: ${buttonConfig.type}`; item.appendChild(nameLabel); const actions = document.createElement('div'); actions.className = 'settings-panel-button-actions'; const editButton = document.createElement('button'); editButton.textContent = '编辑'; editButton.addEventListener('click', () => openEditButtonForm(buttonConfig, index)); actions.appendChild(editButton); const deleteButton = document.createElement('button'); deleteButton.textContent = '删除'; deleteButton.addEventListener('click', () => { currentButtonSettings.splice(index, 1); saveButtonSettings(currentButtonSettings); refreshButtonsList(); renderButtons(); }); actions.appendChild(deleteButton); item.appendChild(actions); return item; } refreshButtonsList(); const addButtonButton = document.createElement('button'); addButtonButton.textContent = '添加按钮'; addButtonButton.addEventListener('click', openAddButtonForm); section.appendChild(addButtonButton); let formContainer = document.createElement('div'); section.appendChild(formContainer); let currentForm = null; function openAddButtonForm() { if (currentForm) formContainer.removeChild(currentForm); currentForm = createButtonForm(null, (newButtonConfig) => { currentButtonSettings.push(newButtonConfig); saveButtonSettings(currentButtonSettings); refreshButtonsList(); renderButtons(); formContainer.removeChild(currentForm); currentForm = null; }, () => { formContainer.removeChild(currentForm); currentForm = null; }); formContainer.appendChild(currentForm); } function openEditButtonForm(buttonConfig, index) { if (currentForm) formContainer.removeChild(currentForm); currentForm = createButtonForm(buttonConfig, (updatedButtonConfig) => { currentButtonSettings[index] = updatedButtonConfig; saveButtonSettings(currentButtonSettings); refreshButtonsList(); renderButtons(); formContainer.removeChild(currentForm); currentForm = null; }, () => { formContainer.removeChild(currentForm); currentForm = null; }); formContainer.appendChild(currentForm); } function createButtonForm(initialConfig, saveCallback, cancelCallback) { const isEdit = initialConfig !== null; const form = document.createElement('div'); const typeLabel = document.createElement('label'); typeLabel.textContent = '按钮类型:'; const typeSelect = document.createElement('select'); const types = ['jump', 'combined', 'multisearch']; types.forEach(type => { const option = document.createElement('option'); option.value = type; option.textContent = type; typeSelect.appendChild(option); }); typeSelect.value = initialConfig ? initialConfig.type : 'jump'; form.appendChild(typeLabel); form.appendChild(typeSelect); const nameLabel = document.createElement('label'); nameLabel.textContent = '按钮名称:'; const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.value = initialConfig ? initialConfig.name : ''; form.appendChild(nameLabel); form.appendChild(nameInput); const urlLabel = document.createElement('label'); urlLabel.textContent = 'URL (Jump/Combined/MultiSearch):'; const urlInput = document.createElement('input'); urlInput.type = 'url'; urlInput.value = initialConfig && initialConfig.url ? initialConfig.url : ''; form.appendChild(urlLabel); form.appendChild(urlInput); const storageKeyLabel = document.createElement('label'); storageKeyLabel.textContent = 'Storage Key (Combined/MultiSearch):'; const storageKeyInput = document.createElement('input'); storageKeyInput.type = 'text'; storageKeyInput.value = initialConfig && initialConfig.storageKey ? initialConfig.storageKey : ''; form.appendChild(storageKeyLabel); form.appendChild(storageKeyInput); const defaultOptionLabel = document.createElement('label'); defaultOptionLabel.textContent = 'Default Option Value (Combined):'; const defaultOptionInput = document.createElement('input'); defaultOptionInput.type = 'text'; defaultOptionInput.value = initialConfig && initialConfig.defaultOption ? initialConfig.defaultOption : ''; form.appendChild(defaultOptionLabel); form.appendChild(defaultOptionInput); const clipboardTextLabel = document.createElement('label'); clipboardTextLabel.textContent = '复制标题到剪贴板 (Jump/Combined/2dfan):'; const clipboardTextSelect = document.createElement('select'); const clipboardOptions = [{value: true, text: '是'}, {value: false, text: '否'}]; clipboardOptions.forEach(opt => { const option = document.createElement('option'); option.value = opt.value; option.textContent = opt.text; clipboardTextSelect.appendChild(option); }); clipboardTextSelect.value = initialConfig && initialConfig.clipboardText !== undefined ? String(initialConfig.clipboardText) : 'false'; form.appendChild(clipboardTextLabel); form.appendChild(clipboardTextSelect); const optionsLabel = document.createElement('label'); optionsLabel.textContent = 'Options (Combined/MultiSearch, JSON Array):'; const optionsTextarea = document.createElement('input'); optionsTextarea.type = 'text'; optionsTextarea.value = initialConfig && initialConfig.options ? JSON.stringify(initialConfig.options) : '[]'; form.appendChild(optionsLabel); form.appendChild(optionsTextarea); const saveButton = document.createElement('button'); saveButton.textContent = '保存按钮'; saveButton.addEventListener('click', () => { let optionsArray = []; try { optionsArray = JSON.parse(optionsTextarea.value || '[]'); } catch (e) { alert('Options JSON 格式错误'); return; } const newConfig = { type: typeSelect.value, name: nameInput.value, url: urlInput.value || undefined, storageKey: storageKeyInput.value || undefined, defaultOption: defaultOptionInput.value || undefined, clipboardText: clipboardTextSelect.value === 'true', options: optionsArray.length > 0 ? optionsArray : undefined }; saveCallback(newConfig); }); form.appendChild(saveButton); const cancelButton = document.createElement('button'); cancelButton.textContent = '取消'; cancelButton.addEventListener('click', cancelCallback); form.appendChild(cancelButton); return form; } return section; } document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { closeSettingsPanel(); } }); } })();