@arcblock/terminal 2.1.61

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/src/Player.js ADDED
@@ -0,0 +1,363 @@
1
+ /* eslint-disable react/no-unused-prop-types */
2
+ import { useReducer, useState, useRef, useEffect } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import useInterval from '@arcblock/react-hooks/lib/useInterval';
5
+ import useWindowSize from 'react-use/lib/useWindowSize';
6
+
7
+ import Terminal from './terminal';
8
+
9
+ import './player.css';
10
+
11
+ import {
12
+ formatFrames,
13
+ formatTime,
14
+ findFrameAt,
15
+ isFrameAt,
16
+ getFrameClass,
17
+ getPlayerClass,
18
+ defaultOptions,
19
+ defaultState,
20
+ } from './util';
21
+
22
+ export default function Player(props) {
23
+ const options = Object.assign({}, defaultOptions, props.options);
24
+ const { frames, totalDuration } = formatFrames(props.frames, options);
25
+
26
+ const terminalOptions = {
27
+ cols: options.cols,
28
+ rows: options.rows,
29
+ cursorStyle: options.cursorStyle,
30
+ fontFamily: options.fontFamily,
31
+ fontSize: options.fontSize,
32
+ lineHeight: options.lineHeight,
33
+ letterSpacing: options.letterSpacing,
34
+ allowTransparency: true,
35
+ scrollback: 0,
36
+ // theme: options.theme,
37
+ };
38
+
39
+ const stateReducer = (state, action) => {
40
+ // console.log(`dispatch.${action.type}`, action.payload);
41
+ switch (action.type) {
42
+ case 'jump':
43
+ return { ...state, isPlaying: false, ...action.payload };
44
+ case 'start':
45
+ return { ...state, isStarted: true, lastTickTime: Date.now() };
46
+ case 'play':
47
+ return {
48
+ ...state,
49
+ isPlaying: true,
50
+ lastTickTime: Date.now(),
51
+ ...action.payload,
52
+ };
53
+ case 'pause':
54
+ return { ...state, isPlaying: false, ...action.payload };
55
+ case 'tickStart':
56
+ return {
57
+ ...state,
58
+ isRendering: true,
59
+ lastTickTime: Date.now(),
60
+ ...action.payload,
61
+ };
62
+ case 'tickEnd':
63
+ return {
64
+ ...state,
65
+ isRendering: false,
66
+ lastTickTime: Date.now(),
67
+ ...action.payload,
68
+ };
69
+ case 'reset':
70
+ return {
71
+ ...state,
72
+ currentFrame: 0,
73
+ currentTime: 0,
74
+ ...action.payload,
75
+ };
76
+ default:
77
+ return { ...state, lastTickTime: Date.now(), ...action.payload };
78
+ }
79
+ };
80
+
81
+ const terminal = useRef(null);
82
+ const progress = useRef(null);
83
+ const container = useRef(null);
84
+ const [maxWidth, setMaxWidth] = useState(0);
85
+ const { width } = useWindowSize();
86
+ const [state, dispatch] = useReducer(stateReducer, defaultState);
87
+
88
+ useEffect(() => {
89
+ if (!terminal.current) {
90
+ return;
91
+ }
92
+
93
+ try {
94
+ const COLUMN_WIDTH = 972 / 121;
95
+ const child = container.current.getBoundingClientRect();
96
+ let containerWidth = child.x < 0 ? child.width + child.x : child.width;
97
+ if (container.current.parentNode) {
98
+ const parent = container.current.parentNode.getBoundingClientRect();
99
+ if (child.width > parent.width) {
100
+ containerWidth = parent.width;
101
+ }
102
+ }
103
+
104
+ if (options.controls) {
105
+ containerWidth -= 12;
106
+ }
107
+ const colContainer = Math.ceil(containerWidth / COLUMN_WIDTH);
108
+ const cols = Math.min(Math.max(colContainer, 40), options.cols);
109
+
110
+ terminal.current.resize(cols, options.rows);
111
+ } catch (err) {
112
+ // Do nothing
113
+ }
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ }, [width, maxWidth]);
116
+
117
+ // console.log('main.render', state, { totalFrame: frames.length, totalDuration });
118
+ // Render a frame
119
+ const renderFrame = (frameIndex, done) => {
120
+ const frame = frames[frameIndex];
121
+ if (frame.content) {
122
+ if (state.requireReset) {
123
+ terminal.current.reset();
124
+ }
125
+
126
+ terminal.current.write(frame.content, () => {
127
+ if (typeof done === 'function') {
128
+ done();
129
+ }
130
+ });
131
+ }
132
+ };
133
+
134
+ // Emit a event
135
+ const emitEvent = (name) => {
136
+ if (typeof props[name] === 'function') {
137
+ props[name]({ state, frames, options });
138
+ }
139
+ };
140
+
141
+ // Render thumbnailTime
142
+ useEffect(() => {
143
+ if (!terminal.current) {
144
+ return;
145
+ }
146
+ if (container.current) {
147
+ try {
148
+ setMaxWidth(container.current.getBoundingClientRect().width);
149
+ } catch (e) {
150
+ // Do nothing
151
+ }
152
+ }
153
+ if (options.autoplay) {
154
+ onStart();
155
+ } else {
156
+ doJump(Math.min(Math.abs(options.thumbnailTime), totalDuration));
157
+ }
158
+ // eslint-disable-next-line react-hooks/exhaustive-deps
159
+ }, []);
160
+
161
+ // Tick intervals
162
+ useInterval(
163
+ async () => {
164
+ if (state.isRendering) {
165
+ return false;
166
+ }
167
+
168
+ const tickDelay = Date.now() - state.lastTickTime;
169
+ const newState = {};
170
+
171
+ if (state.currentTime < totalDuration) {
172
+ newState.currentTime = state.currentTime + tickDelay;
173
+ }
174
+ if (state.currentTime > totalDuration) {
175
+ newState.currentTime = totalDuration;
176
+ }
177
+
178
+ const alreadyRendered = isFrameAt(frames, newState.currentTime, state.currentFrame);
179
+ if (state.currentFrame !== -1 && alreadyRendered) {
180
+ return dispatch({ type: 'tick', payload: newState });
181
+ }
182
+
183
+ // Reached the end
184
+ if (state.currentFrame === frames.length - 1) {
185
+ emitEvent('onComplete');
186
+
187
+ if (options.repeat) {
188
+ // console.log('tick.restart', newState, state);
189
+ return dispatch({ type: 'reset', payload: newState });
190
+ }
191
+
192
+ // console.log('tick.end', newState, state);
193
+ newState.currentTime = 0;
194
+ newState.currentFrame = 0;
195
+ newState.requireReset = true;
196
+ newState.isStarted = false;
197
+ return dispatch({ type: 'pause', payload: newState });
198
+ }
199
+
200
+ // Check if current time belongs to the next frame's duration
201
+ if (isFrameAt(newState.currentTime, state.currentFrame + 1)) {
202
+ newState.currentFrame = state.currentFrame + 1;
203
+ } else {
204
+ newState.currentFrame = findFrameAt(frames, newState.currentTime);
205
+ }
206
+
207
+ // console.log('tick.tick', newState, state);
208
+ dispatch({ type: 'tickStart', payload: newState });
209
+ return renderFrame(newState.currentFrame, () => {
210
+ if (state.requireReset) {
211
+ newState.requireReset = false;
212
+ }
213
+ dispatch({ type: 'tickEnd', payload: newState });
214
+ return emitEvent('onTick');
215
+ });
216
+ },
217
+ state.isPlaying ? 8 : null
218
+ );
219
+
220
+ // If controls are enabled, we need to disable frameBox
221
+ if (options.controls) {
222
+ options.frameBox.title = null;
223
+ options.frameBox.type = null;
224
+ options.frameBox.style = {};
225
+
226
+ if (options.theme.background === 'transparent') {
227
+ options.frameBox.style.background = 'black';
228
+ } else {
229
+ options.frameBox.style.background = options.theme.background;
230
+ }
231
+
232
+ options.frameBox.style.padding = '10px';
233
+ options.frameBox.style.paddingBottom = '40px';
234
+ }
235
+
236
+ const doJump = (time) => {
237
+ terminal.current.reset();
238
+
239
+ const toFrameIndex = findFrameAt(frames, time);
240
+ for (let i = 0; i < toFrameIndex; i++) {
241
+ renderFrame(i);
242
+ }
243
+ };
244
+
245
+ const onJump = (e) => {
246
+ if (!progress.current || !terminal.current || !state.isStarted) {
247
+ return false;
248
+ }
249
+
250
+ const length = progress.current.getBoundingClientRect().width;
251
+ const position = e.nativeEvent.offsetX;
252
+ // console.log('onJump', { length, position, e });
253
+
254
+ const currentTime = Math.floor((totalDuration * position) / length);
255
+ dispatch({ type: 'jump', payload: { currentTime } });
256
+ doJump(currentTime);
257
+ emitEvent('onJump');
258
+
259
+ return false;
260
+ };
261
+
262
+ const onStart = () => {
263
+ if (state.isStarted === false) {
264
+ dispatch({ type: 'start' });
265
+ terminal.current.reset();
266
+ }
267
+
268
+ dispatch({ type: 'play' });
269
+ emitEvent('onStart');
270
+
271
+ return false;
272
+ };
273
+
274
+ const onPause = () => {
275
+ dispatch({ type: 'pause' });
276
+ emitEvent('onPause');
277
+ return false;
278
+ };
279
+
280
+ const onPlay = async () => {
281
+ if (state.currentFrame === frames.length - 1 && state.currentTime === totalDuration) {
282
+ dispatch({ type: 'reset' });
283
+ terminal.current.reset();
284
+ }
285
+
286
+ emitEvent('onPlay');
287
+ return onStart();
288
+ };
289
+
290
+ return (
291
+ <div className={getPlayerClass(options, state)} ref={container}>
292
+ <div className="cover" onClick={onStart} />
293
+ <div className="start" onClick={onStart}>
294
+ <svg style={{ enableBackground: 'new 0 0 30 30' }} viewBox="0 0 30 30">
295
+ <polygon points="6.583,3.186 5,4.004 5,15 26,15 26.483,14.128 " />
296
+ <polygon points="6.583,26.814 5,25.996 5,15 26,15 26.483,15.872 " />
297
+ <circle cx="26" cy="15" r="1" />
298
+ <circle cx="6" cy="4" r="1" />
299
+ <circle cx="6" cy="26" r="1" />
300
+ </svg>
301
+ </div>
302
+ <div className="terminal">
303
+ <div className={getFrameClass(options)} style={options.frameBox.style || {}}>
304
+ <div className="terminal-titlebar">
305
+ <div className="buttons">
306
+ <div className="close-button" />
307
+ <div className="minimize-button" />
308
+ <div className="maximize-button" />
309
+ </div>
310
+ <div className="title">{options.frameBox.title || ''}</div>
311
+ </div>
312
+ <div className="terminal-body">
313
+ <Terminal ref={terminal} options={terminalOptions} />
314
+ </div>
315
+ </div>
316
+ </div>
317
+ <div className="controller">
318
+ {state.isPlaying && (
319
+ <div className="pause" onClick={onPause} title="Pause">
320
+ <span className="icon" />
321
+ </div>
322
+ )}
323
+ {!state.isPlaying && state.isStarted && (
324
+ <div className="play" onClick={onPlay} title="Play">
325
+ <span className="icon" />
326
+ </div>
327
+ )}
328
+ {!state.isPlaying && !state.isStarted && (
329
+ <div className="play" onClick={onStart} title="Start">
330
+ <span className="icon" />
331
+ </div>
332
+ )}
333
+ <div className="timer">{formatTime(state.currentTime)}</div>
334
+ <div className="progressbar-wrapper">
335
+ <div className="progressbar" ref={progress} onClick={onJump}>
336
+ <div className="progress" style={{ width: `${(state.currentTime / totalDuration) * 100}%` }} />
337
+ </div>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ );
342
+ }
343
+
344
+ Player.propTypes = {
345
+ frames: PropTypes.array.isRequired,
346
+ options: PropTypes.object.isRequired,
347
+ onComplete: PropTypes.func,
348
+ onStart: PropTypes.func,
349
+ onStop: PropTypes.func,
350
+ onPause: PropTypes.func,
351
+ onTick: PropTypes.func,
352
+ onJump: PropTypes.func,
353
+ };
354
+
355
+ const noop = () => {};
356
+ Player.defaultProps = {
357
+ onComplete: noop,
358
+ onStart: noop,
359
+ onStop: noop,
360
+ onPause: noop,
361
+ onTick: noop,
362
+ onJump: noop,
363
+ };
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line no-restricted-exports
2
+ export { default } from './Player';