// ==UserScript==
// @name 重庆高教在线播放器
// @namespace https://www.cqooc.com/
// @version 2.1.0
// @description 重庆高教在线课程播放控制:视频播放/暂停、倍速、进度、课件目录、本地计时与学习记录。
// @match https://www.cqooc.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
'use strict';
const VERSION = '2.1.0';
const STORE_PREFIX = 'cqooc-study-helper-v2:';
const PANEL_ID = 'cqooc-player';
const COURSE_PATHS = ['/course/detail/courseStudy', '/learn/mooc/structure'];
const state = {
rows: [],
activeRow: null,
video: null,
studying: false,
collapsed: false,
listOpen: false,
elapsedMs: 0,
lastTick: 0,
timerId: null,
observer: null,
refreshQueued: false,
courseKey: '',
record: null,
speed: 1,
lastPath: location.pathname,
seeking: false
};
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));
const isCoursePage = () => COURSE_PATHS.includes(location.pathname);
function getCourseKey() {
const courseId = new URL(location.href).searchParams.get('id') || 'unknown-course';
return `${STORE_PREFIX}${courseId}`;
}
function loadRecord() {
state.courseKey = getCourseKey();
const saved = GM_getValue(state.courseKey, null);
state.record = saved && typeof saved === 'object'
? saved
: { elapsedMs: 0, reviewed: {}, lastItem: '', updatedAt: 0 };
state.record.reviewed ||= {};
state.elapsedMs = Number(state.record.elapsedMs) || 0;
}
function saveRecord() {
if (!state.record) return;
state.record.elapsedMs = state.elapsedMs;
state.record.updatedAt = Date.now();
GM_setValue(state.courseKey, state.record);
}
function formatDuration(value) {
const total = Math.floor(Math.max(0, Number(value) || 0));
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
const seconds = total % 60;
return hours > 0
? `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
: `${minutes}:${String(seconds).padStart(2, '0')}`;
}
function formatClock(seconds) {
return formatDuration(seconds);
}
function getRows() {
return $$('.courseware-row').filter(row => row.querySelector('.courseware-name'));
}
function getRowName(row) {
return row?.querySelector('.courseware-name')?.textContent?.trim() || '未命名课件';
}
function getRowType(row) {
return row?.querySelector('.file-icon')?.textContent?.trim() || '课件';
}
function getRowKey(row) {
const index = state.rows.indexOf(row);
return `${index}:${getRowName(row)}`;
}
function getActiveRow() {
return state.rows.find(row => row.classList.contains('active')) || null;
}
function getVideoElement() {
return $('.dplayer video') || $('video');
}
function setNotice(message) {
const el = $('#ch-notice');
if (el) el.textContent = message;
}
function ensurePanel() {
if ($('#' + PANEL_ID)) return;
GM_addStyle(`
#${PANEL_ID} {
position: fixed;
top: 86px;
right: 22px;
width: min(980px, calc(100vw - 32px));
z-index: 2147483000;
color: #51483d;
font-family: Georgia, "Noto Serif SC", "Songti SC", "SimSun", serif;
background:
radial-gradient(ellipse at 96% 8%, rgba(194,112,103,.13), transparent 25%),
radial-gradient(ellipse at 4% 100%, rgba(111,137,148,.10), transparent 23%),
repeating-linear-gradient(0deg, rgba(120,93,65,.018) 0, rgba(120,93,65,.018) 1px, transparent 1px, transparent 4px),
linear-gradient(112deg, #f7eddd 0%, #f4e8d7 48%, #f8efdf 100%);
border: 1px solid rgba(139,111,82,.24);
border-radius: 9px 13px 10px 12px;
box-shadow: 0 15px 38px rgba(72,55,39,.22), inset 0 0 0 5px rgba(255,255,255,.20);
overflow: hidden;
box-sizing: border-box;
}
#${PANEL_ID} * { box-sizing: border-box; }
#${PANEL_ID}::before {
content: "";
position: absolute;
pointer-events: none;
inset: 8px;
border: 1px solid rgba(157,126,94,.13);
border-radius: 6px 10px 7px 9px;
}
#${PANEL_ID} .ch-top {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 22px 5px;
cursor: move;
user-select: none;
}
#${PANEL_ID} .ch-brand {
color: #8a7d6d;
font-size: 12px;
font-style: italic;
letter-spacing: 2.2px;
}
#${PANEL_ID} .ch-top-actions { display:flex; align-items:center; gap:12px; }
#${PANEL_ID} .ch-icon {
border: 0;
background: transparent;
color: #62584c;
font: 22px/1 Georgia, serif;
cursor: pointer;
padding: 3px 6px;
}
#${PANEL_ID} .ch-main { position: relative; padding: 6px 30px 22px; }
#${PANEL_ID} .ch-title-row {
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
margin: 0 0 16px;
}
#${PANEL_ID} .ch-title-wrap { display:flex; align-items:center; gap:15px; min-width:0; }
#${PANEL_ID} .ch-title-mark {
width:7px; height:50px; flex:0 0 7px;
background:#bd756c; border-radius:2px;
box-shadow: 1px 1px 0 rgba(255,255,255,.45);
}
#${PANEL_ID} .ch-title {
color:#4d453b;
font-size: clamp(21px, 2.5vw, 32px);
font-weight:600;
line-height:1.25;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
#${PANEL_ID} .ch-subtitle { color:#958777; font: italic 11px Georgia,serif; letter-spacing:1.5px; white-space:nowrap; }
#${PANEL_ID} .ch-compass {
position:relative; flex:0 0 88px; height:72px;
display:flex; align-items:center; justify-content:center;
color:rgba(105,98,85,.58); font:11px Georgia,serif;
}
#${PANEL_ID} .ch-compass .rose { font-size:45px; line-height:1; color:rgba(105,98,85,.45); }
#${PANEL_ID} .ch-compass .north { position:absolute; top:0; }
#${PANEL_ID} .ch-compass .south { position:absolute; bottom:0; }
#${PANEL_ID} .ch-compass .west { position:absolute; left:0; }
#${PANEL_ID} .ch-compass .east { position:absolute; right:0; }
#${PANEL_ID} .ch-progress-row {
display:grid;
grid-template-columns: 74px minmax(80px,1fr) 74px;
align-items:center;
gap:14px;
margin: 0 4px 20px;
color:#5a5147;
font: 21px Georgia,serif;
font-variant-numeric:tabular-nums;
}
#${PANEL_ID} .ch-time { white-space:nowrap; }
#${PANEL_ID} input[type="range"].ch-seek {
appearance:none; width:100%; height:9px; border-radius:99px;
background:linear-gradient(90deg,#b96f67 0%,#b96f67 var(--progress,0%),#d9cbb8 var(--progress,0%),#d9cbb8 100%);
outline:none; cursor:pointer;
}
#${PANEL_ID} input[type="range"].ch-seek::-webkit-slider-thumb {
appearance:none; width:19px; height:19px; border-radius:50%;
background:#fbf3e7; border:3px solid #b96f67;
box-shadow:0 1px 2px rgba(60,45,30,.18);
}
#${PANEL_ID} input[type="range"].ch-seek::-moz-range-thumb {
width:14px; height:14px; border-radius:50%; background:#fbf3e7;
border:3px solid #b96f67;
}
#${PANEL_ID} .ch-controls {
display:flex; align-items:center; justify-content:center;
gap:14px; flex-wrap:wrap;
}
#${PANEL_ID} .ch-control-btn {
display:inline-flex; align-items:center; justify-content:center;
min-width:58px; height:56px; padding:0 15px;
border:0; border-radius:7px;
background:#7d9299; color:#f8f1e6;
font: 24px/1 Arial,sans-serif;
cursor:pointer;
box-shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 2px 3px rgba(70,57,44,.10);
transition: transform .15s, filter .15s;
}
#${PANEL_ID} .ch-control-btn:hover { filter:brightness(1.06); transform:translateY(-1px); }
#${PANEL_ID} .ch-control-btn:active { transform:translateY(1px); }
#${PANEL_ID} .ch-control-btn.primary {
width:82px; height:82px; border-radius:50%;
background:#b96f67; font-size:33px;
}
#${PANEL_ID} .ch-control-btn.stop { font-size:22px; }
#${PANEL_ID} .ch-divider { width:1px; height:62px; background:rgba(125,105,82,.32); margin:0 5px; }
#${PANEL_ID} .ch-speed-wrap { display:flex; align-items:center; gap:10px; color:#655b4e; font-size:18px; }
#${PANEL_ID} select.ch-speed {
appearance:none; min-width:112px; padding:12px 34px 12px 17px;
border:1px solid rgba(151,122,91,.25); border-radius:6px;
color:#554b40; background:linear-gradient(145deg,#f5e8d5,#e9dac5);
font:18px Georgia,serif; cursor:pointer;
background-image:linear-gradient(45deg,transparent 50%,#75695a 50%),linear-gradient(135deg,#75695a 50%,transparent 50%),linear-gradient(145deg,#f5e8d5,#e9dac5);
background-position:calc(100% - 17px) 50%,calc(100% - 12px) 50%,0 0;
background-size:5px 5px,5px 5px,100% 100%; background-repeat:no-repeat;
}
#${PANEL_ID} .ch-list-btn {
display:inline-flex; align-items:center; gap:11px;
border:0; border-radius:6px; padding:15px 22px;
color:#f7f0e5; background:#7d9299;
font:18px "Noto Serif SC","Songti SC",serif; cursor:pointer;
box-shadow:inset 0 1px 0 rgba(255,255,255,.24),0 2px 4px rgba(70,57,44,.12);
}
#${PANEL_ID} .ch-list-btn:hover { filter:brightness(1.06); }
#${PANEL_ID} .ch-list-glyph { font:22px Arial,sans-serif; }
#${PANEL_ID} .ch-utility {
display:flex; align-items:center; justify-content:space-between;
gap:12px; margin-top:17px; padding-top:12px;
border-top:1px solid rgba(139,111,82,.18);
color:#8d806f; font-size:12px;
}
#${PANEL_ID} .ch-utility-left { display:flex; align-items:center; gap:14px; min-width:0; }
#${PANEL_ID} .ch-run-btn, #${PANEL_ID} .ch-small-btn {
border:1px solid rgba(145,115,83,.22); border-radius:5px;
padding:7px 13px; color:#66594a; background:rgba(255,250,240,.55);
font:13px "Noto Serif SC","Songti SC",serif; cursor:pointer;
}
#${PANEL_ID} .ch-run-btn.running { color:#fff8ef; background:#b96f67; border-color:#b96f67; }
#${PANEL_ID} .ch-notice { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#${PANEL_ID} .ch-list-panel {
position:relative; display:none; margin:14px 2px 0; padding:12px 14px;
border-top:1px dashed rgba(145,115,83,.35);
background:rgba(255,250,241,.35); max-height:220px; overflow:auto;
}
#${PANEL_ID} .ch-list-panel.open { display:block; }
#${PANEL_ID} .ch-row {
display:flex; align-items:center; justify-content:space-between; gap:12px;
width:100%; padding:9px 10px; border:0; border-bottom:1px solid rgba(145,115,83,.13);
color:#5c5145; background:transparent; text-align:left;
font:14px "Noto Serif SC","Songti SC",serif; cursor:pointer;
}
#${PANEL_ID} .ch-row:hover { background:rgba(185,111,103,.10); }
#${PANEL_ID} .ch-row.active { color:#fff8ef; background:#b96f67; }
#${PANEL_ID} .ch-row small { opacity:.75; white-space:nowrap; }
#${PANEL_ID} .ch-empty { color:#8d806f; padding:10px; font-size:13px; }
#${PANEL_ID}.collapsed { width:270px; }
#${PANEL_ID}.collapsed .ch-main { display:none; }
@media (max-width:680px) {
#${PANEL_ID} { top:12px; right:8px; width:calc(100vw - 16px); }
#${PANEL_ID} .ch-main { padding:5px 16px 16px; }
#${PANEL_ID} .ch-compass { display:none; }
#${PANEL_ID} .ch-title { font-size:21px; }
#${PANEL_ID} .ch-controls { gap:8px; }
#${PANEL_ID} .ch-control-btn { min-width:46px; height:46px; padding:0 10px; }
#${PANEL_ID} .ch-control-btn.primary { width:66px; height:66px; }
#${PANEL_ID} .ch-divider { display:none; }
#${PANEL_ID} .ch-speed-wrap { font-size:14px; }
#${PANEL_ID} .ch-list-btn { padding:12px 14px; font-size:15px; }
#${PANEL_ID} .ch-progress-row { grid-template-columns:55px minmax(50px,1fr) 55px; gap:8px; font-size:16px; }
}
`);
const panel = document.createElement('section');
panel.id = PANEL_ID;
panel.innerHTML = `
COURSE PLAYER · STUDY NOTES
00:00
00:00
累计 0:00
就绪
`;
document.body.appendChild(panel);
$('#ch-collapse', panel).addEventListener('click', () => {
state.collapsed = !state.collapsed;
panel.classList.toggle('collapsed', state.collapsed);
$('#ch-collapse', panel).textContent = state.collapsed ? '+' : '−';
});
$('#ch-close', panel).addEventListener('click', () => { panel.style.display = 'none'; });
$('#ch-video-toggle', panel).addEventListener('click', toggleVideoPlayback);
$('#ch-prev', panel).addEventListener('click', () => navigateRow(-1));
$('#ch-next', panel).addEventListener('click', () => navigateRow(1));
$('#ch-speed', panel).addEventListener('change', event => setPlaybackSpeed(Number(event.target.value)));
$('#ch-run-toggle', panel).addEventListener('click', toggleStudyTimer);
$('#ch-timer-stop', panel).addEventListener('click', stopStudyTimer);
$('#ch-refresh', panel).addEventListener('click', () => { refreshPageData(); renderCourseList(); setNotice('目录已刷新'); });
$('#ch-mark', panel).addEventListener('click', markCurrentReviewed);
$('#ch-list-toggle', panel).addEventListener('click', toggleCourseList);
$('#ch-seek', panel).addEventListener('input', onSeekInput);
$('#ch-seek', panel).addEventListener('change', onSeekChange);
makeDraggable(panel, $('#ch-drag', panel));
}
function makeDraggable(panel, handle) {
let dragging = false;
let startX = 0, startY = 0, startLeft = 0, startTop = 0;
handle.addEventListener('pointerdown', event => {
if (event.target.closest('button')) return;
const rect = panel.getBoundingClientRect();
dragging = true; startX = event.clientX; startY = event.clientY;
startLeft = rect.left; startTop = rect.top;
handle.setPointerCapture?.(event.pointerId);
});
handle.addEventListener('pointermove', event => {
if (!dragging) return;
const left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + event.clientX - startX));
const top = Math.max(0, Math.min(window.innerHeight - 48, startTop + event.clientY - startY));
panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.right = 'auto';
});
handle.addEventListener('pointerup', () => { dragging = false; });
handle.addEventListener('pointercancel', () => { dragging = false; });
}
function toggleVideoPlayback() {
const video = state.video || getVideoElement();
if (!video) { setNotice('当前页面未检测到视频'); return; }
if (video.paused || video.ended) {
video.play().then(() => setNotice('视频播放中')).catch(() => setNotice('播放未成功,请先点击页面播放器'));
} else {
video.pause();
setNotice('视频已暂停');
}
renderVideoState();
}
function setPlaybackSpeed(speed) {
if (!Number.isFinite(speed) || speed <= 0) return;
state.speed = speed;
const video = state.video || getVideoElement();
if (video) video.playbackRate = speed;
const select = $('#ch-speed');
if (select) select.value = String(speed);
setNotice(`播放速度 ${speed}×`);
}
function navigateRow(offset) {
if (!state.rows.length) { setNotice('尚未读取到课件目录'); return; }
let index = state.rows.indexOf(state.activeRow);
if (index < 0) index = offset > 0 ? -1 : state.rows.length;
const next = Math.max(0, Math.min(state.rows.length - 1, index + offset));
const row = state.rows[next];
if (!row) return;
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
row.click();
setNotice(`已选择:${getRowName(row)}`);
setTimeout(refreshPageData, 700);
setTimeout(refreshPageData, 1600);
}
function toggleCourseList() {
state.listOpen = !state.listOpen;
$('#ch-list-panel')?.classList.toggle('open', state.listOpen);
if (state.listOpen) renderCourseList();
}
function renderCourseList() {
const panel = $('#ch-list-panel');
if (!panel) return;
if (!state.rows.length) {
panel.innerHTML = '暂未识别到课件目录
';
return;
}
panel.replaceChildren();
state.rows.forEach((row, index) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `ch-row${row === state.activeRow ? ' active' : ''}`;
const name = document.createElement('span');
name.textContent = `${index + 1}. ${getRowName(row)}`;
const type = document.createElement('small');
type.textContent = getRowType(row);
button.append(name, type);
button.addEventListener('click', () => {
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
row.click();
setNotice(`已选择:${getRowName(row)}`);
setTimeout(refreshPageData, 700);
setTimeout(refreshPageData, 1600);
});
panel.appendChild(button);
});
}
function markCurrentReviewed() {
if (!state.activeRow) { setNotice('尚未识别当前课件'); return; }
const key = getRowKey(state.activeRow);
state.record.reviewed[key] = { name: getRowName(state.activeRow), markedAt: Date.now(), method: 'manual' };
state.record.lastItem = getRowName(state.activeRow);
saveRecord();
setNotice('本节已加入本地记录');
renderCourseList();
}
function toggleStudyTimer() {
if (state.studying) { pauseStudyTimer(); return; }
state.studying = true;
state.lastTick = Date.now();
if (state.timerId) clearInterval(state.timerId);
state.timerId = setInterval(tickStudyTimer, 1000);
renderTimerState();
setNotice('学习计时已运行');
}
function pauseStudyTimer() {
state.studying = false;
state.lastTick = 0;
if (state.timerId) clearInterval(state.timerId);
state.timerId = null;
saveRecord();
renderTimerState();
setNotice('学习计时已暂停');
}
function stopStudyTimer() {
pauseStudyTimer();
setNotice('计时已停止,累计时间已保存');
}
function tickStudyTimer() {
if (!state.studying) return;
const now = Date.now();
const visible = document.visibilityState === 'visible';
if (visible && isCoursePage() && state.lastTick) {
state.elapsedMs += Math.max(0, Math.min(now - state.lastTick, 2000));
}
state.lastTick = now;
if (Math.floor(now / 10000) !== Math.floor((now - 1000) / 10000)) saveRecord();
renderTimerState();
}
function renderTimerState() {
const button = $('#ch-run-toggle');
if (button) {
button.textContent = state.studying ? '暂停计时' : '运行计时';
button.classList.toggle('running', state.studying);
}
const total = $('#ch-time-total');
if (total) total.textContent = `累计 ${formatDuration(state.elapsedMs / 1000)}`;
}
function attachVideoListeners(video) {
if (video === state.video) return;
if (state.video) {
state.video.removeEventListener('timeupdate', onVideoUpdate);
state.video.removeEventListener('durationchange', onVideoUpdate);
state.video.removeEventListener('loadedmetadata', onVideoUpdate);
state.video.removeEventListener('ended', onVideoEnded);
state.video.removeEventListener('play', onVideoUpdate);
state.video.removeEventListener('pause', onVideoUpdate);
}
state.video = video || null;
if (state.video) {
state.video.playbackRate = state.speed;
state.video.addEventListener('timeupdate', onVideoUpdate);
state.video.addEventListener('durationchange', onVideoUpdate);
state.video.addEventListener('loadedmetadata', onVideoUpdate);
state.video.addEventListener('ended', onVideoEnded);
state.video.addEventListener('play', onVideoUpdate);
state.video.addEventListener('pause', onVideoUpdate);
}
renderVideoState();
}
function onVideoUpdate() { renderVideoState(); }
function onVideoEnded() {
if (!state.activeRow) return;
const key = getRowKey(state.activeRow);
state.record.reviewed[key] = { name: getRowName(state.activeRow), markedAt: Date.now(), method: 'video-ended' };
state.record.lastItem = getRowName(state.activeRow);
saveRecord();
setNotice('视频自然播放结束,本地记录已更新');
renderCourseList();
}
function onSeekInput(event) {
const video = state.video;
if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return;
state.seeking = true;
const percent = Number(event.target.value) / video.duration * 100;
event.target.style.setProperty('--progress', `${percent}%`);
$('#ch-current-time').textContent = formatClock(Number(event.target.value));
}
function onSeekChange(event) {
const video = state.video;
if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return;
video.currentTime = Number(event.target.value);
state.seeking = false;
renderVideoState();
}
function renderVideoState() {
const video = state.video;
const currentEl = $('#ch-current-time');
const durationEl = $('#ch-duration');
const seek = $('#ch-seek');
const toggle = $('#ch-video-toggle');
if (!currentEl || !durationEl || !seek || !toggle) return;
if (!video) {
currentEl.textContent = '00:00'; durationEl.textContent = '00:00';
seek.max = '100'; seek.value = '0'; seek.style.setProperty('--progress', '0%');
toggle.textContent = '▶'; toggle.title = '播放';
return;
}
const duration = Number.isFinite(video.duration) ? video.duration : 0;
const current = Number.isFinite(video.currentTime) ? video.currentTime : 0;
currentEl.textContent = formatClock(current);
durationEl.textContent = formatClock(duration);
seek.max = String(duration || 100);
if (!state.seeking) seek.value = String(current);
const percent = duration > 0 ? Math.min(100, current / duration * 100) : 0;
seek.style.setProperty('--progress', `${percent}%`);
toggle.textContent = video.paused || video.ended ? '▶' : 'Ⅱ';
toggle.title = video.paused || video.ended ? '播放' : '暂停';
}
function renderPanel() {
const panel = $('#' + PANEL_ID);
if (!panel) return;
const active = state.activeRow;
$('#ch-current-name', panel).textContent = active ? getRowName(active) : '等待识别当前课件';
$('#ch-current-type', panel).textContent = active ? `${getRowType(active)} · COURSE MATERIAL` : 'COURSE MATERIAL';
$('#ch-mark', panel).textContent = active && state.record.reviewed[getRowKey(active)] ? '已记录本节' : '记录本节';
$('#ch-mark', panel).disabled = !active;
renderTimerState();
renderVideoState();
if (state.listOpen) renderCourseList();
}
function refreshPageData() {
state.rows = getRows();
state.activeRow = getActiveRow();
attachVideoListeners(getVideoElement());
if (state.activeRow) state.record.lastItem = getRowName(state.activeRow);
renderPanel();
}
function queueRefresh() {
if (state.refreshQueued) return;
state.refreshQueued = true;
requestAnimationFrame(() => { state.refreshQueued = false; refreshPageData(); });
}
function observePage() {
if (state.observer) state.observer.disconnect();
state.observer = new MutationObserver(() => queueRefresh());
state.observer.observe(document.body, {
childList: true, subtree: true, attributes: true,
attributeFilter: ['class', 'src']
});
}
function onRouteChange() {
if (state.lastPath === location.pathname) return;
state.lastPath = location.pathname;
loadRecord();
queueRefresh();
if (!isCoursePage() && state.studying) pauseStudyTimer();
}
function hookHistory() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
const result = originalPushState.apply(this, args); onRouteChange(); return result;
};
history.replaceState = function (...args) {
const result = originalReplaceState.apply(this, args); onRouteChange(); return result;
};
window.addEventListener('popstate', onRouteChange);
}
function init() {
loadRecord();
ensurePanel();
hookHistory();
refreshPageData();
observePage();
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'visible' && state.studying) state.lastTick = Date.now();
});
setTimeout(refreshPageData, 1200);
setTimeout(refreshPageData, 3500);
console.info(`[重庆高教在线播放器] v${VERSION} 已加载`, { path: location.pathname, coursewareRows: state.rows.length });
}
if (document.body) init();
else window.addEventListener('DOMContentLoaded', init, { once: true });
})();