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
@@ -882,7 +882,7 @@ export class WebPlayer extends BasePlayer {
882
882
  }
883
883
  setupClickToUnmute() {
884
884
  if (this.clickToUnmuteHandler) {
885
- this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler);
885
+ this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
886
886
  }
887
887
  this.clickToUnmuteHandler = (e) => {
888
888
  const unmuteBtn = document.getElementById('uvf-unmute-btn');
@@ -894,15 +894,17 @@ export class WebPlayer extends BasePlayer {
894
894
  target.closest('.uvf-settings-menu')) {
895
895
  return;
896
896
  }
897
+ e.stopPropagation();
898
+ e.preventDefault();
897
899
  this.video.muted = false;
898
900
  this.debugLog('🔊 Video unmuted by clicking on player');
899
901
  this.hideUnmuteButton();
900
902
  if (this.clickToUnmuteHandler) {
901
- this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler);
903
+ this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
902
904
  this.clickToUnmuteHandler = null;
903
905
  }
904
906
  };
905
- this.playerWrapper?.addEventListener('click', this.clickToUnmuteHandler);
907
+ this.playerWrapper?.addEventListener('click', this.clickToUnmuteHandler, true);
906
908
  this.debugLog('👆 Click anywhere to unmute enabled');
907
909
  }
908
910
  hideUnmuteButton() {
@@ -912,7 +914,7 @@ export class WebPlayer extends BasePlayer {
912
914
  this.debugLog('Unmute button removed');
913
915
  }
914
916
  if (this.clickToUnmuteHandler) {
915
- this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler);
917
+ this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
916
918
  this.clickToUnmuteHandler = null;
917
919
  }
918
920
  }
@@ -2717,6 +2719,37 @@ export class WebPlayer extends BasePlayer {
2717
2719
  filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
2718
2720
  }
2719
2721
 
2722
+ /* PiP Button Specific Styling */
2723
+ #uvf-pip-btn {
2724
+ background: var(--uvf-button-bg);
2725
+ border: 1px solid var(--uvf-button-border);
2726
+ position: relative;
2727
+ z-index: 10;
2728
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2729
+ }
2730
+
2731
+ #uvf-pip-btn:hover {
2732
+ transform: scale(1.08);
2733
+ box-shadow: 0 4px 12px var(--uvf-button-shadow);
2734
+ }
2735
+
2736
+ #uvf-pip-btn:active {
2737
+ transform: scale(0.95);
2738
+ transition: all 0.1s ease;
2739
+ }
2740
+
2741
+ #uvf-pip-btn svg {
2742
+ opacity: 0.9;
2743
+ transition: all 0.3s ease;
2744
+ filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3));
2745
+ }
2746
+
2747
+ #uvf-pip-btn:hover svg {
2748
+ opacity: 1;
2749
+ transform: scale(1.05);
2750
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
2751
+ }
2752
+
2720
2753
  /* Fullscreen Button Specific Styling */
2721
2754
  #uvf-fullscreen-btn {
2722
2755
  background: var(--uvf-button-bg);
@@ -3220,7 +3253,7 @@ export class WebPlayer extends BasePlayer {
3220
3253
  margin-left: 10px;
3221
3254
  }
3222
3255
 
