// ==UserScript==
// @name bangumi自定义追番时间表
// @namespace http://tampermonkey.net/
// @license MIT
// @version 1.0
// @description bangumi自定义追番时间表,支持按周规划番剧观看顺序。可自主安排番剧到每周的任意日期,适合等待字幕组或喜欢按个人计划节奏来追番的用户。
// @author goldenegg
// @match http*://bgm.tv/*
// @match http*://bangumi.tv/*
// @match http*://chii.in/*
// @icon https://bgm.tv/img/favicon.ico
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js
// ==/UserScript==
(function() {
'use strict';
// 添加 CSS 变量支持
const style = document.createElement('style');
style.textContent = `
:root {
--timetable-primary: rgb(240,145,153);
--timetable-bg: white;
--timetable-text: #333;
--timetable-border: #d1d5db;
--timetable-header-bg: #eee;
--timetable-highlight: #e2e8f0;
--timetable-highlight-border: #3b82f6;
--timetable-empty: #6c757d;
--toggle-bg: #2c3e50;
--toggle-hover: #34495e;
--anime-name-bg: rgba(0, 0, 0, 0.7);
--remove-bg: rgba(108, 117, 125, 0.7);
--remove-hover: rgba(220, 53, 69, 0.9);
}
.dark-mode {
--timetable-bg: rgb(62,62,62);
--timetable-text: white;
--timetable-border: #444;
--timetable-header-bg: rgb(45,46,47);
--timetable-highlight: rgb(85,85,85);
--timetable-empty: #9ca3af;
--timetable-highlight-border: #3b82f6;
--anime-name-bg: rgba(0, 0, 0, 0.8);
}
.timetable-container {
position: fixed;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
transition: transform 0.2s ease-out;
transform-origin: bottom left;
will-change: transform;
}
.timetable-toggle {
background-color: var(--toggle-bg);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
position: fixed;
z-index: 10000;
}
.timetable-toggle:hover {
background-color: var(--toggle-hover);
transform: scale(1.1);
}
.timetable-toggle.transparent {
opacity: 0.5;
}
.timetable-window {
display: none;
background-color: var(--timetable-bg);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
width: 70vw;
max-width: 870px;
max-height: 75vh;
overflow-y: auto;
margin-bottom: 10px;
transition: all 0.3s ease;
color: var(--timetable-text);
}
.timetable-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: var(--timetable-primary);
border-bottom: 1px solid var(--timetable-border);
border-radius: 8px 8px 0 0;
cursor: move;
user-select: none;
}
.timetable-title {
font-weight: bold;
color: white;
font-size: 16px;
}
.timetable-actions {
display: flex;
gap: 10px;
}
.timetable-close, .timetable-export {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: white;
transition: color 0.2s;
}
.timetable-close:hover, .timetable-export:hover {
color: #dc3545;
}
.timetable-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.timetable-table th,
.timetable-table td {
padding: 10px;
text-align: center;
border: 1px solid var(--timetable-border);
}
.timetable-table th {
background-color: var(--timetable-header-bg);
font-weight: 500;
position: relative;
}
.timetable-table th.highlight {
background-color: var(--timetable-highlight);
font-weight: bold;
}
.timetable-table th.highlight::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: var(--timetable-highlight-border);
}
.timetable-table td {
min-height: 100px;
vertical-align: top;
background-color: var(--timetable-bg);
}
.anime-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: flex-start;
min-height: 150px;
}
.anime-item {
position: relative;
margin: 5px;
display: inline-block;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
width: 100px;
}
.anime-item:hover {
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
}
.anime-item img {
width: 100px;
height: 140px;
object-fit: cover;
display: block;
}
.anime-name {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--anime-name-bg);
color: white;
font-size: 12px;
padding: 3px 1px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: all 0.3s ease;
}
.anime-original-name {
display: none;
font-size: 10px;
color: #ccc;
margin-top: 2px;
}
.anime-item:hover .anime-name {
height: 100%;
white-space: normal;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px;
box-sizing: border-box;
}
.anime-item:hover .anime-original-name {
display: block;
text-align: center;
}
.anime-remove {
position: absolute;
top: 3px;
right: 3px;
background-color: var(--remove-bg);
color: white;
width: 18px;
height: 18px;
border-radius: 50%;
display: none;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.anime-item:hover .anime-remove {
display: flex;
}
.anime-remove:hover {
background-color: var(--remove-hover);
}
.dragging {
opacity: 0.5;
transform: none;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.drop-zone {
min-height: 20px;
transition: background-color 0.2s;
}
.drop-zone.highlight {
background-color: #e6f7ff;
}
.timetable-empty {
text-align: center;
padding: 20px;
color: var(--timetable-empty);
font-style: italic;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
min-height: 150px;
}
.drag-ghost {
position: fixed;
pointer-events: none;
z-index: 99999;
opacity: 0.8;
transform: none;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
transition: none;
}
.timetable-container.dragging {
transition: none;
cursor: grabbing;
user-select: none;
}
.export-notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 10000;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.export-notification.show {
opacity: 1;
}
`;
document.head.appendChild(style);
// 通知管理
function createNotify(msg) {
const notify = document.createElement('div');
notify.className = 'export-notification';
notify.textContent = msg;
document.body.appendChild(notify);
return notify;
}
function showNotify(msg) {
let notify = document.querySelector('.export-notification');
if (!notify) {
notify = createNotify(msg);
} else {
notify.textContent = msg;
}
notify.classList.add('show');
return notify;
}
function hideNotify(notify) {
if (notify) {
setTimeout(() => {
notify.classList.remove('show');
setTimeout(() => {
if (notify && document.body.contains(notify)) {
document.body.removeChild(notify);
}
}, 300);
}, 2000);
}
}
// 数据存储
function initStore() {
if (!localStorage.getItem('bangumiTimetable')) {
localStorage.setItem('bangumiTimetable', JSON.stringify({}));
}
if (!localStorage.getItem('timetableWindowPosition')) {
localStorage.setItem('timetableWindowPosition', JSON.stringify({ left: 20, bottom: 70 }));
}
}
function getData() {
return JSON.parse(localStorage.getItem('bangumiTimetable') || '{}');
}
function saveData(data) {
localStorage.setItem('bangumiTimetable', JSON.stringify(data));
}
function savePos(pos) {
localStorage.setItem('timetableWindowPosition', JSON.stringify(pos));
}
// 获取番剧信息
function getAnimeInfo(url) {
if (!url) return null;
const domainMatch = url.match(/^https?:\/\/([^/]+)\//);
const domain = domainMatch ? domainMatch[1] : null;
const idMatch = url.match(/\/subject\/(\d+)/);
const id = idMatch ? idMatch[1] : null;
return id ? { id, domain } : null;
}
// 深色模式检测
function isDark() {
const html = document.documentElement;
const theme = html.getAttribute('data-theme');
return theme === 'dark' || (
!theme &&
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
);
}
function updateDark() {
const win = document.querySelector('.timetable-window');
if (win) {
isDark() ? win.classList.add('dark-mode') : win.classList.remove('dark-mode');
}
}
// 创建时间表窗口
function createWin() {
const cont = document.createElement('div');
cont.className = 'timetable-container';
const pos = JSON.parse(localStorage.getItem('timetableWindowPosition') || '{"left": 20, "bottom": 70}');
cont.style.left = `${pos.left}px`;
cont.style.bottom = `${pos.bottom}px`;
const win = document.createElement('div');
win.className = 'timetable-window';
const head = document.createElement('div');
head.className = 'timetable-header';
const title = document.createElement('div');
title.className = 'timetable-title';
title.textContent = '追番时间表';
const actions = document.createElement('div');
actions.className = 'timetable-actions';
const exportBtn = document.createElement('button');
exportBtn.className = 'timetable-export';
exportBtn.innerHTML = '⎙';
exportBtn.title = '导出为图片';
exportBtn.addEventListener('click', exportImg);
const closeBtn = document.createElement('button');
closeBtn.className = 'timetable-close';
closeBtn.innerHTML = '×';
closeBtn.title = '关闭';
closeBtn.addEventListener('click', () => {
win.style.display = 'none';
localStorage.setItem('timetableWindowState', 'none');
});
actions.appendChild(exportBtn);
actions.appendChild(closeBtn);
head.appendChild(title);
head.appendChild(actions);
const table = document.createElement('table');
table.className = 'timetable-table';
const thead = document.createElement('thead');
const headRow = document.createElement('tr');
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const curDay = new Date().getDay();
days.forEach((day, i) => {
const th = document.createElement('th');
th.textContent = day;
if (i === curDay) th.classList.add('highlight');
headRow.appendChild(th);
});
thead.appendChild(headRow);
const tbody = document.createElement('tbody');
const bodyRow = document.createElement('tr');
days.forEach((_, i) => {
const td = document.createElement('td');
td.dataset.day = i;
td.className = 'drop-zone';
td.addEventListener('dragover', handleOver);
td.addEventListener('dragleave', handleLeave);
td.addEventListener('drop', handleDrop);
const animeCont = document.createElement('div');
animeCont.className = 'anime-container';
const emptyTip = document.createElement('div');
emptyTip.className = 'timetable-empty';
emptyTip.textContent = '拖拽动画条目到此处';
emptyTip.style.display = 'block';
animeCont.appendChild(emptyTip);
td.appendChild(animeCont);
bodyRow.appendChild(td);
});
tbody.appendChild(bodyRow);
table.appendChild(thead);
table.appendChild(tbody);
win.appendChild(head);
win.appendChild(table);
cont.appendChild(win);
document.body.appendChild(cont);
makeDraggable(head, cont);
loadData();
updateDark();
const winState = localStorage.getItem('timetableWindowState');
if (winState === 'block') {
win.style.display = 'block';
}
return { cont, win };
}
// 使元素可拖拽
function makeDraggable(header, cont) {
let dragging = false;
let startX, startY;
let initLeft, initBottom;
let curLeft, curBottom;
function calcPos(x, y) {
const newLeft = initLeft + (x - startX);
const newBottom = initBottom - (y - startY);
const contW = cont.offsetWidth;
const contH = cont.offsetHeight;
const minW = 100;
const minH = 50;
const maxLeft = window.innerWidth - minW;
const maxBottom = window.innerHeight - minH;
return {
left: Math.max(-contW + minW, Math.min(maxLeft, newLeft)),
bottom: Math.max(-contH + minH, Math.min(maxBottom, newBottom))
};
}
function applyPos(pos) {
cont.style.left = `${pos.left}px`;
cont.style.bottom = `${pos.bottom}px`;
curLeft = pos.left;
curBottom = pos.bottom;
}
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.timetable-close, .timetable-export')) return;
dragging = true;
cont.classList.add('dragging');
startX = e.clientX;
startY = e.clientY;
initLeft = curLeft || parseInt(cont.style.left) || 0;
initBottom = curBottom || parseInt(cont.style.bottom) || 0;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
applyPos(calcPos(e.clientX, e.clientY));
});
document.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
cont.classList.remove('dragging');
savePos({ left: curLeft, bottom: curBottom });
}
});
header.addEventListener('selectstart', (e) => {
if (dragging) e.preventDefault();
});
}
// 加载数据
function loadData() {
const data = getData();
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
days.forEach((_, i) => {
const dayData = data[i] || [];
const td = document.querySelector(`.timetable-table td[data-day="${i}"]`);
const cont = td.querySelector('.anime-container');
const emptyTip = td.querySelector('.timetable-empty');
cont.innerHTML = '';
cont.appendChild(emptyTip);
if (dayData.length > 0) {
emptyTip.style.display = 'none';
} else {
emptyTip.style.display = 'flex';
}
dayData.forEach((anime, idx) => addAnime(td, anime, idx));
});
}
// 添加番剧
function addAnime(td, anime, idx) {
const cont = td.querySelector('.anime-container');
const item = document.createElement('div');
item.className = 'anime-item';
item.draggable = true;
item.dataset.id = anime.id;
item.dataset.day = td.dataset.day;
item.dataset.index = idx;
item.dataset.domain = anime.domain || 'bangumi.tv';
const img = document.createElement('img');
img.src = anime.image;
img.alt = anime.name;
img.onerror = () => {
this.src = 'https://bgm.tv/img/no_icon_subject.png';
};
const nameCont = document.createElement('div');
nameCont.className = 'anime-name';
const name = document.createElement('div');
name.textContent = anime.name;
const origName = document.createElement('div');
origName.className = 'anime-original-name';
origName.textContent = anime.originalName || '';
nameCont.appendChild(name);
nameCont.appendChild(origName);
const remove = document.createElement('div');
remove.className = 'anime-remove';
remove.innerHTML = '×';
remove.addEventListener('click', (e) => {
e.stopPropagation();
removeAnime(item);
});
item.appendChild(img);
item.appendChild(nameCont);
item.appendChild(remove);
item.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const winState = document.querySelector('.timetable-window').style.display;
localStorage.setItem('timetableWindowState', winState);
const domain = item.dataset.domain;
window.location.href = `https://${domain}/subject/${anime.id}`;
});
item.addEventListener('dragstart', handleStart);
item.addEventListener('dragend', handleEnd);
cont.appendChild(item);
const emptyTip = td.querySelector('.timetable-empty');
if (cont.children.length > 1) {
emptyTip.style.display = 'none';
}
updateIndices(td);
}
// 更新索引
function updateIndices(td) {
const cont = td.querySelector('.anime-container');
cont.querySelectorAll('.anime-item').forEach((item, idx) => {
item.dataset.index = idx;
});
}
// 移除番剧
function removeAnime(item) {
const day = item.dataset.day;
const id = item.dataset.id;
const data = getData();
if (data[day]) {
data[day] = data[day].filter(a => a.id !== id);
saveData(data);
}
const td = item.parentElement.parentElement;
item.remove();
const cont = td.querySelector('.anime-container');
if (cont.querySelectorAll('.anime-item').length === 0) {
td.querySelector('.timetable-empty').style.display = 'flex';
}
}
// 拖拽事件处理
function handleStart(e) {
const clone = this.cloneNode(true);
clone.classList.add('drag-ghost');
document.body.appendChild(clone);
e.dataTransfer.setDragImage(clone, 50, 70);
e.dataTransfer.setData('text/plain', JSON.stringify({
id: this.dataset.id,
day: this.dataset.day,
index: this.dataset.index,
domain: this.dataset.domain
}));
this.classList.add('dragging');
setTimeout(() => clone.remove(), 0);
}
function handleEnd() {
this.classList.remove('dragging');
document.querySelectorAll('.drop-zone.highlight').forEach(el => {
el.classList.remove('highlight');
});
}
function handleOver(e) {
e.preventDefault();
this.classList.add('highlight');
}
function handleLeave() {
this.classList.remove('highlight');
}
function handleDrop(e) {
e.preventDefault();
const dropZone = e.currentTarget;
dropZone.classList.remove('highlight');
const data = e.dataTransfer.getData('text/plain');
if (!data) return;
try {
const dData = JSON.parse(data);
if (dData.id && dData.day !== undefined && dData.index !== undefined) {
const { id, day: sourceDay, index: sourceIdx, domain } = dData;
const targetDay = parseInt(dropZone.dataset.day);
const data = JSON.parse(JSON.stringify(getData()));
let anime = null;
if (data[sourceDay] && data[sourceDay].length > sourceIdx) {
anime = data[sourceDay][sourceIdx];
}
if (!anime) return;
if (data[sourceDay]) {
data[sourceDay] = data[sourceDay].filter((_, i) => i !== parseInt(sourceIdx));
}
if (!data[targetDay]) {
data[targetDay] = [];
}
const targetIdx = getDropIdx(e, dropZone);
anime.domain = domain;
data[targetDay].splice(targetIdx, 0, anime);
saveData(data);
loadData();
} else {
handleUrlDrop(data, dropZone.dataset.day);
}
} catch (error) {
handleUrlDrop(data, dropZone.dataset.day);
return;
}
}
// 处理URL拖拽
function handleUrlDrop(url, day) {
const info = getAnimeInfo(url);
if (!info){
const notify = showNotify('只能解析动画条目(含subject的链接)', true);
setTimeout(() => hideNotify(notify), 2000);
return;
}
fetchData(info.id, day, info.domain);
}
// 获取放置位置
function getDropIdx(e, td) {
const cont = td.querySelector('.anime-container');
const rect = cont.getBoundingClientRect();
const y = e.clientY - rect.top;
const itemH = 140 + 10;
return Math.min(Math.floor(y / itemH), cont.children.length);
}
// 获取番剧数据
function fetchData(id, day, domain) {
fetch(`https://api.bgm.tv/v0/subjects/${id}`)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP错误,状态码: ${res.status}`);
}
return res.json();
})
.then(data => {
const anime = {
id: id,
name: data.name_cn || data.name,
originalName: data.name,
image: data.images.common,
domain: domain || 'bangumi.tv'
};
const tData = getData();
for (const d in tData) {
const idx = tData[d].findIndex(a => a.id === id);
if (idx !== -1) {
tData[d].splice(idx, 1);
}
}
if (!tData[day]) {
tData[day] = [];
}
tData[day].push(anime);
saveData(tData);
loadData();
})
.catch(err => {
const notify = showNotify(`获取番剧数据失败`);
setTimeout(() => hideNotify(notify), 2000);
});
}
// 导出图片
function exportImg() {
const notify = showNotify('正在导出...');
const expCont = document.createElement('div');
expCont.style.cssText = `
position: fixed;
top: -9999px;
left: -9999px;
width: 870px;
background-color: var(--timetable-bg);
color: var(--timetable-text);
box-sizing: border-box;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
`;
const expTable = document.createElement('table');
expTable.style.cssText = `
width: 100%;
border-collapse: collapse;
table-layout: fixed;
`;
expCont.appendChild(expTable);
const thead = document.createElement('thead');
expTable.appendChild(thead);
const headRow = document.createElement('tr');
thead.appendChild(headRow);
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
days.forEach(day => {
const th = document.createElement('th');
th.textContent = day;
th.style.cssText = `
padding: 10px;
text-align: center;
border: 1px solid var(--timetable-border);
background-color: var(--timetable-header-bg);
font-weight: 500;
word-break: keep-all;
`;
headRow.appendChild(th);
});
const tbody = document.createElement('tbody');
expTable.appendChild(tbody);
const bodyRow = document.createElement('tr');
tbody.appendChild(bodyRow);
const data = getData();
days.forEach((_, d) => {
const td = document.createElement('td');
td.style.cssText = `
min-height: 150px;
padding: 5px;
border: 1px solid var(--timetable-border);
vertical-align: top;
background-color: var(--timetable-bg);
`;
bodyRow.appendChild(td);
const dayData = data[d] || [];
if (dayData.length === 0) {
const empty = document.createElement('div');
empty.textContent = '无';
empty.style.cssText = `
text-align: center;
padding: 20px;
color: var(--timetable-empty);
font-style: italic;
`;
td.appendChild(empty);
} else {
const animeCont = document.createElement('div');
animeCont.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
`;
td.appendChild(animeCont);
dayData.forEach(anime => {
const item = document.createElement('div');
item.style.cssText = `
width: 100px;
margin-bottom: 10px;
position: relative;
`;
animeCont.appendChild(item);
const img = document.createElement('img');
img.src = anime.image;
img.alt = anime.name_cn || anime.name;
img.style.cssText = `
width: 100px;
height: 140px;
object-fit: cover;
display: block;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
`;
img.onerror = function() {
this.src = 'https://bgm.tv/img/no_icon_subject.png';
};
item.appendChild(img);
const nameCont = document.createElement('div');
nameCont.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: rgba(0, 0, 0, 0.7);
color: white;
font-size: 12px;
padding: 3px 0;
text-align: center;
word-break: break-word;
white-space: normal;
line-height: 1.2;
border-radius: 0 0 4px 4px;
`;
nameCont.textContent = anime.name_cn || anime.name;
item.appendChild(nameCont);
});
}
});
document.body.appendChild(expCont);
html2canvas(expCont, {
scale: 1.2,
useCORS: true,
logging: false,
backgroundColor: null
}).then(canvas => {
const link = document.createElement('a');
link.download = `追番时间表.png`;
link.href = canvas.toDataURL('image/png');
link.click();
hideNotify(notify);
showNotify('导出成功');
setTimeout(() => {
document.body.removeChild(expCont);
}, 100);
}).catch(err => {
hideNotify(notify);
showNotify('导出失败');
setTimeout(() => hideNotify(notify), 2000);
document.body.removeChild(expCont);
});
}
// 添加菜单按钮
function addToggle() {
const menu = document.querySelector('#badgeUserPanel');
if (!menu) {
setTimeout(addToggle, 500);
return;
}
const wiki = menu.querySelector('a[href*="/wiki"]');
if (!wiki) {
setTimeout(addToggle, 500);
return;
}
const item = document.createElement('li');
const link = document.createElement('a');
link.href = '#';
link.textContent = '追番时间表';
link.title = '显示/隐藏追番时间表';
link.addEventListener('click', (e) => {
e.preventDefault();
toggleWin();
});
item.appendChild(link);
wiki.parentElement.after(item);
}
// 切换窗口显示状态
function toggleWin() {
const win = document.querySelector('.timetable-window');
if (win) {
const display = win.style.display === 'none' ? 'block' : 'none';
win.style.display = display;
localStorage.setItem('timetableWindowState', display);
if (display === 'block') {
loadData();
updateDark();
}
}
}
//初始化
function init() {
initStore();
createWin();
addToggle();
document.addEventListener('dragover', (e) => {
if (e.dataTransfer.types.includes('text/uri-list') ||
e.dataTransfer.types.includes('text/plain')) {
e.preventDefault();
}
});
document.addEventListener('drop', (e) => {
if (!e.target.closest('.drop-zone')) {
e.preventDefault();
}
});
const html = document.documentElement;
new MutationObserver(updateDark).observe(html, {
attributes: true,
attributeFilter: ['data-theme']
});
const winState = localStorage.getItem('timetableWindowState') || 'none';
const win = document.querySelector('.timetable-window');
if (win) {
win.style.display = winState;
updateDark();
}
}
// 启动脚本
init();
// 启用拖拽链接功能
document.addEventListener('dragstart', (e) => {
if (e.target.tagName === 'A' && e.target.href.includes('/subject/')) {
e.dataTransfer.setData('text/plain', e.target.href);
}
});
})();