@@ -98,17 +98,18 @@ object FingerprintScriptGenerator {
9898 Document.prototype.hasFocus = function() { return true; };
9999 } catch(e) {}
100100
101- // 2. Prevent YouTube from auto-pausing on tab change: intercept visibilitychange and pagehide
101+ // 2. Prevent YouTube from auto-pausing on tab change / backgrounding
102+ const blockedEvents = ['visibilitychange', 'webkitvisibilitychange', 'pagehide', 'blur', 'focusout', 'freeze'];
102103 try {
103104 const origAddEventListener = EventTarget.prototype.addEventListener;
104105 EventTarget.prototype.addEventListener = function(type, listener, options) {
105- if (type === 'visibilitychange' || type === 'webkitvisibilitychange' ) {
106+ if (typeof type === 'string' && blockedEvents.indexOf( type.toLowerCase()) !== -1 ) {
106107 return;
107108 }
108109 return origAddEventListener.apply(this, arguments);
109110 };
110111
111- ['visibilitychange', 'webkitvisibilitychange', 'pagehide'] .forEach(function(evt) {
112+ blockedEvents .forEach(function(evt) {
112113 window.addEventListener(evt, function(e) {
113114 if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
114115 if (e && e.stopPropagation) e.stopPropagation();
@@ -118,8 +119,51 @@ object FingerprintScriptGenerator {
118119 if (e && e.stopPropagation) e.stopPropagation();
119120 }, true);
120121 });
122+
123+ try {
124+ Object.defineProperty(window, 'onblur', { get: function() { return null; }, set: function(v) {}, configurable: true });
125+ Object.defineProperty(window, 'onpagehide', { get: function() { return null; }, set: function(v) {}, configurable: true });
126+ Object.defineProperty(document, 'onvisibilitychange', { get: function() { return null; }, set: function(v) {}, configurable: true });
127+ } catch(e) {}
121128 } catch(e) {}
122129
130+ // Track genuine user interaction vs automatic tab-switch pause
131+ let isUserAction = false;
132+ let userInteractionTimer = null;
133+ let programmaticPauseAllowed = false;
134+
135+ function markUserAction() {
136+ isUserAction = true;
137+ if (userInteractionTimer) clearTimeout(userInteractionTimer);
138+ userInteractionTimer = setTimeout(function() {
139+ isUserAction = false;
140+ }, 1000);
141+ }
142+
143+ ['click', 'pointerdown', 'touchend', 'keydown'].forEach(function(evt) {
144+ window.addEventListener(evt, markUserAction, true);
145+ document.addEventListener(evt, markUserAction, true);
146+ });
147+
148+ // Wrap HTMLMediaElement pause to prevent unwanted tab switch pauses
149+ const origPlay = HTMLMediaElement.prototype.play;
150+ const origPause = HTMLMediaElement.prototype.pause;
151+ HTMLMediaElement.prototype.pause = function() {
152+ if (programmaticPauseAllowed || isUserAction) {
153+ return origPause.apply(this, arguments);
154+ }
155+ // Auto-pause detected from background/tab switch; immediately resume playback
156+ const media = this;
157+ origPause.apply(this, arguments);
158+ setTimeout(function() {
159+ if (!programmaticPauseAllowed && !isUserAction && media.paused && !media.ended) {
160+ try {
161+ origPlay.call(media).catch(function() {});
162+ } catch(e) {}
163+ }
164+ }, 40);
165+ };
166+
123167 // 3. Spoof IntersectionObserver for video / player elements so YouTube mobile doesn't pause when offscreen
124168 try {
125169 if ('IntersectionObserver' in window) {
@@ -303,32 +347,33 @@ object FingerprintScriptGenerator {
303347 };
304348
305349 window.__feather_media_pause = function() {
350+ programmaticPauseAllowed = true;
306351 try {
307352 if (window.__feather_actions && typeof window.__feather_actions['pause'] === 'function') {
308353 window.__feather_actions['pause']();
309- return;
310354 }
311355 } catch(e) {}
312356 try {
313357 const moviePlayer = document.getElementById('movie_player');
314358 if (moviePlayer && typeof moviePlayer.pauseVideo === 'function') {
315359 moviePlayer.pauseVideo();
316- return;
317360 }
318361 } catch(e) {}
319362 try {
320363 const pauseBtn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Pause"]');
321364 if (pauseBtn) {
322365 pauseBtn.click();
323- return;
324366 }
325367 } catch(e) {}
326368 try {
327369 const mediaEls = document.querySelectorAll('video, audio');
328370 mediaEls.forEach(function(m) {
329- if (!m.paused) m.pause( );
371+ if (!m.paused) origPause.call(m );
330372 });
331373 } catch(e) {}
374+ setTimeout(function() {
375+ programmaticPauseAllowed = false;
376+ }, 400);
332377 };
333378
334379 window.__feather_media_toggle = function() {
@@ -448,4 +493,58 @@ object FingerprintScriptGenerator {
448493 })();
449494 """ .trimIndent()
450495 }
496+
497+ /* *
498+ * Injects CSS and JS overrides to ensure websites adhere to the browser's theme setting
499+ * (e.g. Light mode when selected, preventing unwanted dark mode detection).
500+ */
501+ fun generateThemeScript (isDark : Boolean ): String {
502+ val targetScheme = if (isDark) " dark" else " light"
503+ return """
504+ (function() {
505+ try {
506+ const targetScheme = '$targetScheme ';
507+ const isDark = $isDark ;
508+
509+ // 1. Set documentElement color-scheme
510+ if (document.documentElement) {
511+ document.documentElement.style.colorScheme = targetScheme;
512+ }
513+
514+ // 2. Add or update meta color-scheme tag
515+ let meta = document.querySelector('meta[name="color-scheme"]');
516+ if (!meta) {
517+ meta = document.createElement('meta');
518+ meta.name = 'color-scheme';
519+ if (document.head) document.head.appendChild(meta);
520+ }
521+ if (meta) {
522+ meta.content = targetScheme;
523+ }
524+
525+ // 3. Spoof window.matchMedia for prefers-color-scheme media queries
526+ const origMatchMedia = window.matchMedia;
527+ window.matchMedia = function(query) {
528+ if (!query) return origMatchMedia ? origMatchMedia.call(window, query) : null;
529+ const q = String(query).toLowerCase();
530+ if (q.indexOf('prefers-color-scheme') !== -1) {
531+ const matches = isDark ? (q.indexOf('dark') !== -1) : (q.indexOf('light') !== -1);
532+ const mql = origMatchMedia ? origMatchMedia.call(window, query) : {};
533+ return {
534+ matches: matches,
535+ media: query,
536+ onchange: null,
537+ addListener: function(fn) { if (mql && mql.addListener) mql.addListener(fn); },
538+ removeListener: function(fn) { if (mql && mql.removeListener) mql.removeListener(fn); },
539+ addEventListener: function(type, fn, opt) { if (mql && mql.addEventListener) mql.addEventListener(type, fn, opt); },
540+ removeEventListener: function(type, fn, opt) { if (mql && mql.removeEventListener) mql.removeEventListener(type, fn, opt); },
541+ dispatchEvent: function(e) { return mql && mql.dispatchEvent ? mql.dispatchEvent(e) : true; }
542+ };
543+ }
544+ return origMatchMedia ? origMatchMedia.call(window, query) : null;
545+ };
546+ } catch(e) {}
547+ })();
548+ """ .trimIndent()
549+ }
451550}
0 commit comments