Add persistent full-course mute option
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
// ==UserScript==
|
||||
// @name 学员中心 · 2倍速学习助手
|
||||
// @namespace local.icampus.study
|
||||
// @version 1.4.0
|
||||
// @description 支持切换标签页继续2倍速播放与保存进度,自动测验及错题重试。
|
||||
// @version 1.4.1
|
||||
// @description 支持全过程静音、切换标签页继续2倍速播放与保存进度,自动测验及错题重试。
|
||||
// @match https://icampus.yunyinsh.com/course/*
|
||||
// @run-at document-start
|
||||
// @grant none
|
||||
@@ -147,6 +147,10 @@
|
||||
const BACKGROUND_KEY = 'icampus.study.v1.background';
|
||||
const INTENT_KEY = 'icampus.study.v1.enabled';
|
||||
const MUTED_KEY = 'icampus.study.v1.mutedFallback';
|
||||
const FULL_MUTED_KEY = 'icampus.study.v1.fullMuted';
|
||||
let fullMutedEnabled = false;
|
||||
try { fullMutedEnabled = localStorage.getItem(FULL_MUTED_KEY) === 'on'; } catch {}
|
||||
const mutedPlayers = new Map();
|
||||
let mutedFallbackEnabled = true;
|
||||
try { mutedFallbackEnabled = localStorage.getItem(MUTED_KEY) !== 'off'; } catch {}
|
||||
let armed = false;
|
||||
@@ -191,6 +195,39 @@
|
||||
const logs = [];
|
||||
const visible = el => !!el && el.getClientRects().length > 0;
|
||||
const currentVideo = () => document.querySelector('video.player__video');
|
||||
function applyFullMute(video=currentVideo()) {
|
||||
if(!fullMutedEnabled || !video) return;
|
||||
if(!mutedPlayers.has(video)) mutedPlayers.set(video,{muted:!!video.muted,defaultMuted:!!video.defaultMuted});
|
||||
if(!video.defaultMuted) video.defaultMuted=true;
|
||||
if(!video.muted) video.muted=true;
|
||||
// Detached players cannot emit sound; bound the references across course changes.
|
||||
for(const player of mutedPlayers.keys()) if(player!==video && player.isConnected===false) mutedPlayers.delete(player);
|
||||
}
|
||||
function setFullMuted(enabled) {
|
||||
fullMutedEnabled=!!enabled;
|
||||
try { localStorage.setItem(FULL_MUTED_KEY,fullMutedEnabled?'on':'off'); } catch {}
|
||||
if(fullMutedEnabled) {
|
||||
applyFullMute();
|
||||
if(waiting) {waiting.passive=false;waiting.nextAt=0;}
|
||||
} else {
|
||||
for(const [video,previous] of mutedPlayers) {
|
||||
video.defaultMuted=previous.defaultMuted; video.muted=previous.muted;
|
||||
}
|
||||
mutedPlayers.clear();
|
||||
}
|
||||
log(fullMutedEnabled?'已开启全过程静音,切换课程和视频后继续保持静音。':'已关闭全过程静音,恢复播放器原来的静音状态。');
|
||||
render();
|
||||
}
|
||||
function onMediaSound(event) {
|
||||
const video=event.target;
|
||||
if(video && (video===currentVideo() || video.matches?.('video.player__video'))) applyFullMute(video);
|
||||
}
|
||||
for(const event of ['loadstart','loadedmetadata','play','volumechange']) document.addEventListener(event,onMediaSound,true);
|
||||
if(typeof MutationObserver!=='undefined') {
|
||||
const muteObserver=new MutationObserver(()=>applyFullMute());
|
||||
muteObserver.observe(document,{childList:true,subtree:true,attributes:true,attributeFilter:['class']});
|
||||
}
|
||||
applyFullMute();
|
||||
function log(message) {
|
||||
logs.push({time: new Date().toLocaleTimeString(), message: String(message).replace(/https?:\/\/\S+/g, '[链接]').slice(0, 300)});
|
||||
if (logs.length > 60) logs.shift();
|
||||
@@ -685,6 +722,7 @@
|
||||
}
|
||||
function attemptPlay(a, run=runNumber) {
|
||||
if(!running || active!==a || a.playPending || a.attempts>=3 || Date.now()<a.nextPlayAt) return;
|
||||
applyFullMute(a.video);
|
||||
a.playPending=true; ++a.attempts; a.nextPlayAt=Date.now()+2000;
|
||||
const failed=error => {
|
||||
if(!running || run!==runNumber || active!==a) return;
|
||||
@@ -695,7 +733,7 @@
|
||||
log('浏览器限制有声自动播放,尝试静音播放;可在播放器中恢复声音。');
|
||||
return attemptPlay(a,run);
|
||||
}
|
||||
if(a.mutedFallbackTried) a.video.muted=false;
|
||||
if(a.mutedFallbackTried && !fullMutedEnabled) a.video.muted=false;
|
||||
return park('浏览器阻止自动播放,请手动点击网页“点击播放”,助手会自动接续;刷新不能保证解除此限制。',{passive:true});
|
||||
}
|
||||
if(error?.name!=='AbortError') recoverMedia(a.courseId,a.id,'播放启动失败');
|
||||
@@ -732,6 +770,7 @@
|
||||
return park(`超过${saveTimeout/1000}秒未收到平台保存确认,已暂停,避免继续播放却未计入进度。`);
|
||||
}
|
||||
async function tick() {
|
||||
applyFullMute();
|
||||
if(busy || !armed) return;
|
||||
if(!running) {
|
||||
if(!waiting) return;
|
||||
@@ -765,6 +804,7 @@
|
||||
}
|
||||
function render() {
|
||||
if (!panel) return;
|
||||
panel.querySelector('[data-full-muted]').checked=fullMutedEnabled;
|
||||
panel.querySelector('[data-status]').textContent = status;
|
||||
const v = active?.video;
|
||||
const course = details.get(route(location.hash).id)?.course;
|
||||
@@ -807,13 +847,14 @@
|
||||
host.style.maxHeight='calc(100vh - 32px)'; host.style.overflowY='auto';
|
||||
panel = host.attachShadow({mode: 'open'});
|
||||
panel.innerHTML = `<style>:host{font:14px/1.6 system-ui;color:#eaf0fa}section{width:290px;background:#17243a;border:1px solid #6883a6;border-radius:12px;padding:15px;box-shadow:0 5px 24px #0005}strong{font-size:16px}p{margin:9px 0;white-space:pre-line}small{color:#b7c7df}button,select{font:inherit;border-radius:6px;border:1px solid #7186a5;padding:5px 9px;margin:3px 5px 3px 0}button{cursor:pointer;background:#dbeaff;color:#123}button:disabled{opacity:.5;cursor:default}select{width:100%;background:#fff;color:#123}[data-status]{min-height:45px}details{margin-top:8px}pre{white-space:pre-wrap;max-height:160px;overflow:auto;font-size:12px}</style>
|
||||
<section><strong>2倍速学习助手 · 1.4.0</strong><p data-status></p><small data-progress></small>
|
||||
<section><strong>2倍速学习助手 · 1.4.1</strong><p data-status></p><small data-progress></small>
|
||||
<p data-overall></p><progress data-overall-bar aria-label="整体课程完成百分比" max="100" value="0" style="width:100%"></progress>
|
||||
<p data-estimate style="font-size:12px;color:#b7c7df"></p>
|
||||
<select aria-label="播放范围"><option value="all">按列表顺序继续课程</option><option value="current">仅当前课程</option></select>
|
||||
<div><button data-start>开始</button><button data-stop>人工取消</button></div>
|
||||
<label><input type="checkbox" data-quiz checked> 自动测验,按平台合格线通过</label>
|
||||
<div><label><input type="checkbox" data-background> 切标签页继续播放</label></div>
|
||||
<div><label><input type="checkbox" data-full-muted> 全过程静音</label></div>
|
||||
<div><label><input type="checkbox" data-muted> 播放受限时尝试静音</label></div>
|
||||
<small data-background-state></small>
|
||||
<div><button data-quiz-only>只做本门测验</button></div>
|
||||
@@ -824,6 +865,9 @@
|
||||
const backgroundToggle=panel.querySelector('[data-background]');
|
||||
backgroundToggle.checked=backgroundEnabled;
|
||||
backgroundToggle.onchange=()=>setBackgroundEnabled(backgroundToggle.checked);
|
||||
const fullMutedToggle=panel.querySelector('[data-full-muted]');
|
||||
fullMutedToggle.checked=fullMutedEnabled;
|
||||
fullMutedToggle.onchange=()=>setFullMuted(fullMutedToggle.checked);
|
||||
const mutedToggle=panel.querySelector('[data-muted]');
|
||||
mutedToggle.checked=mutedFallbackEnabled;
|
||||
mutedToggle.onchange=()=>{
|
||||
|
||||
Reference in New Issue
Block a user