// ==UserScript== // @name SmartEdu Watch Helper爱课程网与国家高等教育智慧教育平台播放助手 // @namespace https://smartedu.local/ // @version 0.1.3 // @description 为爱课程网与国家高等教育智慧教育平台课程提供自动播放、进度辅助与下一节提示。 // @match https://service.icourses.cn/resCourse/* // @match https://higher.smartedu.cn/* // @grant none // @run-at document-idle // ==/UserScript== (function () { "use strict"; const SCRIPT_ID = "smartedu-watch-helper"; const STORAGE_PREFIX = `${SCRIPT_ID}:course:`; const ACTIVE_CLASS_RE = /\b(active|current|selected|playing|on)\b/i; const NEXT_MARK_CLASS = `${SCRIPT_ID}-next-target`; const LESSON_CACHE_TTL = 1500; const PROGRESS_SAVE_INTERVAL_MS = 5000; const AUTOPLAY_RETRY_DELAY_MS = 1200; const AUTOPLAY_MAX_RETRIES = 8; const AUTO_ADVANCE_DELAY_MS = 800; const AUTO_ADVANCE_AFTER_CLOSE_DELAY_MS = 900; const AUTO_ADVANCE_AFTER_EXPAND_DELAY_MS = 450; const HIGHER_SMARTEDU_ADVANCE_DELAY_MS = 900; const HIGHER_SMARTEDU_NEXT_OPEN_DELAY_MS = 450; const OVERLAY_DISMISS_DELAY_MS = 300; const LEARNING_RECORD_TTL_MS = 15000; const EXPAND_ALL_RETRY_DELAY_MS = 600; const OVERLAY_KEYWORD_RE = /(modal|dialog|popup|overlay|mask|layer|lightbox|intro|guide|poster|cover)/i; const PLAY_TRIGGER_RE = /(播放|开始|继续|学习|观看|进入|play|start|resume|watch)/i; const CLOSE_TRIGGER_RE = /^(关闭|取消|跳过|知道了|我知道了|稍后|以后再说|×|x)$/i; const SMARTEDU_NATIVE_PLAY_SELECTORS = [ ".video-play-img", ".video-play img", ".video-play", ]; const SMARTEDU_MODAL_CLOSE_SELECTORS = [ ".video-modal-close", ".video-modal-mask .video-modal-close", ]; const SMARTEDU_MODAL_SELECTORS = [ ".video-modal-mask", ".video-modal-container", ]; const LESSON_SELECTORS = [ '[class*="catalog"] [class*="item"]', '[class*="chapter"] [class*="item"]', '[class*="section"] [class*="item"]', '[class*="directory"] [class*="item"]', '[class*="course"] [class*="item"]', '[class*="list"] [class*="item"]', '[class*="menu"] [class*="item"]', '[role="treeitem"]', ".el-menu-item", ".ant-menu-item", "aside li", "nav li", ]; let currentVideo = null; let currentLessonKey = ""; let currentLessonTitle = ""; let observer = null; let panel = null; let progressBar = null; let statusLine = null; let lessonLine = null; let nextLine = null; let statsLine = null; let refreshTimer = 0; let lessonCache = []; let lessonCacheAt = 0; let lastProgressSaveAt = 0; let lastProgressValue = -1; let autoplayTimer = 0; let autoplayAttempts = 0; let autoAdvanceTimer = 0; let autoAdvanceInFlight = false; let overlayDismissTimer = 0; let learningRecord = null; let learningRecordAt = 0; let learningRecordInFlight = null; let courseDetail = null; let courseDetailAt = 0; let courseDetailInFlight = null; let expandAllTimer = 0; let expandAllDone = false; let expandAttemptedChapters = new Set(); let lastResumeAppliedKey = ""; let higherSmartEduCurrentIndex = -1; let higherSmartEduAdvanceTimer = 0; let higherSmartEduTrackingBound = false; const state = loadState(); injectStyles(); mountPanel(); bootstrap(); function bootstrap() { bindVideo(findVideo()); bindHigherSmartEduResourceTracking(); expandAllSmartEduChaptersSoon(); refreshLearningRecord(); refreshCourseDetail(); observer = new MutationObserver(() => { const video = findVideo(); if (video && video !== currentVideo) { bindVideo(video); } invalidateLessonCache(); expandAllSmartEduChaptersSoon(); scheduleRefresh(); }); observer.observe(document.documentElement, { childList: true, subtree: true, }); window.addEventListener("load", () => scheduleRefresh({ forceLessonScan: true }), { once: true }); window.setInterval(() => { const video = findVideo(); if (video && video !== currentVideo) { bindVideo(video); } dismissBlockingOverlay({ allowClose: false }); expandAllSmartEduChaptersSoon(); if (!learningRecordInFlight && Date.now() - learningRecordAt >= LEARNING_RECORD_TTL_MS) { refreshLearningRecord(); } if (!courseDetailInFlight && Date.now() - courseDetailAt >= LEARNING_RECORD_TTL_MS) { refreshCourseDetail(); } scheduleRefresh(); }, 2000); scheduleRefresh({ forceLessonScan: true }); } function getStorageKey() { const token = new URLSearchParams(location.search).get("token") || ""; const raw = `${location.host}${location.pathname}::${token}`; return `${STORAGE_PREFIX}${raw}`; } function loadState() { const fallback = { playbackRate: 1, muted: false, lessons: {}, lessonOrder: [], lastLessonKey: "", lastUpdatedAt: 0, }; try { const raw = localStorage.getItem(getStorageKey()); if (!raw) { return fallback; } const parsed = JSON.parse(raw); return { ...fallback, ...parsed, lessons: parsed.lessons || {}, lessonOrder: Array.isArray(parsed.lessonOrder) ? parsed.lessonOrder : [], }; } catch (error) { console.warn(`[${SCRIPT_ID}] Failed to load state`, error); return fallback; } } function saveState() { state.lastUpdatedAt = Date.now(); localStorage.setItem(getStorageKey(), JSON.stringify(state)); } function scheduleRefresh(options = {}) { if (options.forceLessonScan) { invalidateLessonCache(); } if (refreshTimer) { return; } refreshTimer = window.setTimeout(() => { refreshTimer = 0; refreshPanel(); }, 150); } function invalidateLessonCache() { lessonCacheAt = 0; } function findVideo() { return document.querySelector("video"); } function bindVideo(video) { if (!video || video === currentVideo) { return; } if (currentVideo) { unbindVideo(currentVideo); } currentVideo = video; applyPlaybackPreferences(video); syncCurrentLesson(video); lastResumeAppliedKey = ""; video.addEventListener("loadedmetadata", handleLoadedMetadata); video.addEventListener("canplay", handleCanPlay); video.addEventListener("ratechange", handleRateChange); video.addEventListener("volumechange", handleVolumeChange); video.addEventListener("timeupdate", handleTimeUpdate); video.addEventListener("ended", handleEnded); video.addEventListener("play", handlePlayState); video.addEventListener("pause", handlePlayState); autoplayAttempts = 0; autoAdvanceInFlight = false; scheduleAutoplay("bind"); scheduleOverlayDismiss({ allowClose: false }); scheduleRefresh({ forceLessonScan: true }); } function unbindVideo(video) { video.removeEventListener("loadedmetadata", handleLoadedMetadata); video.removeEventListener("canplay", handleCanPlay); video.removeEventListener("ratechange", handleRateChange); video.removeEventListener("volumechange", handleVolumeChange); video.removeEventListener("timeupdate", handleTimeUpdate); video.removeEventListener("ended", handleEnded); video.removeEventListener("play", handlePlayState); video.removeEventListener("pause", handlePlayState); } function applyPlaybackPreferences(video) { if (!video) { return; } if (Number.isFinite(state.playbackRate) && state.playbackRate > 0) { video.playbackRate = clamp(state.playbackRate, 0.5, 4); } video.muted = Boolean(state.muted); } function handleLoadedMetadata() { syncCurrentLesson(currentVideo, true); updateLessonProgress({ duration: safeDuration(currentVideo), progress: currentVideo.duration ? currentVideo.currentTime / currentVideo.duration : 0, }); applySmartEduResumeIfNeeded(currentVideo); scheduleAutoplay("loadedmetadata"); scheduleOverlayDismiss({ allowClose: false }); refreshLearningRecord(); refreshCourseDetail(); scheduleRefresh(); } function handleCanPlay() { scheduleAutoplay("canplay"); scheduleOverlayDismiss({ allowClose: false }); } function handleRateChange() { if (!currentVideo) { return; } state.playbackRate = clamp(currentVideo.playbackRate || 1, 0.5, 4); saveState(); scheduleRefresh(); } function handleVolumeChange() { if (!currentVideo) { return; } state.muted = Boolean(currentVideo.muted); saveState(); scheduleRefresh(); } function handleTimeUpdate() { if (!currentVideo) { return; } const progress = currentVideo.duration ? currentVideo.currentTime / currentVideo.duration : 0; if (!currentLessonKey || Math.abs(progress - lastProgressValue) >= 0.02) { syncCurrentLesson(currentVideo, false); updateLessonProgress({ duration: safeDuration(currentVideo), progress, }); lastProgressValue = progress; } if (Date.now() - lastProgressSaveAt >= PROGRESS_SAVE_INTERVAL_MS) { saveState(); lastProgressSaveAt = Date.now(); } if (Date.now() - learningRecordAt >= LEARNING_RECORD_TTL_MS) { refreshLearningRecord(); } if (Date.now() - courseDetailAt >= LEARNING_RECORD_TTL_MS) { refreshCourseDetail(); } scheduleRefresh(); } function handleEnded() { syncCurrentLesson(currentVideo, true); updateLessonProgress({ progress: 1, completed: true }); highlightNextLesson(); saveState(); refreshLearningRecord(true); refreshCourseDetail(true); if (isHigherSmartEduLmcPage()) { scheduleHigherSmartEduAutoAdvance(); scheduleRefresh(); return; } scheduleAutoAdvance(); scheduleRefresh(); } function handlePlayState() { if (currentVideo && !currentVideo.paused) { clearAutoplayTimer(); autoplayAttempts = 0; scheduleOverlayDismiss({ allowClose: false }); } scheduleRefresh(); } function scheduleAutoplay(reason) { if (!currentVideo) { return; } if (!currentVideo.paused || currentVideo.ended) { clearAutoplayTimer(); return; } if (autoplayAttempts >= AUTOPLAY_MAX_RETRIES) { return; } clearAutoplayTimer(); autoplayTimer = window.setTimeout(() => { autoplayTimer = 0; attemptAutoplay(reason); }, autoplayAttempts === 0 ? 120 : AUTOPLAY_RETRY_DELAY_MS); } function clearAutoplayTimer() { if (!autoplayTimer) { return; } window.clearTimeout(autoplayTimer); autoplayTimer = 0; } function attemptAutoplay(reason) { if (!currentVideo || !currentVideo.paused || currentVideo.ended) { return; } autoplayAttempts += 1; const usedNativeTrigger = startPlaybackFromNativeUI(); if (!usedNativeTrigger) { dismissBlockingOverlay({ allowClose: false }); } if (usedNativeTrigger) { window.setTimeout(() => { if (currentVideo && currentVideo.paused && !currentVideo.ended) { scheduleAutoplay(`${reason}-native-retry`); } }, 700); return; } const playResult = currentVideo.play(); if (playResult && typeof playResult.then === "function") { playResult .then(() => { autoplayAttempts = 0; scheduleRefresh(); }) .catch((error) => { console.debug(`[${SCRIPT_ID}] Autoplay attempt failed (${reason})`, error); if (!usedNativeTrigger) { startPlaybackFromNativeUI(); } scheduleAutoplay(reason); }); return; } scheduleRefresh(); } function scheduleOverlayDismiss(options = {}) { if (overlayDismissTimer) { window.clearTimeout(overlayDismissTimer); } overlayDismissTimer = window.setTimeout(() => { overlayDismissTimer = 0; dismissBlockingOverlay(options); }, OVERLAY_DISMISS_DELAY_MS); } function scheduleAutoAdvance() { if (autoAdvanceInFlight) { return; } if (autoAdvanceTimer) { window.clearTimeout(autoAdvanceTimer); } autoAdvanceTimer = window.setTimeout(() => { autoAdvanceTimer = 0; autoAdvanceToNextLesson(); }, AUTO_ADVANCE_DELAY_MS); } function syncCurrentLesson(video, persist = false) { const lesson = getCurrentLesson(video); currentLessonKey = lesson.key; currentLessonTitle = lesson.title; state.lastLessonKey = lesson.key; rememberLessonOrder(lesson.key, lesson.title); if (persist) { saveState(); } } function getCurrentLesson(video) { const smartEduTitle = getSmartEduCurrentTitle(); if (smartEduTitle) { return { key: smartEduTitle, title: smartEduTitle, }; } const activeItem = findActiveLessonItem(); const title = cleanupText( activeItem?.title || firstText([ '[class*="video"][class*="title"]', '[class*="lesson"][class*="title"]', '[class*="detail"][class*="title"]', "h1", "h2", ]) || deriveNameFromVideo(video) || "当前视频" ); return { key: title, title, }; } function deriveNameFromVideo(video) { if (!video) { return ""; } const src = video.currentSrc || video.src || ""; if (!src) { return ""; } try { const url = new URL(src, location.href); const last = url.pathname.split("/").filter(Boolean).pop() || ""; return decodeURIComponent(last.replace(/\.[a-z0-9]+$/i, "")); } catch { return src; } } function updateLessonProgress(partial) { if (!currentLessonKey) { return; } const lesson = state.lessons[currentLessonKey] || { title: currentLessonTitle || currentLessonKey, progress: 0, completed: false, duration: 0, updatedAt: 0, }; lesson.title = currentLessonTitle || lesson.title; if (typeof partial.duration === "number" && partial.duration > 0) { lesson.duration = partial.duration; } if (typeof partial.progress === "number") { lesson.progress = Math.max(lesson.progress, clamp(partial.progress, 0, 1)); } if (partial.completed) { lesson.completed = true; lesson.progress = 1; } lesson.updatedAt = Date.now(); state.lessons[currentLessonKey] = lesson; rememberLessonOrder(currentLessonKey, lesson.title); } function rememberLessonOrder(key, title) { if (!key) { return; } if (!state.lessonOrder.includes(key)) { state.lessonOrder.push(key); } if (!state.lessons[key]) { state.lessons[key] = { title: title || key, progress: 0, completed: false, duration: 0, updatedAt: 0, }; } } function discoverLessonItems() { if (isSmartEduMultiLevelPage()) { const smartEduItems = discoverSmartEduLessonItems(); lessonCache = smartEduItems; lessonCacheAt = Date.now(); return lessonCache; } if (lessonCacheAt && Date.now() - lessonCacheAt < LESSON_CACHE_TTL) { return lessonCache; } const groups = new Map(); const seen = new Set(); for (const selector of LESSON_SELECTORS) { const nodes = document.querySelectorAll(selector); for (const node of nodes) { if (!(node instanceof HTMLElement) || !isVisible(node)) { continue; } const title = cleanupText(node.innerText || node.textContent || ""); if (!isReasonableLessonTitle(title)) { continue; } const key = `${selector}::${title}`; if (seen.has(key)) { continue; } seen.add(key); const container = node.parentElement || node; const list = groups.get(container) || []; list.push({ el: node, title }); groups.set(container, list); } } let best = []; for (const items of groups.values()) { const uniqueCount = new Set(items.map((item) => item.title)).size; if (uniqueCount >= 2 && items.length > best.length) { best = dedupeByTitle(items); } } lessonCache = best; lessonCacheAt = Date.now(); return lessonCache; } function findActiveLessonItem() { const items = discoverLessonItems(); for (const item of items) { if ( ACTIVE_CLASS_RE.test(item.el.className) || item.el.getAttribute("aria-current") === "true" || item.el.getAttribute("aria-selected") === "true" ) { return item; } } const heading = cleanupText( firstText([ '[class*="video"][class*="title"]', '[class*="lesson"][class*="title"]', "h1", "h2", ]) ); if (!heading) { return null; } return items.find((item) => item.title.includes(heading) || heading.includes(item.title)) || null; } function highlightNextLesson() { document.querySelectorAll(`.${NEXT_MARK_CLASS}`).forEach((el) => { el.classList.remove(NEXT_MARK_CLASS); }); const next = findNextLessonItem(); if (next?.el) { next.el.classList.add(NEXT_MARK_CLASS); } } function getOverallProgress() { const apiProgress = getCourseDetailProgressSummary(); if (apiProgress.total > 0) { return apiProgress; } const smartEduProgress = getSmartEduSystemProgress(); if (smartEduProgress.total > 0) { return smartEduProgress; } const items = discoverLessonItems(); const keys = items.length ? items.map((item) => item.title) : state.lessonOrder.length ? state.lessonOrder : Object.keys(state.lessons); if (!keys.length) { return { overall: 0, completed: 0, total: 0, }; } let totalProgress = 0; let completed = 0; for (const key of keys) { const lesson = state.lessons[key]; const progress = lesson ? clamp(lesson.progress || 0, 0, 1) : 0; totalProgress += progress; if (progress >= 0.999 || lesson?.completed) { completed += 1; } } return { overall: totalProgress / keys.length, completed, total: keys.length, }; } function getNextLessonTitle() { const next = findNextLessonItem(); if (next) { return next.title; } const items = discoverLessonItems(); if (!items.length) { return "未识别到课程列表"; } return "当前已到列表末尾"; } function findNextLessonItem() { const items = discoverLessonItems(); if (!items.length) { return null; } const active = findActiveLessonItem(); const activeIndex = active ? items.findIndex((item) => item.el === active.el) : -1; if (activeIndex >= 0) { return items[activeIndex + 1] || null; } if (currentLessonTitle) { const fallbackIndex = items.findIndex((item) => { return item.title.includes(currentLessonTitle) || currentLessonTitle.includes(item.title); }); if (fallbackIndex >= 0) { return items[fallbackIndex + 1] || null; } } return null; } function autoAdvanceToNextLesson() { if (autoAdvanceInFlight) { return false; } autoAdvanceInFlight = true; const closedModal = closeBlockingModalForNavigation(); const runAdvance = () => { if (trySmartEduAutoAdvance()) { return true; } const next = findNextLessonItem(); if (!next?.el) { autoAdvanceInFlight = false; return false; } highlightNextLesson(); const target = findClickableTarget(next.el); const clicked = triggerLessonClick(target); if (!clicked) { autoAdvanceInFlight = false; return false; } finalizeAutoAdvanceSuccess(); return true; }; if (closedModal) { window.setTimeout(runAdvance, AUTO_ADVANCE_AFTER_CLOSE_DELAY_MS); return true; } return runAdvance(); } function finalizeAutoAdvanceSuccess() { window.setTimeout(() => { autoAdvanceInFlight = false; autoplayAttempts = 0; refreshLearningRecord(true); scheduleOverlayDismiss({ allowClose: false }); scheduleAutoplay("auto-advance"); scheduleRefresh({ forceLessonScan: true }); }, 1500); } function findClickableTarget(el) { if (!(el instanceof HTMLElement)) { return null; } const selector = [ '[role="button"]', '[role="link"]', '[role="treeitem"]', "button", "a[href]", ".el-menu-item", ".ant-menu-item", '[tabindex]:not([tabindex="-1"])', ].join(", "); return el.matches(selector) ? el : el.querySelector(selector) || el; } function triggerLessonClick(target) { return clickElement(target, { block: "center", inline: "nearest" }); } function dismissBlockingOverlay(options = {}) { if (isHigherSmartEduLmcPage()) { return false; } const allowClose = Boolean(options.allowClose); if (dismissSmartEduOverlay({ allowClose })) { return true; } const overlay = findBlockingOverlay(); if (!overlay) { return false; } if (clickOverlayAction(overlay, "play")) { return true; } if (!allowClose) { return false; } return clickOverlayAction(overlay, "close"); } function startPlaybackFromNativeUI() { if (isHigherSmartEduLmcPage()) { return false; } const smartEduPlayTarget = findSmartEduPlayTarget(); if (smartEduPlayTarget && clickElement(smartEduPlayTarget)) { return true; } const overlay = findBlockingOverlay(); if (overlay && clickOverlayAction(overlay, "play")) { return true; } const surface = findNativePlaySurface(); if (!surface) { return false; } return clickElement(surface); } function findBlockingOverlay() { const video = currentVideo || findVideo(); if (!(video instanceof HTMLElement) || !isVisible(video)) { return null; } const videoRect = video.getBoundingClientRect(); if (videoRect.width < 160 || videoRect.height < 90) { return null; } const candidates = Array.from( document.querySelectorAll([ '[role="dialog"]', '[aria-modal="true"]', '[class*="modal"]', '[class*="dialog"]', '[class*="popup"]', '[class*="overlay"]', '[class*="mask"]', '[class*="layer"]', ].join(", ")) ) .filter((el) => el instanceof HTMLElement) .filter((el) => { if (!isVisible(el) || el.id === `${SCRIPT_ID}-panel` || el.contains(panel)) { return false; } const rect = el.getBoundingClientRect(); if (!rect.width || !rect.height || !rectanglesOverlap(rect, videoRect)) { return false; } return isLikelyOverlayElement(el, rect, videoRect); }) .sort((a, b) => { const aRect = a.getBoundingClientRect(); const bRect = b.getBoundingClientRect(); return getRectArea(bRect) - getRectArea(aRect); }); return candidates[0] || findCenterBlockingElement(videoRect); } function dismissSmartEduOverlay(options = {}) { const allowClose = Boolean(options.allowClose); const playTarget = currentVideo && currentVideo.paused ? findSmartEduPlayTarget() : null; if (playTarget && clickElement(playTarget)) { return true; } if (!allowClose) { return false; } const closeTarget = findSmartEduModalCloseTarget(); if (closeTarget && clickElement(closeTarget)) { return true; } return false; } function closeBlockingModalForNavigation() { if (isHigherSmartEduLmcPage()) { return false; } const closeTarget = findSmartEduModalCloseTarget(); if (closeTarget) { return clickElement(closeTarget); } return false; } function findSmartEduPlayTarget() { for (const selector of SMARTEDU_NATIVE_PLAY_SELECTORS) { const node = document.querySelector(selector); if (node instanceof HTMLElement && isVisible(node)) { return node; } } return null; } function isSmartEduMultiLevelPage() { return /service\.icourses\.cn$/i.test(location.host) && Boolean(document.querySelector(".tree-container")); } function isHigherSmartEduLmcPage() { return /higher\.smartedu\.cn$/i.test(location.host) && /^\/course\/lmc\//i.test(location.pathname); } function bindHigherSmartEduResourceTracking() { if (!isHigherSmartEduLmcPage() || higherSmartEduTrackingBound) { return; } higherSmartEduTrackingBound = true; document.addEventListener("click", handleHigherSmartEduResourceClick, true); } function handleHigherSmartEduResourceClick(event) { if (!isHigherSmartEduLmcPage()) { return; } const target = event.target; if (!(target instanceof Element)) { return; } const card = target.closest("figure"); if (!(card instanceof HTMLElement)) { return; } const cards = getHigherSmartEduResourceCards(); const index = cards.indexOf(card); if (index >= 0) { higherSmartEduCurrentIndex = index; const title = getHigherSmartEduCardTitle(card); if (title) { currentLessonKey = title; currentLessonTitle = title; } } } function getHigherSmartEduResourceCards() { if (!isHigherSmartEduLmcPage()) { return []; } return Array.from(document.querySelectorAll("figure")).filter((card) => { return card instanceof HTMLElement && isVisible(card) && /cursor-pointer/.test(card.className || ""); }); } function getHigherSmartEduCardTitle(card) { if (!(card instanceof HTMLElement)) { return ""; } const title = cleanupText(card.parentElement?.querySelector(".mt-2")?.textContent || ""); if (title) { return title; } return cleanupText(card.parentElement?.textContent || ""); } function scheduleHigherSmartEduAutoAdvance() { if (higherSmartEduAdvanceTimer) { window.clearTimeout(higherSmartEduAdvanceTimer); } higherSmartEduAdvanceTimer = window.setTimeout(() => { higherSmartEduAdvanceTimer = 0; autoAdvanceHigherSmartEduResource(); }, HIGHER_SMARTEDU_ADVANCE_DELAY_MS); } function autoAdvanceHigherSmartEduResource() { const cards = getHigherSmartEduResourceCards(); if (!cards.length) { return false; } const nextIndex = higherSmartEduCurrentIndex + 1; if (nextIndex < 0 || nextIndex >= cards.length) { return false; } const closeTarget = findHigherSmartEduModalCloseTarget(); if (closeTarget) { clickElement(closeTarget, { scroll: false }); } window.setTimeout(() => { const refreshedCards = getHigherSmartEduResourceCards(); const nextCard = refreshedCards[nextIndex]; if (!(nextCard instanceof HTMLElement)) { return; } higherSmartEduCurrentIndex = nextIndex; const title = getHigherSmartEduCardTitle(nextCard); if (title) { currentLessonKey = title; currentLessonTitle = title; } clickElement(nextCard); }, HIGHER_SMARTEDU_NEXT_OPEN_DELAY_MS); return true; } function findHigherSmartEduModalCloseTarget() { if (!isHigherSmartEduLmcPage()) { return null; } const video = currentVideo || findVideo(); if (!(video instanceof HTMLElement)) { return null; } const rect = video.getBoundingClientRect(); const buttons = Array.from(document.querySelectorAll("button")).filter((button) => { if (!(button instanceof HTMLElement) || !isVisible(button)) { return false; } const buttonRect = button.getBoundingClientRect(); return buttonRect.width <= 60 && buttonRect.height <= 60 && buttonRect.left >= rect.right - 80 && buttonRect.top <= rect.top + 40 && buttonRect.top >= rect.top - 20; }); return buttons[0] || null; } function getSmartEduCurrentTitle() { return cleanupText( firstText([ ".video-modal-container .row div", ".video-content .row div", ]) ); } function discoverSmartEduLessonItems() { const items = []; const nodes = document.querySelectorAll(".tree-container .child-item"); for (const node of nodes) { if (!(node instanceof HTMLElement) || !isVisible(node)) { continue; } const title = cleanupText(node.querySelector(".tag-txt")?.textContent || node.innerText || ""); if (!isReasonableLessonTitle(title)) { continue; } items.push({ el: node, title }); } return dedupeByTitle(items); } function getSmartEduDisplayedLessonStats() { const items = []; const nodes = document.querySelectorAll(".tree-container .child-item"); for (const node of nodes) { if (!(node instanceof HTMLElement)) { continue; } const title = cleanupText(node.querySelector(".tag-txt")?.textContent || node.innerText || ""); if (!isReasonableLessonTitle(title)) { continue; } const progressText = cleanupText(node.querySelector(".process")?.textContent || ""); items.push({ el: node, title, progress: extractPercent(progressText), }); } return items; } function getSmartEduCurrentProgress() { const courseProgress = getCourseDetailCurrentProgress(); if (courseProgress !== null) { return courseProgress; } const stats = getSmartEduDisplayedLessonStats(); if (!stats.length) { return null; } const currentTitle = getSmartEduCurrentTitle() || currentLessonTitle; if (!currentTitle) { return null; } const match = stats.find((item) => item.title === currentTitle || item.title.includes(currentTitle) || currentTitle.includes(item.title)); return match ? match.progress : null; } function getSmartEduSystemProgress() { const stats = getSmartEduDisplayedLessonStats(); if (!stats.length) { return { overall: 0, completed: 0, total: 0, }; } let totalProgress = 0; let completed = 0; for (const item of stats) { totalProgress += item.progress; if (item.progress >= 0.999) { completed += 1; } } return { overall: totalProgress / stats.length, completed, total: stats.length, }; } function getCourseDetailProgressSummary() { const list = getCourseDetailLessonList(); if (!list.length) { return { overall: 0, completed: 0, total: 0, }; } let totalProgress = 0; let completed = 0; for (const item of list) { const progress = clamp((Number(item.progress) || 0) / 100, 0, 1); totalProgress += progress; if (progress >= 0.999) { completed += 1; } } return { overall: totalProgress / list.length, completed, total: list.length, }; } function getCourseDetailLessonList() { const chapters = courseDetail?.chapterResTree; if (!Array.isArray(chapters)) { return []; } const list = []; for (const chapter of chapters) { if (Array.isArray(chapter?.resList)) { for (const item of chapter.resList) { list.push(item); } } if (Array.isArray(chapter?.children)) { for (const child of chapter.children) { if (Array.isArray(child?.resList)) { for (const item of child.resList) { list.push(item); } } } } } return list; } function getCourseDetailCurrentProgress() { const currentTitle = getSmartEduCurrentTitle() || currentLessonTitle; if (!currentTitle) { return null; } const lesson = findCourseDetailLessonByTitle(currentTitle); if (!lesson) { return null; } return clamp((Number(lesson.progress) || 0) / 100, 0, 1); } function findCourseDetailLessonByTitle(title) { if (!title) { return null; } const normalized = cleanupText(title); return getCourseDetailLessonList().find((item) => { const candidate = cleanupText(item?.resTitle || ""); return candidate === normalized || candidate.includes(normalized) || normalized.includes(candidate); }) || null; } function applySmartEduResumeIfNeeded(video) { if (!isSmartEduMultiLevelPage() || !(video instanceof HTMLVideoElement)) { return; } const title = getSmartEduCurrentTitle() || currentLessonTitle; if (!title || lastResumeAppliedKey === title) { return; } const lesson = findCourseDetailLessonByTitle(title); if (!lesson) { return; } const progress = clamp((Number(lesson.progress) || 0) / 100, 0, 1); const duration = safeDuration(video); if (!duration || progress <= 0.01 || progress >= 0.98) { return; } const resumeTime = clamp(duration * progress, 0, Math.max(duration - 2, 0)); if (resumeTime <= 0 || Math.abs(video.currentTime - resumeTime) < 1) { return; } try { video.currentTime = resumeTime; lastResumeAppliedKey = title; } catch (error) { console.debug(`[${SCRIPT_ID}] Failed to apply resume point`, error); } } function trySmartEduAutoAdvance() { if (!isSmartEduMultiLevelPage()) { return false; } const currentTitle = getSmartEduCurrentTitle() || currentLessonTitle; const visibleResources = discoverSmartEduLessonItems(); const currentIndex = findLessonIndexByTitle(visibleResources, currentTitle); if (currentIndex >= 0 && visibleResources[currentIndex + 1]) { highlightSpecificLesson(visibleResources[currentIndex + 1].el); const clicked = triggerLessonClick(findClickableTarget(visibleResources[currentIndex + 1].el)); if (clicked) { finalizeAutoAdvanceSuccess(); } else { autoAdvanceInFlight = false; } return clicked; } const currentResource = currentIndex >= 0 ? visibleResources[currentIndex].el : null; const currentChapter = findCurrentSmartEduChapter(currentResource, currentTitle); if (openNextSmartEduPanel(currentChapter, currentResource)) { return true; } if (openNextSmartEduChapter(currentChapter)) { return true; } autoAdvanceInFlight = false; return false; } function findLessonIndexByTitle(items, title) { if (!title || !items.length) { return -1; } return items.findIndex((item) => item.title === title || item.title.includes(title) || title.includes(item.title)); } function findCurrentSmartEduChapter(currentResource, currentTitle) { if (currentResource instanceof HTMLElement) { const owner = currentResource.closest("li.list-item"); if (owner instanceof HTMLElement) { return owner; } } const activeChapter = document.querySelector(".tree-container .left-item.active"); if (activeChapter instanceof HTMLElement) { const owner = activeChapter.closest("li.list-item"); if (owner instanceof HTMLElement) { return owner; } } if (currentTitle) { for (const item of document.querySelectorAll(".tree-container li.list-item")) { if (!(item instanceof HTMLElement)) { continue; } if (cleanupText(item.innerText || "").includes(currentTitle)) { return item; } } } return document.querySelector(".tree-container li.list-item"); } function openNextSmartEduPanel(currentChapter, currentResource) { if (!(currentChapter instanceof HTMLElement)) { return false; } const panels = Array.from(currentChapter.querySelectorAll(".ant-collapse-item")); if (!panels.length) { return false; } const currentPanel = currentResource instanceof HTMLElement ? currentResource.closest(".ant-collapse-item") : currentChapter.querySelector(".ant-collapse-item-active"); const currentIndex = currentPanel ? panels.indexOf(currentPanel) : -1; for (let i = currentIndex + 1; i < panels.length; i += 1) { const header = panels[i]?.querySelector(".ant-collapse-header"); if (!(header instanceof HTMLElement)) { continue; } if (!clickElement(header)) { continue; } window.setTimeout(() => { const refreshedPanels = Array.from(currentChapter.querySelectorAll(".ant-collapse-item")); const nextPanel = refreshedPanels[i]; const firstItem = nextPanel?.querySelector(".child-item"); if (firstItem instanceof HTMLElement) { highlightSpecificLesson(firstItem); triggerLessonClick(findClickableTarget(firstItem)); finalizeAutoAdvanceSuccess(); return; } autoAdvanceInFlight = false; }, AUTO_ADVANCE_AFTER_EXPAND_DELAY_MS); return true; } return false; } function openNextSmartEduChapter(currentChapter) { const chapters = Array.from(document.querySelectorAll(".tree-container li.list-item")); if (!chapters.length) { return false; } const currentIndex = currentChapter instanceof HTMLElement ? chapters.indexOf(currentChapter) : -1; for (let i = currentIndex + 1; i < chapters.length; i += 1) { const chapter = chapters[i]; if (!(chapter instanceof HTMLElement)) { continue; } const chapterToggle = chapter.querySelector(".left-item"); if (!(chapterToggle instanceof HTMLElement) || !clickElement(chapterToggle)) { continue; } window.setTimeout(() => { const directItem = chapter.querySelector(".child-item"); if (directItem instanceof HTMLElement) { highlightSpecificLesson(directItem); triggerLessonClick(findClickableTarget(directItem)); finalizeAutoAdvanceSuccess(); return; } const firstPanelHeader = chapter.querySelector(".ant-collapse-item .ant-collapse-header"); if (firstPanelHeader instanceof HTMLElement && clickElement(firstPanelHeader)) { window.setTimeout(() => { const firstPanelItem = chapter.querySelector(".ant-collapse-item .child-item"); if (firstPanelItem instanceof HTMLElement) { highlightSpecificLesson(firstPanelItem); triggerLessonClick(findClickableTarget(firstPanelItem)); finalizeAutoAdvanceSuccess(); return; } autoAdvanceInFlight = false; }, AUTO_ADVANCE_AFTER_EXPAND_DELAY_MS); return; } autoAdvanceInFlight = false; }, AUTO_ADVANCE_AFTER_EXPAND_DELAY_MS); return true; } return false; } function highlightSpecificLesson(el) { document.querySelectorAll(`.${NEXT_MARK_CLASS}`).forEach((node) => { node.classList.remove(NEXT_MARK_CLASS); }); if (el instanceof HTMLElement) { el.classList.add(NEXT_MARK_CLASS); } } function findSmartEduModalCloseTarget() { for (const selector of SMARTEDU_MODAL_CLOSE_SELECTORS) { const node = document.querySelector(selector); if (node instanceof HTMLElement && isVisible(node)) { return node; } } for (const selector of SMARTEDU_MODAL_SELECTORS) { const modal = document.querySelector(selector); if (!(modal instanceof HTMLElement) || !isVisible(modal)) { continue; } const rect = modal.getBoundingClientRect(); const stack = document.elementsFromPoint(rect.right - 20, rect.top + 20); for (const el of stack) { if (el instanceof HTMLElement && isVisible(el) && looksInteractive(el)) { const classText = `${el.className || ""} ${el.id || ""}`; if (/(close|guanbi|关闭|modal)/i.test(classText) || el === modal) { return el; } } } } return null; } function isLikelyOverlayElement(el, rect, videoRect) { const classText = `${el.className || ""} ${el.id || ""}`.toLowerCase(); if (OVERLAY_KEYWORD_RE.test(classText)) { return true; } const overlapArea = getOverlapArea(rect, videoRect); if (overlapArea >= getRectArea(videoRect) * 0.18) { return true; } const style = window.getComputedStyle(el); return style.position === "fixed" || style.position === "absolute"; } function findCenterBlockingElement(videoRect) { const centerX = videoRect.left + videoRect.width / 2; const centerY = videoRect.top + videoRect.height / 2; const stack = document.elementsFromPoint(centerX, centerY); for (const el of stack) { if (!(el instanceof HTMLElement)) { continue; } if (el === currentVideo || currentVideo?.contains(el) || el.id === `${SCRIPT_ID}-panel` || el.contains(panel)) { continue; } const rect = el.getBoundingClientRect(); if (!rect.width || !rect.height) { continue; } if (rectanglesOverlap(rect, videoRect) && looksInteractive(el)) { return el; } } return null; } function clickOverlayAction(overlay, mode) { const candidates = overlay instanceof HTMLElement ? collectOverlayTargets(overlay, mode) : []; for (const target of candidates) { if (!(target instanceof HTMLElement) || !isVisible(target)) { continue; } if (clickElement(target)) { return true; } } return false; } function collectOverlayTargets(overlay, mode) { const selector = [ "button", "a[href]", "span", "div", '[role="button"]', '[class*="play"]', '[class*="start"]', '[class*="close"]', '[class*="btn"]', '[tabindex]:not([tabindex="-1"])', "svg", "i", ].join(", "); const matches = []; const nodes = overlay.matches(selector) ? [overlay, ...overlay.querySelectorAll(selector)] : overlay.querySelectorAll(selector); for (const node of nodes) { if (!(node instanceof HTMLElement) || node.id === `${SCRIPT_ID}-panel` || node.closest(`#${SCRIPT_ID}-panel`)) { continue; } if (mode === "play" && isPlayLikeTarget(node, overlay)) { matches.push(node); } if (mode === "close" && isCloseLikeTarget(node, overlay)) { matches.push(node); } } return matches.sort((a, b) => scoreOverlayTarget(b, overlay, mode) - scoreOverlayTarget(a, overlay, mode)); } function isPlayLikeTarget(node, overlay) { const text = cleanupText(node.innerText || node.textContent || ""); const attrs = [ node.getAttribute("aria-label") || "", node.getAttribute("title") || "", node.getAttribute("class") || "", node.getAttribute("id") || "", ].join(" "); if (PLAY_TRIGGER_RE.test(`${text} ${attrs}`)) { return true; } const rect = node.getBoundingClientRect(); const parentRect = overlay.getBoundingClientRect(); const nearCenter = Math.abs((rect.left + rect.width / 2) - (parentRect.left + parentRect.width / 2)) < parentRect.width * 0.2 && Math.abs((rect.top + rect.height / 2) - (parentRect.top + parentRect.height / 2)) < parentRect.height * 0.2; return nearCenter && looksInteractive(node) && (rect.width >= 36 || rect.height >= 36); } function isCloseLikeTarget(node, overlay) { const text = cleanupText(node.innerText || node.textContent || ""); const attrs = [ node.getAttribute("aria-label") || "", node.getAttribute("title") || "", node.getAttribute("class") || "", node.getAttribute("id") || "", ].join(" "); if (CLOSE_TRIGGER_RE.test(text) || /(close|cancel|dismiss|shutdown|guanbi|关闭)/i.test(attrs)) { return true; } const rect = node.getBoundingClientRect(); const parentRect = overlay.getBoundingClientRect(); return looksInteractive(node) && rect.width <= 80 && rect.height <= 80 && rect.left >= parentRect.left + parentRect.width * 0.72 && rect.top <= parentRect.top + parentRect.height * 0.25; } function scoreOverlayTarget(node, overlay, mode) { const rect = node.getBoundingClientRect(); const parentRect = overlay.getBoundingClientRect(); const centerDistance = Math.abs((rect.left + rect.width / 2) - (parentRect.left + parentRect.width / 2)) + Math.abs((rect.top + rect.height / 2) - (parentRect.top + parentRect.height / 2)); if (mode === "play") { return 10000 - centerDistance + rect.width * rect.height; } return rect.left + (parentRect.top + parentRect.width - rect.top); } function findNativePlaySurface() { if (isHigherSmartEduLmcPage()) { return null; } const smartEduPlayTarget = findSmartEduPlayTarget(); if (smartEduPlayTarget) { return smartEduPlayTarget; } const video = currentVideo || findVideo(); if (!(video instanceof HTMLElement) || !isVisible(video)) { return null; } const rect = video.getBoundingClientRect(); const points = [ [rect.left + rect.width / 2, rect.top + rect.height / 2], [rect.left + rect.width * 0.5, rect.top + rect.height * 0.62], [rect.left + rect.width * 0.82, rect.top + rect.height * 0.14], [rect.left + rect.width * 0.18, rect.top + rect.height * 0.18], ]; for (const [x, y] of points) { const stack = document.elementsFromPoint(x, y); for (const el of stack) { if (!(el instanceof HTMLElement)) { continue; } if (el === video || video.contains(el) || el.id === `${SCRIPT_ID}-panel` || el.closest(`#${SCRIPT_ID}-panel`)) { continue; } if (!isVisible(el)) { continue; } const infoText = cleanupText([ el.innerText || el.textContent || "", el.getAttribute("aria-label") || "", el.getAttribute("title") || "", el.className || "", el.id || "", ].join(" ")); if (PLAY_TRIGGER_RE.test(infoText) || looksInteractive(el)) { return el; } } } return null; } function clickElement(target, options = {}) { if (!(target instanceof HTMLElement)) { return false; } const block = options.block || "center"; const inline = options.inline || "center"; if (options.scroll !== false) { target.scrollIntoView({ block, inline }); } for (const type of ["pointerover", "mouseover", "pointerdown", "mousedown", "pointerup", "mouseup"]) { target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window })); } target.click(); return true; } function looksInteractive(el) { const style = window.getComputedStyle(el); const tagName = el.tagName.toLowerCase(); return tagName === "button" || tagName === "a" || el.getAttribute("role") === "button" || el.tabIndex >= 0 || style.cursor === "pointer"; } function rectanglesOverlap(a, b) { return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom); } function getOverlapArea(a, b) { if (!rectanglesOverlap(a, b)) { return 0; } const width = Math.min(a.right, b.right) - Math.max(a.left, b.left); const height = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top); return Math.max(0, width) * Math.max(0, height); } function getRectArea(rect) { return Math.max(0, rect.width) * Math.max(0, rect.height); } function mountPanel() { if (panel) { return; } panel = document.createElement("section"); panel.id = `${SCRIPT_ID}-panel`; panel.innerHTML = `