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
@@ -3253,7 +3253,7 @@ export class WebPlayer extends BasePlayer {
3253
3253
  margin-left: 10px;
3254
3254
  }
3255
3255
 
3256
- /* Top Bar Container - Contains Title (left) and Controls (right) */
3256
+ /* Top Bar Container - Contains Navigation + Title (left) and Controls (right) */
3257
3257
  .uvf-top-bar {
3258
3258
  position: absolute;
3259
3259
  top: 0;
@@ -3270,16 +3270,97 @@ export class WebPlayer extends BasePlayer {
3270
3270
  transition: all 0.3s ease;
3271
3271
  }
3272
3272
 
3273
+ /* Left side container for navigation + title */
3274
+ .uvf-left-side {
3275
+ display: flex;
3276
+ align-items: center;
3277
+ gap: 12px;
3278
+ flex: 1;
3279
+ max-width: 70%;
3280
+ }
3281
+
3282
+ /* Navigation controls container */
3283
+ .uvf-navigation-controls {
3284
+ display: flex;
3285
+ align-items: center;
3286
+ gap: 8px;
3287
+ flex-shrink: 0;
3288
+ }
3289
+
3290
+ /* Navigation button styles */
3291
+ .uvf-nav-btn {
3292
+ width: 40px;
3293
+ height: 40px;
3294
+ min-width: 40px;
3295
+ min-height: 40px;
3296
+ border-radius: 50%;
3297
+ background: rgba(0, 0, 0, 0.6);
3298
+ border: 1px solid rgba(255, 255, 255, 0.2);
3299
+ color: white;
3300
+ cursor: pointer;
3301
+ display: flex;
3302
+ align-items: center;
3303
+ justify-content: center;
3304
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3305
+ backdrop-filter: blur(8px);
3306
+ position: relative;
3307
+ overflow: hidden;
3308
+ }
3309
+
3310
+ .uvf-nav-btn:hover {
3311
+ background: rgba(255, 255, 255, 0.15);
3312
+ border-color: rgba(255, 255, 255, 0.4);
3313
+ transform: scale(1.05);
3314
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
3315
+ }
3316
+
3317
+ .uvf-nav-btn:active {
3318
+ transform: scale(0.95);
3319
+ }
3320
+
3321
+ .uvf-nav-btn svg {
3322
+ width: 20px;
3323
+ height: 20px;
3324
+ fill: currentColor;
3325
+ transition: all 0.2s ease;
3326
+ }
3327
+
3328
+ .uvf-nav-btn:hover svg {
3329
+ transform: scale(1.1);
3330
+ }
3331
+
3332
+ /* Back button specific styles */
3333
+ #uvf-back-btn {
3334
+ background: rgba(0, 0, 0, 0.7);
3335
+ }
3336
+
3337
+ #uvf-back-btn:hover {
3338
+ background: rgba(255, 255, 255, 0.2);
3339
+ border-color: var(--uvf-accent-1, #ff0000);
3340
+ }
3341
+
3342
+ /* Close button specific styles */
3343
+ #uvf-close-btn {
3344
+ background: rgba(220, 53, 69, 0.8);
3345
+ border-color: rgba(220, 53, 69, 0.6);
3346
+ }
3347
+
3348
+ #uvf-close-btn:hover {
3349
+ background: rgba(220, 53, 69, 1);
3350
+ border-color: rgba(220, 53, 69, 1);
3351
+ box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
3352
+ }
3353
+
3273
3354
  .uvf-player-wrapper:hover .uvf-top-bar,
3274
3355
  .uvf-player-wrapper.controls-visible .uvf-top-bar {
3275
3356
  opacity: 1;
3276
3357
  transform: translateY(0);
3277
3358
  }
3278
3359
 
3279
- /* Title Bar - Left side of top bar */
3360
+ /* Title Bar - After navigation buttons */
3280
3361
  .uvf-title-bar {
3281
- flex: 0 1 auto;
3282
- max-width: 60%;
3362
+ flex: 1;
3363
+ min-width: 0; /* Allow shrinking */
3283
3364
  }