3223
- /* Top Bar Container - Contains Title (left) and Controls (right) */
3256
+ /* Top Bar Container - Contains Navigation + Title (left) and Controls (right) */
3224
3257
  .uvf-top-bar {
3225
3258
  position: absolute;
3226
3259
  top: 0;
@@ -3237,16 +3270,97 @@ export class WebPlayer extends BasePlayer {
3237
3270
  transition: all 0.3s ease;
3238
3271
  }
3239
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
+
3240
3354
  .uvf-player-wrapper:hover .uvf-top-bar,
3241
3355
  .uvf-player-wrapper.controls-visible .uvf-top-bar {
3242
3356
  opacity: 1;
3243
3357
  transform: translateY(0);
3244
3358
  }
3245
3359
 
3246
- /* Title Bar - Left side of top bar */
3360
+ /* Title Bar - After navigation buttons */
3247
3361
  .uvf-title-bar {
3248
- flex: 0 1 auto;
3249
- max-width: 60%;
3362
+ flex: 1;
3363
+ min-width: 0; /* Allow shrinking */
3250
3364
  }
3251
3365
 
3252
3366
  /* Top Controls - Right side of top bar */
@@ -3261,33 +3375,112 @@ export class WebPlayer extends BasePlayer {
3261
3375
  .uvf-title-content {
3262
3376
  display: flex;
3263
3377
  align-items: center;
3264
- gap: 12px;
3378
+ width: 100%;
3379
+ min-width: 0; /* Allow shrinking */
3265
3380
  }
3266
- .uvf-video-thumb {
3267
- width: 56px;
3268
- height: 56px;
3269
- border-radius: 8px;
3270
- object-fit: cover;
3271
- box-shadow: 0 4px 14px rgba(0,0,0,0.5);
3272
- border: 1px solid rgba(255,255,255,0.25);
3273
- 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;
3274
3387
  }
3275
- .uvf-title-text { display: flex; flex-direction: column; }
3388
+
3276
3389
  .uvf-video-title {
3277
3390
  color: var(--uvf-text-primary);
3278
- font-size: 18px;
3391
+ font-size: clamp(14px, 2.5vw, 18px); /* Responsive font size */
3279
3392
  font-weight: 600;
3280
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);
3281
3406
  }
3282
3407
 
3283
3408
  .uvf-video-subtitle {
3284
3409
  color: var(--uvf-text-secondary);
3285
- font-size: 13px;
3286
- margin-top: 4px;
3287
- max-width: min(70vw, 900px);
3410
+ font-size: clamp(11px, 1.8vw, 13px); /* Responsive font size */
3411
+ margin-top: 2px;
3288
3412
  overflow: hidden;
3289
3413
  text-overflow: ellipsis;
3290
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;
3291
3484
  }
3292
3485
 
3293
3486
  /* Above seekbar section with time and branding */
@@ -3366,7 +3559,21 @@ export class WebPlayer extends BasePlayer {
3366
3559
  }
3367
3560
  }
3368
3561
 
3562
+ /* Ultra small screens */
3369
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
+
3370
3577
  .uvf-above-seekbar-section {
3371
3578
  margin-bottom: 5px;
3372
3579
  }
@@ -3754,6 +3961,47 @@ export class WebPlayer extends BasePlayer {
3754
3961
  }
3755
3962
 
3756
3963
 
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
+
3757
4005
  /* Mobile portrait - hide skip buttons, ensure top bar visible */
