Speed/vol, autoplay blocking, per-site isolation. Transparent overlay. Auto-hide. Seek duration.
// ==UserScript==
// @name Universal Video Controls PRO
// @namespace http://tampermonkey.net/
// @version 1.0.8
// @description Speed/vol, autoplay blocking, per-site isolation. Transparent overlay. Auto-hide. Seek duration.
// @author CLumsyKnight48
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @run-at document-idle
// @license MIT
// ==/UserScript==
/*
v1.0.8 — AUDIO / SPEED / BLOCKING / AUTO-HIDE
FIX 1 Audio glitching on replay
Boost.connect() was removing the video from the srcs WeakMap on the
'ended' event without disconnecting the WebAudio MediaElementSourceNode.
On replay Boost.has(v)=false so gain was never updated, but the now-
untracked node still fed into the graph at a stale level — crackling.
Fixed: the 'ended' listener is removed; the source stays live for replays.
Instead an 'emptied' listener disconnects & resets when the src changes
(element recycled) so a fresh source can be created for the new content.
FIX 2 Speed boost intermittently fails on X homepage
lockSpd() returns early when v.duration is NaN (stream not yet loaded),
marking the video as "live". The loadedmetadata retry requires
v===state.activeVideo which may have changed by then. Added short
[200 ms, 600 ms] retries in activate() for autoStop platforms, mirroring
the existing YouTube-specific retries.
FIX 3 Strict autoplay blocking inconsistent on X
Two related bugs. (A) _uvce_mp=true (set when a play is blocked) caused
shouldBlock() to return false for that element. If X re-played via MSE or
a non-prototype path, the play-event listener no longer blocked it — "blocked
once = never blocked again". Fixed: removed !v._uvce_mp from shouldBlock()
so re-plays are always evaluated. (B) The 5-second strict scan skipped
videos with _uvce_mp=true even when they were actively playing. Fixed:
removed the _uvce_mp guard from the scan. The scan is also now on its own
1.5-second interval (was every 5 s) so gaps are shorter.
FIX 4 VM.best() still used full isActuallyVisible (Reddit regression)
The Reddit fix in v2.5.x only removed isActuallyVisible from consider().
VM.best() still used it, so best() returned null for Reddit's lazy-loaded
videos that have offsetWidth/offsetHeight=0 at IO-fire time. Changed to
only filter elements where BOTH offsetWidth AND offsetHeight are 0
(truly invisible / not-rendered), which catches X's invisible pre-loaded
elements while allowing Reddit's normal-sized videos.
FIX 5 Dead _uvce_sp flag removed
_uvce_sp was checked in three places but never set anywhere. The dead
guards were making blocking conditions subtly wrong. Removed.
NEW Optional auto-hide panel (Settings → Display → Auto-hide)
When enabled the panel fades out after 3 seconds with no mouse activity.
Moving the mouse back over the panel (or over the dot) restores it.
Uses the existing userClosed/dot mechanism so it integrates with showMini.
Panel stays visible while settings overlay is open.
*/
(function () {
'use strict';
const UW = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const IS_TOP = window === window.top;
const HAS_GM = typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
const HAS_MC = typeof GM_registerMenuCommand === 'function';
let trustedHTML = s => s;
if (window.trustedTypes && window.trustedTypes.createPolicy) {
try { const p = window.trustedTypes.createPolicy('uvce-safe',{createHTML:s=>s}); trustedHTML=s=>p.createHTML(s); } catch(e){}
}
const RIG_ID = 'uvce_v17';
const GM_KEY = 'uvce_settings';
const LS_BACK = 'uvce_p17';
const Z_TOP = 2147483647;
const VOL_MAX = 6.0;
const RS_MIN = 0.3;
const RS_MAX = 3.0;
const AUTO_HIDE_DELAY = 3000; // ms
const getMaxVol = () => { if(!eff('volumeBoost'))return 1.0; if(PLAT.name==='reddit')return 1.0; return VOL_MAX; };
const SPD_EV = ['ratechange','canplay','canplaythrough','playing','loadedmetadata','seeked','emptied','loadstart'];
const IO_THR = [0,.05,.1,.2,.4,.6,.8,1];
const CORNERS = [{id:'TL',ar:'↖'},{id:'TR',ar:'↗'},{id:'BL',ar:'↙'},{id:'BR',ar:'↘'}];
const MAX_SPDS= [1.5,2,3,4,5,6,10];
const ISOLATABLE = new Set([
'speed','maxSpeed','seekStep','skipGifs',
'blockAutoplay','blockAutoplayStrict','autoUnmute','autoplay','noLoop','safeMode',
'volumeBoost','autoHide',
'hotkeys','hkSpace','hkSpeed','hkVol','hkMute','hkSeek',
'showSeek','showSpeed','showVol','showSafe','showMute','showMini',
'cleanCookies','cleanModals','cleanAds',
]);
const CLEAN = {
cookies:['#onetrust-banner-sdk','#onetrust-consent-sdk','#CybotCookiebotDialog','#CybotCookiebotDialogBodyUnderlay','#didomi-host','.didomi-popup','.cc-banner','.cc-window','[class*="cookie-banner"]','[id*="cookie-banner"]','[class*="cookie-consent"]','[class*="gdpr-banner"]'],
modals: ['[class*="paywall"]','[id*="paywall"]','[class*="subscribe-wall"]','[class*="newsletter-popup"]','[class*="tp-modal"]','[class*="tp-backdrop"]'],
ads: ['[id*="taboola"]','[class*="taboola"]','[id*="outbrain"]','[class*="outbrain"]','[class*="dfp-ad"]'],
};
const PLAT = (() => {
const h = location.hostname.replace(/^(www\.|m\.)/,'');
let n = 'generic';
if(h.includes('facebook.com')||h==='fb.watch') n='facebook';
else if(h.includes('instagram.com')) n='instagram';
else if(h.includes('twitter.com')||h.includes('x.com')) n='twitter';
else if(h.includes('youtube.com')) n='youtube';
else if(h.includes('tiktok.com')) n='tiktok';
else if(h.includes('twitch.tv')) n='twitch';
else if(h.includes('vimeo.com')) n='vimeo';
else if(h.includes('dailymotion.com')) n='dailymotion';
else if(h.includes('reddit.com')) n='reddit';
const autoStop = !['youtube','twitch','vimeo','dailymotion'].includes(n);
const aggMute = ['facebook','instagram','tiktok'].includes(n);
return { name:n, label:n.charAt(0).toUpperCase()+n.slice(1), autoStop, aggMute };
})();
const fmtTime = s => {
if(!isFinite(s)||s<0)return '--:--';
const m=Math.floor(s/60),sec=Math.floor(s%60).toString().padStart(2,'0');
return `${m}:${sec}`;
};
function volPosToGain(p){
const mx=getMaxVol(); if(mx===1.0)return Math.max(0,Math.min(1,p/100));
p=Math.max(0,Math.min(100,p)); if(p<=50)return p/50;
const b=(p-50)/50; return 1+b*b*(mx-1);
}
function volGainToPos(g){
const mx=getMaxVol(); if(mx===1.0)return Math.max(0,Math.min(100,g*100));
g=Math.max(0,Math.min(mx,g)); if(g<=1)return g*50;
return 50+Math.sqrt((g-1)/(mx-1))*50;
}
const DEFAULTS = {
speed:2.0, maxSpeed:3.0, volume:1.0,
safeMode:false, autoplay:true,
blockAutoplay:false, blockAutoplayStrict:false, autoUnmute:false,
noLoop:false, threshold:.2, corner:'BL',
uiScaleW:1.0, uiScaleH:1.0,
volumeBoost:true, verticalMode:false, autoHide:false,
showSeek:true, showSpeed:true, showVol:true, showSafe:true, showMute:true, showMini:true,
hotkeys:true, hkSpace:true, hkSpeed:true, hkVol:true, hkMute:true, hkSeek:true,
seekStep:5, skipGifs:true,
cleanCookies:false, cleanModals:false, cleanAds:false,
};
const Store = (() => {
const read=()=>{if(HAS_GM){try{const r=GM_getValue(GM_KEY,'');if(r)return r;}catch{}}try{return localStorage.getItem(LS_BACK)||'';}catch{}return '';};
const write=j=>{if(HAS_GM){try{GM_setValue(GM_KEY,j);}catch{}}try{localStorage.setItem(LS_BACK,j);}catch{}};
return {
load(){try{const d=Object.assign({},DEFAULTS,JSON.parse(read()));if(d.uiScale!=null&&d.uiScaleW===1.0)d.uiScaleW=d.uiScaleH=Math.max(RS_MIN,Math.min(RS_MAX,d.uiScale));return d;}catch{return{...DEFAULTS};}},
save(s){const d={};for(const k of Object.keys(DEFAULTS))if(k in s&&k!=='volume')d[k]=s[k];write(JSON.stringify(d));},
};
})();
const Site = (() => {
const host=location.hostname.replace(/^(www\.|m\.)/,'');
const lsKey='uvce_s2_'+host, gmKey='uvce_site_'+host;
let gm={}; if(HAS_GM){try{gm=JSON.parse(GM_getValue(gmKey,'{}')||'{}');}catch{}}
let ls={}; try{ls=JSON.parse(localStorage.getItem(lsKey)||'{}');}catch{}
let c={...gm,...ls};
const flush=()=>{const j=JSON.stringify(c),e=!Object.keys(c).length;try{e?localStorage.removeItem(lsKey):localStorage.setItem(lsKey,j);}catch{}if(HAS_GM){try{GM_setValue(gmKey,e?'{}':j);}catch{}}};
return {host,get:k=>k in c?c[k]:null,set(k,v){v===null?delete c[k]:(c[k]=v);flush();}};
})();
const state={...Store.load(),activeVideo:null,userClosed:false,globalUnmuted:false,_lsbr:null,_sptog:false};
state.volume=1.0;
if(Site.get('useOwn')===true)for(const k of ISOLATABLE){const v=Site.get(k);if(v!==null)state[k]=v;}
const isDisabled=()=>Site.get('disable')===true;
const isIsolated=()=>Site.get('useOwn')===true;
const eff=k=>{if(isIsolated()){const v=Site.get(k);if(v!==null)return v;}return state[k];};
function saveSetting(key){
if(key==='volume')return;
if(isIsolated()&&ISOLATABLE.has(key)){Site.set(key,state[key]);}
else{if(isIsolated()){const gs=Store.load();if(key in DEFAULTS){gs[key]=state[key];Store.save(gs);}}else{Store.save(state);}}
}
let _lastInteract=0;
document.addEventListener('click', e=>{if(e.isTrusted)_lastInteract=Date.now();},true);
document.addEventListener('keydown',e=>{if(e.isTrusted&&' Enter'.includes(e.key))_lastInteract=Date.now();},true);
const isFeedFresh=()=>Date.now()-_lastInteract<100;
const isFresh =()=>Date.now()-_lastInteract<500;
const isVeryFresh=()=>Date.now()-_lastInteract<300;
let _selfPaused=null;
const markSelfPaused =v=>{_selfPaused=v;_activelyApproved.delete(v);};
const clearSelfPaused=()=>{_selfPaused=null;};
const _approvedPlays=new WeakSet();
// Persistent consent set — a video lives here while playing with user consent.
// Cleared by markSelfPaused (block or user-pause) and on ended/emptied.
// The strict scan exempts videos in this set so it never re-pauses the
// video the user is actively watching.
const _activelyApproved=new WeakSet();
const approvePlay=v=>{if(v)_approvedPlays.add(v);};
const _lastSpeedPerVideo=new WeakMap();
const deb=(fn,ms)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);};};
// FIX 4: lighter visibility check — only filter both-zero-dimension elements.
// Full isActuallyVisible caused Reddit lazy-loaded videos to never activate.
// The offsetWidth&&offsetHeight combined check still catches X's invisible
// pre-loaded zero-size elements.
function isActuallyVisible(v){
if(!v||!v.isConnected)return false;
if(!v.offsetWidth&&!v.offsetHeight)return false; // both zero = not rendered
const r=v.getBoundingClientRect();
if(r.width<2||r.height<2)return false;
const left=Math.max(r.left,0),right=Math.min(r.right,innerWidth);
const top =Math.max(r.top, 0),bottom=Math.min(r.bottom,innerHeight);
return right>left && bottom>top; // any viewport overlap (removed 50px min)
}
function sealNoLoop(v){
if(v._uvce_ls)return; v._uvce_ls=true; v.loop=false;
try{v.removeAttribute('loop');}catch{}
try{
const GOPD=Object.getOwnPropertyDescriptor,orig=GOPD(HTMLMediaElement.prototype,'loop');
Object.defineProperty(v,'loop',{configurable:true,enumerable:true,get:()=>false,
set(){if(!state.noLoop){delete v._uvce_ls;try{if(orig)Object.defineProperty(v,'loop',orig);else delete v.loop;}catch{}}}});
}catch{}
try{const mo=new MutationObserver(()=>{if(state.noLoop&&v.hasAttribute('loop'))try{v.removeAttribute('loop');}catch{}});mo.observe(v,{attributes:true,attributeFilter:['loop']});v._uvce_lmo=mo;}catch{}
if(!v._uvce_nl_el){
v._uvce_nl_el=true;
v.addEventListener('ended',()=>{if(eff('noLoop'))v._uvce_nl_ended=Date.now();});
v.addEventListener('emptied',()=>{delete v._uvce_nl_ended;});
}
}
function unsealNoLoop(v){
if(!v._uvce_ls)return; delete v._uvce_ls;
if(v._uvce_lmo){v._uvce_lmo.disconnect();delete v._uvce_lmo;}
try{delete v.loop;}catch{}
}
function watchMute(v){
if(v._uvce_mw)return; v._uvce_mw=true;
let _guard=false;
v.addEventListener('volumechange',()=>{
if(_guard||!state.globalUnmuted)return;
_guard=true;
if(v.muted)v.muted=false;
if(v.volume<0.01)try{v.volume=1;}catch{}
_guard=false;
});
}
function watchAutoplayAttr(v){
if(v._uvce_aaw)return; v._uvce_aaw=true;
const strip=()=>{
if((eff('blockAutoplay')||eff('blockAutoplayStrict'))&&PLAT.autoStop&&v.hasAttribute('autoplay')){
v.autoplay=false; try{v.removeAttribute('autoplay');}catch{}
}
};
try{const mo=new MutationObserver(strip);mo.observe(v,{attributes:true,attributeFilter:['autoplay']});v._uvce_aamo=mo;}catch{}
}
function positionHost(host,id){
host.style.setProperty('position','fixed','important');
host.style.setProperty('z-index',String(Z_TOP),'important');
host.style.setProperty('pointer-events','none','important');
for(const s of['top','right','bottom','left'])host.style.setProperty(s,'auto','important');
const safe='max(10px,env(safe-area-inset-bottom,10px))';
if(id==='TL'){host.style.setProperty('top','10px','important');host.style.setProperty('left','10px','important');}
else if(id==='TR'){host.style.setProperty('top','10px','important');host.style.setProperty('right','10px','important');}
else if(id==='BL'){host.style.setProperty('bottom',safe,'important');host.style.setProperty('left','10px','important');}
else {host.style.setProperty('bottom',safe,'important');host.style.setProperty('right','10px','important');}
}
function antiInvert(host){
const chk=deb(()=>{try{const hf=getComputedStyle(document.documentElement).filter||'';const bf=document.body?(getComputedStyle(document.body).filter||''):'';host.style.setProperty('filter',(hf+bf).includes('invert')?'invert(1)':'none','important');}catch{}},300);
chk();
try{new MutationObserver(chk).observe(document.documentElement,{attributes:true,attributeFilter:['style','class']});}catch{}
try{if(document.body)new MutationObserver(chk).observe(document.body,{attributes:true,attributeFilter:['style','class']});}catch{}
}
const BlockAuto=(() => {
const orig=UW.HTMLMediaElement.prototype.play; let on=false;
return {
patch(){
if(on)return; on=true;
UW.HTMLMediaElement.prototype.play=function(...a){
if(eff('noLoop')&&PLAT.autoStop&&this._uvce_nl_ended&&Date.now()-this._uvce_nl_ended<3000&&!this._ok){
this._uvce_mp=true; return Promise.resolve();
}
const blocking=eff('blockAutoplay')||eff('blockAutoplayStrict');
if(!blocking){delete this._ok;return orig.apply(this,a);}
const fresh=PLAT.autoStop
?(!eff('blockAutoplayStrict')&&isFeedFresh())
:(isVeryFresh()||(!eff('blockAutoplayStrict')&&isFresh()));
const gif=this.loop&&isFinite(this.duration)&&this.duration<=4;
if(!this._ok&&!fresh&&!gif){this._uvce_mp=true;return Promise.resolve();}
delete this._ok; return orig.apply(this,a);
};
},
ok:v=>{if(v){v._ok=true;approvePlay(v);}},
};
})();
const Cleaner=(() => {
let mo=null,sel='';
const build=()=>[...(eff('cleanCookies')?CLEAN.cookies:[]),...(eff('cleanModals')?CLEAN.modals:[]),...(eff('cleanAds')?CLEAN.ads:[])].join(',');
const run=()=>{if(!sel)return;try{document.querySelectorAll(sel).forEach(e=>e.style.setProperty('display','none','important'));}catch{}if(eff('cleanModals')||eff('cleanCookies')){try{document.body?.style.removeProperty('overflow');document.documentElement.style.removeProperty('overflow');}catch{}}};
return{apply(){sel=build();if(!sel){mo?.disconnect();mo=null;return;}run();if(!mo){mo=new MutationObserver(run);mo.observe(document.documentElement,{childList:true,subtree:true});}}};
})();
const Boost=(() => {
let ctx=null,gain=null; const srcs=new WeakMap();
const init=()=>{if(ctx)return true;try{ctx=new(UW.AudioContext||UW.webkitAudioContext)();gain=ctx.createGain();gain.connect(ctx.destination);return true;}catch{return false;}};
return {
ensureResumed(){if(ctx&&ctx.state==='suspended')ctx.resume().catch(()=>{});},
// FIX 1: no 'ended' listener — source stays alive for replays so gain is
// always managed. 'emptied' (src change) disconnects and resets properly.
connect(v){
if(getMaxVol()===1.0||srcs.has(v)||v._uvce_bc||!init())return;
try{
if(v.src&&!v.src.startsWith(location.origin)&&v.readyState===0)v.crossOrigin='anonymous';
const s=ctx.createMediaElementSource(v);
s.connect(gain);
srcs.set(v,s);
v._uvce_bc=true;
// When the element is recycled for a new src, tear down cleanly
v.addEventListener('emptied',()=>{
const src=srcs.get(v);
if(src){try{src.disconnect();}catch{}srcs.delete(v);}
delete v._uvce_bc;
},{once:true});
}catch{}
},
setGain:g=>{if(getMaxVol()===1.0)return;if(gain)gain.gain.value=Math.max(0,Math.min(g,getMaxVol()));},
has:v=>srcs.has(v),
};
})();
const Eng=(() => {
const lockC=new WeakMap();
const clampSafe=v=>Math.min(.6,v);
const isGif=v=>eff('skipGifs')&&v.loop&&isFinite(v.duration)&&v.duration<=4&&!(v.audioTracks?.length>0&&!v.muted);
const isLive=v=>!isFinite(v.duration);
const isLocked=v=>lockC.has(v);
function applyVol(v,gainOverride){
if(!v)return;
if(!state.globalUnmuted&&gainOverride==null)return;
if(state.globalUnmuted&&v.muted)v.muted=false;
const target=gainOverride!=null?gainOverride:(eff('safeMode')?clampSafe(state.volume):state.volume);
const mx=getMaxVol(); const t=Math.min(target,mx);
if(Boost.has(v)){if(v.volume<.99)v.volume=1;Boost.setGain(t);}
else if(t>1){Boost.connect(v);if(Boost.has(v)){if(v.volume<.99)v.volume=1;Boost.setGain(t);}else{if(v.volume<.99)v.volume=1;}}
else{if(Math.abs(v.volume-t)>.005)v.volume=t;}
}
function lockSpd(v){
if(!v||isGif(v)||isLive(v)){unlockSpd(v);return;}
unlockSpd(v);
const ctrl=new AbortController(),sig=ctrl.signal; lockC.set(v,ctrl);
const apply=()=>{
if(!v.isConnected||!isFinite(v.duration))return;
const spd=eff('speed');
if(Math.abs(v.playbackRate-spd)>.01)try{v.defaultPlaybackRate=spd;v.playbackRate=spd;}catch{}
};
apply();
let debT=null;
const enf=()=>{clearTimeout(debT);debT=setTimeout(apply,30);};
SPD_EV.forEach(e=>v.addEventListener(e,enf,{signal:sig}));
}
function unlockSpd(v){const c=lockC.get(v);if(c){c.abort();lockC.delete(v);}}
function consider(v){
if(isDisabled())return;
if(!v.isConnected)return;
const w=VM.best(state.threshold);
if(!w||w!==v)return;
activate(v);
}
function activate(v){
if(isDisabled())return;
const isNewVideo=state.activeVideo!==v;
if(isNewVideo)state.userClosed=false;
if(state.activeVideo)unlockSpd(state.activeVideo);
state.activeVideo=v; state.globalUnmuted=false; clearSelfPaused();
_activelyApproved.add(v); // user's current video is always consented
if(_lastSpeedPerVideo.has(v))state.speed=_lastSpeedPerVideo.get(v);
else if(isIsolated()){for(const k of ISOLATABLE){const val=Site.get(k);if(val!==null)state[k]=val;}}
else{const gs=Store.load();state.speed=gs.speed??DEFAULTS.speed;for(const k of ISOLATABLE){if(k!=='speed'&&k in gs)state[k]=gs[k];}}
UI.refreshSpd();UI.refreshVol();UI.refreshChips();
if(state.noLoop)sealNoLoop(v);
if(eff('autoUnmute')){
state.globalUnmuted=true;
const doUnmute=()=>{
if(state.activeVideo!==v||!eff('autoUnmute'))return;
if(v.muted)v.muted=false;
if(v.volume<0.01)try{v.volume=1;}catch{}
};
doUnmute();
[50,150,300,600,1200].forEach(ms=>setTimeout(doUnmute,ms));
}
lockSpd(v); applyVol(v); UI.syncVis(); UI.kickRaf();
// FIX 2: speed retries for YouTube AND autoStop (X, Reddit etc.)
// Covers the case where duration=NaN at activation time
if(PLAT.name==='youtube'){
[500,1500,3000].forEach(ms=>setTimeout(()=>{if(state.activeVideo===v)lockSpd(v);},ms));
[80,250,600].forEach(ms=>setTimeout(()=>UI.syncVis(),ms));
} else if(PLAT.autoStop){
[200,600].forEach(ms=>setTimeout(()=>{if(state.activeVideo===v&&!isLocked(v))lockSpd(v);},ms));
}
if(eff('autoplay')&&v.paused){
if(!(PLAT.autoStop&&(eff('blockAutoplay')||eff('blockAutoplayStrict')))&&PLAT.name!=='youtube'){
v._uvce_mp=false; approvePlay(v); BlockAuto.ok(v); v.play().catch(()=>{});
}
}
}
return{applyVol,lockSpd,unlockSpd,consider,activate,isGif,isLive,isLocked,clampSafe};
})();
const VM=(() => {
const obs=new WeakSet(),ratios=new Map(); let io=null;
// FIX: X stores a reference to the native video.play() before our script
// runs, bypassing the prototype patch. X's internal retry loop fires the
// play event 30-60 times per second. Without debouncing, we called v.pause()
// on every single event → rapid play/pause → audio glitch + CPU spike.
// doPause() allows at most one actual pause() call per 300 ms per element.
// All block-state flags (_uvce_mp, markSelfPaused) are still set on every
// call so the blocking state is always current; only the expensive pause()
// call is rate-limited.
function doPause(v){
v._uvce_mp=true;
const now=Date.now();
if(v._uvce_pausedAt&&now-v._uvce_pausedAt<300){return;} // debounce
v._uvce_pausedAt=now;
markSelfPaused(v);
v.pause();
}
// FIX 3A: removed !v._uvce_mp — previously a blocked video was exempt from
// future blocking. X re-plays via MSE bypassing the prototype patch, but the
// play listener then skipped it because _uvce_mp was still true from the first
// block. Now every unapproved play is evaluated regardless.
function shouldBlock(v){
return (eff('blockAutoplay')||eff('blockAutoplayStrict'))&&PLAT.autoStop&&!_approvedPlays.has(v);
}
function proc(v){
if((eff('blockAutoplay')||eff('blockAutoplayStrict'))&&v.autoplay){v.autoplay=false;v.removeAttribute('autoplay');}
if(PLAT.autoStop)watchAutoplayAttr(v);
if(obs.has(v))return;
obs.add(v); io.observe(v);
if(state.noLoop)sealNoLoop(v);
if(PLAT.aggMute)watchMute(v);
if(shouldBlock(v)&&!v.paused&&!isFeedFresh()){doPause(v);}
v.addEventListener('play',()=>{
if(eff('noLoop')&&v._uvce_nl_ended&&Date.now()-v._uvce_nl_ended<3000){
doPause(v);return;
}
const wasApproved=_approvedPlays.has(v);
if(wasApproved)_approvedPlays.delete(v);
if(shouldBlock(v)&&!wasApproved){
const fresh=!eff('blockAutoplayStrict')&&isFeedFresh();
if(!fresh){doPause(v);return;}
}
// Play is allowed — mark as actively consented so the strict scan
// doesn't pause this video on its next tick.
_activelyApproved.add(v);
if(v!==state.activeVideo)Eng.consider(v);
});
v.addEventListener('loadedmetadata',()=>{if(v===state.activeVideo)Eng.lockSpd(v);});
// Clear active consent when the video ends or the element is recycled.
if(!v._uvce_aa_el){
v._uvce_aa_el=true;
v.addEventListener('ended', ()=>{_activelyApproved.delete(v);});
v.addEventListener('emptied',()=>{_activelyApproved.delete(v);});
}
}
function scan(){document.querySelectorAll('video').forEach(proc);}
function init(){
io=new IntersectionObserver(entries=>{
entries.forEach(e=>{
if(e.target.isConnected){ratios.set(e.target,e.intersectionRatio);if(e.intersectionRatio>=state.threshold)Eng.consider(e.target);}
else{ratios.delete(e.target);Eng.unlockSpd(e.target);}
});
},{threshold:IO_THR});
scan(); setInterval(scan,8000);
// General MO: microtask-batched (YouTube/Reddit safe)
const pending=new Set();
const flush=()=>{pending.forEach(v=>{if(v.isConnected)proc(v);});pending.clear();};
new MutationObserver(mutations=>{
let dirty=false;
for(const m of mutations)
for(const nd of m.addedNodes)
if(nd.nodeType===1&&nd.isConnected){
const tag=nd.tagName;
if(tag==='SCRIPT'||tag==='STYLE'||tag==='LINK'||tag==='META')continue;
if(tag==='VIDEO'){pending.add(nd);dirty=true;}
else{const vs=nd.querySelectorAll?.('video');if(vs?.length){vs.forEach(v=>pending.add(v));dirty=true;}}
}
if(dirty)queueMicrotask(flush);
}).observe(document.documentElement,{childList:true,subtree:true});
// Fast-block MO: synchronous, autoStop only
new MutationObserver(mutations=>{
if(!PLAT.autoStop)return;
const blocking=(eff('blockAutoplay')||eff('blockAutoplayStrict'));
if(!blocking&&!eff('noLoop'))return;
for(const m of mutations)
for(const nd of m.addedNodes)
if(nd.nodeType===1&&nd.isConnected){
const vids=nd.tagName==='VIDEO'?[nd]:[...nd.querySelectorAll?.('video')||[]];
vids.forEach(v=>{
if(v.autoplay){v.autoplay=false;v.removeAttribute('autoplay');}
proc(v);
if(blocking&&!v.paused&&!_approvedPlays.has(v)&&!_activelyApproved.has(v)&&!isFeedFresh()){
doPause(v);
}
});
}
}).observe(document.documentElement,{childList:true,subtree:true});
// Speed lock maintenance (5 s)
setInterval(()=>{
const v=state.activeVideo;
if(v&&!Eng.isGif(v)&&!Eng.isLive(v)&&!Eng.isLocked(v))Eng.lockSpd(v);
},5000);
// FIX 3B: strict scan on its own 1.5 s interval.
// _uvce_mp guard removed — a video that was blocked once but resumed
// (via MSE or recycled element) is now caught on the next tick.
setInterval(()=>{
if(!eff('blockAutoplayStrict')||!PLAT.autoStop)return;
document.querySelectorAll('video').forEach(vid=>{
if(vid.paused||_activelyApproved.has(vid))return;
vid._uvce_mp=true;markSelfPaused(vid);vid.pause();
});
},1500);
}
// FIX 4: replaced full isActuallyVisible with lighter check
const best=t=>{
let b=null,br=-1;
ratios.forEach((r,v)=>{
if(!v.isConnected)return;
if(!v.offsetWidth&&!v.offsetHeight)return; // both-zero = truly invisible
if(r>=t&&r>br){br=r;b=v;}
});
return b;
};
const ratio=v=>ratios.get(v)??0;
return{init,scan,best,ratio};
})();
function pauseVideo(v){if(!v||v.paused)return;markSelfPaused(v);v._uvce_mp=true;v.pause();}
function findTargetVideo(){
if(state.activeVideo&&VM.ratio(state.activeVideo)>=0.1&&isActuallyVisible(state.activeVideo))return state.activeVideo;
let bestV=null,bestArea=0;
document.querySelectorAll('video').forEach(v=>{
if(!isActuallyVisible(v))return;
const r=v.getBoundingClientRect();
const w=Math.min(r.right,innerWidth)-Math.max(r.left,0);
const h=Math.min(r.bottom,innerHeight)-Math.max(r.top,0);
if(w<=0||h<=0)return;
const a=w*h;if(a>bestArea){bestArea=a;bestV=v;}
});
return bestV||VM.best(0)||state.activeVideo;
}
const Keys={init(){
window.addEventListener('keydown',e=>{
if(e.ctrlKey&&e.shiftKey&&e.key.toLowerCase()==='u'){e.preventDefault();e.stopImmediatePropagation();state.userClosed=false;UI.syncVis();return;}
if(!eff('hotkeys'))return;
const tag=document.activeElement?.tagName;
if(tag==='INPUT'||tag==='TEXTAREA'||document.activeElement?.isContentEditable)return;
if(e.ctrlKey||e.metaKey||e.altKey)return;
if(e.key===' '&&eff('hkSpace')){
if(!PLAT.autoStop)return;
e.preventDefault();e.stopPropagation();e.stopImmediatePropagation();
if(state._sptog)return;
state._sptog=true;setTimeout(()=>state._sptog=false,120);
const av=findTargetVideo();if(!av)return;
if(av!==state.activeVideo)Eng.activate(av);
if(av.paused){clearSelfPaused();av._uvce_mp=false;approvePlay(av);BlockAuto.ok(av);av.play().catch(()=>{});}
else pauseVideo(av);
return;
}
const v=state.activeVideo;if(!v)return;
const setSpd=val=>{e.preventDefault();e.stopImmediatePropagation();state.speed=Math.min(eff('maxSpeed'),Math.max(.1,+val.toFixed(2)));if(v){v.playbackRate=state.speed;Eng.lockSpd(v);}UI.refreshSpd();saveSetting('speed');UI.toast(state.speed.toFixed(2)+'×','info',800);};
switch(e.key){
case 'r':case 'R':if(!eff('hkSpeed'))break;e.preventDefault();e.stopImmediatePropagation();if(Math.abs(state.speed-1.0)>0.01){state._lsbr=state.speed;setSpd(1.0);}else if(state._lsbr){setSpd(state._lsbr);state._lsbr=null;}break;
case 's':case 'S':if(eff('hkSpeed'))setSpd(state.speed-.25);break;
case 'd':case 'D':if(eff('hkSpeed'))setSpd(state.speed+.25);break;
case '+':case '=':if(eff('hkSpeed'))setSpd(state.speed+.25);break;
case '-': if(eff('hkSpeed'))setSpd(state.speed-.25);break;
case 'ArrowUp': if(eff('hkVol')){e.preventDefault();e.stopImmediatePropagation();adjVol(.1);}break;
case 'ArrowDown': if(eff('hkVol')){e.preventDefault();e.stopImmediatePropagation();adjVol(-.1);}break;
case 'm':case 'M':if(eff('hkMute')){e.preventDefault();e.stopImmediatePropagation();togMute();}break;
case 'ArrowRight':if(eff('hkSeek')&&!e.shiftKey){e.preventDefault();e.stopImmediatePropagation();v.currentTime=Math.min(v.duration||Infinity,v.currentTime+eff('seekStep'));}break;
case 'ArrowLeft': if(eff('hkSeek')&&!e.shiftKey){e.preventDefault();e.stopImmediatePropagation();v.currentTime=Math.max(0,v.currentTime-eff('seekStep'));}break;
}
},true);
function adjVol(d){
Boost.ensureResumed();
state.volume=Math.min(getMaxVol(),Math.max(0,+((state.volume+d).toFixed(2))));
if(state.volume===0){state.globalUnmuted=false;if(state.activeVideo)state.activeVideo.muted=true;}
else{state.globalUnmuted=true;if(state.activeVideo){state.activeVideo.muted=false;Eng.applyVol(state.activeVideo);}}
UI.refreshVol();UI.toast(Math.round(state.volume*100)+'%','info',800);
}
function togMute(){
const v=state.activeVideo;if(!v)return;
if(v.muted||v.volume<0.01){v.muted=false;state.volume=Math.max(state.volume,1);state.globalUnmuted=true;Eng.applyVol(v);}
else{v.muted=true;state.globalUnmuted=false;}
UI.refreshVol();
}
}};
// ── CSS ───────────────────────────────────────────────────────
const CSS=`
:host{all:initial;pointer-events:none;color-scheme:dark;forced-color-adjust:none;
--bg:#141820;--bg2:#212733;--bdr:#2D3545;--txt:#D9E0EE;--lbl:#6E738D;--ac:#22D3EE;
--sf:#06B6D4;--ss:#2DD4BF;--se:#10B981;--vlo:#FFCC66;--vhs:#c26000;--vhe:#ef4444;
--ear:#F9E2AF;--ply:#A6E3A1;--mut:#94E2D5;--def:#A6ADC8;--hov:#CDD6F4;
--cbg:#212733;--chv:#2A3241;}
*,*::before,*::after{box-sizing:border-box}
.shell{pointer-events:none;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;line-height:1;user-select:none;display:inline-block}
.panel{pointer-events:auto;position:relative;color:var(--txt);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
line-height:1;background:var(--bg);border:1px solid var(--bdr);border-radius:14px;
padding:10px 10px 9px;width:min(calc(200px*var(--sw,1)),88vw);
box-shadow:0 20px 52px rgba(0,0,0,.84);backdrop-filter:blur(28px) saturate(1.5);
display:flex;flex-direction:column;overflow:visible}
.panel::before{content:'';position:absolute;top:0;left:15%;right:15%;height:1px;
background:linear-gradient(90deg,transparent,rgba(255,255,255,.13),transparent);border-radius:50%;pointer-events:none}
.panel.gone{display:none!important;pointer-events:none!important}
.main{display:flex;flex-direction:column;width:100%}
.cfg{display:none;position:absolute;top:0;min-width:min(260px,88vw);max-height:min(78vh,520px);
overflow-y:auto;flex-direction:column;
background:rgba(11,14,22,0.90);-webkit-backdrop-filter:blur(18px) saturate(2);
backdrop-filter:blur(18px) saturate(2);border:1px solid rgba(255,255,255,.1);
border-radius:14px;padding:10px 10px 12px;z-index:50;
box-shadow:0 16px 48px rgba(0,0,0,.75);transition:opacity .2s ease}
.panel.sopen .cfg{display:flex}
.cfg.sizing{opacity:0!important;pointer-events:none!important;transition:none!important}
.mini{display:none;width:22px;height:22px;border-radius:50%;background:rgba(34,211,238,.7);
border:1px solid rgba(34,211,238,.5);pointer-events:auto;cursor:pointer;
align-items:center;justify-content:center;font-size:9px;font-weight:900;color:var(--bg);
box-shadow:0 0 10px rgba(34,211,238,.3);margin-top:4px}
.mini.show{display:flex}.mini:hover{background:rgba(34,211,238,.9);transform:scale(1.15)}
.hdr{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;margin-bottom:8px}
.lw{display:flex;align-items:center;gap:4px}
.dot{width:5px;height:5px;border-radius:50%;background:var(--ac);box-shadow:0 0 6px var(--ac)}
.logo{font-size:7px;font-weight:900;color:var(--ac);text-transform:uppercase;letter-spacing:1.5px}
.plat{font-size:6.5px;font-weight:700;color:var(--lbl);background:rgba(255,255,255,.04);
border-radius:4px;padding:2px 5px;text-transform:uppercase;text-align:center;justify-self:center}
.hbs{display:flex;gap:3px;justify-content:flex-end;align-items:center}
.hb{width:17px;height:17px;border-radius:50%;border:1px solid var(--bdr);background:var(--bg2);
color:var(--def);font-size:9px;cursor:pointer;display:flex;align-items:center;
justify-content:center;transition:all .15s;flex-shrink:0}
.hb:hover{background:var(--chv);color:var(--hov);border-color:var(--hov)}
.hb.x-btn{width:20px;height:20px;font-size:12px;font-weight:900;
background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.25);color:#f87171}
.hb.x-btn:hover{background:rgba(239,68,68,.3);border-color:rgba(239,68,68,.6);color:#fff}
.hb.rz-btn{font-size:9px;color:var(--ac);background:rgba(34,211,238,.08);border-color:rgba(34,211,238,.2)}
.hb.rz-btn:hover{background:rgba(34,211,238,.2);border-color:rgba(34,211,238,.4);color:#fff}
.badge{display:none;font-size:7px;font-weight:900;text-transform:uppercase;letter-spacing:.6px;border-radius:4px;padding:2px 6px;margin-bottom:6px;text-align:center}
.badge.on{display:block}.badge.info{color:#60a5fa;background:rgba(96,165,250,.09)}
.badge.ok{color:#34d399;background:rgba(52,211,153,.09)}.badge.warn{color:#fbbf24;background:rgba(251,191,36,.09)}
.isol-badge{display:none;font-size:7px;font-weight:900;color:var(--ac);background:rgba(34,211,238,.08);
border:1px solid rgba(34,211,238,.2);border-radius:4px;padding:2px 6px;margin-bottom:7px;text-align:center}
.isol-badge.on{display:block}
.sticks-row{display:flex;flex-direction:column;gap:5px}
.stick-wrap{display:contents}
.stick-lbl{display:none;font-size:7px;font-weight:900;color:var(--lbl);text-transform:uppercase;text-align:center;margin-top:2px}
.tr{position:relative;width:100%;height:max(16px,calc(20px*var(--sh,1)));background:var(--bg2);
border-radius:7px;border:1px solid var(--bdr);overflow:hidden;cursor:pointer;margin-bottom:0;transition:border-color .2s}
.tr:hover{border-color:var(--ac)}.tr.gone{display:none!important}
.bar{position:absolute;top:0;left:0;height:100%;pointer-events:none;border-radius:inherit}
.bs{background:linear-gradient(90deg,var(--ss),var(--se));opacity:.75}
.bsp{background:var(--sf);opacity:.75}
#vol_t{background:linear-gradient(90deg,rgba(255,204,102,.07) 50%,rgba(194,96,0,.12) 50%)}
.bv-lo{background:var(--vlo);opacity:.88}
.bv-hi{background:linear-gradient(90deg,var(--vhs),var(--vhe));opacity:.82}
.vol-mark{position:absolute;top:0;bottom:0;left:50%;width:2px;background:rgba(255,255,255,.5);pointer-events:none;z-index:4;border-radius:1px}
.vol-split{position:absolute;top:0;bottom:0;left:50%;width:1px;background:rgba(255,255,255,.2);pointer-events:none;z-index:3}
.lbl{position:absolute;left:7px;top:50%;transform:translateY(-50%);font-size:7px;font-weight:900;
color:var(--lbl);text-transform:uppercase;letter-spacing:.5px;pointer-events:none;white-space:nowrap}
.lbl.vl{pointer-events:auto;cursor:pointer;transition:color .15s;padding:3px 0}
.lbl.vl:hover{color:var(--vlo)}.lbl.vl.bst{color:var(--vhs)}
.val{position:absolute;right:7px;top:50%;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--txt);pointer-events:none;white-space:nowrap}
.val.bst{color:var(--vhs);font-weight:900}
.val.dur{font-size:7.5px;color:var(--lbl)}
input[type=range]{position:absolute;width:100%;height:100%;opacity:0;cursor:pointer;z-index:5;margin:0;padding:0}
.sfb{display:none;position:relative;height:2px;border-radius:2px;
background:linear-gradient(90deg,#10b981 40%,#f59e0b 70%,#ef4444 100%);margin-bottom:4px;opacity:.75}
.sfb.on{display:block}.sfn{position:absolute;top:-3px;width:3px;height:8px;background:#fff;border-radius:2px;transform:translateX(-50%);box-shadow:0 0 5px rgba(255,255,255,.5)}
.row{display:flex;gap:3px;margin-top:4px}
.btn{flex:1;min-width:0;height:max(24px,calc(29px*var(--sh,1)));background:var(--bg2);border:1px solid var(--bdr);border-radius:8px;color:var(--def);font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s}
.btn:hover{background:var(--chv);color:var(--txt);border-color:var(--ac)}
.btn.on{background:rgba(34,211,238,.12);color:var(--ac);border-color:rgba(34,211,238,.28)}
.btn.safe-on{background:rgba(52,211,153,.1);color:#34d399;border-color:rgba(52,211,153,.22)}
.btn.mut{background:rgba(239,68,68,.1);color:#f87171;border-color:rgba(239,68,68,.22)}
.btn.gone{display:none!important}
#b_sf{color:var(--ear)}#b_sf:hover{background:rgba(249,226,175,.15);border-color:var(--ear)}
#b_pl{color:var(--ply)}#b_pl:hover{background:rgba(166,227,161,.15);border-color:var(--ply)}
#b_mu{color:var(--mut)}#b_mu:hover{background:rgba(148,226,213,.15);border-color:var(--mut)}
.sec{margin-bottom:10px}
.stitle{font-size:6.5px;font-weight:900;color:var(--lbl);text-transform:uppercase;letter-spacing:1.2px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid var(--bdr)}
.stitle.iso{color:var(--ac);border-color:rgba(34,211,238,.25)}
.srow{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:5px;gap:5px}
.slbl{font-size:7px;font-weight:900;color:var(--lbl);text-transform:uppercase;white-space:nowrap;flex-shrink:0;padding-top:4px}
.chips{display:flex;gap:3px;flex-wrap:wrap;justify-content:flex-end}
.chip{padding:3px 6px;border-radius:5px;border:1px solid var(--bdr);background:var(--cbg);color:var(--lbl);font-size:7.5px;font-weight:800;cursor:pointer;transition:all .12s;white-space:nowrap}
.chip:hover{color:var(--txt);border-color:var(--ac);background:var(--chv)}
.chip.on{background:rgba(34,211,238,.12);color:var(--ac);border-color:rgba(34,211,238,.28)}
.chip.on-r{background:rgba(239,68,68,.1);color:#f87171;border-color:rgba(239,68,68,.25)}
.chip.soff{background:rgba(251,191,36,.1);color:#fbbf24;border-color:rgba(251,191,36,.25)}
.rs-row{display:flex;align-items:center;gap:4px;flex:1;min-width:0}
.rs-tr{height:16px!important;flex:1;min-width:40px;border-radius:6px!important;cursor:pointer}
.rs-tr input[type=range]{height:100%}
.rs-bar-w{background:var(--ac);opacity:.85}
.rs-bar-h{background:#8B5CF6;opacity:.85}
.panel.vert-mode{width:min(calc(110px*var(--sw,1)),28vw)!important;padding:8px 6px 8px}
.panel.vert-mode::before{display:none}
.vert-mode .main .hdr{display:flex;justify-content:center;flex-wrap:wrap;gap:2px;margin-bottom:4px}
.vert-mode .main .lw{gap:2px}
.vert-mode .main .plat{display:none}
.vert-mode .main .dot{width:4px;height:4px}
.vert-mode .main .logo{font-size:6px}
.vert-mode .main .hb{width:15px;height:15px;font-size:8px}
.vert-mode .main .hb.x-btn{width:18px!important;height:18px!important;font-size:11px!important}
.vert-mode .main .row{flex-direction:row;flex-wrap:wrap;gap:2px;margin-top:4px}
.vert-mode .main .btn{height:max(22px,calc(24px*var(--sh,1)));font-size:11px;border-radius:5px}
.vert-mode .sticks-row{flex-direction:row;gap:4px;height:max(90px,calc(140px*var(--sh,1)));align-items:stretch}
.vert-mode .stick-wrap{display:flex;flex-direction:column;align-items:center;flex:1;min-width:0;gap:2px}
.vert-mode .stick-lbl{display:block;font-size:6px}
.vert-mode .stick-wrap:has(.tr.gone){display:none!important}
.vert-mode .sticks-row .tr{flex:1;width:100%!important;height:auto!important;min-height:0;margin-bottom:0!important;border-radius:7px}
.vert-mode .sticks-row input[type=range]{writing-mode:vertical-lr;direction:rtl;-webkit-appearance:slider-vertical;appearance:slider-vertical;width:100%;height:100%;position:absolute;top:0;left:0;opacity:0;cursor:pointer;z-index:5;margin:0;padding:0}
.vert-mode .sticks-row .bar{width:100%!important;top:auto!important;bottom:0!important;left:0;right:0}
.vert-mode .sticks-row .bv-hi{bottom:50%!important}
.vert-mode #vol_t{background:linear-gradient(to top,rgba(255,204,102,.07) 50%,rgba(194,96,0,.12) 50%)}
.vert-mode .sticks-row .vol-mark{top:auto!important;bottom:50%!important;left:0!important;right:0!important;width:100%!important;height:2px!important}
.vert-mode .sticks-row .vol-split{top:auto!important;bottom:50%!important;left:0!important;right:0!important;width:100%!important;height:1px!important}
.vert-mode .sticks-row .lbl,.vert-mode .sticks-row .val{display:none}
.vert-mode .main .isol-badge{font-size:6px;padding:1px 4px;margin-bottom:4px}
@media(max-width:600px),(pointer:coarse){
.panel{width:min(calc(180px*var(--sw,1)),88vw)!important}
.panel.vert-mode{width:min(calc(90px*var(--sw,1)),26vw)!important}
.tr{height:max(20px,calc(20px*var(--sh,1)))}
.btn{height:max(28px,calc(29px*var(--sh,1)));font-size:13px}
.hb.x-btn{width:26px!important;height:26px!important;font-size:14px!important}
.vert-mode .sticks-row{height:max(110px,calc(140px*var(--sh,1)))}
.mini{width:24px;height:24px;font-size:9px}
.rs-tr{height:22px!important;border-radius:8px!important}
}`;
function mkHTML(){
const iso=isIsolated(),ev=k=>eff(k);
const pos=volGainToPos(state.volume);
const loPos=Math.min(pos,50),hiPos=Math.max(0,pos-50);
const disPct=Math.round(state.volume*100)+'%',boost=state.volume>1;
const spPct=((state.speed-.1)/(ev('maxSpeed')-.1)*100);
const maxC=MAX_SPDS.map(v=>`<span class="chip${ev('maxSpeed')===v?' on':''}" data-max="${v}">${v}×</span>`).join('');
const corC=CORNERS.map(o=>`<span class="chip${state.corner===o.id?' on':''}" data-cor="${o.id}">${o.ar}</span>`).join('');
const vertOn=ev('verticalMode');
const wFill=((state.uiScaleW-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1);
const hFill=((state.uiScaleH-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1);
const sk=ev('showSeek')?'':' gone',ss=ev('showSpeed')?'':' gone',sv=ev('showVol')?'':' gone';
return `<div class="shell" id="shell"><div class="panel${state.userClosed?' gone':''}${vertOn?' vert-mode':''}" id="panel">
<div class="main">
<div class="hdr">
<div class="lw"><div class="dot"></div><span class="logo">UVCE</span></div>
<span class="plat">${PLAT.label}</span>
<div class="hbs">
<div class="hb rz-btn" id="b_resetSize" title="Reset size">⟲</div>
<div class="hb" id="bs" title="Settings">⚙</div>
<div class="hb x-btn" id="bx" title="Close">✕</div>
</div>
</div>
<div class="badge" id="badge"></div>
<div class="isol-badge${iso?' on':''}" id="isol_badge">⚙ ${Site.host} — isolated</div>
<div class="sticks-row">
<div class="stick-wrap">
<div class="tr${sk}" id="seek_t"><span class="lbl">Seek</span><input type="range" id="sld_seek" min="0" max="1000" step="1" value="0"><div class="bar bs" id="bar_s" style="width:0%"></div><div class="val dur" id="val_dur">─</div></div>
<span class="stick-lbl">Seek</span>
</div>
<div class="stick-wrap">
<div class="tr${ss}" id="speed_t"><span class="lbl">Speed</span><input type="range" id="sld_s" min=".1" max="${ev('maxSpeed')}" step=".05" value="${state.speed}"><div class="bar bsp" id="bar_sp" style="width:${spPct.toFixed(1)}%"></div><div class="val" id="val_s">${state.speed.toFixed(2)}×</div></div>
<span class="stick-lbl">Spd</span>
</div>
<div class="stick-wrap">
<div class="tr${sv}" id="vol_t"><span class="lbl vl${boost?' bst':''}" id="vol_lbl" title="Click: reset 100%">${boost?'⚡':'Vol'}</span><input type="range" id="sld_v" min="0" max="100" step="0.5" value="${pos.toFixed(1)}"><div class="bar bv-lo" id="bar_vlo" style="width:${loPos.toFixed(1)}%"></div><div class="bar bv-hi" id="bar_vhi" style="left:50%;width:${hiPos.toFixed(1)}%"></div><div class="vol-mark"></div><div class="vol-split"></div><div class="val${boost?' bst':''}" id="val_v">${disPct}</div></div>
<span class="stick-lbl">Vol</span>
</div>
</div>
<div class="sfb${ev('safeMode')?' on':''}" id="sfb"><div class="sfn" id="sfn" style="left:0%"></div></div>
<div class="row">
<button class="btn${ev('safeMode')?' safe-on':''}${ev('showSafe')?'':' gone'}" id="b_sf">👂</button>
<button class="btn" id="b_pl">▶</button>
<button class="btn${ev('showMute')?'':' gone'}" id="b_mu">🔊</button>
</div>
</div>
<div class="cfg" id="cfg">
<div class="hdr" style="margin-bottom:8px">
<div class="lw"><span style="font-size:7px;font-weight:900;color:var(--ac);text-transform:uppercase;letter-spacing:1.5px">Settings</span></div>
<span></span>
<div class="hbs"><div class="hb x-btn" id="bsc">✕</div></div>
</div>
<div class="sec">
<div class="stitle${iso?' iso':''}">${iso?`⚙ ${Site.host}`:'Global defaults'}</div>
<div class="srow"><span class="slbl">Max speed</span><div class="chips">${maxC}</div></div>
<div class="srow"><span class="slbl">Auto-play</span><div class="chips">
<span class="chip${ev('autoplay')?' on':''}" id="c_ap">▶ Resume</span>
<span class="chip${ev('blockAutoplay')?' on':''}" id="c_ba">⛔ Block</span>
<span class="chip${ev('blockAutoplayStrict')?' on-r':''}" id="c_bas" title="Independent: zero-window + 1.5 s full scan">🚫 Strict</span>
<span class="chip${ev('autoUnmute')?' on':''}" id="c_au">🔊 Unmute</span>
</div></div>
<div class="srow"><span class="slbl">Volume</span><div class="chips">
<span class="chip${ev('safeMode')?' on':''}" id="c_sf2">👂 Cap 60%</span>
<span class="chip${ev('volumeBoost')?' on':''}" id="c_vboost">⚡ Boost</span>
</div></div>
<div class="srow"><span class="slbl">No loop</span><div class="chips">
<span class="chip${ev('noLoop')?' on':''}" id="c_nl">🔁 Loop</span>
</div></div>
<div class="srow"><span class="slbl">Sticks</span><div class="chips">
<span class="chip${ev('showSeek')?' on':''}" id="c_seek">Seek</span>
<span class="chip${ev('showSpeed')?' on':''}" id="c_spd">Speed</span>
<span class="chip${ev('showVol')?' on':''}" id="c_vol">Volume</span>
</div></div>
<div class="srow"><span class="slbl">Buttons</span><div class="chips">
<span class="chip${ev('showSafe')?' on':''}" id="c_shsf">👂</span>
<span class="chip${ev('showMute')?' on':''}" id="c_shmu">🔊</span>
<span class="chip${ev('showMini')?' on':''}" id="c_shmini" title="Show dot when panel is closed">● Dot</span>
</div></div>
</div>
<div class="sec">
<div class="stitle">Display</div>
<div class="srow"><span class="slbl">Corner</span><div class="chips">${corC}</div></div>
<div class="srow"><span class="slbl">Mode</span><div class="chips">
<span class="chip${!vertOn?' on':''}" id="c_horiz">⬌ Horiz</span>
<span class="chip${vertOn?' on':''}" id="c_vert">⬍ Vert</span>
</div></div>
<div class="srow"><span class="slbl">Auto-hide</span><div class="chips">
<span class="chip${ev('autoHide')?' on':''}" id="c_ah" title="Panel hides after ${AUTO_HIDE_DELAY/1000} s with no mouse activity — click dot to restore">⏱ ${AUTO_HIDE_DELAY/1000} s</span>
</div></div>
<div class="srow" style="align-items:center">
<span class="slbl" style="min-width:36px">⬌ W<br><span id="rs_w_pct" style="color:var(--ac);font-size:7px">${Math.round(state.uiScaleW*100)}%</span></span>
<div class="rs-row"><div class="tr rs-tr"><div class="bar rs-bar-w" id="rs_w_fill" style="width:${wFill}%"></div><input type="range" id="sld_sw" min="${RS_MIN}" max="${RS_MAX}" step="0.05" value="${state.uiScaleW}"></div></div>
</div>
<div class="srow" style="align-items:center">
<span class="slbl" style="min-width:36px">⬍ H<br><span id="rs_h_pct" style="color:#8B5CF6;font-size:7px">${Math.round(state.uiScaleH*100)}%</span></span>
<div class="rs-row"><div class="tr rs-tr"><div class="bar rs-bar-h" id="rs_h_fill" style="width:${hFill}%"></div><input type="range" id="sld_sh" min="${RS_MIN}" max="${RS_MAX}" step="0.05" value="${state.uiScaleH}"></div></div>
</div>
</div>
<div class="sec">
<div class="stitle">Shortcuts</div>
<div class="srow"><span class="slbl">Master</span><div class="chips"><span class="chip${ev('hotkeys')?' on':''}" id="c_hk">⌨ All</span></div></div>
<div class="srow"><span class="slbl">Space</span><div class="chips"><span class="chip${ev('hkSpace')?' on':''}" id="c_hks">⏸ Play</span></div></div>
<div class="srow"><span class="slbl">Speed</span><div class="chips"><span class="chip${ev('hkSpeed')?' on':''}" id="c_hkd">⚡ SDR</span></div></div>
<div class="srow"><span class="slbl">Vol/Mute</span><div class="chips"><span class="chip${ev('hkVol')?' on':''}" id="c_hkv">↑↓</span><span class="chip${ev('hkMute')?' on':''}" id="c_hkm">M</span></div></div>
<div class="srow"><span class="slbl">Seek</span><div class="chips"><span class="chip${ev('hkSeek')?' on':''}" id="c_hkl">←→</span></div></div>
</div>
<div class="sec">
<div class="stitle">🌐 ${Site.host}</div>
<div class="srow"><span class="slbl">Isolate</span><div class="chips">
<span class="chip${iso?' on':''}" id="so_own">⚙ Isolated</span>
<span class="chip${Site.get('disable')?' soff':''}" id="so_dis">🚫 Off</span>
</div></div>
</div>
<div class="sec"><div class="stitle">Danger</div><div class="srow"><div class="chips"><span class="chip" id="so_reset">🗑 Reset all</span></div></div></div>
</div>
</div></div><button class="mini" id="mini" title="Re-open UVCE">●</button>`;
}
const UI=(() => {
let rafId=null,lastFT=0,ui={};
// AUTO-HIDE: starts a timer that sets userClosed after AUTO_HIDE_DELAY ms.
// Cancelled while mouse is over the panel or settings is open.
function startAutoHide(){
clearTimeout(ui._ahTimer);
if(!eff('autoHide')||state.userClosed)return;
if(ui.panel?.classList.contains('sopen'))return;
ui._ahTimer=setTimeout(()=>{
if(!eff('autoHide'))return;
if(ui.panel?.classList.contains('sopen'))return;
state.userClosed=true;
syncVis();
},AUTO_HIDE_DELAY);
}
function setBarFill(bar,pct){
if(!bar)return; pct=Math.max(0,Math.min(100,pct));
if(eff('verticalMode')){bar.style.width='100%';bar.style.height=pct.toFixed(1)+'%';bar.style.top='auto';bar.style.bottom='0';}
else{bar.style.width=pct.toFixed(1)+'%';bar.style.height='';bar.style.top='';bar.style.bottom='';}
}
function setVolBars(pos){
const lo=Math.max(0,Math.min(50,pos)),hi=Math.max(0,pos-50);
if(eff('verticalMode')){
if(ui.bar_vlo)ui.bar_vlo.style.cssText=`width:100%;height:${lo.toFixed(1)}%;top:auto;bottom:0;left:0`;
if(ui.bar_vhi)ui.bar_vhi.style.cssText=`width:100%;height:${hi.toFixed(1)}%;top:auto;bottom:50%;left:0`;
}else{
if(ui.bar_vlo)ui.bar_vlo.style.cssText=`width:${lo.toFixed(1)}%;height:100%;top:0;bottom:auto;left:0`;
if(ui.bar_vhi)ui.bar_vhi.style.cssText=`width:${hi.toFixed(1)}%;height:100%;top:0;bottom:auto;left:50%`;
}
}
function toast(msg,type='info',ms=2400){if(!ui.badge)return;clearTimeout(ui.badge._t);ui.badge.textContent=msg;ui.badge.className=`badge on ${type}`;ui.badge._t=setTimeout(()=>{ui.badge.className='badge';},ms);}
function positionCfg(){
const cfg=ui.cfg;if(!cfg)return;
const c=state.corner;
cfg.style.left =(c==='TR'||c==='BR')?'auto':'0';
cfg.style.right =(c==='TR'||c==='BR')?'0':'auto';
cfg.style.top =(c==='TL'||c==='TR')?'0':'auto';
cfg.style.bottom=(c==='BL'||c==='BR')?'0':'auto';
}
function applyScaleW(w){
const bW=state.verticalMode?110:200;
w=Math.max(RS_MIN,Math.min(w,RS_MAX,(window.innerWidth-20)/bW));
state.uiScaleW=w;
if(ui.shell)ui.shell.style.setProperty('--sw',w.toFixed(3));
const root=ui.panel?.getRootNode();
if(root){
const s=root.querySelector('#sld_sw');if(s)s.value=w;
const p=root.querySelector('#rs_w_pct');if(p)p.textContent=Math.round(w*100)+'%';
const f=root.querySelector('#rs_w_fill');if(f)f.style.width=((w-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1)+'%';
}
saveSetting('uiScaleW');
}
function applyScaleH(h){
h=Math.max(RS_MIN,Math.min(h,RS_MAX));
state.uiScaleH=h;
if(ui.shell)ui.shell.style.setProperty('--sh',h.toFixed(3));
const root=ui.panel?.getRootNode();
if(root){
const s=root.querySelector('#sld_sh');if(s)s.value=h;
const p=root.querySelector('#rs_h_pct');if(p)p.textContent=Math.round(h*100)+'%';
const f=root.querySelector('#rs_h_fill');if(f)f.style.width=((h-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1)+'%';
}
saveSetting('uiScaleH');
}
function attachResizeHide(sliderEl){
if(!sliderEl)return;
const hide=()=>{
if(!ui.cfg)return;
ui.cfg.classList.add('sizing');
const restore=()=>ui.cfg?.classList.remove('sizing');
window.addEventListener('pointerup',restore,{once:true,capture:true});
window.addEventListener('touchend', restore,{once:true,capture:true});
};
sliderEl.addEventListener('pointerdown',hide,{passive:true});
sliderEl.addEventListener('touchstart', hide,{passive:true});
}
function applyVerticalMode(on){
if(!ui.panel)return;
ui.panel.classList.toggle('vert-mode',on);
const root=ui.panel.getRootNode();
root.querySelector('#c_horiz')?.classList.toggle('on',!on);
root.querySelector('#c_vert')?.classList.toggle('on',on);
refreshSpd();refreshVol();
}
function applyBarVis(){
if(!ui.panel)return;
const root=ui.panel.getRootNode();
root.querySelector('#seek_t')?.classList.toggle('gone',!eff('showSeek'));
root.querySelector('#speed_t')?.classList.toggle('gone',!eff('showSpeed'));
if(ui.vol_t)ui.vol_t.classList.toggle('gone',!eff('showVol'));
if(ui.b_sf)ui.b_sf.classList.toggle('gone',!eff('showSafe'));
if(ui.b_mu)ui.b_mu.classList.toggle('gone',!eff('showMute'));
}
function refreshSpd(){
if(!ui.sld_s)return;
ui.sld_s.value=state.speed;
if(ui.val_s)ui.val_s.textContent=state.speed.toFixed(2)+'×';
setBarFill(ui.bar_sp,(state.speed-.1)/(eff('maxSpeed')-.1)*100);
}
function refreshVol(){
if(!ui.sld_v)return;
const v=state.activeVideo;
const isMuted=v?(v.muted||v.volume<0.01):false;
const dg=isMuted?0:state.volume;
const pos=volGainToPos(dg),pct=Math.round(dg*100)+'%',boost=dg>1;
ui.sld_v.value=isMuted?'0':pos.toFixed(1);
if(ui.val_v){ui.val_v.textContent=isMuted?'0%':pct;ui.val_v.classList.toggle('bst',boost);}
setVolBars(isMuted?0:pos);
if(ui.vol_lbl){ui.vol_lbl.textContent=(boost&&!isMuted)?'⚡':'Vol';ui.vol_lbl.classList.toggle('bst',boost&&!isMuted);}
}
function refreshChips(){
if(!ui.panel)return;
const root=ui.panel.getRootNode();
const q=id=>root.querySelector('#'+id);
const upd=(id,val,cls='on')=>{const el=q(id);if(el)el.classList.toggle(cls,!!val);};
upd('c_ap',eff('autoplay'));upd('c_ba',eff('blockAutoplay'));upd('c_bas',eff('blockAutoplayStrict'),'on-r');
upd('c_au',eff('autoUnmute'));upd('c_nl',eff('noLoop'));upd('c_sf2',eff('safeMode'));
upd('c_vboost',eff('volumeBoost'));upd('c_seek',eff('showSeek'));upd('c_spd',eff('showSpeed'));
upd('c_vol',eff('showVol'));upd('c_shsf',eff('showSafe'));upd('c_shmu',eff('showMute'));
upd('c_shmini',eff('showMini'));upd('c_ah',eff('autoHide'));
upd('c_hk',eff('hotkeys'));upd('c_hks',eff('hkSpace'));upd('c_hkd',eff('hkSpeed'));
upd('c_hkv',eff('hkVol'));upd('c_hkm',eff('hkMute'));upd('c_hkl',eff('hkSeek'));
upd('c_horiz',!eff('verticalMode'));upd('c_vert',eff('verticalMode'));
const iso=isIsolated();upd('so_own',iso);upd('isol_badge',iso);
applyBarVis();
}
function syncVis(){
if(!ui.panel)return;
const show=!isDisabled()&&!state.userClosed;
ui.panel.classList.toggle('gone',!show);
if(ui.mini)ui.mini.classList.toggle('show',!show&&eff('showMini'));
if(!show){
clearTimeout(ui._ahTimer);
if(rafId!==null){cancelAnimationFrame(rafId);rafId=null;}
}else{
if(state.activeVideo&&rafId===null)kickRaf();
startAutoHide(); // restart 3 s countdown whenever panel becomes visible
}
}
function kickRaf(){if(rafId===null&&state.activeVideo)rafId=requestAnimationFrame(frame);}
function frame(now){
if(!state.activeVideo||(ui.panel&&ui.panel.classList.contains('gone'))){rafId=null;return;}
if(now-lastFT<33){rafId=requestAnimationFrame(frame);return;}
lastFT=now;
const v=state.activeVideo;
if(v){
if(isFinite(v.duration)&&v.duration>0){
const pct=v.currentTime/v.duration;
if(ui.sld_seek)ui.sld_seek.value=Math.round(pct*1000);
setBarFill(ui.bar_s,pct*100);
if(ui.val_dur)ui.val_dur.textContent=fmtTime(v.currentTime)+' / '+fmtTime(v.duration);
}else if(v.duration===Infinity){
if(ui.val_dur)ui.val_dur.textContent='LIVE';
}else{
if(ui.val_dur)ui.val_dur.textContent='─';
}
if(ui.b_pl)ui.b_pl.textContent=v.paused?'▶':'⏸';
if(ui.b_mu){const m=v.muted||v.volume<0.01;ui.b_mu.textContent=m?'🔇':'🔊';ui.b_mu.classList.toggle('mut',m);}
if(Boost.has(v))Boost.setGain(eff('safeMode')?Eng.clampSafe(state.volume):state.volume);
const ep=Math.round((eff('safeMode')?Eng.clampSafe(state.volume):Math.min(state.volume,1))*100);
if(ui.sfn)ui.sfn.style.left=Math.min(100,ep)+'%';
}
rafId=requestAnimationFrame(frame);
}
function inject(){
if(document.getElementById(RIG_ID))return;
const host=document.createElement('div');host.id=RIG_ID;
positionHost(host,state.corner);
const sr=host.attachShadow({mode:'open'});
sr.innerHTML=trustedHTML(`<style>${CSS}</style>${mkHTML()}`);
const q=id=>sr.querySelector('#'+id);
ui={
shell:q('shell'),panel:q('panel'),cfg:q('cfg'),badge:q('badge'),mini:q('mini'),
isol_badge:q('isol_badge'),
bar_s:q('bar_s'),bar_sp:q('bar_sp'),bar_vlo:q('bar_vlo'),bar_vhi:q('bar_vhi'),
sld_seek:q('sld_seek'),sld_s:q('sld_s'),sld_v:q('sld_v'),
val_s:q('val_s'),val_v:q('val_v'),val_dur:q('val_dur'),
vol_lbl:q('vol_lbl'),vol_t:q('vol_t'),
sfb:q('sfb'),sfn:q('sfn'),seek_t:q('seek_t'),
b_pl:q('b_pl'),b_mu:q('b_mu'),b_sf:q('b_sf'),
b_resetSize:q('b_resetSize'),host,
};
bindEvents(sr);
(document.body||document.documentElement).appendChild(host);
antiInvert(host);
ui.shell.style.setProperty('--sw',state.uiScaleW.toFixed(3));
ui.shell.style.setProperty('--sh',state.uiScaleH.toFixed(3));
applyVerticalMode(state.verticalMode);
positionCfg();
// Auto-hide: reset timer on mouse-over; restart on mouse-leave
if(ui.panel){
ui.panel.addEventListener('mouseenter',()=>{clearTimeout(ui._ahTimer);});
ui.panel.addEventListener('mouseleave',()=>{startAutoHide();});
}
syncVis();refreshSpd();refreshVol();kickRaf();
window.addEventListener('resize',deb(()=>applyScaleW(state.uiScaleW),200));
}
function bindEvents(sr){
const q=id=>sr.querySelector('#'+id);
const sc=id=>{const el=q(id);return el||{onclick:null,oninput:null};};
sc('b_resetSize').onclick=()=>{applyScaleW(1.0);applyScaleH(1.0);toast('Size reset','ok',800);};
const sw=q('sld_sw');if(sw){attachResizeHide(sw);sw.oninput=()=>applyScaleW(parseFloat(sw.value));}
const sh=q('sld_sh');if(sh){attachResizeHide(sh);sh.oninput=()=>applyScaleH(parseFloat(sh.value));}
sc('c_horiz').onclick=()=>{state.verticalMode=false;applyVerticalMode(false);saveSetting('verticalMode');};
sc('c_vert').onclick=()=>{state.verticalMode=true;applyVerticalMode(true);saveSetting('verticalMode');};
if(ui.sld_seek)ui.sld_seek.oninput=()=>{
const v=state.activeVideo;if(!v||!isFinite(v.duration))return;
const pct=parseFloat(ui.sld_seek.value)/1000;
v.currentTime=pct*v.duration;setBarFill(ui.bar_s,pct*100);
if(ui.val_dur)ui.val_dur.textContent=fmtTime(v.currentTime)+' / '+fmtTime(v.duration);
};
q('sld_s').oninput=()=>{
state.speed=parseFloat(q('sld_s').value);refreshSpd();
if(state.activeVideo){Eng.lockSpd(state.activeVideo);_lastSpeedPerVideo.set(state.activeVideo,state.speed);}
saveSetting('speed');
};
q('sld_v').oninput=()=>{
Boost.ensureResumed();
state.volume=volPosToGain(parseFloat(q('sld_v').value)||0);
const v=state.activeVideo;
if(state.volume===0){state.globalUnmuted=false;if(v)v.muted=true;}
else{state.globalUnmuted=true;if(v){if(v.muted)v.muted=false;Eng.applyVol(v);}}
refreshVol();
};
q('vol_lbl').onclick=e=>{
e.stopPropagation();state.volume=1.0;state.globalUnmuted=true;
if(state.activeVideo){state.activeVideo.muted=false;Eng.applyVol(state.activeVideo);}
refreshVol();toast('Volume reset','ok',1000);
};
sc('b_pl').onclick=()=>{
const av=findTargetVideo();if(!av)return;
if(av!==state.activeVideo)Eng.activate(av);
if(av.paused){clearSelfPaused();av._uvce_mp=false;approvePlay(av);BlockAuto.ok(av);av.play().catch(()=>{});}
else pauseVideo(av);
};
sc('b_mu').onclick=()=>{
const v=state.activeVideo;if(!v)return;
if(v.muted||v.volume<0.01){v.muted=false;state.volume=Math.max(state.volume,1);state.globalUnmuted=true;Eng.applyVol(v);}
else{v.muted=true;state.globalUnmuted=false;}
refreshVol();
};
sc('b_sf').onclick=()=>{state.safeMode=!state.safeMode;q('b_sf')?.classList.toggle('safe-on',state.safeMode);if(ui.sfb)ui.sfb.classList.toggle('on',state.safeMode);saveSetting('safeMode');};
sc('bx').onclick=()=>{state.userClosed=true;syncVis();};
sc('bs').onclick=()=>{refreshChips();ui.panel.classList.toggle('sopen');positionCfg();clearTimeout(ui._ahTimer);};
sc('bsc').onclick=()=>{ui.panel.classList.remove('sopen');startAutoHide();};
sc('mini').onclick=()=>{state.userClosed=false;syncVis();};
sr.querySelectorAll('.chip[data-cor]').forEach(c=>c.onclick=()=>{
state.corner=c.dataset.cor;positionHost(ui.host,state.corner);applyScaleW(state.uiScaleW);positionCfg();
sr.querySelectorAll('.chip[data-cor]').forEach(x=>x.classList.toggle('on',x.dataset.cor===state.corner));
saveSetting('corner');
});
sr.querySelectorAll('.chip[data-max]').forEach(c=>c.onclick=()=>{
state.maxSpeed=parseFloat(c.dataset.max);q('sld_s').max=state.maxSpeed;
if(state.speed>state.maxSpeed){state.speed=state.maxSpeed;Eng.lockSpd(state.activeVideo);}
sr.querySelectorAll('.chip[data-max]').forEach(x=>x.classList.toggle('on',x.dataset.max==state.maxSpeed));
refreshSpd();saveSetting('maxSpeed');
});
const tog=(id,key,cls,cb)=>{
if(typeof cls==='function'){cb=cls;cls='on';}
cls=cls||'on';
const el=q(id);if(!el)return;
el.onclick=()=>{state[key]=!state[key];el.classList.toggle(cls,state[key]);if(cb)cb(state[key]);saveSetting(key);};
};
tog('c_ap','autoplay');
tog('c_ba','blockAutoplay',en=>{if(en)BlockAuto.patch();else{state.blockAutoplayStrict=false;q('c_bas')?.classList.remove('on-r');saveSetting('blockAutoplayStrict');}});
tog('c_bas','blockAutoplayStrict','on-r',en=>{if(en)BlockAuto.patch();});
tog('c_au','autoUnmute',en=>{if(en&&state.activeVideo){const v=state.activeVideo;state.globalUnmuted=true;if(v.muted)v.muted=false;if(v.volume<0.01)try{v.volume=1;}catch{}}});
tog('c_nl','noLoop',en=>{document.querySelectorAll('video').forEach(en?sealNoLoop:unsealNoLoop);if(en)BlockAuto.patch();});
tog('c_sf2','safeMode',()=>{if(ui.sfb)ui.sfb.classList.toggle('on',state.safeMode);q('b_sf')?.classList.toggle('safe-on',state.safeMode);});
tog('c_vboost','volumeBoost',()=>{refreshVol();if(state.activeVideo)Eng.applyVol(state.activeVideo);});
tog('c_seek','showSeek',v=>{const t=sr.querySelector('#seek_t');if(t)t.classList.toggle('gone',!v);});
tog('c_spd','showSpeed',v=>{const t=sr.querySelector('#speed_t');if(t)t.classList.toggle('gone',!v);});
tog('c_vol','showVol',v=>{if(ui.vol_t)ui.vol_t.classList.toggle('gone',!v);});
tog('c_shsf','showSafe',v=>{if(ui.b_sf)ui.b_sf.classList.toggle('gone',!v);});
tog('c_shmu','showMute',v=>{if(ui.b_mu)ui.b_mu.classList.toggle('gone',!v);});
tog('c_shmini','showMini',()=>{syncVis();});
tog('c_ah','autoHide',en=>{if(en)startAutoHide();else clearTimeout(ui._ahTimer);});
tog('c_hk','hotkeys');tog('c_hks','hkSpace');tog('c_hkd','hkSpeed');
tog('c_hkv','hkVol');tog('c_hkm','hkMute');tog('c_hkl','hkSeek');
sc('so_own').onclick=()=>{
if(isIsolated()){for(const k of ISOLATABLE)Site.set(k,null);Site.set('useOwn',false);Object.assign(state,Store.load(),{activeVideo:state.activeVideo,userClosed:state.userClosed});state.volume=1.0;}
else{Site.set('useOwn',true);for(const k of ISOLATABLE)Site.set(k,state[k]);}
refreshChips();refreshSpd();refreshVol();
};
sc('so_dis').onclick=()=>{
const cur=Site.get('disable');Site.set('disable',cur?null:true);
q('so_dis')?.classList.toggle('soff',!cur);
if(!cur&&state.activeVideo){Eng.unlockSpd(state.activeVideo);state.activeVideo=null;}
syncVis();
};
sc('so_reset').onclick=()=>{
for(const k of ISOLATABLE)Site.set(k,null);Site.set('useOwn',false);Site.set('disable',null);
Object.assign(state,DEFAULTS,{activeVideo:state.activeVideo,userClosed:false,globalUnmuted:false});
state.volume=1.0;Store.save(state);
applyScaleW(1.0);applyScaleH(1.0);applyVerticalMode(false);
refreshSpd();refreshVol();refreshChips();syncVis();
toast('Settings reset','ok',2000);
};
}
return{inject,syncVis,kickRaf,refreshSpd,refreshVol,refreshChips,toast};
})();
if(eff('blockAutoplay')||eff('blockAutoplayStrict')||eff('noLoop'))BlockAuto.patch();
VM.init();Keys.init();
if(IS_TOP){
Cleaner.apply();UI.inject();
if(PLAT.name==='youtube'){
window.addEventListener('yt-navigate-finish',()=>{
if(!document.getElementById(RIG_ID))UI.inject();
state.userClosed=false;UI.syncVis();
});
}
}
if(HAS_MC){
GM_registerMenuCommand('▶ Show UVCE',()=>{state.userClosed=false;UI.syncVis();},'u');
GM_registerMenuCommand('⛔ Block Auto',()=>{state.blockAutoplay=!state.blockAutoplay;if(state.blockAutoplay)BlockAuto.patch();else state.blockAutoplayStrict=false;saveSetting('blockAutoplay');saveSetting('blockAutoplayStrict');},'b');
GM_registerMenuCommand('🚫 Strict',()=>{state.blockAutoplayStrict=!state.blockAutoplayStrict;if(state.blockAutoplayStrict)BlockAuto.patch();saveSetting('blockAutoplayStrict');},'t');
GM_registerMenuCommand('🔁 No Loop',()=>{state.noLoop=!state.noLoop;document.querySelectorAll('video').forEach(state.noLoop?sealNoLoop:unsealNoLoop);if(state.noLoop)BlockAuto.patch();saveSetting('noLoop');},'l');
GM_registerMenuCommand('⌨ Hotkeys',()=>{state.hotkeys=!state.hotkeys;saveSetting('hotkeys');},'k');
GM_registerMenuCommand('🚫 Disable on '+Site.host,()=>{const cur=Site.get('disable');Site.set('disable',cur?null:true);if(!cur&&state.activeVideo){Eng.unlockSpd(state.activeVideo);state.activeVideo=null;}},'d');
}
})();