myetv-player 1.0.0 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.github/workflows/codeql.yml +100 -0
  2. package/README.md +36 -58
  3. package/SECURITY.md +50 -0
  4. package/css/myetv-player.css +301 -218
  5. package/css/myetv-player.min.css +1 -1
  6. package/dist/myetv-player.js +1713 -1503
  7. package/dist/myetv-player.min.js +1670 -1471
  8. package/package.json +6 -1
  9. package/plugins/README.md +1016 -0
  10. package/plugins/cloudflare/README.md +1068 -0
  11. package/plugins/cloudflare/myetv-player-cloudflare-stream-plugin.js +556 -0
  12. package/plugins/facebook/README.md +1024 -0
  13. package/plugins/facebook/myetv-player-facebook-plugin.js +437 -0
  14. package/plugins/gamepad-remote-controller/README.md +816 -0
  15. package/plugins/gamepad-remote-controller/myetv-player-gamepad-remote-plugin.js +678 -0
  16. package/plugins/google-adsense-ads/README.md +1 -0
  17. package/plugins/google-adsense-ads/g-adsense-ads-plugin.js +158 -0
  18. package/plugins/google-ima-ads/README.md +1 -0
  19. package/plugins/google-ima-ads/g-ima-ads-plugin.js +355 -0
  20. package/plugins/twitch/README.md +1185 -0
  21. package/plugins/twitch/myetv-player-twitch-plugin.js +569 -0
  22. package/plugins/vast-vpaid-ads/README.md +1 -0
  23. package/plugins/vast-vpaid-ads/vast-vpaid-ads-plugin.js +346 -0
  24. package/plugins/vimeo/README.md +1416 -0
  25. package/plugins/vimeo/myetv-player-vimeo.js +640 -0
  26. package/plugins/youtube/README.md +851 -0
  27. package/plugins/youtube/myetv-player-youtube-plugin.js +1714 -210
  28. package/scss/README.md +160 -0
  29. package/scss/_menus.scss +840 -672
  30. package/scss/_responsive.scss +67 -105
  31. package/scss/_volume.scss +67 -105
  32. package/src/README.md +559 -0
  33. package/src/controls.js +16 -4
  34. package/src/core.js +1192 -1062
  35. package/src/i18n.js +27 -1
  36. package/src/quality.js +478 -436
  37. package/src/subtitles.js +2 -2
