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
|
-
|
|
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
|
*/
|