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.
- package/package.json +1 -1
- package/packages/core/dist/interfaces.d.ts +0 -1
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces.ts +0 -1
- package/packages/web/dist/WebPlayer.d.ts +18 -1
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +605 -236
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +712 -244
|
@@ -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
|
-
|
|
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
|
-
/*
|
|
4404
|
-
.uvf-
|
|
4405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4416
|
-
margin:
|
|
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
|
-
|
|
4420
|
-
|
|
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
|
-
|
|
4496
|
+
position: absolute;
|
|
4497
|
+
top: 0;
|
|
4498
|
+
left: 0;
|
|
4499
|
+
width: 100vw;
|
|
4500
|
+
height: 25vh;
|
|
4501
|
+
height: 25dvh;
|
|
4423
4502
|
background: #000;
|
|
4424
|
-
|
|
4503
|
+
z-index: 1;
|
|
4504
|
+
pointer-events: all;
|
|
4505
|
+
touch-action: manipulation;
|
|
4425
4506
|
}
|
|
4426
4507
|
|
|
4427
|
-
/*
|
|
4428
|
-
.uvf-
|
|
4508
|
+
/* Bottom black section (25%) - Surface container */
|
|
4509
|
+
.uvf-player-wrapper.uvf-material-you-mobile::after {
|
|
4429
4510
|
content: '';
|
|
4430
|
-
|
|
4431
|
-
|
|
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
|
-
/*
|
|
4436
|
-
.uvf-
|
|
4437
|
-
|
|
4438
|
-
|
|
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
|
-
|
|
4441
|
-
justify-content:
|
|
4442
|
-
|
|
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
|
-
|
|
4453
|
-
|
|
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
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
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
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
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
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
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
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
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
|
-
/*
|
|
4546
|
-
.uvf-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
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
|
-
|
|
4555
|
-
|
|
4556
|
-
opacity: 1 !important;
|
|
4557
|
-
visibility: visible !important;
|
|
4613
|
+
.uvf-chapter-marker.intro {
|
|
4614
|
+
background: #4CAF50;
|
|
4558
4615
|
}
|
|
4559
4616
|
|
|
4560
|
-
.uvf-
|
|
4561
|
-
|
|
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-
|
|
4567
|
-
background:
|
|
4568
|
-
height: 100%;
|
|
4569
|
-
border-radius: 2px;
|
|
4621
|
+
.uvf-chapter-marker.credits {
|
|
4622
|
+
background: #9C27B0;
|
|
4570
4623
|
}
|
|
4571
4624
|
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
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
|
-
/*
|
|
4585
|
-
.uvf-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
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
|
-
/*
|
|
4595
|
-
.uvf-control-btn {
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
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
|
-
|
|
4601
|
-
|
|
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
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
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
|
-
|
|
4611
|
-
|
|
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
|
-
|
|
4616
|
-
|
|
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-
|
|
4624
|
-
|
|
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-
|
|
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
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
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
|
-
|
|
4644
|
-
|
|
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
|
-
|
|
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-
|
|
4654
|
-
|
|
4787
|
+
.uvf-longpress-indicator.active {
|
|
4788
|
+
opacity: 1;
|
|
4655
4789
|
}
|
|
4656
4790
|
|
|
4657
|
-
/*
|
|
4658
|
-
.uvf-top-controls
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
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
|
-
/*
|
|
4666
|
-
.uvf-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
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)
|
|
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
|
-
|
|
6173
|
-
epgBtn
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9147
|
-
|
|
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.
|
|
9161
|
-
this.debugLog('EPG button hidden
|
|
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
|
-
|
|
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> {
|