unified-video-framework 1.4.209 → 1.4.211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/BasePlayer.d.ts +6 -5
  3. package/packages/core/dist/BasePlayer.d.ts.map +1 -1
  4. package/packages/core/dist/BasePlayer.js.map +1 -1
  5. package/packages/core/dist/VideoPlayer.js +1 -1
  6. package/packages/core/dist/VideoPlayer.js.map +1 -1
  7. package/packages/core/dist/interfaces/IVideoPlayer.d.ts +5 -3
  8. package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
  9. package/packages/core/dist/interfaces.d.ts +26 -1
  10. package/packages/core/dist/interfaces.d.ts.map +1 -1
  11. package/packages/core/src/BasePlayer.ts +5 -5
  12. package/packages/core/src/VideoPlayer.ts +1 -1
  13. package/packages/core/src/interfaces/IVideoPlayer.ts +6 -3
  14. package/packages/core/src/interfaces.ts +60 -26
  15. package/packages/web/dist/WebPlayer.d.ts +8 -0
  16. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  17. package/packages/web/dist/WebPlayer.js +541 -36
  18. package/packages/web/dist/WebPlayer.js.map +1 -1
  19. package/packages/web/dist/react/WebPlayerView.d.ts +24 -0
  20. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  21. package/packages/web/dist/react/WebPlayerView.js +8 -0
  22. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  23. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.d.ts.map +1 -1
  24. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.js +0 -8
  25. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.js.map +1 -1
  26. package/packages/web/dist/react/components/EPGOverlay.d.ts.map +1 -1
  27. package/packages/web/dist/react/components/EPGOverlay.js +0 -8
  28. package/packages/web/dist/react/components/EPGOverlay.js.map +1 -1
  29. package/packages/web/src/WebPlayer.ts +632 -41
  30. package/packages/web/src/react/WebPlayerView.tsx +39 -0
  31. package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +0 -8
  32. package/packages/web/src/react/components/EPGOverlay.tsx +0 -8
@@ -3750,7 +3750,7 @@ export class WebPlayer extends BasePlayer {
3750
3750
  margin-left: 10px;
3751
3751
  }
3752
3752
 
3753
- /* Top Bar Container - Contains Title (left) and Controls (right) */
3753
+ /* Top Bar Container - Contains Navigation + Title (left) and Controls (right) */
3754
3754
  .uvf-top-bar {
3755
3755
  position: absolute;
3756
3756
  top: 0;
@@ -3767,16 +3767,97 @@ export class WebPlayer extends BasePlayer {
3767
3767
  transition: all 0.3s ease;
3768
3768
  }
3769
3769
 
3770
+ /* Left side container for navigation + title */
3771
+ .uvf-left-side {
3772
+ display: flex;
3773
+ align-items: center;
3774
+ gap: 12px;
3775
+ flex: 1;
3776
+ max-width: 70%;
3777
+ }
3778
+
3779
+ /* Navigation controls container */
3780
+ .uvf-navigation-controls {
3781
+ display: flex;
3782
+ align-items: center;
3783
+ gap: 8px;
3784
+ flex-shrink: 0;
3785
+ }
3786
+
3787
+ /* Navigation button styles */
3788
+ .uvf-nav-btn {
3789
+ width: 40px;
3790
+ height: 40px;
3791
+ min-width: 40px;
3792
+ min-height: 40px;
3793
+ border-radius: 50%;
3794
+ background: rgba(0, 0, 0, 0.6);
3795
+ border: 1px solid rgba(255, 255, 255, 0.2);
3796
+ color: white;
3797
+ cursor: pointer;
3798
+ display: flex;
3799
+ align-items: center;
3800
+ justify-content: center;
3801
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3802
+ backdrop-filter: blur(8px);
3803
+ position: relative;
3804
+ overflow: hidden;
3805
+ }
3806
+
3807
+ .uvf-nav-btn:hover {
3808
+ background: rgba(255, 255, 255, 0.15);
3809
+ border-color: rgba(255, 255, 255, 0.4);
3810
+ transform: scale(1.05);
3811
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3812
+ }
3813
+
3814
+ .uvf-nav-btn:active {
3815
+ transform: scale(0.95);
3816
+ }
3817
+
3818
+ .uvf-nav-btn svg {
3819
+ width: 20px;
3820
+ height: 20px;
3821
+ fill: currentColor;
3822
+ transition: all 0.2s ease;
3823
+ }
3824
+
3825
+ .uvf-nav-btn:hover svg {
3826
+ transform: scale(1.1);
3827
+ }
3828
+
3829
+ /* Back button specific styles */
3830
+ #uvf-back-btn {
3831
+ background: rgba(0, 0, 0, 0.7);
3832
+ }
3833
+
3834
+ #uvf-back-btn:hover {
3835
+ background: rgba(255, 255, 255, 0.2);
3836
+ border-color: var(--uvf-accent-1, #ff0000);
3837
+ }
3838
+
3839
+ /* Close button specific styles */
3840
+ #uvf-close-btn {
3841
+ background: rgba(220, 53, 69, 0.8);
3842
+ border-color: rgba(220, 53, 69, 0.6);
3843
+ }
3844
+
3845
+ #uvf-close-btn:hover {
3846
+ background: rgba(220, 53, 69, 1);
3847
+ border-color: rgba(220, 53, 69, 1);
3848
+ box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
3849
+ }
3850
+
3770
3851
  .uvf-player-wrapper:hover .uvf-top-bar,
