@ldelia/react-media 0.9.0 → 0.11.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/components/reproduction-widget/models/Reproduction.d.ts +14 -6
- package/dist/components/reproduction-widget/models/Reproduction.js +75 -20
- package/dist/components/timeline/RangeSelectorCanvas/RangeSelectorCanvas.js +8 -2
- package/dist/stories/timeline.stories.d.ts +1 -0
- package/dist/stories/timeline.stories.js +68 -1
- package/package.json +7 -7
|
@@ -2,8 +2,14 @@ import { ReproductionBuilder } from './ReproductionBuilder';
|
|
|
2
2
|
import { PlayAlongPlayer } from './Player/PlayAlongPlayer';
|
|
3
3
|
import { YouTubePlayer } from './Player/YouTubePlayer';
|
|
4
4
|
type Player = PlayAlongPlayer | YouTubePlayer;
|
|
5
|
+
export declare const REPRODUCTION_STATES: {
|
|
6
|
+
STOPPED: number;
|
|
7
|
+
COUNTING_IN: number;
|
|
8
|
+
PLAYING: number;
|
|
9
|
+
PAUSED: number;
|
|
10
|
+
};
|
|
11
|
+
type ReproductionState = (typeof REPRODUCTION_STATES)[keyof typeof REPRODUCTION_STATES];
|
|
5
12
|
type Handler = (args: object) => void;
|
|
6
|
-
declare const dispatchOnReadyHandlers: unique symbol;
|
|
7
13
|
declare const dispatchOnSongStartHandlers: unique symbol;
|
|
8
14
|
declare const dispatchOnCountingInHandlers: unique symbol;
|
|
9
15
|
declare const dispatchOnPlayHandlers: unique symbol;
|
|
@@ -12,15 +18,14 @@ declare const dispatchOnPausedHandlers: unique symbol;
|
|
|
12
18
|
declare const dispatchOnFinishHandlers: unique symbol;
|
|
13
19
|
declare const dispatchOnErrorHandlers: unique symbol;
|
|
14
20
|
export declare class Reproduction {
|
|
15
|
-
private player;
|
|
16
|
-
private requiresCountingIn;
|
|
17
|
-
private songTempo;
|
|
21
|
+
private readonly player;
|
|
22
|
+
private readonly requiresCountingIn;
|
|
23
|
+
private readonly songTempo;
|
|
18
24
|
private state;
|
|
19
25
|
private interval;
|
|
20
26
|
private loopInterval;
|
|
21
27
|
private loopRange;
|
|
22
28
|
private countingInCounter;
|
|
23
|
-
private [dispatchOnReadyHandlers];
|
|
24
29
|
private [dispatchOnSongStartHandlers];
|
|
25
30
|
private [dispatchOnCountingInHandlers];
|
|
26
31
|
private [dispatchOnPlayHandlers];
|
|
@@ -45,7 +50,8 @@ export declare class Reproduction {
|
|
|
45
50
|
PAUSED: number;
|
|
46
51
|
};
|
|
47
52
|
static newBuilder(): ReproductionBuilder;
|
|
48
|
-
on(eventName: keyof typeof Reproduction.EVENTS, handler: Handler):
|
|
53
|
+
on(eventName: keyof typeof Reproduction.EVENTS, handler: Handler): () => void;
|
|
54
|
+
off(eventName: keyof typeof Reproduction.EVENTS, handler: Handler): void;
|
|
49
55
|
dispatch(eventName: keyof typeof Reproduction.EVENTS, args?: {}): void;
|
|
50
56
|
start(): void;
|
|
51
57
|
play(): void;
|
|
@@ -56,6 +62,7 @@ export declare class Reproduction {
|
|
|
56
62
|
isStopped(): boolean;
|
|
57
63
|
isPaused(): boolean;
|
|
58
64
|
isCountingIn(): boolean;
|
|
65
|
+
getState(): ReproductionState;
|
|
59
66
|
getPlayer(): Player;
|
|
60
67
|
getTempo(): number;
|
|
61
68
|
getCurrentTime(): number;
|
|
@@ -63,6 +70,7 @@ export declare class Reproduction {
|
|
|
63
70
|
seekTo(seconds: number): void;
|
|
64
71
|
getVolume(): number;
|
|
65
72
|
setVolume(volume: number): void;
|
|
73
|
+
setLoopRange(from: number, to: number): boolean;
|
|
66
74
|
getAvailablePlaybackRates(): number[];
|
|
67
75
|
setPlaybackRate(playbackRate: number): void;
|
|
68
76
|
isAvailable(): boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLAYER_EVENTS } from './Player/PlayerEvents';
|
|
2
2
|
import { ReproductionBuilder } from './ReproductionBuilder';
|
|
3
|
-
const
|
|
3
|
+
export const REPRODUCTION_STATES = {
|
|
4
4
|
STOPPED: 0,
|
|
5
5
|
COUNTING_IN: 1,
|
|
6
6
|
PLAYING: 2,
|
|
@@ -15,7 +15,6 @@ const EVENTS = {
|
|
|
15
15
|
FINISH: 'FINISH',
|
|
16
16
|
ERROR: 'ERROR',
|
|
17
17
|
};
|
|
18
|
-
const dispatchOnReadyHandlers = Symbol();
|
|
19
18
|
const dispatchOnSongStartHandlers = Symbol();
|
|
20
19
|
const dispatchOnCountingInHandlers = Symbol();
|
|
21
20
|
const dispatchOnPlayHandlers = Symbol();
|
|
@@ -25,7 +24,6 @@ const dispatchOnFinishHandlers = Symbol();
|
|
|
25
24
|
const dispatchOnErrorHandlers = Symbol();
|
|
26
25
|
export class Reproduction {
|
|
27
26
|
constructor(player, requiresCountingIn, songTempo, volume) {
|
|
28
|
-
this[dispatchOnReadyHandlers] = [];
|
|
29
27
|
this[dispatchOnSongStartHandlers] = [];
|
|
30
28
|
this[dispatchOnCountingInHandlers] = [];
|
|
31
29
|
this[dispatchOnPlayHandlers] = [];
|
|
@@ -62,29 +60,75 @@ export class Reproduction {
|
|
|
62
60
|
return EVENTS;
|
|
63
61
|
}
|
|
64
62
|
static get STATES() {
|
|
65
|
-
return
|
|
63
|
+
return REPRODUCTION_STATES;
|
|
66
64
|
}
|
|
67
65
|
static newBuilder() {
|
|
68
66
|
return new ReproductionBuilder();
|
|
69
67
|
}
|
|
70
68
|
on(eventName, handler) {
|
|
69
|
+
if (typeof handler !== 'function') {
|
|
70
|
+
throw new Error('Handler must be a function');
|
|
71
|
+
}
|
|
71
72
|
switch (eventName) {
|
|
72
73
|
case Reproduction.EVENTS.START:
|
|
73
|
-
|
|
74
|
+
this[dispatchOnSongStartHandlers].push(handler);
|
|
75
|
+
break;
|
|
74
76
|
case Reproduction.EVENTS.COUNTING_IN:
|
|
75
|
-
|
|
77
|
+
this[dispatchOnCountingInHandlers].push(handler);
|
|
78
|
+
break;
|
|
76
79
|
case Reproduction.EVENTS.PLAY:
|
|
77
|
-
|
|
80
|
+
this[dispatchOnPlayHandlers].push(handler);
|
|
81
|
+
break;
|
|
78
82
|
case Reproduction.EVENTS.PLAYING:
|
|
79
|
-
|
|
83
|
+
this[dispatchOnPlayingHandlers].push(handler);
|
|
84
|
+
break;
|
|
80
85
|
case Reproduction.EVENTS.PAUSED:
|
|
81
|
-
|
|
86
|
+
this[dispatchOnPausedHandlers].push(handler);
|
|
87
|
+
break;
|
|
82
88
|
case Reproduction.EVENTS.FINISH:
|
|
83
|
-
|
|
89
|
+
this[dispatchOnFinishHandlers].push(handler);
|
|
90
|
+
break;
|
|
84
91
|
case Reproduction.EVENTS.ERROR:
|
|
85
|
-
|
|
92
|
+
this[dispatchOnErrorHandlers].push(handler);
|
|
93
|
+
break;
|
|
86
94
|
default:
|
|
95
|
+
throw new Error(`Unknown event: ${eventName}`);
|
|
96
|
+
}
|
|
97
|
+
return () => this.off(eventName, handler);
|
|
98
|
+
}
|
|
99
|
+
off(eventName, handler) {
|
|
100
|
+
if (typeof handler !== 'function') {
|
|
101
|
+
throw new Error('Handler must be a function');
|
|
102
|
+
}
|
|
103
|
+
let handlers;
|
|
104
|
+
switch (eventName) {
|
|
105
|
+
case Reproduction.EVENTS.START:
|
|
106
|
+
handlers = this[dispatchOnSongStartHandlers];
|
|
87
107
|
break;
|
|
108
|
+
case Reproduction.EVENTS.COUNTING_IN:
|
|
109
|
+
handlers = this[dispatchOnCountingInHandlers];
|
|
110
|
+
break;
|
|
111
|
+
case Reproduction.EVENTS.PLAY:
|
|
112
|
+
handlers = this[dispatchOnPlayHandlers];
|
|
113
|
+
break;
|
|
114
|
+
case Reproduction.EVENTS.PLAYING:
|
|
115
|
+
handlers = this[dispatchOnPlayingHandlers];
|
|
116
|
+
break;
|
|
117
|
+
case Reproduction.EVENTS.PAUSED:
|
|
118
|
+
handlers = this[dispatchOnPausedHandlers];
|
|
119
|
+
break;
|
|
120
|
+
case Reproduction.EVENTS.FINISH:
|
|
121
|
+
handlers = this[dispatchOnFinishHandlers];
|
|
122
|
+
break;
|
|
123
|
+
case Reproduction.EVENTS.ERROR:
|
|
124
|
+
handlers = this[dispatchOnErrorHandlers];
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
throw new Error(`Unknown event: ${eventName}`);
|
|
128
|
+
}
|
|
129
|
+
const index = handlers.indexOf(handler);
|
|
130
|
+
if (index > -1) {
|
|
131
|
+
handlers.splice(index, 1);
|
|
88
132
|
}
|
|
89
133
|
}
|
|
90
134
|
dispatch(eventName, args = {}) {
|
|
@@ -136,23 +180,20 @@ export class Reproduction {
|
|
|
136
180
|
play() {
|
|
137
181
|
clearInterval(this.interval);
|
|
138
182
|
this.player.play();
|
|
139
|
-
const
|
|
183
|
+
const bpmInterval = this.getBPMInterval();
|
|
184
|
+
const tickInterval = Math.min(bpmInterval / 4, 50);
|
|
140
185
|
this.interval = setInterval(() => {
|
|
141
186
|
if (this.isPlaying()) {
|
|
142
187
|
this.dispatch(Reproduction.EVENTS.PLAYING);
|
|
143
188
|
}
|
|
144
|
-
},
|
|
189
|
+
}, tickInterval);
|
|
145
190
|
}
|
|
146
191
|
playLoop(from, to) {
|
|
147
|
-
if (!
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (to <= from) {
|
|
192
|
+
if (!this.setLoopRange(from, to)) {
|
|
151
193
|
return;
|
|
152
194
|
}
|
|
153
195
|
clearInterval(this.loopInterval);
|
|
154
196
|
this.loopInterval = null;
|
|
155
|
-
this.loopRange = { from, to };
|
|
156
197
|
this.seekTo(from);
|
|
157
198
|
this.play();
|
|
158
199
|
const loopCheckInterval = 100;
|
|
@@ -196,6 +237,9 @@ export class Reproduction {
|
|
|
196
237
|
isCountingIn() {
|
|
197
238
|
return this.state === Reproduction.STATES.COUNTING_IN;
|
|
198
239
|
}
|
|
240
|
+
getState() {
|
|
241
|
+
return this.state;
|
|
242
|
+
}
|
|
199
243
|
getPlayer() {
|
|
200
244
|
return this.player;
|
|
201
245
|
}
|
|
@@ -224,6 +268,13 @@ export class Reproduction {
|
|
|
224
268
|
}
|
|
225
269
|
this.player.setVolume(volume);
|
|
226
270
|
}
|
|
271
|
+
setLoopRange(from, to) {
|
|
272
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
this.loopRange = { from, to };
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
227
278
|
getAvailablePlaybackRates() {
|
|
228
279
|
return this.player.getAvailablePlaybackRates();
|
|
229
280
|
}
|
|
@@ -239,7 +290,9 @@ export class Reproduction {
|
|
|
239
290
|
countInAndPlay(timeout, limit) {
|
|
240
291
|
// the initial count starts instantly, no need to wait
|
|
241
292
|
this.countingInCounter++;
|
|
242
|
-
this.dispatch(Reproduction.EVENTS.COUNTING_IN, {
|
|
293
|
+
this.dispatch(Reproduction.EVENTS.COUNTING_IN, {
|
|
294
|
+
countingInCounter: this.countingInCounter,
|
|
295
|
+
});
|
|
243
296
|
const interval = setInterval(() => {
|
|
244
297
|
this.countingInCounter++;
|
|
245
298
|
if (this.countingInCounter === limit) {
|
|
@@ -253,7 +306,9 @@ export class Reproduction {
|
|
|
253
306
|
}
|
|
254
307
|
}
|
|
255
308
|
else {
|
|
256
|
-
this.dispatch(Reproduction.EVENTS.COUNTING_IN, {
|
|
309
|
+
this.dispatch(Reproduction.EVENTS.COUNTING_IN, {
|
|
310
|
+
countingInCounter: this.countingInCounter,
|
|
311
|
+
});
|
|
257
312
|
}
|
|
258
313
|
}, timeout);
|
|
259
314
|
}
|
|
@@ -45,7 +45,7 @@ const RangeSelectorCanvas = ({ selectedRange, onChange, onRangeChange, }) => {
|
|
|
45
45
|
context.fillRect(pixelX0, 0, pixelX1 - pixelX0, context.canvas.height);
|
|
46
46
|
context.globalAlpha = 1.0;
|
|
47
47
|
};
|
|
48
|
-
// Check if mouse is near the start or end edge of the selection
|
|
48
|
+
// Check if the mouse pointer is near the start or end edge of the selection
|
|
49
49
|
const isNearSelectionEdge = (pixel) => {
|
|
50
50
|
if (selectedRange.length !== 2)
|
|
51
51
|
return { isNear: false, edge: null };
|
|
@@ -180,4 +180,10 @@ const RangeSelectorCanvas = ({ selectedRange, onChange, onRangeChange, }) => {
|
|
|
180
180
|
}, [selection, zoomContextValue]);
|
|
181
181
|
return (React.createElement(OverlayCanvas, { ref: canvasRef, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, style: { cursor: cursorStyle }, className: 'media-timeline-range-selector-canvas' }));
|
|
182
182
|
};
|
|
183
|
-
|
|
183
|
+
const areEqual = (prevProps, nextProps) => {
|
|
184
|
+
return (prevProps.selectedRange[0] === nextProps.selectedRange[0] &&
|
|
185
|
+
prevProps.selectedRange[1] === nextProps.selectedRange[1] &&
|
|
186
|
+
prevProps.onChange === nextProps.onChange &&
|
|
187
|
+
prevProps.onRangeChange === nextProps.onRangeChange);
|
|
188
|
+
};
|
|
189
|
+
export default React.memo(RangeSelectorCanvas, areEqual);
|
|
@@ -9,3 +9,4 @@ export declare const WithCustomClassName: import("storybook/internal/csf").Annot
|
|
|
9
9
|
export declare const WithoutTimeBlocks: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
|
|
10
10
|
export declare const Minimalist: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
|
|
11
11
|
export declare const WithSelectedRangeAndMarkers: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
|
|
12
|
+
export declare const MediaPlaybackSimulation: import("storybook/internal/csf").AnnotatedStoryFn<import("@storybook/react").ReactRenderer, TimelineProps>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { Timeline } from '../components/timeline';
|
|
4
4
|
import './timeline.stories.custom.css';
|
|
@@ -17,6 +17,65 @@ export default {
|
|
|
17
17
|
},
|
|
18
18
|
};
|
|
19
19
|
const Template = (args) => (React.createElement(StyledTimeline, Object.assign({}, args)));
|
|
20
|
+
const PlaybackSimulation = (args) => {
|
|
21
|
+
const [currentValue, setCurrentValue] = useState(args.value || 0);
|
|
22
|
+
const [selectedRange, setSelectedRange] = useState(null);
|
|
23
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
24
|
+
const intervalRef = useRef(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isPlaying && currentValue < args.duration) {
|
|
27
|
+
intervalRef.current = setInterval(() => {
|
|
28
|
+
setCurrentValue((prev) => {
|
|
29
|
+
const next = prev + 0.05; // Advance 50ms (0.05 seconds)
|
|
30
|
+
return next >= args.duration ? args.duration : next;
|
|
31
|
+
});
|
|
32
|
+
}, 50); // Update every 50ms
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (intervalRef.current) {
|
|
36
|
+
clearInterval(intervalRef.current);
|
|
37
|
+
intervalRef.current = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return () => {
|
|
41
|
+
if (intervalRef.current) {
|
|
42
|
+
clearInterval(intervalRef.current);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}, [isPlaying, currentValue, args.duration]);
|
|
46
|
+
const handlePlayPause = () => {
|
|
47
|
+
if (currentValue >= args.duration) {
|
|
48
|
+
setCurrentValue(0); // Reset to beginning if we reached the end
|
|
49
|
+
}
|
|
50
|
+
setIsPlaying(!isPlaying);
|
|
51
|
+
};
|
|
52
|
+
const handleReset = () => {
|
|
53
|
+
setIsPlaying(false);
|
|
54
|
+
setCurrentValue(0);
|
|
55
|
+
};
|
|
56
|
+
const onRangeChangeHandler = (values) => {
|
|
57
|
+
setSelectedRange(values);
|
|
58
|
+
};
|
|
59
|
+
return (React.createElement("div", null,
|
|
60
|
+
React.createElement("div", { style: { marginBottom: '10px' } },
|
|
61
|
+
React.createElement("button", { onClick: handlePlayPause, style: { marginRight: '10px' } }, isPlaying ? 'Pause' : 'Play'),
|
|
62
|
+
React.createElement("button", { onClick: handleReset }, "Reset"),
|
|
63
|
+
React.createElement("span", { style: { marginLeft: '20px' } },
|
|
64
|
+
"Time: ",
|
|
65
|
+
currentValue.toFixed(2),
|
|
66
|
+
"s / ",
|
|
67
|
+
args.duration,
|
|
68
|
+
"s"),
|
|
69
|
+
React.createElement("span", { style: { marginLeft: '20px' } },
|
|
70
|
+
"Selected range:",
|
|
71
|
+
' ',
|
|
72
|
+
selectedRange ? `${selectedRange[0]}-${selectedRange[1]}` : 'N/A')),
|
|
73
|
+
React.createElement(StyledTimeline, Object.assign({}, args, { value: currentValue, onChange: (value) => {
|
|
74
|
+
var _a;
|
|
75
|
+
setCurrentValue(value);
|
|
76
|
+
(_a = args.onChange) === null || _a === void 0 ? void 0 : _a.call(args, value);
|
|
77
|
+
}, onRangeChange: onRangeChangeHandler }))));
|
|
78
|
+
};
|
|
20
79
|
export const Default = Template.bind({});
|
|
21
80
|
Default.args = {
|
|
22
81
|
duration: 305,
|
|
@@ -61,3 +120,11 @@ WithSelectedRangeAndMarkers.args = {
|
|
|
61
120
|
selectedRange: [20, 30],
|
|
62
121
|
markers: [90, 108],
|
|
63
122
|
};
|
|
123
|
+
export const MediaPlaybackSimulation = PlaybackSimulation.bind({});
|
|
124
|
+
MediaPlaybackSimulation.args = {
|
|
125
|
+
duration: 120, // 2 minutes
|
|
126
|
+
value: 0,
|
|
127
|
+
zoomLevel: 0,
|
|
128
|
+
selectedRange: [30, 45], // Show a selected range
|
|
129
|
+
markers: [15, 60, 90], // Add some markers
|
|
130
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ldelia/react-media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A React components collection for media-related features.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": [
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"@babel/preset-react": "^7.24.7",
|
|
115
115
|
"@babel/preset-typescript": "^7.24.7",
|
|
116
116
|
"@chromatic-com/storybook": "^5.0.1",
|
|
117
|
-
"@storybook/addon-docs": "^10.2.
|
|
118
|
-
"@storybook/addon-links": "^10.2.
|
|
119
|
-
"@storybook/addon-onboarding": "^10.2.
|
|
117
|
+
"@storybook/addon-docs": "^10.2.13",
|
|
118
|
+
"@storybook/addon-links": "^10.2.13",
|
|
119
|
+
"@storybook/addon-onboarding": "^10.2.13",
|
|
120
120
|
"@storybook/react-docgen-typescript-plugin": "^1.0.6--canary.9.0c3f3b7.0",
|
|
121
|
-
"@storybook/react-webpack5": "^10.2.
|
|
121
|
+
"@storybook/react-webpack5": "^10.2.13",
|
|
122
122
|
"@testing-library/jest-dom": "^6.4.6",
|
|
123
123
|
"@testing-library/react": "^16.0.0",
|
|
124
124
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"babel-loader": "^9.1.3",
|
|
132
132
|
"eslint-config-prettier": "^9.1.0",
|
|
133
133
|
"eslint-plugin-prettier": "^5.1.3",
|
|
134
|
-
"eslint-plugin-storybook": "^10.2.
|
|
134
|
+
"eslint-plugin-storybook": "^10.2.13",
|
|
135
135
|
"jest-environment-jsdom": "^30.2.0",
|
|
136
136
|
"postcss-flexbugs-fixes": "^5.0.2",
|
|
137
137
|
"postcss-normalize": "^10.0.1",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"prettier": "^3.3.2",
|
|
140
140
|
"prop-types": "^15.8.1",
|
|
141
141
|
"react-docgen-typescript-plugin": "^1.0.8",
|
|
142
|
-
"storybook": "^10.2.
|
|
142
|
+
"storybook": "^10.2.13",
|
|
143
143
|
"ts-loader": "^9.5.1",
|
|
144
144
|
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
|
145
145
|
"webpack": "^5.105.2"
|