unified-video-framework 1.4.165 → 1.4.166

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.
@@ -53,6 +53,16 @@ export class WebPlayer extends BasePlayer {
53
53
  this.hasTriedButtonFallback = false;
54
54
  this.lastUserInteraction = 0;
55
55
  this.showTimeTooltip = false;
56
+ this.lastTapTime = 0;
57
+ this.tapCount = 0;
58
+ this.tapTimeout = null;
59
+ this.longPressTimer = null;
60
+ this.longPressActive = false;
61
+ this.longPressStartTime = 0;
62
+ this.originalPlaybackRate = 1;
63
+ this.dominantColor = '#ff0000';
64
+ this.accentColor = '#ff4d4f';
65
+ this.surfaceTint = 'rgba(255, 0, 0, 0.08)';
56
66
  this.autoplayCapabilities = {
57
67
  canAutoplay: false,
58
68
  canAutoplayMuted: false,
@@ -230,6 +240,26 @@ export class WebPlayer extends BasePlayer {
230
240
  catch (_) { }
231
241
  this.setupCastContextSafe();
232
242
  this.updateMetadataUI();
243
+ this.enableMaterialYouMobileIfNeeded();
244
+ }
245
+ enableMaterialYouMobileIfNeeded() {
246
+ const isMobile = this.isMobileDevice();
247
+ const isPortrait = window.innerHeight > window.innerWidth;
248
+ if (isMobile && isPortrait && this.playerWrapper) {
249
+ this.debugLog('Enabling Material You mobile layout');
250
+ this.playerWrapper.classList.add('uvf-material-you-mobile');
251
+ window.addEventListener('resize', () => {
252
+ const isNowPortrait = window.innerHeight > window.innerWidth;
253
+ if (this.playerWrapper) {
254
+ if (isMobile && isNowPortrait) {
255
+ this.playerWrapper.classList.add('uvf-material-you-mobile');
256
+ }
257
+ else {
258
+ this.playerWrapper.classList.remove('uvf-material-you-mobile');
259
+ }
260
+ }
261
+ });
262
+ }
233
263
  }
234
264
  setupVideoEventListeners() {
235
265
  if (!this.video)
@@ -307,6 +337,7 @@ export class WebPlayer extends BasePlayer {
307
337
  this.state.duration = this.video.duration || 0;
308
338
  this.debugLog('Metadata loaded - duration:', this.video.duration);
309
339
  this.updateTimeDisplay();
340
+ this.renderChapterMarkersOnProgressBar();
310
341
  this.emit('onLoadedMetadata', {
311
342
  duration: this.video.duration || 0,
312
343
  width: this.video.videoWidth || 0,
@@ -1876,6 +1907,8 @@ export class WebPlayer extends BasePlayer {
1876
1907
  --uvf-scrollbar-thumb-hover-start: rgba(255,0,0,0.5);
1877
1908
  --uvf-scrollbar-thumb-hover-end: rgba(255,0,0,0.6);
1878
1909
  --uvf-firefox-scrollbar-color: rgba(255,255,255,0.25);
1910
+ /* Material You surface tint */
1911
+ --uvf-surface-tint: rgba(255, 0, 0, 0.08);
1879
1912
  }
1880
1913
 
1881
1914
  /* Player focus styles for better UX */
@@ -3931,6 +3964,362 @@ export class WebPlayer extends BasePlayer {
3931
3964
  }
3932
3965
 
3933
3966
  /* Enhanced Responsive Media Queries with UX Best Practices */
3967
+
3968
+ /* Material You Mobile Portrait Layout - 25% Black + 50% Video + 25% Black */
3969
+ @media screen and (max-width: 767px) and (orientation: portrait) {
3970
+ /* Enable Material You mode with class flag */
3971
+ .uvf-player-wrapper.uvf-material-you-mobile {
3972
+ /* Full viewport height layout */
3973
+ height: 100vh;
3974
+ height: 100dvh;
3975
+ width: 100vw;
3976
+ position: fixed;
3977
+ top: 0;
3978
+ left: 0;
3979
+ display: flex;
3980
+ flex-direction: column;
3981
+ background: #000;
3982
+ overflow: hidden;
3983
+ }
3984
+
3985
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-video-container {
3986
+ /* Video occupies middle 50% */
3987
+ height: 50vh;
3988
+ height: 50dvh;
3989
+ width: 100vw;
3990
+ position: relative;
3991
+ margin-top: 25vh;
3992
+ margin-top: 25dvh;
3993
+ aspect-ratio: unset !important;
3994
+ /* Match existing theme background */
3995
+ background: radial-gradient(ellipse at center, #1a1a2e 0%, #000 100%);
3996
+ /* Material Design elevation */
3997
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
3998
+ 0 4px 16px rgba(0, 0, 0, 0.3),
3999
+ 0 2px 8px rgba(0, 0, 0, 0.2);
4000
+ }
4001
+
4002
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-video {
4003
+ width: 100%;
4004
+ height: 100%;
4005
+ object-fit: contain;
4006
+ }
4007
+
4008
+ /* Top black section (25%) - Tap zone overlay */
4009
+ .uvf-player-wrapper.uvf-material-you-mobile::before {
4010
+ content: '';
4011
+ position: absolute;
4012
+ top: 0;
4013
+ left: 0;
4014
+ width: 100vw;
4015
+ height: 25vh;
4016
+ height: 25dvh;
4017
+ background: #000;
4018
+ z-index: 1;
4019
+ pointer-events: all;
4020
+ touch-action: manipulation;
4021
+ }
4022
+
4023
+ /* Bottom black section (25%) - Surface container */
4024
+ .uvf-player-wrapper.uvf-material-you-mobile::after {
4025
+ content: '';
4026
+ position: absolute;
4027
+ bottom: 0;
4028
+ left: 0;
4029
+ width: 100vw;
4030
+ height: 25vh;
4031
+ height: 25dvh;
4032
+ background: linear-gradient(to top,
4033
+ #000 0%,
4034
+ rgba(0, 0, 0, 0.98) 20%,
4035
+ rgba(0, 0, 0, 0.95) 100%);
4036
+ z-index: 1;
4037
+ pointer-events: none;
4038
+ }
4039
+
4040
+ /* Material surface container for controls */
4041
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-controls-bar {
4042
+ position: absolute;
4043
+ bottom: 0;
4044
+ left: 0;
4045
+ right: 0;
4046
+ height: auto;
4047
+ max-height: 25vh;
4048
+ max-height: 25dvh;
4049
+ padding: 16px 20px;
4050
+ padding-bottom: calc(16px + var(--uvf-safe-area-bottom, 0px));
4051
+ background: transparent;
4052
+ z-index: 2;
4053
+ display: flex;
4054
+ flex-direction: column;
4055
+ justify-content: flex-end;
4056
+ /* Material Design surface with tint */
4057
+ backdrop-filter: blur(24px);
4058
+ -webkit-backdrop-filter: blur(24px);
4059
+ }
4060
+
4061
+ /* Material surface tint overlay */
4062
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-controls-bar::before {
4063
+ content: '';
4064
+ position: absolute;
4065
+ inset: 0;
4066
+ background: var(--uvf-surface-tint, rgba(255, 0, 0, 0.08));
4067
+ border-radius: 28px 28px 0 0;
4068
+ pointer-events: none;
4069
+ z-index: -1;
4070
+ }
4071
+
4072
+ /* Progress bar with chapter markers */
4073
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-section {
4074
+ margin-bottom: 12px;
4075
+ position: relative;
4076
+ }
4077
+
4078
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-bar-wrapper {
4079
+ padding: 12px 0;
4080
+ position: relative;
4081
+ }
4082
+
4083
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-bar {
4084
+ height: 4px;
4085
+ background: rgba(255, 255, 255, 0.2);
4086
+ border-radius: 4px;
4087
+ position: relative;
4088
+ overflow: visible;
4089
+ /* Material elevation */
4090
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
4091
+ }
4092
+
4093
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-filled {
4094
+ background: var(--uvf-accent-1, #ff0000);
4095
+ box-shadow: 0 0 8px var(--uvf-accent-1, #ff0000);
4096
+ }
4097
+
4098
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-handle {
4099
+ width: 20px;
4100
+ height: 20px;
4101
+ background: var(--uvf-accent-1, #ff0000);
4102
+ /* Material Design state layer */
4103
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3),
4104
+ 0 0 0 0 var(--uvf-accent-1, #ff0000);
4105
+ transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4106
+ }
4107
+
4108
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-progress-handle:active {
4109
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4),
4110
+ 0 0 0 12px rgba(255, 0, 0, 0.15);
4111
+ transform: translate(-50%, -50%) scale(1.2);
4112
+ }
4113
+
4114
+ /* Chapter markers on progress bar */
4115
+ .uvf-chapter-marker {
4116
+ position: absolute;
4117
+ top: 0;
4118
+ height: 100%;
4119
+ width: 3px;
4120
+ background: rgba(255, 255, 255, 0.4);
4121
+ border-radius: 2px;
4122
+ transform: translateX(-50%);
4123
+ pointer-events: none;
4124
+ z-index: 1;
4125
+ transition: all 0.2s ease;
4126
+ }
4127
+
4128
+ .uvf-chapter-marker.intro {
4129
+ background: #4CAF50;
4130
+ }
4131
+
4132
+ .uvf-chapter-marker.recap {
4133
+ background: #FFC107;
4134
+ }
4135
+
4136
+ .uvf-chapter-marker.credits {
4137
+ background: #9C27B0;
4138
+ }
4139
+
4140
+ .uvf-progress-bar-wrapper:hover .uvf-chapter-marker {
4141
+ width: 4px;
4142
+ opacity: 1;
4143
+ }
4144
+
4145
+ /* Material Design control buttons */
4146
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn {
4147
+ width: 48px;
4148
+ height: 48px;
4149
+ min-width: 48px;
4150
+ min-height: 48px;
4151
+ background: rgba(255, 255, 255, 0.12);
4152
+ border-radius: 24px;
4153
+ /* Material elevation level 1 */
4154
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12),
4155
+ 0 1px 2px rgba(0, 0, 0, 0.24);
4156
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
4157
+ position: relative;
4158
+ overflow: hidden;
4159
+ }
4160
+
4161
+ /* Material ripple effect */
4162
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn::before {
4163
+ content: '';
4164
+ position: absolute;
4165
+ inset: 0;
4166
+ background: rgba(255, 255, 255, 0.1);
4167
+ border-radius: inherit;
4168
+ opacity: 0;
4169
+ transition: opacity 0.2s ease;
4170
+ }
4171
+
4172
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn:active::before {
4173
+ opacity: 1;
4174
+ }
4175
+
4176
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn:active {
4177
+ /* Material elevation level 2 */
4178
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16),
4179
+ 0 3px 6px rgba(0, 0, 0, 0.23);
4180
+ transform: scale(0.95);
4181
+ }
4182
+
4183
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn.play-pause {
4184
+ width: 56px;
4185
+ height: 56px;
4186
+ min-width: 56px;
4187
+ min-height: 56px;
4188
+ border-radius: 28px;
4189
+ /* Material elevated button */
4190
+ background: linear-gradient(135deg,
4191
+ var(--uvf-accent-1, #ff0000),
4192
+ var(--uvf-accent-2, #ff4d4f));
4193
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2),
4194
+ 0 2px 4px rgba(0, 0, 0, 0.15),
4195
+ 0 0 0 0 var(--uvf-accent-1, #ff0000);
4196
+ }
4197
+
4198
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-control-btn.play-pause:active {
4199
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25),
4200
+ 0 4px 8px rgba(0, 0, 0, 0.20),
4201
+ 0 0 0 8px rgba(255, 0, 0, 0.12);
4202
+ }
4203
+
4204
+ /* Controls row with Material spacing */
4205
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-controls-row {
4206
+ gap: 16px;
4207
+ padding: 0;
4208
+ align-items: center;
4209
+ }
4210
+
4211
+ /* Time display with Material surface */
4212
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-time-display {
4213
+ background: rgba(255, 255, 255, 0.1);
4214
+ backdrop-filter: blur(8px);
4215
+ border-radius: 16px;
4216
+ padding: 6px 12px;
4217
+ font-size: 13px;
4218
+ font-weight: 500;
4219
+ font-feature-settings: 'tnum';
4220
+ /* Material elevation */
4221
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
4222
+ }
4223
+
4224
+ /* Double-tap overlay indicators */
4225
+ .uvf-doubletap-indicator {
4226
+ position: absolute;
4227
+ top: 50%;
4228
+ transform: translateY(-50%);
4229
+ width: 80px;
4230
+ height: 80px;
4231
+ background: rgba(255, 255, 255, 0.2);
4232
+ backdrop-filter: blur(10px);
4233
+ border-radius: 40px;
4234
+ display: flex;
4235
+ align-items: center;
4236
+ justify-content: center;
4237
+ pointer-events: none;
4238
+ opacity: 0;
4239
+ z-index: 100;
4240
+ transition: opacity 0.3s ease;
4241
+ /* Material elevation level 3 */
4242
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19),
4243
+ 0 6px 6px rgba(0, 0, 0, 0.23);
4244
+ }
4245
+
4246
+ .uvf-doubletap-indicator.left {
4247
+ left: 15%;
4248
+ }
4249
+
4250
+ .uvf-doubletap-indicator.right {
4251
+ right: 15%;
4252
+ }
4253
+
4254
+ .uvf-doubletap-indicator.active {
4255
+ opacity: 1;
4256
+ animation: doubletap-pulse 0.4s cubic-bezier(0.4, 0, 0.2, 1);
4257
+ }
4258
+
4259
+ @keyframes doubletap-pulse {
4260
+ 0% {
4261
+ transform: translateY(-50%) scale(0.8);
4262
+ opacity: 0;
4263
+ }
4264
+ 50% {
4265
+ transform: translateY(-50%) scale(1.1);
4266
+ opacity: 1;
4267
+ }
4268
+ 100% {
4269
+ transform: translateY(-50%) scale(1);
4270
+ opacity: 1;
4271
+ }
4272
+ }
4273
+
4274
+ .uvf-doubletap-indicator svg {
4275
+ width: 40px;
4276
+ height: 40px;
4277
+ fill: #fff;
4278
+ }
4279
+
4280
+ /* Long-press 2x speed indicator */
4281
+ .uvf-longpress-indicator {
4282
+ position: absolute;
4283
+ top: 50%;
4284
+ left: 50%;
4285
+ transform: translate(-50%, -50%);
4286
+ background: rgba(0, 0, 0, 0.8);
4287
+ backdrop-filter: blur(16px);
4288
+ padding: 16px 24px;
4289
+ border-radius: 24px;
4290
+ color: #fff;
4291
+ font-size: 18px;
4292
+ font-weight: 600;
4293
+ pointer-events: none;
4294
+ opacity: 0;
4295
+ z-index: 100;
4296
+ transition: opacity 0.2s ease;
4297
+ /* Material elevation level 4 */
4298
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
4299
+ 0 10px 10px rgba(0, 0, 0, 0.22);
4300
+ }
4301
+
4302
+ .uvf-longpress-indicator.active {
4303
+ opacity: 1;
4304
+ }
4305
+
4306
+ /* Hide desktop elements in Material You mode */
4307
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-top-controls,
4308
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-title-bar,
4309
+ .uvf-player-wrapper.uvf-material-you-mobile .uvf-volume-control {
4310
+ display: none !important;
4311
+ }
4312
+
4313
+ /* Optimize settings button for Material You */
4314
+ .uvf-player-wrapper.uvf-material-you-mobile #uvf-settings-btn {
4315
+ width: 48px !important;
4316
+ height: 48px !important;
4317
+ min-width: 48px !important;
4318
+ min-height: 48px !important;
4319
+ border-radius: 24px !important;
4320
+ }
4321
+ }
4322
+
3934
4323
  /* Mobile devices (portrait) - Enhanced UX with Safe Areas */
3935
4324
  @media screen and (max-width: 767px) and (orientation: portrait) {
3936
4325
  .uvf-responsive-container {
@@ -5225,6 +5614,21 @@ export class WebPlayer extends BasePlayer {
5225
5614
  shortcutIndicator.className = 'uvf-shortcut-indicator';
5226
5615
  shortcutIndicator.id = 'uvf-shortcut-indicator';
5227
5616
  container.appendChild(shortcutIndicator);
5617
+ const doubleTapLeft = document.createElement('div');
5618
+ doubleTapLeft.className = 'uvf-doubletap-indicator left';
5619
+ doubleTapLeft.id = 'uvf-doubletap-left';
5620
+ doubleTapLeft.innerHTML = '<svg viewBox="0 0 24 24"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/></svg><div style="margin-top:4px;font-size:14px;font-weight:600;">-10s</div>';
5621
+ container.appendChild(doubleTapLeft);
5622
+ const doubleTapRight = document.createElement('div');
5623
+ doubleTapRight.className = 'uvf-doubletap-indicator right';
5624
+ doubleTapRight.id = 'uvf-doubletap-right';
5625
+ doubleTapRight.innerHTML = '<svg viewBox="0 0 24 24"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg><div style="margin-top:4px;font-size:14px;font-weight:600;">+10s</div>';
5626
+ container.appendChild(doubleTapRight);
5627
+ const longPressIndicator = document.createElement('div');
5628
+ longPressIndicator.className = 'uvf-longpress-indicator';
5629
+ longPressIndicator.id = 'uvf-longpress-indicator';
5630
+ longPressIndicator.textContent = '2x';
5631
+ container.appendChild(longPressIndicator);
5228
5632
  const controlsBar = document.createElement('div');
5229
5633
  controlsBar.className = 'uvf-controls-bar';
5230
5634
  controlsBar.id = 'uvf-controls';
@@ -5457,7 +5861,7 @@ export class WebPlayer extends BasePlayer {
5457
5861
  });
5458
5862
  centerPlay?.addEventListener('click', () => this.togglePlayPause());
5459
5863
  playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
5460
- this.video.addEventListener('click', () => this.togglePlayPause());
5864
+ this.setupMaterialYouGestures(wrapper);
5461
5865
  this.video.addEventListener('play', () => {
5462
5866
  const playIcon = document.getElementById('uvf-play-icon');
5463
5867
  const pauseIcon = document.getElementById('uvf-pause-icon');
@@ -6258,6 +6662,187 @@ export class WebPlayer extends BasePlayer {
6258
6662
  this.mute();
6259
6663
  }
6260
6664
  }
6665
+ setupMaterialYouGestures(wrapper) {
6666
+ if (!wrapper || !this.video)
6667
+ return;
6668
+ const doubleTapLeft = document.getElementById('uvf-doubletap-left');
6669
+ const doubleTapRight = document.getElementById('uvf-doubletap-right');
6670
+ const longPressIndicator = document.getElementById('uvf-longpress-indicator');
6671
+ const videoElement = this.video;
6672
+ const videoContainer = wrapper.querySelector('.uvf-video-container');
6673
+ if (!videoContainer)
6674
+ return;
6675
+ videoContainer.addEventListener('touchstart', (e) => {
6676
+ const touchEvent = e;
6677
+ const touch = touchEvent.touches[0];
6678
+ if (!touch || !videoElement)
6679
+ return;
6680
+ this.longPressStartTime = Date.now();
6681
+ this.longPressTimer = setTimeout(() => {
6682
+ this.longPressActive = true;
6683
+ this.originalPlaybackRate = videoElement.playbackRate;
6684
+ videoElement.playbackRate = 2.0;
6685
+ if (longPressIndicator) {
6686
+ longPressIndicator.classList.add('active');
6687
+ }
6688
+ }, 500);
6689
+ }, { passive: true });
6690
+ videoContainer.addEventListener('touchend', (e) => {
6691
+ const touchEvent = e;
6692
+ const touch = touchEvent.changedTouches[0];
6693
+ if (!touch || !videoElement)
6694
+ return;
6695
+ if (this.longPressTimer) {
6696
+ clearTimeout(this.longPressTimer);
6697
+ this.longPressTimer = null;
6698
+ }
6699
+ if (this.longPressActive) {
6700
+ videoElement.playbackRate = this.originalPlaybackRate;
6701
+ this.longPressActive = false;
6702
+ if (longPressIndicator) {
6703
+ longPressIndicator.classList.remove('active');
6704
+ }
6705
+ return;
6706
+ }
6707
+ const now = Date.now();
6708
+ const timeSinceLastTap = now - this.lastTapTime;
6709
+ if (timeSinceLastTap < 300) {
6710
+ if (this.tapTimeout) {
6711
+ clearTimeout(this.tapTimeout);
6712
+ this.tapTimeout = null;
6713
+ }
6714
+ const rect = videoContainer.getBoundingClientRect();
6715
+ const x = touch.clientX - rect.left;
6716
+ const isLeftSide = x < rect.width / 2;
6717
+ if (isLeftSide) {
6718
+ this.seek(videoElement.currentTime - 10);
6719
+ if (doubleTapLeft) {
6720
+ doubleTapLeft.classList.add('active');
6721
+ setTimeout(() => {
6722
+ doubleTapLeft.classList.remove('active');
6723
+ }, 400);
6724
+ }
6725
+ }
6726
+ else {
6727
+ this.seek(videoElement.currentTime + 10);
6728
+ if (doubleTapRight) {
6729
+ doubleTapRight.classList.add('active');
6730
+ setTimeout(() => {
6731
+ doubleTapRight.classList.remove('active');
6732
+ }, 400);
6733
+ }
6734
+ }
6735
+ this.tapCount = 0;
6736
+ this.lastTapTime = 0;
6737
+ }
6738
+ else {
6739
+ this.tapCount = 1;
6740
+ this.lastTapTime = now;
6741
+ this.tapTimeout = setTimeout(() => {
6742
+ if (this.tapCount === 1) {
6743
+ this.toggleControls();
6744
+ }
6745
+ this.tapCount = 0;
6746
+ }, 300);
6747
+ }
6748
+ }, { passive: true });
6749
+ videoContainer.addEventListener('touchmove', () => {
6750
+ if (this.longPressTimer) {
6751
+ clearTimeout(this.longPressTimer);
6752
+ this.longPressTimer = null;
6753
+ }
6754
+ }, { passive: true });
6755
+ videoContainer.addEventListener('touchcancel', () => {
6756
+ if (this.longPressTimer) {
6757
+ clearTimeout(this.longPressTimer);
6758
+ this.longPressTimer = null;
6759
+ }
6760
+ if (this.longPressActive && videoElement) {
6761
+ videoElement.playbackRate = this.originalPlaybackRate;
6762
+ this.longPressActive = false;
6763
+ if (longPressIndicator) {
6764
+ longPressIndicator.classList.remove('active');
6765
+ }
6766
+ }
6767
+ }, { passive: true });
6768
+ }
6769
+ toggleControls() {
6770
+ const wrapper = this.container?.querySelector('.uvf-player-wrapper');
6771
+ if (wrapper) {
6772
+ if (wrapper.classList.contains('controls-visible')) {
6773
+ this.hideControls();
6774
+ }
6775
+ else {
6776
+ this.showControls();
6777
+ if (this.state.isPlaying) {
6778
+ this.scheduleHideControls();
6779
+ }
6780
+ }
6781
+ }
6782
+ }
6783
+ applyDynamicTheming(primaryColor) {
6784
+ if (!this.playerWrapper)
6785
+ return;
6786
+ const color = primaryColor || this.dominantColor;
6787
+ const rgb = this.hexToRgb(color);
6788
+ if (rgb) {
6789
+ this.playerWrapper.style.setProperty('--uvf-accent-1', color);
6790
+ this.playerWrapper.style.setProperty('--uvf-accent-2', this.lightenColor(color, 10));
6791
+ this.playerWrapper.style.setProperty('--uvf-surface-tint', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.08)`);
6792
+ this.debugLog(`Applied dynamic theming with color: ${color}`);
6793
+ }
6794
+ }
6795
+ hexToRgb(hex) {
6796
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
6797
+ return result ? {
6798
+ r: parseInt(result[1], 16),
6799
+ g: parseInt(result[2], 16),
6800
+ b: parseInt(result[3], 16)
6801
+ } : null;
6802
+ }
6803
+ lightenColor(hex, percent) {
6804
+ const rgb = this.hexToRgb(hex);
6805
+ if (!rgb)
6806
+ return hex;
6807
+ const amount = Math.floor(255 * (percent / 100));
6808
+ const r = Math.min(255, rgb.r + amount);
6809
+ const g = Math.min(255, rgb.g + amount);
6810
+ const b = Math.min(255, rgb.b + amount);
6811
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
6812
+ }
6813
+ renderChapterMarkersOnProgressBar() {
6814
+ if (!this.video || !this.chapterConfig.enabled || !this.chapterConfig.showChapterMarkers) {
6815
+ return;
6816
+ }
6817
+ const progressBar = document.querySelector('.uvf-progress-bar');
6818
+ if (!progressBar)
6819
+ return;
6820
+ const existingMarkers = progressBar.querySelectorAll('.uvf-chapter-marker');
6821
+ existingMarkers.forEach(marker => marker.remove());
6822
+ const chapters = this.chapterConfig.data;
6823
+ if (!chapters || !Array.isArray(chapters) || chapters.length === 0) {
6824
+ return;
6825
+ }
6826
+ const duration = this.video.duration;
6827
+ if (!duration || duration === 0)
6828
+ return;
6829
+ chapters.forEach((chapter) => {
6830
+ if (!chapter.startTime && chapter.startTime !== 0)
6831
+ return;
6832
+ const marker = document.createElement('div');
6833
+ marker.className = 'uvf-chapter-marker';
6834
+ if (chapter.type) {
6835
+ marker.classList.add(chapter.type.toLowerCase());
6836
+ }
6837
+ const percent = (chapter.startTime / duration) * 100;
6838
+ marker.style.left = `${percent}%`;
6839
+ if (chapter.title) {
6840
+ marker.setAttribute('title', chapter.title);
6841
+ }
6842
+ progressBar.appendChild(marker);
6843
+ });
6844
+ this.debugLog(`Rendered ${chapters.length} chapter markers on progress bar`);
6845
+ }
6261
6846
  isMobileDevice() {
6262
6847
  const userAgent = navigator.userAgent.toLowerCase();
6263
6848
  const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone', 'mobile'];