unified-video-framework 1.4.208 → 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 +557 -36
  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 +651 -41
  26. package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +0 -8
  27. package/packages/web/src/react/components/EPGOverlay.tsx +0 -8
@@ -1142,7 +1142,7 @@ export class WebPlayer extends BasePlayer {
1142
1142
  private setupClickToUnmute(): void {
1143
1143
  // Remove any existing listener first
1144
1144
  if (this.clickToUnmuteHandler) {
1145
- this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler);
1145
+ this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
1146
1146
  }
1147
1147
 
1148
1148
  this.clickToUnmuteHandler = (e: MouseEvent) => {
@@ -1159,6 +1159,10 @@ export class WebPlayer extends BasePlayer {
1159
1159
  return;
1160
1160
  }
1161
1161
 
1162
+ // Stop the event from triggering play/pause
1163
+ e.stopPropagation();
1164
+ e.preventDefault();
1165
+
1162
1166
  // Unmute the video
1163
1167
  this.video.muted = false;
1164
1168
  this.debugLog('🔊 Video unmuted by clicking on player');
@@ -1166,12 +1170,13 @@ export class WebPlayer extends BasePlayer {
1166
1170
 
1167
1171
  // Clean up the handler
1168
1172
  if (this.clickToUnmuteHandler) {
1169
- this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler);
1173
+ this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
1170
1174
  this.clickToUnmuteHandler = null;
1171
1175
  }
1172
1176
  };
1173
1177
 
1174
- this.playerWrapper?.addEventListener('click', this.clickToUnmuteHandler);
1178
+ // Use capture phase to intercept clicks before they reach the video element
1179
+ this.playerWrapper?.addEventListener('click', this.clickToUnmuteHandler, true);
1175
1180
  this.debugLog('👆 Click anywhere to unmute enabled');
1176
1181
  }
1177
1182
 
@@ -1189,7 +1194,7 @@ export class WebPlayer extends BasePlayer {
1189
1194
 
1190
1195
  // Remove click to unmute handler when button is hidden
1191
1196
  if (this.clickToUnmuteHandler) {
1192
- this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler);
1197
+ this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
1193
1198
  this.clickToUnmuteHandler = null;
1194
1199
  }
1195
1200
  }
@@ -3211,6 +3216,37 @@ export class WebPlayer extends BasePlayer {
3211
3216
  filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
3212
3217
  }
3213
3218
 
3219
+ /* PiP Button Specific Styling */
3220
+ #uvf-pip-btn {
3221
+ background: var(--uvf-button-bg);
3222
+ border: 1px solid var(--uvf-button-border);
3223
+ position: relative;
3224
+ z-index: 10;
3225
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3226
+ }
3227
+
3228
+ #uvf-pip-btn:hover {
3229
+ transform: scale(1.08);
3230
+ box-shadow: 0 4px 12px var(--uvf-button-shadow);
3231
+ }
3232
+
3233
+ #uvf-pip-btn:active {
3234
+ transform: scale(0.95);
3235
+ transition: all 0.1s ease;
3236
+ }
3237
+
3238
+ #uvf-pip-btn svg {
3239
+ opacity: 0.9;
3240
+ transition: all 0.3s ease;
3241
+ filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3));
3242
+ }
3243
+
3244
+ #uvf-pip-btn:hover svg {
3245
+ opacity: 1;
3246
+ transform: scale(1.05);
3247
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
3248
+ }
3249
+
3214
3250
  /* Fullscreen Button Specific Styling */
3215
3251
  #uvf-fullscreen-btn {
3216
3252
  background: var(--uvf-button-bg);
@@ -3714,7 +3750,7 @@ export class WebPlayer extends BasePlayer {
3714
3750
  margin-left: 10px;
3715
3751
  }
3716
3752
 
