@@ -1,5 +1,8 @@
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm ;
var isUserSeek = false ; // Track if seek was user-initiated
var lastToggleSpeed = { } ; // Store last toggle speeds per video
var tc = {
settings : {
lastSpeed : 1.0 ,
@@ -20,14 +23,13 @@ var tc = {
teams.microsoft.com
` . replace ( regStrip , "" ) ,
defaultLogLevel : 4 ,
logLevel : 3 ,
// --- Nudge settings (ADDED) ---
logLevel : 5 , // Set to 5 to see your debug logs
enableSubtitleNudge : true ,
subtitleNudgeInterval : 25 ,
subtitleNudgeAmount : 0.001
} ,
mediaElements : [ ] ,
isNudging : false // ADDED: Flag for nudge operation
isNudging : false
} ;
/* Log levels */
@@ -48,80 +50,74 @@ function log(message, level) {
}
chrome . storage . sync . get ( tc . settings , function ( storage ) {
tc . settings . keyBindings =
Array . isArray ( storage . keyBindings ) &&
storage . keyBindings . length > 0 &&
storage . keyBindings [ 0 ] . hasOwnProperty ( "predefined" )
? storage . keyBindings
: [
{
// Original initialization from your code
tc . settings . keyBindings = storage . keyBindings ;
if ( storage . keyBindings . length == 0 ) {
tc . settings . keyBindings . push ( {
action : "slower" ,
key : Number ( storage . slowerKeyCode ) || 83 ,
value : Number ( storage . speedStep ) || 0.1 ,
force : false ,
predefined : true
} ,
{
} ) ;
tc . settings . keyBindings . push ( {
action : "faster" ,
key : Number ( storage . fasterKeyCode ) || 68 ,
value : Number ( storage . speedStep ) || 0.1 ,
force : false ,
predefined : true
} ,
{
} ) ;
tc . settings . keyBindings . push ( {
action : "rewind" ,
key : Number ( storage . rewindKeyCode ) || 90 ,
value : Number ( storage . rewindTime ) || 10 ,
force : false ,
predefined : true
} ,
{
} ) ;
tc . settings . keyBindings . push ( {
action : "advance" ,
key : Number ( storage . advanceKeyCode ) || 88 ,
value : Number ( storage . advanceTime ) || 10 ,
force : false ,
predefined : true
} ,
{
} ) ;
tc . settings . keyBindings . push ( {
action : "reset" ,
key : Number ( storage . resetKeyCode ) || 82 ,
value : 1.0 ,
force : false ,
predefined : true
} ,
{
} ) ;
tc . settings . keyBindings . push ( {
action : "fast" ,
key : Number ( storage . fastKeyCode ) || 71 ,
value : Number ( storage . fastSpeed ) || 1.8 ,
force : false ,
predefined : true
}
] ;
if (
! Array . isArray ( storage . keyBindings ) ||
storage . keyBindings . length === 0 ||
( storage . keyBindings . length > 0 &&
! storage . keyBindings [ 0 ] . hasOwnProperty ( "predefined" ) )
) {
}) ;
tc . settings . version = "0.5.3" ;
chrome . storage . sync . set ( {
keyBindings : tc . settings . keyBindings ,
version : "0.6.3.14"
} ) ; // Incremented version
version : tc . settings . version ,
displayKeyCode : tc . settings . displayKeyCode ,
rememberSpeed : tc . settings . rememberSpeed ,
forceLastSavedSpeed : tc . settings . forceLastSavedSpeed ,
audioBoolean : tc . settings . audioBoolean ,
startHidden : tc . settings . startHidden ,
enabled : tc . settings . enabled ,
controllerOpacity : tc . settings . controllerOpacity ,
blacklist : tc . settings . blacklist . replace ( regStrip , "" )
} ) ;
}
tc . settings . lastSpeed = Number ( storage . lastSpeed ) || 1.0 ;
tc . settings . displayKeyCode = Number ( storage . displayKeyCode ) || 86 ;
tc . settings . lastSpeed = Number ( storage . lastSpeed ) ;
tc . settings . displayKeyCode = Number ( storage . displayKeyCode ) ;
tc . settings . rememberSpeed = Boolean ( storage . rememberSpeed ) ;
tc . settings . forceLastSavedSpeed = Boolean ( storage . forceLastSavedSpeed ) ;
tc . settings . audioBoolean = Boolean ( storage . audioBoolean ) ;
tc . settings . enabled =
typeof storage . enabled !== "undefined" ? Boolean ( storage . enabled ) : true ;
tc . settings . enabled = Boolean ( storage . enabled ) ;
tc . settings . startHidden = Boolean ( storage . startHidden ) ;
tc . settings . controllerOpacity = Number ( storage . controllerOpacity ) || 0.3 ;
tc . settings . blacklist = String ( storage . blacklist || tc . settings . blacklist ) ;
if ( typeof storage . logLevel !== "undefined" )
tc . settings . logLevel = Number ( storage . logLevel ) ;
tc . settings . controllerOpacity = Number ( storage . controllerOpacity ) ;
tc . settings . blacklist = String ( storage . blacklist ) ;
tc . settings . enableSubtitleNudge =
typeof storage . enableSubtitleNudge !== "undefined"
? Boolean ( storage . enableSubtitleNudge )
@@ -130,50 +126,55 @@ chrome.storage.sync.get(tc.settings, function (storage) {
Number ( storage . subtitleNudgeInterval ) || 25 ;
tc . settings . subtitleNudgeAmount =
Number ( storage . subtitleNudgeAmount ) || tc . settings . subtitleNudgeAmount ;
if (
tc . settings . keyBindings . filter ( ( x ) => x . action == "display" ) . length == 0
) {
tc . settings . keyBindings . push ( {
action : "display" ,
key : tc . settings . displayKeyCode ,
key : Number ( storage . displayKeyCode ) || 86 ,
value : 0 ,
force : false ,
predefined : true
} ) ;
}
// Add a listener for messages from the popup.
// We use a global flag to ensure the listener is only attached once.
if ( ! window . vscMessageListener ) {
chrome . runtime . onMessage . addListener (
function ( request , sender , sendResponse ) {
// Check if the message is a request to re-scan the page.
if ( request . action === "rescan_page" ) {
log ( "Re-scan command received from popup." , 4 ) ;
// Call the main initialization function. It's designed to be safe
// to run multiple times and will pick up any new videos.
initializeWhenReady ( document ) ;
// Send a response to the popup to confirm completion.
sendResponse ( { status : "complete" } ) ;
}
// Required to allow for asynchronous responses.
return true ;
}
) ;
// Set the flag to prevent adding the listener again.
window . vscMessageListener = true ;
}
initializeWhenReady ( document ) ;
} ) ;
function getKeyBindings ( action , what = "value" ) {
if ( ! tc . settings . keyBindings ) return false ;
try {
const binding = tc . settings . keyBindings . find (
( item ) => item . action === action
) ;
if ( binding ) return binding [ what ] ;
if ( what === "value" ) {
if ( action === "slower" || action === "faster" ) return 0.1 ;
if ( action === "rewind" || action === "advance" ) return 10 ;
if ( action === "reset" ) return 1.0 ;
if ( action === "fast" ) return 1.8 ;
}
return false ;
return tc . settings . keyBindings . find ( ( item ) => item . action === action ) [ what ] ;
} catch ( e ) {
log ( ` Error in getKeyBindings for ${ action } ( ${ what } ): ${ e . message } ` , 2 ) ;
return false ;
}
}
function setKeyBindings ( action , value ) {
// Original setKeyBindings
if ( ! tc . settings . keyBindings ) return ;
const binding = tc . settings . keyBindings . find (
( item ) => item . action === action
) ;
if ( binding ) {
binding [ "value" ] = value ;
}
tc . settings . keyBindings . find ( ( item ) => item . action === action ) [ "value" ] =
value ;
}
function defineVideoController ( ) {
@@ -182,75 +183,78 @@ function defineVideoController() {
tc . mediaElements . push ( target ) ;
target . vsc = this ;
this . video = target ;
this . parent = parent || target. parentElement ;
this . nudgeIntervalId = null ; // ADDED
this . parent = target . parentElement || parent ;
this . nudgeIntervalId = null ;
let storedSpeed ; // Original logic for initial speed determination
// Determine what speed to use
let storedSpeed = tc . settings . speeds [ target . currentSrc ] ;
if ( ! tc . settings . rememberSpeed ) {
storedSpeed = tc . settings . speeds [ target . currentSrc ] ;
if ( ! storedSpeed ) storedSpeed = 1.0 ;
setKeyBindings ( "reset" , getKeyBindings ( "fast" ) ) ; // Original call
} else {
storedSpeed =
tc . settings . speeds [ target . currentSrc ] || tc . settings . lastSpeed ;
if ( ! storedSpeed ) {
storedSpeed = 1.0 ;
}
if ( tc . settings . forceLastSavedSpeed ) storedSpeed = tc . settings . lastSpeed ;
} else {
storedSpeed = tc . settings . lastSpeed ;
}
if ( tc . settings . forceLastSavedSpeed ) {
storedSpeed = tc . settings . lastSpeed ;
}
// FIXED: Actually apply the speed to the video element
// Use setSpeed function to properly set the speed with all the necessary logic
setTimeout ( ( ) => {
if ( this . video && this . video . vsc ) {
setSpeed ( this . video , storedSpeed , true , false ) ;
}
} , 0 ) ;
this . div = this . initializeControls ( ) ;
if ( Math . abs ( target . playbackRate - storedSpeed ) > 0.001 ) {
// MODIFIED: Pass isUserKeyPress = false for initial/automatic speed settings
setSpeed ( target , storedSpeed , true , false ) ;
} else {
if ( this . speedIndicator )
this . speedIndicator . textContent = storedSpeed . toFixed ( 2 ) ;
if (
! tc . settings . forceLastSavedSpeed &&
tc . settings . lastSpeed !== storedSpeed
) {
tc . settings . lastSpeed = storedSpeed ;
}
}
// Make the controller visible for 5 seconds on startup
runAction ( "blink" , 5000 , null , this . video ) ;
// MODIFIED: mediaEventAction to correctly maintain speed on play/pause/seek/ended
// Rewritten mediaEventAction to prevent speed reset on pause.
var mediaEventAction = function ( event ) {
const video = event . target ;
if ( ! video . vsc ) return ;
let speedToMaintain ;
if ( tc . settings . forceLastSavedSpeed ) {
speedToMaintain = tc . settings . lastSpeed ;
} else if ( tc . settings . rememberSpeed ) {
speedToMaintain =
tc . settings . speeds [ video . currentSrc ] || tc . settings . lastSpeed ;
} else {
// Not forcing, not remembering per-video. Maintain the current session's tc.settings.lastSpeed.
speedToMaintain = tc . settings . lastSpeed ;
}
// The original setKeyBindings("reset", getKeyBindings("fast")) from old mediaEventAction
// was related to the complex 'R' key toggle. That logic is within the main `resetSpeed` function.
// Here, we just ensure the determined `speedToMaintain` is applied if needed.
if ( Math . abs ( video . playbackRate - speedToMaintain ) > 0.001 ) {
log (
` Media event ' ${ event . type } ': video rate ${ video . playbackRate . toFixed ( 2 ) } vs target ${ speedToMaintain . toFixed ( 2 ) } . Correcting. ` ,
4
) ;
// Corrections from play/pause/seek/ended are not direct user key presses for blink.
setSpeed ( video , speedToMaintain , false , false ) ; // isInitialCall=false, isUserKeyPress=false
} else {
log (
` Media event ' ${ event . type } ': video rate ${ video . playbackRate . toFixed ( 2 ) } matches target. No speed change needed. ` ,
6
) ;
}
// Manage nudge based on event type
// Handle subtitle nudging based on the event type first.
if ( event . type === "play" ) {
if ( video . playbackRate !== 1.0 ) video . vsc . startSubtitleNudge ( ) ; // Only start if not 1.0x
this . startSubtitleNudge ( ) ;
// FIXED: Only reapply speed if there's a significant mismatch AND it's a new video
const currentSpeed = event . target . playbackRate ;
const videoId =
event . target . currentSrc || event . target . src || "default" ;
// Get the expected speed based on settings
let expectedSpeed ;
if ( tc . settings . forceLastSavedSpeed ) {
expectedSpeed = tc . settings . lastSpeed ;
} else {
expectedSpeed = tc . settings . speeds [ videoId ] || tc . settings . lastSpeed ;
}
// Only reapply speed if:
// 1. The current speed is 1.0 (default) AND we have a stored speed that's different
// 2. OR if forceLastSavedSpeed is enabled and speeds don't match
const shouldReapplySpeed =
( Math . abs ( currentSpeed - 1.0 ) < 0.01 &&
Math . abs ( expectedSpeed - 1.0 ) > 0.01 ) ||
( tc . settings . forceLastSavedSpeed &&
Math . abs ( currentSpeed - expectedSpeed ) > 0.01 ) ;
if ( shouldReapplySpeed ) {
setTimeout ( ( ) => {
if ( event . target . vsc ) {
setSpeed ( event . target , expectedSpeed , false , false ) ;
}
} , 10 ) ;
}
} else if ( event . type === "pause" || event . type === "ended" ) {
video . vsc . stopSubtitleNudge ( ) ;
this . stopSubtitleNudge ( ) ;
}
// For seek events, don't mess with speed
if ( event . type === "seeked" && isUserSeek ) {
isUserSeek = false ;
return ;
}
} ;
@@ -271,50 +275,96 @@ function defineVideoController() {
( this . handleSeek = mediaEventAction . bind ( this ) )
) ;
// ADDITIONAL FIX: Listen for loadedmetadata to reapply speed when video source changes
target . addEventListener ( "loadedmetadata" , ( ) => {
if ( this . video && this . video . vsc ) {
const currentSpeed = this . video . playbackRate ;
const videoId = this . video . currentSrc || this . video . src || "default" ;
// Get expected speed
let expectedSpeed ;
if ( tc . settings . forceLastSavedSpeed ) {
expectedSpeed = tc . settings . lastSpeed ;
} else {
expectedSpeed = tc . settings . speeds [ videoId ] || tc . settings . lastSpeed ;
}
// Only reapply if current speed is default (1.0) and we have a different stored speed
const shouldReapplySpeed =
Math . abs ( currentSpeed - 1.0 ) < 0.01 &&
Math . abs ( expectedSpeed - 1.0 ) > 0.01 ;
if ( shouldReapplySpeed ) {
setSpeed ( this . video , expectedSpeed , false , false ) ;
}
}
} ) ;
var srcObserver = new MutationObserver ( ( mutations ) => {
// Original srcObserver
mutations . forEach ( ( mutation ) => {
if (
mutation . type === "attributes" &&
( mutation . attributeName === "src" ||
mutation . attributeName === "currentSrc" )
) {
if ( ! this . div ) return ;
if ( this . div ) {
this . stopSubtitleNudge ( ) ;
if ( ! mutation . target . src && ! mutation . target . currentSrc )
if ( ! mutation . target . src && ! mutation . target . currentSrc ) {
this . div . classList . add ( "vsc-nosource" ) ;
else {
this . div . classList . remove ( "vsc-nosource" ) ;
let newSrcSpeed = tc . settings . speeds [ mutation . target . currentSrc ] ;
if ( ! tc . settings . rememberSpeed ) {
if ( ! newSrcSpeed ) newSrcSpeed = 1.0 ;
} else {
newSrcSpeed = newSrcSpeed || tc . settings . lastSpeed ;
this . div . classList . remove ( "vsc-nosource" ) ;
// FIXED: Reapply speed when source changes (like in shorts)
const expectedSpeed = tc . settings . forceLastSavedSpeed
? tc . settings . lastSpeed
: tc . settings . speeds [ mutation . target . currentSrc ] ||
tc . settings . lastSpeed ;
setTimeout ( ( ) => {
if ( mutation . target . vsc ) {
setSpeed ( mutation . target , expectedSpeed , false , false ) ;
}
} , 100 ) ;
if ( ! mutation . target . paused ) this . startSubtitleNudge ( ) ;
}
if ( tc . settings . forceLastSavedSpeed )
newSrcSpeed = tc . settings . lastSpeed ;
setSpeed ( mutation . target , newSrcSpeed , true , false ) ; // isInitialCall=true, isUserKeyPress=false
if ( ! mutation . target . paused && mutation . target . playbackRate !== 1.0 )
this . startSubtitleNudge ( ) ;
}
}
} ) ;
} ) ;
srcObserver . observe ( target , { attributeFilter : [ "src" , "currentSrc" ] } ) ;
if ( ! target . paused && target . playbackRate !== 1.0 )
this . startSubtitleNudge ( ) ;
} ;
tc . videoController . prototype . remove = function ( ) {
this . stopSubtitleNudge ( ) ;
if ( this . div ) this . div . remove ( ) ;
if ( this . video ) {
this . video . removeEventListener ( "play" , this . handlePlay ) ;
this . video . removeEventListener ( "pause" , this . handlePause ) ;
this . video . removeEventListener ( "ended" , this . handleEnded ) ;
this . video . removeEventListener ( "seeked" , this . handleSeek ) ;
delete this . video . vsc ;
}
let idx = tc . mediaElements . indexOf ( this . video ) ;
if ( idx != - 1 ) tc . mediaElements . splice ( idx , 1 ) ;
} ;
tc . videoController . prototype . startSubtitleNudge = function ( ) {
// ADDED
if ( ! location . hostname . includes ( "youtube.com" ) ) return ;
const isYouTube =
( this . video &&
this . video . currentSrc &&
this . video . currentSrc . includes ( "googlevideo.com" ) ) ||
location . hostname . includes ( "youtube.com" ) ;
if ( ! isYouTube ) return ;
if (
! tc . settings . enableSubtitleNudge ||
this . nudgeIntervalId !== null ||
! this . video
)
) {
return ;
}
if ( this . video . paused || this . video . playbackRate === 1.0 ) {
this . stopSubtitleNudge ( ) ;
return ;
@@ -346,8 +396,8 @@ function defineVideoController() {
} ) ;
} , tc . settings . subtitleNudgeInterval ) ;
} ;
tc . videoController . prototype . stopSubtitleNudge = function ( ) {
// ADDED
if ( this . nudgeIntervalId !== null ) {
log ( ` Nudge: Stopping. ` , 5 ) ;
clearInterval ( this . nudgeIntervalId ) ;
@@ -355,34 +405,25 @@ function defineVideoController() {
}
} ;
tc . videoController . prototype . remove = function ( ) {
// Original remove
this . stopSubtitleNudge ( ) ; // ADDED
if ( this . div && this . div . parentNode ) this . div . remove ( ) ;
if ( this . video ) {
this . video . removeEventListener ( "play" , this . handlePlay ) ;
this . video . removeEventListener ( "pause" , this . handlePause ) ; // ADDED
this . video . removeEventListener ( "ended" , this . handleEnded ) ; // ADDED
this . video . removeEventListener ( "seeked" , this . handleSeek ) ; // Original was "seek"
delete this . video . vsc ;
}
let idx = tc . mediaElements . indexOf ( this . video ) ;
if ( idx !== - 1 ) tc . mediaElements . splice ( idx , 1 ) ;
} ;
tc . videoController . prototype . initializeControls = function ( ) {
/* ... Same as your provided ... */
const doc = this . video . ownerDocument ;
const speedForUI = this . video . playbackRate . toFixed ( 2 ) ;
var top = Math . max ( this . video . offsetTop , 0 ) + "px" ,
const speed = this . video . playbackRate . toFixed ( 2 ) ;
// Fix for videos rendered after page load - use relative positioning
var top = "10px" ,
left = "10px" ;
// Try to get actual position, but fallback to default if not available
if ( this . video . offsetTop > 0 || this . video . offsetLeft > 0 ) {
top = Math . max ( this . video . offsetTop , 0 ) + "px" ;
left = Math . max ( this . video . offsetLeft , 0 ) + "px" ;
}
var wrapper = doc . createElement ( "div" ) ;
wrapper . classList . add ( "vsc-controller" ) ;
if ( ! this . video . src && ! this . video . currentSrc )
wrapper . classList . add ( "vsc-nosource" ) ;
if ( tc . settings . startHidden ) wrapper . classList . add ( "vsc-hidden" ) ;
var shadow = wrapper . attachShadow ( { mode : "open" } ) ;
shadow . innerHTML = ` <style> @import " ${ chrome . runtime . getURL ( "shadow.css" ) } "; </style><div id="controller" style="top: ${ top } ; left: ${ left } ; opacity: ${ tc . settings . controllerOpacity } "><span data-action="drag" class="draggable"> ${ speedForUI } </span><span id="controls"><button data-action="rewind" class="rw">«</button><button data-action="slower">− </button><button data-action="faster">+</button><button data-action="advance" class="rw">»</button><button data-action="display" class="hideButton">× </button></span></div> ` ;
shadow . innerHTML = ` <style> @import " ${ chrome . runtime . getURL ( "shadow.css" ) } "; </style><div id="controller" style="top: ${ top } ; left: ${ left } ; opacity: ${ tc . settings . controllerOpacity } "><span data-action="drag" class="draggable"> ${ speed } </span><span id="controls"><button data-action="rewind" class="rw">«</button><button data-action="slower">− </button><button data-action="faster">+</button><button data-action="advance" class="rw">»</button><button data-action="display" class="hideButton">× </button></span></div> ` ;
this . speedIndicator = shadow . querySelector ( ".draggable" ) ;
shadow . querySelector ( ".draggable" ) . addEventListener (
"mousedown" ,
@@ -390,28 +431,26 @@ function defineVideoController() {
runAction (
e . target . dataset [ "action" ] ,
getKeyBindings ( e . target . dataset [ "action" ] , "value" ) ,
e ,
this . video
e
) ;
e . stopPropagation ( ) ;
} ,
true
) ;
shadow . querySelectorAll ( "button" ) . forEach ( ( bt n ) =>
bt n . addEventListener (
shadow . querySelectorAll ( "button" ) . forEach ( ( butto n ) => {
butto n . addEventListener (
"click" ,
( e ) => {
runAction (
e . target . dataset [ "action" ] ,
getKeyBindings ( e . target . dataset [ "action" ] ) ,
e ,
this . video
e
) ;
e . stopPropagation ( ) ;
} ,
true
)
) ;
} ) ;
shadow
. querySelector ( "#controller" )
. addEventListener ( "click" , ( e ) => e . stopPropagation ( ) , false ) ;
@@ -420,8 +459,8 @@ function defineVideoController() {
. addEventListener ( "mousedown" , ( e ) => e . stopPropagation ( ) , false ) ;
var fragment = doc . createDocumentFragment ( ) ;
fragment . appendChild ( wrapper ) ;
const pEl = this . parent || this . video . parentElement ;
if ( ! pEl || ! pEl . parentNode ) {
const parent El = this . parent || this . video . parentElement ;
if ( ! parent El || ! parent El . parentNode ) {
doc . body . appendChild ( fragment ) ;
return wrapper ;
}
@@ -429,24 +468,23 @@ function defineVideoController() {
case location . hostname == "www.amazon.com" :
case location . hostname == "www.reddit.com" :
case /hbogo\./ . test ( location . hostname ) :
pEl . parentElement . insertBefore ( fragment , pEl ) ;
parent El . parentElement . insertBefore ( fragment , parent El ) ;
break ;
case location . hostname == "www.facebook.com" :
let p =
pEl . parentElement . parentElement . parentElement . parentElement
parent El . parentElement . parentElement . parentElement . parentElement
. parentElement . parentElement . parentElement ;
if ( p && p . firstChild ) p . insertBefore ( fragment , p . firstChild ) ;
else if ( pEl . firstChild ) pEl . insertBefore ( fragment , pEl . firstChild ) ;
else pEl . appendChild ( fragment ) ;
else parent El . insertBefore ( fragment , parent El . firstChild ) ;
break ;
case location . hostname == "tv.apple.com" :
const r = pEl . getRootNode ( ) ;
const r = parent El . getRootNode ( ) ;
const s = r && r . querySelector ? r . querySelector ( ".scrim" ) : null ;
if ( s ) s . prepend ( fragment ) ;
else pEl . insertBefore ( fragment , pEl . firstChild ) ;
else parent El . insertBefore ( fragment , parent El . firstChild ) ;
break ;
default :
pEl . insertBefore ( fragment , pEl . firstChild ) ;
parent El . insertBefore ( fragment , parent El . firstChild ) ;
}
return wrapper ;
} ;
@@ -457,107 +495,75 @@ function escapeStringRegExp(str) {
return str . replace ( m , "\\$&" ) ;
}
function isBlacklisted ( ) {
/* ... Same as your provided ... */
let blacklisted = false ;
const bl = tc . settings . blacklist ? tc . settings . blacklist . split ( "\n" ) : [ ] ;
bl . forEach ( ( m ) => {
if ( blacklisted ) return ;
let b = false ;
const l = tc . settings . blacklist ? tc . settings . blacklist . split ( "\n" ) : [ ] ;
l . forEach ( ( m ) => {
if ( b ) return ;
m = m . replace ( regStrip , "" ) ;
if ( m . length == 0 ) return ;
let rgx ;
let r ;
if ( m . startsWith ( "/" ) && m . lastIndexOf ( "/" ) > 0 ) {
try {
const ls = m . lastIndexOf ( "/" ) ;
rgx = new RegExp ( m . substring ( 1 , ls ) , m . substring ( ls + 1 ) ) ;
r = new RegExp ( m . substring ( 1 , ls ) , m . substring ( ls + 1 ) ) ;
} catch ( e ) {
log ( ` Invalid regex: ${ m } . ${ e . message } ` , 2 ) ;
return ;
}
} else rgx = new RegExp ( escapeStringRegExp ( m ) ) ;
if ( rgx && rgx . test ( location . href ) ) blacklisted = true ;
} else r = new RegExp ( escapeStringRegExp ( m ) ) ;
if ( r && r . test ( location . href ) ) b = true ;
} ) ;
if ( blacklisted ) log ( ` Page ${ location . href } blacklisted. ` , 4 ) ;
return blacklisted ;
if ( b ) log ( ` Page ${ location . href } blacklisted. ` , 4 ) ;
return b ;
}
var coolDown = false ;
function refreshCoolDown ( ) {
/* ... Same as your provided ... */
if ( coolDown ) clearTimeout ( coolDown ) ;
coolDown = setTimeout ( ( ) => {
coolDown = setTimeout ( function ( ) {
coolDown = false ;
} , 1000 ) ;
}
function setupListener ( ) {
if ( document . vscRateListenerAttached ) return ;
// MODIFIED: This function NO LONGER calls runAction("blink")
function updateSpeedFromEvent ( video ) {
function updateSpeedFromEvent ( video , fromUserInput = false ) {
if ( ! video . vsc || ! video . vsc . speedIndicator ) return ;
var speed = Number ( video . playbackRate . toFixed ( 2 ) ) ;
log ( ` updateSpeedFromEvent: Rate is ${ speed } . ` , 4 ) ;
video . vsc . speedIndicator . textContent = speed . toFixed ( 2 ) ;
tc . settings . speeds [ video . currentSrc || "unknown_src" ] = speed ;
tc . settings . lastSpeed = speed ;
chrome . storage . sync . set ( { lastSpeed : speed } , ( ) => {
/* ... */
} ) ;
// Blink is now handled directly in setSpeed for user key presses
chrome . storage . sync . set ( { lastSpeed : speed } , ( ) => { } ) ;
if ( fromUserInput ) {
runAction ( "blink" , 1000 , null , video ) ;
}
if ( video . vsc ) {
if ( speed === 1.0 || video . paused ) video . vsc . stopSubtitleNudge ( ) ;
else video . vsc . startSubtitleNudge ( ) ;
}
}
document . addEventListener (
"ratechange" ,
function ( event ) {
if ( tc . isNudging ) return ; // ADDED: Ignore nudge events
if ( tc . isNudging ) return ;
if ( coolDown ) {
log ( "Blocked by coolDown" , 4 ) ;
event . stopImmediatePropagation ( ) ;
return ;
}
var video = event . target ;
if ( ! video || typeof video . playbackRate === "undefined" || ! video . vsc )
return ;
const eventOrigin = event . detail && event . detail . origin ;
// The 'fromUserInput' flag previously passed to updateSpeedFromEvent is removed.
// Blinking is now directly triggered by `setSpeed` if it's a user key press.
if ( tc . settings . forceLastSavedSpeed ) {
if ( eventO rigin === "videoSpeed" ) {
// This event is from setSpeed's forceLastSavedSpeed path.
// setSpeed would have already handled blinking if it was a user key press.
if ( event . detail . speed ) {
const detailSpeedNum = Number ( event . detail . speed ) ;
if (
! isNaN ( detailSpeedNum ) &&
Math . abs ( video . playbackRate - detailSpeedNum ) > 0.001
) {
video . playbackRate = detailSpeedNum ;
}
}
updateSpeedFromEvent ( video ) ; // Update state
event . stopImmediatePropagation ( ) ;
if ( event. detail && event . detail . o rigin === "videoSpeed" ) {
video . playbackRate = event . detail . speed ;
updateSpeedFromEvent ( video , event . detail . fromUserInput === true ) ;
} else {
if ( Math . abs ( video . playbackRate - tc . settings . lastSpeed ) > 0.001 ) {
video . playbackRate = tc . settings . lastSpeed ;
}
event . stopImmediatePropagation ( ) ;
} else {
updateSpeedFromEvent ( video ) ; // Just confirming speed
}
}
} else {
// forceLastSavedSpeed is OFF
updateSpeedFromEvent ( video ) ; // Update state
// DO NOT stop propagation
updateSpeedFromEvent ( video , video . vscIsDirectlySettingRate === true ) ;
if ( video . vscIsDirectlySettingRate )
delete video . vscIsDirectlySettingRate ;
}
} ,
true
@@ -567,36 +573,24 @@ function setupListener() {
var vscInitializedDocuments = new Set ( ) ;
function initializeWhenReady ( doc ) {
/* ... Same robust init from your code ... */
if ( doc . vscInitWhenReadyUniqueFlag1 && doc . readyState ! == "loading" ) return ;
doc . vscInitWhenReadyUniqueFlag1 = true ;
if ( isBlacklisted ( ) ) return ;
if ( doc === window . document && ! wind ow. vscPageLoadListenerUniqueFlag1 ) {
window . addEventListener ( "load" , ( ) => initializeNow ( window . document ) , {
if ( vscInitializedDocuments . has ( doc ) || ! doc . body ) return ;
if ( doc . readyState = == "complete" ) {
initializeNow ( doc ) ;
} else {
doc . addEventListener ( "DOMContentLoaded" , ( ) => initializeN ow ( doc ) , {
once : true
} ) ;
window . vscPageLoadListenerUniqueFlag1 = true ;
}
if ( doc . readyState === "complete" ) initializeNow ( doc ) ;
else if ( ! doc . vscReadyStateListenerUniqueFlag1 ) {
doc . addEventListener ( "readystatechange" , function onRSChangeUnique1 ( ) {
if ( doc . readyState === "complete" ) {
doc . removeEventListener ( "readystatechange" , onRSChangeUnique1 ) ;
initializeNow ( doc ) ;
}
} ) ;
doc . vscReadyStateListenerUniqueFlag1 = true ;
}
}
function inIframe ( ) {
/* ... Same ... */ try {
try {
return window . self !== window . top ;
} catch ( e ) {
return true ;
}
}
function getShadow ( parent ) {
/* ... Same ... */ let r = [ ] ;
let r = [ ] ;
function gC ( p ) {
if ( p . firstElementChild ) {
var c = p . firstElementChild ;
@@ -613,159 +607,169 @@ function getShadow(parent) {
}
function initializeNow ( doc ) {
/* ... Same robust init from your code ... */
if ( vscInitializedDocuments . has ( doc ) || ! doc . body ) return ;
if ( ! tc . settings . enabled ) return ;
if ( ! doc . body . classList . contains ( "vsc-initialized" ) )
doc . body . classList . add ( "vsc-initialized" ) ;
if ( typeof tc . videoController === "undefined" ) defineVideoController ( ) ;
setupListener ( ) ;
if (
inIframe ( ) &&
doc !== window . top . document &&
! doc . head . querySelector ( 'link[href*="inject.css"]' )
) {
var l = doc . createElement ( "link" ) ;
l . href = chrome . runtime . getURL ( "inject.css" ) ;
l . type = "text/css" ;
l . rel = "stylesheet" ;
doc . head . appendChild ( l ) ;
}
const dFK = new Set ( [ doc ] ) ;
var docs = Array ( doc ) ;
try {
if ( inIframe ( ) && window . top . document ) dFK . add ( window . top . document ) ;
if ( inIframe ( ) ) docs . push ( window . top . document ) ;
} catch ( e ) { }
dFK . forEach ( ( lD ) => {
if ( ! lD . vscKDLFlagC ) {
lD . addEventListener (
docs . forEach ( function ( d ) {
if ( d . vscKeydownListenerAttached ) return ; // Prevent duplicate listeners
d . addEventListener (
"keydown" ,
function ( evt ) {
if ( ! tc . settings . enabled ) return ;
const tgt = evt . target ;
function ( even t ) {
var keyCode = event . keyCode ;
if (
tgt . nodeName === "INPUT" ||
tg t . nodeName === "TEXTAREA" ||
tg t . isContentEditable
! event . getModifierState ||
even t. getModifierState ( "Alt" ) ||
even t. getModifierState ( "Control" ) ||
event . getModifierState ( "Fn" ) ||
event . getModifierState ( "Meta" ) ||
event . getModifierState ( "Hyper" ) ||
event . getModifierState ( "OS" )
)
return ;
if (
evt . getModifierState &&
( evt . getModifierState ( "Alt" ) ||
evt . getModifierState ( "Control" ) ||
evt . getModifierState ( "Meta" ) ||
evt . getModifierState ( "Fn" ) ||
evt . getModifierState ( "Hyper" ) ||
evt . getModifierState ( "OS" ) )
even t . target . nodeName === "INPUT" ||
even t . target . nodeName === "TEXTAREA" ||
even t . target . isContentEditable
)
return ;
if ( tc . mediaElements . length === 0 && ! lD . querySelector ( "video,audio" ) )
return ;
var itm = tc . settings . keyBindings . find ( ( k ) => k . key === evt . keyCode ) ;
if ( itm ) {
runAction ( itm . action , itm . value , evt ) ;
if ( itm . force === "true" || itm . force === true ) {
evt . preventDefault ( ) ;
evt . stopPropagation ( ) ;
if ( ! tc . mediaElements . length ) return ;
var item = tc . settings . keyBindings . find ( ( item ) => item . key === keyCode ) ;
if ( item ) {
runAction ( ite m . action , item . value , event ) ;
if ( ite m . force === "true" ) {
event . preventDefault ( ) ;
even t . stopPropagation ( ) ;
}
}
return false ;
} ,
true
) ;
lD . vscKDLFlagC = true ;
}
d . vscKeydownListenerAttached = true ;
} ) ;
if ( ! doc . vscMOFlagC ) {
const o = new MutationObserver ( ( m ) => {
if ( typeof requestIdleCallback === "function" )
requestIdleCallback ( ( ) => pM ( m ) , { timeout : 1000 } ) ;
else setTimeout ( ( ) => pM ( m ) , 200 ) ;
if ( ! doc . vscMutationObserverAttached ) {
const observer = new MutationObserver ( function ( mutations ) {
requestIdleCallback (
( _ ) => {
mutations . forEach ( function ( mutation ) {
switch ( mutation . type ) {
case "childList" :
mutation . addedNodes . forEach ( function ( node ) {
if ( typeof node === "function" ) return ;
checkForVideo ( node , node . parentNode || mutation . target , true ) ;
} ) ;
function pM ( ml ) {
for ( const m of ml ) {
if ( m . type === "childList" ) {
m . addedN odes . forEach ( ( n ) => {
if ( n instanceof Element ) cV ( n , n . parentNode || m . target , true ) ;
} ) ;
m . removedNodes . forEach ( ( n ) => {
if ( n instanceof Element ) cV ( n , n . parentNode || m . target , false ) ;
} ) ;
} else if (
m . type === "attributes" &&
m . attributeName === "aria-hidden" &&
m . target instanceof Element &&
m . target . getAttribute ( "aria-hidden" ) === "false"
) {
const vIT = Array . from ( getShadow ( m . target ) ) . filter (
( el ) => el . tagName === "VIDEO"
mutation . removedNodes . forEach ( function ( node ) {
i f ( typeof node === "function" ) return ;
checkForVideo (
n ode ,
node . parentNode || mutation . target ,
false
) ;
vIT . forEach ( ( vE ) => {
if ( ! vE . vsc ) cV ( vE , vE . parentNode || m . target , true ) ;
} ) ;
}
}
}
function cV ( n , p , a ) {
if ( ! a && ! n . isConnected ) {
} else if ( ! a && n . isConnected ) return ;
break ;
case "attributes" :
if (
n . nodeName === "VIDEO" ||
( n . nodeName === "AUDIO" && tc . settings . audioBoolean )
mutation . target . attributes [ "aria-hidden" ] &&
mutation . target . attributes [ "aria-hidden" ] . value == "false"
) {
if ( a ) {
if ( ! n . vsc ) new tc . videoController ( n , p ) ;
} else {
if ( n . vsc ) n . vsc . remove ( ) ;
}
} else if ( n . children && n . children . length > 0 ) {
for ( let i = 0 ; i < n . children . length ; i ++ )
cV ( n . children [ i ] , n . children [ i ] . parentNode || p , a ) ;
var flattenedNodes = getShadow ( document . body ) ;
var node = flattenedNodes . filter (
( x ) => x . tagName == "VIDEO"
) [ 0 ] ;
if ( node ) {
if ( node . vsc ) node . vsc . remove ( ) ;
checkForVideo (
node ,
node . parentNode || mutation . target ,
true
) ;
}
}
o . observe ( doc , {
childList : true ,
subtree : true ,
attributes : true ,
attributeFilter : [ "aria-hidden" ]
break ;
}
} ) ;
doc . vscMOFlagC = true ;
} ,
{ timeout : 1000 }
) ;
} ) ;
function checkForVideo ( node , parent , added ) {
if ( ! added && document . body . contains ( node ) ) return ;
if (
node . nodeName === "VIDEO" ||
( node . nodeName === "AUDIO" && tc . settings . audioBoolean )
) {
if ( added ) {
if ( ! node . vsc ) node . vsc = new tc . videoController ( node , parent ) ;
} else {
if ( node . vsc ) node . vsc . remove ( ) ;
}
} else if ( node . children != undefined ) {
for ( var i = 0 ; i < node . children . length ; i ++ ) {
const child = node . children [ i ] ;
checkForVideo ( child , child . parentNode || parent , added ) ;
}
}
}
observer . observe ( doc , {
attributeFilter : [ "aria-hidden" ] ,
childList : true ,
subtree : true
} ) ;
doc . vscMutationObserverAttached = true ;
}
const q = tc . settings . audioBoolean ? "video,audio" : "video" ;
doc . querySelectorAll ( q ) . forEach ( ( v ) => {
const foundVideos = doc . querySelectorAll ( q ) ;
foundVideos . forEach ( ( v ) => {
if ( ! v . vsc ) new tc . videoController ( v , v . parentElement ) ;
} ) ;
Array . from ( doc . getElementsByTagName ( "iframe" ) ) . forEach ( ( f ) => {
if ( f . vscLoadListenerAttached ) return ;
f . addEventListener ( "load" , ( ) => {
try {
if ( f . contentDocument ) initializeWhenReady ( f . contentDocument ) ;
} catch ( e ) { }
if ( f . contentDocument ) {
initializeWhenReady ( f . contentDocument ) ;
}
} catch ( e ) {
// Silently ignore CORS errors
}
} ) ;
f . vscLoadListenerAttached = true ;
try {
if ( f . contentDocument ) {
initializeWhenReady ( f . contentDocument ) ;
}
} catch ( e ) {
// Silently ignore CORS errors
}
} ) ;
vscInitializedDocuments . add ( doc ) ;
}
// MODIFIED: setSpeed now takes `isInitialCall` and `isUserKeyPress`
function setSpeed ( video , speed , isInitialCall = false , isUserKeyPress = false ) {
const numericSpeed = Number ( speed ) ;
if ( isNaN ( numericSpeed ) || numericSpeed <= 0 || numericSpeed > 16 ) return ;
if ( ! video || ! video . vsc || ! video . vsc . speedIndicator ) return ;
log (
` setSpeed: Target ${ numericSpeed . toFixed ( 2 ) } . Initial: ${ isInitialCall } . UserKeyPress: ${ isUserKeyPress } ` ,
4
) ;
tc . settings . lastSpeed = numericSpeed ;
video . vsc . speedIndicator . textContent = numericSpeed . toFixed ( 2 ) ;
// MODIFIED: Directly trigger blink here if it's a user key press and not initial setup
if ( isUserKeyPress && ! isInitialCall && video . vsc ) {
// Ensure controller is available before trying to blink
if ( video . vsc . div ) {
// Check if controller div exists
log (
` setSpeed: User key press detected, triggering blink for controller. ` ,
5
) ;
// Pass the specific video to runAction for blink
runAction ( "blink" , getKeyBindings ( "blink" , "value" ) || 1000 , null , video ) ;
}
if ( isUserKeyPress && ! isInitialCall && video . vsc && video . vsc . div ) {
runAction ( "blink" , 1000 , null , video ) ; // Pass video to blink
}
if ( tc . settings . forceLastSavedSpeed ) {
@@ -775,80 +779,54 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
origin : "videoSpeed" ,
speed : numericSpeed . toFixed ( 2 ) ,
fromUserInput : isUserKeyPress
} // Pass isUserKeyPress
}
} )
) ;
} else {
if ( Math . abs ( video . playbackRate - numericSpeed ) > 0.001 ) {
if ( isUserKeyPress && ! isInitialCall ) {
video . vscIsDirectlySettingRate = true ; // Set flag for ratechange listener
}
video . playbackRate = numericSpeed ;
}
}
if ( ! isInitialCall ) refreshCoolDown ( ) ; // Original call
if ( ! isInitialCall ) refreshCoolDown ( ) ;
if ( video . vsc ) {
if ( numericSpeed === 1.0 || video . paused ) video . vsc . stopSubtitleNudge ( ) ;
else video . vsc . startSubtitleNudge ( ) ;
}
}
// MODIFIED: r unA ction passes `isUserKeyPress=true` to setSpeed for relevant actions
function runAction ( action , value , e , specificVideo = null ) {
var mediaTagsToProcess = [ ] ; // ... (same robust mediaTagsToProcess logic as before) ...
if ( specificVideo ) mediaTagsToProcess = [ specificVideo ] ;
else if ( e && e . target ) {
const dC = e . target . ownerDocument || document ;
let aV = tc . mediaElements . find (
( v ) =>
v . ownerDocument === dC &&
( dC . activeElement === v || v . contains ( dC . activeElement ) )
) ;
if ( aV ) mediaTagsToProcess = [ aV ] ;
else {
aV = tc . mediaElements . find (
( v ) =>
v . ownerDocument === dC &&
v . offsetParent !== null &&
( ! v . paused || v . readyState > 0 )
) ;
if ( aV ) mediaTagsToProcess = [ aV ] ;
else {
mediaTagsToProcess = tc . mediaElements . filter (
( v ) => v . ownerDocument === dC
) ;
if ( mediaTagsToProcess . length === 0 && tc . mediaElements . length > 0 )
mediaTagsToProcess = [ tc . mediaElements [ 0 ] ] ;
else if ( mediaTagsToProcess . length === 0 ) mediaTagsToProcess = [ ] ;
}
}
} else mediaTagsToProcess = tc . mediaElements ;
if (
mediaTagsToProcess . length === 0 &&
action !== "display" &&
action !== "blink"
)
return ; // Allow blink even if no media for global feedback
f unction runAction ( action , value , e ) {
log ( "runAction Begin" , 5 ) ;
var mediaTagsToProcess ;
const specificVideo = arguments [ 3 ] || null ;
var targetControllerFromEvent =
e && e . target && e . target . getRootNode && e . target . getRootNode ( ) . host
? e . target . getRootNode ( ) . host
: null ;
const currentAction Context = action ;
if ( specificVideo ) {
mediaTagsToProcess = [ specificVideo ] ;
} else if ( e && e . target && e . target . getRootNode ) {
// Event-driven action
const doc Context = e . target . ownerDocument || document ;
mediaTagsToProcess = tc . mediaElements . filter (
( v ) => v . ownerDocument === docContext
) ;
const targetController = e . target . getRootNode ( ) . host ;
if ( targetController ) {
// If it's a click on a controller, only use that one video
const videoFromController = tc . mediaElements . find (
( v ) => v . vsc && v . vsc . div === targetController
) ;
if ( videoFromController ) mediaTagsToProcess = [ videoFromController ] ;
}
} else {
mediaTagsToProcess = tc . mediaElements ;
}
if ( mediaTagsToProcess . length === 0 && action !== "display" ) return ;
mediaTagsToProcess . forEach ( function ( v ) {
if ( ! v || ! v . vsc || ! v . vsc . div || ! v . vsc . speedIndicator ) return ;
var controllerDiv = v . vsc . div ;
if (
targetControllerFromEvent &&
targetControllerFromEvent !== controllerDiv &&
action !== "blink"
)
return ;
if ( action === "blink" && specificVideo && v !== specificVideo ) return ;
// MODIFIED: `showController` is only called if action implies user directly interacting with speed/video state.
// "display" handles its own visibility. "blink" is for feedback *after* a state change.
const actionsThatShouldShowControllerTemporarily = [
if ( ! v . vsc ) return ; // Don't process videos without a controller
var controller = v . vsc . div ;
const userDrivenActionsThatShowController = [
"rewind" ,
"advance" ,
"faster" ,
@@ -861,24 +839,20 @@ function runAction(action, value, e, specificVideo = null) {
"jump" ,
"drag"
] ;
if ( a ctionsThatShouldSho wControllerTemporarily . includes ( action ) ) {
// The original showController is a timed visibility.
// The "blink" action also provides timed visibility.
// By having setSpeed call blink directly for user key presses, this might be redundant here for speed changes.
// However, for seek, pause etc., this existing showController is still relevant.
showController ( controllerDiv ) ;
if ( userDrivenA ctionsThatShowController. includes ( action ) ) {
showController ( controller ) ;
}
if ( v . classList . contains ( "vsc-cancelled" ) ) return ;
const numValue = parseFloat ( value ) ;
switch ( action ) {
case "rewind" :
isUserSeek = true ;
v . currentTime -= numValue ;
break ;
case "advance" :
isUserSeek = true ;
v . currentTime += numValue ;
break ;
// MODIFIED: Pass `isUserKeyPress = true`
case "faster" :
setSpeed (
v ,
@@ -893,42 +867,34 @@ function runAction(action, value, e, specificVideo = null) {
case "slower" :
setSpeed ( v , Math . max ( v . playbackRate - numValue , 0.07 ) , false , true ) ;
break ;
// MODIFIED: Calls original resetSpeed, passing currentActionContext
case "reset" :
resetSpeed ( v , 1.0 , currentActionContext ) ;
resetSpeed ( v , 1.0 , false ) ; // Use enhanced resetSpeed
break ;
case "fast" :
resetSpeed ( v , numValue , currentActionContext ) ;
resetSpeed ( v , numValue , true ) ; // Use enhanced resetSpeed
break ;
case "display" :
controllerDiv . classList . add ( "vsc-manual" ) ;
controllerDiv . classList . toggle ( "vsc-hidden" ) ;
controller . classList . add ( "vsc-manual" ) ;
controller . classList . toggle ( "vsc-hidden" ) ;
break ;
case "blink" : // This action is now primarily called by setSpeed itself for user key presses
if ( ! controllerDiv ) return ; // Safety check
case "blink" :
if (
controllerDiv . classList . contains ( "vsc-hidden" ) ||
controllerDiv . blinkTimeOut !== undefined
controller . classList . contains ( "vsc-hidden" ) ||
controller . blinkTimeOut !== undefined
) {
clearTimeout ( controllerDiv . blinkTimeOut ) ;
controllerDiv . classList . remove ( "vsc-hidden" ) ;
controllerDiv . blinkTimeOut = setTimeout (
( ) => {
// If user manually set controller to be visible (vsc-manual and NOT vsc-hidden), blink should not hide it.
clearTimeout ( controller . blinkTimeOut ) ;
controller . classList . remove ( "vsc-hidden" ) ;
controller . blinkTimeOut = setTimeout ( ( ) => {
if (
controllerDiv . classList . contains ( "vsc-manual" ) &&
! controllerDiv . classList . contains ( "vsc-hidden" )
! (
controller . classList . contains ( "vsc-manual" ) &&
! controller . classList . contains ( "vsc-hidden" )
)
) {
// Do nothing, respect manual visibility
} else {
// Otherwise, (it was auto-shown by blink, or was already hidden, or user manually hid it)
// blink will ensure it ends up hidden.
controllerDiv . classList . add ( "vsc-hidden" ) ;
controller . classList . add ( "vsc-hidden" ) ;
}
controllerDiv . blinkTimeOut = undefined ;
} ,
typeof value === "number" && ! isNaN ( value ) ? value : 1000
) ; // Value for blink duration
controller . blinkTimeOut = undefined ;
} , numValue || 1000 ) ; // FIXED: Use numValue for consistency
}
break ;
case "drag" :
@@ -946,125 +912,98 @@ function runAction(action, value, e, specificVideo = null) {
case "jump" :
jumpToMark ( v ) ;
break ;
default :
log ( ` Unknown action: ${ action } ` , 3 ) ;
}
} ) ;
log ( "runAction End" , 5 ) ;
}
function pause ( v ) {
/* ... Same as your original ... */ if ( v . paused )
v . play ( ) . catch ( ( e ) => log ( ` Play err: ${ e . message } ` , 2 ) ) ;
if ( v . paused ) v . play ( ) . catch ( ( e ) => log ( ` Play err: ${ e . message } ` , 2 ) ) ;
else v . pause ( ) ;
}
// MODIFIED: resetSpeed now calls setSpeed with isUserKeyPress = true
function resetSpeed ( v , target , currentActionContext = null ) {
log (
` resetSpeed (original): Video current: ${ v . playbackRate . toFixed ( 2 ) } , Target: ${ target . toFixed ( 2 ) } , Context: ${ currentActionContext } ` ,
4
) ;
if ( Math . abs ( v . playbackRate - target ) < 0.01 ) {
if ( v . playbackRate === ( getKeyBindings ( "reset" , "value" ) || 1.0 ) ) {
if ( target !== 1.0 ) {
setSpeed ( v , 1 .0, false , true ) ;
function resetSpeed ( v , target , isFastKey = false ) {
const videoId = v . currentSrc || v . src || "default" ;
const currentSpeed = v . playbackRate ;
if ( isFastKey ) {
// G key: Toggle between current speed and preferred speed (e.g., 1.8)
const preferredSpeed = target ;
const lastToggle = lastToggleSpeed [ videoId ] || currentSpeed ;
if ( Math . abs ( currentSpeed - preferredSpeed ) < 0 .01 ) {
// Currently at preferred speed, toggle to the last speed
setSpeed ( v , lastToggle , false , true ) ;
} else {
setSpeed ( v , getKeyBindings ( "fast" , "value" ) , false , true ) ;
// Not at preferred speed, save current as toggle speed and go to preferred
lastToggleSpeed [ videoId ] = currentSpeed ;
setSpeed ( v , preferredSpeed , false , true ) ;
}
} else {
setSpeed ( v , getKeyBindings ( "reset" , "value" ) || 1.0 , false , true ) ;
}
// R key: Toggle between current speed and 1.0
const resetSpeedValue = 1.0 ;
const lastToggle = lastToggleSpeed [ videoId ] || currentSpeed ;
if ( Math . abs ( currentSpeed - resetSpeedValue ) < 0.01 ) {
// Currently at 1.0, toggle to the last speed (or 1.8 if no history)
const speedToRestore =
Math . abs ( lastToggle - 1.0 ) < 0.01
? getKeyBindings ( "fast" ) || 1.8
: lastToggle ;
setSpeed ( v , speedToRestore , false , true ) ;
} else {
if ( currentActionContext === "reset" ) {
setKeyBindings ( "reset" , v . playbackRate ) ;
// Not at 1.0, save current as toggle speed and go to 1.0
lastToggleSpeed [ videoId ] = currentSpeed ;
setSpeed ( v , resetSpeedValue , false , true ) ;
}
setSpeed ( v , target , false , true ) ;
}
}
function muted ( v ) {
/* ... Same as your original ... */ v . muted = ! v . muted ;
log ( ` Mute: ${ v . muted } ` , 5 ) ;
v . muted = ! v . muted ;
}
function setMark ( v ) {
/* ... Same as your original ... */ if ( ! v . vsc ) v . vsc = { } ;
v . vsc . mark = v . currentTime ;
log ( ` Mark: ${ v . vsc . mark . toFixed ( 2 ) } ` , 5 ) ;
}
function jumpToMark ( v ) {
/* ... Same as your original ... */ if (
v . vsc &&
typeof v . vsc . mark === "number"
)
v . currentTime = v . vsc . mark ;
else log ( "No mark." , 4 ) ;
if ( v . vsc && typeof v . vsc . mark === "number" ) v . currentTime = v . vsc . mark ;
}
function handleDrag ( video , e ) {
/* ... Same as your original ... */
if ( ! video || ! video . vsc || ! video . vsc . div || ! video . vsc . div . shadowRoot )
return ;
const ctl = video . vsc . div ;
const sCtl = ctl . shadowRoot . querySelector ( "#controller" ) ;
if ( ! sCtl ) return ;
var pE = ctl . parentElement ;
const c = video . vsc . div ;
const sC = c . shadowRoot . querySelector ( "#controller" ) ;
var pE = c . parentElement ;
while (
pE &&
pE . parentNode &&
pE . parentNode !== document &&
pE . parentNode . offsetHeight === pE . offsetHeight &&
pE . parentNode . offsetWidth === pE . offsetWidth
)
pE = pE . parentNode ;
const dB = pE || video . ownerDocument . body ;
video . classList . add ( "vcs-dragging" ) ;
sCtl . classList . add ( "dragging" ) ;
sC . classList . add ( "dragging" ) ;
const iXY = [ e . clientX , e . clientY ] ,
iCtl XY = [
parseInt ( sCtl . style . left , 10 ) || 0 ,
parseInt ( sCtl . style . top , 10 ) || 0
] ;
const sD = ( mE ) => {
let s = sCtl . style ;
s . left = iCtlXY [ 0 ] + mE . clientX - iXY [ 0 ] + "px" ;
s . top = iCtlXY [ 1 ] + mE . clientY - iXY [ 1 ] + "px" ;
mE . preventDefault ( ) ;
iCXY = [ parseInt ( sC . style . left ) , parseInt ( sC . style . top ) ] ;
const sD = ( e ) => {
let s = sC . style ;
s . left = iCXY [ 0 ] + e . clientX - iXY [ 0 ] + "px" ;
s . top = iCXY [ 1 ] + e . clientY - iXY [ 1 ] + "px" ;
} ;
const eD = ( ) => {
dB . removeEventListener ( "mousemove" , sD ) ;
dB . removeEventListener ( "mouseup" , eD ) ;
dB . removeEventListener ( "mouseleave" , eD ) ;
sCtl . classList . remove ( "dragging" ) ;
pE . removeEventListener ( "mousemove" , sD ) ;
pE . removeEventListener ( "mouseup" , eD ) ;
pE . removeEventListener ( "mouseleave" , eD ) ;
sC . classList . remove ( "dragging" ) ;
video . classList . remove ( "vcs-dragging" ) ;
} ;
dB . addEventListener ( "mousemove " , s D) ;
dB . addEventListener ( "mouseup " , eD ) ;
dB . addEventListener ( "mouselea ve" , e D) ;
pE . addEventListener ( "mouseup " , e D) ;
pE . addEventListener ( "mouseleave " , eD ) ;
pE . addEventListener ( "mousemo ve" , s D) ;
}
var timer = null ;
function showController ( controller ) {
/* ... Same as your original ... */
if ( ! controller || typeof controller . classList === "undefined" ) return ;
// If controller is manually hidden by user (V pressed to hide), don't auto-show it
// The vsc-manual class is added when 'display' action is triggered.
// vsc-hidden is toggled by 'display' action.
// So, if vsc-manual AND vsc-hidden are present, user explicitly hid it.
if (
controller . classList . contains ( "vsc-manual" ) &&
controller . classList . contains ( "vsc-hidden" )
) {
// log("Controller is manually hidden by user, showController will not override.", 5);
return ;
}
controller . classList . add ( "vcs-show" ) ; // For autohide sites like YouTube
// The blink action has its own logic for vsc-hidden and vsc-manual.
// This showController is for the general "make it visible for a bit" from user actions.
// It should not remove vsc-hidden if it was there before adding vsc-show, because blink handles that.
// The original showController just adds vsc-show and sets a timer to remove it.
// The "blink" action in runAction explicitly removes vsc-hidden if present, then adds it back.
controller. classList . add ( "vsc-show" ) ;
if ( timer ) clearTimeout ( timer ) ;
timer = setTimeout ( function ( ) {
if ( controller && controller . classList )
controller . classList . remove ( "vsc-show" ) ;
timer = false ;
} , 2000 ) ;