@streamscloud/embeddable 4.0.0 → 5.1.0
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.
- package/dist/ads/ad-card/cmp.ad-card.svelte +34 -7
- package/dist/ads/ad-card/cmp.ad-card.svelte.d.ts +1 -1
- package/dist/ads/ad-card/mapper.js +1 -0
- package/dist/ads/ad-card/operations.generated.d.ts +1 -0
- package/dist/ads/ad-card/operations.generated.js +1 -0
- package/dist/ads/ad-card/operations.graphql +1 -0
- package/dist/ads/ad-card/types.d.ts +1 -0
- package/dist/products/product-card/cmp.product-card.svelte +3 -2
- package/dist/short-videos/short-video-viewer/cmp.short-video-controls.svelte +24 -13
- package/dist/short-videos/short-video-viewer/cmp.short-video-controls.svelte.d.ts +2 -2
- package/dist/short-videos/short-video-viewer/cmp.short-video-viewer.svelte +4 -3
- package/dist/short-videos/short-video-viewer/cmp.short-video-viewer.svelte.d.ts +3 -2
- package/dist/short-videos/short-video-viewer/index.d.ts +2 -2
- package/dist/short-videos/short-video-viewer/index.js +1 -1
- package/dist/short-videos/short-video-viewer/mapper.d.ts +1 -1
- package/dist/short-videos/short-video-viewer/mapper.js +38 -5
- package/dist/short-videos/short-video-viewer/operations.generated.d.ts +1 -0
- package/dist/short-videos/short-video-viewer/operations.generated.js +87 -98
- package/dist/short-videos/short-video-viewer/operations.graphql +40 -2
- package/dist/short-videos/short-video-viewer/types.d.ts +35 -6
- package/dist/short-videos/short-video-viewer/types.js +1 -1
- package/dist/short-videos/short-video-viewer/ui-manager.svelte.js +2 -2
- package/dist/short-videos/short-videos-player/cmp.short-videos-player.svelte +3 -1
- package/dist/short-videos/short-videos-player/cmp.short-videos-player.svelte.d.ts +4 -0
- package/dist/short-videos/short-videos-player/controls.svelte +37 -20
- package/dist/short-videos/short-videos-player/controls.svelte.d.ts +2 -1
- package/dist/short-videos/short-videos-player/index.js +6 -0
- package/dist/short-videos/short-videos-player/operations.generated.d.ts +1 -0
- package/dist/short-videos/short-videos-player/operations.generated.js +73 -84
- package/dist/short-videos/short-videos-player/short-videos-player-view.svelte +19 -6
- package/dist/short-videos/short-videos-player/short-videos-player-view.svelte.d.ts +4 -0
- package/dist/short-videos/short-videos-player/types.d.ts +9 -9
- package/dist/short-videos/short-videos-player/ui-manager.svelte.d.ts +1 -0
- package/dist/short-videos/short-videos-player/ui-manager.svelte.js +2 -1
- package/dist/streams/layout/models/mapper.js +2 -1
- package/dist/streams/layout/models/stream-layout-short-video-model.d.ts +1 -0
- package/dist/streams/stream-player/cmp.stream-player.svelte +6 -4
- package/dist/streams/stream-player/cmp.stream-player.svelte.d.ts +2 -0
- package/dist/streams/stream-player/controls.svelte +25 -5
- package/dist/streams/stream-player/controls.svelte.d.ts +2 -0
- package/dist/streams/stream-player/index.d.ts +2 -0
- package/dist/streams/stream-player/index.js +2 -1
- package/dist/streams/stream-player/mapper.d.ts +1 -1
- package/dist/streams/stream-player/mapper.js +1 -1
- package/dist/streams/stream-player/ui-manager.svelte.d.ts +1 -0
- package/dist/streams/stream-player/ui-manager.svelte.js +2 -1
- package/dist/ui/line-clamp/cmp.line-clamp.svelte +35 -13
- package/dist/ui/player/cmp.player-slider.svelte +75 -9
- package/dist/ui/progress/cmp.progress.svelte +4 -1
- package/dist/ui/seek-bar/cmp.seek-bar.svelte +134 -28
- package/dist/ui/seek-bar/cmp.seek-bar.svelte.d.ts +3 -0
- package/dist/ui/video/cmp.video.svelte +40 -42
- package/dist/ui/video/cmp.video.svelte.d.ts +2 -0
- package/dist/ui/video/index.d.ts +1 -0
- package/dist/ui/video/index.js +1 -0
- package/dist/ui/video/types.d.ts +4 -0
- package/dist/ui/video/types.js +5 -0
- package/package.json +5 -1
|
@@ -19,7 +19,7 @@ import { PlayerSlider } from '../../ui/player';
|
|
|
19
19
|
import { SpotlightLayout } from '../../ui/spotlight-layout';
|
|
20
20
|
import { SwipeIndicator } from '../../ui/swipe-indicator';
|
|
21
21
|
import { default as Controls } from './controls.svelte';
|
|
22
|
-
import {
|
|
22
|
+
import { mapToStreamPlayerModel } from './mapper';
|
|
23
23
|
import { GetStreamDocument } from './operations.generated';
|
|
24
24
|
import { default as Overview } from './stream-overview.svelte';
|
|
25
25
|
import { StreamPlayerBuffer } from './stream-player-buffer.svelte';
|
|
@@ -27,7 +27,7 @@ import { StreamPlayerLocalization } from './stream-player-localization';
|
|
|
27
27
|
import { StreamPlayerUiManager } from './ui-manager.svelte';
|
|
28
28
|
import { AppEventsTracker } from '@streamscloud/streams-analytics-collector';
|
|
29
29
|
import { onMount } from 'svelte';
|
|
30
|
-
let { streamId, graphqlOrigin, localization: localizationInit = 'en', showStreamsCloudWatermark, initiator, on } = $props();
|
|
30
|
+
let { streamId, graphqlOrigin, localization: localizationInit = 'en', showStreamsCloudWatermark, shortVideoSocialInteractionsHandler, initiator, on } = $props();
|
|
31
31
|
const localization = $derived(new StreamPlayerLocalization(localizationInit));
|
|
32
32
|
let model = $state(null);
|
|
33
33
|
let buffer = $state.raw(null);
|
|
@@ -72,7 +72,7 @@ onMount(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
72
72
|
image: ((_d = streamPayload.data.stream.cover) === null || _d === void 0 ? void 0 : _d.url) || null
|
|
73
73
|
});
|
|
74
74
|
// start tracking the stream
|
|
75
|
-
model =
|
|
75
|
+
model = mapToStreamPlayerModel(streamPayload.data.stream);
|
|
76
76
|
buffer = new StreamPlayerBuffer({ graphql, streamId });
|
|
77
77
|
AppEventsTracker.trackStreamView(streamPayload.data.stream.id);
|
|
78
78
|
startActivityTracking();
|
|
@@ -211,6 +211,7 @@ const onProgress = (pageId, videoId, progress) => {
|
|
|
211
211
|
progress: (progress) => onProgress(item.id, item.shortVideo.id, progress)
|
|
212
212
|
}}
|
|
213
213
|
autoplay="on-appearance"
|
|
214
|
+
socialInteractionsHandler={shortVideoSocialInteractionsHandler}
|
|
214
215
|
localization={localization.shortVideoViewerLocalization}
|
|
215
216
|
showAttachments={uiManager.showShortVideoOverlay}
|
|
216
217
|
showControls={uiManager.showShortVideoOverlay} />
|
|
@@ -238,6 +239,7 @@ const onProgress = (pageId, videoId, progress) => {
|
|
|
238
239
|
buffer={buffer}
|
|
239
240
|
uiManager={uiManager}
|
|
240
241
|
localization={localization}
|
|
242
|
+
shortVideoSocialInteractionsHandler={shortVideoSocialInteractionsHandler}
|
|
241
243
|
on={{
|
|
242
244
|
closePlayer: () => onPlayerClose(),
|
|
243
245
|
productClick: (productId: String) => onProductCardClick(productId)
|
|
@@ -267,7 +269,7 @@ const onProgress = (pageId, videoId, progress) => {
|
|
|
267
269
|
container-type: inline-size;
|
|
268
270
|
display: flex;
|
|
269
271
|
position: relative;
|
|
270
|
-
background-color:
|
|
272
|
+
background-color: #c1c1c1;
|
|
271
273
|
background-image: var(--background-image);
|
|
272
274
|
background-size: cover;
|
|
273
275
|
background-blend-mode: multiply;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Locale } from '../../core/locale';
|
|
2
|
+
import { type ShortVideoSocialInteractionsHandler } from '../../short-videos/short-video-viewer';
|
|
2
3
|
import { type IStreamPlayerLocalization } from './stream-player-localization';
|
|
3
4
|
type Props = {
|
|
4
5
|
streamId: string;
|
|
5
6
|
localization?: IStreamPlayerLocalization | Locale;
|
|
6
7
|
graphqlOrigin?: string;
|
|
7
8
|
showStreamsCloudWatermark?: boolean;
|
|
9
|
+
shortVideoSocialInteractionsHandler?: ShortVideoSocialInteractionsHandler;
|
|
8
10
|
initiator?: string;
|
|
9
11
|
on?: {
|
|
10
12
|
closePlayer?: () => void;
|
|
@@ -9,7 +9,7 @@ import { StreamPlayerLocalization } from './stream-player-localization';
|
|
|
9
9
|
import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_28_regular.svg?raw';
|
|
10
10
|
import IconChevronUp from '@fluentui/svg-icons/icons/chevron_up_28_regular.svg?raw';
|
|
11
11
|
import IconDismiss from '@fluentui/svg-icons/icons/dismiss_28_regular.svg?raw';
|
|
12
|
-
let { buffer, uiManager, localization, on } = $props();
|
|
12
|
+
let { buffer, uiManager, shortVideoSocialInteractionsHandler, localization, on } = $props();
|
|
13
13
|
const shortVideo = $derived(((_a = buffer.current) === null || _a === void 0 ? void 0 : _a.type) === 'short-video' && ((_b = buffer.current) === null || _b === void 0 ? void 0 : _b.shortVideo) ? mapToShortVideoViewerModel(buffer.current.shortVideo) : null);
|
|
14
14
|
const singleWebViewPage = $derived.by(() => {
|
|
15
15
|
var _a;
|
|
@@ -46,7 +46,7 @@ const changeShowShortVideoAttachments = () => {
|
|
|
46
46
|
};
|
|
47
47
|
</script>
|
|
48
48
|
|
|
49
|
-
{#if !uiManager.showShortVideoOverlay}
|
|
49
|
+
{#if uiManager.viewInitialized && !uiManager.showShortVideoOverlay}
|
|
50
50
|
<div class="stream-player-controls">
|
|
51
51
|
<div class="stream-player-controls__left">
|
|
52
52
|
{#if shortVideo}
|
|
@@ -56,15 +56,22 @@ const changeShowShortVideoAttachments = () => {
|
|
|
56
56
|
<ShortVideoViewerAttachmentsInline model={shortVideo} />
|
|
57
57
|
</div>
|
|
58
58
|
{/if}
|
|
59
|
-
<ShortVideoControls
|
|
59
|
+
<ShortVideoControls
|
|
60
|
+
model={shortVideo}
|
|
61
|
+
socialInteractionsHandler={shortVideoSocialInteractionsHandler}
|
|
62
|
+
on={{ attachmentsClicked: changeShowShortVideoAttachments }} />
|
|
60
63
|
</div>
|
|
61
64
|
{/if}
|
|
62
65
|
<div class="stream-player-controls__navigation-buttons" class:stream-player-controls__navigation-buttons--single-web-view-page={singleWebViewPage}>
|
|
63
66
|
<button type="button" class="navigation-button" disabled={!buffer.canLoadPrevious} onclick={buffer.loadPrevious}>
|
|
64
|
-
<
|
|
67
|
+
<span class="navigation-button__icon">
|
|
68
|
+
<Icon src={IconChevronUp} />
|
|
69
|
+
</span>
|
|
65
70
|
</button>
|
|
66
71
|
<button type="button" class="navigation-button" disabled={!buffer.canLoadNext} onclick={buffer.loadNext}>
|
|
67
|
-
<
|
|
72
|
+
<span class="navigation-button__icon">
|
|
73
|
+
<Icon src={IconChevronDown} />
|
|
74
|
+
</span>
|
|
68
75
|
</button>
|
|
69
76
|
</div>
|
|
70
77
|
</div>
|
|
@@ -209,6 +216,10 @@ const changeShowShortVideoAttachments = () => {
|
|
|
209
216
|
opacity: 0.5;
|
|
210
217
|
cursor: default;
|
|
211
218
|
}
|
|
219
|
+
.close-button:hover {
|
|
220
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
221
|
+
transition: background-color 0.5s;
|
|
222
|
+
}
|
|
212
223
|
@container (width < 576px) {
|
|
213
224
|
.close-button {
|
|
214
225
|
left: unset;
|
|
@@ -217,6 +228,7 @@ const changeShowShortVideoAttachments = () => {
|
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
.navigation-button {
|
|
231
|
+
--_icon-scale: 1;
|
|
220
232
|
width: var(--stream-player--button--size);
|
|
221
233
|
min-width: var(--stream-player--button--size);
|
|
222
234
|
max-width: var(--stream-player--button--size);
|
|
@@ -233,4 +245,12 @@ const changeShowShortVideoAttachments = () => {
|
|
|
233
245
|
.navigation-button:disabled {
|
|
234
246
|
opacity: 0.5;
|
|
235
247
|
cursor: default;
|
|
248
|
+
}
|
|
249
|
+
.navigation-button:hover:not(:disabled) {
|
|
250
|
+
--_icon-scale: 1.2;
|
|
251
|
+
}
|
|
252
|
+
.navigation-button__icon {
|
|
253
|
+
display: block;
|
|
254
|
+
transform: scale(var(--_icon-scale));
|
|
255
|
+
transition: 0.3s;
|
|
236
256
|
}</style>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type ShortVideoSocialInteractionsHandler } from '../../short-videos/short-video-viewer';
|
|
1
2
|
import type { StreamPlayerBuffer } from './stream-player-buffer.svelte';
|
|
2
3
|
import { StreamPlayerLocalization } from './stream-player-localization';
|
|
3
4
|
import type { StreamPlayerUiManager } from './ui-manager.svelte';
|
|
4
5
|
type Props = {
|
|
5
6
|
buffer: StreamPlayerBuffer;
|
|
6
7
|
uiManager: StreamPlayerUiManager;
|
|
8
|
+
shortVideoSocialInteractionsHandler?: ShortVideoSocialInteractionsHandler;
|
|
7
9
|
localization: StreamPlayerLocalization;
|
|
8
10
|
on: {
|
|
9
11
|
closePlayer: () => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ShortVideoSocialInteractionsHandler } from '../../short-videos/short-video-viewer';
|
|
1
2
|
import type { IStreamPlayerLocalization } from './stream-player-localization';
|
|
2
3
|
export type { IStreamPlayerLocalization };
|
|
3
4
|
/**
|
|
@@ -28,6 +29,7 @@ export declare const openStreamPlayer: (init: {
|
|
|
28
29
|
graphqlOrigin?: string;
|
|
29
30
|
localization?: IStreamPlayerLocalization | "en" | "no";
|
|
30
31
|
showStreamsCloudWatermark?: boolean;
|
|
32
|
+
shortVideoSocialInteractionsHandler?: ShortVideoSocialInteractionsHandler;
|
|
31
33
|
initiator?: string;
|
|
32
34
|
on?: {
|
|
33
35
|
streamActivated?: (data: {
|
|
@@ -27,7 +27,7 @@ import { mount, unmount } from 'svelte';
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export const openStreamPlayer = (init) => {
|
|
30
|
-
const { streamId, graphqlOrigin, localization, showStreamsCloudWatermark, initiator } = init;
|
|
30
|
+
const { streamId, graphqlOrigin, localization, showStreamsCloudWatermark, shortVideoSocialInteractionsHandler, initiator } = init;
|
|
31
31
|
const shadowHost = new ModalShadowHost();
|
|
32
32
|
const mounted = mount(StreamPlayer, {
|
|
33
33
|
target: shadowHost.shadowRoot,
|
|
@@ -36,6 +36,7 @@ export const openStreamPlayer = (init) => {
|
|
|
36
36
|
graphqlOrigin,
|
|
37
37
|
localization: getLocale(localization),
|
|
38
38
|
showStreamsCloudWatermark,
|
|
39
|
+
shortVideoSocialInteractionsHandler,
|
|
39
40
|
initiator,
|
|
40
41
|
on: {
|
|
41
42
|
streamActivated: (data) => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { StreamPlayerPayloadFragment } from './operations.generated';
|
|
2
2
|
import type { StreamPlayerModel } from './types';
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const mapToStreamPlayerModel: (payload: StreamPlayerPayloadFragment) => StreamPlayerModel;
|
|
@@ -13,7 +13,8 @@ export class StreamPlayerUiManager {
|
|
|
13
13
|
return values.join(';');
|
|
14
14
|
});
|
|
15
15
|
isMobileView = $derived.by(() => this.viewTotalWidth <= 576);
|
|
16
|
-
|
|
16
|
+
viewInitialized = $derived.by(() => !!this.viewTotalWidth && !!this.mainViewColumnWidth);
|
|
17
|
+
showShortVideoOverlay = $derived.by(() => this.viewInitialized && (this.viewTotalWidth - this.mainViewColumnWidth) / 2 <= 85);
|
|
17
18
|
buttonSize = 48;
|
|
18
19
|
iconSize = 28;
|
|
19
20
|
controlsOffsetHorizontal = 28;
|
|
@@ -51,20 +51,25 @@ const toggleShowMore = () => {
|
|
|
51
51
|
</script>
|
|
52
52
|
|
|
53
53
|
<div class="line-clamp" bind:this={element}>
|
|
54
|
-
<div class="line-clamp__wrapper"
|
|
55
|
-
{
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
{
|
|
54
|
+
<div class="line-clamp__wrapper-container">
|
|
55
|
+
<div class="line-clamp__wrapper" bind:this={clampWrapperRef}>
|
|
56
|
+
{#if children}
|
|
57
|
+
{@render children()}
|
|
58
|
+
{:else if value}
|
|
59
|
+
{@html value}
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{#if enableShowMore && isTruncated && !showingAllText}
|
|
64
|
+
<button type="button" class="line-clamp__show-more-button line-clamp__show-more-button--inline" onclick={toggleShowMore}>
|
|
65
|
+
{localization.showMore}
|
|
66
|
+
</button>
|
|
59
67
|
{/if}
|
|
60
68
|
</div>
|
|
61
|
-
|
|
69
|
+
|
|
70
|
+
{#if enableShowMore && showingAllText}
|
|
62
71
|
<button type="button" class="line-clamp__show-more-button" onclick={toggleShowMore}>
|
|
63
|
-
{
|
|
64
|
-
{localization.showLess}
|
|
65
|
-
{:else}
|
|
66
|
-
{localization.showMore}
|
|
67
|
-
{/if}
|
|
72
|
+
{localization.showLess}
|
|
68
73
|
</button>
|
|
69
74
|
{/if}
|
|
70
75
|
</div>
|
|
@@ -75,6 +80,9 @@ const toggleShowMore = () => {
|
|
|
75
80
|
display: flex;
|
|
76
81
|
flex-direction: column;
|
|
77
82
|
}
|
|
83
|
+
.line-clamp__wrapper-container {
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
78
86
|
.line-clamp__wrapper {
|
|
79
87
|
display: -webkit-box;
|
|
80
88
|
overflow: hidden;
|
|
@@ -83,7 +91,21 @@ const toggleShowMore = () => {
|
|
|
83
91
|
-webkit-box-orient: vertical;
|
|
84
92
|
}
|
|
85
93
|
.line-clamp__show-more-button {
|
|
86
|
-
margin-top: 0.5em;
|
|
87
94
|
font-size: 0.9em;
|
|
88
|
-
|
|
95
|
+
font-style: italic;
|
|
96
|
+
}
|
|
97
|
+
.line-clamp__show-more-button--inline {
|
|
98
|
+
position: absolute;
|
|
99
|
+
bottom: 0;
|
|
100
|
+
right: 0;
|
|
101
|
+
padding-left: 1em;
|
|
102
|
+
backdrop-filter: blur(2px);
|
|
103
|
+
}
|
|
104
|
+
.line-clamp__show-more-button--inline::after {
|
|
105
|
+
content: "...";
|
|
106
|
+
position: absolute;
|
|
107
|
+
left: 0;
|
|
108
|
+
}
|
|
109
|
+
.line-clamp__show-more-button:not(.line-clamp__show-more-button--inline) {
|
|
110
|
+
margin-top: 0.5em;
|
|
89
111
|
}</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
import { isScrollingPrevented } from './prevent-slider-scroll';
|
|
1
|
+
<script lang="ts">import { isScrollingPrevented } from './prevent-slider-scroll';
|
|
3
2
|
import { onDestroy, onMount, untrack } from 'svelte';
|
|
3
|
+
const MOUSE_DETECTION_THRESHOLD_MS = 100;
|
|
4
4
|
let { buffer, on, children } = $props();
|
|
5
5
|
let slidesRef;
|
|
6
6
|
let sliderHeight = $state(0);
|
|
@@ -90,7 +90,28 @@ onMount(() => {
|
|
|
90
90
|
}
|
|
91
91
|
reset();
|
|
92
92
|
});
|
|
93
|
-
|
|
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();
|
|
94
115
|
const checkCanHandleWheel = (node) => {
|
|
95
116
|
while (node && node !== slidesRef) {
|
|
96
117
|
if (isScrollingPrevented(node)) {
|
|
@@ -103,13 +124,55 @@ onMount(() => {
|
|
|
103
124
|
if (!checkCanHandleWheel(e.target)) {
|
|
104
125
|
return;
|
|
105
126
|
}
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
140
|
}
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
111
165
|
}
|
|
112
|
-
|
|
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
|
+
});
|
|
113
176
|
slidesRef.addEventListener('transitionend', (e) => {
|
|
114
177
|
if (e.target !== slidesRef) {
|
|
115
178
|
return;
|
|
@@ -142,7 +205,10 @@ const styles = $derived.by(() => {
|
|
|
142
205
|
<div class="player-slider__slides" bind:this={slidesRef} style={styles}>
|
|
143
206
|
{#each buffer.loaded as item, index (item)}
|
|
144
207
|
<div class="player-slider__slide">
|
|
145
|
-
{
|
|
208
|
+
{#if index >= activeIndex - 1 && index <= activeIndex + 1}
|
|
209
|
+
<!-- Only render the active slide and its immediate neighbors for performance -->
|
|
210
|
+
{@render children({ item, active: index === activeIndex })}
|
|
211
|
+
{/if}
|
|
146
212
|
</div>
|
|
147
213
|
{/each}
|
|
148
214
|
</div>
|
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
--_progress--height: var(--progress--height, 0.25em);
|
|
23
23
|
--_progress--back-color: var(--progress--back-color, #b0b0b0);
|
|
24
24
|
--_progress--front-color: var(--progress--front-color, #ffffff);
|
|
25
|
+
--_progress--box-shadow: var(--progress--box-shadow, 0 2px 3px rgba(0, 0, 0, 0.25) inset);
|
|
26
|
+
--_progress--border-radius: var(--progress--border-radius, 0);
|
|
25
27
|
width: 100%;
|
|
26
28
|
background: var(--_progress--back-color);
|
|
27
29
|
height: var(--_progress--height);
|
|
28
|
-
box-shadow:
|
|
30
|
+
box-shadow: var(--_progress--box-shadow);
|
|
31
|
+
border-radius: var(--_progress--border-radius);
|
|
29
32
|
}
|
|
30
33
|
.progress__value {
|
|
31
34
|
background: var(--_progress--front-color);
|
|
@@ -1,30 +1,112 @@
|
|
|
1
|
-
<script lang="ts">let { value, on } = $props();
|
|
1
|
+
<script lang="ts">let { value, listenParentClicks = false, on } = $props();
|
|
2
|
+
let seekBarRef;
|
|
3
|
+
let scrubberRef;
|
|
2
4
|
let progressRef;
|
|
3
|
-
let
|
|
5
|
+
let isDragging = $state(false);
|
|
6
|
+
const cssValue = $derived(`${100 * (value <= 1 ? value : 1)}%`);
|
|
4
7
|
const handleSeek = (e) => {
|
|
8
|
+
if (!progressRef) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
5
11
|
e.stopPropagation();
|
|
6
12
|
const { left, width } = progressRef.getBoundingClientRect();
|
|
7
13
|
const x = e.clientX - left;
|
|
8
|
-
const percent = x / width;
|
|
9
|
-
value = percent;
|
|
10
|
-
valueRef.style.transition = 'none';
|
|
11
|
-
valueRef.style.width = `${100 * percent}%`;
|
|
12
|
-
setTimeout(() => {
|
|
13
|
-
valueRef.style.transition = '';
|
|
14
|
-
}, 0);
|
|
14
|
+
const percent = Math.max(0, Math.min(1, x / width));
|
|
15
15
|
on === null || on === void 0 ? void 0 : on.seek(percent);
|
|
16
16
|
};
|
|
17
|
+
const handleParentClick = (e) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
if (!progressRef || !seekBarRef || isDragging) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const seekBarRect = progressRef.getBoundingClientRect();
|
|
24
|
+
if (e.clientX >= seekBarRect.left && e.clientX <= seekBarRect.right) {
|
|
25
|
+
const x = e.clientX - seekBarRect.left;
|
|
26
|
+
const percent = Math.max(0, Math.min(1, x / seekBarRect.width));
|
|
27
|
+
on === null || on === void 0 ? void 0 : on.seek(percent);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const onMouseMove = (e) => {
|
|
31
|
+
if (isDragging) {
|
|
32
|
+
handleSeek(e);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const onMouseUp = () => {
|
|
36
|
+
var _a;
|
|
37
|
+
isDragging = false;
|
|
38
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
39
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
40
|
+
scrubberRef === null || scrubberRef === void 0 ? void 0 : scrubberRef.blur();
|
|
41
|
+
(_a = on === null || on === void 0 ? void 0 : on.dragEnd) === null || _a === void 0 ? void 0 : _a.call(on);
|
|
42
|
+
};
|
|
43
|
+
const onMouseDown = (e) => {
|
|
44
|
+
var _a;
|
|
45
|
+
isDragging = true;
|
|
46
|
+
(_a = on === null || on === void 0 ? void 0 : on.dragStart) === null || _a === void 0 ? void 0 : _a.call(on);
|
|
47
|
+
handleSeek(e);
|
|
48
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
49
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
50
|
+
scrubberRef === null || scrubberRef === void 0 ? void 0 : scrubberRef.blur();
|
|
51
|
+
};
|
|
52
|
+
const handleScrubberKeyDown = (event) => {
|
|
53
|
+
const step = 0.05;
|
|
54
|
+
let newValue;
|
|
55
|
+
if (event.key === 'ArrowLeft') {
|
|
56
|
+
newValue = Math.max(0, value - step);
|
|
57
|
+
}
|
|
58
|
+
else if (event.key === 'ArrowRight') {
|
|
59
|
+
newValue = Math.min(1, value + step);
|
|
60
|
+
}
|
|
61
|
+
else if (event.key === 'Home') {
|
|
62
|
+
newValue = 0;
|
|
63
|
+
}
|
|
64
|
+
else if (event.key === 'End') {
|
|
65
|
+
newValue = 1;
|
|
66
|
+
}
|
|
67
|
+
if (newValue !== undefined) {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
on === null || on === void 0 ? void 0 : on.seek(newValue);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
$effect(() => {
|
|
73
|
+
let parent = null;
|
|
74
|
+
if (listenParentClicks && seekBarRef) {
|
|
75
|
+
parent = seekBarRef.parentElement;
|
|
76
|
+
if (parent) {
|
|
77
|
+
parent.addEventListener('click', handleParentClick);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return () => {
|
|
81
|
+
if (parent) {
|
|
82
|
+
parent.removeEventListener('click', handleParentClick);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
$effect(() => {
|
|
87
|
+
return () => {
|
|
88
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
89
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
90
|
+
};
|
|
91
|
+
});
|
|
17
92
|
</script>
|
|
18
93
|
|
|
19
|
-
<div class="seek-bar"
|
|
94
|
+
<div class="seek-bar" onmousedown={onMouseDown} onkeydown={() => ({})} role="none" bind:this={seekBarRef}>
|
|
20
95
|
<div class="seek-bar__container" bind:this={progressRef}>
|
|
21
|
-
<span
|
|
22
|
-
|
|
23
|
-
class
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
96
|
+
<span class="seek-bar__value" style="width: {cssValue}"> </span>
|
|
97
|
+
<div
|
|
98
|
+
class="seek-bar__scrubber"
|
|
99
|
+
bind:this={scrubberRef}
|
|
100
|
+
class:is-dragging={isDragging}
|
|
101
|
+
style="left: {cssValue}"
|
|
102
|
+
role="slider"
|
|
103
|
+
tabindex="0"
|
|
104
|
+
aria-valuemin="0"
|
|
105
|
+
aria-valuemax="1"
|
|
106
|
+
aria-valuenow={value}
|
|
107
|
+
aria-label="Media position slider"
|
|
108
|
+
onkeydown={handleScrubberKeyDown}>
|
|
109
|
+
</div>
|
|
28
110
|
</div>
|
|
29
111
|
</div>
|
|
30
112
|
|
|
@@ -40,24 +122,48 @@ const handleSeek = (e) => {
|
|
|
40
122
|
}
|
|
41
123
|
}
|
|
42
124
|
.seek-bar {
|
|
43
|
-
--_seek-bar--height: var(--seek-bar--height, 0.25em);
|
|
44
|
-
--_seek-bar--back-color: var(--seek-bar--back-color, #b0b0b0);
|
|
45
|
-
--_seek-bar--front-color: var(--seek-bar--front-color, #ffffff);
|
|
46
125
|
cursor: pointer;
|
|
126
|
+
position: relative;
|
|
127
|
+
padding: 0.3125rem 0;
|
|
128
|
+
--_seek-bar--container-color: var(--seek-bar--container-color, #b0b0b0);
|
|
129
|
+
--_seek-bar--value-color: var(--seek-bar--value-color, #ffffff);
|
|
130
|
+
--_seek-bar--scrubber-color: var(--seek-bar--scrubber-color, #ffffff);
|
|
131
|
+
--_seek-bar--scrubber-border-color: var(--seek-bar--scrubber-border-color, #b0b0b0);
|
|
132
|
+
--_seek-bar--scrubber-opacity: 0;
|
|
133
|
+
}
|
|
134
|
+
.seek-bar:hover {
|
|
135
|
+
--_seek-bar--scrubber-opacity: 1;
|
|
47
136
|
}
|
|
48
137
|
.seek-bar__container {
|
|
49
138
|
width: 100%;
|
|
50
|
-
background: var(--_seek-bar--
|
|
51
|
-
height:
|
|
52
|
-
|
|
53
|
-
border-radius: calc(var(--_seek-bar--height) / 2);
|
|
54
|
-
overflow: hidden;
|
|
139
|
+
background: var(--_seek-bar--container-color);
|
|
140
|
+
height: 0.3125rem;
|
|
141
|
+
position: relative;
|
|
55
142
|
}
|
|
56
143
|
.seek-bar__value {
|
|
57
|
-
background: var(--_seek-bar--
|
|
144
|
+
background: var(--_seek-bar--value-color);
|
|
145
|
+
border-color: var(--_seek-bar--container-color);
|
|
146
|
+
border-width: 0.0625rem;
|
|
58
147
|
display: inline-block;
|
|
59
148
|
height: 100%;
|
|
60
149
|
}
|
|
61
|
-
.seek-
|
|
62
|
-
|
|
150
|
+
.seek-bar__scrubber {
|
|
151
|
+
position: absolute;
|
|
152
|
+
top: 50%;
|
|
153
|
+
width: 0.75rem;
|
|
154
|
+
height: 0.75rem;
|
|
155
|
+
background: var(--_seek-bar--scrubber-color);
|
|
156
|
+
border-color: var(--_seek-bar--scrubber-border-color);
|
|
157
|
+
border-width: 0.0625rem;
|
|
158
|
+
border-radius: 50%;
|
|
159
|
+
transform: translate(-50%, -50%);
|
|
160
|
+
z-index: 1;
|
|
161
|
+
opacity: var(--_seek-bar--scrubber-opacity);
|
|
162
|
+
transition: opacity 0.2s ease-in-out;
|
|
163
|
+
}
|
|
164
|
+
.seek-bar__scrubber.is-dragging, .seek-bar__scrubber:focus {
|
|
165
|
+
--_seek-bar--scrubber-opacity: 1;
|
|
166
|
+
}
|
|
167
|
+
.seek-bar__scrubber:focus-visible {
|
|
168
|
+
outline: none;
|
|
63
169
|
}</style>
|