3284
3365
 
3285
3366
  /* Top Controls - Right side of top bar */
@@ -3294,33 +3375,112 @@ export class WebPlayer extends BasePlayer {
3294
3375
  .uvf-title-content {
3295
3376
  display: flex;
3296
3377
  align-items: center;
3297
- gap: 12px;
3378
+ width: 100%;
3379
+ min-width: 0; /* Allow shrinking */
3298
3380
  }
3299
- .uvf-video-thumb {
3300
- width: 56px;
3301
- height: 56px;
3302
- border-radius: 8px;
3303
- object-fit: cover;
3304
- box-shadow: 0 4px 14px rgba(0,0,0,0.5);
3305
- border: 1px solid rgba(255,255,255,0.25);
3306
- background: rgba(255,255,255,0.05);
3381
+
3382
+ .uvf-title-text {
3383
+ display: flex;
3384
+ flex-direction: column;
3385
+ min-width: 0; /* Allow shrinking */
3386
+ flex: 1;
3307
3387
  }
3308
- .uvf-title-text { display: flex; flex-direction: column; }
3388
+
3309
3389
  .uvf-video-title {
3310
3390
  color: var(--uvf-text-primary);
3311
- font-size: 18px;
3391
+ font-size: clamp(14px, 2.5vw, 18px); /* Responsive font size */
3312
3392
  font-weight: 600;
3313
3393
  text-shadow: 0 2px 4px rgba(0,0,0,0.5);
3394
+ line-height: 1.3;
3395
+ overflow: hidden;
3396
+ text-overflow: ellipsis;
3397
+ white-space: nowrap;
3398
+ max-width: 100%;
3399
+ cursor: pointer;
3400
+ transition: color 0.3s ease;
3401
+ position: relative;
3402
+ }
3403
+
3404
+ .uvf-video-title:hover {
3405
+ color: var(--uvf-accent-1, #ff0000);
3314
3406
  }
3315
3407
 
3316
3408
  .uvf-video-subtitle {
3317
3409
  color: var(--uvf-text-secondary);
3318
- font-size: 13px;
3319
- margin-top: 4px;
3320
- max-width: min(70vw, 900px);
3410
+ font-size: clamp(11px, 1.8vw, 13px); /* Responsive font size */
3411
+ margin-top: 2px;
3321
3412
  overflow: hidden;
3322
3413
  text-overflow: ellipsis;
3323
3414
  white-space: nowrap;
3415
+ max-width: 100%;
3416
+ opacity: 0.9;
3417
+ line-height: 1.4;
3418
+ cursor: pointer;
3419
+ transition: opacity 0.3s ease;
3420
+ position: relative;
3421
+ }
3422
+
3423
+ .uvf-video-subtitle:hover {
3424
+ opacity: 1;
3425
+ }
3426
+
3427
+ /* Tooltip for long text */
3428
+ .uvf-text-tooltip {
3429
+ position: absolute;
3430
+ bottom: 100%;
3431
+ left: 0;
3432
+ background: rgba(0, 0, 0, 0.9);
3433
+ color: white;
3434
+ padding: 8px 12px;
3435
+ border-radius: 6px;
3436
+ font-size: 13px;
3437
+ line-height: 1.4;
3438
+ max-width: 400px;
3439
+ word-wrap: break-word;
3440
+ white-space: normal;
3441
+ z-index: 1000;
3442
+ opacity: 0;
3443
+ visibility: hidden;
3444
+ transform: translateY(-5px);
3445
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
3446
+ pointer-events: none;
3447
+ border: 1px solid rgba(255, 255, 255, 0.2);
3448
+ backdrop-filter: blur(8px);
3449
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
3450
+ }
3451
+
3452
+ .uvf-text-tooltip::before {
3453
+ content: '';
3454
+ position: absolute;
3455
+ top: 100%;
3456
+ left: 12px;
3457
+ border: 5px solid transparent;
3458
+ border-top-color: rgba(0, 0, 0, 0.9);
3459
+ }
3460
+
3461
+ .uvf-text-tooltip.show {
3462
+ opacity: 1;
3463
+ visibility: visible;
3464
+ transform: translateY(0);
3465
+ }
3466
+
3467
+ /* Multi-line title option for desktop */
3468
+ .uvf-video-title.multiline {
3469
+ white-space: normal;
3470
+ display: -webkit-box;
3471
+ -webkit-line-clamp: 2;
3472
+ -webkit-box-orient: vertical;
3473
+ line-height: 1.2;
3474
+ max-height: 2.4em;
3475
+ }
3476
+
3477
+ .uvf-video-subtitle.multiline {
3478
+ white-space: normal;
3479
+ display: -webkit-box;
3480
+ -webkit-line-clamp: 3;
3481
+ -webkit-box-orient: vertical;
3482
+ line-height: 1.3;
3483
+ max-height: 3.9em;
3324
3484
  }
3325
3485
 
3326
3486
  /* Above seekbar section with time and branding */
@@ -3399,7 +3559,21 @@ export class WebPlayer extends BasePlayer {
3399
3559
  }
3400
3560
  }
3401
3561
 
3562
+ /* Ultra small screens */
3402
3563
  @media (max-width: 480px) {
3564
+ .uvf-video-title {
3565
+ font-size: clamp(11px, 3.5vw, 14px) !important;
3566
+ }
3567
+
3568
+ .uvf-video-subtitle {
3569
+ font-size: clamp(9px, 2.5vw, 11px) !important;
3570
+ -webkit-line-clamp: 1; /* Single line on very small screens */
3571
+ }
3572
+
3573
+ .uvf-left-side {
3574
+ max-width: 80%;
3575
+ }
3576
+
3403
3577
  .uvf-above-seekbar-section {
3404
3578
  margin-bottom: 5px;
3405
3579
  }
@@ -3787,14 +3961,55 @@ export class WebPlayer extends BasePlayer {
3787
3961
  }
3788
3962
 
3789
3963
 
3790
- /* Mobile portrait - hide skip buttons, ensure top bar visible */
3964
+ /* Mobile responsive styles for navigation buttons */
3965
+ @media screen and (max-width: 767px) {
3966
+ .uvf-nav-btn {
3967
+ width: 36px;
3968
+ height: 36px;
3969
+ min-width: 36px;
3970
+ min-height: 36px;
3971
+ }
3972
+
3973
+ .uvf-nav-btn svg {
3974
+ width: 18px;
3975
+ height: 18px;
3976
+ }
3977
+
3978
+ .uvf-navigation-controls {
3979
+ gap: 6px;
3980
+ }
3981
+
3982
+ .uvf-left-side {
3983
+ gap: 8px;
3984
+ max-width: 75%;
3985
+ }
3986
+
3987
+ /* Mobile title adjustments */
3988
+ .uvf-video-title {
3989
+ font-size: clamp(12px, 3vw, 16px) !important;
3990
+ line-height: 1.2;
3991
+ }
3992
+
3993
+ .uvf-video-subtitle {
3994
+ font-size: clamp(10px, 2.2vw, 12px) !important;
3995
+ margin-top: 1px;
3996
+ /* Allow wrapping on mobile if needed */
3997
+ white-space: normal;
3998
+ display: -webkit-box;
3999
+ -webkit-line-clamp: 2;
4000
+ -webkit-box-orient: vertical;
4001
+ overflow: hidden;
4002
+ }
4003
+ }
4004
+
4005
+ /* Mobile portrait - hide skip buttons, ensure top bar syncs with controls */
3791
4006
  @media screen and (max-width: 767px) and (orientation: portrait) {
3792
4007
  #uvf-skip-back,
3793
4008
  #uvf-skip-forward {
3794
4009
  display: none !important;
3795
4010
  }
3796
4011
 
3797
- /* Ensure top bar and controls are visible */
4012
+ /* Top bar structure */
3798
4013
  .uvf-top-bar {
3799
4014
  display: flex !important;
3800
4015
  z-index: 10 !important;
@@ -3804,8 +4019,13 @@ export class WebPlayer extends BasePlayer {
3804
4019
  display: flex !important;
3805
4020
  }
3806
4021
 
3807
- /* Show top bar when controls are visible or on hover */
3808
- .uvf-player-wrapper:hover .uvf-top-bar,
4022
+ /* Disable hover effect on mobile - use only controls-visible class */
4023
+ .uvf-player-wrapper:hover .uvf-top-bar {
4024
+ opacity: 0 !important;
4025
+ transform: translateY(-10px) !important;
4026
+ }
4027
+
4028
+ /* Show top bar ONLY when controls are visible */
3809
4029
  .uvf-player-wrapper.controls-visible .uvf-top-bar {
3810
4030
  opacity: 1 !important;
3811
4031
  transform: translateY(0) !important;
@@ -3941,6 +4161,18 @@ export class WebPlayer extends BasePlayer {
3941
4161
  height: 16px;
3942
4162
  }
3943
4163
 
4164
+ /* Disable hover effect on mobile landscape - use only controls-visible class */
4165
+ .uvf-player-wrapper:hover .uvf-top-bar {
4166
+ opacity: 0 !important;
4167
+ transform: translateY(-10px) !important;
4168
+ }
4169
+
4170
+ /* Show top bar ONLY when controls are visible */
4171
+ .uvf-player-wrapper.controls-visible .uvf-top-bar {
4172
+ opacity: 1 !important;
4173
+ transform: translateY(0) !important;
4174
+ }
4175
+
3944
4176
  /* Top bar in fullscreen landscape */
3945
4177
  .uvf-player-wrapper.uvf-fullscreen .uvf-top-bar,
3946
4178
  .uvf-player-wrapper.uvf-fullscreen .uvf-video-container .uvf-top-bar,
@@ -3954,6 +4186,31 @@ export class WebPlayer extends BasePlayer {
3954
4186
 
3955
4187
  /* Tablet devices - Enhanced UX with desktop features */
3956
4188
  @media screen and (min-width: 768px) and (max-width: 1023px) {
4189
+ /* Tablet navigation and title adjustments */
4190
+ .uvf-nav-btn {
4191
+ width: 38px;
4192
+ height: 38px;
4193
+ min-width: 38px;
4194
+ min-height: 38px;
4195
+ }
4196
+
4197
+ .uvf-nav-btn svg {
4198
+ width: 19px;
4199
+ height: 19px;
4200
+ }
4201
+
4202
+ .uvf-left-side {
4203
+ max-width: 70%;
4204
+ }
4205
+
4206
+ .uvf-video-title {
4207
+ font-size: clamp(15px, 2.2vw, 17px) !important;
4208
+ }
4209
+
4210
+ .uvf-video-subtitle {
4211
+ font-size: clamp(12px, 1.8vw, 13px) !important;
4212
+ }
4213
+
3957
4214
  .uvf-controls-bar {
3958
4215
  padding: 18px 16px;
3959
4216
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 70%, var(--uvf-overlay-transparent) 100%);
@@ -4486,6 +4743,40 @@ export class WebPlayer extends BasePlayer {
4486
4743
  }
4487
4744
  }
4488
4745
 
4746
+ /* Desktop styles for title and navigation */
4747
+ @media screen and (min-width: 1024px) {
4748
+ .uvf-left-side {
4749
+ max-width: 65%; /* More space for title on desktop */
4750
+ }
4751
+
4752
+ .uvf-video-title {
4753
+ font-size: clamp(16px, 1.8vw, 20px) !important;
4754
+ font-weight: 700; /* Bolder on desktop */
4755
+ }
4756
+
4757
+ .uvf-video-subtitle {
4758
+ font-size: clamp(13px, 1.4vw, 15px) !important;
4759
+ margin-top: 3px;
4760
+ }
4761
+
4762
+ /* Allow hover effects on desktop */
4763
+ .uvf-title-bar:hover .uvf-video-title {
4764
+ color: var(--uvf-accent-1, #ff0000);
4765
+ transition: color 0.3s ease;
4766
+ }
4767
+ }
4768
+
4769
+ /* Ultra-wide screens */
4770
+ @media screen and (min-width: 1440px) {
4771
+ .uvf-video-title {
4772
+ font-size: clamp(18px, 1.6vw, 22px) !important;
4773
+ }
4774
+
4775
+ .uvf-video-subtitle {
4776
+ font-size: clamp(14px, 1.2vw, 16px) !important;
4777
+ }
4778
+ }
4779
+
4489
4780
  /* Paywall Desktop */
4490
4781
  @media screen and (min-width: 1024px) {
4491
4782
  .uvf-paywall-modal {
@@ -4544,6 +4835,100 @@ export class WebPlayer extends BasePlayer {
4544
4835
  container.appendChild(brandingContainer);
4545
4836
  this.debugLog('Framework branding added');
4546
4837
  }
4838
+ createNavigationButtons(container) {
4839
+ const navigationConfig = this.config.navigation;
4840
+ if (!navigationConfig)
4841
+ return;
4842
+ const { backButton, closeButton } = navigationConfig;
4843
+ if (backButton?.enabled) {
4844
+ const backBtn = document.createElement('button');
4845
+ backBtn.className = 'uvf-control-btn uvf-nav-btn';
4846
+ backBtn.id = 'uvf-back-btn';
4847
+ backBtn.title = backButton.title || 'Back';
4848
+ backBtn.setAttribute('aria-label', backButton.ariaLabel || 'Go back');
4849
+ const backIcon = this.getNavigationIcon(backButton.icon || 'arrow', backButton.customIcon);
4850
+ backBtn.innerHTML = backIcon;
4851
+ backBtn.addEventListener('click', async (e) => {
4852
+ e.preventDefault();
4853
+ e.stopPropagation();
4854
+ if (backButton.onClick) {
4855
+ await backButton.onClick();
4856
+ }
4857
+ else if (backButton.href) {
4858
+ if (backButton.replace) {
4859
+ window.history.replaceState(null, '', backButton.href);
4860
+ }
4861
+ else {
4862
+ window.location.href = backButton.href;
4863
+ }
4864
+ }
4865
+ else {
4866
+ window.history.back();
4867
+ }
4868
+ this.emit('navigationBackClicked');
4869
+ });
4870
+ container.appendChild(backBtn);
4871
+ }
4872
+ if (closeButton?.enabled) {
4873
+ const closeBtn = document.createElement('button');
4874
+ closeBtn.className = 'uvf-control-btn uvf-nav-btn';
4875
+ closeBtn.id = 'uvf-close-btn';
4876
+ closeBtn.title = closeButton.title || 'Close';
4877
+ closeBtn.setAttribute('aria-label', closeButton.ariaLabel || 'Close player');
4878
+ const closeIcon = this.getNavigationIcon(closeButton.icon || 'x', closeButton.customIcon);
4879
+ closeBtn.innerHTML = closeIcon;
4880
+ closeBtn.addEventListener('click', async (e) => {
4881
+ e.preventDefault();
4882
+ e.stopPropagation();
4883
+ if (closeButton.onClick) {
4884
+ await closeButton.onClick();
4885
+ }
4886
+ else {
4887
+ if (closeButton.exitFullscreen && this.isFullscreen()) {
4888
+ await this.exitFullscreen();
4889
+ }
4890
+ if (closeButton.closeModal) {
4891
+ const playerWrapper = this.container?.querySelector('.uvf-player-wrapper');
4892
+ if (playerWrapper) {
4893
+ playerWrapper.style.display = 'none';
4894
+ }
4895
+ }
4896
+ }
4897
+ this.emit('navigationCloseClicked');
4898
+ });
4899
+ container.appendChild(closeBtn);
4900
+ }
4901
+ }
4902
+ getNavigationIcon(iconType, customIcon) {
4903
+ if (customIcon) {
4904
+ if (customIcon.startsWith('http') || customIcon.includes('.')) {
4905
+ return `<img src="${customIcon}" alt="" style="width: 20px; height: 20px;" />`;
4906
+ }
4907
+ return customIcon;
4908
+ }
4909
+ switch (iconType) {
4910
+ case 'arrow':
4911
+ return `<svg viewBox="0 0 24 24">
4912
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
4913
+ </svg>`;
4914
+ case 'chevron':
4915
+ return `<svg viewBox="0 0 24 24">
4916
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
4917
+ </svg>`;
4918
+ case 'x':
4919
+ return `<svg viewBox="0 0 24 24">
4920
+ <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"/>
4921
+ </svg>`;
4922
+ case 'close':
4923
+ return `<svg viewBox="0 0 24 24">
4924
+ <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"/>
4925
+ </svg>`;
4926
+ default:
4927
+ return `<svg viewBox="0 0 24 24">
4928
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
4929
+ </svg>`;
4930
+ }
4931
+ }
4547
4932
  createCustomControls(container) {
4548
4933
  const topGradient = document.createElement('div');
4549
4934
  topGradient.className = 'uvf-top-gradient';
@@ -4553,18 +4938,24 @@ export class WebPlayer extends BasePlayer {
4553
4938
  container.appendChild(controlsGradient);
4554
4939
  const topBar = document.createElement('div');
4555
4940
  topBar.className = 'uvf-top-bar';
4941
+ const leftSide = document.createElement('div');
4942
+ leftSide.className = 'uvf-left-side';
4943
+ const navigationControls = document.createElement('div');
4944
+ navigationControls.className = 'uvf-navigation-controls';
4945
+ this.createNavigationButtons(navigationControls);
4946
+ leftSide.appendChild(navigationControls);
4556
4947
  const titleBar = document.createElement('div');
4557
4948
  titleBar.className = 'uvf-title-bar';
4558
4949
  titleBar.innerHTML = `
4559
4950
  <div class="uvf-title-content">
4560
- <img class="uvf-video-thumb" id="uvf-video-thumb" alt="thumbnail" style="display:none;" />
4561
4951
  <div class="uvf-title-text">
4562
4952
  <div class=\"uvf-video-title\" id=\"uvf-video-title\" style=\"display:none;\"></div>
4563
4953
  <div class=\"uvf-video-subtitle\" id=\"uvf-video-description\" style=\"display:none;\"></div>
4564
4954
  </div>
4565
4955
  </div>
4566
4956
  `;
4567
- topBar.appendChild(titleBar);
4957
+ leftSide.appendChild(titleBar);
4958
+ topBar.appendChild(leftSide);
4568
4959
  const topControls = document.createElement('div');
4569
4960
  topControls.className = 'uvf-top-controls';
4570
4961
  topControls.innerHTML = `
@@ -5225,11 +5616,13 @@ export class WebPlayer extends BasePlayer {
5225
5616
  break;
5226
5617
  case 'ArrowLeft':
5227
5618
  e.preventDefault();
5619
+ e.stopImmediatePropagation();
5228
5620
  this.seek(Math.max(0, this.video.currentTime - 10));
5229
5621
  shortcutText = '-10s';
5230
5622
  break;
5231
5623
  case 'ArrowRight':
5232
5624
  e.preventDefault();
5625
+ e.stopImmediatePropagation();
5233
5626
  this.seek(Math.min(this.video.duration, this.video.currentTime + 10));
5234
5627
  shortcutText = '+10s';
5235
5628
  break;
@@ -6210,7 +6603,7 @@ export class WebPlayer extends BasePlayer {
6210
6603
  autoSkip: this.chapterConfig.userPreferences?.autoSkipIntro || false,
6211
6604
  onChapterChange: (chapter) => {
6212
6605
  this.debugLog('Core chapter changed:', chapter?.title || 'none');
6213
- this.emit('chapterchange', chapter);
6606
+ this.emit('onChapterchange', chapter);
6214
6607
  },
6215
6608
  onSegmentEntered: (segment) => {
6216
6609
  this.debugLog('Core segment entered:', segment.title);
@@ -7323,6 +7716,123 @@ export class WebPlayer extends BasePlayer {
7323
7716
  this.showNotification('Share failed');
7324
7717
  }
7325
7718
  }
7719
+ isTextTruncated(element) {
7720
+ return element.scrollWidth > element.offsetWidth || element.scrollHeight > element.offsetHeight;
7721
+ }
7722
+ showTextTooltip(element, text) {
7723
+ const existingTooltip = element.querySelector('.uvf-text-tooltip');
7724
+ if (existingTooltip) {
7725
+ existingTooltip.remove();
7726
+ }
7727
+ const tooltip = document.createElement('div');
7728
+ tooltip.className = 'uvf-text-tooltip';
7729
+ tooltip.textContent = text;
7730
+ element.appendChild(tooltip);
7731
+ setTimeout(() => {
7732
+ tooltip.classList.add('show');
7733
+ }, 100);
7734
+ }
7735
+ hideTextTooltip(element) {
7736
+ const tooltip = element.querySelector('.uvf-text-tooltip');
7737
+ if (tooltip) {
7738
+ tooltip.classList.remove('show');
7739
+ setTimeout(() => {
7740
+ if (tooltip.parentElement) {
7741
+ tooltip.remove();
7742
+ }
7743
+ }, 300);
7744
+ }
7745
+ }
7746
+ setupTextTooltips() {
7747
+ const titleElement = document.getElementById('uvf-video-title');
7748
+ const descElement = document.getElementById('uvf-video-description');
7749
+ if (titleElement) {
7750
+ titleElement.addEventListener('mouseenter', () => {
7751
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
7752
+ if (this.isTextTruncated(titleElement) && titleText) {
7753
+ this.showTextTooltip(titleElement, titleText);
7754
+ }
7755
+ });
7756
+ titleElement.addEventListener('mouseleave', () => {
7757
+ this.hideTextTooltip(titleElement);
7758
+ });
7759
+ titleElement.addEventListener('touchstart', () => {
7760
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
7761
+ if (this.isTextTruncated(titleElement) && titleText) {
7762
+ this.showTextTooltip(titleElement, titleText);
7763
+ setTimeout(() => {
7764
+ this.hideTextTooltip(titleElement);
7765
+ }, 3000);
7766
+ }
7767
+ });
7768
+ }
7769
+ if (descElement) {
7770
+ descElement.addEventListener('mouseenter', () => {
7771
+ const descText = (this.source?.metadata?.description || '').toString().trim();
7772
+ if (this.isTextTruncated(descElement) && descText) {
7773
+ this.showTextTooltip(descElement, descText);
7774
+ }
7775
+ });
7776
+ descElement.addEventListener('mouseleave', () => {
7777
+ this.hideTextTooltip(descElement);
7778
+ });
7779
+ descElement.addEventListener('touchstart', () => {
7780
+ const descText = (this.source?.metadata?.description || '').toString().trim();
7781
+ if (this.isTextTruncated(descElement) && descText) {
7782
+ this.showTextTooltip(descElement, descText);
7783
+ setTimeout(() => {
7784
+ this.hideTextTooltip(descElement);
7785
+ }, 3000);
7786
+ }
7787
+ });
7788
+ }
7789
+ }
7790
+ smartTruncateText(text, maxWords = 12) {
7791
+ const words = text.split(' ');
7792
+ if (words.length <= maxWords) {
7793
+ return { truncated: text, needsTooltip: false };
7794
+ }
7795
+ const truncated = words.slice(0, maxWords).join(' ') + '...';
7796
+ return { truncated, needsTooltip: true };
7797
+ }
7798
+ applySmartTextDisplay(titleEl, descEl, titleText, descText) {
7799
+ const isDesktop = window.innerWidth >= 1024;
7800
+ const isMobile = window.innerWidth < 768;
7801
+ if (titleEl && titleText) {
7802
+ const wordCount = titleText.split(' ').length;
7803
+ if (isDesktop && wordCount > 8 && wordCount <= 15) {
7804
+ titleEl.classList.add('multiline');
7805
+ titleEl.textContent = titleText;
7806
+ }
7807
+ else if (wordCount > 12) {
7808
+ const maxWords = isMobile ? 8 : isDesktop ? 12 : 10;
7809
+ const { truncated } = this.smartTruncateText(titleText, maxWords);
7810
+ titleEl.textContent = truncated;
7811
+ titleEl.classList.remove('multiline');
7812
+ }
7813
+ else {
7814
+ titleEl.textContent = titleText;
7815
+ titleEl.classList.remove('multiline');
7816
+ }
7817
+ }
7818
+ if (descEl && descText) {
7819
+ const wordCount = descText.split(' ').length;
7820
+ if (isDesktop && wordCount > 15 && wordCount <= 25) {
7821
+ descEl.classList.add('multiline');
7822
+ descEl.textContent = descText;
7823
+ }
7824
+ else if (wordCount > 20) {
7825
+ const maxWords = isMobile ? 12 : isDesktop ? 18 : 15;
7826
+ const { truncated } = this.smartTruncateText(descText, maxWords);
7827
+ descEl.textContent = truncated;
7828
+ descEl.classList.remove('multiline');
7829
+ }
7830
+ else {
7831
+ descEl.textContent = descText;
7832
+ descEl.classList.remove('multiline');
7833
+ }
7834
+ }
7835
+ }
7326
7836
  updateMetadataUI() {
7327
7837
  try {
7328
7838
  const md = this.source?.metadata || {};
@@ -7333,28 +7843,23 @@ export class WebPlayer extends BasePlayer {
7333
7843
  const titleText = (md.title || '').toString().trim();
7334
7844
  const descText = (md.description || '').toString().trim();
7335
7845
  const thumbUrl = (md.thumbnailUrl || '').toString().trim();
7846
+ this.applySmartTextDisplay(titleEl, descEl, titleText, descText);
7336
7847
  if (titleEl) {
7337
- titleEl.textContent = titleText;
7338
7848
  titleEl.style.display = titleText ? 'block' : 'none';
7339
7849
  }
7340
7850
  if (descEl) {
7341
- descEl.textContent = descText;
7342
7851
  descEl.style.display = descText ? 'block' : 'none';
7343
7852
  }
7344
7853
  if (thumbEl) {
7345
- if (thumbUrl) {
7346
- thumbEl.src = thumbUrl;
7347
- thumbEl.style.display = 'block';
7348
- }
7349
- else {
7350
- thumbEl.removeAttribute('src');
7351
- thumbEl.style.display = 'none';
7352
- }
7854
+ thumbEl.style.display = 'none';
7353
7855
  }
7354
- const hasAny = !!(titleText || descText || thumbUrl);
7856
+ const hasAny = !!(titleText || descText);
7355
7857
  if (titleBar) {
7356
7858
  titleBar.style.display = hasAny ? '' : 'none';
7357
7859
  }
7860
+ setTimeout(() => {
7861
+ this.setupTextTooltips();
7862
+ }, 100);
7358
7863
  }
7359
7864
  catch (_) { }
7360
7865
  }