3717
- /* Top Bar Container - Contains Title (left) and Controls (right) */
3753
+ /* Top Bar Container - Contains Navigation + Title (left) and Controls (right) */
3718
3754
  .uvf-top-bar {
3719
3755
  position: absolute;
3720
3756
  top: 0;
@@ -3731,16 +3767,97 @@ export class WebPlayer extends BasePlayer {
3731
3767
  transition: all 0.3s ease;
3732
3768
  }
3733
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
+
3734
3851
  .uvf-player-wrapper:hover .uvf-top-bar,
3735
3852
  .uvf-player-wrapper.controls-visible .uvf-top-bar {
3736
3853
  opacity: 1;
3737
3854
  transform: translateY(0);
3738
3855
  }
3739
3856
 
3740
- /* Title Bar - Left side of top bar */
3857
+ /* Title Bar - After navigation buttons */
3741
3858
  .uvf-title-bar {
3742
- flex: 0 1 auto;
3743
- max-width: 60%;
3859
+ flex: 1;
3860
+ min-width: 0; /* Allow shrinking */
3744
3861
  }
3745
3862
 
3746
3863
  /* Top Controls - Right side of top bar */
@@ -3755,33 +3872,112 @@ export class WebPlayer extends BasePlayer {
3755
3872
  .uvf-title-content {
3756
3873
  display: flex;
3757
3874
  align-items: center;
3758
- gap: 12px;
3875
+ width: 100%;
3876
+ min-width: 0; /* Allow shrinking */
3759
3877
  }
3760
- .uvf-video-thumb {
3761
- width: 56px;
3762
- height: 56px;
3763
- border-radius: 8px;
3764
- object-fit: cover;
3765
- box-shadow: 0 4px 14px rgba(0,0,0,0.5);
3766
- border: 1px solid rgba(255,255,255,0.25);
3767
- 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;
3768
3884
  }
3769
- .uvf-title-text { display: flex; flex-direction: column; }
3885
+
3770
3886
  .uvf-video-title {
3771
3887
  color: var(--uvf-text-primary);
3772
- font-size: 18px;
3888
+ font-size: clamp(14px, 2.5vw, 18px); /* Responsive font size */
3773
3889
  font-weight: 600;
3774
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);
3775
3903
  }
3776
3904
 
3777
3905
  .uvf-video-subtitle {
3778
3906
  color: var(--uvf-text-secondary);
3779
- font-size: 13px;
3780
- margin-top: 4px;
3781
- max-width: min(70vw, 900px);
3907
+ font-size: clamp(11px, 1.8vw, 13px); /* Responsive font size */
3908
+ margin-top: 2px;
3782
3909
  overflow: hidden;
3783
3910
  text-overflow: ellipsis;
3784
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;
3785
3981
  }
3786
3982
 
3787
3983
  /* Above seekbar section with time and branding */
@@ -3860,7 +4056,21 @@ export class WebPlayer extends BasePlayer {
3860
4056
  }
3861
4057
  }
3862
4058
 
4059
+ /* Ultra small screens */
3863
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
+
3864
4074
  .uvf-above-seekbar-section {
3865
4075
  margin-bottom: 5px;
3866
4076
  }
@@ -4248,6 +4458,47 @@ export class WebPlayer extends BasePlayer {
4248
4458
  }
4249
4459
 
4250
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
+
4251
4502
  /* Mobile portrait - hide skip buttons, ensure top bar visible */