3771
3852
  .uvf-player-wrapper.controls-visible .uvf-top-bar {
3772
3853
  opacity: 1;
3773
3854
  transform: translateY(0);
3774
3855
  }
3775
3856
 
3776
- /* Title Bar - Left side of top bar */
3857
+ /* Title Bar - After navigation buttons */
3777
3858
  .uvf-title-bar {
3778
- flex: 0 1 auto;
3779
- max-width: 60%;
3859
+ flex: 1;
3860
+ min-width: 0; /* Allow shrinking */
3780
3861
  }
3781
3862
 
3782
3863
  /* Top Controls - Right side of top bar */
@@ -3791,33 +3872,112 @@ export class WebPlayer extends BasePlayer {
3791
3872
  .uvf-title-content {
3792
3873
  display: flex;
3793
3874
  align-items: center;
3794
- gap: 12px;
3875
+ width: 100%;
3876
+ min-width: 0; /* Allow shrinking */
3795
3877
  }
3796
- .uvf-video-thumb {
3797
- width: 56px;
3798
- height: 56px;
3799
- border-radius: 8px;
3800
- object-fit: cover;
3801
- box-shadow: 0 4px 14px rgba(0,0,0,0.5);
3802
- border: 1px solid rgba(255,255,255,0.25);
3803
- background: rgba(255,255,255,0.05);
3878
+
3879
+ .uvf-title-text {
3880
+ display: flex;
3881
+ flex-direction: column;
3882
+ min-width: 0; /* Allow shrinking */
3883
+ flex: 1;
3804
3884
  }
3805
- .uvf-title-text { display: flex; flex-direction: column; }
3885
+
3806
3886
  .uvf-video-title {
3807
3887
  color: var(--uvf-text-primary);
3808
- font-size: 18px;
3888
+ font-size: clamp(14px, 2.5vw, 18px); /* Responsive font size */
3809
3889
  font-weight: 600;
3810
3890
  text-shadow: 0 2px 4px rgba(0,0,0,0.5);
3891
+ line-height: 1.3;
3892
+ overflow: hidden;
3893
+ text-overflow: ellipsis;
3894
+ white-space: nowrap;
3895
+ max-width: 100%;
3896
+ cursor: pointer;
3897
+ transition: color 0.3s ease;
3898
+ position: relative;
3899
+ }
3900
+
3901
+ .uvf-video-title:hover {
3902
+ color: var(--uvf-accent-1, #ff0000);
3811
3903
  }
3812
3904
 
3813
3905
  .uvf-video-subtitle {
3814
3906
  color: var(--uvf-text-secondary);
3815
- font-size: 13px;
3816
- margin-top: 4px;
3817
- max-width: min(70vw, 900px);
3907
+ font-size: clamp(11px, 1.8vw, 13px); /* Responsive font size */
3908
+ margin-top: 2px;
3818
3909
  overflow: hidden;
3819
3910
  text-overflow: ellipsis;
3820
3911
  white-space: nowrap;
3912
+ max-width: 100%;
3913
+ opacity: 0.9;
3914
+ line-height: 1.4;
3915
+ cursor: pointer;
3916
+ transition: opacity 0.3s ease;
3917
+ position: relative;
3918
+ }
3919
+
3920
+ .uvf-video-subtitle:hover {
3921
+ opacity: 1;
3922
+ }
3923
+
3924
+ /* Tooltip for long text */
3925
+ .uvf-text-tooltip {
3926
+ position: absolute;
3927
+ bottom: 100%;
3928
+ left: 0;
3929
+ background: rgba(0, 0, 0, 0.9);
3930
+ color: white;
3931
+ padding: 8px 12px;
3932
+ border-radius: 6px;
3933
+ font-size: 13px;
3934
+ line-height: 1.4;
3935
+ max-width: 400px;
3936
+ word-wrap: break-word;
3937
+ white-space: normal;
3938
+ z-index: 1000;
3939
+ opacity: 0;
3940
+ visibility: hidden;
3941
+ transform: translateY(-5px);
3942
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3943
+ pointer-events: none;
3944
+ border: 1px solid rgba(255, 255, 255, 0.2);
3945
+ backdrop-filter: blur(8px);
3946
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
3947
+ }
3948
+
3949
+ .uvf-text-tooltip::before {
3950
+ content: '';
3951
+ position: absolute;
3952
+ top: 100%;
3953
+ left: 12px;
3954
+ border: 5px solid transparent;
3955
+ border-top-color: rgba(0, 0, 0, 0.9);
3956
+ }
3957
+
3958
+ .uvf-text-tooltip.show {
3959
+ opacity: 1;
3960
+ visibility: visible;
3961
+ transform: translateY(0);
3962
+ }
3963
+
3964
+ /* Multi-line title option for desktop */
3965
+ .uvf-video-title.multiline {
3966
+ white-space: normal;
3967
+ display: -webkit-box;
3968
+ -webkit-line-clamp: 2;
3969
+ -webkit-box-orient: vertical;
3970
+ line-height: 1.2;
3971
+ max-height: 2.4em;
3972
+ }
3973
+
3974
+ .uvf-video-subtitle.multiline {
3975
+ white-space: normal;
3976
+ display: -webkit-box;
3977
+ -webkit-line-clamp: 3;
3978
+ -webkit-box-orient: vertical;
3979
+ line-height: 1.3;
3980
+ max-height: 3.9em;
3821
3981
  }
3822
3982
 
3823
3983
  /* Above seekbar section with time and branding */
@@ -3896,7 +4056,21 @@ export class WebPlayer extends BasePlayer {
3896
4056
  }
3897
4057
  }
3898
4058
 
4059
+ /* Ultra small screens */
3899
4060
  @media (max-width: 480px) {
4061
+ .uvf-video-title {
4062
+ font-size: clamp(11px, 3.5vw, 14px) !important;
4063
+ }
4064
+
4065
+ .uvf-video-subtitle {
4066
+ font-size: clamp(9px, 2.5vw, 11px) !important;
4067
+ -webkit-line-clamp: 1; /* Single line on very small screens */
4068
+ }
4069
+
4070
+ .uvf-left-side {
4071
+ max-width: 80%;
4072
+ }
4073
+
3900
4074
  .uvf-above-seekbar-section {
3901
4075
  margin-bottom: 5px;
3902
4076
  }
@@ -4284,14 +4458,55 @@ export class WebPlayer extends BasePlayer {
4284
4458
  }
4285
4459
 
4286
4460
 
4287
- /* Mobile portrait - hide skip buttons, ensure top bar visible */
4461
+ /* Mobile responsive styles for navigation buttons */
4462
+ @media screen and (max-width: 767px) {
4463
+ .uvf-nav-btn {
4464
+ width: 36px;
4465
+ height: 36px;
4466
+ min-width: 36px;
4467
+ min-height: 36px;
4468
+ }
4469
+
4470
+ .uvf-nav-btn svg {
4471
+ width: 18px;
4472
+ height: 18px;
4473
+ }
4474
+
4475
+ .uvf-navigation-controls {
4476
+ gap: 6px;
4477
+ }
4478
+
4479
+ .uvf-left-side {
4480
+ gap: 8px;
4481
+ max-width: 75%;
4482
+ }
4483
+
4484
+ /* Mobile title adjustments */
4485
+ .uvf-video-title {
4486
+ font-size: clamp(12px, 3vw, 16px) !important;
4487
+ line-height: 1.2;
4488
+ }
4489
+
4490
+ .uvf-video-subtitle {
4491
+ font-size: clamp(10px, 2.2vw, 12px) !important;
4492
+ margin-top: 1px;
4493
+ /* Allow wrapping on mobile if needed */
4494
+ white-space: normal;
4495
+ display: -webkit-box;
4496
+ -webkit-line-clamp: 2;
4497
+ -webkit-box-orient: vertical;
4498
+ overflow: hidden;
4499
+ }
4500
+ }
4501
+
4502
+ /* Mobile portrait - hide skip buttons, ensure top bar syncs with controls */
4288
4503
  @media screen and (max-width: 767px) and (orientation: portrait) {
4289
4504
  #uvf-skip-back,
4290
4505
  #uvf-skip-forward {
4291
4506
  display: none !important;
4292
4507
  }
4293
4508
 
4294
- /* Ensure top bar and controls are visible */
4509
+ /* Top bar structure */
4295
4510
  .uvf-top-bar {
4296
4511
  display: flex !important;
4297
4512
  z-index: 10 !important;
@@ -4301,8 +4516,13 @@ export class WebPlayer extends BasePlayer {
4301
4516
  display: flex !important;
4302
4517
  }
4303
4518
 
4304
- /* Show top bar when controls are visible or on hover */
4305
- .uvf-player-wrapper:hover .uvf-top-bar,
4519
+ /* Disable hover effect on mobile - use only controls-visible class */
4520
+ .uvf-player-wrapper:hover .uvf-top-bar {
4521
+ opacity: 0 !important;
4522
+ transform: translateY(-10px) !important;
4523
+ }
4524
+
4525
+ /* Show top bar ONLY when controls are visible */
4306
4526
  .uvf-player-wrapper.controls-visible .uvf-top-bar {
4307
4527
  opacity: 1 !important;
4308
4528
  transform: translateY(0) !important;
@@ -4438,6 +4658,18 @@ export class WebPlayer extends BasePlayer {
4438
4658
  height: 16px;
4439
4659
  }
4440
4660
 
4661
+ /* Disable hover effect on mobile landscape - use only controls-visible class */
4662
+ .uvf-player-wrapper:hover .uvf-top-bar {
4663
+ opacity: 0 !important;
4664
+ transform: translateY(-10px) !important;
4665
+ }
4666
+
4667
+ /* Show top bar ONLY when controls are visible */
4668
+ .uvf-player-wrapper.controls-visible .uvf-top-bar {
4669
+ opacity: 1 !important;
4670
+ transform: translateY(0) !important;
4671
+ }
4672
+
4441
4673
  /* Top bar in fullscreen landscape */
4442
4674
  .uvf-player-wrapper.uvf-fullscreen .uvf-top-bar,
4443
4675
  .uvf-player-wrapper.uvf-fullscreen .uvf-video-container .uvf-top-bar,
@@ -4451,6 +4683,31 @@ export class WebPlayer extends BasePlayer {
4451
4683
 
4452
4684
  /* Tablet devices - Enhanced UX with desktop features */
4453
4685
  @media screen and (min-width: 768px) and (max-width: 1023px) {
4686
+ /* Tablet navigation and title adjustments */
4687
+ .uvf-nav-btn {
4688
+ width: 38px;
4689
+ height: 38px;
4690
+ min-width: 38px;
4691
+ min-height: 38px;
4692
+ }
4693
+
4694
+ .uvf-nav-btn svg {
4695
+ width: 19px;
4696
+ height: 19px;
4697
+ }
4698
+
4699
+ .uvf-left-side {
4700
+ max-width: 70%;
4701
+ }
4702
+
4703
+ .uvf-video-title {
4704
+ font-size: clamp(15px, 2.2vw, 17px) !important;
4705
+ }
4706
+
4707
+ .uvf-video-subtitle {
4708
+ font-size: clamp(12px, 1.8vw, 13px) !important;
4709
+ }
4710
+
4454
4711
  .uvf-controls-bar {
4455
4712
  padding: 18px 16px;
4456
4713
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 70%, var(--uvf-overlay-transparent) 100%);
@@ -4983,6 +5240,40 @@ export class WebPlayer extends BasePlayer {
4983
5240
  }
4984
5241
  }
4985
5242
 
5243
+ /* Desktop styles for title and navigation */
5244
+ @media screen and (min-width: 1024px) {
5245
+ .uvf-left-side {
5246
+ max-width: 65%; /* More space for title on desktop */
5247
+ }
5248
+
5249
+ .uvf-video-title {
5250
+ font-size: clamp(16px, 1.8vw, 20px) !important;
5251
+ font-weight: 700; /* Bolder on desktop */
5252
+ }
5253
+
5254
+ .uvf-video-subtitle {
5255
+ font-size: clamp(13px, 1.4vw, 15px) !important;
5256
+ margin-top: 3px;
5257
+ }
5258
+
5259
+ /* Allow hover effects on desktop */
5260
+ .uvf-title-bar:hover .uvf-video-title {
5261
+ color: var(--uvf-accent-1, #ff0000);
5262
+ transition: color 0.3s ease;
5263
+ }
5264
+ }
5265
+
5266
+ /* Ultra-wide screens */
5267
+ @media screen and (min-width: 1440px) {
5268
+ .uvf-video-title {
5269
+ font-size: clamp(18px, 1.6vw, 22px) !important;
5270
+ }
5271
+
5272
+ .uvf-video-subtitle {
5273
+ font-size: clamp(14px, 1.2vw, 16px) !important;
5274
+ }
5275
+ }
5276
+
4986
5277
  /* Paywall Desktop */
4987
5278
  @media screen and (min-width: 1024px) {
4988
5279
  .uvf-paywall-modal {
@@ -5058,6 +5349,132 @@ export class WebPlayer extends BasePlayer {
5058
5349
  this.debugLog('Framework branding added');
5059
5350
  }
5060
5351
 
5352
+ /**
5353
+ * Create navigation buttons (back/close) based on configuration
5354
+ */
5355
+ private createNavigationButtons(container: HTMLElement): void {
5356
+ const navigationConfig = (this.config as any).navigation;
5357
+ if (!navigationConfig) return;
5358
+
5359
+ const { backButton, closeButton } = navigationConfig;
5360
+
5361
+ // Back button
5362
+ if (backButton?.enabled) {
5363
+ const backBtn = document.createElement('button');
5364
+ backBtn.className = 'uvf-control-btn uvf-nav-btn';
5365
+ backBtn.id = 'uvf-back-btn';
5366
+ backBtn.title = backButton.title || 'Back';
5367
+ backBtn.setAttribute('aria-label', backButton.ariaLabel || 'Go back');
5368
+
5369
+ // Get icon based on config
5370
+ const backIcon = this.getNavigationIcon(backButton.icon || 'arrow', backButton.customIcon);
5371
+ backBtn.innerHTML = backIcon;
5372
+
5373
+ // Add click handler
5374
+ backBtn.addEventListener('click', async (e) => {
5375
+ e.preventDefault();
5376
+ e.stopPropagation();
5377
+
5378
+ if (backButton.onClick) {
5379
+ await backButton.onClick();
5380
+ } else if (backButton.href) {
5381
+ if (backButton.replace) {
5382
+ window.history.replaceState(null, '', backButton.href);
5383
+ } else {
5384
+ window.location.href = backButton.href;
5385
+ }
5386
+ } else {
5387
+ // Default: go back in history
5388
+ window.history.back();
5389
+ }
5390
+
5391
+ this.emit('navigationBackClicked');
5392
+ });
5393
+
5394
+ container.appendChild(backBtn);
5395
+ }
5396
+
5397
+ // Close button
5398
+ if (closeButton?.enabled) {
5399
+ const closeBtn = document.createElement('button');
5400
+ closeBtn.className = 'uvf-control-btn uvf-nav-btn';
5401
+ closeBtn.id = 'uvf-close-btn';
5402
+ closeBtn.title = closeButton.title || 'Close';
5403
+ closeBtn.setAttribute('aria-label', closeButton.ariaLabel || 'Close player');
5404
+
5405
+ // Get icon based on config
5406
+ const closeIcon = this.getNavigationIcon(closeButton.icon || 'x', closeButton.customIcon);
5407
+ closeBtn.innerHTML = closeIcon;
5408
+
5409
+ // Add click handler
5410
+ closeBtn.addEventListener('click', async (e) => {
5411
+ e.preventDefault();
5412
+ e.stopPropagation();
5413
+
5414
+ if (closeButton.onClick) {
5415
+ await closeButton.onClick();
5416
+ } else {
5417
+ // Default behaviors
5418
+ if (closeButton.exitFullscreen && this.isFullscreen()) {
5419
+ await this.exitFullscreen();
5420
+ }
5421
+
5422
+ if (closeButton.closeModal) {
5423
+ // Hide player or remove from DOM
5424
+ const playerWrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
5425
+ if (playerWrapper) {
5426
+ playerWrapper.style.display = 'none';
5427
+ }
5428
+ }
5429
+ }
5430
+
5431
+ this.emit('navigationCloseClicked');
5432
+ });
5433
+
5434
+ container.appendChild(closeBtn);
5435
+ }
5436
+ }
5437
+
5438
+ /**
5439
+ * Get navigation icon SVG based on type
5440
+ */
5441
+ private getNavigationIcon(iconType: string, customIcon?: string): string {
5442
+ if (customIcon) {
5443
+ // If it's a URL, create img tag, otherwise assume it's SVG
5444
+ if (customIcon.startsWith('http') || customIcon.includes('.')) {
5445
+ return `<img src="${customIcon}" alt="" style="width: 20px; height: 20px;" />`;
5446
+ }
5447
+ return customIcon;
5448
+ }
5449
+
5450
+ switch (iconType) {
5451
+ case 'arrow':
5452
+ return `<svg viewBox="0 0 24 24">
5453
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
5454
+ </svg>`;
5455
+
5456
+ case 'chevron':
5457
+ return `<svg viewBox="0 0 24 24">
5458
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
5459
+ </svg>`;
5460
+
5461
+ case 'x':
5462
+ return `<svg viewBox="0 0 24 24">
5463
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
5464
+ </svg>`;
5465
+
5466
+ case 'close':
5467
+ return `<svg viewBox="0 0 24 24">
5468
+ <path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z" fill="currentColor"/>
5469
+ </svg>`;
5470
+
5471
+ default:
5472
+ return `<svg viewBox="0 0 24 24">
5473
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
5474
+ </svg>`;
5475
+ }
5476
+ }
5477
+
5061
5478
  private createCustomControls(container: HTMLElement): void {
5062
5479
  // Add gradients
5063
5480
  const topGradient = document.createElement('div');
@@ -5068,23 +5485,34 @@ export class WebPlayer extends BasePlayer {
5068
5485
  controlsGradient.className = 'uvf-controls-gradient';
5069
5486
  container.appendChild(controlsGradient);
5070
5487
 
5071
- // Combined top bar: title on left, controls on right
5488
+ // Combined top bar: navigation buttons title controls
5072
5489
  const topBar = document.createElement('div');
5073
5490
  topBar.className = 'uvf-top-bar';
5074
5491
 
5075
- // Title bar (left side)
5492
+ // Left side container for navigation + title
5493
+ const leftSide = document.createElement('div');
5494
+ leftSide.className = 'uvf-left-side';
5495
+
5496
+ // Navigation buttons (back/close)
5497
+ const navigationControls = document.createElement('div');
5498
+ navigationControls.className = 'uvf-navigation-controls';
5499
+ this.createNavigationButtons(navigationControls);
5500
+ leftSide.appendChild(navigationControls);
5501
+
5502
+ // Title bar (after navigation buttons)
5076
5503
  const titleBar = document.createElement('div');
5077
5504
  titleBar.className = 'uvf-title-bar';
5078
5505
  titleBar.innerHTML = `
5079
5506
  <div class="uvf-title-content">
5080
- <img class="uvf-video-thumb" id="uvf-video-thumb" alt="thumbnail" style="display:none;" />
5081
5507
  <div class="uvf-title-text">
5082
5508
  <div class=\"uvf-video-title\" id=\"uvf-video-title\" style=\"display:none;\"></div>
5083
5509
  <div class=\"uvf-video-subtitle\" id=\"uvf-video-description\" style=\"display:none;\"></div>
5084
5510
  </div>
5085
5511
  </div>
5086
5512
  `;
5087
- topBar.appendChild(titleBar);
5513
+ leftSide.appendChild(titleBar);
5514
+
5515
+ topBar.appendChild(leftSide);
5088
5516
 
5089
5517
  // Top controls (right side - Cast and Share buttons)
5090
5518
  const topControls = document.createElement('div');
@@ -5931,11 +6359,13 @@ export class WebPlayer extends BasePlayer {
5931
6359
  break;
5932
6360
  case 'ArrowLeft':
5933
6361
  e.preventDefault();
6362
+ e.stopImmediatePropagation(); // Prevent duplicate handler triggers
5934
6363
  this.seek(Math.max(0, this.video!.currentTime - 10));
5935
6364
  shortcutText = '-10s';
5936
6365
  break;
5937
6366
  case 'ArrowRight':
5938
6367
  e.preventDefault();
6368
+ e.stopImmediatePropagation(); // Prevent duplicate handler triggers
5939
6369
  this.seek(Math.min(this.video!.duration, this.video!.currentTime + 10));
5940
6370
  shortcutText = '+10s';
5941
6371
  break;
@@ -7116,7 +7546,7 @@ export class WebPlayer extends BasePlayer {
7116
7546
  autoSkip: this.chapterConfig.userPreferences?.autoSkipIntro || false,
7117
7547
  onChapterChange: (chapter: Chapter | null) => {
7118
7548
  this.debugLog('Core chapter changed:', chapter?.title || 'none');
7119
- this.emit('chapterchange', chapter);
7549
+ this.emit('onChapterchange', chapter);
7120
7550
  },
7121
7551
  onSegmentEntered: (segment: ChapterSegment) => {
7122
7552
  this.debugLog('Core segment entered:', segment.title);
@@ -8399,6 +8829,168 @@ export class WebPlayer extends BasePlayer {
8399
8829
  }
8400
8830
  }
8401
8831
 
8832
+ /**
8833
+ * Check if text is truncated and needs tooltip
8834
+ */
8835
+ private isTextTruncated(element: HTMLElement): boolean {
8836
+ return element.scrollWidth > element.offsetWidth || element.scrollHeight > element.offsetHeight;
8837
+ }
8838
+
8839
+ /**
8840
+ * Show tooltip for truncated text
8841
+ */
8842
+ private showTextTooltip(element: HTMLElement, text: string): void {
8843
+ // Remove existing tooltip
8844
+ const existingTooltip = element.querySelector('.uvf-text-tooltip');
8845
+ if (existingTooltip) {
8846
+ existingTooltip.remove();
8847
+ }
8848
+
8849
+ // Create tooltip
8850
+ const tooltip = document.createElement('div');
8851
+ tooltip.className = 'uvf-text-tooltip';
8852
+ tooltip.textContent = text;
8853
+
8854
+ element.appendChild(tooltip);
8855
+
8856
+ // Show tooltip with delay
8857
+ setTimeout(() => {
8858
+ tooltip.classList.add('show');
8859
+ }, 100);
8860
+ }
8861
+
8862
+ /**
8863
+ * Hide tooltip
8864
+ */
8865
+ private hideTextTooltip(element: HTMLElement): void {
8866
+ const tooltip = element.querySelector('.uvf-text-tooltip');
8867
+ if (tooltip) {
8868
+ tooltip.classList.remove('show');
8869
+ setTimeout(() => {
8870
+ if (tooltip.parentElement) {
8871
+ tooltip.remove();
8872
+ }
8873
+ }, 300);
8874
+ }
8875
+ }
8876
+
8877
+ /**
8878
+ * Setup tooltip handlers for title and description
8879
+ */
8880
+ private setupTextTooltips(): void {
8881
+ const titleElement = document.getElementById('uvf-video-title');
8882
+ const descElement = document.getElementById('uvf-video-description');
8883
+
8884
+ if (titleElement) {
8885
+ titleElement.addEventListener('mouseenter', () => {
8886
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
8887
+ if (this.isTextTruncated(titleElement) && titleText) {
8888
+ this.showTextTooltip(titleElement, titleText);
8889
+ }
8890
+ });
8891
+
8892
+ titleElement.addEventListener('mouseleave', () => {
8893
+ this.hideTextTooltip(titleElement);
8894
+ });
8895
+
8896
+ // Touch support for mobile
8897
+ titleElement.addEventListener('touchstart', () => {
8898
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
8899
+ if (this.isTextTruncated(titleElement) && titleText) {
8900
+ this.showTextTooltip(titleElement, titleText);
8901
+ // Auto-hide after 3 seconds on touch
8902
+ setTimeout(() => {
8903
+ this.hideTextTooltip(titleElement);
8904
+ }, 3000);
8905
+ }
8906
+ });
8907
+ }
8908
+
8909
+ if (descElement) {
8910
+ descElement.addEventListener('mouseenter', () => {
8911
+ const descText = (this.source?.metadata?.description || '').toString().trim();
8912
+ if (this.isTextTruncated(descElement) && descText) {
8913
+ this.showTextTooltip(descElement, descText);
8914
+ }
8915
+ });
8916
+
8917
+ descElement.addEventListener('mouseleave', () => {
8918
+ this.hideTextTooltip(descElement);
8919
+ });
8920
+
8921
+ // Touch support for mobile
8922
+ descElement.addEventListener('touchstart', () => {
8923
+ const descText = (this.source?.metadata?.description || '').toString().trim();
8924
+ if (this.isTextTruncated(descElement) && descText) {
8925
+ this.showTextTooltip(descElement, descText);
8926
+ // Auto-hide after 3 seconds on touch
8927
+ setTimeout(() => {
8928
+ this.hideTextTooltip(descElement);
8929
+ }, 3000);
8930
+ }
8931
+ });
8932
+ }
8933
+ }
8934
+
8935
+ /**
8936
+ * Smart text truncation based on word count
8937
+ */
8938
+ private smartTruncateText(text: string, maxWords: number = 12): { truncated: string, needsTooltip: boolean } {
8939
+ const words = text.split(' ');
8940
+ if (words.length <= maxWords) {
8941
+ return { truncated: text, needsTooltip: false };
8942
+ }
8943
+
8944
+ const truncated = words.slice(0, maxWords).join(' ') + '...';
8945
+ return { truncated, needsTooltip: true };
8946
+ }
8947
+
8948
+ /**
8949
+ * Apply smart text display based on screen size and content length
8950
+ */
8951
+ private applySmartTextDisplay(titleEl: HTMLElement | null, descEl: HTMLElement | null, titleText: string, descText: string): void {
8952
+ const isDesktop = window.innerWidth >= 1024;
8953
+ const isMobile = window.innerWidth < 768;
8954
+
8955
+ if (titleEl && titleText) {
8956
+ const wordCount = titleText.split(' ').length;
8957
+
8958
+ if (isDesktop && wordCount > 8 && wordCount <= 15) {
8959
+ // Use multiline for moderately long titles on desktop
8960
+ titleEl.classList.add('multiline');
8961
+ titleEl.textContent = titleText;
8962
+ } else if (wordCount > 12) {
8963
+ // Smart truncation for very long titles
8964
+ const maxWords = isMobile ? 8 : isDesktop ? 12 : 10;
8965
+ const { truncated } = this.smartTruncateText(titleText, maxWords);
8966
+ titleEl.textContent = truncated;
8967
+ titleEl.classList.remove('multiline');
8968
+ } else {
8969
+ titleEl.textContent = titleText;
8970
+ titleEl.classList.remove('multiline');
8971
+ }
8972
+ }
8973
+
8974
+ if (descEl && descText) {
8975
+ const wordCount = descText.split(' ').length;
8976
+
8977
+ if (isDesktop && wordCount > 15 && wordCount <= 25) {
8978
+ // Use multiline for moderately long descriptions on desktop
8979
+ descEl.classList.add('multiline');
8980
+ descEl.textContent = descText;
8981
+ } else if (wordCount > 20) {
8982
+ // Smart truncation for very long descriptions
8983
+ const maxWords = isMobile ? 12 : isDesktop ? 18 : 15;
8984
+ const { truncated } = this.smartTruncateText(descText, maxWords);
8985
+ descEl.textContent = truncated;
8986
+ descEl.classList.remove('multiline');
8987
+ } else {
8988
+ descEl.textContent = descText;
8989
+ descEl.classList.remove('multiline');
8990
+ }
8991
+ }
8992
+ }
8993
+
8402
8994
  private updateMetadataUI(): void {
8403
8995
  try {
8404
8996
  const md = this.source?.metadata || ({} as any);
@@ -8411,34 +9003,33 @@ export class WebPlayer extends BasePlayer {
8411
9003
  const descText = (md.description || '').toString().trim();
8412
9004
  const thumbUrl = (md.thumbnailUrl || '').toString().trim();
8413
9005
 
8414
- // Title
9006
+ // Apply smart text display with truncation and multiline support
9007
+ this.applySmartTextDisplay(titleEl, descEl, titleText, descText);
9008
+
9009
+ // Show/hide elements
8415
9010
  if (titleEl) {
8416
- titleEl.textContent = titleText;
8417
9011
  titleEl.style.display = titleText ? 'block' : 'none';
8418
9012
  }
8419
-
8420
- // Description
8421
9013
  if (descEl) {
8422
- descEl.textContent = descText;
8423
9014
  descEl.style.display = descText ? 'block' : 'none';
8424
9015
  }
8425
9016
 
8426
- // Thumbnail
9017
+ // Thumbnail (removed from layout but keeping for compatibility)
8427
9018
  if (thumbEl) {
8428
- if (thumbUrl) {
8429
- thumbEl.src = thumbUrl;
8430
- thumbEl.style.display = 'block';
8431
- } else {
8432
- thumbEl.removeAttribute('src');
8433
- thumbEl.style.display = 'none';
8434
- }
9019
+ thumbEl.style.display = 'none'; // Always hidden in new layout
8435
9020
  }
8436
9021
 
8437
9022
  // Hide entire title bar if nothing to show
8438
- const hasAny = !!(titleText || descText || thumbUrl);
9023
+ const hasAny = !!(titleText || descText);
8439
9024
  if (titleBar) {
8440
9025
  titleBar.style.display = hasAny ? '' : 'none';
8441
9026
  }
9027
+
9028
+ // Setup tooltips for truncated text
9029
+ setTimeout(() => {
9030
+ this.setupTextTooltips();
9031
+ }, 100); // Small delay to ensure elements are rendered
9032
+
8442
9033
  } catch (_) { /* ignore */ }
8443
9034
  }
8444
9035