@@ -0,0 +1,678 @@
1
+ /**
2
+ * MYETV Player - Gamepad Plugin
3
+ * File: myetv-player-gamepad-remote-plugin.js
4
+ * Adds gamepad/controller support for video playback control
5
+ * Perfect for Smart TVs, gaming consoles, and accessibility
6
+ * Created by https://www.myetv.tv https://oskarcosimo.com
7
+ */
8
+
9
+ (function () {
10
+ 'use strict';
11
+
12
+ class GamepadPlugin {
13
+ constructor(player, options = {}) {
14
+ this.player = player;
15
+ this.options = {
16
+ // Enable/disable plugin
17
+ enabled: options.enabled !== false,
18
+
19
+ // Preset mappings ('xbox', 'playstation', 'nintendo', 'tv-remote', 'generic')
20
+ preset: options.preset || 'xbox',
21
+
22
+ // Custom mapping (overrides preset if provided)
23
+ customMapping: options.customMapping || null,
24
+
25
+ // Analog stick settings
26
+ deadZone: options.deadZone || 0.2, // Ignore small movements
27
+ seekSensitivity: options.seekSensitivity || 5, // Seconds to seek per input
28
+ volumeSensitivity: options.volumeSensitivity || 0.05, // Volume change per input
29
+
30
+ // Auto-detect controller type
31
+ autoDetect: options.autoDetect !== false,
32
+
33
+ // Polling rate (ms)
34
+ pollingRate: options.pollingRate || 100,
35
+
36
+ // Vibration/haptic feedback
37
+ enableVibration: options.enableVibration !== false,
38
+ vibrationIntensity: options.vibrationIntensity || 0.5,
39
+
40
+ // UI feedback
41
+ showFeedback: options.showFeedback !== false,
42
+ feedbackDuration: options.feedbackDuration || 1000,
43
+
44
+ // Debug mode
45
+ debug: options.debug || false,
46
+
47
+ ...options
48
+ };
49
+
50
+ this.connectedGamepads = {};
51
+ this.pollingInterval = null;
52
+ this.lastButtonStates = {};
53
+ this.lastAxisValues = {};
54
+ this.feedbackTimeout = null;
55
+
56
+ // Preset mappings
57
+ this.presetMappings = this.getPresetMappings();
58
+
59
+ // Current mapping
60
+ this.currentMapping = this.options.customMapping ||
61
+ this.presetMappings[this.options.preset] ||
62
+ this.presetMappings.generic;
63
+
64
+ // Get plugin API
65
+ this.api = player.getPluginAPI ? player.getPluginAPI() : {
66
+ player: player,
67
+ video: player.video,
68
+ container: player.container,
69
+ controls: player.controls,
70
+ debug: (msg) => {
71
+ if (this.options.debug) console.log('🎮 Gamepad Plugin:', msg);
72
+ },
73
+ triggerEvent: (event, data) => player.triggerEvent(event, data)
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Get preset controller mappings
79
+ */
80
+ getPresetMappings() {
81
+ return {
82
+ // Xbox controller (standard mapping)
83
+ xbox: {
84
+ playPause: { button: 0 }, // A button
85
+ stop: { button: 1 }, // B button
86
+ fullscreen: { button: 2 }, // X button
87
+ mute: { button: 3 }, // Y button
88
+ seekBackward: { button: 4 }, // LB
89
+ seekForward: { button: 5 }, // RB
90
+ volumeDown: { button: 6 }, // LT
91
+ volumeUp: { button: 7 }, // RT
92
+ showInfo: { button: 8 }, // Back/Select
93
+ settings: { button: 9 }, // Start
94
+ seekAxis: { axis: 0 }, // Left stick X
95
+ volumeAxis: { axis: 1 } // Left stick Y
96
+ },
97
+
98
+ // PlayStation controller
99
+ playstation: {
100
+ playPause: { button: 0 }, // Cross
101
+ stop: { button: 1 }, // Circle
102
+ fullscreen: { button: 2 }, // Square
103
+ mute: { button: 3 }, // Triangle
104
+ seekBackward: { button: 4 }, // L1
105
+ seekForward: { button: 5 }, // R1
106
+ volumeDown: { button: 6 }, // L2
107
+ volumeUp: { button: 7 }, // R2
108
+ showInfo: { button: 8 }, // Share
109
+ settings: { button: 9 }, // Options
110
+ seekAxis: { axis: 0 }, // Left stick X
111
+ volumeAxis: { axis: 1 } // Left stick Y
112
+ },
113
+
114
+ // Nintendo Switch controller
115
+ nintendo: {
116
+ playPause: { button: 1 }, // A (bottom)
117
+ stop: { button: 0 }, // B (right)
118
+ fullscreen: { button: 3 }, // X (top)
119
+ mute: { button: 2 }, // Y (left)
120
+ seekBackward: { button: 4 }, // L
121
+ seekForward: { button: 5 }, // R
122
+ volumeDown: { button: 6 }, // ZL
123
+ volumeUp: { button: 7 }, // ZR
124
+ showInfo: { button: 8 }, // -
125
+ settings: { button: 9 }, // +
126
+ seekAxis: { axis: 0 },
127
+ volumeAxis: { axis: 1 }
128
+ },
129
+
130
+ // TV Remote / Generic mapping
131
+ 'tv-remote': {
132
+ playPause: { button: 0 }, // OK/Select
133
+ stop: { button: 1 }, // Back
134
+ seekBackward: { button: 4 }, // Left
135
+ seekForward: { button: 5 }, // Right
136
+ volumeDown: { button: 6 }, // Volume -
137
+ volumeUp: { button: 7 }, // Volume +
138
+ mute: { button: 3 }, // Mute
139
+ fullscreen: { button: 2 }, // Guide/Info
140
+ settings: { button: 9 } // Menu
141
+ },
142
+
143
+ // Generic fallback
144
+ generic: {
145
+ playPause: { button: 0 },
146
+ stop: { button: 1 },
147
+ fullscreen: { button: 2 },
148
+ mute: { button: 3 },
149
+ seekBackward: { button: 4 },
150
+ seekForward: { button: 5 },
151
+ volumeDown: { button: 6 },
152
+ volumeUp: { button: 7 },
153
+ seekAxis: { axis: 0 },
154
+ volumeAxis: { axis: 1 }
155
+ }
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Setup plugin
161
+ */
162
+ setup() {
163
+ if (!this.options.enabled) {
164
+ this.api.debug('Plugin disabled');
165
+ return;
166
+ }
167
+
168
+ // Check Gamepad API support
169
+ if (!navigator.getGamepads) {
170
+ console.warn('🎮 Gamepad API not supported in this browser');
171
+ return;
172
+ }
173
+
174
+ this.api.debug('Setup started with preset: ' + this.options.preset);
175
+
176
+ // Setup gamepad event listeners
177
+ this.setupGamepadListeners();
178
+
179
+ // Create feedback UI if enabled
180
+ if (this.options.showFeedback) {
181
+ this.createFeedbackUI();
182
+ }
183
+
184
+ // Add custom methods
185
+ this.addCustomMethods();
186
+
187
+ this.api.debug('Setup completed');
188
+ }
189
+
190
+ /**
191
+ * Setup gamepad event listeners
192
+ */
193
+ setupGamepadListeners() {
194
+ // Gamepad connected
195
+ window.addEventListener('gamepadconnected', (e) => {
196
+ this.onGamepadConnected(e.gamepad);
197
+ });
198
+
199
+ // Gamepad disconnected
200
+ window.addEventListener('gamepaddisconnected', (e) => {
201
+ this.onGamepadDisconnected(e.gamepad);
202
+ });
203
+
204
+ // Check for already connected gamepads
205
+ this.checkExistingGamepads();
206
+ }
207
+
208
+ /**
209
+ * Check for already connected gamepads
210
+ */
211
+ checkExistingGamepads() {
212
+ const gamepads = navigator.getGamepads();
213
+ for (let i = 0; i < gamepads.length; i++) {
214
+ if (gamepads[i]) {
215
+ this.onGamepadConnected(gamepads[i]);
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Gamepad connected handler
222
+ */
223
+ onGamepadConnected(gamepad) {
224
+ this.api.debug('Gamepad connected: ' + gamepad.id + ' (index: ' + gamepad.index + ')');
225
+
226
+ this.connectedGamepads[gamepad.index] = gamepad;
227
+ this.lastButtonStates[gamepad.index] = [];
228
+ this.lastAxisValues[gamepad.index] = [];
229
+
230
+ // Auto-detect controller type
231
+ if (this.options.autoDetect) {
232
+ this.detectControllerType(gamepad);
233
+ }
234
+
235
+ // Start polling if not already started
236
+ if (!this.pollingInterval) {
237
+ this.startPolling();
238
+ }
239
+
240
+ // Show feedback
241
+ this.showFeedback('🎮 Controller Connected: ' + this.getControllerName(gamepad));
242
+
243
+ // Trigger event
244
+ this.api.triggerEvent('gamepad:connected', {
245
+ id: gamepad.id,
246
+ index: gamepad.index,
247
+ buttons: gamepad.buttons.length,
248
+ axes: gamepad.axes.length
249
+ });
250
+ }
251
+
252
+ /**
253
+ * Gamepad disconnected handler
254
+ */
255
+ onGamepadDisconnected(gamepad) {
256
+ this.api.debug('Gamepad disconnected: ' + gamepad.id);
257
+
258
+ delete this.connectedGamepads[gamepad.index];
259
+ delete this.lastButtonStates[gamepad.index];
260
+ delete this.lastAxisValues[gamepad.index];
261
+
262
+ // Stop polling if no gamepads left
263
+ if (Object.keys(this.connectedGamepads).length === 0) {
264
+ this.stopPolling();
265
+ }
266
+
267
+ // Show feedback
268
+ this.showFeedback('🎮 Controller Disconnected');
269
+
270
+ // Trigger event
271
+ this.api.triggerEvent('gamepad:disconnected', {
272
+ id: gamepad.id,
273
+ index: gamepad.index
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Detect controller type from ID
279
+ */
280
+ detectControllerType(gamepad) {
281
+ const id = gamepad.id.toLowerCase();
282
+
283
+ if (id.includes('xbox') || id.includes('xinput')) {
284
+ this.api.debug('Detected Xbox controller');
285
+ this.currentMapping = this.presetMappings.xbox;
286
+ } else if (id.includes('playstation') || id.includes('dualshock') || id.includes('dualsense')) {
287
+ this.api.debug('Detected PlayStation controller');
288
+ this.currentMapping = this.presetMappings.playstation;
289
+ } else if (id.includes('nintendo') || id.includes('switch')) {
290
+ this.api.debug('Detected Nintendo controller');
291
+ this.currentMapping = this.presetMappings.nintendo;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Get friendly controller name
297
+ */
298
+ getControllerName(gamepad) {
299
+ const id = gamepad.id.toLowerCase();
300
+
301
+ if (id.includes('xbox')) return 'Xbox Controller';
302
+ if (id.includes('playstation') || id.includes('dualshock')) return 'PlayStation Controller';
303
+ if (id.includes('dualsense')) return 'DualSense Controller';
304
+ if (id.includes('nintendo') || id.includes('switch')) return 'Nintendo Controller';
305
+
306
+ return gamepad.id.substring(0, 30) + (gamepad.id.length > 30 ? '...' : '');
307
+ }
308
+
309
+ /**
310
+ * Start polling gamepads
311
+ */
312
+ startPolling() {
313
+ this.api.debug('Started polling gamepads');
314
+
315
+ this.pollingInterval = setInterval(() => {
316
+ this.pollGamepads();
317
+ }, this.options.pollingRate);
318
+ }
319
+
320
+ /**
321
+ * Stop polling gamepads
322
+ */
323
+ stopPolling() {
324
+ if (this.pollingInterval) {
325
+ clearInterval(this.pollingInterval);
326
+ this.pollingInterval = null;
327
+ this.api.debug('Stopped polling gamepads');
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Poll all connected gamepads
333
+ */
334
+ pollGamepads() {
335
+ const gamepads = navigator.getGamepads();
336
+
337
+ for (let i = 0; i < gamepads.length; i++) {
338
+ const gamepad = gamepads[i];
339
+ if (!gamepad) continue;
340
+
341
+ // Check buttons
342
+ this.checkButtons(gamepad);
343
+
344
+ // Check axes
345
+ this.checkAxes(gamepad);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Check gamepad buttons
351
+ */
352
+ checkButtons(gamepad) {
353
+ const index = gamepad.index;
354
+
355
+ for (let i = 0; i < gamepad.buttons.length; i++) {
356
+ const button = gamepad.buttons[i];
357
+ const wasPressed = this.lastButtonStates[index][i];
358
+ const isPressed = button.pressed;
359
+
360
+ // Button just pressed (edge detection)
361
+ if (isPressed && !wasPressed) {
362
+ this.handleButtonPress(i, gamepad);
363
+ }
364
+
365
+ // Update state
366
+ this.lastButtonStates[index][i] = isPressed;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Check gamepad axes
372
+ */
373
+ checkAxes(gamepad) {
374
+ const index = gamepad.index;
375
+
376
+ for (let i = 0; i < gamepad.axes.length; i++) {
377
+ const value = gamepad.axes[i];
378
+ const lastValue = this.lastAxisValues[index][i] || 0;
379
+
380
+ // Check if axis moved beyond dead zone
381
+ if (Math.abs(value) > this.options.deadZone) {
382
+ this.handleAxisMove(i, value, gamepad);
383
+ }
384
+
385
+ // Update state
386
+ this.lastAxisValues[index][i] = value;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Handle button press
392
+ */
393
+ handleButtonPress(buttonIndex, gamepad) {
394
+ this.api.debug('Button pressed: ' + buttonIndex);
395
+
396
+ // Find action for this button
397
+ for (const [action, mapping] of Object.entries(this.currentMapping)) {
398
+ if (mapping.button === buttonIndex) {
399
+ this.executeAction(action, gamepad);
400
+ return;
401
+ }
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Handle axis movement
407
+ */
408
+ handleAxisMove(axisIndex, value, gamepad) {
409
+ // Check seek axis
410
+ if (this.currentMapping.seekAxis && this.currentMapping.seekAxis.axis === axisIndex) {
411
+ if (value > this.options.deadZone) {
412
+ this.executeAction('seekForward', gamepad);
413
+ } else if (value < -this.options.deadZone) {
414
+ this.executeAction('seekBackward', gamepad);
415
+ }
416
+ }
417
+
418
+ // Check volume axis
419
+ if (this.currentMapping.volumeAxis && this.currentMapping.volumeAxis.axis === axisIndex) {
420
+ if (value > this.options.deadZone) {
421
+ this.executeAction('volumeDown', gamepad);
422
+ } else if (value < -this.options.deadZone) {
423
+ this.executeAction('volumeUp', gamepad);
424
+ }
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Execute action
430
+ */
431
+ executeAction(action, gamepad) {
432
+ this.api.debug('Executing action: ' + action);
433
+
434
+ switch (action) {
435
+ case 'playPause':
436
+ this.api.player.togglePlayPause();
437
+ this.showFeedback(this.api.video.paused ? '▶️ Play' : '⏸️ Pause');
438
+ this.vibrate(100);
439
+ break;
440
+
441
+ case 'stop':
442
+ this.api.player.pause();
443
+ this.api.video.currentTime = 0;
444
+ this.showFeedback('⏹️ Stop');
445
+ this.vibrate(100);
446
+ break;
447
+
448
+ case 'seekForward':
449
+ this.api.video.currentTime += this.options.seekSensitivity;
450
+ this.showFeedback('⏩ +' + this.options.seekSensitivity + 's');
451
+ this.vibrate(50);
452
+ break;
453
+
454
+ case 'seekBackward':
455
+ this.api.video.currentTime -= this.options.seekSensitivity;
456
+ this.showFeedback('⏪ -' + this.options.seekSensitivity + 's');
457
+ this.vibrate(50);
458
+ break;
459
+
460
+ case 'volumeUp':
461
+ const newVolumeUp = Math.min(1, this.api.video.volume + this.options.volumeSensitivity);
462
+ this.api.video.volume = newVolumeUp;
463
+ this.showFeedback('🔊 Volume: ' + Math.round(newVolumeUp * 100) + '%');
464
+ this.vibrate(30);
465
+ break;
466
+
467
+ case 'volumeDown':
468
+ const newVolumeDown = Math.max(0, this.api.video.volume - this.options.volumeSensitivity);
469
+ this.api.video.volume = newVolumeDown;
470
+ this.showFeedback('🔉 Volume: ' + Math.round(newVolumeDown * 100) + '%');
471
+ this.vibrate(30);
472
+ break;
473
+
474
+ case 'mute':
475
+ this.api.video.muted = !this.api.video.muted;
476
+ this.showFeedback(this.api.video.muted ? '🔇 Muted' : '🔊 Unmuted');
477
+ this.vibrate(100);
478
+ break;
479
+
480
+ case 'fullscreen':
481
+ this.api.player.toggleFullscreen();
482
+ this.showFeedback(document.fullscreenElement ? '⛶ Fullscreen' : '⛶ Exit Fullscreen');
483
+ this.vibrate(100);
484
+ break;
485
+
486
+ case 'showInfo':
487
+ this.showVideoInfo();
488
+ this.vibrate(50);
489
+ break;
490
+
491
+ case 'settings':
492
+ this.api.triggerEvent('gamepad:settings');
493
+ this.showFeedback('⚙️ Settings');
494
+ this.vibrate(50);
495
+ break;
496
+ }
497
+
498
+ // Trigger custom event
499
+ this.api.triggerEvent('gamepad:action', {
500
+ action: action,
501
+ gamepadIndex: gamepad.index
502
+ });
503
+ }
504
+
505
+ /**
506
+ * Vibrate gamepad (if supported)
507
+ */
508
+ vibrate(duration = 100) {
509
+ if (!this.options.enableVibration) return;
510
+
511
+ const gamepads = navigator.getGamepads();
512
+ for (let i = 0; i < gamepads.length; i++) {
513
+ const gamepad = gamepads[i];
514
+ if (gamepad && gamepad.vibrationActuator) {
515
+ gamepad.vibrationActuator.playEffect('dual-rumble', {
516
+ duration: duration,
517
+ strongMagnitude: this.options.vibrationIntensity,
518
+ weakMagnitude: this.options.vibrationIntensity * 0.5
519
+ });
520
+ }
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Create feedback UI
526
+ */
527
+ createFeedbackUI() {
528
+ this.feedbackElement = document.createElement('div');
529
+ this.feedbackElement.className = 'gamepad-feedback';
530
+ this.feedbackElement.style.cssText = `
531
+ position: fixed;
532
+ top: 20px;
533
+ right: 20px;
534
+ background: rgba(0, 0, 0, 0.8);
535
+ color: white;
536
+ padding: 15px 25px;
537
+ border-radius: 8px;
538
+ font-size: 16px;
539
+ font-weight: bold;
540
+ z-index: 10000;
541
+ display: none;
542
+ animation: fadeIn 0.2s;
543
+ `;
544
+ document.body.appendChild(this.feedbackElement);
545
+ }
546
+
547
+ /**
548
+ * Show feedback message
549
+ */
550
+ showFeedback(message) {
551
+ if (!this.options.showFeedback || !this.feedbackElement) return;
552
+
553
+ this.feedbackElement.textContent = message;
554
+ this.feedbackElement.style.display = 'block';
555
+
556
+ // Clear existing timeout
557
+ if (this.feedbackTimeout) {
558
+ clearTimeout(this.feedbackTimeout);
559
+ }
560
+
561
+ // Hide after duration
562
+ this.feedbackTimeout = setTimeout(() => {
563
+ this.feedbackElement.style.display = 'none';
564
+ }, this.options.feedbackDuration);
565
+ }
566
+
567
+ /**
568
+ * Show video info
569
+ */
570
+ showVideoInfo() {
571
+ const currentTime = this.formatTime(this.api.video.currentTime);
572
+ const duration = this.formatTime(this.api.video.duration);
573
+ const volume = Math.round(this.api.video.volume * 100);
574
+
575
+ this.showFeedback(
576
+ `⏱️ ${currentTime} / ${duration}\n` +
577
+ `🔊 Volume: ${volume}%`
578
+ );
579
+ }
580
+
581
+ /**
582
+ * Format time
583
+ */
584
+ formatTime(seconds) {
585
+ const mins = Math.floor(seconds / 60);
586
+ const secs = Math.floor(seconds % 60);
587
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
588
+ }
589
+
590
+ /**
591
+ * Add custom methods to player
592
+ */
593
+ addCustomMethods() {
594
+ // Get connected gamepads
595
+ this.api.player.getConnectedGamepads = () => {
596
+ return Object.values(this.connectedGamepads);
597
+ };
598
+
599
+ // Change mapping preset
600
+ this.api.player.setGamepadPreset = (preset) => {
601
+ return this.setPreset(preset);
602
+ };
603
+
604
+ // Set custom mapping
605
+ this.api.player.setGamepadMapping = (mapping) => {
606
+ return this.setCustomMapping(mapping);
607
+ };
608
+
609
+ // Get current mapping
610
+ this.api.player.getGamepadMapping = () => {
611
+ return this.currentMapping;
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Set preset
617
+ */
618
+ setPreset(preset) {
619
+ if (!this.presetMappings[preset]) {
620
+ console.warn('Unknown preset:', preset);
621
+ return false;
622
+ }
623
+
624
+ this.options.preset = preset;
625
+ this.currentMapping = this.presetMappings[preset];
626
+ this.api.debug('Preset changed to: ' + preset);
627
+ this.showFeedback('🎮 Preset: ' + preset);
628
+
629
+ return true;
630
+ }
631
+
632
+ /**
633
+ * Set custom mapping
634
+ */
635
+ setCustomMapping(mapping) {
636
+ this.currentMapping = mapping;
637
+ this.api.debug('Custom mapping applied');
638
+ this.showFeedback('🎮 Custom mapping applied');
639
+
640
+ return true;
641
+ }
642
+
643
+ /**
644
+ * Dispose plugin
645
+ */
646
+ dispose() {
647
+ this.api.debug('Disposing plugin');
648
+
649
+ // Stop polling
650
+ this.stopPolling();
651
+
652
+ // Remove feedback UI
653
+ if (this.feedbackElement) {
654
+ this.feedbackElement.remove();
655
+ this.feedbackElement = null;
656
+ }
657
+
658
+ // Clear timeouts
659
+ if (this.feedbackTimeout) {
660
+ clearTimeout(this.feedbackTimeout);
661
+ }
662
+
663
+ // Remove event listeners
664
+ window.removeEventListener('gamepadconnected', this.onGamepadConnected);
665
+ window.removeEventListener('gamepaddisconnected', this.onGamepadDisconnected);
666
+
667
+ this.api.debug('Plugin disposed');
668
+ }
669
+ }
670
+
671
+ // Register plugin globally
672
+ if (typeof window.registerMYETVPlugin === 'function') {
673
+ window.registerMYETVPlugin('gamepad', GamepadPlugin);
674
+ } else {
675
+ console.error('🎮 MYETV Player plugin system not found');
676
+ }
677
+
678
+ })();