unified-video-framework 1.4.209 → 1.4.210

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 (27) 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 +520 -32
  18. package/packages/web/dist/WebPlayer.js.map +1 -1
  19. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.d.ts.map +1 -1
  20. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.js +0 -8
  21. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.js.map +1 -1
  22. package/packages/web/dist/react/components/EPGOverlay.d.ts.map +1 -1
  23. package/packages/web/dist/react/components/EPGOverlay.js +0 -8
  24. package/packages/web/dist/react/components/EPGOverlay.js.map +1 -1
  25. package/packages/web/src/WebPlayer.ts +611 -37
  26. package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +0 -8
  27. 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,6 +4458,47 @@ export class WebPlayer extends BasePlayer {
4284
4458
  }
4285
4459
 
4286
4460
 
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
+
4287
4502
  /* Mobile portrait - hide skip buttons, ensure top bar visible */
4288
4503
  @media screen and (max-width: 767px) and (orientation: portrait) {
4289
4504
  #uvf-skip-back,
@@ -4451,6 +4666,31 @@ export class WebPlayer extends BasePlayer {
4451
4666
 
4452
4667
  /* Tablet devices - Enhanced UX with desktop features */
4453
4668
  @media screen and (min-width: 768px) and (max-width: 1023px) {
4669
+ /* Tablet navigation and title adjustments */
4670
+ .uvf-nav-btn {
4671
+ width: 38px;
4672
+ height: 38px;
4673
+ min-width: 38px;
4674
+ min-height: 38px;
4675
+ }
4676
+
4677
+ .uvf-nav-btn svg {
4678
+ width: 19px;
4679
+ height: 19px;
4680
+ }
4681
+
4682
+ .uvf-left-side {
4683
+ max-width: 70%;
4684
+ }
4685
+
4686
+ .uvf-video-title {
4687
+ font-size: clamp(15px, 2.2vw, 17px) !important;
4688
+ }
4689
+
4690
+ .uvf-video-subtitle {
4691
+ font-size: clamp(12px, 1.8vw, 13px) !important;
4692
+ }
4693
+
4454
4694
  .uvf-controls-bar {
4455
4695
  padding: 18px 16px;
4456
4696
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 70%, var(--uvf-overlay-transparent) 100%);
@@ -4983,6 +5223,40 @@ export class WebPlayer extends BasePlayer {
4983
5223
  }
4984
5224
  }
4985
5225
 
5226
+ /* Desktop styles for title and navigation */
5227
+ @media screen and (min-width: 1024px) {
5228
+ .uvf-left-side {
5229
+ max-width: 65%; /* More space for title on desktop */
5230
+ }
5231
+
5232
+ .uvf-video-title {
5233
+ font-size: clamp(16px, 1.8vw, 20px) !important;
5234
+ font-weight: 700; /* Bolder on desktop */
5235
+ }
5236
+
5237
+ .uvf-video-subtitle {
5238
+ font-size: clamp(13px, 1.4vw, 15px) !important;
5239
+ margin-top: 3px;
5240
+ }
5241
+
5242
+ /* Allow hover effects on desktop */
5243
+ .uvf-title-bar:hover .uvf-video-title {
5244
+ color: var(--uvf-accent-1, #ff0000);
5245
+ transition: color 0.3s ease;
5246
+ }
5247
+ }
5248
+
5249
+ /* Ultra-wide screens */
5250
+ @media screen and (min-width: 1440px) {
5251
+ .uvf-video-title {
5252
+ font-size: clamp(18px, 1.6vw, 22px) !important;
5253
+ }
5254
+
5255
+ .uvf-video-subtitle {
5256
+ font-size: clamp(14px, 1.2vw, 16px) !important;
5257
+ }
5258
+ }
5259
+
4986
5260
  /* Paywall Desktop */
4987
5261
  @media screen and (min-width: 1024px) {
4988
5262
  .uvf-paywall-modal {
@@ -5058,6 +5332,132 @@ export class WebPlayer extends BasePlayer {
5058
5332
  this.debugLog('Framework branding added');
5059
5333
  }
5060
5334
 
5335
+ /**
5336
+ * Create navigation buttons (back/close) based on configuration
5337
+ */
5338
+ private createNavigationButtons(container: HTMLElement): void {
5339
+ const navigationConfig = (this.config as any).navigation;
5340
+ if (!navigationConfig) return;
5341
+
5342
+ const { backButton, closeButton } = navigationConfig;
5343
+
5344
+ // Back button
5345
+ if (backButton?.enabled) {
5346
+ const backBtn = document.createElement('button');
5347
+ backBtn.className = 'uvf-control-btn uvf-nav-btn';
5348
+ backBtn.id = 'uvf-back-btn';
5349
+ backBtn.title = backButton.title || 'Back';
5350
+ backBtn.setAttribute('aria-label', backButton.ariaLabel || 'Go back');
5351
+
5352
+ // Get icon based on config
5353
+ const backIcon = this.getNavigationIcon(backButton.icon || 'arrow', backButton.customIcon);
5354
+ backBtn.innerHTML = backIcon;
5355
+
5356
+ // Add click handler
5357
+ backBtn.addEventListener('click', async (e) => {
5358
+ e.preventDefault();
5359
+ e.stopPropagation();
5360
+
5361
+ if (backButton.onClick) {
5362
+ await backButton.onClick();
5363
+ } else if (backButton.href) {
5364
+ if (backButton.replace) {
5365
+ window.history.replaceState(null, '', backButton.href);
5366
+ } else {
5367
+ window.location.href = backButton.href;
5368
+ }
5369
+ } else {
5370
+ // Default: go back in history
5371
+ window.history.back();
5372
+ }
5373
+
5374
+ this.emit('navigationBackClicked');
5375
+ });
5376
+
5377
+ container.appendChild(backBtn);
5378
+ }
5379
+
5380
+ // Close button
5381
+ if (closeButton?.enabled) {
5382
+ const closeBtn = document.createElement('button');
5383
+ closeBtn.className = 'uvf-control-btn uvf-nav-btn';
5384
+ closeBtn.id = 'uvf-close-btn';
5385
+ closeBtn.title = closeButton.title || 'Close';
5386
+ closeBtn.setAttribute('aria-label', closeButton.ariaLabel || 'Close player');
5387
+
5388
+ // Get icon based on config
5389
+ const closeIcon = this.getNavigationIcon(closeButton.icon || 'x', closeButton.customIcon);
5390
+ closeBtn.innerHTML = closeIcon;
5391
+
5392
+ // Add click handler
5393
+ closeBtn.addEventListener('click', async (e) => {
5394
+ e.preventDefault();
5395
+ e.stopPropagation();
5396
+
5397
+ if (closeButton.onClick) {
5398
+ await closeButton.onClick();
5399
+ } else {
5400
+ // Default behaviors
5401
+ if (closeButton.exitFullscreen && this.isFullscreen()) {
5402
+ await this.exitFullscreen();
5403
+ }
5404
+
5405
+ if (closeButton.closeModal) {
5406
+ // Hide player or remove from DOM
5407
+ const playerWrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
5408
+ if (playerWrapper) {
5409
+ playerWrapper.style.display = 'none';
5410
+ }
5411
+ }
5412
+ }
5413
+
5414
+ this.emit('navigationCloseClicked');
5415
+ });
5416
+
5417
+ container.appendChild(closeBtn);
5418
+ }
5419
+ }
5420
+
5421
+ /**
5422
+ * Get navigation icon SVG based on type
5423
+ */
5424
+ private getNavigationIcon(iconType: string, customIcon?: string): string {
5425
+ if (customIcon) {
5426
+ // If it's a URL, create img tag, otherwise assume it's SVG
5427
+ if (customIcon.startsWith('http') || customIcon.includes('.')) {
5428
+ return `<img src="${customIcon}" alt="" style="width: 20px; height: 20px;" />`;
5429
+ }
5430
+ return customIcon;
5431
+ }
5432
+
5433
+ switch (iconType) {
5434
+ case 'arrow':
5435
+ return `<svg viewBox="0 0 24 24">
5436
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
5437
+ </svg>`;
5438
+
5439
+ case 'chevron':
5440
+ return `<svg viewBox="0 0 24 24">
5441
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
5442
+ </svg>`;
5443
+
5444
+ case 'x':
5445
+ return `<svg viewBox="0 0 24 24">
5446
+ <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"/>
5447
+ </svg>`;
5448
+
5449
+ case 'close':
5450
+ return `<svg viewBox="0 0 24 24">
5451
+ <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"/>
5452
+ </svg>`;
5453
+
5454
+ default:
5455
+ return `<svg viewBox="0 0 24 24">
5456
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
5457
+ </svg>`;
5458
+ }
5459
+ }
5460
+
5061
5461
  private createCustomControls(container: HTMLElement): void {
5062
5462
  // Add gradients
5063
5463
  const topGradient = document.createElement('div');
@@ -5068,23 +5468,34 @@ export class WebPlayer extends BasePlayer {
5068
5468
  controlsGradient.className = 'uvf-controls-gradient';
5069
5469
  container.appendChild(controlsGradient);
5070
5470
 
5071
- // Combined top bar: title on left, controls on right
5471
+ // Combined top bar: navigation buttons title controls
5072
5472
  const topBar = document.createElement('div');
5073
5473
  topBar.className = 'uvf-top-bar';
5074
5474
 
5075
- // Title bar (left side)
5475
+ // Left side container for navigation + title
5476
+ const leftSide = document.createElement('div');
5477
+ leftSide.className = 'uvf-left-side';
5478
+
5479
+ // Navigation buttons (back/close)
5480
+ const navigationControls = document.createElement('div');
5481
+ navigationControls.className = 'uvf-navigation-controls';
5482
+ this.createNavigationButtons(navigationControls);
5483
+ leftSide.appendChild(navigationControls);
5484
+
5485
+ // Title bar (after navigation buttons)
5076
5486
  const titleBar = document.createElement('div');
5077
5487
  titleBar.className = 'uvf-title-bar';
5078
5488
  titleBar.innerHTML = `
5079
5489
  <div class="uvf-title-content">
5080
- <img class="uvf-video-thumb" id="uvf-video-thumb" alt="thumbnail" style="display:none;" />
5081
5490
  <div class="uvf-title-text">
5082
5491
  <div class=\"uvf-video-title\" id=\"uvf-video-title\" style=\"display:none;\"></div>
5083
5492
  <div class=\"uvf-video-subtitle\" id=\"uvf-video-description\" style=\"display:none;\"></div>
5084
5493
  </div>
5085
5494
  </div>
5086
5495
  `;
5087
- topBar.appendChild(titleBar);
5496
+ leftSide.appendChild(titleBar);
5497
+
5498
+ topBar.appendChild(leftSide);
5088
5499
 
5089
5500
  // Top controls (right side - Cast and Share buttons)
5090
5501
  const topControls = document.createElement('div');
@@ -5931,11 +6342,13 @@ export class WebPlayer extends BasePlayer {
5931
6342
  break;
5932
6343
  case 'ArrowLeft':
5933
6344
  e.preventDefault();
6345
+ e.stopImmediatePropagation(); // Prevent duplicate handler triggers
5934
6346
  this.seek(Math.max(0, this.video!.currentTime - 10));
5935
6347
  shortcutText = '-10s';
5936
6348
  break;
5937
6349
  case 'ArrowRight':
5938
6350
  e.preventDefault();
6351
+ e.stopImmediatePropagation(); // Prevent duplicate handler triggers
5939
6352
  this.seek(Math.min(this.video!.duration, this.video!.currentTime + 10));
5940
6353
  shortcutText = '+10s';
5941
6354
  break;
@@ -7116,7 +7529,7 @@ export class WebPlayer extends BasePlayer {
7116
7529
  autoSkip: this.chapterConfig.userPreferences?.autoSkipIntro || false,
7117
7530
  onChapterChange: (chapter: Chapter | null) => {
7118
7531
  this.debugLog('Core chapter changed:', chapter?.title || 'none');
7119
- this.emit('chapterchange', chapter);
7532
+ this.emit('onChapterchange', chapter);
7120
7533
  },
7121
7534
  onSegmentEntered: (segment: ChapterSegment) => {
7122
7535
  this.debugLog('Core segment entered:', segment.title);
@@ -8399,6 +8812,168 @@ export class WebPlayer extends BasePlayer {
8399
8812
  }
8400
8813
  }
8401
8814
 
8815
+ /**
8816
+ * Check if text is truncated and needs tooltip
8817
+ */
8818
+ private isTextTruncated(element: HTMLElement): boolean {
8819
+ return element.scrollWidth > element.offsetWidth || element.scrollHeight > element.offsetHeight;
8820
+ }
8821
+
8822
+ /**
8823
+ * Show tooltip for truncated text
8824
+ */
8825
+ private showTextTooltip(element: HTMLElement, text: string): void {
8826
+ // Remove existing tooltip
8827
+ const existingTooltip = element.querySelector('.uvf-text-tooltip');
8828
+ if (existingTooltip) {
8829
+ existingTooltip.remove();
8830
+ }
8831
+
8832
+ // Create tooltip
8833
+ const tooltip = document.createElement('div');
8834
+ tooltip.className = 'uvf-text-tooltip';
8835
+ tooltip.textContent = text;
8836
+
8837
+ element.appendChild(tooltip);
8838
+
8839
+ // Show tooltip with delay
8840
+ setTimeout(() => {
8841
+ tooltip.classList.add('show');
8842
+ }, 100);
8843
+ }
8844
+
8845
+ /**
8846
+ * Hide tooltip
8847
+ */
8848
+ private hideTextTooltip(element: HTMLElement): void {
8849
+ const tooltip = element.querySelector('.uvf-text-tooltip');
8850
+ if (tooltip) {
8851
+ tooltip.classList.remove('show');
8852
+ setTimeout(() => {
8853
+ if (tooltip.parentElement) {
8854
+ tooltip.remove();
8855
+ }
8856
+ }, 300);
8857
+ }
8858
+ }
8859
+
8860
+ /**
8861
+ * Setup tooltip handlers for title and description
8862
+ */
8863
+ private setupTextTooltips(): void {
8864
+ const titleElement = document.getElementById('uvf-video-title');
8865
+ const descElement = document.getElementById('uvf-video-description');
8866
+
8867
+ if (titleElement) {
8868
+ titleElement.addEventListener('mouseenter', () => {
8869
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
8870
+ if (this.isTextTruncated(titleElement) && titleText) {
8871
+ this.showTextTooltip(titleElement, titleText);
8872
+ }
8873
+ });
8874
+
8875
+ titleElement.addEventListener('mouseleave', () => {
8876
+ this.hideTextTooltip(titleElement);
8877
+ });
8878
+
8879
+ // Touch support for mobile
8880
+ titleElement.addEventListener('touchstart', () => {
8881
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
8882
+ if (this.isTextTruncated(titleElement) && titleText) {
8883
+ this.showTextTooltip(titleElement, titleText);
8884
+ // Auto-hide after 3 seconds on touch
8885
+ setTimeout(() => {
8886
+ this.hideTextTooltip(titleElement);
8887
+ }, 3000);
8888
+ }
8889
+ });
8890
+ }
8891
+
8892
+ if (descElement) {
8893
+ descElement.addEventListener('mouseenter', () => {
8894
+ const descText = (this.source?.metadata?.description || '').toString().trim();
8895
+ if (this.isTextTruncated(descElement) && descText) {
8896
+ this.showTextTooltip(descElement, descText);
8897
+ }
8898
+ });
8899
+
8900
+ descElement.addEventListener('mouseleave', () => {
8901
+ this.hideTextTooltip(descElement);
8902
+ });
8903
+
8904
+ // Touch support for mobile
8905
+ descElement.addEventListener('touchstart', () => {
8906
+ const descText = (this.source?.metadata?.description || '').toString().trim();
8907
+ if (this.isTextTruncated(descElement) && descText) {
8908
+ this.showTextTooltip(descElement, descText);
8909
+ // Auto-hide after 3 seconds on touch
8910
+ setTimeout(() => {
8911
+ this.hideTextTooltip(descElement);
8912
+ }, 3000);
8913
+ }
8914
+ });
8915
+ }
8916
+ }
8917
+
8918
+ /**
8919
+ * Smart text truncation based on word count
8920
+ */
8921
+ private smartTruncateText(text: string, maxWords: number = 12): { truncated: string, needsTooltip: boolean } {
8922
+ const words = text.split(' ');
8923
+ if (words.length <= maxWords) {
8924
+ return { truncated: text, needsTooltip: false };
8925
+ }
8926
+
8927
+ const truncated = words.slice(0, maxWords).join(' ') + '...';
8928
+ return { truncated, needsTooltip: true };
8929
+ }
8930
+
8931
+ /**
8932
+ * Apply smart text display based on screen size and content length
8933
+ */
8934
+ private applySmartTextDisplay(titleEl: HTMLElement | null, descEl: HTMLElement | null, titleText: string, descText: string): void {
8935
+ const isDesktop = window.innerWidth >= 1024;
8936
+ const isMobile = window.innerWidth < 768;
8937
+
8938
+ if (titleEl && titleText) {
8939
+ const wordCount = titleText.split(' ').length;
8940
+
8941
+ if (isDesktop && wordCount > 8 && wordCount <= 15) {
8942
+ // Use multiline for moderately long titles on desktop
8943
+ titleEl.classList.add('multiline');
8944
+ titleEl.textContent = titleText;
8945
+ } else if (wordCount > 12) {
8946
+ // Smart truncation for very long titles
8947
+ const maxWords = isMobile ? 8 : isDesktop ? 12 : 10;
8948
+ const { truncated } = this.smartTruncateText(titleText, maxWords);
8949
+ titleEl.textContent = truncated;
8950
+ titleEl.classList.remove('multiline');
8951
+ } else {
8952
+ titleEl.textContent = titleText;
8953
+ titleEl.classList.remove('multiline');
8954
+ }
8955
+ }
8956
+
8957
+ if (descEl && descText) {
8958
+ const wordCount = descText.split(' ').length;
8959
+
8960
+ if (isDesktop && wordCount > 15 && wordCount <= 25) {
8961
+ // Use multiline for moderately long descriptions on desktop
8962
+ descEl.classList.add('multiline');
8963
+ descEl.textContent = descText;
8964
+ } else if (wordCount > 20) {
8965
+ // Smart truncation for very long descriptions
8966
+ const maxWords = isMobile ? 12 : isDesktop ? 18 : 15;
8967
+ const { truncated } = this.smartTruncateText(descText, maxWords);
8968
+ descEl.textContent = truncated;
8969
+ descEl.classList.remove('multiline');
8970
+ } else {
8971
+ descEl.textContent = descText;
8972
+ descEl.classList.remove('multiline');
8973
+ }
8974
+ }
8975
+ }
8976
+
8402
8977
  private updateMetadataUI(): void {
8403
8978
  try {
8404
8979
  const md = this.source?.metadata || ({} as any);
@@ -8411,34 +8986,33 @@ export class WebPlayer extends BasePlayer {
8411
8986
  const descText = (md.description || '').toString().trim();
8412
8987
  const thumbUrl = (md.thumbnailUrl || '').toString().trim();
8413
8988
 
8414
- // Title
8989
+ // Apply smart text display with truncation and multiline support
8990
+ this.applySmartTextDisplay(titleEl, descEl, titleText, descText);
8991
+
8992
+ // Show/hide elements
8415
8993
  if (titleEl) {
8416
- titleEl.textContent = titleText;
8417
8994
  titleEl.style.display = titleText ? 'block' : 'none';
8418
8995
  }
8419
-
8420
- // Description
8421
8996
  if (descEl) {
8422
- descEl.textContent = descText;
8423
8997
  descEl.style.display = descText ? 'block' : 'none';
8424
8998
  }
8425
8999
 
8426
- // Thumbnail
9000
+ // Thumbnail (removed from layout but keeping for compatibility)
8427
9001
  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
- }
9002
+ thumbEl.style.display = 'none'; // Always hidden in new layout
8435
9003
  }
8436
9004
 
8437
9005
  // Hide entire title bar if nothing to show
8438
- const hasAny = !!(titleText || descText || thumbUrl);
9006
+ const hasAny = !!(titleText || descText);
8439
9007
  if (titleBar) {
8440
9008
  titleBar.style.display = hasAny ? '' : 'none';
8441
9009
  }
9010
+
9011
+ // Setup tooltips for truncated text
9012
+ setTimeout(() => {
9013
+ this.setupTextTooltips();
9014
+ }, 100); // Small delay to ensure elements are rendered
9015
+
8442
9016
  } catch (_) { /* ignore */ }
8443
9017
  }
8444
9018