// ==UserScript==
// @name B站个人面板美化
// @namespace http://tampermonkey.net/
// @version 10.0
// @description 磨砂面板 + 昵称拖拽布局 + HARD-CORE标签 + 深浅双主题(独立版)
// @author Anonymity
// @match *://*.bilibili.com/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ====================== 配置 ======================
const STORAGE = {
SPACE_STYLE: 'bili_space_style_enabled',
TARGET_UID: 'bili_space_target_uid',
CUSTOM_CONFIG: 'bili_custom_panel_config',
NICKNAME_LAYOUT: 'bili_nickname_layout',
LIST_BEAUTY_CONFIG: 'bili_list_beauty_config'
};
// ====================== 列表管理器美化共享工具 ======================
const LIST_BG_DB_NAME = 'BiliListManagerDB';
const LIST_BG_DB_VERSION = 1;
const LIST_BG_STORE = 'bg_images';
function isListManagerLoaded() {
const _g = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
return _g.__biliListManagerLoaded === true || !!document.getElementById('bili-force-trigger');
}
function getCollectionIdFromPage() {
try {
// 和1.js逻辑保持一致:从DOM读取合集BV列表
const container = document.querySelector('.video-pod__list');
if (container) {
const items = container.querySelectorAll('[data-key]');
const bvs = new Set();
items.forEach(el => { const bv = el.getAttribute('data-key'); if (bv && bv.startsWith('BV')) bvs.add(bv); });
if (bvs.size > 0) {
return 'list_' + Array.from(bvs).sort().join('|');
}
}
// 单个视频:从URL获取BV号
const bvMatch = location.href.match(/\/video\/(BV[0-9a-zA-Z]+)/);
if (bvMatch) return 'single_' + bvMatch[1];
return null;
} catch(e) { return null; }
}
function getListBeautyConfig() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE.LIST_BEAUTY_CONFIG) || '{}');
return {
frostedGlass: saved.frostedGlass !== false,
glassBlur: saved.glassBlur || 12,
drawerOpacity: saved.drawerOpacity !== undefined ? saved.drawerOpacity : 0.85,
defaultBgOpacity: saved.defaultBgOpacity !== undefined ? saved.defaultBgOpacity : 0.15,
bgSize: saved.bgSize || 'cover',
bgPosition: saved.bgPosition || 'center'
};
} catch(e) {
return { frostedGlass: true, glassBlur: 12, drawerOpacity: 0.85, defaultBgOpacity: 0.15, bgSize: 'cover', bgPosition: 'center' };
}
}
function saveListBeautyConfig(cfg) {
localStorage.setItem(STORAGE.LIST_BEAUTY_CONFIG, JSON.stringify(cfg));
}
function openListBgDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(LIST_BG_DB_NAME, LIST_BG_DB_VERSION);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains(LIST_BG_STORE)) {
db.createObjectStore(LIST_BG_STORE, { keyPath: 'id' });
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function saveListBgImage(id, imageData, opacity) {
try {
const db = await openListBgDB();
return new Promise((resolve) => {
const tx = db.transaction(LIST_BG_STORE, 'readwrite');
const store = tx.objectStore(LIST_BG_STORE);
store.put({ id, imageData, opacity, uploadTime: Date.now() });
tx.oncomplete = () => {
if (window.__biliListRefreshBg) window.__biliListRefreshBg();
resolve(true);
};
tx.onerror = () => resolve(false);
});
} catch(e) { return false; }
}
async function deleteListBgImage(id) {
try {
const db = await openListBgDB();
return new Promise((resolve) => {
const tx = db.transaction(LIST_BG_STORE, 'readwrite');
const store = tx.objectStore(LIST_BG_STORE);
store.delete(id);
tx.oncomplete = () => {
if (window.__biliListRefreshBg) window.__biliListRefreshBg();
resolve(true);
};
tx.onerror = () => resolve(false);
});
} catch(e) { return false; }
}
// ====================== 个人面板背景图 IndexedDB ======================
const PANEL_BG_DB_NAME = 'BiliPanelBgDB';
const PANEL_BG_DB_VERSION = 1;
const PANEL_BG_STORE = 'bg_image';
const PANEL_BG_KEY = 'panel_bg';
function openPanelBgDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(PANEL_BG_DB_NAME, PANEL_BG_DB_VERSION);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains(PANEL_BG_STORE)) {
db.createObjectStore(PANEL_BG_STORE, { keyPath: 'id' });
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function savePanelBgImage(imageData, opts = {}) {
try {
const db = await openPanelBgDB();
return new Promise((resolve) => {
const tx = db.transaction(PANEL_BG_STORE, 'readwrite');
const store = tx.objectStore(PANEL_BG_STORE);
store.put({ id: PANEL_BG_KEY, imageData, uploadTime: Date.now(), ...opts });
tx.oncomplete = () => resolve(true);
tx.onerror = () => resolve(false);
});
} catch(e) { return false; }
}
async function getPanelBgImage() {
try {
const db = await openPanelBgDB();
return new Promise((resolve) => {
const tx = db.transaction(PANEL_BG_STORE, 'readonly');
const req = tx.objectStore(PANEL_BG_STORE).get(PANEL_BG_KEY);
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => resolve(null);
});
} catch(e) { return null; }
}
async function deletePanelBgImage() {
try {
const db = await openPanelBgDB();
return new Promise((resolve) => {
const tx = db.transaction(PANEL_BG_STORE, 'readwrite');
const store = tx.objectStore(PANEL_BG_STORE);
store.delete(PANEL_BG_KEY);
tx.oncomplete = () => resolve(true);
tx.onerror = () => resolve(false);
});
} catch(e) { return false; }
}
const DEFAULT_PANEL_CONFIG = {
bgWatermarkNum: '6',
bgRotate: -6,
bgOpacityDark: 0.15,
bgOpacityLight: 0.01,
bgBreathAnim: true,
bgFrosted: false,
glassBlurDark: 12,
glassBlurLight: 12,
wmSizeDark: 600,
wmSizeLight: 500,
wmColorDark: '#B4B4B4',
wmColorLight: '#505050',
showHardCoreTag: true,
hardCoreText: '硬核会员',
hardCoreDarkBorder: '#FF69B4',
hardCoreLightBorder: '#66ccff',
primaryDark: '#FF69B4',
primaryLight: '#66ccff',
panelBgOpacity: 0.3,
panelBgImageSet: false
};
const DEFAULT_NICKNAME_LAYOUT = {
effect: 'none',
effectColor: '#FF69B4',
effectSpeed: 5,
columns: [{
id: 'col1',
width: '100%',
blocks: [
{ id: 'b1', text: 'Sunday', x: -5, y: -10, color: '#FF69B4', fontSize: 22, rotation: -2 },
{ id: 'b2', text: ' 大好き', x: 5, y: 15, color: '#FF69B4', fontSize: 20, rotation: 2 },
{ id: 'b3', text: 'が', x: 40, y: 30, color: '#FF69B4', fontSize: 28, rotation: 5 }
]
}]
};
let spaceEnabled = GM_getValue(STORAGE.SPACE_STYLE, true);
let targetUid = GM_getValue(STORAGE.TARGET_UID, '327239767');
let config = GM_getValue(STORAGE.CUSTOM_CONFIG, { ...DEFAULT_PANEL_CONFIG });
config = { ...DEFAULT_PANEL_CONFIG, ...config };
// 内存中保存面板背景图 dataURL(从IndexedDB加载,不放进GM存储)
let panelBgDataUrl = null;
let panelBgSize = 'cover';
let panelBgPos = 'center';
let panelBgPosX = 50, panelBgPosY = 50;
// 迁移:如果旧版本把图片存在config里,迁移到DB并清除GM中的大对象
if (config.panelBgImage) {
const legacyImg = config.panelBgImage;
delete config.panelBgImage;
config.panelBgImageSet = true;
panelBgDataUrl = legacyImg;
savePanelBgImage(legacyImg);
GM_setValue(STORAGE.CUSTOM_CONFIG, config);
}
// 启动时从DB加载面板背景图(含位置/尺寸/透明度)
getPanelBgImage().then(entry => {
if (entry && entry.imageData) {
panelBgDataUrl = entry.imageData;
if (entry.bgSize) panelBgSize = entry.bgSize;
if (entry.bgPosition) {
panelBgPos = entry.bgPosition;
const m = entry.bgPosition.match(/(-?\d+(?:\.\d+)?)%\s+(-?\d+(?:\.\d+)?)%/);
if (m) { panelBgPosX = parseFloat(m[1]); panelBgPosY = parseFloat(m[2]); }
}
if (entry.opacity !== undefined) config.panelBgOpacity = entry.opacity;
if (config.panelBgImageSet === undefined) config.panelBgImageSet = true;
rebuild();
}
});
let nicknameLayout = GM_getValue(STORAGE.NICKNAME_LAYOUT, null);
if (!nicknameLayout) {
nicknameLayout = { ...DEFAULT_NICKNAME_LAYOUT };
GM_setValue(STORAGE.NICKNAME_LAYOUT, nicknameLayout);
}
let styleEl = null;
let editorEl = null;
let configPanelEl = null;
let editorBackup = null;
let tagInserted = false;
let observer = null;
// ====================== 工具 ======================
function throttle(fn, delay) {
let timer = null;
return (...args) => { if (!timer) { fn(...args); timer = setTimeout(() => timer = null, delay); } };
}
function uid() { return 'uid_' + Math.random().toString(36).substr(2, 9); }
function saveConfig() {
// 剥离可能残留的大字段,避免GM存储超限导致保存失败
const toSave = { ...config };
delete toSave.panelBgImage;
GM_setValue(STORAGE.CUSTOM_CONFIG, toSave);
rebuild();
}
function saveLayout() { GM_setValue(STORAGE.NICKNAME_LAYOUT, nicknameLayout); applyNickname(); }
// ====================== 主题检测 ======================
function getTheme() {
const html = document.documentElement;
const dt = html.getAttribute('data-theme');
if (dt === 'light' || dt === 'dark') return dt;
if (html.classList.contains('bili_dark') || html.classList.contains('dark')) return 'dark';
if (html.classList.contains('light')) return 'light';
const ls = localStorage.getItem('bilibili_theme');
if (ls === 'light' || ls === 'dark') return ls;
try {
const bodyBg = getComputedStyle(document.body).backgroundColor;
if (bodyBg && bodyBg !== 'rgba(0, 0, 0, 0)') {
const nums = bodyBg.replace(/[^\d,]/g, '').split(',').map(Number);
if (nums.length >= 3) {
const brightness = nums[0] * 0.299 + nums[1] * 0.587 + nums[2] * 0.114;
return brightness < 128 ? 'dark' : 'light';
}
}
const textColor = getComputedStyle(html).color;
const tnums = textColor.replace(/[^\d,]/g, '').split(',').map(Number);
if (tnums.length >= 3) {
const tb = tnums[0] * 0.299 + tnums[1] * 0.587 + tnums[2] * 0.114;
return tb < 128 ? 'dark' : 'light';
}
} catch(e) {}
return 'dark';
}
function observeHtmlTheme() {
const html = document.documentElement;
const mo = new MutationObserver(() => applyThemeClass());
mo.observe(html, { attributes: true, attributeFilter: ['data-theme', 'class'] });
}
function applyThemeClass() {
const panel = document.querySelector('.avatar-panel-popover');
if (!panel) return;
panel.classList.remove('theme-dark', 'theme-light');
panel.classList.add('theme-' + getTheme());
}
// ====================== CSS 生成(关键修改:极透明背景 + 隐藏水印) ======================
function hex2rgb(h) {
return `${parseInt(h.slice(1,3),16)},${parseInt(h.slice(3,5),16)},${parseInt(h.slice(5,7),16)}`;
}
function buildCSS() {
const uidDark = `a.nickname-item.dark[href="//space.bilibili.com/${targetUid}"]`;
const uidLight = `a.nickname-item.light[href="//space.bilibili.com/${targetUid}"]`;
const {
bgWatermarkNum, bgRotate, bgOpacityDark, bgOpacityLight, bgBreathAnim, bgFrosted,
glassBlurDark, glassBlurLight, wmSizeDark, wmSizeLight, wmColorDark, wmColorLight,
hardCoreDarkBorder, hardCoreLightBorder, primaryDark, primaryLight
} = config;
const pd = hex2rgb(primaryDark);
const pl = hex2rgb(primaryLight);
return `
/* ====== 核心变量(html data-theme 驱动) ====== */
html[data-theme="dark"] .avatar-panel-popover,
html.dark .avatar-panel-popover,
html.bili_dark .avatar-panel-popover {
--panel-bg: rgba(16,16,22,0.04); /* 极透明 */
--panel-text: #fff;
--panel-shadow: 0 2px 10px rgba(0,0,0,0.25),0 12px 40px rgba(0,0,0,0.48),inset 0 0 0 1px rgba(255,255,255,0.07);
--panel-hover-shadow: 0 2px 10px rgba(0,0,0,0.25),0 12px 40px rgba(0,0,0,0.48),0 0 26px 7px rgba(${pd},0.36),inset 0 0 0 1px rgba(${pd},0.12);
--logout-bg: #2E0017;
--logout-hover-shadow: 0 4px 16px rgba(${pd},0.4);
}
html[data-theme="light"] .avatar-panel-popover,
html.light .avatar-panel-popover {
--panel-bg: rgba(252,252,250,0.04); /* 极透明 */
--panel-text: #2d2d2d;
--panel-shadow: 0 4px 16px rgba(0,0,0,0.08),0 12px 40px rgba(0,0,0,0.15),inset 0 0 0 1px rgba(0,0,0,0.05);
--panel-hover-shadow: 0 4px 16px rgba(0,0,0,0.08),0 12px 40px rgba(0,0,0,0.15),0 0 26px 7px rgba(${pl},0.36),inset 0 0 0 1px rgba(${pd},0.1);
--logout-bg: linear-gradient(135deg,${primaryLight},#47b8f0);
--logout-hover-shadow: 0 4px 16px rgba(${pl},0.3);
}
/* ====== 装饰变量(panel class 驱动) ====== */
.theme-dark.avatar-panel-popover, .avatar-panel-popover.theme-dark {
--panel-bg: rgba(16,16,22,0.04);
--panel-text: #fff;
--panel-shadow: 0 2px 10px rgba(0,0,0,0.25),0 12px 40px rgba(0,0,0,0.48),inset 0 0 0 1px rgba(255,255,255,0.07);
--panel-hover-shadow: 0 2px 10px rgba(0,0,0,0.25),0 12px 40px rgba(0,0,0,0.48),0 0 26px 7px rgba(${pd},0.36),inset 0 0 0 1px rgba(${pd},0.12);
--logout-bg: #2E0017;
--logout-hover-shadow: 0 4px 16px rgba(${pd},0.4);
--wm-color: rgba(${hex2rgb(wmColorDark)},${bgOpacityDark});
--wm-size: ${wmSizeDark}px;
--hardcore-border: ${hardCoreDarkBorder};
--item-bg: rgba(255,255,255,0.04);
--item-shadow: 0 4px 12px rgba(0,0,0,0.3),0 1px 3px rgba(0,0,0,0.15);
--glass-blur: ${glassBlurDark}px;
--link-icon-fill: #ccc;
--split-line-bg: linear-gradient(90deg,transparent,rgba(255,255,255,0.12),transparent);
--level-bar-bg: #1E1E1E;
--level-inner-bg: #555;
--primary: ${primaryDark};
--primary-rgb: ${pd};
--avatar-shadow: drop-shadow(0 0 12px rgba(0,221,255,0.6));
--avatar-hover-shadow: drop-shadow(0 0 20px ${primaryDark});
--single-link-hover-bg: rgba(${pd},0.1);
--coin-text: inherit;
--coin-text-secondary: inherit;
--count-hover-color: ${primaryDark};
}
.theme-light.avatar-panel-popover, .avatar-panel-popover.theme-light {
--panel-bg: rgba(252,252,250,0.04);
--panel-text: #2d2d2d;
--panel-shadow: 0 4px 16px rgba(0,0,0,0.08),0 12px 40px rgba(0,0,0,0.15),inset 0 0 0 1px rgba(0,0,0,0.05);
--panel-hover-shadow: 0 4px 16px rgba(0,0,0,0.08),0 12px 40px rgba(0,0,0,0.15),0 0 26px 7px rgba(${pl},0.36),inset 0 0 0 1px rgba(${pd},0.1);
--logout-bg: linear-gradient(135deg,${primaryLight},#47b8f0);
--logout-hover-shadow: 0 4px 16px rgba(${pl},0.3);
--wm-color: rgba(${hex2rgb(wmColorLight)},${bgOpacityLight});
--wm-size: ${wmSizeLight}px;
--hardcore-border: ${hardCoreLightBorder};
--item-bg: rgba(0,0,0,0.03);
--item-shadow: 0 2px 8px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.04);
--glass-blur: ${glassBlurLight}px;
--link-icon-fill: #555;
--split-line-bg: linear-gradient(90deg,transparent,rgba(0,0,0,0.06),transparent);
--level-bar-bg: #E0E0E0;
--level-inner-bg: #aaa;
--primary: ${primaryLight};
--primary-rgb: ${pl};
--avatar-shadow: drop-shadow(0 0 12px rgba(0,221,255,0.4));
--avatar-hover-shadow: drop-shadow(0 0 20px ${primaryLight});
--single-link-hover-bg: rgba(${pd},0.08);
--coin-text: #2d2d2d;
--coin-text-secondary: #666;
--count-hover-color: ${primaryLight};
}
/* ====== panel 本体 ====== */
.avatar-panel-popover {
position:relative; z-index:1;
background: var(--panel-bg) !important;
color: var(--panel-text) !important;
box-shadow: var(--panel-shadow) !important;
animation: pfPopoverFadeIn 0.25s cubic-bezier(0.2,0,0.2,1);
}
.avatar-panel-popover::before {
content:"${String(bgWatermarkNum).replace(/\\/g,'\\\\').replace(/"/g,'\\"').replace(/[\r\n]+/g,'')}"; position:absolute; inset:0;
font-size:var(--wm-size); font-weight:bold; display:flex; align-items:center; justify-content:center;
color:var(--wm-color); transform:rotate(${bgRotate}deg);
user-select:none; pointer-events:none; z-index:-1; border-radius:16px;
clip-path:inset(0 round 16px);
${bgBreathAnim ? 'animation:pfBgBreath 12s ease-in-out infinite;' : ''}
}
.avatar-panel-popover:hover {
box-shadow: var(--panel-hover-shadow) !important;
}
.avatar-panel-popover * { -webkit-font-smoothing:antialiased !important; }
${bgFrosted ? `
/* ====== 玻璃特效 ====== */
.v-popover-content.avatar-popover,
.v-popover.is-bottom { background: transparent !important; }
.avatar-panel-popover {
backdrop-filter: blur(var(--glass-blur)) !important;
-webkit-backdrop-filter: blur(var(--glass-blur)) !important;
}
` : ''}
/* ====== 子元素(全用 var) ====== */
.vip-entry-containter { display:none !important; }
.icon { display:none; }
.senior { display:none !important; }
/* 昵称 */
${uidDark},${uidLight} {
display:inline-flex !important; justify-content:space-between !important;
width:85px !important; height:38px !important; font-size:0 !important;
color:transparent !important; align-items:center !important; line-height:1 !important;
margin:0 !important; padding:0 !important;
transform:translateX(65px) translateY(-20px) !important;
text-decoration:none !important; overflow:visible !important;
position:relative !important;
}
${uidDark}::before,${uidLight}::before,
${uidDark}::after,${uidLight}::after { content:none !important; }
/* HARD-CORE */
.hard-core-tag {
width:fit-content !important; margin:16px auto 20px !important;
padding:5px 16px !important; border-radius:8px !important;
font-size:13px !important; font-weight:500;
letter-spacing:1px; white-space:nowrap; pointer-events:none;
border:1px solid var(--hardcore-border) !important;
color: var(--panel-text) !important;
}
/* 数据卡片 */
.coins-item, .counts-item {
background: var(--item-bg) !important;
box-shadow: var(--item-shadow) !important;
border-radius:12px;
}
.coin-item__num, .count-num { color: var(--coin-text); }
.coin-item__text, .count-text { color: var(--coin-text-secondary); }
.counts-item .single-count-item { transition:all 1.5s ease; cursor:pointer; }
.counts-item .single-count-item:hover { transform:scale(1.1); color: var(--count-hover-color); }
.counts-item .single-count-item:hover .count-num { color: var(--count-hover-color); }
/* 链接图标 */
.link-icon path { fill: var(--link-icon-fill) !important; }
/* 分割线 */
.split-line { background: var(--split-line-bg) !important; }
/* 退出 */
.logout-item {
margin-top:16px; padding:16px; border-radius:12px;
transition:all 0.3s ease;
background: var(--logout-bg) !important;
}
.logout-item:hover {
filter:brightness(1.15); transform:translateY(-2px);
box-shadow: var(--logout-hover-shadow);
}
.logout-item span { font-size:16px; font-weight:500; letter-spacing:1px; }
.theme-dark .logout-item span { color: ${primaryDark} !important; }
html[data-theme="dark"] .logout-item .link-icon path,
html.dark .logout-item .link-icon path,
html.bili_dark .logout-item .link-icon path { fill: #fff !important; }
.theme-light .logout-item span { color: #fff !important; }
html[data-theme="light"] .logout-item .link-icon path,
html.light .logout-item .link-icon path { fill: ${primaryLight} !important; }
/* 等级条 */
.level-item { position:relative; transition:transform 0.3s ease; z-index:10; }
.level-item:hover { transform:scale(1.08); }
.level-progress { background: var(--level-bar-bg) !important; border-radius:3px; height:6px; overflow:visible; }
.level-progress__inner { height:100%; background: var(--level-inner-bg) !important; border-radius:3px; transition:all 0.4s ease; position:relative; }
.level-progress__inner::after {
content:''; position:absolute; right:-10px; top:50%; transform:translateY(-50%);
width:20px; height:20px; background:radial-gradient(circle,#8a2be2,transparent);
opacity:0; transition:opacity 0.3s; filter:blur(2px);
}
.level-item:hover .level-progress__inner::after { opacity:1; }
.level-item:hover .level-progress__inner {
background: var(--primary);
box-shadow: 0 0 10px var(--primary);
}
.level-intro { display:none; }
/* 大头像 */
.big-avatar { margin:0 auto 12px; display:block; filter: var(--avatar-shadow); transition:all 0.3s ease; }
.big-avatar:hover { transform:scale(1.05) translateY(-4px); filter: var(--avatar-hover-shadow); }
/* 菜单(面板内部) */
.avatar-panel-popover .single-link-item:hover {
background: var(--single-link-hover-bg) !important;
border-left:3px solid var(--primary) !important;
color: var(--primary);
}
/* 子菜单弹窗 */
.v-popover.is-right { overflow:visible; z-index:9999; }
html[data-theme="dark"] .sub-links-item,
html.dark .sub-links-item,
html.bili_dark .sub-links-item {
border-radius:12px; backdrop-filter:blur(12px);
background:rgba(16,16,22,0.85); color:#fff; box-shadow:0 8px 24px rgba(0,0,0,0.4);
}
html[data-theme="light"] .sub-links-item,
html.light .sub-links-item {
border-radius:12px; backdrop-filter:blur(12px);
background:rgba(252,252,250,0.88); color:#2d2d2d; box-shadow:0 8px 24px rgba(0,0,0,0.12);
}
html[data-theme="dark"] .sub-links-item .single-link-item:hover,
html.dark .sub-links-item .single-link-item:hover,
html.bili_dark .sub-links-item .single-link-item:hover {
background:rgba(${pd},0.1); color:${primaryDark};
}
html[data-theme="light"] .sub-links-item .single-link-item:hover,
html.light .sub-links-item .single-link-item:hover {
background:rgba(${pd},0.08); color:${primaryDark};
}
/* 关键帧 */
@keyframes pfPopoverFadeIn {
from { opacity:0; transform:translateY(-12px) scale(0.97); }
to { opacity:1; transform:translateY(0) scale(1); }
}
@keyframes pfBgBreath {
0%,100% { opacity:0.1; } 50% { opacity:0.5; }
}
.level-item__bar--next:hover { animation:pfCharge 0.6s infinite ease-in-out; }
@keyframes pfCharge {
0%,100% { transform:scale(1) rotate(0deg); filter:drop-shadow(0 0 5px #ff0000); }
50% { transform:scale(1.25) rotate(0deg); filter:drop-shadow(0 0 15px #ff0000); }
}
#top {
font-size: 0; /* 隐藏原有文字 */
}
#top::before {
content: "⚠️";
font-size: initial; /* 恢复图标字号 */
}
#contents span {
font-size: 20px;
color: #2266dd;
}
`;
}
function applyPanelBackground() {
const panel = document.querySelector('.avatar-panel-popover');
if (!panel) return;
// 移除旧背景层
const oldBg = panel.querySelector('.panel-custom-bg-layer');
if (oldBg) oldBg.remove();
// 用内存中的 dataURL(从IndexedDB加载),不存GM存储
// 只要DB里有图就显示,不依赖panelBgImageSet标志(避免刷新后标志丢失导致不显示)
const imgData = panelBgDataUrl;
if (imgData) {
const bgLayer = document.createElement('div');
bgLayer.className = 'panel-custom-bg-layer';
bgLayer.style.cssText = `
position:absolute; inset:0; z-index:0; pointer-events:none;
background-image:url("${imgData}");
background-size:${panelBgSize};
background-position:${panelBgPos};
background-repeat:no-repeat;
opacity:${config.panelBgOpacity};
`;
panel.style.position = 'relative';
panel.insertBefore(bgLayer, panel.firstChild);
// 确保内容在背景上层
Array.from(panel.children).forEach(child => {
if (!child.classList.contains('panel-custom-bg-layer')) {
const pos = getComputedStyle(child).position;
if (pos === 'static') child.style.position = 'relative';
if (!child.style.zIndex) child.style.zIndex = '1';
}
});
}
}
// ====================== rebuild 等函数 ======================
function rebuild() {
cleanupDOM();
if (styleEl) styleEl.remove();
styleEl = document.createElement('style');
styleEl.id = 'bili-panel-style';
styleEl.textContent = buildCSS();
document.head.appendChild(styleEl);
applyThemeClass();
applyPanelBackground();
if (config.showHardCoreTag) insertTag();
else removeTag();
applyNickname();
}
function cleanupDOM() {
document.querySelectorAll('.nickname-item').forEach(el => {
el.style.fontSize = '';
el.style.color = '';
});
document.querySelectorAll('.nickname-layout-container').forEach(c => c.remove());
document.querySelectorAll('.panel-custom-bg-layer').forEach(c => c.remove());
removeTag();
}
function fullCleanup() {
cleanupDOM();
if (styleEl) { styleEl.remove(); styleEl = null; }
tagInserted = false;
}
function insertTag() {
if (tagInserted || !config.showHardCoreTag) return;
const panel = document.querySelector('.avatar-panel-popover');
const nickEl = panel?.querySelector('.nickname-item');
const coinEl = panel?.querySelector('.coins-item');
if (!panel || !nickEl || !coinEl) return;
let tag = panel.querySelector('.hard-core-tag');
if (!tag) {
tag = document.createElement('div');
tag.className = 'hard-core-tag';
nickEl.parentNode.insertBefore(tag, coinEl);
}
tag.textContent = config.hardCoreText;
tagInserted = true;
}
function removeTag() {
const tag = document.querySelector('.hard-core-tag');
if (tag) tag.remove();
tagInserted = false;
}
function applyNickname() {
// 注入特效动画CSS(只注入一次)
if (!document.getElementById('bili-nick-effect-style')) {
const s = document.createElement('style');
s.id = 'bili-nick-effect-style';
s.textContent = `
@keyframes nickRainbowFlow {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes nickMonoFlow {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes nickGlowPulse {
0%, 100% {
filter: brightness(1) drop-shadow(0 0 4px currentColor);
opacity: 0.9;
}
50% {
filter: brightness(1.3) drop-shadow(0 0 10px currentColor) drop-shadow(0 0 18px currentColor);
opacity: 1;
}
}
.nickname-layout-container {
--nick-duration: 3s;
}
.nickname-layout-container.nick-effect-rainbow .nickname-block {
background: linear-gradient(90deg, #ff0000, #ff9900, #33ff00, #00ffff, #0066ff, #9900ff, #ff00ff, #ff0000);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent !important;
-webkit-text-fill-color: transparent;
animation: nickRainbowFlow var(--nick-duration) linear infinite;
will-change: background-position;
transform: translateZ(0);
}
.nickname-layout-container.nick-effect-mono .nickname-block {
background: linear-gradient(90deg, transparent 0%, currentColor 40%, currentColor 60%, transparent 100%);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent !important;
-webkit-text-fill-color: transparent;
animation: nickMonoFlow var(--nick-duration) linear infinite;
will-change: background-position;
transform: translateZ(0);
}
.nickname-layout-container.nick-effect-glow .nickname-block {
animation: nickGlowPulse var(--nick-duration) ease-in-out infinite;
will-change: filter, opacity;
transform: translateZ(0);
}
`;
document.head.appendChild(s);
}
const panel = document.querySelector('.avatar-panel-popover');
if (!panel) return;
document.querySelectorAll('.nickname-layout-container').forEach(c => c.remove());
// 兼容旧配置
if (!nicknameLayout.effect) nicknameLayout.effect = 'none';
if (!nicknameLayout.effectColor) nicknameLayout.effectColor = '#FF69B4';
if (!nicknameLayout.effectSpeed) nicknameLayout.effectSpeed = 5;
// 计算动画时长:速度1→8秒,速度10→1秒
const duration = (9 - nicknameLayout.effectSpeed * 0.8) + 's';
const nickItems = panel.querySelectorAll('.nickname-item.dark, .nickname-item.light');
nickItems.forEach(nickItem => {
nickItem.style.fontSize = '0';
nickItem.style.color = 'transparent';
const container = document.createElement('div');
container.className = 'nickname-layout-container';
container.style.cssText = 'position:absolute; inset:0; display:flex; pointer-events:none;';
container.style.setProperty('--nick-duration', duration);
// 添加特效类和颜色
if (nicknameLayout.effect && nicknameLayout.effect !== 'none') {
container.classList.add('nick-effect-' + nicknameLayout.effect);
// 单色流光和荧光需要设置主颜色
if (nicknameLayout.effect === 'mono' || nicknameLayout.effect === 'glow') {
container.style.color = nicknameLayout.effectColor;
}
}
// 先收集所有块,计算最大宽度用于延迟计算
const allBlocks = [];
if (nicknameLayout?.columns) {
let colOffsetX = 0;
nicknameLayout.columns.forEach(col => {
const colDiv = document.createElement('div');
colDiv.className = 'nickname-col';
colDiv.style.cssText = `position:relative; width:${col.width}; height:100%; flex-shrink:0;`;
if (col.blocks) {
col.blocks.forEach(block => {
allBlocks.push({ block, colDiv, colOffsetX, x: block.x });
});
}
container.appendChild(colDiv);
});
// 计算整体流动的总宽度(用于动画延迟同步)
// 找到所有块的最左x和最右x,总范围约350px足够覆盖
const flowRange = 400;
// 为每个块创建元素,设置动画延迟实现整体连贯流动
allBlocks.forEach(({ block, colDiv }) => {
const span = document.createElement('span');
span.className = 'nickname-block';
span.textContent = block.text;
span.style.cssText = `
position:absolute; left:${block.x}px; top:${block.y}px;
color:${block.color}; font-size:${block.fontSize}px;
transform:rotate(${block.rotation}deg) translateZ(0); white-space:pre;
pointer-events:none; line-height:1; font-weight:bold;
`;
// 流光特效:根据x位置设置负动画延迟,实现整体连贯流动
if ((nicknameLayout.effect === 'rainbow' || nicknameLayout.effect === 'mono')) {
// 从左到右延迟递减,x=0时delay=0,x越大delay越负(光从左流向右)
const delay = -(block.x / flowRange) * parseFloat(duration);
span.style.animationDelay = delay + 's';
}
colDiv.appendChild(span);
});
nickItem.appendChild(container);
}
});
}
function toggle() {
spaceEnabled = !spaceEnabled;
GM_setValue(STORAGE.SPACE_STYLE, spaceEnabled);
if (spaceEnabled) rebuild();
else fullCleanup();
}
function updateUid() {
const val = prompt('请输入目标UID(纯数字)', targetUid);
if (val && /^\d+$/.test(val.trim())) {
targetUid = val.trim();
GM_setValue(STORAGE.TARGET_UID, targetUid);
rebuild();
} else if (val !== null) alert('UID必须为纯数字');
}
// ====================== 编辑器 ======================
function openEditor() {
if (editorEl) { editorEl.style.display = 'block'; return; }
// 兼容旧配置
if (!nicknameLayout.effect) nicknameLayout.effect = 'none';
if (!nicknameLayout.effectColor) nicknameLayout.effectColor = '#FF69B4';
editorBackup = JSON.parse(JSON.stringify(nicknameLayout));
const panel = document.createElement('div');
panel.id = 'bili-nickname-editor';
panel.style.cssText = `
position:fixed; top:30px; left:50%; transform:translateX(-50%);
width:900px; max-width:95vw; background:rgba(20,20,30,0.95);
backdrop-filter:blur(16px); color:#fff; border-radius:14px;
box-shadow:0 8px 32px rgba(0,0,0,0.6); z-index:100000;
padding:20px; font-size:13px; max-height:92vh; overflow-y:auto;
`;
panel.innerHTML = `
✨ 昵称拖拽布局编辑器
预览区(拖动文字调整位置,点击文字编辑属性,所有分栏并排显示,和实际效果一致)
`;
document.body.appendChild(panel);
editorEl = panel;
// 初始化特效选择
document.getElementById('nickEffect').value = nicknameLayout.effect;
updateEffectColorVisibility();
renderCols();
renderPreview();
bindEditorEvents();
document.getElementById('addColBtn').onclick = () => {
nicknameLayout.columns.push({ id: uid(), width: `${Math.floor(100/(nicknameLayout.columns.length+1))}%`, blocks: [] });
// 平均调整所有列宽
const avgW = Math.floor(100/nicknameLayout.columns.length);
nicknameLayout.columns.forEach(c => c.width = avgW + '%');
renderCols(); renderPreview();
};
document.getElementById('addBlockBtn').onclick = () => {
if (!nicknameLayout.columns.length) { alert('请先添加分栏'); return; }
const idx = activeColIdx();
nicknameLayout.columns[idx].blocks.push({ id: uid(), text: '新文本', x: 10, y: 10, color: nicknameLayout.effectColor !== '#000000' ? nicknameLayout.effectColor : '#FF69B4', fontSize: 22, rotation: 0 });
renderPreview(); renderCols();
};
document.getElementById('saveNickBtn').onclick = () => { saveLayout(); closeEditor(true); };
document.getElementById('cancelNickBtn').onclick = () => {
nicknameLayout = JSON.parse(JSON.stringify(editorBackup));
applyNickname();
closeEditor(false);
};
// 特效切换
document.getElementById('nickEffect').onchange = (e) => {
nicknameLayout.effect = e.target.value;
updateEffectColorVisibility();
applyNickname();
};
document.getElementById('nickEffectColor').oninput = (e) => {
nicknameLayout.effectColor = e.target.value;
applyNickname();
};
document.getElementById('nickEffectSpeed').oninput = (e) => {
nicknameLayout.effectSpeed = parseInt(e.target.value);
document.getElementById('effectSpeedVal').textContent = nicknameLayout.effectSpeed;
applyNickname();
};
}
function updateEffectColorVisibility() {
const row = document.getElementById('effectColorRow');
if (!row) return;
row.style.display = nicknameLayout.effect === 'rainbow' ? 'none' : 'flex';
}
function closeEditor(saved) {
if (!editorEl) return;
if (!saved) { nicknameLayout = JSON.parse(JSON.stringify(editorBackup)); applyNickname(); }
editorEl.remove(); editorEl = null;
}
function activeColIdx() {
const a = document.querySelector('#editorColumns .col-tag.active');
return a ? parseInt(a.dataset.index) : 0;
}
function renderCols() {
const c = document.getElementById('editorColumns');
if (!c) return;
c.innerHTML = '';
nicknameLayout.columns.forEach((col, i) => {
const tag = document.createElement('div');
tag.className = 'col-tag' + (i === 0 ? ' active' : '');
tag.dataset.index = i;
tag.style.cssText = 'padding:4px 8px; background:#3a3a5c; border-radius:4px; cursor:pointer; margin-right:4px; font-size:12px; display:flex; align-items:center;';
tag.textContent = `列${i+1} (${col.blocks.length})`;
tag.onclick = () => {
document.querySelectorAll('#editorColumns .col-tag').forEach(t => t.classList.remove('active'));
tag.classList.add('active');
renderPreview();
};
const del = document.createElement('span');
del.textContent = '×';
del.style.cssText = 'margin-left:6px; color:#f66; cursor:pointer;';
del.onclick = e => {
e.stopPropagation();
if (nicknameLayout.columns.length <= 1) return alert('至少保留一个分栏');
nicknameLayout.columns.splice(i, 1);
renderCols(); renderPreview();
};
tag.appendChild(del);
c.appendChild(tag);
});
}
function renderPreview() {
const preview = document.getElementById('editorPreview');
if (!preview) return;
preview.innerHTML = '';
// Word式:所有分栏并排显示,每列之间加分隔线
nicknameLayout.columns.forEach((col, colIdx) => {
const colDiv = document.createElement('div');
colDiv.className = 'nickname-editor-col';
colDiv.style.cssText = `
position:relative; width:${col.width}; height:100%;
border-right:${colIdx < nicknameLayout.columns.length-1 ? '1px dashed rgba(255,255,255,0.15)' : 'none'};
`;
colDiv.dataset.colIndex = colIdx;
// 点击空白区域取消选中
colDiv.addEventListener('click', e => {
if (e.target === colDiv) {
document.getElementById('blockProps').style.display = 'none';
}
});
col.blocks.forEach(block => {
const span = document.createElement('span');
span.textContent = block.text;
span.style.cssText = `
position:absolute; left:${block.x}px; top:${block.y}px;
color:${block.color}; font-size:${block.fontSize}px;
transform:rotate(${block.rotation}deg); white-space:pre;
cursor:move; user-select:none; font-weight:bold;
line-height:1; border:1px solid rgba(255,255,255,0.25);
padding:2px 3px; background:rgba(0,0,0,0.4);
border-radius:2px;
`;
span.dataset.blockId = block.id;
span.dataset.colIndex = colIdx;
span.addEventListener('mousedown', onDragStart);
span.addEventListener('click', e => {
e.stopPropagation();
selectBlock(block.id, colIdx);
// 切换当前列为选中列
document.querySelectorAll('#editorColumns .col-tag').forEach(t => t.classList.remove('active'));
const tag = document.querySelector(`#editorColumns .col-tag[data-index="${colIdx}"]`);
if (tag) tag.classList.add('active');
});
colDiv.appendChild(span);
});
preview.appendChild(colDiv);
});
}
let dragState = null;
function onDragStart(e) {
e.preventDefault();
e.stopPropagation();
const preview = document.getElementById('editorPreview');
const rect = preview.getBoundingClientRect();
const colIdx = parseInt(e.target.dataset.colIndex);
const block = nicknameLayout.columns[colIdx].blocks.find(b => b.id === e.target.dataset.blockId);
if (!block) return;
dragState = { block, colIdx, startX: e.clientX, startY: e.clientY, origX: block.x, origY: block.y, rect };
window.addEventListener('mousemove', onDragMove);
window.addEventListener('mouseup', onDragEnd);
}
function onDragMove(e) {
if (!dragState) return;
// 扩大拖动范围,允许拖到列外±80px
dragState.block.x = Math.max(-80, Math.min(200+80, dragState.origX + e.clientX - dragState.startX));
dragState.block.y = Math.max(-80, Math.min(80+80, dragState.origY + e.clientY - dragState.startY));
renderPreview();
// 保持选中状态
selectBlock(dragState.block.id, dragState.colIdx);
applyNickname();
}
function onDragEnd() {
window.removeEventListener('mousemove', onDragMove);
window.removeEventListener('mouseup', onDragEnd);
dragState = null;
}
function selectBlock(blockId, colIdx) {
if (colIdx === undefined) colIdx = activeColIdx();
const block = nicknameLayout.columns[colIdx]?.blocks.find(b => b.id === blockId);
if (!block) return;
const p = document.getElementById('blockProps');
p.style.display = 'block';
document.getElementById('blockText').value = block.text;
document.getElementById('blockColor').value = block.color;
document.getElementById('blockSize').value = block.fontSize;
document.getElementById('blockRotate').value = block.rotation;
p.dataset.blockId = blockId;
p.dataset.colIndex = colIdx;
// 高亮选中块
document.querySelectorAll('#editorPreview span').forEach(s => {
s.style.outline = s.dataset.blockId === blockId ? '2px solid #ff69b4' : 'none';
});
}
function bindEditorEvents() {
['blockText','blockColor','blockSize','blockRotate'].forEach(id => {
document.getElementById(id).oninput = () => {
const p = document.getElementById('blockProps');
const ci = parseInt(p.dataset.colIndex);
const block = nicknameLayout.columns[ci]?.blocks.find(b => b.id === p.dataset.blockId);
if (!block) return;
block.text = document.getElementById('blockText').value;
block.color = document.getElementById('blockColor').value;
block.fontSize = parseInt(document.getElementById('blockSize').value) || 22;
block.rotation = parseFloat(document.getElementById('blockRotate').value) || 0;
renderPreview();
selectBlock(block.id, ci);
applyNickname();
};
});
document.getElementById('deleteBlockBtn').onclick = () => {
const p = document.getElementById('blockProps');
const ci = parseInt(p.dataset.colIndex);
const col = nicknameLayout.columns[ci];
if (!col) return;
col.blocks = col.blocks.filter(b => b.id !== p.dataset.blockId);
p.style.display = 'none';
renderCols(); renderPreview();
applyNickname();
};
}
// ====================== 配置面板 ======================
function openConfig() {
if (configPanelEl) { configPanelEl.style.display = 'block'; return; }
const listMgrExists = isListManagerLoaded();
const currentCid = getCollectionIdFromPage();
const beautyCfg = getListBeautyConfig();
const panel = document.createElement('div');
panel.id = 'bili-custom-config-panel';
panel.style.cssText = `
position:fixed; top:80px; left:20px; width:420px;
background:rgba(16,16,22,0.85); backdrop-filter:blur(14px);
color:#fff; padding:16px; border-radius:14px;
box-shadow:0 6px 24px rgba(0,0,0,0.4); z-index:99999;
font-size:13px; max-height:85vh; overflow-y:auto;
`;
let listSectionHtml = '';
if (listMgrExists) {
listSectionHtml = `
`;
} else {
listSectionHtml = `
⚠️ 未检测到「候选分P列表管理器」脚本,列表美化配置项已隐藏
`;
}
panel.innerHTML = `
个性化面板配置
${listSectionHtml}
`;
GM_addStyle('#bili-custom-config-panel input, #bili-custom-config-panel select, #bili-custom-config-panel textarea { background:#222 !important; color:#fff !important; border:1px solid #444; }');
document.body.appendChild(panel);
configPanelEl = panel;
// ========== 列表美化功能 ==========
if (listMgrExists) {
const _global = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const previewEl = document.getElementById('listBgPreview');
const previewImg = document.getElementById('listBgPreviewImg');
const placeholder = document.getElementById('previewPlaceholder');
const navEl = document.getElementById('listBgNav');
const navImg = document.getElementById('listBgNavImg');
const navFrame = document.getElementById('listBgNavFrame');
let currentPreviewDataUrl = null;
let currentTargetId = '__default__'; // 当前正在编辑的图片ID
let imgNaturalW = 0, imgNaturalH = 0;
let currentBgSize = beautyCfg.bgSize || 'cover';
let isDragging = false;
let isDraggingNav = false;
let dragStartX = 0, dragStartY = 0;
let dragStartPosX = 50, dragStartPosY = 50;
let currentPosX = 50, currentPosY = 50;
const PREVIEW_W = 280, PREVIEW_H = 460;
// 计算当前size下图片在容器中的渲染尺寸(仅用于拖动计算和小地图)
const calcRenderSize = (sizeMode, containerW, containerH, imgW, imgH) => {
if (!imgW || !imgH) return { renderW: containerW, renderH: containerH };
let renderW, renderH;
if (sizeMode === 'cover') {
const scale = Math.max(containerW / imgW, containerH / imgH);
renderW = imgW * scale;
renderH = imgH * scale;
} else if (sizeMode === 'contain') {
const scale = Math.min(containerW / imgW, containerH / imgH);
renderW = imgW * scale;
renderH = imgH * scale;
} else if (sizeMode === 'stretch') {
renderW = containerW;
renderH = containerH;
} else {
renderW = imgW;
renderH = imgH;
}
return { renderW, renderH };
};
// 更新导航小地图裁切框
const updateNavFrame = () => {
if (!imgNaturalW || !imgNaturalH || !currentPreviewDataUrl) { navFrame.style.display = 'none'; return; }
const { renderW, renderH } = calcRenderSize(currentBgSize, PREVIEW_W, PREVIEW_H, imgNaturalW, imgNaturalH);
const imgOffsetX = (PREVIEW_W - renderW) * currentPosX / 100;
const imgOffsetY = (PREVIEW_H - renderH) * currentPosY / 100;
const visibleX = Math.max(0, -imgOffsetX);
const visibleY = Math.max(0, -imgOffsetY);
const visibleW = Math.min(renderW, PREVIEW_W - Math.max(0, imgOffsetX));
const visibleH = Math.min(renderH, PREVIEW_H - Math.max(0, imgOffsetY));
if (visibleW <= 1 || visibleH <= 1) { navFrame.style.display = 'none'; return; }
const navScale = Math.min(80 / imgNaturalW, 134 / imgNaturalH);
const navImgW = imgNaturalW * navScale;
const navImgH = imgNaturalH * navScale;
const navOffX = (84 - navImgW) / 2;
const navOffY = (138 - navImgH) / 2;
const scale = renderW / imgNaturalW;
navFrame.style.display = 'block';
navFrame.style.left = (navOffX + visibleX / scale * navScale) + 'px';
navFrame.style.top = (navOffY + visibleY / scale * navScale) + 'px';
navFrame.style.width = Math.max(4, visibleW / scale * navScale) + 'px';
navFrame.style.height = Math.max(4, visibleH / scale * navScale) + 'px';
};
// 保存当前位置/尺寸/透明度到DB和全局配置,实时应用到面板
const saveAndApply = (updateGlobalCfg = false) => {
const opacity = Number(document.getElementById('listBgOpacity').value);
const position = `${currentPosX}% ${currentPosY}%`;
// 1. 更新预览显示
previewImg.style.opacity = opacity;
previewImg.style.backgroundSize = currentBgSize;
previewImg.style.backgroundPosition = position;
document.getElementById('bgOpacityVal').textContent = Math.round(opacity*100) + '%';
// 2. 更新全局配置(磨砂、面板透明度等)
const globalUpdate = {};
if (updateGlobalCfg) {
globalUpdate.frostedGlass = document.getElementById('listFrostedGlass').checked;
globalUpdate.glassBlur = Number(document.getElementById('listGlassBlur').value);
globalUpdate.drawerOpacity = Number(document.getElementById('listDrawerOpacity').value);
document.getElementById('glassBlurVal').textContent = globalUpdate.glassBlur + 'px';
document.getElementById('drawerOpacityVal').textContent = Math.round(globalUpdate.drawerOpacity*100) + '%';
}
globalUpdate.bgSize = currentBgSize;
globalUpdate.bgPosition = position;
globalUpdate.defaultBgOpacity = opacity;
// 保存全局配置
const cfg = { ...getListBeautyConfig(), ...globalUpdate };
saveListBeautyConfig(cfg);
// 3. 如果有正在编辑的图片,直接保存位置/尺寸/透明度到DB条目
if (currentPreviewDataUrl && _global.__biliListSaveBgImage) {
_global.__biliListSaveBgImage(currentTargetId, currentPreviewDataUrl, {
opacity: opacity,
bgSize: currentBgSize,
bgPosition: position
});
} else {
// 没有图片,直接调用更新函数
if (_global.__biliListUpdateBeautyCfg) _global.__biliListUpdateBeautyCfg(globalUpdate);
// 触发信号元素更新(备用)
let signal = document.getElementById('__bili_list_beauty_signal');
if (!signal) { signal = document.createElement('div'); signal.id = '__bili_list_beauty_signal'; signal.style.display = 'none'; document.body.appendChild(signal); }
signal.setAttribute('data-version', Date.now().toString());
}
// 4. 更新小地图
updateNavFrame();
};
// 加载图片获取尺寸
const loadImageSize = (dataUrl) => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => { imgNaturalW = img.naturalWidth; imgNaturalH = img.naturalHeight; resolve(); };
img.onerror = () => resolve();
img.src = dataUrl;
});
};
// 加载背景到预览
const loadBgToPreview = async (targetId) => {
currentTargetId = targetId;
let bgEntry = null;
const db = await openListBgDB();
bgEntry = await new Promise(resolve => {
const tx = db.transaction(LIST_BG_STORE, 'readonly');
const req = tx.objectStore(LIST_BG_STORE).get(targetId);
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => resolve(null);
});
if (targetId !== '__default__' && !bgEntry) {
// 当前列表没有自定义,加载默认
bgEntry = await new Promise(resolve => {
const tx = db.transaction(LIST_BG_STORE, 'readonly');
const req = tx.objectStore(LIST_BG_STORE).get('__default__');
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => resolve(null);
});
currentTargetId = '__default__';
}
if (bgEntry && bgEntry.imageData) {
currentPreviewDataUrl = bgEntry.imageData;
previewImg.style.backgroundImage = `url("${bgEntry.imageData}")`;
navImg.style.backgroundImage = `url("${bgEntry.imageData}")`;
placeholder.style.display = 'none';
navEl.style.display = 'block';
previewEl.style.cursor = 'grab';
if (bgEntry.opacity !== undefined) document.getElementById('listBgOpacity').value = bgEntry.opacity;
if (bgEntry.bgSize) {
currentBgSize = bgEntry.bgSize;
document.getElementById('listBgSize').value = bgEntry.bgSize.endsWith('px') ? 'cover' : bgEntry.bgSize;
}
if (bgEntry.bgPosition) {
const m = bgEntry.bgPosition.match(/(-?\d+(?:\.\d+)?)%\s+(-?\d+(?:\.\d+)?)%/);
if (m) { currentPosX = parseFloat(m[1]); currentPosY = parseFloat(m[2]); }
}
await loadImageSize(bgEntry.imageData);
} else {
currentPreviewDataUrl = null;
imgNaturalW = 0; imgNaturalH = 0;
previewImg.style.backgroundImage = 'none';
navImg.style.backgroundImage = 'none';
placeholder.style.display = 'block';
navEl.style.display = 'none';
previewEl.style.cursor = 'default';
currentPosX = 50; currentPosY = 50;
}
saveAndApply(true);
};
// 初始化加载
const initCid = currentCid || '__default__';
loadBgToPreview(initCid);
// 绑定所有控件事件
document.getElementById('listFrostedGlass').addEventListener('change', () => saveAndApply(true));
document.getElementById('listGlassBlur').addEventListener('input', () => saveAndApply(true));
document.getElementById('listDrawerOpacity').addEventListener('input', () => saveAndApply(true));
document.getElementById('listBgOpacity').addEventListener('input', () => saveAndApply(false));
document.getElementById('listBgSize').addEventListener('change', (e) => {
currentBgSize = e.target.value;
currentPosX = 50; currentPosY = 50; // 切换模式重置到中心
saveAndApply(false);
});
// 主预览拖动
previewEl.addEventListener('mousedown', (e) => {
if (e.target.closest('#listBgNav')) return;
if (!currentPreviewDataUrl || !imgNaturalW) return;
isDragging = true;
dragStartX = e.clientX; dragStartY = e.clientY;
dragStartPosX = currentPosX; dragStartPosY = currentPosY;
previewEl.style.cursor = 'grabbing';
e.preventDefault();
});
// 导航拖动
navEl.addEventListener('mousedown', (e) => {
if (!currentPreviewDataUrl || !imgNaturalW) return;
isDraggingNav = true;
dragStartX = e.clientX; dragStartY = e.clientY;
dragStartPosX = currentPosX; dragStartPosY = currentPosY;
e.preventDefault(); e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging && !isDraggingNav) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const { renderW, renderH } = calcRenderSize(currentBgSize, PREVIEW_W, PREVIEW_H, imgNaturalW, imgNaturalH);
const denomX = PREVIEW_W - renderW;
const denomY = PREVIEW_H - renderH;
let dpx = 0, dpy = 0;
if (isDraggingNav) {
const navScale = Math.min(80 / imgNaturalW, 134 / imgNaturalH);
const scale = renderW / imgNaturalW;
const factor = navScale * scale;
dpx = denomX !== 0 ? (-dx / factor) * 100 / denomX : 0;
dpy = denomY !== 0 ? (-dy / factor) * 100 / denomY : 0;
} else {
dpx = denomX !== 0 ? (-dx) * 100 / denomX : 0;
dpy = denomY !== 0 ? (-dy) * 100 / denomY : 0;
}
currentPosX = dragStartPosX + dpx;
currentPosY = dragStartPosY + dpy;
// 允许上下左右各拖出500px空白
const extraX = denomX !== 0 ? (500 / Math.abs(denomX)) * 100 : 300;
const extraY = denomY !== 0 ? (500 / Math.abs(denomY)) * 100 : 300;
currentPosX = Math.max(-extraX, Math.min(100 + extraX, currentPosX));
currentPosY = Math.max(-extraY, Math.min(100 + extraY, currentPosY));
saveAndApply(false);
});
document.addEventListener('mouseup', () => {
isDragging = false; isDraggingNav = false;
previewEl.style.cursor = currentPreviewDataUrl ? 'grab' : 'default';
});
// 图片上传
const handleUpload = (input, targetId) => {
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) { alert('请选择图片'); return; }
const reader = new FileReader();
reader.onload = async (ev) => {
const dataUrl = ev.target.result;
await loadImageSize(dataUrl);
currentPreviewDataUrl = dataUrl;
currentTargetId = targetId;
currentPosX = 50; currentPosY = 50;
document.getElementById('listBgSize').value = 'cover';
currentBgSize = 'cover';
previewImg.style.backgroundImage = `url("${dataUrl}")`;
navImg.style.backgroundImage = `url("${dataUrl}")`;
placeholder.style.display = 'none';
navEl.style.display = 'block';
previewEl.style.cursor = 'grab';
saveAndApply(true);
alert('背景图已上传并实时应用');
};
reader.readAsDataURL(file);
input.value = '';
});
};
if (currentCid) handleUpload(document.getElementById('currentBgUpload'), currentCid);
handleUpload(document.getElementById('defaultBgUpload'), '__default__');
// 清除背景
document.getElementById('clearCurrentBg').onclick = async () => {
if (!currentCid) return alert('当前不是视频播放页');
if (!confirm('确定清除当前列表背景?')) return;
if (_global.__biliListDeleteBgImage) await _global.__biliListDeleteBgImage(currentCid);
else deleteListBgImage(currentCid);
loadBgToPreview(currentCid);
alert('已清除');
};
document.getElementById('clearDefaultBg').onclick = async () => {
if (!confirm('确定清除默认背景?')) return;
if (_global.__biliListDeleteBgImage) await _global.__biliListDeleteBgImage('__default__');
else deleteListBgImage('__default__');
loadBgToPreview(currentCid || '__default__');
alert('已清除');
};
}
// ========== 个人面板背景编辑器(与列表背景完全独立) ==========
const pBgPreview = document.getElementById('panelBgPreview');
const pBgPreviewImg = document.getElementById('panelBgPreviewImg');
const pBgPlaceholder = document.getElementById('panelBgPlaceholder');
const pBgNav = document.getElementById('panelBgNav');
const pBgNavImg = document.getElementById('panelBgNavImg');
const pBgNavFrame = document.getElementById('panelBgNavFrame');
const P_PREVIEW_W = 280, P_PREVIEW_H = 360;
let pBgNaturalW = 0, pBgNaturalH = 0;
let pBgIsDragging = false, pBgIsDraggingNav = false;
let pBgDragStartX = 0, pBgDragStartY = 0;
let pBgDragStartPosX = 50, pBgDragStartPosY = 50;
const panelCalcRenderSize = (sizeMode, containerW, containerH, imgW, imgH) => {
if (!imgW || !imgH) return { renderW: containerW, renderH: containerH };
let renderW, renderH;
if (sizeMode === 'cover') {
const scale = Math.max(containerW / imgW, containerH / imgH);
renderW = imgW * scale; renderH = imgH * scale;
} else if (sizeMode === 'contain') {
const scale = Math.min(containerW / imgW, containerH / imgH);
renderW = imgW * scale; renderH = imgH * scale;
} else if (sizeMode === 'stretch' || sizeMode === '100% 100%') {
renderW = containerW; renderH = containerH;
} else {
renderW = imgW; renderH = imgH;
}
return { renderW, renderH };
};
const updatePanelNavFrame = () => {
if (!pBgNaturalW || !pBgNaturalH || !panelBgDataUrl) { pBgNavFrame.style.display = 'none'; return; }
const { renderW, renderH } = panelCalcRenderSize(panelBgSize, P_PREVIEW_W, P_PREVIEW_H, pBgNaturalW, pBgNaturalH);
const imgOffsetX = (P_PREVIEW_W - renderW) * panelBgPosX / 100;
const imgOffsetY = (P_PREVIEW_H - renderH) * panelBgPosY / 100;
const visibleX = Math.max(0, -imgOffsetX);
const visibleY = Math.max(0, -imgOffsetY);
const visibleW = Math.min(renderW, P_PREVIEW_W - Math.max(0, imgOffsetX));
const visibleH = Math.min(renderH, P_PREVIEW_H - Math.max(0, imgOffsetY));
if (visibleW <= 1 || visibleH <= 1) { pBgNavFrame.style.display = 'none'; return; }
const navScale = Math.min(70 / pBgNaturalW, 90 / pBgNaturalH);
const navImgW = pBgNaturalW * navScale;
const navImgH = pBgNaturalH * navScale;
const navOffX = (70 - navImgW) / 2;
const navOffY = (90 - navImgH) / 2;
const scale = renderW / pBgNaturalW;
pBgNavFrame.style.display = 'block';
pBgNavFrame.style.left = (navOffX + visibleX / scale * navScale) + 'px';
pBgNavFrame.style.top = (navOffY + visibleY / scale * navScale) + 'px';
pBgNavFrame.style.width = Math.max(4, visibleW / scale * navScale) + 'px';
pBgNavFrame.style.height = Math.max(4, visibleH / scale * navScale) + 'px';
};
// 保存个人面板背景(预览+DB+实际面板实时应用)
const savePanelBgAndApply = () => {
const opacity = Number(document.getElementById('panelBgOpacity').value);
const position = `${panelBgPosX}% ${panelBgPosY}%`;
pBgPreviewImg.style.opacity = opacity;
pBgPreviewImg.style.backgroundSize = panelBgSize;
pBgPreviewImg.style.backgroundPosition = position;
document.getElementById('panelBgOpacityVal').textContent = Math.round(opacity*100) + '%';
config.panelBgOpacity = opacity;
panelBgPos = position;
if (panelBgDataUrl) {
savePanelBgImage(panelBgDataUrl, { opacity, bgSize: panelBgSize, bgPosition: position });
}
applyPanelBackground();
updatePanelNavFrame();
};
const loadPanelImgSize = (dataUrl) => new Promise(resolve => {
const img = new Image();
img.onload = () => { pBgNaturalW = img.naturalWidth; pBgNaturalH = img.naturalHeight; resolve(); };
img.onerror = () => resolve();
img.src = dataUrl;
});
// 从DB加载个人面板背景到预览
const loadPanelBgToPreview = async () => {
const entry = await getPanelBgImage();
if (entry && entry.imageData) {
panelBgDataUrl = entry.imageData;
pBgPreviewImg.style.backgroundImage = `url("${entry.imageData}")`;
pBgNavImg.style.backgroundImage = `url("${entry.imageData}")`;
pBgPlaceholder.style.display = 'none';
pBgNav.style.display = 'block';
pBgPreview.style.cursor = 'grab';
if (entry.opacity !== undefined) document.getElementById('panelBgOpacity').value = entry.opacity;
if (entry.bgSize) {
panelBgSize = entry.bgSize;
document.getElementById('panelBgSize').value = entry.bgSize.endsWith('px') ? 'cover' : entry.bgSize;
}
if (entry.bgPosition) {
const m = entry.bgPosition.match(/(-?\d+(?:\.\d+)?)%\s+(-?\d+(?:\.\d+)?)%/);
if (m) { panelBgPosX = parseFloat(m[1]); panelBgPosY = parseFloat(m[2]); }
}
await loadPanelImgSize(entry.imageData);
} else {
panelBgDataUrl = null;
pBgNaturalW = 0; pBgNaturalH = 0;
pBgPreviewImg.style.backgroundImage = 'none';
pBgNavImg.style.backgroundImage = 'none';
pBgPlaceholder.style.display = 'block';
pBgNav.style.display = 'none';
pBgPreview.style.cursor = 'default';
panelBgPosX = 50; panelBgPosY = 50;
}
savePanelBgAndApply();
};
loadPanelBgToPreview();
// 绑定个人面板背景控件
document.getElementById('panelBgOpacity').addEventListener('input', savePanelBgAndApply);
document.getElementById('panelBgSize').addEventListener('change', (e) => {
panelBgSize = e.target.value;
panelBgPosX = 50; panelBgPosY = 50;
savePanelBgAndApply();
});
// 主预览拖动
pBgPreview.addEventListener('mousedown', (e) => {
if (e.target.closest('#panelBgNav')) return;
if (!panelBgDataUrl || !pBgNaturalW) return;
pBgIsDragging = true;
pBgDragStartX = e.clientX; pBgDragStartY = e.clientY;
pBgDragStartPosX = panelBgPosX; pBgDragStartPosY = panelBgPosY;
pBgPreview.style.cursor = 'grabbing';
e.preventDefault();
});
// 小地图拖动
pBgNav.addEventListener('mousedown', (e) => {
if (!panelBgDataUrl || !pBgNaturalW) return;
pBgIsDraggingNav = true;
pBgDragStartX = e.clientX; pBgDragStartY = e.clientY;
pBgDragStartPosX = panelBgPosX; pBgDragStartPosY = panelBgPosY;
e.preventDefault(); e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!pBgIsDragging && !pBgIsDraggingNav) return;
const dx = e.clientX - pBgDragStartX;
const dy = e.clientY - pBgDragStartY;
const { renderW, renderH } = panelCalcRenderSize(panelBgSize, P_PREVIEW_W, P_PREVIEW_H, pBgNaturalW, pBgNaturalH);
const denomX = P_PREVIEW_W - renderW;
const denomY = P_PREVIEW_H - renderH;
let dpx = 0, dpy = 0;
if (pBgIsDraggingNav) {
const navScale = Math.min(70 / pBgNaturalW, 90 / pBgNaturalH);
const scale = renderW / pBgNaturalW;
const factor = navScale * scale;
dpx = denomX !== 0 ? (-dx / factor) * 100 / denomX : 0;
dpy = denomY !== 0 ? (-dy / factor) * 100 / denomY : 0;
} else {
dpx = denomX !== 0 ? (-dx) * 100 / denomX : 0;
dpy = denomY !== 0 ? (-dy) * 100 / denomY : 0;
}
panelBgPosX = pBgDragStartPosX + dpx;
panelBgPosY = pBgDragStartPosY + dpy;
// 允许上下左右各拖出500px空白
const extraX = denomX !== 0 ? (500 / Math.abs(denomX)) * 100 : 300;
const extraY = denomY !== 0 ? (500 / Math.abs(denomY)) * 100 : 300;
panelBgPosX = Math.max(-extraX, Math.min(100 + extraX, panelBgPosX));
panelBgPosY = Math.max(-extraY, Math.min(100 + extraY, panelBgPosY));
savePanelBgAndApply();
});
document.addEventListener('mouseup', () => {
pBgIsDragging = false; pBgIsDraggingNav = false;
pBgPreview.style.cursor = panelBgDataUrl ? 'grab' : 'default';
});
// 个人面板背景上传(存入IndexedDB,不占GM存储)
document.getElementById('panelBgUpload').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) { alert('请选择图片'); return; }
const reader = new FileReader();
reader.onload = async (ev) => {
const dataUrl = ev.target.result;
await loadPanelImgSize(dataUrl);
config.panelBgImageSet = true;
panelBgDataUrl = dataUrl;
panelBgSize = 'cover';
panelBgPosX = 50; panelBgPosY = 50;
document.getElementById('panelBgSize').value = 'cover';
pBgPreviewImg.style.backgroundImage = `url("${dataUrl}")`;
pBgNavImg.style.backgroundImage = `url("${dataUrl}")`;
pBgPlaceholder.style.display = 'none';
pBgNav.style.display = 'block';
pBgPreview.style.cursor = 'grab';
await savePanelBgImage(dataUrl, {
opacity: Number(document.getElementById('panelBgOpacity').value),
bgSize: 'cover',
bgPosition: '50% 50%'
});
savePanelBgAndApply();
saveConfig(); // 持久化panelBgImageSet等配置到GM(内部会rebuild),刷新后不丢失
alert('个人面板背景已上传并实时应用');
};
reader.readAsDataURL(file);
e.target.value = '';
});
// 清除个人面板背景(从DB删除)
document.getElementById('clearPanelBg').onclick = () => {
if (!config.panelBgImageSet && !panelBgDataUrl) return alert('当前没有设置背景图');
if (!confirm('确定清除个人面板背景?')) return;
config.panelBgImageSet = false;
panelBgDataUrl = null;
panelBgSize = 'cover';
panelBgPosX = 50; panelBgPosY = 50;
pBgPreviewImg.style.backgroundImage = 'none';
pBgNavImg.style.backgroundImage = 'none';
pBgPlaceholder.style.display = 'block';
pBgNav.style.display = 'none';
pBgPreview.style.cursor = 'default';
deletePanelBgImage().then(() => {
saveConfig(); // 持久化清除状态(内部会rebuild)
});
};
const realtimeApply = () => {
config.bgWatermarkNum = document.getElementById('bgNum').value;
config.bgRotate = Number(document.getElementById('bgRotate').value) || 0;
config.bgBreathAnim = document.getElementById('bgBreath').checked;
config.bgFrosted = document.getElementById('bgFrosted').checked;
config.glassBlurDark = Number(document.getElementById('glassBlurDark').value) || 12;
config.glassBlurLight = Number(document.getElementById('glassBlurLight').value) || 12;
config.wmSizeDark = Number(document.getElementById('wmSizeDark').value) || 600;
config.wmSizeLight = Number(document.getElementById('wmSizeLight').value) || 500;
config.wmColorDark = document.getElementById('wmColorDark').value;
config.wmColorLight = document.getElementById('wmColorLight').value;
config.bgOpacityDark = Number(document.getElementById('bgOpacityDark').value) || 0.15;
config.bgOpacityLight = Number(document.getElementById('bgOpacityLight').value) || 0.08;
config.showHardCoreTag = document.getElementById('showHC').checked;
config.hardCoreText = document.getElementById('hcText').value;
config.primaryDark = document.getElementById('darkPrimary').value;
config.primaryLight = document.getElementById('lightPrimary').value;
// 实时预览并立即持久化到GM存储(面板提示"自动保存生效"),刷新后不丢失
saveConfig();
};
// 所有控件绑定实时更新(panelBgOpacity已由个人面板背景编辑器单独处理)
['bgNum','bgRotate','wmSizeDark','wmSizeLight','bgOpacityDark','bgOpacityLight','glassBlurDark','glassBlurLight','hcText'].forEach(id => {
document.getElementById(id).addEventListener('input', realtimeApply);
});
['bgBreath','bgFrosted','showHC','wmColorDark','wmColorLight','darkPrimary','lightPrimary'].forEach(id => {
document.getElementById(id).addEventListener('change', realtimeApply);
document.getElementById(id).addEventListener('input', realtimeApply);
});
document.getElementById('saveConfig').onclick = () => {
realtimeApply();
if (listMgrExists) {
const _g = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
// 配置已经实时保存了,这里再确认一下
const newBeautyCfg = {
frostedGlass: document.getElementById('listFrostedGlass').checked,
glassBlur: Number(document.getElementById('listGlassBlur').value) || 12,
drawerOpacity: Number(document.getElementById('listDrawerOpacity').value) || 0.85,
defaultBgOpacity: Number(document.getElementById('listBgOpacity').value) || 0.15,
bgSize: document.getElementById('listBgSize').value,
bgPosition: `${currentPosX}% ${currentPosY}%`
};
saveListBeautyConfig(newBeautyCfg);
if (_g.__biliListRefreshBg) _g.__biliListRefreshBg();
}
saveConfig();
showToast('✅ 配置已保存');
};
document.getElementById('resetConfig').onclick = () => {
if (!confirm('恢复默认配置?')) return;
config = { ...DEFAULT_PANEL_CONFIG };
panelBgDataUrl = null;
deletePanelBgImage();
saveConfig();
rebuild();
};
document.getElementById('closeConfig').onclick = () => { panel.remove(); configPanelEl = null; };
}
// ====================== 菜单 ======================
GM_registerMenuCommand(`面板美化:${spaceEnabled?'关闭':'开启'}`, toggle);
GM_registerMenuCommand('设置目标UID', updateUid);
GM_registerMenuCommand('昵称布局编辑器', openEditor);
GM_registerMenuCommand('个性化配置面板', openConfig);
// ====================== DOM 监听 ======================
function startObserver() {
if (observer) return;
observer = new MutationObserver(throttle(() => {
if (!spaceEnabled) return;
const panel = document.querySelector('.avatar-panel-popover');
if (!panel) return;
applyThemeClass();
applyPanelBackground();
if (config.showHardCoreTag) insertTag();
else removeTag();
applyNickname();
}, 150));
observer.observe(document.body, { childList: true, subtree: true });
}
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
tagInserted = false;
if (spaceEnabled) setTimeout(rebuild, 400);
}
}, 1000);
// ====================== 启动 ======================
observeHtmlTheme();
if (spaceEnabled) setTimeout(rebuild, 300);
startObserver();
let lastTheme = getTheme();
setInterval(() => {
const html = document.documentElement;
const dt = html.getAttribute('data-theme');
const hasDark = html.classList.contains('dark');
const hasLight = html.classList.contains('light');
const hasBiliDark = html.classList.contains('bili_dark');
const ls = localStorage.getItem('bilibili_theme');
const current = getTheme();
const allAttrs = {};
for (let i = 0; i < html.attributes.length; i++) {
allAttrs[html.attributes[i].name] = html.attributes[i].value;
}
const bg = getComputedStyle(document.body).backgroundColor;
const rootBg = getComputedStyle(html).getPropertyValue('--app-bg').trim() || '(empty)';
const rootColor = getComputedStyle(html).color;
if (current !== lastTheme) {
lastTheme = current;
applyThemeClass();
}
}, 6000);
})();
/******************************************************************************
* 主题系统架构说明 —— 修改前必读
* ============================================================================
*
* 一、背景:B站主题切换机制
* - B站新版深/浅切换 不设 data-theme 属性,不给 加 dark/light class
* → 唯一可靠的切换检测手段是监听 body 背景色亮度变化
* - 是 B站固定 class,深浅模式下都存在,不是主题标记
* - localStorage('bilibili_theme') 可读但 B站切换时不更新
*
* 二、检测策略(getTheme 函数)
* 优先级从高到低:
* 1. document.documentElement.getAttribute('data-theme')
* → B站某些页面(如设置页)会设此属性
* 2. html.classList.contains('light') / contains('dark')
* → B站部分子页面会设
* 3. localStorage.getItem('bilibili_theme')
* → 仅兜底,B站切换时不更新
* 4. 计算 document.body 背景色亮度(核心手段)
* → 亮度 < 128 判定为深色,≥ 128 判定为浅色
* 5. 以上均失败 → 默认 'dark'
*
* 轮询(setInterval 每 2 秒)检测上述任一值变化,
* 一旦检测到切换 → 调用 applyThemeClass 更新 panel 的 theme class。
*
* 禁止:移除或替换 body 背景色检测;禁止将 gray 作为主题标记。
*
* 三、CSS 架构(两套定义 + 两个域)
*
* 【第一套:html[data-theme] 驱动 — 切换无延迟】
* 选择器:html[data-theme="dark"] .avatar-panel-popover
* html.dark .avatar-panel-popover
* html[data-theme="light"] .avatar-panel-popover
* html.light .avatar-panel-popover
* 作用:当 B站设了 data-theme 或 dark/light class 时即时生效。
* 目前 B站深浅切换时不会触发此套,保留做兼容。
*
* 【第二套:panel class 驱动 — 兜底主力】
* 选择器:.theme-dark.avatar-panel-popover
* .theme-light.avatar-panel-popover
* 作用:由 applyThemeClass 设置 panel 的 theme-dark / theme-light class。
* B站浅色模式无任何 html 标记时,此套为唯一的样式来源。
* 必须包含全部变量(核心变量 + 装饰变量),不可省略。
*
* 两个域:
* A. 面板内部元素(.avatar-panel-popover 的子元素)
* → 使用 CSS 变量 var(--xxx),变量定义在 panel class 的 CSS 块中。
* 优点:不受 B站子元素加 .dark / .light class 影响。
* 所有 var() 必须加 !important,防止被 B站样式覆盖。
*
* B. 面板外部元素(.sub-links-item 等弹窗)
* → 直接使用 html[data-theme] 选择器硬编码值,不用 var()。
* 因为这些元素不在 panel 内部,继承不到 panel 上的 CSS 变量。
*
* 四、绝对不能做的事
* 1. 不要删除 body 背景色检测(getTheme 第 4 步)— 它是深浅切换的唯一可靠信号
* 2. 不要将 html.gray 作为主题标记加入检测或 CSS — gray 始终存在
* 3. 不要拆掉 .theme-dark / .theme-light 中的核心变量(panel-bg, logout-bg 等)
* — 它们是 B站浅色模式无 html 标记时的唯一样式来源
* 4. 不要去掉子元素样式中的 !important — B站会通过子元素 class 覆盖
* 5. 不要删除 2 秒轮询(setInterval)— MutationObserver 监听不到 body 背景色变化
* 6. 不要用两套 CSS 重新生成代替 class 切换 — 必须用 class 切换,不重建