// ==UserScript==
// @name BiliTab
// @name:zh-CN B 站视频后台标签页打开
// @namespace https://github.com/dcjanus/userscripts
// @description 支持打开 B 站视频到后台新标签页,而不是打断当前浏览的页面
// @author DCjanus
// @match https://t.bilibili.com/*
// @match https://www.bilibili.com/*
// @match https://space.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @version 20230630
// @license MIT
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// ==/UserScript==
'use strict';
const MENU_VALUE_PREFIX = 'bool_menu_value_for_';
const MENU_ID_LIST_KEY = 'bool_menu_id_list';
const PROCESSED_ATTR = 'x-bili-tab-processed';
const ACTIVE_CLASS = 'x-bili-tab-active';
const SCRIPT_NAME = GM_info.script.name;
class Page {
constructor(object) {
this.name = object.name;
this.key = object.key;
this.selector = object.selector;
this.page_match = object.page_match;
this.default_enable = object.default_enable;
}
refresh_menu() {
const menu_value_key = MENU_VALUE_PREFIX + this.key;
const enabled = this.enabled();
const menu_icon = enabled ? '✅' : '❌';
const menu_name = `${menu_icon} ${this.name} 点击切换`;
registerMenuCommand(menu_name, () => {
GM_setValue(menu_value_key, !enabled);
refresh_menus();
});
}
attach() {
const url = new URL(window.location.href);
if (!this.page_match(url)) {
return;
}
this.on_page(); // 首次进入页面时执行一次
setInterval(this.on_page.bind(this), 500);
}
on_page() {
const elements = document.querySelectorAll(this.selector);
for (const element of elements) {
set_background_click(element, this.key, this.default_enable);
}
if (elements.length > 0) {
console.log(`[${SCRIPT_NAME}] ${elements.length} 个链接已处理`);
}
}
enabled() {
return GM_getValue(MENU_VALUE_PREFIX + this.key, this.default_enable);
}
}
const PAGES = [
new Page({
name: 'B 站首页',
key: 'bili_home',
selector: `div.bili-video-card a[href*="//www.bilibili.com/video/"]:not([${PROCESSED_ATTR}="true"])`,
page_match: (url) =>
url.host === 'www.bilibili.com' && url.pathname === '/',
default_enable: true,
}),
new Page({
name: 'B 站动态',
key: 'bili_activity',
selector: `a.bili-dyn-card-video[href*="//www.bilibili.com/video/"]:not([${PROCESSED_ATTR}="true"])`,
page_match: (url) => url.host === 't.bilibili.com',
default_enable: true,
}),
new Page({
name: 'B 站空间页',
key: 'bili_space',
selector: `a.cover[href*="//www.bilibili.com/video/"]:not([${PROCESSED_ATTR}="true"])`,
page_match: (url) => url.host === 'space.bilibili.com',
default_enable: true,
}),
];
function refresh_menus() {
cleanAllMenu();
// TODO: 现有菜单点击开关的体验不是很好,切换成点击菜单时弹出对话框选择开关
for (const page of PAGES) {
page.refresh_menu();
}
registerMenuCommand('🗑️重置所有设置', () => {
cleanAllMenu();
for (const key of GM_listValues()) {
GM_deleteValue(key);
}
refresh_menus();
});
}
function set_background_click(old_element, page_name, default_enable) {
const new_element = old_element.cloneNode(false);
for (const child of old_element.childNodes) {
// 避免影响子元素的事件绑定
new_element.appendChild(child);
}
old_element.parentNode.replaceChild(new_element, old_element);
new_element.setAttribute('target', '_blank');
new_element.addEventListener('click', (event) => {
event.preventDefault();
// 增加点击后的交互效果,因为不够精通 CSS,所以靠简单的 定时器 + class 来实现
new_element.classList.add(ACTIVE_CLASS);
setTimeout(() => new_element.classList.remove(ACTIVE_CLASS), 50);
const tmp_ele = document.createElement('a');
tmp_ele.href = new_element.href;
tmp_ele.target = '_blank';
// 如果用户按下了 Ctrl 键或者 Command 键,默认行为是在后台标签页打开
let background_open = event.ctrlKey || event.metaKey;
// 为了保证切换开关后对当前页面立即生效,这里直接读取开关值
const enable = GM_getValue(
MENU_VALUE_PREFIX + page_name,
default_enable,
);
if (enable) {
// 如果当前开关打开,则反转默认行为
background_open = !background_open;
}
const mouse_event = new MouseEvent('click', {
ctrlKey: background_open, // for Windows and Linux
metaKey: background_open, // for Mac OS
});
tmp_ele.dispatchEvent(new MouseEvent('click', mouse_event));
});
new_element.setAttribute(PROCESSED_ATTR, 'true');
}
function registerMenuCommand(name, callback, accessKey) {
const menu_id = GM_registerMenuCommand(name, callback, accessKey);
const current = GM_getValue(MENU_ID_LIST_KEY, []);
current.push(menu_id);
GM_setValue(MENU_ID_LIST_KEY, current);
}
function cleanAllMenu() {
const current = GM_getValue(MENU_ID_LIST_KEY, []);
for (const menu_id of current) {
GM_unregisterMenuCommand(menu_id);
}
GM_deleteValue(MENU_ID_LIST_KEY);
}
function injectStyle() {
const style = document.createElement('style');
style.innerHTML = `
.${ACTIVE_CLASS} {
filter: brightness(95%);
}
`;
document.head.appendChild(style);
}
function main() {
injectStyle();
refresh_menus();
const url = new URL(window.location.href);
for (const page of PAGES) {
if (page.page_match(url)) {
page.attach();
}
}
}
try {
main();
} catch (e) {
console.error(`[${SCRIPT_NAME}] ${e}`);
}