// ==UserScript==
// @name OpenMindClub 标题导航器
// @namespace http://tampermonkey.net/
// @version 0.0.4
// @description 为 OpenMindClub 网页提供悬浮标题导航功能,支持自动主题切换
// @author awyugan
// @match https://m.openmindclub.com/stu/*/homework*
// @match https://m.openmindclub.com/stu/*/discussion
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// 配置
const CONFIG = {
storageKey: "omc_heading_navigator_collapsed",
refreshInterval: 1500, // 2秒检查一次DOM变化
maxRetries: 30, // 最多重试30次(1分钟)
};
let floatingPanel = null;
let retryCount = 0;
let lastHeadingCount = 0;
let currentTheme = "dark";
// 获取本地存储的折叠状态
function getCollapsedState() {
return GM_getValue(CONFIG.storageKey, false);
}
// 保存折叠状态到本地存储
function saveCollapsedState(collapsed) {
GM_setValue(CONFIG.storageKey, collapsed);
}
// 检测页面主题
function detectTheme() {
// 检测系统暗色模式偏好
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
// 检测页面背景色
const bodyBg = window.getComputedStyle(document.body).backgroundColor;
const htmlBg = window.getComputedStyle(document.documentElement).backgroundColor;
// 检测页面是否有暗色类名
const hasDarkClass =
document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark") ||
document.documentElement.classList.contains("theme-dark") ||
document.body.classList.contains("theme-dark");
// 综合判断
if (hasDarkClass) return "dark";
// 检查背景色亮度
const getBrightness = (color) => {
if (color === "rgba(0, 0, 0, 0)" || color === "transparent") return null;
const rgb = color.match(/\d+/g);
if (!rgb) return null;
return (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;
};
const bodyBrightness = getBrightness(bodyBg);
const htmlBrightness = getBrightness(htmlBg);
// 如果能检测到背景色且较暗,使用深色主题
if (bodyBrightness !== null && bodyBrightness < 128) return "dark";
if (htmlBrightness !== null && htmlBrightness < 128) return "dark";
// 如果检测不到明确的背景色,使用系统偏好
if (bodyBrightness === null && htmlBrightness === null && prefersDark) return "dark";
return "light";
}
// 获取主题样式
function getThemeStyles(theme) {
if (theme === "dark") {
return {
background: "#2d2d2d",
border: "#404040",
headerBg: "#3a3a3a",
headerHover: "#434343",
textColor: "#e8e8e8",
itemColor: "#d0d0d0",
itemHover: "rgba(255, 255, 255, 0.05)",
toggleColor: "#b8b8b8",
arrowColor: "#888",
emptyColor: "#888",
scrollTrack: "#333",
scrollThumb: "#555",
scrollThumbHover: "#777",
};
} else {
return {
background: "#ffffff",
border: "#e0e0e0",
headerBg: "#f8f9fa",
headerHover: "#f0f1f2",
textColor: "#333333",
itemColor: "#555555",
itemHover: "rgba(0, 0, 0, 0.05)",
toggleColor: "#666666",
arrowColor: "#999999",
emptyColor: "#6c757d",
scrollTrack: "#f1f1f1",
scrollThumb: "#c1c1c1",
scrollThumbHover: "#a8a8a8",
};
}
}
// 创建悬浮窗样式
function createStyles(theme = "dark") {
const colors = getThemeStyles(theme);
const style = document.createElement("style");
style.id = "omc-navigator-styles";
style.textContent = `
#omc-heading-navigator {
position: fixed;
top: 20px;
right: 20px;
width: 280px;
max-height: 80vh;
background: ${colors.background};
border: 1px solid ${colors.border};
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, ${theme === "dark" ? "0.3" : "0.15"});
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
overflow: hidden;
transition: all 0.3s ease;
color: ${colors.textColor};
}
#omc-heading-navigator.collapsed {
height: 36px !important;
}
.omc-nav-header {
background: ${colors.headerBg};
padding: 8px 12px;
border-bottom: 1px solid ${colors.border};
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
}
.omc-nav-header:hover {
background: ${colors.headerHover};
}
.omc-nav-title {
font-weight: 500;
color: ${colors.textColor};
margin: 0;
font-size: 13px;
}
.omc-nav-toggle {
background: none;
border: none;
font-size: 12px;
cursor: pointer;
color: ${colors.toggleColor};
padding: 2px;
transition: transform 0.3s ease;
}
.omc-nav-toggle.collapsed {
transform: rotate(-90deg);
}
.omc-nav-content {
max-height: calc(80vh - 50px);
overflow-y: auto;
padding: 6px 0;
background: ${colors.background};
}
.omc-nav-content.collapsed {
display: none;
}
.omc-heading-item {
padding: 6px 12px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
position: relative;
color: ${colors.itemColor};
line-height: 1.3;
}
.omc-heading-item:hover {
background: ${colors.itemHover};
color: ${colors.textColor};
}
.omc-heading-item.has-children {
cursor: pointer;
}
.omc-heading-toggle {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 4px;
color: ${colors.arrowColor};
font-size: 10px;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.omc-heading-toggle.expanded {
transform: rotate(90deg);
}
.omc-heading-toggle:hover {
color: ${colors.textColor};
}
.omc-heading-text {
flex: 1;
color: inherit;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 层级缩进样式 */
.omc-heading-item[data-level="1"] { padding-left: 12px; }
.omc-heading-item[data-level="2"] { padding-left: 28px; }
.omc-heading-item[data-level="3"] { padding-left: 44px; }
.omc-heading-item[data-level="4"] { padding-left: 60px; }
.omc-heading-item[data-level="5"] { padding-left: 76px; }
.omc-heading-item[data-level="6"] { padding-left: 92px; }
/* 折叠状态 */
.omc-heading-children.collapsed {
display: none;
}
.omc-nav-empty {
padding: 20px 12px;
text-align: center;
color: ${colors.emptyColor};
font-style: italic;
font-size: 12px;
}
.omc-nav-loading {
padding: 15px 12px;
text-align: center;
color: ${colors.emptyColor};
font-size: 12px;
}
/* 滚动条样式 */
.omc-nav-content::-webkit-scrollbar {
width: 4px;
}
.omc-nav-content::-webkit-scrollbar-track {
background: ${colors.scrollTrack};
}
.omc-nav-content::-webkit-scrollbar-thumb {
background: ${colors.scrollThumb};
border-radius: 2px;
}
.omc-nav-content::-webkit-scrollbar-thumb:hover {
background: ${colors.scrollThumbHover};
}
`;
// 移除旧样式
const oldStyle = document.getElementById("omc-navigator-styles");
if (oldStyle) {
oldStyle.remove();
}
document.head.appendChild(style);
}
// 更新主题
function updateTheme() {
const newTheme = detectTheme();
if (newTheme !== currentTheme) {
currentTheme = newTheme;
createStyles(currentTheme);
console.log(`OpenMindClub 标题导航器切换到${currentTheme === "dark" ? "深色" : "浅色"}主题`);
}
}
// 创建悬浮窗HTML结构
function createFloatingPanel() {
const panel = document.createElement("div");
panel.id = "omc-heading-navigator";
const collapsed = getCollapsedState();
if (collapsed) {
panel.classList.add("collapsed");
}
panel.innerHTML = `
<div class="omc-nav-header">
<h3 class="omc-nav-title">大纲</h3>
<button class="omc-nav-toggle ${collapsed ? "collapsed" : ""}">▼</button>
</div>
<div class="omc-nav-content ${collapsed ? "collapsed" : ""}">
<div class="omc-nav-loading">正在加载标题...</div>
</div>
`;
// 添加点击事件
const header = panel.querySelector(".omc-nav-header");
const content = panel.querySelector(".omc-nav-content");
const toggle = panel.querySelector(".omc-nav-toggle");
header.addEventListener("click", function () {
const isCollapsed = panel.classList.toggle("collapsed");
content.classList.toggle("collapsed", isCollapsed);
toggle.classList.toggle("collapsed", isCollapsed);
saveCollapsedState(isCollapsed);
});
document.body.appendChild(panel);
return panel;
}
// 获取页面中的所有标题
function getHeadings() {
const headings = [];
const headingElements = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
headingElements.forEach((heading, index) => {
// 排除我们自己的导航器中的标题
if (heading.closest("#omc-heading-navigator")) {
return;
}
const text = heading.textContent.trim();
if (text) {
const level = parseInt(heading.tagName.charAt(1));
headings.push({
element: heading,
level: level,
tagName: heading.tagName.toLowerCase(),
text: text,
id: heading.id || `heading-${index}`,
children: [],
collapsed: GM_getValue(`collapsed_${heading.id || `heading-${index}`}`, false),
});
}
});
return buildHeadingTree(headings);
}
// 构建标题树结构
function buildHeadingTree(headings) {
const tree = [];
const stack = [];
headings.forEach((heading) => {
// 找到合适的父级
while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
stack.pop();
}
if (stack.length === 0) {
tree.push(heading);
} else {
stack[stack.length - 1].children.push(heading);
}
stack.push(heading);
});
return tree;
}
// 渲染标题树
function renderHeadingTree(headings, level = 1) {
let html = "";
headings.forEach((heading) => {
const hasChildren = heading.children && heading.children.length > 0;
const isCollapsed = heading.collapsed;
html += `
<div class="omc-heading-item ${hasChildren ? "has-children" : ""}"
data-target="${heading.id}"
data-level="${level}">
${
hasChildren
? `<span class="omc-heading-toggle ${!isCollapsed ? "expanded" : ""}" data-heading-id="${heading.id}">▶</span>`
: '<span class="omc-heading-toggle"></span>'
}
<span class="omc-heading-text">${heading.text}</span>
</div>
`;
if (hasChildren) {
html += `<div class="omc-heading-children ${isCollapsed ? "collapsed" : ""}" data-parent="${heading.id}">`;
html += renderHeadingTree(heading.children, level + 1);
html += "</div>";
}
});
return html;
}
// 更新悬浮窗内容
function updateFloatingPanel() {
if (!floatingPanel) return;
const headingTree = getHeadings();
const content = floatingPanel.querySelector(".omc-nav-content");
if (headingTree.length === 0) {
content.innerHTML = '<div class="omc-nav-empty">暂未发现页面标题</div>';
return false;
}
// 计算总标题数
function countHeadings(tree) {
let count = 0;
tree.forEach((heading) => {
count++;
if (heading.children) {
count += countHeadings(heading.children);
}
});
return count;
}
lastHeadingCount = countHeadings(headingTree);
const html = renderHeadingTree(headingTree);
content.innerHTML = html;
// 添加事件监听
addEventListeners(content, headingTree);
return true;
}
// 添加事件监听器
function addEventListeners(content, headingTree) {
// 点击切换折叠状态
content.querySelectorAll(".omc-heading-toggle").forEach((toggle) => {
toggle.addEventListener("click", function (e) {
e.stopPropagation();
const headingId = this.getAttribute("data-heading-id");
if (!headingId) return;
const childrenContainer = content.querySelector(`[data-parent="${headingId}"]`);
const isCollapsed = childrenContainer.classList.contains("collapsed");
childrenContainer.classList.toggle("collapsed", !isCollapsed);
this.classList.toggle("expanded", isCollapsed);
// 保存折叠状态
GM_setValue(`collapsed_${headingId}`, !isCollapsed);
});
});
// 点击标题跳转
content.querySelectorAll(".omc-heading-text").forEach((textElement) => {
textElement.addEventListener("click", function () {
const item = this.closest(".omc-heading-item");
const targetId = item.getAttribute("data-target");
// 查找目标元素
function findHeadingById(tree, id) {
for (const heading of tree) {
if (heading.id === id) return heading;
if (heading.children) {
const found = findHeadingById(heading.children, id);
if (found) return found;
}
}
return null;
}
const targetHeading = findHeadingById(headingTree, targetId);
if (targetHeading && targetHeading.element) {
targetHeading.element.scrollIntoView({
behavior: "smooth",
block: "start",
});
// 高亮效果
targetHeading.element.style.transition = "background-color 0.3s ease";
targetHeading.element.style.backgroundColor = "#fff3cd";
setTimeout(() => {
targetHeading.element.style.backgroundColor = "";
}, 2000);
}
});
});
}
// 检查页面内容变化
function checkForChanges() {
const currentHeadingCount = document.querySelectorAll("h1, h2, h3, h4, h5, h6").length;
if (currentHeadingCount !== lastHeadingCount) {
updateFloatingPanel();
}
// 如果还没有找到标题且重试次数未超限,继续检查
if (currentHeadingCount === 0 && retryCount < CONFIG.maxRetries) {
retryCount++;
setTimeout(checkForChanges, CONFIG.refreshInterval);
} else if (currentHeadingCount > 0) {
retryCount = 0; // 重置重试计数
// 找到标题后,定期检查变化
setTimeout(checkForChanges, CONFIG.refreshInterval);
}
}
// 初始化脚本
function init() {
// 检测并设置初始主题
currentTheme = detectTheme();
createStyles(currentTheme);
// 创建悬浮窗
floatingPanel = createFloatingPanel();
// 初始更新
updateFloatingPanel();
// 开始检查变化
setTimeout(checkForChanges, 1000);
// 监听主题变化
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addListener(updateTheme);
// 监听页面类名变化
const observer = new MutationObserver(() => {
setTimeout(updateTheme, 100); // 延迟检测,确保样式已应用
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ["class"],
});
console.log(`OpenMindClub 标题导航器已启动 (${currentTheme === "dark" ? "深色" : "浅色"}主题)`);
}
// 等待页面完全加载后初始化
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();