您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为 HTML5 视频添加 Netflix 风格悬浮控件,默认播放速度0.1x,修复UI显示问题,支持一键部署(快速跳转至99.9%、暂停、更新进度、自动取消弹窗按新逻辑、跳过已学习视频),停止/继续部署,拖动控件,拦截人脸识别,跳转后自动更新进度,修复停止/继续部署问题。
当前为
// ==UserScript== // @name 调试 - 稳定版通用视频控制器 (Netflix 风格 + UI修复 + 人脸拦截 + 跳转后更新 + 修复暂停/继续) // @namespace http://tampermonkey.net/ // @version 3.4_zh-CN_fixed_deploy_pause_resume // @description 为 HTML5 视频添加 Netflix 风格悬浮控件,默认播放速度0.1x,修复UI显示问题,支持一键部署(快速跳转至99.9%、暂停、更新进度、自动取消弹窗按新逻辑、跳过已学习视频),停止/继续部署,拖动控件,拦截人脸识别,跳转后自动更新进度,修复停止/继续部署问题。 // @author Gemini (由 Google 开发), Grok (优化) // @match *://gd.aqscwlxy.com/* // @exclude https://*.netflix.com/* // @grant GM_addStyle // @run-at document-start // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI0U1MDkyMCI+PHBhdGggZD0iTTE5IDNINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS1xLTkuOS0yLTItMnptLTggMTBoOXYtMkg3Vjl2LTJoMnYyIGgtMnYyeiIvPjwvc3ZnPg== // ==/UserScript== (function() { 'use strict'; // --- 配置 --- const PANEL_ID = 'universal-video-controller-panel-zh-v13'; const SPEEDS = [0.1, 0.5, 1.0]; const FIXED_JUMP_PERCENTAGES = [ { label: '50%', value: 50 }, { label: '99.9%', value: 99.9 } ]; const SKIP_AMOUNT = 10; const HIDE_DELAY = 1800; const MIN_VIDEO_SIZE = 100; const SAFE_ACTION_FLAG = '_UVC_safeAction_v13'; const AUTO_SLOWDOWN_THRESHOLD = 3.0; const AUTO_SLOWDOWN_FLAG = '_UVC_isAutoSlowdown_v13'; const SLOWDOWN_RATE = 0.1; const PIN_PANEL_FLAG = '_UVC_isPanelPinned_v13'; const CONFIRM_TEXT = '确认'; const CANCEL_TEXT = '取消'; const GOTO_TEST_TEXT = '去测试'; const UPDATE_TEXT = '更新进度'; const BLOCKED_URL = "https://gd.aqscwlxy.com/gd_api/face/launch_face_auth.php"; const STUDYRECORD_URL = '/gd_api/study/studyrecord.php'; const BLOCK_EVENTS = ['blur', 'visibilitychange', 'pagehide', 'freeze']; const UPDATE_CLICK_COUNT = 5; const FAKE_SUCCESS_RESPONSE = { code: 200, msg: "上传成功 (由脚本修改)", data: null }; const FAKE_SUCCESS_RESPONSE_TEXT = JSON.stringify(FAKE_SUCCESS_RESPONSE); // --- 状态变量 --- let currentVideo = null; let controlPanel = null; let hideTimeout = null; let activeSpeedButton = null; let playPauseButton = null; let dragButton = null; let muteButton = null; let deployButton = null; let stopDeployButton = null; let currentTimeDisplay = null; let durationDisplay = null; let timeUpdateInterval = null; let progressBarContainer = null; let progressTrack = null; let progressBuffer = null; let progressFill = null; let progressThumb = null; let isDraggingProgress = false; let wasPausedBeforeDrag = false; let progressPercentDisplay = null; let originalSpeedBeforeSlowdown = null; let pinPanelButton = null; let allowPauseAtSlowSpeed = true; let isDeploying = false; let isDeploymentPaused = false; let isDraggingPanel = false; let dragStartX = 0; let dragStartY = 0; window[AUTO_SLOWDOWN_FLAG] = false; window[PIN_PANEL_FLAG] = true; // --- 日志函数 --- function log(message) { console.log('[VideoController]', message); } // --- 拦截人脸识别和强制更新学习记录 --- if (window.location.hostname === 'gd.aqscwlxy.com') { const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; this._method = method; if (typeof url === 'string') { if (url.includes(STUDYRECORD_URL) && method.toUpperCase() === 'POST') { this.addEventListener('readystatechange', function() { if (this.readyState === 4) { Object.defineProperties(this, { response: { value: FAKE_SUCCESS_RESPONSE_TEXT, writable: true, configurable: true }, responseText: { value: FAKE_SUCCESS_RESPONSE_TEXT, writable: true, configurable: true }, status: { value: 200, writable: true, configurable: true }, statusText: { value: 'OK (faked)', writable: true, configurable: true } }); log('学习记录已强制更新为成功'); } }, false); } if (url === BLOCKED_URL) { this.addEventListener('readystatechange', function() { if (this.readyState === 4) { Object.defineProperties(this, { status: { value: 0, writable: true, configurable: true }, statusText: { value: 'Blocked by script', writable: true, configurable: true } }); log('人脸识别请求被拦截'); } }, false); } } originalOpen.apply(this, arguments); }; const originalFetch = window.fetch; window.fetch = function(input, init) { let url = (typeof input === 'string') ? input : (input ? input.url : ''); let method = (init && init.method) ? init.method.toUpperCase() : 'GET'; if (typeof url === 'string') { if (url.includes(STUDYRECORD_URL) && method === 'POST') { const fakeResponse = new Response(FAKE_SUCCESS_RESPONSE_TEXT, { status: 200, statusText: 'OK (faked)', headers: { 'Content-Type': 'application/json;charset=utf-8' } }); log('学习记录 Fetch 请求已强制返回成功'); return Promise.resolve(fakeResponse); } if (url === BLOCKED_URL) { log('人脸识别 Fetch 请求被拦截'); return Promise.reject(new Error("人脸识别请求被脚本拦截")); } } return originalFetch.apply(this, arguments); }; } // --- 防护功能 --- if (window.location.hostname === 'gd.aqscwlxy.com') { const winAdd = window.addEventListener; window.addEventListener = function(type, listener, options) { if (BLOCK_EVENTS.includes(type)) return; winAdd.call(this, type, listener, options); }; const docAdd = document.addEventListener; document.addEventListener = function(type, listener, options) { if (BLOCK_EVENTS.includes(type)) return; docAdd.call(this, type, listener, options); }; Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true }); Object.defineProperty(document, 'hidden', { get: () => false, configurable: true }); document.hasFocus = () => true; const originalPause = HTMLMediaElement.prototype.pause; HTMLMediaElement.prototype.pause = function() { if (window[SAFE_ACTION_FLAG]) { window[SAFE_ACTION_FLAG] = false; return originalPause.apply(this, arguments); } const fromScript = !(new Error().stack.includes('native code')); if (fromScript && !allowPauseAtSlowSpeed) { return; } return originalPause.apply(this, arguments); }; window.addEventListener('load', () => { const originalCurrentTime = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'currentTime'); Object.defineProperty(HTMLMediaElement.prototype, 'currentTime', { get: function() { return originalCurrentTime.get.call(this); }, set: function(val) { if (window[SAFE_ACTION_FLAG]) { window[SAFE_ACTION_FLAG] = false; return originalCurrentTime.set.call(this, val); } if (typeof val === 'number' && val < this.currentTime - 1) { return; } return originalCurrentTime.set.call(this, val); }, configurable: true }); }); } // --- 样式 --- GM_addStyle(` #${PANEL_ID} { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(20, 20, 20, 0.9); border-radius: 8px; padding: 10px 15px; flex-direction: column; gap: 8px; z-index: 2147483647; font-family: Arial, sans-serif; backdrop-filter: blur(6px); box-shadow: 0 3px 12px rgba(0, 0, 0, 0.6); color: #fff; transition: opacity 0.3s ease; width: clamp(550px, 75vw, 950px); box-sizing: border-box; cursor: move; } #${PANEL_ID}.visible { display: flex; opacity: 1; bottom: 30px; } #${PANEL_ID} .progress-bar-container { width: 100%; height: 16px; display: flex; align-items: center; cursor: pointer; padding: 5px 0; } #${PANEL_ID} .progress-track { width: 100%; height: 5px; background-color: rgba(100, 100, 100, 0.6); border-radius: 3px; position: relative; overflow: hidden; } #${PANEL_ID} .progress-buffer { position: absolute; height: 100%; width: 0%; background-color: rgba(150, 150, 150, 0.6); } #${PANEL_ID} .progress-fill { position: absolute; height: 100%; width: 0%; background-color: #E50914; z-index: 1; } #${PANEL_ID} .progress-thumb { position: absolute; top: 50%; width: 14px; height: 14px; background-color: #E50914; border: 1px solid rgba(255,255,255,0.8); border-radius: 50%; transform: translate(-50%, -50%); z-index: 2; cursor: pointer; } #${PANEL_ID} .controls-row { display: flex; width: 100%; justify-content: space-between; align-items: center; gap: 10px; } #${PANEL_ID} .controls-left, #${PANEL_ID} .controls-center, #${PANEL_ID} .controls-right { display: flex; align-items: center; gap: 8px; } #${PANEL_ID} .controls-center { flex-grow: 1; justify-content: center; gap: 6px; } #${PANEL_ID} .progress-percent-display { font-size: 13px; font-weight: bold; color: #E0E0E0; min-width: 45px; text-align: right; margin-right: 10px; } #${PANEL_ID} .speed-controls { display: flex; align-items: center; gap: 6px; } #${PANEL_ID} .speed-controls button.active { background-color: rgba(229, 9, 20, 0.85); border-color: #fff; font-weight: bold; } #${PANEL_ID} .jump-controls-group { display: flex; align-items: center; gap: 5px; border-left: 1px solid #555; padding-left: 10px; } #${PANEL_ID} button { background: rgba(70, 70, 70, 0.75); color: #fff; border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 5px; padding: 5px 9px; cursor: pointer; font-size: 12px; transition: background-color 0.2s ease; min-width: 28px; } #${PANEL_ID} button:hover { background: rgba(100, 100, 100, 0.9); } #${PANEL_ID} .deploy-button { background: rgba(0, 128, 0, 0.75); padding: 5px 12px; } #${PANEL_ID} .deploy-button:hover { background: rgba(0, 160, 0, 0.9); } #${PANEL_ID} .stop-deploy-button { background: rgba(255, 0, 0, 0.75); padding: 5px 12px; } #${PANEL_ID} .stop-deploy-button:hover { background: rgba(255, 50, 50, 0.9); } #${PANEL_ID} .continue-deploy-button { background: rgba(0, 128, 0, 0.75); padding: 5px 12px; } #${PANEL_ID} .continue-deploy-button:hover { background: rgba(0, 160, 0, 0.9); } #${PANEL_ID} .time-display { font-size: 12px; color: #ddd; min-width: 80px; text-align: center; } #${PANEL_ID} .pin-panel-button.pinned { background-color: rgba(229, 9, 20, 0.85); } #${PANEL_ID} .drag-button::before { content: '✥'; font-size: 15px; } #${PANEL_ID} .play-icon::before { content: '▶'; font-size: 10px; margin-right: 3px; } #${PANEL_ID} .pause-icon::before { content: '❚❚'; font-size: 10px; margin-right: 3px; } #${PANEL_ID} .skip-back-icon::before { content: '⏪'; margin-right: 2px; font-size: 14px; } #${PANEL_ID} .skip-fwd-icon::after { content: '⏩'; margin-left: 2px; font-size: 14px; } #${PANEL_ID} .mute-icon::before { content: '🔇'; font-size: 14px; } #${PANEL_ID} .unmute-icon::before { content: '🔊'; font-size: 14px; } #${PANEL_ID} .deploy-icon::before { content: '🚀'; font-size: 14px; margin-right: 4px; } .auto-dismiss-alert { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.9); color: white; padding: 15px 25px; border-radius: 5px; z-index: 2147483647; font-size: 16px; } .chapter-end-notification { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.8); color: white; padding: 15px 25px; border-radius: 8px; z-index: 10000; } `); // --- 核心功能函数 --- function setSafeAction(enable) { window[SAFE_ACTION_FLAG] = enable; } function findButtons(text) { return Array.from(document.querySelectorAll('button')).filter(button => { const isVisible = button.offsetParent !== null; const isEnabled = !button.disabled; return isVisible && isEnabled && button.textContent?.trim().includes(text); }); } function checkForCancelButton() { const cancelButtons = findButtons(CANCEL_TEXT); const gotoTestButtons = findButtons(GOTO_TEST_TEXT); const confirmButtons = findButtons(CONFIRM_TEXT); if (cancelButtons.length > 0) { if (gotoTestButtons.length > 0) { log(`发现 "${CANCEL_TEXT}" 和 "${GOTO_TEST_TEXT}",点击 "${CANCEL_TEXT}"`); cancelButtons[0].click(); return true; } if (confirmButtons.length > 0) { log(`发现 "${CANCEL_TEXT}" 和 "${CONFIRM_TEXT}",点击 "${CONFIRM_TEXT}"`); confirmButtons[0].click(); return true; } } return false; } function isUpdateButtonPresent() { const buttons = document.querySelectorAll('div.ro-comment > span:nth-child(2)'); return Array.from(buttons).some(button => button.textContent.trim() === UPDATE_TEXT && isVisible(button)); } function isVideoStudied(item) { const finishElement = item.querySelector('.ro-finish'); return finishElement && finishElement.textContent.includes('已学习'); } function showAutoDismissAlert(message) { const alertDiv = document.createElement('div'); alertDiv.className = 'auto-dismiss-alert'; alertDiv.textContent = message; document.body.appendChild(alertDiv); setTimeout(() => alertDiv.remove(), 2500); } function showChapterEndNotification(message) { const notification = document.createElement('div'); notification.className = 'chapter-end-notification'; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => notification.remove(), 2500); } function updateProgress(actionType) { const buttons = document.querySelectorAll('div.ro-comment > span:nth-child(2)'); buttons.forEach(button => { if (button.textContent.trim() === UPDATE_TEXT && isVisible(button)) { for (let i = 0; i < UPDATE_CLICK_COUNT; i++) { setTimeout(() => { button.click(); log(`点击 "${UPDATE_TEXT}" 第 ${i + 1} 次`); }, i * 50); } } }); } function updateProgressOnDeploy() { return new Promise(resolve => { if (isUpdateButtonPresent()) { updateProgress("部署或跳转后更新"); setTimeout(resolve, UPDATE_CLICK_COUNT * 50 + 50); } else { log("未找到“更新进度”按钮,跳过更新"); resolve(); } }); } function getVideoItems() { const container = document.querySelector('div.items'); return container ? Array.from(container.querySelectorAll('div.item')) : []; } function getActiveItem() { const container = document.querySelector('div.items'); return container ? container.querySelector('div.item.item-active') : null; } function isVisible(elem) { if (!(elem instanceof Element)) return false; const style = window.getComputedStyle(elem); return style.display !== 'none' && style.visibility !== 'hidden' && elem.offsetParent !== null; } function createControlPanel() { if (document.getElementById(PANEL_ID)) return; controlPanel = document.createElement('div'); controlPanel.id = PANEL_ID; controlPanel.style.display = 'none'; progressBarContainer = document.createElement('div'); progressBarContainer.className = 'progress-bar-container'; progressTrack = document.createElement('div'); progressTrack.className = 'progress-track'; progressBuffer = document.createElement('div'); progressBuffer.className = 'progress-buffer'; progressFill = document.createElement('div'); progressFill.className = 'progress-fill'; progressThumb = document.createElement('div'); progressThumb.className = 'progress-thumb'; progressTrack.appendChild(progressBuffer); progressTrack.appendChild(progressFill); progressTrack.appendChild(progressThumb); progressBarContainer.appendChild(progressTrack); controlPanel.appendChild(progressBarContainer); progressBarContainer.addEventListener('click', handleProgressBarClick); progressBarContainer.addEventListener('mousedown', handleDragStart); const controlsRow = document.createElement('div'); controlsRow.className = 'controls-row'; const controlsLeft = document.createElement('div'); controlsLeft.className = 'controls-left'; playPauseButton = document.createElement('button'); playPauseButton.title = "播放/暂停"; playPauseButton.addEventListener('click', handlePlayPause); controlsLeft.appendChild(playPauseButton); const skipBackButton = document.createElement('button'); skipBackButton.innerHTML = `<span class="skip-back-icon"></span>`; skipBackButton.title = `后退 ${SKIP_AMOUNT} 秒`; skipBackButton.addEventListener('click', handleSkipBack); controlsLeft.appendChild(skipBackButton); const skipForwardButton = document.createElement('button'); skipForwardButton.innerHTML = `<span class="skip-fwd-icon"></span>`; skipForwardButton.title = `快进 ${SKIP_AMOUNT} 秒`; skipForwardButton.addEventListener('click', handleSkipForward); controlsLeft.appendChild(skipForwardButton); const controlsCenter = document.createElement('div'); controlsCenter.className = 'controls-center'; progressPercentDisplay = document.createElement('span'); progressPercentDisplay.className = 'progress-percent-display'; progressPercentDisplay.textContent = '0%'; controlsCenter.appendChild(progressPercentDisplay); const speedContainer = document.createElement('div'); speedContainer.className = 'speed-controls'; const speedLabel = document.createElement('span'); speedLabel.textContent = '速度:'; speedContainer.appendChild(speedLabel); SPEEDS.forEach(speed => { const speedButton = document.createElement('button'); speedButton.textContent = `${speed}x`; speedButton.dataset.speed = speed; speedButton.addEventListener('click', handleSpeedChange); if (speed === 0.1) { speedButton.classList.add('active'); activeSpeedButton = speedButton; } speedContainer.appendChild(speedButton); }); controlsCenter.appendChild(speedContainer); const jumpGroupContainer = document.createElement('div'); jumpGroupContainer.className = 'jump-controls-group'; FIXED_JUMP_PERCENTAGES.forEach(jump => { const jumpButton = document.createElement('button'); jumpButton.textContent = jump.label; jumpButton.dataset.percent = jump.value; jumpButton.addEventListener('click', () => jumpToPercentAndPause(jump.value)); jumpGroupContainer.appendChild(jumpButton); }); controlsCenter.appendChild(jumpGroupContainer); const controlsRight = document.createElement('div'); controlsRight.className = 'controls-right'; const timeContainer = document.createElement('div'); timeContainer.className = 'time-display'; currentTimeDisplay = document.createElement('span'); durationDisplay = document.createElement('span'); timeContainer.appendChild(currentTimeDisplay); timeContainer.appendChild(document.createTextNode(' / ')); timeContainer.appendChild(durationDisplay); controlsRight.appendChild(timeContainer); pinPanelButton = document.createElement('button'); pinPanelButton.className = 'pin-panel-button'; pinPanelButton.innerHTML = '📌'; pinPanelButton.addEventListener('click', togglePinPanel); controlsRight.appendChild(pinPanelButton); deployButton = document.createElement('button'); deployButton.className = 'deploy-button'; deployButton.innerHTML = '<span class="deploy-icon"></span> 一键部署'; deployButton.addEventListener('click', handleOneClickDeploy); controlsRight.appendChild(deployButton); stopDeployButton = document.createElement('button'); stopDeployButton.className = 'stop-deploy-button'; stopDeployButton.innerHTML = '停止部署'; stopDeployButton.addEventListener('click', toggleDeployPause); controlsRight.appendChild(stopDeployButton); muteButton = document.createElement('button'); muteButton.innerHTML = '<span class="mute-icon"></span>'; muteButton.addEventListener('click', handleMuteToggle); controlsRight.appendChild(muteButton); dragButton = document.createElement('button'); dragButton.className = 'drag-button'; dragButton.addEventListener('mousedown', startDraggingPanel); controlsRight.appendChild(dragButton); controlsRow.appendChild(controlsLeft); controlsRow.appendChild(controlsCenter); controlsRow.appendChild(controlsRight); controlPanel.appendChild(controlsRow); document.body.appendChild(controlPanel); updatePinButtonState(); log("控制面板已创建 (v3.4)"); } function formatTime(seconds) { if (isNaN(seconds) || !isFinite(seconds)) return "--:--"; const date = new Date(0); date.setSeconds(Math.round(seconds)); return date.toISOString().substr(11, 8).startsWith('00:') ? date.toISOString().substr(14, 5) : date.toISOString().substr(11, 8); } function updateProgressAndTimers() { if (!currentVideo || !controlPanel) return; const currentTime = currentVideo.currentTime; const duration = currentVideo.duration; currentTimeDisplay.textContent = formatTime(currentTime); durationDisplay.textContent = formatTime(duration); if (duration && isFinite(duration)) { const progressPercent = (currentTime / duration) * 100; progressFill.style.width = `${progressPercent}%`; progressThumb.style.left = `${progressPercent}%`; progressBuffer.style.width = `${calculateBufferPercent(currentVideo, currentTime, duration)}%`; progressPercentDisplay.textContent = `${Math.round(progressPercent)}%`; const remainingTime = duration - currentTime; if (remainingTime <= AUTO_SLOWDOWN_THRESHOLD && !currentVideo.paused && !window[AUTO_SLOWDOWN_FLAG]) { originalSpeedBeforeSlowdown = currentVideo.playbackRate; setSafeAction(true); currentVideo.playbackRate = SLOWDOWN_RATE; setSafeAction(false); window[AUTO_SLOWDOWN_FLAG] = true; updateControlState(currentVideo); } else if (remainingTime > AUTO_SLOWDOWN_THRESHOLD && window[AUTO_SLOWDOWN_FLAG]) { restoreOriginalSpeed(); } } } function calculateBufferPercent(video, currentTime, duration) { let bufferedEnd = currentTime; if (video.buffered && video.buffered.length > 0) { for (let i = 0; i < video.buffered.length; i++) { if (video.buffered.start(i) <= currentTime && video.buffered.end(i) > currentTime) { bufferedEnd = video.buffered.end(i); break; } } } return (bufferedEnd / duration) * 100; } function restoreOriginalSpeed() { if (window[AUTO_SLOWDOWN_FLAG] && currentVideo) { const speedToRestore = originalSpeedBeforeSlowdown || 0.1; setSafeAction(true); currentVideo.playbackRate = speedToRestore; setSafeAction(false); window[AUTO_SLOWDOWN_FLAG] = false; originalSpeedBeforeSlowdown = null; updateControlState(currentVideo); } } function updateControlState(video) { if (!controlPanel || !video) return; playPauseButton.innerHTML = video.paused ? '<span class="play-icon"></span>' : '<span class="pause-icon"></span>'; playPauseButton.title = video.paused ? "播放" : "暂停"; const currentRate = video.playbackRate; controlPanel.querySelectorAll('.speed-controls button').forEach(btn => { btn.classList.toggle('active', Math.abs(parseFloat(btn.dataset.speed) - currentRate) < 0.01); }); muteButton.innerHTML = video.muted ? '<span class="mute-icon"></span>' : '<span class="unmute-icon"></span>'; muteButton.title = video.muted ? "取消静音" : "静音"; updateProgressAndTimers(); if (!video.paused && !timeUpdateInterval) { timeUpdateInterval = setInterval(updateProgressAndTimers, 250); } else if (video.paused && timeUpdateInterval) { clearInterval(timeUpdateInterval); timeUpdateInterval = null; } } function showControls(video) { cancelHide(); if (!controlPanel) createControlPanel(); currentVideo = video; controlPanel.style.display = 'flex'; controlPanel.classList.add('visible'); updateControlState(video); } function scheduleHide() { cancelHide(); if (!window[PIN_PANEL_FLAG] && !isDraggingPanel && !isDraggingProgress) { hideTimeout = setTimeout(() => { if (controlPanel) controlPanel.classList.remove('visible'); }, HIDE_DELAY); } } function cancelHide() { if (hideTimeout) clearTimeout(hideTimeout); hideTimeout = null; } function handlePlayPause(e) { e.stopPropagation(); if (!currentVideo) return; setSafeAction(true); if (currentVideo.paused) { currentVideo.play(); } else { currentVideo.pause(); } setSafeAction(false); updateControlState(currentVideo); } function resetAutoSlowdownIfNeeded() { if (!currentVideo || !currentVideo.duration || !isFinite(currentVideo.duration)) return; const remainingTime = currentVideo.duration - currentVideo.currentTime; if (remainingTime > AUTO_SLOWDOWN_THRESHOLD && window[AUTO_SLOWDOWN_FLAG]) { restoreOriginalSpeed(); } } function handleSkipBack(e) { e.stopPropagation(); if (!currentVideo || !currentVideo.duration) return; setSafeAction(true); currentVideo.currentTime = Math.max(0, currentVideo.currentTime - SKIP_AMOUNT); setSafeAction(false); resetAutoSlowdownIfNeeded(); updateProgressAndTimers(); } function handleSkipForward(e) { e.stopPropagation(); if (!currentVideo || !currentVideo.duration) return; setSafeAction(true); currentVideo.currentTime = Math.min(currentVideo.duration, currentVideo.currentTime + SKIP_AMOUNT); setSafeAction(false); resetAutoSlowdownIfNeeded(); updateProgressAndTimers(); } function handleSpeedChange(e) { e.stopPropagation(); if (!currentVideo || isDeploying) return; const speed = parseFloat(e.target.dataset.speed); setSafeAction(true); currentVideo.playbackRate = speed; setSafeAction(false); allowPauseAtSlowSpeed = speed <= 0.5; if (!allowPauseAtSlowSpeed && currentVideo.paused) { setSafeAction(true); currentVideo.play(); setSafeAction(false); } if (window[AUTO_SLOWDOWN_FLAG]) { window[AUTO_SLOWDOWN_FLAG] = false; originalSpeedBeforeSlowdown = null; } updateControlState(currentVideo); } function handleMuteToggle(e) { e.stopPropagation(); if (!currentVideo) return; currentVideo.muted = !currentVideo.muted; updateControlState(currentVideo); } async function handleOneClickDeploy(e) { e.stopPropagation(); if (!currentVideo || isDeploying) return; if (!isUpdateButtonPresent()) { showAutoDismissAlert("请等待“更新进度”按钮出现后再部署!"); return; } isDeploying = true; isDeploymentPaused = false; let currentItem = getActiveItem(); if (!currentItem) { isDeploying = false; log("未找到活动项目,部署中止"); return; } const startItem = currentItem; let passesCompleted = 0; log("开始一键部署"); while (passesCompleted < 2 && isDeploying) { let currentPassItem = currentItem; log(`第 ${passesCompleted + 1} 轮部署开始,当前项目: ${currentPassItem.textContent.trim().substring(0, 10)}...`); while (isDeploying) { if (isDeploymentPaused) { log("部署暂停,等待继续"); await waitForResume(); if (!isDeploying) break; // 如果在暂停期间取消部署,直接退出 log("部署继续"); } if (isVideoStudied(currentPassItem)) { const nextElement = currentPassItem.nextElementSibling; if (nextElement && nextElement.classList.contains('item')) { currentPassItem = nextElement; nextElement.click(); log(`跳过已学习视频,切换至: ${nextElement.textContent.trim().substring(0, 10)}...`); await waitForVideoLoad(); continue; } else { log("没有更多视频可切换,结束当前轮次"); break; } } setSafeAction(true); currentVideo.playbackRate = 0.1; setSafeAction(false); if (passesCompleted === 0) { jumpToPercentAndPause(99.9); await updateProgressOnDeploy(); } else { await updateProgressOnDeploy(); } checkForCancelButton(); const nextElement = currentPassItem.nextElementSibling; if (nextElement && nextElement.classList.contains('item')) { nextElement.click(); currentPassItem = nextElement; log(`切换至下一视频: ${nextElement.textContent.trim().substring(0, 10)}...`); await waitForVideoLoad(); } else { log("本轮部署完成,无下一视频"); break; } } passesCompleted++; if (passesCompleted < 2 && isDeploying) { startItem.click(); currentItem = startItem; log("返回起始项目,开始第二轮部署"); await waitForVideoLoad(); } else if (isDeploying) { showChapterEndNotification("🎉 本章已全部部署完成!"); log("部署完成"); } } isDeploying = false; log("一键部署结束"); } function toggleDeployPause(e) { e.stopPropagation(); if (!isDeploying) return; isDeploymentPaused = !isDeploymentPaused; stopDeployButton.className = isDeploymentPaused ? 'continue-deploy-button' : 'stop-deploy-button'; stopDeployButton.innerHTML = isDeploymentPaused ? '继续部署' : '停止部署'; log(isDeploymentPaused ? "部署已暂停" : "部署已恢复"); } function waitForResume() { return new Promise(resolve => { const checkResume = () => { if (!isDeploymentPaused || !isDeploying) { resolve(); } else { setTimeout(checkResume, 100); } }; checkResume(); }); } function waitForVideoLoad() { return new Promise(resolve => { const timeout = setTimeout(resolve, 2000); const checkVideo = () => { const newVideo = document.querySelector('video'); if (newVideo && newVideo !== currentVideo && newVideo.readyState >= 2) { currentVideo = newVideo; clearTimeout(timeout); resolve(); } else { setTimeout(checkVideo, 50); } }; checkVideo(); }); } function handleProgressBarClick(e) { if (!currentVideo || !currentVideo.duration || isDraggingProgress) return; e.stopPropagation(); const rect = progressTrack.getBoundingClientRect(); const seekRatio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); setSafeAction(true); currentVideo.currentTime = seekRatio * currentVideo.duration; setSafeAction(false); resetAutoSlowdownIfNeeded(); updateProgressAndTimers(); } function handleDragStart(e) { if (!currentVideo || !currentVideo.duration) return; e.stopPropagation(); cancelHide(); isDraggingProgress = true; wasPausedBeforeDrag = currentVideo.paused; if (timeUpdateInterval) clearInterval(timeUpdateInterval); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); handleDragMove(e); } function handleDragMove(e) { if (!isDraggingProgress || !currentVideo) return; e.preventDefault(); const rect = progressTrack.getBoundingClientRect(); const seekRatio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const seekTime = seekRatio * currentVideo.duration; progressFill.style.width = `${seekRatio * 100}%`; progressThumb.style.left = `${seekRatio * 100}%`; currentTimeDisplay.textContent = formatTime(seekTime); } async function handleDragEnd(e) { if (!isDraggingProgress || !currentVideo) return; e.stopPropagation(); document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); const rect = progressTrack.getBoundingClientRect(); const seekRatio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); setSafeAction(true); currentVideo.currentTime = seekRatio * currentVideo.duration; currentVideo.pause(); setSafeAction(false); resetAutoSlowdownIfNeeded(); isDraggingProgress = false; updateProgressAndTimers(); if (isUpdateButtonPresent()) { await updateProgressOnDeploy(); log("进度条拖动后已更新进度"); } scheduleHide(); } async function jumpToPercentAndPause(percent) { if (!currentVideo || !currentVideo.duration) return; const seekTime = percent >= 99.99 ? currentVideo.duration - 0.1 : (percent / 100) * currentVideo.duration; setSafeAction(true); currentVideo.currentTime = seekTime; currentVideo.pause(); setSafeAction(false); resetAutoSlowdownIfNeeded(); updateControlState(currentVideo); if (isUpdateButtonPresent()) { await updateProgressOnDeploy(); log(`跳转至 ${percent}% 后已更新进度`); } } function updatePinButtonState() { if (!pinPanelButton) return; pinPanelButton.classList.toggle('pinned', window[PIN_PANEL_FLAG]); } function togglePinPanel(e) { e.stopPropagation(); window[PIN_PANEL_FLAG] = !window[PIN_PANEL_FLAG]; updatePinButtonState(); if (!window[PIN_PANEL_FLAG]) scheduleHide(); else cancelHide(); } function startDraggingPanel(e) { e.preventDefault(); isDraggingPanel = true; dragStartX = e.clientX - (parseInt(controlPanel.style.left) || window.innerWidth / 2 - controlPanel.offsetWidth / 2); dragStartY = e.clientY - (window.innerHeight - (parseInt(controlPanel.style.bottom) || 20) - controlPanel.offsetHeight); document.addEventListener('mousemove', dragPanel); document.addEventListener('mouseup', stopDraggingPanel); controlPanel.style.transition = 'none'; } function dragPanel(e) { if (!isDraggingPanel) return; e.preventDefault(); const x = e.clientX - dragStartX; const y = window.innerHeight - (e.clientY - dragStartY) - controlPanel.offsetHeight; controlPanel.style.left = `${x}px`; controlPanel.style.bottom = `${y}px`; controlPanel.style.transform = 'none'; } function stopDraggingPanel() { isDraggingPanel = false; document.removeEventListener('mousemove', dragPanel); document.removeEventListener('mouseup', stopDraggingPanel); controlPanel.style.transition = 'opacity 0.3s ease'; scheduleHide(); } function isValidVideo(video) { return video.offsetWidth >= MIN_VIDEO_SIZE && video.offsetHeight >= MIN_VIDEO_SIZE && !video._hasListener_v13; } function attachListenersToVideo(video) { if (!isValidVideo(video)) return; video.addEventListener('mouseenter', () => showControls(video)); video.addEventListener('mouseleave', scheduleHide); video.addEventListener('play', () => showControls(video)); video.addEventListener('pause', () => updateControlState(video)); video.addEventListener('ratechange', () => updateControlState(video)); video.addEventListener('loadedmetadata', () => { video.muted = true; setSafeAction(true); video.playbackRate = 0.1; setSafeAction(false); showControls(video); }); video.addEventListener('seeked', updateProgressAndTimers); video.addEventListener('progress', updateProgressAndTimers); video._hasListener_v13 = true; } function scanForVideos() { document.querySelectorAll('video').forEach(attachListenersToVideo); } createControlPanel(); if (document.readyState === 'complete' || document.readyState === 'interactive') { scanForVideos(); } else { document.addEventListener('DOMContentLoaded', scanForVideos); } const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'VIDEO') attachListenersToVideo(node); else node.querySelectorAll('video').forEach(attachListenersToVideo); } }); checkForCancelButton(); }); }); observer.observe(document.body, { childList: true, subtree: true }); log("通用视频控制器 (v3.4 - UI修复、人脸拦截、跳转后更新、修复暂停/继续) 已加载"); })();