// ==UserScript==
// @name 115调试 - 稳定版通用视频控制器 (Netflix 风格 + UI修复 + 人脸拦截 + 跳转后更新 + 修复暂停/继续 + 加载时双重更新 + 99%跳转 + 图标修复)
// @namespace http://tampermonkey.net/
// @version 3.5_zh-CN_fixed_deploy_pause_resume_double_update_99percent_icons
// @description 为 HTML5 视频添加 Netflix 风格悬浮控件,默认播放速度0.1x,修复UI显示问题,支持一键部署(快速跳转至99%、暂停、更新进度、自动取消弹窗按新逻辑、跳过已学习视频),停止/继续部署,拖动控件,拦截人脸识别,跳转后自动更新进度,修复停止/继续部署问题,每次加载视频时更新两次进度,跳转目标调整为99%,修复按钮图标显示。
// @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%', value: 99 }
];
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);
const DOUBLE_UPDATE_DELAY = 100; // 双重更新之间的延迟(毫秒)
// --- 状态变量 ---
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; display: flex; align-items: center; justify-content: center; }
#${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-button::before { content: '▶'; font-size: 12px; }
#${PANEL_ID} .pause-button::before { content: '❚❚'; font-size: 12px; }
#${PANEL_ID} .skip-back-button::before { content: '⏪'; font-size: 14px; }
#${PANEL_ID} .skip-forward-button::before { content: '⏩'; font-size: 14px; }
#${PANEL_ID} .mute-button::before { content: '🔇'; font-size: 14px; }
#${PANEL_ID} .unmute-button::before { content: '🔊'; font-size: 14px; }
#${PANEL_ID} .deploy-button::before { content: '🚀'; font-size: 14px; margin-right: 4px; }
#${PANEL_ID} .stop-deploy-button::before { content: '⏹'; font-size: 12px; margin-right: 4px; }
#${PANEL_ID} .continue-deploy-button::before { content: '▶'; font-size: 12px; margin-right: 4px; }
#${PANEL_ID} .pin-panel-button::before { content: '📌'; font-size: 14px; }
.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} 次 (${actionType})`);
}, i * 50);
}
}
});
}
async function doubleUpdateProgress(actionType) {
if (isUpdateButtonPresent()) {
updateProgress(`${actionType} - 第一次`);
await new Promise(resolve => setTimeout(resolve, UPDATE_CLICK_COUNT * 50 + DOUBLE_UPDATE_DELAY));
updateProgress(`${actionType} - 第二次`);
await new Promise(resolve => setTimeout(resolve, UPDATE_CLICK_COUNT * 50 + 50));
} else {
log(`未找到“${UPDATE_TEXT}”按钮,跳过${actionType}双重更新`);
}
}
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.className = 'play-button';
playPauseButton.title = "播放/暂停";
playPauseButton.addEventListener('click', handlePlayPause);
controlsLeft.appendChild(playPauseButton);
const skipBackButton = document.createElement('button');
skipBackButton.className = 'skip-back-button';
skipBackButton.title = `后退 ${SKIP_AMOUNT} 秒`;
skipBackButton.addEventListener('click', handleSkipBack);
controlsLeft.appendChild(skipBackButton);
const skipForwardButton = document.createElement('button');
skipForwardButton.className = 'skip-forward-button';
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.title = '固定/取消固定面板';
pinPanelButton.addEventListener('click', togglePinPanel);
controlsRight.appendChild(pinPanelButton);
deployButton = document.createElement('button');
deployButton.className = 'deploy-button';
deployButton.innerHTML = '一键部署';
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.className = 'mute-button';
muteButton.title = '静音/取消静音';
muteButton.addEventListener('click', handleMuteToggle);
controlsRight.appendChild(muteButton);
dragButton = document.createElement('button');
dragButton.className = 'drag-button';
dragButton.title = '拖动面板';
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.5 - 图标修复)");
}
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.className = video.paused ? 'play-button' : 'pause-button';
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.className = video.muted ? 'mute-button' : 'unmute-button';
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);
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();
});
}
async 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);
log("新视频已加载,准备双重更新");
doubleUpdateProgress("视频加载完成").then(() => {
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 / 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;
}
async 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', async () => {
video.muted = true;
setSafeAction(true);
video.playbackRate = 0.1;
setSafeAction(false);
showControls(video);
await doubleUpdateProgress("视频元数据加载完成");
});
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.5 - UI修复、人脸拦截、跳转后更新、修复暂停/继续、加载时双重更新、99%跳转、图标修复) 已加载");
})();