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