3758
4006
  @media screen and (max-width: 767px) and (orientation: portrait) {
3759
4007
  #uvf-skip-back,
@@ -3921,6 +4169,31 @@ export class WebPlayer extends BasePlayer {
3921
4169
 
3922
4170
  /* Tablet devices - Enhanced UX with desktop features */
3923
4171
  @media screen and (min-width: 768px) and (max-width: 1023px) {
4172
+ /* Tablet navigation and title adjustments */
4173
+ .uvf-nav-btn {
4174
+ width: 38px;
4175
+ height: 38px;
4176
+ min-width: 38px;
4177
+ min-height: 38px;
4178
+ }
4179
+
4180
+ .uvf-nav-btn svg {
4181
+ width: 19px;
4182
+ height: 19px;
4183
+ }
4184
+
4185
+ .uvf-left-side {
4186
+ max-width: 70%;
4187
+ }
4188
+
4189
+ .uvf-video-title {
4190
+ font-size: clamp(15px, 2.2vw, 17px) !important;
4191
+ }
4192
+
4193
+ .uvf-video-subtitle {
4194
+ font-size: clamp(12px, 1.8vw, 13px) !important;
4195
+ }
4196
+
3924
4197
  .uvf-controls-bar {
3925
4198
  padding: 18px 16px;
3926
4199
  background: linear-gradient(to top, var(--uvf-overlay-strong) 0%, var(--uvf-overlay-medium) 70%, var(--uvf-overlay-transparent) 100%);
@@ -4453,6 +4726,40 @@ export class WebPlayer extends BasePlayer {
4453
4726
  }
4454
4727
  }
4455
4728
 
4729
+ /* Desktop styles for title and navigation */
4730
+ @media screen and (min-width: 1024px) {
4731
+ .uvf-left-side {
4732
+ max-width: 65%; /* More space for title on desktop */
4733
+ }
4734
+
4735
+ .uvf-video-title {
4736
+ font-size: clamp(16px, 1.8vw, 20px) !important;
4737
+ font-weight: 700; /* Bolder on desktop */
4738
+ }
4739
+
4740
+ .uvf-video-subtitle {
4741
+ font-size: clamp(13px, 1.4vw, 15px) !important;
4742
+ margin-top: 3px;
4743
+ }
4744
+
4745
+ /* Allow hover effects on desktop */
4746
+ .uvf-title-bar:hover .uvf-video-title {
4747
+ color: var(--uvf-accent-1, #ff0000);
4748
+ transition: color 0.3s ease;
4749
+ }
4750
+ }
4751
+
4752
+ /* Ultra-wide screens */
4753
+ @media screen and (min-width: 1440px) {
4754
+ .uvf-video-title {
4755
+ font-size: clamp(18px, 1.6vw, 22px) !important;
4756
+ }
4757
+
4758
+ .uvf-video-subtitle {
4759
+ font-size: clamp(14px, 1.2vw, 16px) !important;
4760
+ }
4761
+ }
4762
+
4456
4763
  /* Paywall Desktop */
4457
4764
  @media screen and (min-width: 1024px) {
4458
4765
  .uvf-paywall-modal {
@@ -4511,6 +4818,100 @@ export class WebPlayer extends BasePlayer {
4511
4818
  container.appendChild(brandingContainer);
4512
4819
  this.debugLog('Framework branding added');
4513
4820
  }
4821
+ createNavigationButtons(container) {
4822
+ const navigationConfig = this.config.navigation;
4823
+ if (!navigationConfig)
4824
+ return;
4825
+ const { backButton, closeButton } = navigationConfig;
4826
+ if (backButton?.enabled) {
4827
+ const backBtn = document.createElement('button');
4828
+ backBtn.className = 'uvf-control-btn uvf-nav-btn';
4829
+ backBtn.id = 'uvf-back-btn';
4830
+ backBtn.title = backButton.title || 'Back';
4831
+ backBtn.setAttribute('aria-label', backButton.ariaLabel || 'Go back');
4832
+ const backIcon = this.getNavigationIcon(backButton.icon || 'arrow', backButton.customIcon);
4833
+ backBtn.innerHTML = backIcon;
4834
+ backBtn.addEventListener('click', async (e) => {
4835
+ e.preventDefault();
4836
+ e.stopPropagation();
4837
+ if (backButton.onClick) {
4838
+ await backButton.onClick();
4839
+ }
4840
+ else if (backButton.href) {
4841
+ if (backButton.replace) {
4842
+ window.history.replaceState(null, '', backButton.href);
4843
+ }
4844
+ else {
4845
+ window.location.href = backButton.href;
4846
+ }
4847
+ }
4848
+ else {
4849
+ window.history.back();
4850
+ }
4851
+ this.emit('navigationBackClicked');
4852
+ });
4853
+ container.appendChild(backBtn);
4854
+ }
4855
+ if (closeButton?.enabled) {
4856
+ const closeBtn = document.createElement('button');
4857
+ closeBtn.className = 'uvf-control-btn uvf-nav-btn';
4858
+ closeBtn.id = 'uvf-close-btn';
4859
+ closeBtn.title = closeButton.title || 'Close';
4860
+ closeBtn.setAttribute('aria-label', closeButton.ariaLabel || 'Close player');
4861
+ const closeIcon = this.getNavigationIcon(closeButton.icon || 'x', closeButton.customIcon);
4862
+ closeBtn.innerHTML = closeIcon;
4863
+ closeBtn.addEventListener('click', async (e) => {
4864
+ e.preventDefault();
4865
+ e.stopPropagation();
4866
+ if (closeButton.onClick) {
4867
+ await closeButton.onClick();
4868
+ }
4869
+ else {
4870
+ if (closeButton.exitFullscreen && this.isFullscreen()) {
4871
+ await this.exitFullscreen();
4872
+ }
4873
+ if (closeButton.closeModal) {
4874
+ const playerWrapper = this.container?.querySelector('.uvf-player-wrapper');
4875
+ if (playerWrapper) {
4876
+ playerWrapper.style.display = 'none';
4877
+ }
4878
+ }
4879
+ }
4880
+ this.emit('navigationCloseClicked');
4881
+ });
4882
+ container.appendChild(closeBtn);
4883
+ }
4884
+ }
4885
+ getNavigationIcon(iconType, customIcon) {
4886
+ if (customIcon) {
4887
+ if (customIcon.startsWith('http') || customIcon.includes('.')) {
4888
+ return `<img src="${customIcon}" alt="" style="width: 20px; height: 20px;" />`;
4889
+ }
4890
+ return customIcon;
4891
+ }
4892
+ switch (iconType) {
4893
+ case 'arrow':
4894
+ return `<svg viewBox="0 0 24 24">
4895
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
4896
+ </svg>`;
4897
+ case 'chevron':
4898
+ return `<svg viewBox="0 0 24 24">
4899
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
4900
+ </svg>`;
4901
+ case 'x':
4902
+ return `<svg viewBox="0 0 24 24">
4903
+ <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"/>
4904
+ </svg>`;
4905
+ case 'close':
4906
+ return `<svg viewBox="0 0 24 24">
4907
+ <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"/>
4908
+ </svg>`;
4909
+ default:
4910
+ return `<svg viewBox="0 0 24 24">
4911
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
4912
+ </svg>`;
4913
+ }
4914
+ }
4514
4915
  createCustomControls(container) {
4515
4916
  const topGradient = document.createElement('div');
4516
4917
  topGradient.className = 'uvf-top-gradient';
@@ -4520,18 +4921,24 @@ export class WebPlayer extends BasePlayer {
4520
4921
  container.appendChild(controlsGradient);
4521
4922
  const topBar = document.createElement('div');
4522
4923
  topBar.className = 'uvf-top-bar';
4924
+ const leftSide = document.createElement('div');
4925
+ leftSide.className = 'uvf-left-side';
4926
+ const navigationControls = document.createElement('div');
4927
+ navigationControls.className = 'uvf-navigation-controls';
4928
+ this.createNavigationButtons(navigationControls);
4929
+ leftSide.appendChild(navigationControls);
4523
4930
  const titleBar = document.createElement('div');
4524
4931
  titleBar.className = 'uvf-title-bar';
4525
4932
  titleBar.innerHTML = `
4526
4933
  <div class="uvf-title-content">
4527
- <img class="uvf-video-thumb" id="uvf-video-thumb" alt="thumbnail" style="display:none;" />
4528
4934
  <div class="uvf-title-text">
4529
4935
  <div class=\"uvf-video-title\" id=\"uvf-video-title\" style=\"display:none;\"></div>
4530
4936
  <div class=\"uvf-video-subtitle\" id=\"uvf-video-description\" style=\"display:none;\"></div>
4531
4937
  </div>
4532
4938
  </div>
4533
4939
  `;
4534
- topBar.appendChild(titleBar);
4940
+ leftSide.appendChild(titleBar);
4941
+ topBar.appendChild(leftSide);
4535
4942
  const topControls = document.createElement('div');
4536
4943
  topControls.className = 'uvf-top-controls';
4537
4944
  topControls.innerHTML = `
@@ -5192,11 +5599,13 @@ export class WebPlayer extends BasePlayer {
5192
5599
  break;
5193
5600
  case 'ArrowLeft':
5194
5601
  e.preventDefault();
5602
+ e.stopImmediatePropagation();
5195
5603
  this.seek(Math.max(0, this.video.currentTime - 10));
5196
5604
  shortcutText = '-10s';
5197
5605
  break;
5198
5606
  case 'ArrowRight':
5199
5607
  e.preventDefault();
5608
+ e.stopImmediatePropagation();
5200
5609
  this.seek(Math.min(this.video.duration, this.video.currentTime + 10));
5201
5610
  shortcutText = '+10s';
5202
5611
  break;
@@ -6177,7 +6586,7 @@ export class WebPlayer extends BasePlayer {
6177
6586
  autoSkip: this.chapterConfig.userPreferences?.autoSkipIntro || false,
6178
6587
  onChapterChange: (chapter) => {
6179
6588
  this.debugLog('Core chapter changed:', chapter?.title || 'none');
6180
- this.emit('chapterchange', chapter);
6589
+ this.emit('onChapterchange', chapter);
6181
6590
  },
6182
6591
  onSegmentEntered: (segment) => {
6183
6592
  this.debugLog('Core segment entered:', segment.title);
@@ -7290,6 +7699,123 @@ export class WebPlayer extends BasePlayer {
7290
7699
  this.showNotification('Share failed');
7291
7700
  }
7292
7701
  }
7702
+ isTextTruncated(element) {
7703
+ return element.scrollWidth > element.offsetWidth || element.scrollHeight > element.offsetHeight;
7704
+ }
7705
+ showTextTooltip(element, text) {
7706
+ const existingTooltip = element.querySelector('.uvf-text-tooltip');
7707
+ if (existingTooltip) {
7708
+ existingTooltip.remove();
7709
+ }
7710
+ const tooltip = document.createElement('div');
7711
+ tooltip.className = 'uvf-text-tooltip';
7712
+ tooltip.textContent = text;
7713
+ element.appendChild(tooltip);
7714
+ setTimeout(() => {
7715
+ tooltip.classList.add('show');
7716
+ }, 100);
7717
+ }
7718
+ hideTextTooltip(element) {
7719
+ const tooltip = element.querySelector('.uvf-text-tooltip');
7720
+ if (tooltip) {
7721
+ tooltip.classList.remove('show');
7722
+ setTimeout(() => {
7723
+ if (tooltip.parentElement) {
7724
+ tooltip.remove();
7725
+ }
7726
+ }, 300);
7727
+ }
7728
+ }
7729
+ setupTextTooltips() {
7730
+ const titleElement = document.getElementById('uvf-video-title');
7731
+ const descElement = document.getElementById('uvf-video-description');
7732
+ if (titleElement) {
7733
+ titleElement.addEventListener('mouseenter', () => {
7734
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
7735
+ if (this.isTextTruncated(titleElement) && titleText) {
7736
+ this.showTextTooltip(titleElement, titleText);
7737
+ }
7738
+ });
7739
+ titleElement.addEventListener('mouseleave', () => {
7740
+ this.hideTextTooltip(titleElement);
7741
+ });
7742
+ titleElement.addEventListener('touchstart', () => {
7743
+ const titleText = (this.source?.metadata?.title || '').toString().trim();
7744
+ if (this.isTextTruncated(titleElement) && titleText) {
7745
+ this.showTextTooltip(titleElement, titleText);
7746
+ setTimeout(() => {
7747
+ this.hideTextTooltip(titleElement);
7748
+ }, 3000);
7749
+ }
7750
+ });
7751
+ }
7752
+ if (descElement) {
7753
+ descElement.addEventListener('mouseenter', () => {
7754
+ const descText = (this.source?.metadata?.description || '').toString().trim();
7755
+ if (this.isTextTruncated(descElement) && descText) {
7756
+ this.showTextTooltip(descElement, descText);
7757
+ }
7758
+ });
7759
+ descElement.addEventListener('mouseleave', () => {
7760
+ this.hideTextTooltip(descElement);
7761
+ });
7762
+ descElement.addEventListener('touchstart', () => {
7763
+ const descText = (this.source?.metadata?.description || '').toString().trim();
7764
+ if (this.isTextTruncated(descElement) && descText) {
7765
+ this.showTextTooltip(descElement, descText);
7766
+ setTimeout(() => {
7767
+ this.hideTextTooltip(descElement);
7768
+ }, 3000);
7769
+ }
7770
+ });
7771
+ }
7772
+ }
7773
+ smartTruncateText(text, maxWords = 12) {
7774
+ const words = text.split(' ');
7775
+ if (words.length <= maxWords) {
7776
+ return { truncated: text, needsTooltip: false };
7777
+ }
7778
+ const truncated = words.slice(0, maxWords).join(' ') + '...';
7779
+ return { truncated, needsTooltip: true };
7780
+ }
7781
+ applySmartTextDisplay(titleEl, descEl, titleText, descText) {
7782
+ const isDesktop = window.innerWidth >= 1024;
7783
+ const isMobile = window.innerWidth < 768;
7784
+ if (titleEl && titleText) {
7785
+ const wordCount = titleText.split(' ').length;
7786
+ if (isDesktop && wordCount > 8 && wordCount <= 15) {
7787
+ titleEl.classList.add('multiline');
7788
+ titleEl.textContent = titleText;
7789
+ }
7790
+ else if (wordCount > 12) {
7791
+ const maxWords = isMobile ? 8 : isDesktop ? 12 : 10;
7792
+ const { truncated } = this.smartTruncateText(titleText, maxWords);
7793
+ titleEl.textContent = truncated;
7794
+ titleEl.classList.remove('multiline');
7795
+ }
7796
+ else {
7797
+ titleEl.textContent = titleText;
7798
+ titleEl.classList.remove('multiline');
7799
+ }
7800
+ }
7801
+ if (descEl && descText) {
7802
+ const wordCount = descText.split(' ').length;
7803
+ if (isDesktop && wordCount > 15 && wordCount <= 25) {
7804
+ descEl.classList.add('multiline');
7805
+ descEl.textContent = descText;
7806
+ }
7807
+ else if (wordCount > 20) {
7808
+ const maxWords = isMobile ? 12 : isDesktop ? 18 : 15;
7809
+ const { truncated } = this.smartTruncateText(descText, maxWords);
7810
+ descEl.textContent = truncated;
7811
+ descEl.classList.remove('multiline');
7812
+ }
7813
+ else {
7814
+ descEl.textContent = descText;
7815
+ descEl.classList.remove('multiline');
7816
+ }
7817
+ }
7818
+ }
7293
7819
  updateMetadataUI() {
7294
7820
  try {
7295
7821
  const md = this.source?.metadata || {};
@@ -7300,28 +7826,23 @@ export class WebPlayer extends BasePlayer {
7300
7826
  const titleText = (md.title || '').toString().trim();
7301
7827
  const descText = (md.description || '').toString().trim();
7302
7828
  const thumbUrl = (md.thumbnailUrl || '').toString().trim();
7829
+ this.applySmartTextDisplay(titleEl, descEl, titleText, descText);
7303
7830
  if (titleEl) {
7304
- titleEl.textContent = titleText;
7305
7831
  titleEl.style.display = titleText ? 'block' : 'none';
7306
7832
  }
7307
7833
  if (descEl) {
7308
- descEl.textContent = descText;
7309
7834
  descEl.style.display = descText ? 'block' : 'none';
7310
7835
  }
7311
7836
  if (thumbEl) {
7312
- if (thumbUrl) {
7313
- thumbEl.src = thumbUrl;
7314
- thumbEl.style.display = 'block';
7315
- }
7316
- else {
7317
- thumbEl.removeAttribute('src');
7318
- thumbEl.style.display = 'none';
7319
- }
7837
+ thumbEl.style.display = 'none';
7320
7838
  }
7321
- const hasAny = !!(titleText || descText || thumbUrl);
7839
+ const hasAny = !!(titleText || descText);
7322
7840
  if (titleBar) {
7323
7841
  titleBar.style.display = hasAny ? '' : 'none';
7324
7842
  }
7843
+ setTimeout(() => {
7844
+ this.setupTextTooltips();
7845
+ }, 100);
7325
7846
  }
7326
7847
  catch (_) { }
7327
7848
  }