@streamscloud/embeddable 6.0.1 → 6.0.2

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.
@@ -1 +1 @@
1
- export const runningInBrowser = () => !import.meta.env.SSR;
1
+ export const runningInBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -17,9 +17,9 @@ import { MediaCenterLocalization } from './media-center-localization';
17
17
  import { default as Overview } from './overview.svelte';
18
18
  import { makeShortVideosProvider } from './short-video-resources-generator';
19
19
  import { MediaCenterMode } from './types';
20
- import IconTextColumnThree from '@fluentui/svg-icons/icons/text_column_three_20_regular.svg?raw';
21
20
  import IconLineHorizontal3 from '@fluentui/svg-icons/icons/line_horizontal_3_20_regular.svg?raw';
22
- import { onMount } from 'svelte';
21
+ import IconTextColumnThree from '@fluentui/svg-icons/icons/text_column_three_20_regular.svg?raw';
22
+ import { onDestroy, onMount } from 'svelte';
23
23
  import { fade } from 'svelte/transition';
24
24
  let { dataProvider, playerProps, localization: localizationInit = 'en' } = $props();
25
25
  const localization = $derived(new MediaCenterLocalization(localizationInit));
@@ -32,6 +32,7 @@ let headerHeight = $state(0);
32
32
  let shortVideoProps = $state.raw(playerProps.type === MediaCenterMode.ShortVideos ? playerProps.props : null);
33
33
  let streamProps = $state.raw(playerProps.type === MediaCenterMode.Stream ? playerProps.props : null);
34
34
  let overviewData = $state.raw(null);
