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