@streamscloud/kit 0.0.1-1771006476137 → 0.0.1-1771075495185
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/ui/player/buttons/cmp.mobile-player-buttons.svelte +37 -0
- package/dist/ui/player/buttons/cmp.mobile-player-buttons.svelte.d.ts +7 -0
- package/dist/ui/player/buttons/cmp.player-buttons.svelte +127 -0
- package/dist/ui/player/buttons/cmp.player-buttons.svelte.d.ts +9 -0
- package/dist/ui/player/buttons/cmp.responsive-player-buttons.svelte +32 -0
- package/dist/ui/player/buttons/cmp.responsive-player-buttons.svelte.d.ts +8 -0
- package/dist/ui/player/buttons/index.d.ts +4 -0
- package/dist/ui/player/buttons/index.js +3 -0
- package/dist/ui/player/buttons/types.d.ts +7 -0
- package/dist/ui/player/buttons/types.js +1 -0
- package/dist/ui/player/carousel/carousel-localization.d.ts +3 -0
- package/dist/ui/player/carousel/carousel-localization.js +12 -0
- package/dist/ui/player/carousel/cmp.carousel.svelte +418 -0
- package/dist/ui/player/carousel/cmp.carousel.svelte.d.ts +38 -0
- package/dist/ui/player/carousel/index.d.ts +2 -0
- package/dist/ui/player/carousel/index.js +1 -0
- package/dist/ui/player/carousel/types.d.ts +1 -0
- package/dist/ui/player/carousel/types.js +1 -0
- package/dist/ui/player/feed-slider/cmp.feed-slider.svelte +206 -0
- package/dist/ui/player/feed-slider/cmp.feed-slider.svelte.d.ts +43 -0
- package/dist/ui/player/feed-slider/index.d.ts +3 -0
- package/dist/ui/player/feed-slider/index.js +2 -0
- package/dist/ui/player/feed-slider/prevent-feed-scroll.d.ts +5 -0
- package/dist/ui/player/feed-slider/prevent-feed-scroll.js +11 -0
- package/dist/ui/player/feed-slider/types.d.ts +16 -0
- package/dist/ui/player/feed-slider/types.js +1 -0
- package/dist/ui/player/feed-slider/wheel-gestures-adapter.d.ts +17 -0
- package/dist/ui/player/feed-slider/wheel-gestures-adapter.js +79 -0
- package/dist/ui/player/providers/chunks-player-buffer/index.d.ts +2 -0
- package/dist/ui/player/providers/chunks-player-buffer/index.js +2 -0
- package/dist/ui/player/providers/chunks-player-buffer/player-chunk-item.svelte.d.ts +9 -0
- package/dist/ui/player/providers/chunks-player-buffer/player-chunk-item.svelte.js +9 -0
- package/dist/ui/player/providers/chunks-player-buffer/player-chunk.svelte.d.ts +30 -0
- package/dist/ui/player/providers/chunks-player-buffer/player-chunk.svelte.js +72 -0
- package/dist/ui/player/providers/chunks-player-buffer/player-chunks-manager.svelte.d.ts +23 -0
- package/dist/ui/player/providers/chunks-player-buffer/player-chunks-manager.svelte.js +220 -0
- package/dist/ui/player/providers/default-chunks-player-buffer.svelte.d.ts +23 -0
- package/dist/ui/player/providers/default-chunks-player-buffer.svelte.js +71 -0
- package/dist/ui/player/providers/default-feed-player-buffer.svelte.d.ts +29 -0
- package/dist/ui/player/providers/default-feed-player-buffer.svelte.js +121 -0
- package/dist/ui/player/providers/index.d.ts +4 -0
- package/dist/ui/player/providers/index.js +3 -0
- package/dist/ui/player/providers/service.d.ts +2 -0
- package/dist/ui/player/providers/service.js +13 -0
- package/dist/ui/player/providers/types.d.ts +54 -0
- package/dist/ui/player/providers/types.js +1 -0
- package/dist/ui/player/utils/index.d.ts +1 -0
- package/dist/ui/player/utils/index.js +1 -0
- package/dist/ui/player/utils/touch-synchronizer.d.ts +7 -0
- package/dist/ui/player/utils/touch-synchronizer.js +21 -0
- package/package.json +23 -5
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CarouselMode } from './types';
|
|
2
|
+
import { type Snippet } from 'svelte';
|
|
3
|
+
declare function $$render<T>(): {
|
|
4
|
+
props: {
|
|
5
|
+
items: T[];
|
|
6
|
+
initialIndex: number;
|
|
7
|
+
mode?: CarouselMode;
|
|
8
|
+
autoSlideMs?: number;
|
|
9
|
+
on?: {
|
|
10
|
+
indexChanged: (index: number) => void;
|
|
11
|
+
};
|
|
12
|
+
children: Snippet<[T]>;
|
|
13
|
+
dot?: Snippet<[{
|
|
14
|
+
active: boolean;
|
|
15
|
+
}]>;
|
|
16
|
+
};
|
|
17
|
+
exports: {};
|
|
18
|
+
bindings: "";
|
|
19
|
+
slots: {};
|
|
20
|
+
events: {};
|
|
21
|
+
};
|
|
22
|
+
declare class __sveltets_Render<T> {
|
|
23
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
24
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
25
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
26
|
+
bindings(): "";
|
|
27
|
+
exports(): {};
|
|
28
|
+
}
|
|
29
|
+
interface $$IsomorphicComponent {
|
|
30
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
31
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
32
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
33
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
34
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
35
|
+
}
|
|
36
|
+
declare const Cmp: $$IsomorphicComponent;
|
|
37
|
+
type Cmp<T> = InstanceType<typeof Cmp<T>>;
|
|
38
|
+
export default Cmp;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Carousel } from './cmp.carousel.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type CarouselMode = 'arrows-and-dots' | 'arrows-only' | 'dots-only' | 'dots-only-below' | 'arrows-with-counts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends { id: string }">import { TouchSynchronizer } from '../utils';
|
|
2
|
+
import { createWheelAdapter } from './wheel-gestures-adapter';
|
|
3
|
+
import { onDestroy, onMount, untrack } from 'svelte';
|
|
4
|
+
let { buffer, on, children } = $props();
|
|
5
|
+
let slidesRef;
|
|
6
|
+
let sliderHeight = $state(0);
|
|
7
|
+
let swipeTransition = $state(0);
|
|
8
|
+
let cssAnimationDuration = $derived(buffer.animationDuration * 0.55);
|
|
9
|
+
let resizeObserver;
|
|
10
|
+
let sliderIndex = $state.raw(untrack(() => buffer.currentIndex));
|
|
11
|
+
const actualTransition = $derived(-sliderHeight * sliderIndex);
|
|
12
|
+
const onKeyPress = (e) => {
|
|
13
|
+
if (e.key === 'ArrowUp' || e.key === 'PageUp') {
|
|
14
|
+
buffer.loadPrevious();
|
|
15
|
+
}
|
|
16
|
+
if (e.key === 'ArrowDown' || e.key === 'PageDown') {
|
|
17
|
+
buffer.loadNext();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (buffer.currentIndex >= 0 && sliderIndex >= 0 && Math.abs(sliderIndex - buffer.currentIndex) === 1) {
|
|
22
|
+
slidesRef.classList.toggle('animate', true);
|
|
23
|
+
}
|
|
24
|
+
else if (buffer.currentIndex >= 0 && (Math.abs(sliderIndex - buffer.currentIndex) !== 0 || !active)) {
|
|
25
|
+
untrack(() => notifyOnItemChanged());
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
let active = null;
|
|
29
|
+
$effect(() => {
|
|
30
|
+
sliderIndex = buffer.currentIndex;
|
|
31
|
+
});
|
|
32
|
+
const notifyOnItemChanged = () => {
|
|
33
|
+
if (active && buffer.current?.id !== active.id) {
|
|
34
|
+
on?.itemDeactivated?.(active.id);
|
|
35
|
+
}
|
|
36
|
+
active = buffer.current;
|
|
37
|
+
if (active) {
|
|
38
|
+
on?.itemActivated?.(active.id);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
onMount(() => {
|
|
42
|
+
let touchStartX = 0;
|
|
43
|
+
let touchStartY = 0;
|
|
44
|
+
let touchMoveY = 0;
|
|
45
|
+
let touchStartTime = 0;
|
|
46
|
+
let swipeState = 'not-initialized';
|
|
47
|
+
window.addEventListener(`keydown`, onKeyPress);
|
|
48
|
+
slidesRef.addEventListener('touchstart', (e) => {
|
|
49
|
+
TouchSynchronizer.touchStarted(slidesRef);
|
|
50
|
+
// The movement gets all janky if there's a transition on the elements.
|
|
51
|
+
slidesRef.classList.toggle('animate', false);
|
|
52
|
+
touchStartX = e.changedTouches[0].screenX;
|
|
53
|
+
touchStartY = e.changedTouches[0].screenY;
|
|
54
|
+
touchStartTime = Date.now();
|
|
55
|
+
swipeState = 'not-initialized';
|
|
56
|
+
}, { passive: true });
|
|
57
|
+
slidesRef.addEventListener('touchmove', (e) => {
|
|
58
|
+
if (swipeState === 'vertical') {
|
|
59
|
+
// Already determined this is a vertical swipe
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
TouchSynchronizer.touchMovePropagationBlocked(slidesRef);
|
|
63
|
+
}
|
|
64
|
+
else if (swipeState === 'horizontal') {
|
|
65
|
+
// Already determined this is a horizontal swipe
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const touchCurrentX = e.changedTouches[0].screenX;
|
|
69
|
+
const touchCurrentY = e.changedTouches[0].screenY;
|
|
70
|
+
const diffX = Math.abs(touchCurrentX - touchStartX);
|
|
71
|
+
const diffY = Math.abs(touchCurrentY - touchStartY);
|
|
72
|
+
// Determine direction on first significant movement
|
|
73
|
+
if (diffX > 10 || diffY > 10) {
|
|
74
|
+
if (diffY > diffX) {
|
|
75
|
+
// Vertical swipe - handle it
|
|
76
|
+
swipeState = 'vertical';
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
TouchSynchronizer.touchMovePropagationBlocked(slidesRef);
|
|
80
|
+
const newPosition = touchCurrentY;
|
|
81
|
+
const diff = newPosition - touchStartY;
|
|
82
|
+
if ((diff > 0 && buffer.canLoadPrevious) || (diff < 0 && buffer.canLoadNext)) {
|
|
83
|
+
touchMoveY = diff;
|
|
84
|
+
swipeTransition = diff + actualTransition;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Horizontal swipe - allow child to handle
|
|
89
|
+
swipeState = 'horizontal';
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}, { passive: false });
|
|
94
|
+
slidesRef.addEventListener('touchend', (e) => {
|
|
95
|
+
const reset = () => {
|
|
96
|
+
swipeTransition = 0;
|
|
97
|
+
touchMoveY = 0;
|
|
98
|
+
};
|
|
99
|
+
// Check if this is a vertical swipe
|
|
100
|
+
if (swipeState !== 'vertical') {
|
|
101
|
+
// Horizontal swipe - don't handle and don't block
|
|
102
|
+
reset();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// This is a vertical swipe - block propagation
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
TouchSynchronizer.touchEndPropatationBlocked(slidesRef);
|
|
108
|
+
TouchSynchronizer.touchEnded(slidesRef);
|
|
109
|
+
slidesRef.classList.toggle('animate', true);
|
|
110
|
+
const isFastSwipe = Date.now() - touchStartTime < 300;
|
|
111
|
+
if (!touchMoveY || (!isFastSwipe && Math.abs(touchMoveY) < sliderHeight / 6)) {
|
|
112
|
+
return reset();
|
|
113
|
+
}
|
|
114
|
+
if (touchMoveY > 0) {
|
|
115
|
+
buffer.loadPrevious();
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
buffer.loadNext();
|
|
119
|
+
}
|
|
120
|
+
reset();
|
|
121
|
+
});
|
|
122
|
+
slidesRef.addEventListener('transitionend', (e) => {
|
|
123
|
+
if (e.target !== slidesRef) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
slidesRef.classList.toggle('animate', false);
|
|
127
|
+
notifyOnItemChanged();
|
|
128
|
+
});
|
|
129
|
+
sliderHeight = slidesRef.clientHeight;
|
|
130
|
+
resizeObserver = new ResizeObserver(() => {
|
|
131
|
+
sliderHeight = slidesRef.clientHeight;
|
|
132
|
+
});
|
|
133
|
+
resizeObserver.observe(slidesRef);
|
|
134
|
+
});
|
|
135
|
+
onDestroy(() => {
|
|
136
|
+
if (resizeObserver) {
|
|
137
|
+
resizeObserver.disconnect();
|
|
138
|
+
}
|
|
139
|
+
window.removeEventListener('keydown', onKeyPress);
|
|
140
|
+
});
|
|
141
|
+
const wheelCallbacks = {
|
|
142
|
+
canLoadNext: () => buffer.canLoadNext,
|
|
143
|
+
canLoadPrevious: () => buffer.canLoadPrevious,
|
|
144
|
+
onTrigger: (direction) => {
|
|
145
|
+
// direction: 1 -> next, -1 -> previous
|
|
146
|
+
if (direction > 0) {
|
|
147
|
+
buffer.loadNext();
|
|
148
|
+
}
|
|
149
|
+
else if (direction < 0) {
|
|
150
|
+
buffer.loadPrevious();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
getAnimationDurationMs: () => buffer.animationDuration
|
|
154
|
+
};
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<div class="feed-slider">
|
|
158
|
+
<div
|
|
159
|
+
class="feed-slider__slides"
|
|
160
|
+
bind:this={slidesRef}
|
|
161
|
+
use:createWheelAdapter={{ cbs: wheelCallbacks }}
|
|
162
|
+
style:transform="translateY({swipeTransition || actualTransition}px)"
|
|
163
|
+
style:--feed-slider--animation="{cssAnimationDuration}ms ease-out transform">
|
|
164
|
+
{#each buffer.loaded as item, index (item)}
|
|
165
|
+
<div class="feed-slider__slide">
|
|
166
|
+
{#if index >= sliderIndex - 1 && index <= sliderIndex + 1}
|
|
167
|
+
<!-- Only render the active slide and its immediate neighbors for performance -->
|
|
168
|
+
{@render children({ item, active: index === sliderIndex })}
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
{/each}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<style>.feed-slider {
|
|
176
|
+
position: relative;
|
|
177
|
+
overflow: clip;
|
|
178
|
+
height: 100%;
|
|
179
|
+
width: 100%;
|
|
180
|
+
min-width: 100%;
|
|
181
|
+
max-width: 100%;
|
|
182
|
+
}
|
|
183
|
+
.feed-slider__slides {
|
|
184
|
+
--_feed-slider--animation: var(--feed-slider--animation);
|
|
185
|
+
height: 100%;
|
|
186
|
+
min-height: 100%;
|
|
187
|
+
max-height: 100%;
|
|
188
|
+
width: 100%;
|
|
189
|
+
min-width: 100%;
|
|
190
|
+
max-width: 100%;
|
|
191
|
+
}
|
|
192
|
+
.feed-slider__slides:global(.animate) {
|
|
193
|
+
transition: var(--_feed-slider--animation);
|
|
194
|
+
}
|
|
195
|
+
.feed-slider__slide {
|
|
196
|
+
display: flex;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
align-items: center;
|
|
199
|
+
position: relative;
|
|
200
|
+
height: 100%;
|
|
201
|
+
min-height: 100%;
|
|
202
|
+
max-height: 100%;
|
|
203
|
+
width: 100%;
|
|
204
|
+
min-width: 100%;
|
|
205
|
+
max-width: 100%;
|
|
206
|
+
}</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FeedSliderBuffer, FeedSliderCallbacks } from './types';
|
|
2
|
+
import { type Snippet } from 'svelte';
|
|
3
|
+
declare function $$render<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}>(): {
|
|
6
|
+
props: {
|
|
7
|
+
buffer: FeedSliderBuffer<T>;
|
|
8
|
+
on?: FeedSliderCallbacks;
|
|
9
|
+
children: Snippet<[{
|
|
10
|
+
item: T;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
}]>;
|
|
13
|
+
};
|
|
14
|
+
exports: {};
|
|
15
|
+
bindings: "";
|
|
16
|
+
slots: {};
|
|
17
|
+
events: {};
|
|
18
|
+
};
|
|
19
|
+
declare class __sveltets_Render<T extends {
|
|
20
|
+
id: string;
|
|
21
|
+
}> {
|
|
22
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
23
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
24
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
25
|
+
bindings(): "";
|
|
26
|
+
exports(): {};
|
|
27
|
+
}
|
|
28
|
+
interface $$IsomorphicComponent {
|
|
29
|
+
new <T extends {
|
|
30
|
+
id: string;
|
|
31
|
+
}>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
32
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
33
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
34
|
+
<T extends {
|
|
35
|
+
id: string;
|
|
36
|
+
}>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
37
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
38
|
+
}
|
|
39
|
+
declare const Cmp: $$IsomorphicComponent;
|
|
40
|
+
type Cmp<T extends {
|
|
41
|
+
id: string;
|
|
42
|
+
}> = InstanceType<typeof Cmp<T>>;
|
|
43
|
+
export default Cmp;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @type {import('svelte/action').Action} */
|
|
2
|
+
export const preventFeedScroll = (node, value = true) => {
|
|
3
|
+
node.dataset[preventFeedScrollingAttribute] = value.toString();
|
|
4
|
+
return {
|
|
5
|
+
destroy() {
|
|
6
|
+
// the node has been removed from the DOM
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export const isScrollingPrevented = (node) => node.dataset[preventFeedScrollingAttribute] === 'true';
|
|
11
|
+
const preventFeedScrollingAttribute = 'preventFeedScrolling';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type FeedSliderCallbacks = {
|
|
2
|
+
itemActivated?: (e: string) => void;
|
|
3
|
+
itemDeactivated?: (e: string) => void;
|
|
4
|
+
};
|
|
5
|
+
export interface FeedSliderBuffer<T extends {
|
|
6
|
+
id: string;
|
|
7
|
+
}> {
|
|
8
|
+
readonly current: T | null;
|
|
9
|
+
readonly loaded: T[];
|
|
10
|
+
readonly currentIndex: number;
|
|
11
|
+
readonly canLoadNext: boolean;
|
|
12
|
+
readonly canLoadPrevious: boolean;
|
|
13
|
+
readonly animationDuration: number;
|
|
14
|
+
loadNext: () => void;
|
|
15
|
+
loadPrevious: () => void;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type WheelAdapterCallbacks = {
|
|
2
|
+
canLoadNext: () => boolean;
|
|
3
|
+
canLoadPrevious: () => boolean;
|
|
4
|
+
onTrigger: (direction: number) => void;
|
|
5
|
+
getAnimationDurationMs: () => number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Minimal, robust wheel adapter:
|
|
9
|
+
* - EMA over axisVelocity[1] to smooth noisy streams (esp. on Windows)
|
|
10
|
+
* - Cooldown blocks triggers while animation runs
|
|
11
|
+
* - Mouse fallback: if velocity is near zero but delta is large, treat as a discrete "kick"
|
|
12
|
+
*/
|
|
13
|
+
export declare const createWheelAdapter: (target: HTMLElement, params: {
|
|
14
|
+
cbs: WheelAdapterCallbacks;
|
|
15
|
+
}) => {
|
|
16
|
+
destroy(): void;
|
|
17
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// wheel-gestures-adapter.ts
|
|
2
|
+
import { WheelGestures } from 'wheel-gestures';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal, robust wheel adapter:
|
|
5
|
+
* - EMA over axisVelocity[1] to smooth noisy streams (esp. on Windows)
|
|
6
|
+
* - Cooldown blocks triggers while animation runs
|
|
7
|
+
* - Mouse fallback: if velocity is near zero but delta is large, treat as a discrete "kick"
|
|
8
|
+
*/
|
|
9
|
+
export const createWheelAdapter = (target, params) => {
|
|
10
|
+
const { cbs } = params;
|
|
11
|
+
// Tunables
|
|
12
|
+
const PEAK_THRESHOLD = 0.4; // EMA magnitude threshold to consider as a "peak"
|
|
13
|
+
const ACCEL_THRESHOLD = 0.02; // minimal directional EMA rise to count as real acceleration
|
|
14
|
+
const EMA_ALPHA = 0.35; // EMA smoothing factor (0..1); higher = snappier, noisier
|
|
15
|
+
const MOUSE_DELTA_KICK = 12; // mouse: large delta step (≈12 mac / ≈100 win)
|
|
16
|
+
const MOUSE_STEP_RATIO_KICK = 350; // dimensionless; tune 200–500 if needed
|
|
17
|
+
const wheelGestures = WheelGestures({ preventWheelAction: true, reverseSign: false });
|
|
18
|
+
wheelGestures.observe(target);
|
|
19
|
+
let isAnimating = false;
|
|
20
|
+
let cooldownTimer = null;
|
|
21
|
+
// EMA state
|
|
22
|
+
let emaVelocity = 0;
|
|
23
|
+
let previousEmaVelocity = 0;
|
|
24
|
+
const startCooldown = () => {
|
|
25
|
+
if (cooldownTimer) {
|
|
26
|
+
clearTimeout(cooldownTimer);
|
|
27
|
+
}
|
|
28
|
+
cooldownTimer = window.setTimeout(() => {
|
|
29
|
+
isAnimating = false;
|
|
30
|
+
}, cbs.getAnimationDurationMs() + 100);
|
|
31
|
+
};
|
|
32
|
+
const fire = (direction) => {
|
|
33
|
+
// Respect external guards (e.g., paging boundaries)
|
|
34
|
+
if ((direction > 0 && !cbs.canLoadNext()) || (direction < 0 && !cbs.canLoadPrevious())) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
isAnimating = true;
|
|
38
|
+
cbs.onTrigger(direction);
|
|
39
|
+
startCooldown();
|
|
40
|
+
};
|
|
41
|
+
wheelGestures.on('wheel', ({ axisDelta: [, axisDeltaY], axisVelocity: [, axisVelocityY] }) => {
|
|
42
|
+
const velocityY = axisVelocityY || 0;
|
|
43
|
+
const deltaY = axisDeltaY || 0;
|
|
44
|
+
// Tracking only: always update EMA and compute signs/acceleration
|
|
45
|
+
previousEmaVelocity = emaVelocity;
|
|
46
|
+
emaVelocity += (velocityY - emaVelocity) * EMA_ALPHA;
|
|
47
|
+
const emaMagnitude = Math.abs(emaVelocity);
|
|
48
|
+
const velocitySign = Math.sign(emaVelocity) || Math.sign(velocityY);
|
|
49
|
+
const emaAcceleration = emaVelocity - previousEmaVelocity;
|
|
50
|
+
// During animation we only track; no arming, no triggering
|
|
51
|
+
if (isAnimating) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Path 1: mouse-like discrete kick (platform-agnostic via delta/velocity ratio)
|
|
55
|
+
const absDelta = Math.abs(deltaY);
|
|
56
|
+
const absVel = Math.abs(velocityY);
|
|
57
|
+
const stepRatio = absDelta / Math.max(1e-6, absVel);
|
|
58
|
+
const isMouseLikeKick = absDelta >= MOUSE_DELTA_KICK && stepRatio >= MOUSE_STEP_RATIO_KICK;
|
|
59
|
+
if (isMouseLikeKick) {
|
|
60
|
+
const direction = deltaY > 0 ? 1 : -1; // 1 = next/down, -1 = previous/up
|
|
61
|
+
fire(direction);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Path 2: trackpad/inertia via EMA acceleration and peak gating
|
|
65
|
+
const isAcceleratingInDirection = velocitySign !== 0 && emaAcceleration * velocitySign > ACCEL_THRESHOLD;
|
|
66
|
+
if (isAcceleratingInDirection && emaMagnitude > PEAK_THRESHOLD) {
|
|
67
|
+
const direction = velocitySign > 0 ? 1 : -1; // 1 = next/down, -1 = previous/up
|
|
68
|
+
fire(direction);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
destroy() {
|
|
73
|
+
wheelGestures.unobserve(target);
|
|
74
|
+
if (cooldownTimer) {
|
|
75
|
+
clearTimeout(cooldownTimer);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PlayerChunkItem } from './player-chunk-item.svelte';
|
|
2
|
+
import type { WithId } from '../types';
|
|
3
|
+
export declare class PlayerChunk<TItem extends WithId, TChunk extends WithId> {
|
|
4
|
+
readonly model: TChunk;
|
|
5
|
+
readonly items: TItem[];
|
|
6
|
+
readonly chunkItems: PlayerChunkItem<TItem>[];
|
|
7
|
+
readonly activeChunkItem: PlayerChunkItem<TItem>;
|
|
8
|
+
private _chunkItems;
|
|
9
|
+
private _activeItemIndex;
|
|
10
|
+
private _fetchDeferred;
|
|
11
|
+
private _itemsLoader;
|
|
12
|
+
constructor(data: {
|
|
13
|
+
chunk: TChunk;
|
|
14
|
+
provider: {
|
|
15
|
+
loadChunkItems: (chunkId: string, continuationToken: string | null | undefined) => Promise<{
|
|
16
|
+
items: TItem[];
|
|
17
|
+
continuationToken: string | null;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
callbacks?: {
|
|
21
|
+
onChunkFullyLoaded: (chunk: PlayerChunk<TItem, TChunk>) => void;
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
get isLoading(): boolean;
|
|
25
|
+
get canLoadMore(): boolean;
|
|
26
|
+
get activeItemIndex(): number;
|
|
27
|
+
loadMore: () => Promise<TItem[]>;
|
|
28
|
+
setActiveItemIndex: (index: number) => Promise<void>;
|
|
29
|
+
mutateChunkItems: (items: PlayerChunkItem<TItem>[]) => void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ContinuationToken, Deferred } from '../../../../core';
|
|
2
|
+
import { CursorDataLoader } from '../../../../core/data-loaders';
|
|
3
|
+
import { PlayerChunkItem } from './player-chunk-item.svelte';
|
|
4
|
+
export class PlayerChunk {
|
|
5
|
+
model;
|
|
6
|
+
items = $derived.by(() => this._chunkItems.map((i) => i.model));
|
|
7
|
+
chunkItems = $derived.by(() => this._chunkItems);
|
|
8
|
+
activeChunkItem = $derived.by(() => this._chunkItems[this._activeItemIndex] ?? null);
|
|
9
|
+
_chunkItems = $state.raw([]);
|
|
10
|
+
_activeItemIndex = $state(0);
|
|
11
|
+
_fetchDeferred = $state.raw(null);
|
|
12
|
+
_itemsLoader;
|
|
13
|
+
constructor(data) {
|
|
14
|
+
const { chunk, provider, callbacks } = data;
|
|
15
|
+
this.model = chunk;
|
|
16
|
+
this._itemsLoader = new CursorDataLoader({
|
|
17
|
+
loadPage: async (continuationToken) => {
|
|
18
|
+
if (this._fetchDeferred) {
|
|
19
|
+
return await this._fetchDeferred.promise;
|
|
20
|
+
}
|
|
21
|
+
if (!this.canLoadMore) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
this._fetchDeferred = new Deferred();
|
|
26
|
+
const itemsResult = await provider.loadChunkItems(this.model.id, continuationToken.toNextChunkString());
|
|
27
|
+
const newItems = itemsResult.items;
|
|
28
|
+
this._chunkItems = [
|
|
29
|
+
...this._chunkItems,
|
|
30
|
+
...newItems.map((item) => new PlayerChunkItem({
|
|
31
|
+
model: item,
|
|
32
|
+
chunkId: this.model.id
|
|
33
|
+
}))
|
|
34
|
+
];
|
|
35
|
+
const continuationTokenResult = ContinuationToken.fromPayload(itemsResult.continuationToken);
|
|
36
|
+
if (!continuationTokenResult.canLoadMore) {
|
|
37
|
+
callbacks?.onChunkFullyLoaded(this);
|
|
38
|
+
}
|
|
39
|
+
const result = {
|
|
40
|
+
items: newItems,
|
|
41
|
+
continuationToken: continuationTokenResult
|
|
42
|
+
};
|
|
43
|
+
this._fetchDeferred.resolve(result);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
this._fetchDeferred?.resolve(null);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
this._fetchDeferred = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
get isLoading() {
|
|
57
|
+
return !!this._fetchDeferred;
|
|
58
|
+
}
|
|
59
|
+
get canLoadMore() {
|
|
60
|
+
return this._itemsLoader.continuationToken.canLoadMore;
|
|
61
|
+
}
|
|
62
|
+
get activeItemIndex() {
|
|
63
|
+
return this._activeItemIndex;
|
|
64
|
+
}
|
|
65
|
+
loadMore = () => this._itemsLoader.loadMore();
|
|
66
|
+
setActiveItemIndex = async (index) => {
|
|
67
|
+
this._activeItemIndex = index;
|
|
68
|
+
};
|
|
69
|
+
mutateChunkItems = (items) => {
|
|
70
|
+
this._chunkItems = items;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PlayerChunk } from './player-chunk.svelte';
|
|
2
|
+
import type { IChunksPlayerDataProvider, WithId } from '../types';
|
|
3
|
+
export declare class PlayerChunksManager<TItem extends WithId, TChunk extends WithId> {
|
|
4
|
+
private provider;
|
|
5
|
+
private _activeChunkIndex;
|
|
6
|
+
private _loadedChunks;
|
|
7
|
+
private _warmUpDeferred;
|
|
8
|
+
constructor(provider: IChunksPlayerDataProvider<TItem, TChunk>);
|
|
9
|
+
get activeChunk(): PlayerChunk<TItem, TChunk>;
|
|
10
|
+
get loadedChunks(): PlayerChunk<TItem, TChunk>[];
|
|
11
|
+
get isLoading(): boolean;
|
|
12
|
+
get flattenedChunkItems(): TItem[];
|
|
13
|
+
get flattenedActiveChunkItemIndex(): number;
|
|
14
|
+
tryActivateItemById: (id: string) => boolean;
|
|
15
|
+
removeItemById: (id: string) => boolean;
|
|
16
|
+
removeChunkById: (id: string) => boolean | undefined;
|
|
17
|
+
initialize: () => Promise<void>;
|
|
18
|
+
setActiveChunkIndex: (index: number, chunkItemIndex: number) => Promise<void>;
|
|
19
|
+
activateItemAtFlattenedIndex: (index: number) => Promise<void>;
|
|
20
|
+
warmUp: () => Promise<void>;
|
|
21
|
+
reset: () => Promise<void>;
|
|
22
|
+
private warmUpSequentially;
|
|
23
|
+
}
|