35
+ let scrollResizeObserver = null;
35
36
  const categories = $derived.by(() => {
36
37
  if (!mediaCenterConfig) {
37
38
  return [];
@@ -60,7 +61,13 @@ onMount(() => __awaiter(void 0, void 0, void 0, function* () {
60
61
  mediaDataLoading = false;
61
62
  }
62
63
  }
64
+ scrollResizeObserver = new ResizeObserver(() => {
65
+ updateScrollShadows();
66
+ });
63
67
  }));
68
+ onDestroy(() => {
69
+ scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.disconnect();
70
+ });
64
71
  const selectCategory = (categoryId) => {
65
72
  if (!dataProvider) {
66
73
  return;
@@ -145,8 +152,7 @@ let scrollRef = null;
145
152
  let scrollHasLeft = $state(false);
146
153
  let scrollHasRight = $state(false);
147
154
  const mounted = (node, callback) => {
148
- const scrollResizeObserver = new ResizeObserver(updateScrollShadows);
149
- scrollResizeObserver.observe(node);
155
+ scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.observe(node);
150
156
  const heightResizeObserver = new ResizeObserver(() => {
151
157
  headerHeight = node.clientHeight;
152
158
  callback({ height: headerHeight });
@@ -154,7 +160,6 @@ const mounted = (node, callback) => {
154
160
  heightResizeObserver.observe(node);
155
161
  return {
156
162
  destroy: () => {
157
- scrollResizeObserver.disconnect();
158
163
  heightResizeObserver.disconnect();
159
164
  }
160
165
  };
@@ -169,9 +174,7 @@ const updateScrollShadows = () => {
169
174
  };
170
175
  const onScrollMounted = (node) => {
171
176
  scrollRef = node;
172
- requestAnimationFrame(() => {
173
- updateScrollShadows();
174
- });
177
+ scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.observe(node);
175
178
  };
176
179
  </script>
177
180
 
@@ -181,9 +184,6 @@ const onScrollMounted = (node) => {
181
184
  {#snippet categoriesSwitcher(data: { maxItemsWidth: Number; onMounted: (data: { height: Number }) => void })}
182
185
  <div class="media-center" use:mounted={data.onMounted}>
183
186
  <div class="media-center__row" style={`max-width: ${data.maxItemsWidth}px;`}>
184
- <button type="button" class="media-center__overview-button" onclick={toggleOverview}>
185
- <Icon src={IconTextColumnThree} />
186
- </button>
187
187
  <div
188
188
  class="media-center__scroll"
189
189
  class:media-center__scroll--has-left={scrollHasLeft}
@@ -191,16 +191,17 @@ const onScrollMounted = (node) => {
191
191
  class:media-center__scroll--has-both={scrollHasRight && scrollHasLeft}
192
192
  use:onScrollMounted
193
193
  onscroll={updateScrollShadows}>
194
- <div class="media-center__items">
195
- {#each categories as category (category.id)}
196
- <button
197
- type="button"
198
- class="media-center__category-button"
199
- class:media-center__category-button--active={selectedCategoryId === category.id}
200
- title={category.name}
201
- onclick={() => selectCategory(category.id)}>{category.name}</button>
202
- {/each}
203
- </div>
194
+ <button type="button" class="media-center__overview-button" onclick={toggleOverview}>
195
+ <Icon src={IconTextColumnThree} />
196
+ </button>
197
+ {#each categories as category (category.id)}
198
+ <button
199
+ type="button"
200
+ class="media-center__category-button"
201
+ class:media-center__category-button--active={selectedCategoryId === category.id}
202
+ title={category.name}
203
+ onclick={() => selectCategory(category.id)}>{category.name}</button>
204
+ {/each}
204
205
  </div>
205
206
  </div>
206
207
  <div class="media-center__overview-dropdown">
@@ -276,6 +277,7 @@ const onScrollMounted = (node) => {
276
277
  width: 100%;
277
278
  display: flex;
278
279
  align-items: center;
280
+ justify-content: center;
279
281
  gap: 0.75rem;
280
282
  /* Set 'container-type: inline-size;' to reference container*/
281
283
  }
@@ -288,12 +290,16 @@ const onScrollMounted = (node) => {
288
290
  pointer-events: auto;
289
291
  position: relative;
290
292
  flex: 1 1 auto;
293
+ max-width: max-content;
291
294
  min-width: 0;
292
295
  overflow-x: auto;
293
296
  overflow-y: hidden;
294
297
  -webkit-overflow-scrolling: touch;
295
298
  scrollbar-width: none;
296
299
  display: flex;
300
+ align-items: center;
301
+ gap: 0.75rem;
302
+ flex-wrap: nowrap;
297
303
  mask-image: none;
298
304
  }
299
305
  .media-center__scroll::-webkit-scrollbar {
@@ -308,14 +314,6 @@ const onScrollMounted = (node) => {
308
314
  .media-center__scroll--has-both {
309
315
  mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, rgb(0, 0, 0) 32px, rgb(0, 0, 0) calc(100% - 32px), rgba(0, 0, 0, 0) 100%);
310
316
  }
311
- .media-center__items {
312
- display: inline-flex;
313
- align-items: center;
314
- gap: 0.75rem;
315
- flex-wrap: nowrap;
316
- pointer-events: none;
317
- padding-inline: 0.25rem;
318
- }
319
317
  .media-center__overview-button {
320
318
  pointer-events: auto;
321
319
  padding: 0.375rem 0.75rem;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">import { runningInBrowser } from '../../core/browser';
2
- import { isIgnored } from './dropdown-ignore';
3
2
  import { Icon } from '../icon';
3
+ import { isIgnored } from './dropdown-ignore';
4
4
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
5
5
  import { createPopper } from '@popperjs/core';
6
6
  import { onDestroy } from 'svelte';
@@ -1,6 +1,5 @@
1
- <script lang="ts">import { isScrollingPrevented } from './prevent-slider-scroll';
1
+ <script lang="ts">import { createWheelPeakDetector } from './wheel-peak-detector';
2
2
  import { onDestroy, onMount, untrack } from 'svelte';
3
- const MOUSE_DETECTION_THRESHOLD_MS = 100;
4
3
  let { buffer, on, children } = $props();
5
4
  let slidesRef;
6
5
  let sliderHeight = $state(0);
@@ -90,89 +89,6 @@ onMount(() => {
90
89
  }
91
90
  reset();
92
91
  });
93
- let waveDetector = {
94
- events: [],
95
- direction: 0,
96
- peakReached: false,
97
- lastPeak: 0,
98
- waveStartTime: 0
99
- };
100
- let isAnimatingWheel = false;
101
- const triggerAnimation = (direction) => {
102
- isAnimatingWheel = true;
103
- if (direction > 0 && buffer.canLoadNext) {
104
- buffer.loadNext();
105
- }
106
- else if (direction < 0 && buffer.canLoadPrevious) {
107
- buffer.loadPrevious();
108
- }
109
- setTimeout(() => {
110
- isAnimatingWheel = false;
111
- }, buffer.animationDuration + 100);
112
- };
113
- slidesRef.addEventListener('wheel', (e) => {
114
- e.preventDefault();
115
- const checkCanHandleWheel = (node) => {
116
- while (node && node !== slidesRef) {
117
- if (isScrollingPrevented(node)) {
118
- return false;
119
- }
120
- node = node.parentElement;
121
- }
122
- return true;
123
- };
124
- if (!checkCanHandleWheel(e.target)) {
125
- return;
126
- }
127
- const now = Date.now();
128
- const absDelta = Math.abs(e.deltaY);
129
- const direction = Math.sign(e.deltaY);
130
- // Mouse - large stable values, trigger immediately
131
- if (absDelta >= 10 && !isAnimatingWheel) {
132
- const lastEvent = waveDetector.events[waveDetector.events.length - 1];
133
- const timeSinceLastEvent = lastEvent ? now - lastEvent.time : 1000;
134
- // If enough time has passed since the last event - it's a mouse
135
- if (timeSinceLastEvent > MOUSE_DETECTION_THRESHOLD_MS) {
136
- triggerAnimation(direction);
137
- waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
138
- return;
139
- }
140
- }
141
- // Touchpad - small, variable values, need to analyze the wave
142
- // New wave if: direction changed or a lot of time has passed
143
- if (direction !== waveDetector.direction || (waveDetector.waveStartTime && now - waveDetector.waveStartTime > 1500)) {
144
- // Finalize the previous wave if it existed
145
- if (waveDetector.peakReached && !isAnimatingWheel) {
146
- triggerAnimation(waveDetector.direction);
147
- }
148
- // Start a new wave
149
- waveDetector = {
150
- events: [{ delta: absDelta, time: now }],
151
- direction: direction,
152
- peakReached: false,
153
- lastPeak: absDelta,
154
- waveStartTime: now
155
- };
156
- return;
157
- }
158
- // Continue the current wave
159
- waveDetector.events.push({ delta: absDelta, time: now });
160
- // Determine the phase of the wave
161
- if (absDelta > waveDetector.lastPeak) {
162
- // The wave is growing
163
- waveDetector.lastPeak = absDelta;
164
- waveDetector.peakReached = absDelta >= 5; // The minimum peak to consider a valid wave
165
- }
166
- else if (absDelta < waveDetector.lastPeak * 0.5) {
167
- // The wave has dropped significantly - consider the gesture complete
168
- if (waveDetector.peakReached && !isAnimatingWheel) {
169
- triggerAnimation(waveDetector.direction);
170
- waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
171
- }
172
- }
173
- // Cleanup old events
174
- waveDetector.events = waveDetector.events.filter((evt) => now - evt.time < 2000);
175
- });
176
92
  slidesRef.addEventListener('transitionend', (e) => {
177
93
  if (e.target !== slidesRef) {
178
94
  return;
@@ -199,10 +115,24 @@ const styles = $derived.by(() => {
199
115
  ];
200
116
  return values.join(';');
201
117
  });
118
+ const peakDetectorCallbacks = {
119
+ canLoadNext: () => buffer.canLoadNext,
120
+ canLoadPrevious: () => buffer.canLoadPrevious,
121
+ onTrigger: (direction) => {
122
+ // direction: 1 -> next, -1 -> previous
123
+ if (direction > 0) {
124
+ buffer.loadNext();
125
+ }
126
+ else if (direction < 0) {
127
+ buffer.loadPrevious();
128
+ }
129
+ },
130
+ getAnimationDurationMs: () => buffer.animationDuration
131
+ };
202
132
  </script>
203
133
 
204
134
  <div class="player-slider">
205
- <div class="player-slider__slides" bind:this={slidesRef} style={styles}>
135
+ <div class="player-slider__slides" bind:this={slidesRef} use:createWheelPeakDetector={{ cbs: peakDetectorCallbacks }} style={styles}>
206
136
  {#each buffer.loaded as item, index (item)}
207
137
  <div class="player-slider__slide">
208
138
  {#if index >= activeIndex - 1 && index <= activeIndex + 1}
@@ -0,0 +1,22 @@
1
+ export type PeakDetectorConfig = {
2
+ mouseGapMs?: number;
3
+ mouseDeltaThreshold?: number;
4
+ interEventTimeout?: number;
5
+ minPeakToTrigger?: number;
6
+ waveMaxAgeMs?: number;
7
+ directionFlipEndsWave?: boolean;
8
+ animationCooldownMs?: number;
9
+ directionChangeMinAbsDelta?: number;
10
+ };
11
+ export type PeakDetectorCallbacks = {
12
+ canLoadNext: () => boolean;
13
+ canLoadPrevious: () => boolean;
14
+ onTrigger: (direction: number) => void;
15
+ getAnimationDurationMs: () => number;
16
+ };
17
+ export declare const createWheelPeakDetector: (target: HTMLElement, params: {
18
+ cbs: PeakDetectorCallbacks;
19
+ cfg?: PeakDetectorConfig;
20
+ }) => {
21
+ destroy(): void;
22
+ };
@@ -0,0 +1,157 @@
1
+ import { isScrollingPrevented } from './prevent-slider-scroll';
2
+ export const createWheelPeakDetector = (target, params) => {
3
+ const { cbs, cfg } = params;
4
+ // Defaults tuned for smooth touchpads; adjust in caller if needed.
5
+ const config = {
6
+ mouseGapMs: cfg?.mouseGapMs ?? 120,
7
+ mouseDeltaThreshold: cfg?.mouseDeltaThreshold ?? 12,
8
+ interEventTimeout: cfg?.interEventTimeout ?? 180,
9
+ minPeakToTrigger: cfg?.minPeakToTrigger ?? 6,
10
+ waveMaxAgeMs: cfg?.waveMaxAgeMs ?? 1800,
11
+ directionFlipEndsWave: cfg?.directionFlipEndsWave ?? true,
12
+ animationCooldownMs: cfg?.animationCooldownMs ?? 100, // extra after CSS transition
13
+ directionChangeMinAbsDelta: cfg?.directionChangeMinAbsDelta ?? 2 // ignore tiny opposite spikes
14
+ };
15
+ let wave = null;
16
+ let lastWheelTime = 0;
17
+ let lastDelta = 0;
18
+ let isAnimating = false;
19
+ let cooldownTimer = null;
20
+ const clearWave = () => {
21
+ wave = null;
22
+ };
23
+ const setAnimatingWithCooldown = () => {
24
+ isAnimating = true;
25
+ const total = cbs.getAnimationDurationMs() + config.animationCooldownMs;
26
+ if (cooldownTimer) {
27
+ clearTimeout(cooldownTimer);
28
+ }
29
+ cooldownTimer = window.setTimeout(() => {
30
+ isAnimating = false;
31
+ cooldownTimer = null;
32
+ }, total);
33
+ };
34
+ const trigger = (direction) => {
35
+ if (direction > 0 && !cbs.canLoadNext()) {
36
+ return;
37
+ }
38
+ if (direction < 0 && !cbs.canLoadPrevious()) {
39
+ return;
40
+ }
41
+ setAnimatingWithCooldown();
42
+ cbs.onTrigger(direction);
43
+ };
44
+ const canHandle = (node) => {
45
+ while (node && node !== target) {
46
+ if (isScrollingPrevented(node)) {
47
+ return false;
48
+ }
49
+ node = node.parentElement;
50
+ }
51
+ return true;
52
+ };
53
+ const finalizeWaveWithTrigger = () => {
54
+ if (wave && !isAnimating) {
55
+ if (wave.maxAbsDelta >= config.minPeakToTrigger) {
56
+ trigger(wave.direction);
57
+ }
58
+ }
59
+ clearWave();
60
+ };
61
+ const finalizeWaveSilently = () => {
62
+ // Finish the wave without triggering (used on direction flip to avoid ghost slide)
63
+ clearWave();
64
+ };
65
+ const onWheel = (e) => {
66
+ e.preventDefault();
67
+ if (!canHandle(e.target)) {
68
+ return;
69
+ }
70
+ const now = Date.now();
71
+ const dy = e.deltaY;
72
+ const absDelta = Math.abs(dy);
73
+ console.warn(absDelta);
74
+ const dir = Math.sign(dy);
75
+ // Mouse branch: big, sparse deltas
76
+ const timeSinceLast = now - lastWheelTime;
77
+ if (absDelta >= config.mouseDeltaThreshold && timeSinceLast > config.mouseGapMs && !isAnimating) {
78
+ trigger(dir);
79
+ clearWave();
80
+ lastWheelTime = now;
81
+ lastDelta = dy;
82
+ return;
83
+ }
84
+ // Determine direction change with hysteresis to avoid micro flips
85
+ const directionChanged = !!wave && dir !== 0 && dir !== wave.direction && absDelta >= config.directionChangeMinAbsDelta;
86
+ const inactiveTooLong = wave && now - wave?.lastEventTime > config.interEventTimeout;
87
+ const waveTooOld = wave && now - wave?.startTime > config.waveMaxAgeMs;
88
+ // If direction flip should end the wave, do it silently (no trigger of the old direction)
89
+ if (wave && config.directionFlipEndsWave && directionChanged) {
90
+ finalizeWaveSilently();
91
+ // Start a new wave immediately in the new direction
92
+ wave = {
93
+ startTime: now,
94
+ lastEventTime: now,
95
+ direction: dir,
96
+ maxAbsDelta: absDelta,
97
+ sumAbsDelta: absDelta,
98
+ events: 1
99
+ };
100
+ lastWheelTime = now;
101
+ lastDelta = dy;
102
+ return;
103
+ }
104
+ // Finalize by inactivity or age (these DO trigger)
105
+ if (inactiveTooLong || waveTooOld) {
106
+ finalizeWaveWithTrigger();
107
+ }
108
+ // Start condition for new wave:
109
+ // - no wave
110
+ // - current amplitude grows vs previous
111
+ // - long gap since last wheel
112
+ const growing = absDelta > Math.abs(lastDelta);
113
+ const longGap = timeSinceLast > config.interEventTimeout;
114
+ if (!wave || growing || longGap) {
115
+ // Opportunistic finalize only if not animating and not caused by a flip
116
+ // (flip branch handled earlier and is silent)
117
+ if (wave && !isAnimating) {
118
+ if (wave.maxAbsDelta >= config.minPeakToTrigger) {
119
+ trigger(wave.direction);
120
+ }
121
+ }
122
+ wave = {
123
+ startTime: now,
124
+ lastEventTime: now,
125
+ direction: dir || (wave?.direction ?? 0),
126
+ maxAbsDelta: absDelta,
127
+ sumAbsDelta: absDelta,
128
+ events: 1
129
+ };
130
+ lastWheelTime = now;
131
+ lastDelta = dy;
132
+ return;
133
+ }
134
+ // Continue current wave
135
+ if (wave) {
136
+ wave.lastEventTime = now;
137
+ wave.events += 1;
138
+ wave.sumAbsDelta += absDelta;
139
+ if (absDelta > wave.maxAbsDelta) {
140
+ wave.maxAbsDelta = absDelta; // peak update
141
+ }
142
+ }
143
+ lastWheelTime = now;
144
+ lastDelta = dy;
145
+ };
146
+ target.addEventListener('wheel', onWheel, { passive: false });
147
+ return {
148
+ destroy() {
149
+ target.removeEventListener('wheel', onWheel);
150
+ if (cooldownTimer) {
151
+ clearTimeout(cooldownTimer);
152
+ cooldownTimer = null;
153
+ }
154
+ wave = null;
155
+ }
156
+ };
157
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/embeddable",
3
- "version": "6.0.1",
3
+ "version": "6.0.2",
4
4
  "author": "StreamsCloud",
5
5
  "repository": "https://github.com/StreamsCloud/streamscloud-frontend-packages.git",
6
6
  "type": "module",