11import { PlatformAdapter , ItemChangeCallback } from '../../core/interfaces/platform-adapter' ;
22import { PlatformId } from '../../types' ;
3- import { StorageChangeListener } from '../../core/storage/driver ' ;
4- import { StorageFacade } from '../../core/storage/index ' ;
3+ import { isYouTubeUrl , getVideoId , getPlatformType } from './parser ' ;
4+ import { YouTubeDomEnforcer } from './enforcer ' ;
55
66export class YouTubeAdapter implements PlatformAdapter {
77 public readonly id : PlatformId ;
88 public name : string ;
99
10- // Dynamic threshold: 1.5s for Shorts, 10s for regular Watch videos
1110 private get WATCH_THRESHOLD ( ) {
1211 return this . id === 'youtube_shorts' ? 1500 : 10000 ;
1312 }
1413
15- private readonly BLOCKED_KEYS = [ ' ' , 'k' , 'ArrowUp' , 'ArrowDown' ] ;
16-
17- private storageListener : StorageChangeListener | null = null ;
1814 private callback : ItemChangeCallback | null = null ;
19-
2015 private lastVideoId : string | null = null ;
2116 private pendingVideoId : string | null = null ;
2217
23- private punishmentObserver : MutationObserver | null = null ;
24- private keydownHandler : ( ( e : KeyboardEvent ) => void ) | null = null ;
25-
2618 private ytNavigationHandler : ( ( e : Event ) => void ) | null = null ;
2719 private popstateHandler : ( ( ) => void ) | null = null ;
20+ private visibilityHandler : ( ( ) => void ) | null = null ;
2821 private urlCheckInterval : number | null = null ;
2922
3023 private watchTimer : number | null = null ;
3124 private isWatching : boolean = false ;
32- private visibilityHandler : ( ( ) => void ) | null = null ;
33-
3425 private countedVideos = new Set < string > ( ) ;
3526
27+ private enforcer = new YouTubeDomEnforcer ( ) ;
28+
3629 constructor (
37- private storage : StorageFacade ,
3830 currentUrl : string ,
3931 private onModeChange : ( ) => void ,
4032 ) {
41- const isShorts = currentUrl . includes ( '/shorts/' ) ;
42- this . id = isShorts ? 'youtube_shorts' : 'youtube_watch' ;
43- this . name = isShorts ? 'YouTube Shorts' : 'YouTube Watch' ;
33+ this . id = getPlatformType ( currentUrl ) ;
34+ this . name = this . id === 'youtube_shorts' ? 'YouTube Shorts' : 'YouTube Watch' ;
4435 }
4536
4637 private startOrResumeWatchTimer ( id : string ) {
@@ -49,15 +40,13 @@ export class YouTubeAdapter implements PlatformAdapter {
4940
5041 this . isWatching = true ;
5142 this . watchTimer = window . setTimeout ( ( ) => {
52- const currentId = this . getVideoId ( window . location . href ) ;
53-
43+ const currentId = getVideoId ( window . location . href ) ;
5444 if ( ! document . hidden && currentId === id ) {
5545 if ( ! this . countedVideos . has ( id ) ) {
5646 this . countedVideos . add ( id ) ;
5747 if ( this . callback ) this . callback ( id ) ;
5848 }
5949 }
60-
6150 this . isWatching = false ;
6251 this . watchTimer = null ;
6352 } , this . WATCH_THRESHOLD ) ;
@@ -74,14 +63,13 @@ export class YouTubeAdapter implements PlatformAdapter {
7463 private handleVideoChange ( id : string ) {
7564 this . pendingVideoId = id ;
7665 this . pauseWatchTimer ( ) ;
77-
7866 if ( ! document . hidden ) {
7967 this . startOrResumeWatchTimer ( id ) ;
8068 }
8169 }
8270
8371 public isCurrentPlatform ( url : string ) : boolean {
84- return url . includes ( 'youtube.com' ) ;
72+ return isYouTubeUrl ( url ) ;
8573 }
8674
8775 public observe ( onItemChange : ItemChangeCallback ) : void {
@@ -92,26 +80,13 @@ export class YouTubeAdapter implements PlatformAdapter {
9280 if ( document . hidden ) {
9381 this . pauseWatchTimer ( ) ;
9482 } else {
95- if ( this . pendingVideoId && this . pendingVideoId === this . getVideoId ( window . location . href ) ) {
83+ if ( this . pendingVideoId && this . pendingVideoId === getVideoId ( window . location . href ) ) {
9684 this . startOrResumeWatchTimer ( this . pendingVideoId ) ;
9785 }
9886 }
9987 } ;
10088 document . addEventListener ( 'visibilitychange' , this . visibilityHandler ) ;
10189
102- this . storageListener = (
103- changes : Record < string , import ( '../../core/storage/driver' ) . StorageChange > ,
104- ) => {
105- if ( changes [ `limitra_${ this . id } _count` ] || changes [ `limitra_${ this . id } _time_spent` ] ) {
106- void this . storage . isCurrentlyBlocked ( this . id ) . then ( ( isBlocked : boolean ) => {
107- if ( isBlocked ) {
108- this . executePunishment ( ) ;
109- }
110- } ) ;
111- }
112- } ;
113- this . storage . onChange ( this . storageListener ) ;
114-
11590 this . observeUrlChanges ( ) ;
11691 }
11792
@@ -123,117 +98,40 @@ export class YouTubeAdapter implements PlatformAdapter {
12398 window . clearInterval ( this . urlCheckInterval ) ;
12499 this . urlCheckInterval = null ;
125100 }
126-
127101 if ( this . ytNavigationHandler ) {
128102 window . removeEventListener ( 'yt-navigate-finish' , this . ytNavigationHandler ) ;
129103 this . ytNavigationHandler = null ;
130104 }
131-
132105 if ( this . popstateHandler ) {
133106 window . removeEventListener ( 'popstate' , this . popstateHandler ) ;
134107 this . popstateHandler = null ;
135108 }
136-
137109 if ( this . visibilityHandler ) {
138110 document . removeEventListener ( 'visibilitychange' , this . visibilityHandler ) ;
139111 this . visibilityHandler = null ;
140112 }
141113
142- if ( this . storageListener ) {
143- this . storage . removeListener ( this . storageListener ) ;
144- this . storageListener = null ;
145- }
146-
147- if ( this . punishmentObserver ) {
148- this . punishmentObserver . disconnect ( ) ;
149- this . punishmentObserver = null ;
150- }
151-
152- if ( this . keydownHandler ) {
153- window . removeEventListener ( 'keydown' , this . keydownHandler , true ) ;
154- this . keydownHandler = null ;
155- }
114+ this . enforcer . stop ( ) ;
156115
157- document . body . classList . remove ( 'limitra-global-punishment' ) ;
158116 this . callback = null ;
159117 this . pendingVideoId = null ;
160118 this . lastVideoId = null ;
161119 }
162120
163121 public executePunishment ( ) : void {
164- if ( this . punishmentObserver ) return ;
165-
166- document . body . classList . add ( 'limitra-global-punishment' ) ;
167-
168- const silenceVideos = ( ) => {
169- const videos = document . querySelectorAll < HTMLVideoElement > ( 'video' ) ;
170- videos . forEach ( ( video ) => {
171- if ( ! video . paused || ! video . muted ) {
172- video . muted = true ;
173- video . pause ( ) ;
174- }
175- } ) ;
176- } ;
177-
178- silenceVideos ( ) ;
179-
180- let scheduled = false ;
181-
182- this . punishmentObserver = new MutationObserver ( ( ) => {
183- if ( scheduled ) return ;
184- scheduled = true ;
185-
186- requestAnimationFrame ( ( ) => {
187- silenceVideos ( ) ;
188- scheduled = false ;
189- } ) ;
190- } ) ;
191-
192- const appContainer = document . querySelector ( 'ytd-app' ) || document . body ;
193- this . punishmentObserver . observe ( appContainer , {
194- childList : true ,
195- subtree : true ,
196- } ) ;
197-
198- this . keydownHandler = ( e : KeyboardEvent ) => {
199- if ( this . BLOCKED_KEYS . includes ( e . key ) ) {
200- e . preventDefault ( ) ;
201- e . stopPropagation ( ) ;
202- }
203- } ;
204-
205- window . addEventListener ( 'keydown' , this . keydownHandler , true ) ;
206- }
207-
208- // Unified ID extraction supporting both Shorts and Watch URLs
209- private getVideoId ( url : string ) : string | null {
210- if ( url . includes ( '/shorts/' ) ) {
211- const match = url . match ( / s h o r t s \/ ( [ ^ ? ] + ) / ) ;
212- return match ? match [ 1 ] : null ;
213- } else if ( url . includes ( '/watch' ) ) {
214- try {
215- const urlParams = new URLSearchParams ( new URL ( url ) . search ) ;
216- return urlParams . get ( 'v' ) ;
217- } catch {
218- return null ;
219- }
220- }
221- return null ;
122+ this . enforcer . enforce ( ) ;
222123 }
223124
224125 private checkUrl ( ) {
225126 const currentUrl = window . location . href ;
226- const isShorts = currentUrl . includes ( '/shorts/' ) ;
227- const evaluatedMode : PlatformId = isShorts ? 'youtube_shorts' : 'youtube_watch' ;
127+ const evaluatedMode = getPlatformType ( currentUrl ) ;
228128
229- // TRIGGER HOT-SWAP: If the user navigated across platform boundaries
230129 if ( evaluatedMode !== this . id ) {
231130 this . onModeChange ( ) ;
232131 return ;
233132 }
234133
235- const currentId = this . getVideoId ( currentUrl ) ;
236-
134+ const currentId = getVideoId ( currentUrl ) ;
237135 if ( currentId !== this . lastVideoId ) {
238136 this . lastVideoId = currentId ;
239137 if ( currentId ) {
@@ -254,9 +152,8 @@ export class YouTubeAdapter implements PlatformAdapter {
254152
255153 this . urlCheckInterval = window . setInterval ( ( ) => {
256154 this . checkUrl ( ) ;
257-
258155 if ( ! document . hidden && ! this . isWatching && this . pendingVideoId ) {
259- if ( this . getVideoId ( window . location . href ) === this . pendingVideoId ) {
156+ if ( getVideoId ( window . location . href ) === this . pendingVideoId ) {
260157 this . startOrResumeWatchTimer ( this . pendingVideoId ) ;
261158 }
262159 }
0 commit comments