4252
4503
  @media screen and (max-width: 767px) and (orientation: portrait) {
4253
4504
  #uvf-skip-back,
@@ -4415,6 +4666,31 @@ export class WebPlayer extends BasePlayer {
4415
4666
 
4416
4667
  /* Tablet devices - Enhanced UX with desktop features */
4417
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
+
4418
4694
  .uvf-controls-bar {
4419
4695
  padding: 18px 16px;
4420
4696
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 70%, var(--uvf-overlay-transparent) 100%);
@@ -4947,6 +5223,40 @@ export class WebPlayer extends BasePlayer {
4947
5223
  }
4948
5224
  }
4949
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
+
4950
5260
  /* Paywall Desktop */
4951
5261
  @media screen and (min-width: 1024px) {
4952
5262
  .uvf-paywall-modal {
@@ -5022,6 +5332,132 @@ export class WebPlayer extends BasePlayer {
5022
5332
  this.debugLog('Framework branding added');
5023
5333
  }
5024
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
+
5025
5461
  private createCustomControls(container: HTMLElement): void {
5026
5462
  // Add gradients
5027
5463
  const topGradient = document.createElement('div');
@@ -5032,23 +5468,34 @@ export class WebPlayer extends BasePlayer {
5032
5468
  controlsGradient.className = 'uvf-controls-gradient';
5033
5469
  container.appendChild(controlsGradient);
5034
5470
 
5035
- // Combined top bar: title on left, controls on right
5471
+ // Combined top bar: navigation buttons title controls
5036
5472
  const topBar = document.createElement('div');
5037
5473
  topBar.className = 'uvf-top-bar';
5038
5474
 
5039
- // 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)
5040
5486
  const titleBar = document.createElement('div');
5041
5487
  titleBar.className = 'uvf-title-bar';
5042
5488
  titleBar.innerHTML = `
5043
5489
  <div class="uvf-title-content">
5044
- <img class="uvf-video-thumb" id="uvf-video-thumb" alt="thumbnail" style="display:none;" />
5045
5490
  <div class="uvf-title-text">
5046
5491
  <div class=\"uvf-video-title\" id=\"uvf-video-title\" style=\"display:none;\"></div>
5047
5492
  <div class=\"uvf-video-subtitle\" id=\"uvf-video-description\" style=\"display:none;\"></div>
5048
5493
  </div>
5049
5494
  </div>
5050
5495
  `;
5051
- topBar.appendChild(titleBar);
5496
+ leftSide.appendChild(titleBar);
5497
+
5498
+ topBar.appendChild(leftSide);
5052
5499
 
5053
5500
  // Top controls (right side - Cast and Share buttons)
5054
5501
  const topControls = document.createElement('div');
@@ -5895,11 +6342,13 @@ export class WebPlayer extends BasePlayer {
5895
6342
  break;
5896
6343
  case 'ArrowLeft':
5897
6344
  e.preventDefault();
6345
+ e.stopImmediatePropagation(); // Prevent duplicate handler triggers
5898
6346
  this.seek(Math.max(0, this.video!.currentTime - 10));
5899
6347
  shortcutText = '-10s';
5900
6348
  break;
5901
6349
  case 'ArrowRight':
5902
6350
  e.preventDefault();
6351
+ e.stopImmediatePropagation(); // Prevent duplicate handler triggers
5903
6352
  this.seek(Math.min(this.video!.duration, this.video!.currentTime + 10));
5904
6353
  shortcutText = '+10s';
5905
6354
  break;
@@ -7080,7 +7529,7 @@ export class WebPlayer extends BasePlayer {
7080
7529
  autoSkip: this.chapterConfig.userPreferences?.autoSkipIntro || false,
7081
7530
  onChapterChange: (chapter: Chapter | null) => {
7082
7531
  this.debugLog('Core chapter changed:', chapter?.title || 'none');
7083
- this.emit('chapterchange', chapter);
7532
+ this.emit('onChapterchange', chapter);
7084
7533
  },
7085
7534
  onSegmentEntered: (segment: ChapterSegment) => {
7086
7535
  this.debugLog('Core segment entered:', segment.title);
@@ -8363,6 +8812,168 @@ export class WebPlayer extends BasePlayer {
8363
8812
  }
8364
8813
  }
8365
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
+
8366
8977
  private updateMetadataUI(): void {
8367
8978
  try {
8368
8979
  const md = this.source?.metadata || ({} as any);
@@ -8375,34 +8986,33 @@ export class WebPlayer extends BasePlayer {
8375
8986
  const descText = (md.description || '').toString().trim();
8376
8987
  const thumbUrl = (md.thumbnailUrl || '').toString().trim();
8377
8988
 
8378
- // Title
8989
+ // Apply smart text display with truncation and multiline support
8990
+ this.applySmartTextDisplay(titleEl, descEl, titleText, descText);
8991
+
8992
+ // Show/hide elements
8379
8993
  if (titleEl) {
8380
- titleEl.textContent = titleText;
8381
8994
  titleEl.style.display = titleText ? 'block' : 'none';
8382
8995
  }
8383
-
8384
- // Description
8385
8996
  if (descEl) {
8386
- descEl.textContent = descText;
8387
8997
  descEl.style.display = descText ? 'block' : 'none';
8388
8998
  }
8389
8999
 
8390
- // Thumbnail
9000
+ // Thumbnail (removed from layout but keeping for compatibility)
8391
9001
  if (thumbEl) {
8392
- if (thumbUrl) {
8393
- thumbEl.src = thumbUrl;
8394
- thumbEl.style.display = 'block';
8395
- } else {
8396
- thumbEl.removeAttribute('src');
8397
- thumbEl.style.display = 'none';
8398
- }
9002
+ thumbEl.style.display = 'none'; // Always hidden in new layout
8399
9003
  }
8400
9004
 
8401
9005
  // Hide entire title bar if nothing to show
8402
- const hasAny = !!(titleText || descText || thumbUrl);
9006
+ const hasAny = !!(titleText || descText);
8403
9007
  if (titleBar) {
8404
9008
  titleBar.style.display = hasAny ? '' : 'none';
8405
9009
  }
9010
+
9011
+ // Setup tooltips for truncated text
9012
+ setTimeout(() => {
9013
+ this.setupTextTooltips();
9014
+ }, 100); // Small delay to ensure elements are rendered
9015
+
8406
9016
  } catch (_) { /* ignore */ }
8407
9017
  }
8408
9018