unified-video-framework 1.4.165 → 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.
@@ -100,6 +100,20 @@ export class WebPlayer extends BasePlayer {
100
100
  // Progress bar tooltip state
101
101
  private showTimeTooltip: boolean = false;
102
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
+
103
117
  // Autoplay enhancement state
104
118
  private autoplayCapabilities: {
105
119
  canAutoplay: boolean;
@@ -345,6 +359,35 @@ export class WebPlayer extends BasePlayer {
345
359
 
346
360
  // Initialize metadata UI to hidden/empty by default
347
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
+ }
348
391
  }
349
392
 
350
393
  private setupVideoEventListeners(): void {
@@ -437,6 +480,9 @@ export class WebPlayer extends BasePlayer {
437
480
  // Update time display immediately when metadata loads
438
481
  this.updateTimeDisplay();
439
482
 
483
+ // Render chapter markers if enabled
484
+ this.renderChapterMarkersOnProgressBar();
485
+
440
486
  this.emit('onLoadedMetadata', {
441
487
  duration: this.video.duration || 0,
442
488
  width: this.video.videoWidth || 0,
@@ -2346,6 +2392,8 @@ export class WebPlayer extends BasePlayer {
2346
2392
  --uvf-scrollbar-thumb-hover-start: rgba(255,0,0,0.5);
2347
2393
  --uvf-scrollbar-thumb-hover-end: rgba(255,0,0,0.6);
2348
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);
2349
2397
  }
2350
2398
 
2351
2399
  /* Player focus styles for better UX */
@@ -4401,6 +4449,362 @@ export class WebPlayer extends BasePlayer {
4401
4449
  }
4402
4450
 
4403
4451
  /* Enhanced Responsive Media Queries with UX Best Practices */
4452
+
4453
+ /* Material You Mobile Portrait Layout - 25% Black + 50% Video + 25% Black */
4454
+ @media screen and (max-width: 767px) and (orientation: portrait) {
4455
+ /* Enable Material You mode with class flag */
4456
+ .uvf-player-wrapper.uvf-material-you-mobile {
4457
+ /* Full viewport height layout */
4458
+ height: 100vh;
4459
+ height: 100dvh;
4460
+ width: 100vw;
4461
+ position: fixed;
4462
+ top: 0;
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;
4474
+ width: 100vw;
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);
4485
+ }
4486
+
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 {
4495
+ content: '';
4496
+ position: absolute;
4497
+ top: 0;
4498
+ left: 0;
4499
+ width: 100vw;
4500
+ height: 25vh;
4501
+ height: 25dvh;
4502
+ background: #000;
4503
+ z-index: 1;
4504
+ pointer-events: all;
4505
+ touch-action: manipulation;
4506
+ }
4507
+
4508
+ /* Bottom black section (25%) - Surface container */
4509
+ .uvf-player-wrapper.uvf-material-you-mobile::after {
4510
+ content: '';
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;
4522
+ pointer-events: none;
4523
+ }
4524
+
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;
4538
+ display: flex;
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;
4560
+ position: relative;
4561
+ }
4562
+
4563
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-bar-wrapper {
4564
+ padding: 12px 0;
4565
+ position: relative;
4566
+ }
4567
+
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);
4576
+ }
4577
+
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);
4581
+ }
4582
+
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);
4591
+ }
4592
+
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);
4597
+ }
4598
+
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;
4611
+ }
4612
+
4613
+ .uvf-chapter-marker.intro {
4614
+ background: #4CAF50;
4615
+ }
4616
+
4617
+ .uvf-chapter-marker.recap {
4618
+ background: #FFC107;
4619
+ }
4620
+
4621
+ .uvf-chapter-marker.credits {
4622
+ background: #9C27B0;
4623
+ }
4624
+
4625
+ .uvf-progress-bar-wrapper:hover .uvf-chapter-marker {
4626
+ width: 4px;
4627
+ opacity: 1;
4628
+ }
4629
+
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;
4644
+ }
4645
+
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;
4693
+ align-items: center;
4694
+ }
4695
+
4696
+ /* Time display with Material surface */
4697
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-time-display {
4698
+ background: rgba(255, 255, 255, 0.1);
4699
+ backdrop-filter: blur(8px);
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);
4707
+ }
4708
+
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;
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);
4729
+ }
4730
+
4731
+ .uvf-doubletap-indicator.left {
4732
+ left: 15%;
4733
+ }
4734
+
4735
+ .uvf-doubletap-indicator.right {
4736
+ right: 15%;
4737
+ }
4738
+
4739
+ .uvf-doubletap-indicator.active {
4740
+ opacity: 1;
4741
+ animation: doubletap-pulse 0.4s cubic-bezier(0.4, 0, 0.2, 1);
4742
+ }
4743
+
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
+ }
4757
+ }
4758
+
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 {
4767
+ position: absolute;
4768
+ top: 50%;
4769
+ left: 50%;
4770
+ transform: translate(-50%, -50%);
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;
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);
4785
+ }
4786
+
4787
+ .uvf-longpress-indicator.active {
4788
+ opacity: 1;
4789
+ }
4790
+
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;
4796
+ }
4797
+
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
+
4404
4808
  /* Mobile devices (portrait) - Enhanced UX with Safe Areas */
4405
4809
  @media screen and (max-width: 767px) and (orientation: portrait) {
4406
4810
  .uvf-responsive-container {
@@ -5726,6 +6130,26 @@ export class WebPlayer extends BasePlayer {
5726
6130
  shortcutIndicator.className = 'uvf-shortcut-indicator';
5727
6131
  shortcutIndicator.id = 'uvf-shortcut-indicator';
5728
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);
5729
6153
 
5730
6154
  // Create controls bar
5731
6155
  const controlsBar = document.createElement('div');
@@ -6019,7 +6443,9 @@ export class WebPlayer extends BasePlayer {
6019
6443
  // Play/Pause
6020
6444
  centerPlay?.addEventListener('click', () => this.togglePlayPause());
6021
6445
  playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
6022
- this.video.addEventListener('click', () => this.togglePlayPause());
6446
+
6447
+ // Material You touch gestures for mobile
6448
+ this.setupMaterialYouGestures(wrapper);
6023
6449
 
6024
6450
  // Update play/pause icons
6025
6451
  this.video.addEventListener('play', () => {
@@ -6985,6 +7411,267 @@ export class WebPlayer extends BasePlayer {
6985
7411
  }
6986
7412
  }
6987
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
+
6988
7675
  /**
6989
7676
  * Detect if user is on a mobile device
6990
7677
  */