unified-video-framework 1.4.164 → 1.4.166

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.
@@ -51,6 +51,9 @@ export class WebPlayer extends BasePlayer {
51
51
  private currentPlaybackRate = 1;
52
52
  private isDragging: boolean = false;
53
53
 
54
+ // Settings menu state guard to keep controls interactive while open
55
+ private isSettingsOpen: boolean = false;
56
+
54
57
  // Settings configuration
55
58
  private settingsConfig = {
56
59
  enabled: true, // Show settings button
@@ -97,6 +100,20 @@ export class WebPlayer extends BasePlayer {
97
100
  // Progress bar tooltip state
98
101
  private showTimeTooltip: boolean = false;
99
102
 
103
+ // Material You touch gesture state
104
+ private lastTapTime: number = 0;
105
+ private tapCount: number = 0;
106
+ private tapTimeout: NodeJS.Timeout | null = null;
107
+ private longPressTimer: NodeJS.Timeout | null = null;
108
+ private longPressActive: boolean = false;
109
+ private longPressStartTime: number = 0;
110
+ private originalPlaybackRate: number = 1;
111
+
112
+ // Material You dynamic theming - matches existing theme
113
+ private dominantColor: string = '#ff0000'; // Primary accent from theme
114
+ private accentColor: string = '#ff4d4f'; // Secondary accent from theme
115
+ private surfaceTint: string = 'rgba(255, 0, 0, 0.08)'; // Surface tint for Material containers
116
+
100
117
  // Autoplay enhancement state
101
118
  private autoplayCapabilities: {
102
119
  canAutoplay: boolean;
@@ -342,6 +359,35 @@ export class WebPlayer extends BasePlayer {
342
359
 
343
360
  // Initialize metadata UI to hidden/empty by default
344
361
  this.updateMetadataUI();
362
+
363
+ // Enable Material You mobile layout on mobile portrait devices
364
+ this.enableMaterialYouMobileIfNeeded();
365
+ }
366
+
367
+ /**
368
+ * Enable Material You mobile layout if on mobile portrait device
369
+ */
370
+ private enableMaterialYouMobileIfNeeded(): void {
371
+ // Check if mobile and portrait
372
+ const isMobile = this.isMobileDevice();
373
+ const isPortrait = window.innerHeight > window.innerWidth;
374
+
375
+ if (isMobile && isPortrait && this.playerWrapper) {
376
+ this.debugLog('Enabling Material You mobile layout');
377
+ this.playerWrapper.classList.add('uvf-material-you-mobile');
378
+
379
+ // Listen for orientation changes
380
+ window.addEventListener('resize', () => {
381
+ const isNowPortrait = window.innerHeight > window.innerWidth;
382
+ if (this.playerWrapper) {
383
+ if (isMobile && isNowPortrait) {
384
+ this.playerWrapper.classList.add('uvf-material-you-mobile');
385
+ } else {
386
+ this.playerWrapper.classList.remove('uvf-material-you-mobile');
387
+ }
388
+ }
389
+ });
390
+ }
345
391
  }
346
392
 
347
393
  private setupVideoEventListeners(): void {
@@ -434,6 +480,9 @@ export class WebPlayer extends BasePlayer {
434
480
  // Update time display immediately when metadata loads
435
481
  this.updateTimeDisplay();
436
482
 
483
+ // Render chapter markers if enabled
484
+ this.renderChapterMarkersOnProgressBar();
485
+
437
486
  this.emit('onLoadedMetadata', {
438
487
  duration: this.video.duration || 0,
439
488
  width: this.video.videoWidth || 0,
@@ -2343,6 +2392,8 @@ export class WebPlayer extends BasePlayer {
2343
2392
  --uvf-scrollbar-thumb-hover-start: rgba(255,0,0,0.5);
2344
2393
  --uvf-scrollbar-thumb-hover-end: rgba(255,0,0,0.6);
2345
2394
  --uvf-firefox-scrollbar-color: rgba(255,255,255,0.25);
2395
+ /* Material You surface tint */
2396
+ --uvf-surface-tint: rgba(255, 0, 0, 0.08);
2346
2397
  }
2347
2398
 
2348
2399
  /* Player focus styles for better UX */
@@ -4398,278 +4449,417 @@ export class WebPlayer extends BasePlayer {
4398
4449
  }
4399
4450
 
4400
4451
  /* Enhanced Responsive Media Queries with UX Best Practices */
4401
- /* Mobile Portrait Layout - CENTERED PLAYER with TOP/BOTTOM BLACK AREAS */
4452
+
4453
+ /* Material You Mobile Portrait Layout - 25% Black + 50% Video + 25% Black */
4402
4454
  @media screen and (max-width: 767px) and (orientation: portrait) {
4403
- /* CENTERED LAYOUT: 25% top black + 50% player + 25% bottom black */
4404
- .uvf-responsive-container {
4405
- display: flex;
4406
- flex-direction: column;
4455
+ /* Enable Material You mode with class flag */
4456
+ .uvf-player-wrapper.uvf-material-you-mobile {
4457
+ /* Full viewport height layout */
4407
4458
  height: 100vh;
4408
4459
  height: 100dvh;
4409
- background: #000;
4410
- overflow: hidden;
4460
+ width: 100vw;
4411
4461
  position: fixed;
4412
4462
  top: 0;
4413
4463
  left: 0;
4464
+ display: flex;
4465
+ flex-direction: column;
4466
+ background: #000;
4467
+ overflow: hidden;
4468
+ }
4469
+
4470
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-video-container {
4471
+ /* Video occupies middle 50% */
4472
+ height: 50vh;
4473
+ height: 50dvh;
4414
4474
  width: 100vw;
4415
- padding: 0;
4416
- margin: 0;
4475
+ position: relative;
4476
+ margin-top: 25vh;
4477
+ margin-top: 25dvh;
4478
+ aspect-ratio: unset !important;
4479
+ /* Match existing theme background */
4480
+ background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 100%);
4481
+ /* Material Design elevation */
4482
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
4483
+ 0 4px 16px rgba(0, 0, 0, 0.3),
4484
+ 0 2px 8px rgba(0, 0, 0, 0.2);
4417
4485
  }
4418
4486
 
4419
- /* TOP BLACK AREA - 25% of viewport - COMPLETELY EMPTY */
4420
- .uvf-responsive-container::before {
4487
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-video {
4488
+ width: 100%;
4489
+ height: 100%;
4490
+ object-fit: contain;
4491
+ }
4492
+
4493
+ /* Top black section (25%) - Tap zone overlay */
4494
+ .uvf-player-wrapper.uvf-material-you-mobile::before {
4421
4495
  content: '';
4422
- flex: 0 0 25vh;
4496
+ position: absolute;
4497
+ top: 0;
4498
+ left: 0;
4499
+ width: 100vw;
4500
+ height: 25vh;
4501
+ height: 25dvh;
4423
4502
  background: #000;
4424
- pointer-events: none;
4503
+ z-index: 1;
4504
+ pointer-events: all;
4505
+ touch-action: manipulation;
4425
4506
  }
4426
4507
 
4427
- /* BOTTOM BLACK AREA - 25% of viewport - COMPLETELY EMPTY */
4428
- .uvf-responsive-container::after {
4508
+ /* Bottom black section (25%) - Surface container */
4509
+ .uvf-player-wrapper.uvf-material-you-mobile::after {
4429
4510
  content: '';
4430
- flex: 0 0 25vh;
4431
- background: #000;
4511
+ position: absolute;
4512
+ bottom: 0;
4513
+ left: 0;
4514
+ width: 100vw;
4515
+ height: 25vh;
4516
+ height: 25dvh;
4517
+ background: linear-gradient(to top,
4518
+ #000 0%,
4519
+ rgba(0, 0, 0, 0.98) 20%,
4520
+ rgba(0, 0, 0, 0.95) 100%);
4521
+ z-index: 1;
4432
4522
  pointer-events: none;
4433
4523
  }
4434
4524
 
4435
- /* CENTERED VIDEO PLAYER WRAPPER - 50% of viewport */
4436
- .uvf-responsive-container .uvf-player-wrapper {
4437
- flex: 1; /* Takes remaining 50% */
4438
- width: 100vw;
4525
+ /* Material surface container for controls */
4526
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-controls-bar {
4527
+ position: absolute;
4528
+ bottom: 0;
4529
+ left: 0;
4530
+ right: 0;
4531
+ height: auto;
4532
+ max-height: 25vh;
4533
+ max-height: 25dvh;
4534
+ padding: 16px 20px;
4535
+ padding-bottom: calc(16px + var(--uvf-safe-area-bottom, 0px));
4536
+ background: transparent;
4537
+ z-index: 2;
4439
4538
  display: flex;
4440
- align-items: center;
4441
- justify-content: center;
4442
- background: #000;
4539
+ flex-direction: column;
4540
+ justify-content: flex-end;
4541
+ /* Material Design surface with tint */
4542
+ backdrop-filter: blur(24px);
4543
+ -webkit-backdrop-filter: blur(24px);
4544
+ }
4545
+
4546
+ /* Material surface tint overlay */
4547
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-controls-bar::before {
4548
+ content: '';
4549
+ position: absolute;
4550
+ inset: 0;
4551
+ background: var(--uvf-surface-tint, rgba(255, 0, 0, 0.08));
4552
+ border-radius: 28px 28px 0 0;
4553
+ pointer-events: none;
4554
+ z-index: -1;
4555
+ }
4556
+
4557
+ /* Progress bar with chapter markers */
4558
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-section {
4559
+ margin-bottom: 12px;
4443
4560
  position: relative;
4444
- overflow: hidden !important; /* CRITICAL: Ensure nothing extends beyond this area */
4445
- /* STRICT CONTAINMENT - Force all child elements within bounds */
4446
- contain: layout style paint size !important;
4447
- isolation: isolate !important;
4448
- /* Create strict clipping boundary */
4449
- clip: rect(0, 100vw, 50vh, 0) !important;
4450
4561
  }
4451
4562
 
4452
- /* Video container fills player wrapper */
4453
- .uvf-responsive-container .uvf-video-container {
4454
- width: 100%;
4455
- height: 100%;
4563
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-bar-wrapper {
4564
+ padding: 12px 0;
4456
4565
  position: relative;
4457
- background: #000;
4458
- border-radius: 0;
4459
- overflow: hidden;
4460
4566
  }
4461
4567
 
4462
- /* Video element fills container */
4463
- .uvf-responsive-container .uvf-video {
4464
- width: 100%;
4465
- height: 100%;
4466
- object-fit: contain;
4467
- background: #000;
4568
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-bar {
4569
+ height: 4px;
4570
+ background: rgba(255, 255, 255, 0.2);
4571
+ border-radius: 4px;
4572
+ position: relative;
4573
+ overflow: visible;
4574
+ /* Material elevation */
4575
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
4468
4576
  }
4469
4577
 
4470
- /* CONTROLS STRICTLY CONTAINED WITHIN VIDEO AREA - NEVER EXTEND TO BLACK AREAS */
4471
- .uvf-controls-bar {
4472
- position: absolute !important;
4473
- /* Keep controls INSIDE video container with margins from all edges */
4474
- bottom: 20px !important; /* 20px margin from video bottom */
4475
- left: 16px !important; /* 16px margin from video left */
4476
- right: 16px !important; /* 16px margin from video right */
4477
- top: auto !important;
4478
- background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.7) 60%, transparent 100%);
4479
- padding: 12px 16px;
4480
- border-radius: 12px;
4481
- z-index: 1000;
4482
- /* Ensure controls are visible and contained */
4483
- opacity: 1 !important;
4484
- visibility: visible !important;
4485
- display: flex !important;
4486
- flex-direction: column !important;
4487
- /* Hardware acceleration */
4488
- -webkit-transform: translate3d(0,0,0);
4489
- transform: translate3d(0,0,0);
4490
- /* CRITICAL: Prevent any overflow into black areas */
4491
- max-height: calc(100% - 40px) !important; /* Leave 20px margin from top and bottom */
4492
- max-width: calc(100% - 32px) !important; /* Leave 16px margin from left and right */
4493
- box-sizing: border-box !important;
4494
- /* Visual containment indicators */
4495
- border: 1px solid rgba(255, 255, 255, 0.1);
4496
- backdrop-filter: blur(10px);
4497
- /* STRICT CSS containment with size to prevent overflow */
4498
- contain: layout style paint size !important;
4499
- /* Absolute overflow prevention */
4500
- overflow: hidden !important;
4501
- /* Force clip to container bounds */
4502
- clip-path: inset(0) !important;
4503
- }
4504
-
4505
- /* Force controls and all child elements to stay within video container */
4506
- .uvf-responsive-container .uvf-controls-bar {
4507
- opacity: 1 !important;
4508
- visibility: visible !important;
4509
- transform: translateY(0) !important;
4510
- pointer-events: auto !important;
4511
- }
4512
-
4513
- /* CRITICAL: Prevent ALL child elements from extending beyond controls container */
4514
- .uvf-responsive-container .uvf-controls-bar *,
4515
- .uvf-responsive-container .uvf-controls-bar *::before,
4516
- .uvf-responsive-container .uvf-controls-bar *::after {
4517
- max-width: 100% !important;
4518
- max-height: 100% !important;
4519
- overflow: hidden !important;
4520
- box-sizing: border-box !important;
4521
- position: relative !important;
4578
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-filled {
4579
+ background: var(--uvf-accent-1, #ff0000);
4580
+ box-shadow: 0 0 8px var(--uvf-accent-1, #ff0000);
4522
4581
  }
4523
4582
 
4524
- /* Ensure settings menu stays within video container bounds */
4525
- .uvf-responsive-container .uvf-settings-menu {
4526
- max-height: calc(50vh - 80px) !important; /* Video height minus controls margin */
4527
- max-width: calc(100vw - 64px) !important; /* Video width minus side margins */
4528
- overflow-y: auto !important;
4529
- position: absolute !important;
4530
- /* Keep menu within video boundaries */
4531
- bottom: 60px !important;
4532
- right: 0 !important;
4533
- left: auto !important;
4534
- top: auto !important;
4535
- /* Ensure no child elements extend beyond container */
4536
- contain: layout style paint;
4583
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-handle {
4584
+ width: 20px;
4585
+ height: 20px;
4586
+ background: var(--uvf-accent-1, #ff0000);
4587
+ /* Material Design state layer */
4588
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3),
4589
+ 0 0 0 0 var(--uvf-accent-1, #ff0000);
4590
+ transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4537
4591
  }
4538
4592
 
4539
- /* Ensure all child elements of controls stay within bounds */
4540
- .uvf-responsive-container .uvf-controls-bar * {
4541
- max-width: 100%;
4542
- box-sizing: border-box;
4593
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-handle:active {
4594
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4),
4595
+ 0 0 0 12px rgba(255, 0, 0, 0.15);
4596
+ transform: translate(-50%, -50%) scale(1.2);
4543
4597
  }
4544
4598
 
4545
- /* Progress section contained within controls */
4546
- .uvf-progress-section {
4547
- width: 100%;
4548
- margin-bottom: 8px;
4549
- opacity: 1 !important;
4550
- visibility: visible !important;
4551
- display: block !important;
4599
+ /* Chapter markers on progress bar */
4600
+ .uvf-chapter-marker {
4601
+ position: absolute;
4602
+ top: 0;
4603
+ height: 100%;
4604
+ width: 3px;
4605
+ background: rgba(255, 255, 255, 0.4);
4606
+ border-radius: 2px;
4607
+ transform: translateX(-50%);
4608
+ pointer-events: none;
4609
+ z-index: 1;
4610
+ transition: all 0.2s ease;
4552
4611
  }
4553
4612
 
4554
- /* Progress bar styling */
4555
- .uvf-progress-bar-wrapper {
4556
- opacity: 1 !important;
4557
- visibility: visible !important;
4613
+ .uvf-chapter-marker.intro {
4614
+ background: #4CAF50;
4558
4615
  }
4559
4616
 
4560
- .uvf-progress-bar {
4561
- height: 4px;
4562
- background: rgba(255, 255, 255, 0.3);
4563
- border-radius: 2px;
4617
+ .uvf-chapter-marker.recap {
4618
+ background: #FFC107;
4564
4619
  }
4565
4620
 
4566
- .uvf-progress-filled {
4567
- background: var(--uvf-accent-1, #8B5CF6);
4568
- height: 100%;
4569
- border-radius: 2px;
4621
+ .uvf-chapter-marker.credits {
4622
+ background: #9C27B0;
4570
4623
  }
4571
4624
 
4572
- /* Controls row alignment - ensure visibility */
4573
- .uvf-controls-row {
4574
- width: 100%;
4575
- display: flex !important;
4576
- align-items: center;
4577
- justify-content: flex-start;
4578
- gap: 12px;
4579
- opacity: 1 !important;
4580
- visibility: visible !important;
4581
- margin-top: 8px;
4625
+ .uvf-progress-bar-wrapper:hover .uvf-chapter-marker {
4626
+ width: 4px;
4627
+ opacity: 1;
4582
4628
  }
4583
4629
 
4584
- /* Time display visibility */
4585
- .uvf-time-display {
4586
- color: #fff;
4587
- font-size: 12px;
4588
- font-weight: 500;
4589
- opacity: 1 !important;
4590
- visibility: visible !important;
4591
- display: block !important;
4630
+ /* Material Design control buttons */
4631
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn {
4632
+ width: 48px;
4633
+ height: 48px;
4634
+ min-width: 48px;
4635
+ min-height: 48px;
4636
+ background: rgba(255, 255, 255, 0.12);
4637
+ border-radius: 24px;
4638
+ /* Material elevation level 1 */
4639
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12),
4640
+ 0 1px 2px rgba(0, 0, 0, 0.24);
4641
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4642
+ position: relative;
4643
+ overflow: hidden;
4592
4644
  }
4593
4645
 
4594
- /* Control buttons visibility */
4595
- .uvf-control-btn {
4596
- min-width: 44px;
4597
- min-height: 44px;
4598
- display: flex !important;
4646
+ /* Material ripple effect */
4647
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn::before {
4648
+ content: '';
4649
+ position: absolute;
4650
+ inset: 0;
4651
+ background: rgba(255, 255, 255, 0.1);
4652
+ border-radius: inherit;
4653
+ opacity: 0;
4654
+ transition: opacity 0.2s ease;
4655
+ }
4656
+
4657
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn:active::before {
4658
+ opacity: 1;
4659
+ }
4660
+
4661
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn:active {
4662
+ /* Material elevation level 2 */
4663
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16),
4664
+ 0 3px 6px rgba(0, 0, 0, 0.23);
4665
+ transform: scale(0.95);
4666
+ }
4667
+
4668
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn.play-pause {
4669
+ width: 56px;
4670
+ height: 56px;
4671
+ min-width: 56px;
4672
+ min-height: 56px;
4673
+ border-radius: 28px;
4674
+ /* Material elevated button */
4675
+ background: linear-gradient(135deg,
4676
+ var(--uvf-accent-1, #ff0000),
4677
+ var(--uvf-accent-2, #ff4d4f));
4678
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2),
4679
+ 0 2px 4px rgba(0, 0, 0, 0.15),
4680
+ 0 0 0 0 var(--uvf-accent-1, #ff0000);
4681
+ }
4682
+
4683
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn.play-pause:active {
4684
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25),
4685
+ 0 4px 8px rgba(0, 0, 0, 0.20),
4686
+ 0 0 0 8px rgba(255, 0, 0, 0.12);
4687
+ }
4688
+
4689
+ /* Controls row with Material spacing */
4690
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-controls-row {
4691
+ gap: 16px;
4692
+ padding: 0;
4599
4693
  align-items: center;
4600
- justify-content: center;
4601
- border-radius: 50%;
4694
+ }
4695
+
4696
+ /* Time display with Material surface */
4697
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-time-display {
4602
4698
  background: rgba(255, 255, 255, 0.1);
4603
4699
  backdrop-filter: blur(8px);
4604
- color: #fff;
4605
- opacity: 1 !important;
4606
- visibility: visible !important;
4607
- transition: all 0.2s ease;
4700
+ border-radius: 16px;
4701
+ padding: 6px 12px;
4702
+ font-size: 13px;
4703
+ font-weight: 500;
4704
+ font-feature-settings: 'tnum';
4705
+ /* Material elevation */
4706
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
4608
4707
  }
4609
4708
 
4610
- .uvf-control-btn:active {
4611
- transform: scale(0.95);
4709
+ /* Double-tap overlay indicators */
4710
+ .uvf-doubletap-indicator {
4711
+ position: absolute;
4712
+ top: 50%;
4713
+ transform: translateY(-50%);
4714
+ width: 80px;
4715
+ height: 80px;
4612
4716
  background: rgba(255, 255, 255, 0.2);
4717
+ backdrop-filter: blur(10px);
4718
+ border-radius: 40px;
4719
+ display: flex;
4720
+ align-items: center;
4721
+ justify-content: center;
4722
+ pointer-events: none;
4723
+ opacity: 0;
4724
+ z-index: 100;
4725
+ transition: opacity 0.3s ease;
4726
+ /* Material elevation level 3 */
4727
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19),
4728
+ 0 6px 6px rgba(0, 0, 0, 0.23);
4613
4729
  }
4614
4730
 
4615
- /* Play/pause button prominence */
4616
- .uvf-control-btn.play-pause {
4617
- background: linear-gradient(135deg, var(--uvf-accent-1), var(--uvf-accent-2));
4618
- min-width: 52px;
4619
- min-height: 52px;
4620
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
4731
+ .uvf-doubletap-indicator.left {
4732
+ left: 15%;
4621
4733
  }
4622
4734
 
4623
- .uvf-control-btn.play-pause:active {
4624
- transform: scale(0.92);
4625
- box-shadow: 0 1px 4px rgba(0,0,0,0.4);
4735
+ .uvf-doubletap-indicator.right {
4736
+ right: 15%;
4626
4737
  }
4627
4738
 
4628
- .uvf-control-btn svg {
4629
- fill: #fff;
4739
+ .uvf-doubletap-indicator.active {
4630
4740
  opacity: 1;
4741
+ animation: doubletap-pulse 0.4s cubic-bezier(0.4, 0, 0.2, 1);
4631
4742
  }
4632
4743
 
4633
- /* Right controls */
4634
- .uvf-right-controls {
4635
- margin-left: auto;
4636
- display: flex !important;
4637
- align-items: center;
4638
- gap: 8px;
4639
- opacity: 1 !important;
4640
- visibility: visible !important;
4744
+ @keyframes doubletap-pulse {
4745
+ 0% {
4746
+ transform: translateY(-50%) scale(0.8);
4747
+ opacity: 0;
4748
+ }
4749
+ 50% {
4750
+ transform: translateY(-50%) scale(1.1);
4751
+ opacity: 1;
4752
+ }
4753
+ 100% {
4754
+ transform: translateY(-50%) scale(1);
4755
+ opacity: 1;
4756
+ }
4641
4757
  }
4642
4758
 
4643
- /* Center play button positioned within video */
4644
- .uvf-center-play-container {
4759
+ .uvf-doubletap-indicator svg {
4760
+ width: 40px;
4761
+ height: 40px;
4762
+ fill: #fff;
4763
+ }
4764
+
4765
+ /* Long-press 2x speed indicator */
4766
+ .uvf-longpress-indicator {
4645
4767
  position: absolute;
4646
4768
  top: 50%;
4647
4769
  left: 50%;
4648
4770
  transform: translate(-50%, -50%);
4649
- z-index: 8;
4771
+ background: rgba(0, 0, 0, 0.8);
4772
+ backdrop-filter: blur(16px);
4773
+ padding: 16px 24px;
4774
+ border-radius: 24px;
4775
+ color: #fff;
4776
+ font-size: 18px;
4777
+ font-weight: 600;
4650
4778
  pointer-events: none;
4779
+ opacity: 0;
4780
+ z-index: 100;
4781
+ transition: opacity 0.2s ease;
4782
+ /* Material elevation level 4 */
4783
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
4784
+ 0 10px 10px rgba(0, 0, 0, 0.22);
4651
4785
  }
4652
4786
 
4653
- .uvf-center-play-btn {
4654
- pointer-events: auto;
4787
+ .uvf-longpress-indicator.active {
4788
+ opacity: 1;
4655
4789
  }
4656
4790
 
4657
- /* Top controls within video */
4658
- .uvf-top-controls {
4659
- position: absolute;
4660
- top: calc(12px + var(--uvf-safe-area-top));
4661
- right: calc(16px + var(--uvf-safe-area-right));
4662
- z-index: 9;
4791
+ /* Hide desktop elements in Material You mode */
4792
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-top-controls,
4793
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-title-bar,
4794
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-volume-control {
4795
+ display: none !important;
4663
4796
  }
4664
4797
 
4665
- /* Title bar within video */
4666
- .uvf-title-bar {
4667
- position: absolute;
4668
- top: calc(12px + var(--uvf-safe-area-top));
4669
- left: calc(16px + var(--uvf-safe-area-left));
4670
- right: calc(80px + var(--uvf-safe-area-right));
4671
- z-index: 9;
4672
- padding: 8px 0;
4798
+ /* Optimize settings button for Material You */
4799
+ .uvf-player-wrapper.uvf-material-you-mobile #uvf-settings-btn {
4800
+ width: 48px !important;
4801
+ height: 48px !important;
4802
+ min-width: 48px !important;
4803
+ min-height: 48px !important;
4804
+ border-radius: 24px !important;
4805
+ }
4806
+ }
4807
+
4808
+ /* Mobile devices (portrait) - Enhanced UX with Safe Areas */
4809
+ @media screen and (max-width: 767px) and (orientation: portrait) {
4810
+ .uvf-responsive-container {
4811
+ padding: 0;
4812
+ width: 100vw !important;
4813
+ height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4814
+ margin: 0;
4815
+ position: relative;
4816
+ overflow: hidden;
4817
+ }
4818
+
4819
+ @supports (height: 100dvh) {
4820
+ .uvf-responsive-container {
4821
+ height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4822
+ }
4823
+ }
4824
+
4825
+ .uvf-responsive-container .uvf-player-wrapper {
4826
+ width: 100vw !important;
4827
+ height: 100% !important;
4828
+ min-height: calc(100vh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4829
+ }
4830
+
4831
+ @supports (height: 100dvh) {
4832
+ .uvf-responsive-container .uvf-player-wrapper {
4833
+ min-height: calc(100dvh - var(--uvf-safe-area-top) - var(--uvf-safe-area-bottom));
4834
+ }
4835
+ }
4836
+
4837
+ .uvf-responsive-container .uvf-video-container {
4838
+ width: 100vw !important;
4839
+ height: 100% !important;
4840
+ aspect-ratio: unset !important;
4841
+ min-height: inherit;
4842
+ }
4843
+
4844
+ /* Enhanced mobile controls bar with safe area padding - iOS Safari specific fixes */
4845
+ .uvf-controls-bar {
4846
+ position: absolute !important;
4847
+ bottom: 0 !important;
4848
+ left: 0 !important;
4849
+ right: 0 !important;
4850
+ padding: 16px 12px;
4851
+ padding-bottom: calc(16px + var(--uvf-safe-area-bottom, 0px));
4852
+ padding-left: calc(12px + var(--uvf-safe-area-left, 0px));
4853
+ padding-right: calc(12px + var(--uvf-safe-area-right, 0px));
4854
+ background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 80%, var(--uvf-overlay-transparent) 100%);
4855
+ box-sizing: border-box;
4856
+ z-index: 1000 !important;
4857
+ /* iOS Safari specific fixes */
4858
+ transform: translateZ(0);
4859
+ -webkit-transform: translateZ(0);
4860
+ will-change: transform;
4861
+ /* Ensure proper stacking */
4862
+ isolation: isolate;
4673
4863
  }
4674
4864
 
4675
4865
  .uvf-progress-section {
@@ -5940,6 +6130,26 @@ export class WebPlayer extends BasePlayer {
5940
6130
  shortcutIndicator.className = 'uvf-shortcut-indicator';
5941
6131
  shortcutIndicator.id = 'uvf-shortcut-indicator';
5942
6132
  container.appendChild(shortcutIndicator);
6133
+
6134
+ // Add Material You double-tap indicators
6135
+ const doubleTapLeft = document.createElement('div');
6136
+ doubleTapLeft.className = 'uvf-doubletap-indicator left';
6137
+ doubleTapLeft.id = 'uvf-doubletap-left';
6138
+ doubleTapLeft.innerHTML = '<svg viewBox="0 0 24 24"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/></svg><div style="margin-top:4px;font-size:14px;font-weight:600;">-10s</div>';
6139
+ container.appendChild(doubleTapLeft);
6140
+
6141
+ const doubleTapRight = document.createElement('div');
6142
+ doubleTapRight.className = 'uvf-doubletap-indicator right';
6143
+ doubleTapRight.id = 'uvf-doubletap-right';
6144
+ doubleTapRight.innerHTML = '<svg viewBox="0 0 24 24"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg><div style="margin-top:4px;font-size:14px;font-weight:600;">+10s</div>';
6145
+ container.appendChild(doubleTapRight);
6146
+
6147
+ // Add Material You long-press indicator
6148
+ const longPressIndicator = document.createElement('div');
6149
+ longPressIndicator.className = 'uvf-longpress-indicator';
6150
+ longPressIndicator.id = 'uvf-longpress-indicator';
6151
+ longPressIndicator.textContent = '2x';
6152
+ container.appendChild(longPressIndicator);
5943
6153
 
5944
6154
  // Create controls bar
5945
6155
  const controlsBar = document.createElement('div');
@@ -6158,7 +6368,7 @@ export class WebPlayer extends BasePlayer {
6158
6368
  this.debugLog('Settings button NOT created - settings disabled');
6159
6369
  }
6160
6370
 
6161
- // EPG button (Electronic Program Guide) - Only show if EPG data is available
6371
+ // EPG button (Electronic Program Guide)
6162
6372
  const epgBtn = document.createElement('button');
6163
6373
  epgBtn.className = 'uvf-control-btn';
6164
6374
  epgBtn.id = 'uvf-epg-btn';
@@ -6169,17 +6379,8 @@ export class WebPlayer extends BasePlayer {
6169
6379
  <rect x="19" y="3" width="2" height="2"/>
6170
6380
  <path d="M17 1v2h2V1h2v2h1c.55 0 1 .45 1 1v16c0 .55-.45 1-1 1H2c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h1V1h2v2h12z" fill="none" stroke="currentColor" stroke-width="0.5"/>
6171
6381
  </svg>`;
6172
- // CRITICAL: EPG button should ONLY be visible when EPG data is available
6173
- epgBtn.style.display = 'none !important'; // Force hidden - will only show when EPG data is loaded
6174
-
6175
- // Only add to controls if EPG functionality is enabled and data will be available
6176
- const epgConfig = (this.config as any).epg;
6177
- if (epgConfig && epgConfig.enabled) {
6178
- rightControls.appendChild(epgBtn);
6179
- this.debugLog('EPG button created but hidden - will show when EPG data is loaded');
6180
- } else {
6181
- this.debugLog('EPG button not created - EPG functionality disabled');
6182
- }
6382
+ epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
6383
+ rightControls.appendChild(epgBtn);
6183
6384
 
6184
6385
  // PiP button - only show on desktop/supported browsers
6185
6386
  const pipBtn = document.createElement('button');
@@ -6242,7 +6443,9 @@ export class WebPlayer extends BasePlayer {
6242
6443
  // Play/Pause
6243
6444
  centerPlay?.addEventListener('click', () => this.togglePlayPause());
6244
6445
  playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
6245
- this.video.addEventListener('click', () => this.togglePlayPause());
6446
+
6447
+ // Material You touch gestures for mobile
6448
+ this.setupMaterialYouGestures(wrapper);
6246
6449
 
6247
6450
  // Update play/pause icons
6248
6451
  this.video.addEventListener('play', () => {
@@ -6617,7 +6820,12 @@ export class WebPlayer extends BasePlayer {
6617
6820
 
6618
6821
  // Force visibility if menu is active, hide if not active
6619
6822
  if (settingsMenu) {
6620
- if (settingsMenu.classList.contains('active')) {
6823
+ const activating = settingsMenu.classList.contains('active');
6824
+ if (activating) {
6825
+ this.isSettingsOpen = true;
6826
+ // Keep controls visible and interactive while menu is open
6827
+ this.showControls();
6828
+ if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
6621
6829
  settingsMenu.style.display = 'block';
6622
6830
  settingsMenu.style.visibility = 'visible';
6623
6831
  settingsMenu.style.opacity = '1';
@@ -6633,10 +6841,13 @@ export class WebPlayer extends BasePlayer {
6633
6841
  settingsMenu.style.padding = '10px 0';
6634
6842
  this.debugLog('Applied fallback styles to show menu');
6635
6843
  } else {
6844
+ this.isSettingsOpen = false;
6636
6845
  settingsMenu.style.display = 'none';
6637
6846
  settingsMenu.style.visibility = 'hidden';
6638
6847
  settingsMenu.style.opacity = '0';
6639
6848
  this.debugLog('Applied fallback styles to hide menu');
6849
+ // After closing, allow auto-hide again if playing
6850
+ this.scheduleHideControls();
6640
6851
  }
6641
6852
  }
6642
6853
 
@@ -6676,6 +6887,19 @@ export class WebPlayer extends BasePlayer {
6676
6887
  }
6677
6888
  });
6678
6889
 
6890
+ // Keep controls active while interacting with settings menu
6891
+ if (settingsMenu) {
6892
+ const keepAlive = () => {
6893
+ if (!this.isSettingsOpen) return;
6894
+ this.showControls();
6895
+ if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
6896
+ };
6897
+ settingsMenu.addEventListener('mouseenter', keepAlive);
6898
+ settingsMenu.addEventListener('mousemove', keepAlive);
6899
+ settingsMenu.addEventListener('touchstart', keepAlive, { passive: true } as any);
6900
+ settingsMenu.addEventListener('touchmove', keepAlive, { passive: true } as any);
6901
+ }
6902
+
6679
6903
  // Add Escape key handler for settings menu
6680
6904
  document.addEventListener('keydown', (e) => {
6681
6905
  if (e.key === 'Escape' && settingsMenu?.classList.contains('active')) {
@@ -7187,6 +7411,267 @@ export class WebPlayer extends BasePlayer {
7187
7411
  }
7188
7412
  }
7189
7413
 
7414
+ /**
7415
+ * Setup Material You touch gestures for mobile
7416
+ */
7417
+ private setupMaterialYouGestures(wrapper: HTMLElement | null): void {
7418
+ if (!wrapper || !this.video) return;
7419
+
7420
+ const doubleTapLeft = document.getElementById('uvf-doubletap-left');
7421
+ const doubleTapRight = document.getElementById('uvf-doubletap-right');
7422
+ const longPressIndicator = document.getElementById('uvf-longpress-indicator');
7423
+
7424
+ // Single tap, double tap, and long press handling
7425
+ const videoElement = this.video;
7426
+ const videoContainer = wrapper.querySelector('.uvf-video-container');
7427
+
7428
+ if (!videoContainer) return;
7429
+
7430
+ // Touch start for long-press detection
7431
+ videoContainer.addEventListener('touchstart', (e: Event) => {
7432
+ const touchEvent = e as TouchEvent;
7433
+ const touch = touchEvent.touches[0];
7434
+
7435
+ if (!touch || !videoElement) return;
7436
+
7437
+ this.longPressStartTime = Date.now();
7438
+
7439
+ // Start long-press timer
7440
+ this.longPressTimer = setTimeout(() => {
7441
+ this.longPressActive = true;
7442
+ this.originalPlaybackRate = videoElement.playbackRate;
7443
+ videoElement.playbackRate = 2.0;
7444
+
7445
+ // Show indicator
7446
+ if (longPressIndicator) {
7447
+ longPressIndicator.classList.add('active');
7448
+ }
7449
+ }, 500); // 500ms for long press
7450
+ }, { passive: true });
7451
+
7452
+ // Touch end to handle tap/double-tap and stop long-press
7453
+ videoContainer.addEventListener('touchend', (e: Event) => {
7454
+ const touchEvent = e as TouchEvent;
7455
+ const touch = touchEvent.changedTouches[0];
7456
+
7457
+ if (!touch || !videoElement) return;
7458
+
7459
+ // Clear long-press timer
7460
+ if (this.longPressTimer) {
7461
+ clearTimeout(this.longPressTimer);
7462
+ this.longPressTimer = null;
7463
+ }
7464
+
7465
+ // If long-press was active, restore playback rate
7466
+ if (this.longPressActive) {
7467
+ videoElement.playbackRate = this.originalPlaybackRate;
7468
+ this.longPressActive = false;
7469
+
7470
+ if (longPressIndicator) {
7471
+ longPressIndicator.classList.remove('active');
7472
+ }
7473
+ return; // Don't process as tap
7474
+ }
7475
+
7476
+ // Tap detection
7477
+ const now = Date.now();
7478
+ const timeSinceLastTap = now - this.lastTapTime;
7479
+
7480
+ if (timeSinceLastTap < 300) { // Double tap detected (within 300ms)
7481
+ // Clear single tap timeout
7482
+ if (this.tapTimeout) {
7483
+ clearTimeout(this.tapTimeout);
7484
+ this.tapTimeout = null;
7485
+ }
7486
+
7487
+ // Determine if left or right side
7488
+ const rect = videoContainer.getBoundingClientRect();
7489
+ const x = touch.clientX - rect.left;
7490
+ const isLeftSide = x < rect.width / 2;
7491
+
7492
+ if (isLeftSide) {
7493
+ // Skip backward 10s
7494
+ this.seek(videoElement.currentTime - 10);
7495
+
7496
+ // Show indicator
7497
+ if (doubleTapLeft) {
7498
+ doubleTapLeft.classList.add('active');
7499
+ setTimeout(() => {
7500
+ doubleTapLeft.classList.remove('active');
7501
+ }, 400);
7502
+ }
7503
+ } else {
7504
+ // Skip forward 10s
7505
+ this.seek(videoElement.currentTime + 10);
7506
+
7507
+ // Show indicator
7508
+ if (doubleTapRight) {
7509
+ doubleTapRight.classList.add('active');
7510
+ setTimeout(() => {
7511
+ doubleTapRight.classList.remove('active');
7512
+ }, 400);
7513
+ }
7514
+ }
7515
+
7516
+ this.tapCount = 0;
7517
+ this.lastTapTime = 0;
7518
+ } else {
7519
+ // First tap
7520
+ this.tapCount = 1;
7521
+ this.lastTapTime = now;
7522
+
7523
+ // Wait for potential second tap
7524
+ this.tapTimeout = setTimeout(() => {
7525
+ if (this.tapCount === 1) {
7526
+ // Single tap - toggle overlay controls visibility
7527
+ this.toggleControls();
7528
+ }
7529
+ this.tapCount = 0;
7530
+ }, 300); // Wait 300ms for double tap
7531
+ }
7532
+ }, { passive: true });
7533
+
7534
+ // Touch move/cancel to reset long-press
7535
+ videoContainer.addEventListener('touchmove', () => {
7536
+ if (this.longPressTimer) {
7537
+ clearTimeout(this.longPressTimer);
7538
+ this.longPressTimer = null;
7539
+ }
7540
+ }, { passive: true });
7541
+
7542
+ videoContainer.addEventListener('touchcancel', () => {
7543
+ if (this.longPressTimer) {
7544
+ clearTimeout(this.longPressTimer);
7545
+ this.longPressTimer = null;
7546
+ }
7547
+
7548
+ if (this.longPressActive && videoElement) {
7549
+ videoElement.playbackRate = this.originalPlaybackRate;
7550
+ this.longPressActive = false;
7551
+
7552
+ if (longPressIndicator) {
7553
+ longPressIndicator.classList.remove('active');
7554
+ }
7555
+ }
7556
+ }, { passive: true });
7557
+ }
7558
+
7559
+ /**
7560
+ * Toggle controls visibility
7561
+ */
7562
+ private toggleControls(): void {
7563
+ const wrapper = this.container?.querySelector('.uvf-player-wrapper');
7564
+ if (wrapper) {
7565
+ if (wrapper.classList.contains('controls-visible')) {
7566
+ this.hideControls();
7567
+ } else {
7568
+ this.showControls();
7569
+ // Auto-hide after delay if playing
7570
+ if (this.state.isPlaying) {
7571
+ this.scheduleHideControls();
7572
+ }
7573
+ }
7574
+ }
7575
+ }
7576
+
7577
+ /**
7578
+ * Apply dynamic color theming based on video content (Material You)
7579
+ * This can be enhanced to extract colors from video frames
7580
+ */
7581
+ private applyDynamicTheming(primaryColor?: string): void {
7582
+ if (!this.playerWrapper) return;
7583
+
7584
+ const color = primaryColor || this.dominantColor;
7585
+ const rgb = this.hexToRgb(color);
7586
+
7587
+ if (rgb) {
7588
+ // Update CSS variables for Material You theming
7589
+ this.playerWrapper.style.setProperty('--uvf-accent-1', color);
7590
+ this.playerWrapper.style.setProperty('--uvf-accent-2', this.lightenColor(color, 10));
7591
+ this.playerWrapper.style.setProperty('--uvf-surface-tint', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.08)`);
7592
+
7593
+ this.debugLog(`Applied dynamic theming with color: ${color}`);
7594
+ }
7595
+ }
7596
+
7597
+ /**
7598
+ * Convert hex color to RGB
7599
+ */
7600
+ private hexToRgb(hex: string): { r: number, g: number, b: number } | null {
7601
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
7602
+ return result ? {
7603
+ r: parseInt(result[1], 16),
7604
+ g: parseInt(result[2], 16),
7605
+ b: parseInt(result[3], 16)
7606
+ } : null;
7607
+ }
7608
+
7609
+ /**
7610
+ * Lighten a hex color by a percentage
7611
+ */
7612
+ private lightenColor(hex: string, percent: number): string {
7613
+ const rgb = this.hexToRgb(hex);
7614
+ if (!rgb) return hex;
7615
+
7616
+ const amount = Math.floor(255 * (percent / 100));
7617
+ const r = Math.min(255, rgb.r + amount);
7618
+ const g = Math.min(255, rgb.g + amount);
7619
+ const b = Math.min(255, rgb.b + amount);
7620
+
7621
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
7622
+ }
7623
+
7624
+ /**
7625
+ * Render chapter markers on progress bar for Material You layout
7626
+ */
7627
+ private renderChapterMarkersOnProgressBar(): void {
7628
+ if (!this.video || !this.chapterConfig.enabled || !this.chapterConfig.showChapterMarkers) {
7629
+ return;
7630
+ }
7631
+
7632
+ const progressBar = document.querySelector('.uvf-progress-bar');
7633
+ if (!progressBar) return;
7634
+
7635
+ // Remove existing markers
7636
+ const existingMarkers = progressBar.querySelectorAll('.uvf-chapter-marker');
7637
+ existingMarkers.forEach(marker => marker.remove());
7638
+
7639
+ // Get chapter data
7640
+ const chapters = this.chapterConfig.data;
7641
+ if (!chapters || !Array.isArray(chapters) || chapters.length === 0) {
7642
+ return;
7643
+ }
7644
+
7645
+ const duration = this.video.duration;
7646
+ if (!duration || duration === 0) return;
7647
+
7648
+ // Create marker for each chapter
7649
+ chapters.forEach((chapter: any) => {
7650
+ if (!chapter.startTime && chapter.startTime !== 0) return;
7651
+
7652
+ const marker = document.createElement('div');
7653
+ marker.className = 'uvf-chapter-marker';
7654
+
7655
+ // Add chapter type class if available
7656
+ if (chapter.type) {
7657
+ marker.classList.add(chapter.type.toLowerCase());
7658
+ }
7659
+
7660
+ // Position based on startTime
7661
+ const percent = (chapter.startTime / duration) * 100;
7662
+ marker.style.left = `${percent}%`;
7663
+
7664
+ // Add title attribute for tooltip
7665
+ if (chapter.title) {
7666
+ marker.setAttribute('title', chapter.title);
7667
+ }
7668
+
7669
+ progressBar.appendChild(marker);
7670
+ });
7671
+
7672
+ this.debugLog(`Rendered ${chapters.length} chapter markers on progress bar`);
7673
+ }
7674
+
7190
7675
  /**
7191
7676
  * Detect if user is on a mobile device
7192
7677
  */
@@ -7327,6 +7812,8 @@ export class WebPlayer extends BasePlayer {
7327
7812
 
7328
7813
  private hideControls(): void {
7329
7814
  if (!this.state.isPlaying) return;
7815
+ // Never hide controls while settings menu is open
7816
+ if (this.isSettingsOpen) return;
7330
7817
 
7331
7818
  const wrapper = this.container?.querySelector('.uvf-player-wrapper');
7332
7819
  if (wrapper) {
@@ -7337,12 +7824,14 @@ export class WebPlayer extends BasePlayer {
7337
7824
 
7338
7825
  private scheduleHideControls(): void {
7339
7826
  if (!this.state.isPlaying) return;
7827
+ // Do not schedule auto-hide while settings are open
7828
+ if (this.isSettingsOpen) return;
7340
7829
 
7341
7830
  if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
7342
7831
  // Use longer timeout in fullscreen for better UX
7343
7832
  const timeout = this.isFullscreen() ? 4000 : 3000;
7344
7833
  this.hideControlsTimeout = setTimeout(() => {
7345
- if (this.state.isPlaying && !this.controlsContainer?.matches(':hover')) {
7834
+ if (this.state.isPlaying && !this.controlsContainer?.matches(':hover') && !this.isSettingsOpen) {
7346
7835
  this.hideControls();
7347
7836
  }
7348
7837
  }, timeout);
@@ -8551,6 +9040,7 @@ export class WebPlayer extends BasePlayer {
8551
9040
  if (!settingsMenu) return;
8552
9041
 
8553
9042
  settingsMenu.classList.remove('active');
9043
+ this.isSettingsOpen = false;
8554
9044
 
8555
9045
  // Apply fallback styles to ensure menu is hidden
8556
9046
  settingsMenu.style.display = 'none';
@@ -8563,6 +9053,8 @@ export class WebPlayer extends BasePlayer {
8563
9053
  });
8564
9054
 
8565
9055
  this.debugLog('Settings menu hidden via hideSettingsMenu()');
9056
+ // Resume auto-hide if appropriate
9057
+ this.scheduleHideControls();
8566
9058
  }
8567
9059
 
8568
9060
  /**
@@ -9143,9 +9635,8 @@ export class WebPlayer extends BasePlayer {
9143
9635
  public showEPGButton(): void {
9144
9636
  const epgBtn = document.getElementById('uvf-epg-btn');
9145
9637
  if (epgBtn) {
9146
- // Remove any forced display none and set to block
9147
- epgBtn.style.setProperty('display', 'block', 'important');
9148
- this.debugLog('EPG button shown with !important override');
9638
+ epgBtn.style.display = 'block';
9639
+ this.debugLog('EPG button shown');
9149
9640
  } else {
9150
9641
  this.debugLog('EPG button not found in DOM');
9151
9642
  }
@@ -9157,8 +9648,8 @@ export class WebPlayer extends BasePlayer {
9157
9648
  public hideEPGButton(): void {
9158
9649
  const epgBtn = document.getElementById('uvf-epg-btn');
9159
9650
  if (epgBtn) {
9160
- epgBtn.style.setProperty('display', 'none', 'important');
9161
- this.debugLog('EPG button hidden with !important');
9651
+ epgBtn.style.display = 'none';
9652
+ this.debugLog('EPG button hidden');
9162
9653
  }
9163
9654
  }
9164
9655
 
@@ -9183,30 +9674,7 @@ export class WebPlayer extends BasePlayer {
9183
9674
  */
9184
9675
  public isEPGButtonVisible(): boolean {
9185
9676
  const epgBtn = document.getElementById('uvf-epg-btn');
9186
- if (epgBtn) {
9187
- const isVisible = epgBtn.style.display !== 'none';
9188
- this.debugLog(`EPG button visibility check: ${isVisible}, display style: ${epgBtn.style.display}`);
9189
- return isVisible;
9190
- }
9191
- this.debugLog('EPG button visibility check: false (button not found in DOM)');
9192
- return false;
9193
- }
9194
-
9195
- /**
9196
- * Debug method to log current EPG button state and configuration
9197
- */
9198
- public debugEPGState(): void {
9199
- const epgBtn = document.getElementById('uvf-epg-btn');
9200
- const epgConfig = (this.config as any).epg;
9201
- this.debugLog('=== EPG DEBUG STATE ===');
9202
- this.debugLog(`EPG Config Enabled: ${epgConfig?.enabled || false}`);
9203
- this.debugLog(`EPG Button Exists: ${!!epgBtn}`);
9204
- if (epgBtn) {
9205
- this.debugLog(`EPG Button Display Style: ${epgBtn.style.display}`);
9206
- this.debugLog(`EPG Button Computed Display: ${window.getComputedStyle(epgBtn).display}`);
9207
- this.debugLog(`EPG Button Visible: ${this.isEPGButtonVisible()}`);
9208
- }
9209
- this.debugLog('=====================');
9677
+ return epgBtn ? epgBtn.style.display !== 'none' : false;
9210
9678
  }
9211
9679
 
9212
9680
  private async cleanup(): Promise<void> {