@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/LICENSE +13 -0
- package/README.md +19 -0
- package/lib/Player.js +464 -0
- package/lib/index.js +15 -0
- package/lib/player.css +378 -0
- package/lib/terminal.js +180 -0
- package/lib/util.js +193 -0
- package/lib/xterm.css +171 -0
- package/package.json +68 -0
- package/src/Player.js +363 -0
- package/src/index.js +2 -0
- package/src/player.css +378 -0
- package/src/terminal.js +151 -0
- package/src/util.js +167 -0
- package/src/xterm.css +171 -0
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