@arcblock/terminal 3.4.15 → 3.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/terminal",
3
- "version": "3.4.15",
3
+ "version": "3.5.0",
4
4
  "description": "A react wrapper for xterm allowing you to easily render a terminal in the browser",
5
5
  "keywords": [
6
6
  "react",
@@ -30,6 +30,9 @@
30
30
  "bugs": {
31
31
  "url": "https://github.com/ArcBlock/ux/issues"
32
32
  },
33
+ "files": [
34
+ "lib"
35
+ ],
33
36
  "publishConfig": {
34
37
  "access": "public"
35
38
  },
@@ -40,10 +43,10 @@
40
43
  "peerDependencies": {
41
44
  "react": "^19.0.0"
42
45
  },
43
- "gitHead": "73039d82d507d9ec5408d8cfc67d9efeffae3e74",
46
+ "gitHead": "1cfc816004525cf1b352ec2b64d459f4769f0237",
44
47
  "dependencies": {
45
- "@arcblock/react-hooks": "3.4.15",
46
- "@arcblock/ux": "3.4.15",
48
+ "@arcblock/react-hooks": "3.5.0",
49
+ "@arcblock/ux": "3.5.0",
47
50
  "@emotion/react": "^11.14.0",
48
51
  "@emotion/styled": "^11.14.0",
49
52
  "@xterm/addon-fit": "^0.10.0",
package/src/Player.jsx DELETED
@@ -1,532 +0,0 @@
1
- /* eslint-disable react/no-unused-prop-types, no-console */
2
- import { useReducer, useRef, useEffect, useCallback } from 'react';
3
- import PropTypes from 'prop-types';
4
- import isUndefined from 'lodash/isUndefined';
5
- import noop from 'lodash/noop';
6
-
7
- import Terminal from './Terminal';
8
-
9
- import { PlayerRoot } from './styles';
10
-
11
- import {
12
- formatFrames,
13
- formatTime,
14
- findFrameAt,
15
- getFrameClass,
16
- getPlayerClass,
17
- defaultOptions,
18
- defaultState,
19
- } from './util';
20
-
21
- export const PLAYER_FRAME_DELAY = 8;
22
-
23
- export default function Player({ ...rawProps }) {
24
- const props = Object.assign({}, rawProps);
25
- if (isUndefined(props.onComplete)) {
26
- props.onComplete = noop;
27
- }
28
- if (isUndefined(props.onStart)) {
29
- props.onStart = noop;
30
- }
31
- if (isUndefined(props.onStop)) {
32
- props.onStop = noop;
33
- }
34
- if (isUndefined(props.onPause)) {
35
- props.onPause = noop;
36
- }
37
- if (isUndefined(props.onTick)) {
38
- props.onTick = noop;
39
- }
40
- if (isUndefined(props.onJump)) {
41
- props.onJump = noop;
42
- }
43
-
44
- const options = Object.assign({}, defaultOptions, props.options);
45
- const { frames, totalDuration } = formatFrames(props.frames, options);
46
-
47
- const terminalOptions = {
48
- cols: options.cols,
49
- rows: options.rows,
50
- cursorStyle: options.cursorStyle,
51
- cursorBlink: options.cursorBlink ?? true,
52
- fontFamily: options.fontFamily,
53
- fontSize: options.fontSize,
54
- lineHeight: options.lineHeight,
55
- letterSpacing: options.letterSpacing,
56
- allowTransparency: true,
57
- scrollback: 0,
58
- theme: options.enableTheme ? options.theme : {},
59
- };
60
-
61
- const stateReducer = (state, action) => {
62
- // console.log(`dispatch.${action.type}`, action.payload);
63
- switch (action.type) {
64
- case 'jump':
65
- return { ...state, ...action.payload };
66
- case 'start':
67
- return { ...state, isStarted: true, lastTickTime: Date.now() };
68
- case 'play':
69
- return { ...state, isPlaying: true, lastTickTime: Date.now(), ...action.payload };
70
- case 'pause':
71
- return { ...state, isPlaying: false, ...action.payload };
72
- case 'tickStart':
73
- return { ...state, isRendering: true, lastTickTime: Date.now(), ...action.payload };
74
- case 'tickEnd':
75
- return { ...state, isRendering: false, lastTickTime: Date.now(), ...action.payload };
76
- case 'reset':
77
- return { ...state, currentFrame: -1, currentTime: 0, ...action.payload };
78
- default:
79
- return { ...state, lastTickTime: Date.now(), ...action.payload };
80
- }
81
- };
82
-
83
- const terminal = useRef(null);
84
- const progress = useRef(null);
85
- const container = useRef(null);
86
- const animationRef = useRef(null);
87
- const startTimeRef = useRef(null);
88
- const autoPlayRef = useRef(false);
89
- const [state, dispatch] = useReducer(stateReducer, defaultState);
90
-
91
- // Render a frame with Promise-based approach
92
- const renderFrame = useCallback(
93
- (frameIndex) => {
94
- return new Promise((resolve) => {
95
- const frame = frames[frameIndex];
96
- if (frame && frame.content && terminal.current) {
97
- if (state.requireReset) {
98
- terminal.current.reset();
99
- }
100
-
101
- terminal.current.write(frame.content, () => {
102
- resolve();
103
- });
104
- } else {
105
- resolve();
106
- }
107
- });
108
- },
109
- [frames, state.requireReset]
110
- );
111
-
112
- // Render a frame synchronously for jump operations (no delay)
113
- const renderFrameSync = useCallback(
114
- (frameIndex) => {
115
- const frame = frames[frameIndex];
116
- if (frame && frame.content && terminal.current) {
117
- if (state.requireReset) {
118
- terminal.current.reset();
119
- }
120
- // Use synchronous write without callback for instant rendering
121
- terminal.current.write(frame.content);
122
- }
123
- },
124
- [frames, state.requireReset]
125
- );
126
-
127
- // Emit a event
128
- const emitEvent = useCallback(
129
- (name) => {
130
- if (typeof props[name] === 'function') {
131
- props[name]({ state, frames, options });
132
- }
133
- },
134
- [props, state, frames, options]
135
- );
136
-
137
- const doJump = useCallback(
138
- (time) => {
139
- if (!terminal.current) return;
140
-
141
- terminal.current.reset();
142
- const toFrameIndex = findFrameAt(frames, time);
143
-
144
- if (toFrameIndex >= 0) {
145
- // Render all frames up to the target frame synchronously for instant jump
146
- for (let i = 0; i <= toFrameIndex; i++) {
147
- renderFrameSync(i);
148
- }
149
- }
150
- },
151
- [frames, renderFrameSync]
152
- );
153
-
154
- const onJump = useCallback(
155
- (e) => {
156
- if (!progress.current || !terminal.current || !state.isStarted) {
157
- return false;
158
- }
159
-
160
- const length = progress.current.getBoundingClientRect().width;
161
- const position = e.nativeEvent.offsetX;
162
-
163
- const currentTime = Math.floor((totalDuration * position) / length);
164
- const targetFrameIndex = findFrameAt(frames, currentTime);
165
-
166
- // Preserve the current playing state
167
- const isCurrentlyPlaying = state.isPlaying;
168
-
169
- // Update state to reflect the jump while preserving play state
170
- dispatch({
171
- type: 'jump',
172
- payload: {
173
- currentTime,
174
- currentFrame: targetFrameIndex,
175
- isPlaying: isCurrentlyPlaying, // Keep the same playing state
176
- },
177
- });
178
-
179
- // Reset the timing for accurate playback after jump
180
- startTimeRef.current = null;
181
-
182
- // Perform the jump
183
- doJump(currentTime);
184
- emitEvent('onJump');
185
-
186
- return false;
187
- },
188
- [state.isStarted, state.isPlaying, totalDuration, frames, doJump, emitEvent]
189
- );
190
-
191
- const onPlay = useCallback(() => {
192
- if (state.currentFrame >= frames.length - 1) {
193
- // 触发一下 resize,保证文字输出正常
194
- terminal.current.resize();
195
- // Reset to beginning if at the end
196
- dispatch({ type: 'reset', payload: { currentFrame: -1, currentTime: 0 } });
197
- if (terminal.current) {
198
- terminal.current.reset();
199
- }
200
- startTimeRef.current = null;
201
- // Start from beginning
202
- dispatch({ type: 'play', payload: { currentFrame: -1, currentTime: 0 } });
203
- } else {
204
- // Continue from current position
205
- startTimeRef.current = null; // Reset timing for accurate playback
206
- dispatch({ type: 'play' });
207
- }
208
-
209
- emitEvent('onPlay');
210
- return false;
211
- }, [state.currentFrame, frames.length, emitEvent]);
212
-
213
- const onStart = useCallback(() => {
214
- if (state.isStarted === false) {
215
- // 触发一下 resize,保证文字输出正常
216
- terminal.current.resize();
217
-
218
- dispatch({ type: 'start' });
219
- if (terminal.current) {
220
- terminal.current.reset();
221
- }
222
- }
223
-
224
- // Reset start time and frame for accurate timing
225
- startTimeRef.current = null;
226
- dispatch({ type: 'play', payload: { currentFrame: -1, currentTime: 0 } });
227
- emitEvent('onStart');
228
-
229
- return false;
230
- }, [state.isStarted, emitEvent]);
231
-
232
- const onPause = useCallback(() => {
233
- dispatch({ type: 'pause' });
234
- emitEvent('onPause');
235
- return false;
236
- }, [emitEvent]);
237
-
238
- // Intersection Observer for viewport detection
239
- useEffect(() => {
240
- if (!container.current || !options.autoplay || autoPlayRef.current) {
241
- return;
242
- }
243
-
244
- const observer = new IntersectionObserver(
245
- (entries) => {
246
- const [entry] = entries;
247
- if (entry.isIntersecting && !state.isStarted) {
248
- // 根据视野范围自动开始播放
249
- onStart();
250
- autoPlayRef.current = true;
251
- // 播放后,断开 observer,避免触发多次
252
- observer.unobserve(container.current);
253
- observer.disconnect();
254
- }
255
- },
256
- {
257
- threshold: options.autoplayThreshold, // 可配置的可见度阈值
258
- rootMargin: options.autoplayRootMargin, // 可配置的根边距
259
- }
260
- );
261
-
262
- observer.observe(container.current);
263
-
264
- // eslint-disable-next-line consistent-return
265
- return () => {
266
- observer.disconnect();
267
- };
268
- }, [options.autoplay, options.autoplayThreshold, options.autoplayRootMargin, state.isStarted, onStart]);
269
-
270
- // Render thumbnailTime
271
- useEffect(() => {
272
- if (!terminal.current) {
273
- return;
274
- }
275
-
276
- // Focus terminal to ensure cursor is visible
277
- terminal.current.focus();
278
-
279
- if (!options.autoplay) {
280
- doJump(Math.min(Math.abs(options.thumbnailTime), totalDuration));
281
- }
282
- // eslint-disable-next-line react-hooks/exhaustive-deps
283
- }, []);
284
-
285
- // Use ref to store the latest state and avoid recreating the loop function
286
- const stateRef = useRef(state);
287
- stateRef.current = state;
288
-
289
- // Animation loop - frame-based sequential rendering
290
- const animationLoop = useCallback(async () => {
291
- const currentState = stateRef.current;
292
-
293
- if (!currentState.isPlaying || currentState.isRendering) {
294
- return;
295
- }
296
-
297
- const now = performance.now();
298
- if (!startTimeRef.current) {
299
- // Calculate start time based on current frame position
300
- const currentFrameTime = currentState.currentFrame >= 0 ? frames[currentState.currentFrame]?.startTime || 0 : 0;
301
- startTimeRef.current = now - currentFrameTime;
302
- }
303
-
304
- const elapsed = now - startTimeRef.current;
305
-
306
- // Check if it's time to render the next frame
307
- const { currentFrame } = currentState;
308
- const nextFrameIndex = currentFrame + 1;
309
-
310
- // If we've rendered all frames, we're done
311
- if (currentFrame >= frames.length - 1) {
312
- emitEvent('onComplete');
313
-
314
- if (options.repeat) {
315
- // Reset for repeat
316
- startTimeRef.current = now;
317
- dispatch({ type: 'reset', payload: { currentTime: 0, currentFrame: -1 } });
318
- return;
319
- }
320
- // Stop playing
321
- const endState = {
322
- currentTime: totalDuration,
323
- currentFrame: frames.length - 1,
324
- requireReset: true,
325
- isStarted: false,
326
- };
327
- dispatch({ type: 'pause', payload: endState });
328
- startTimeRef.current = null;
329
- return;
330
- }
331
-
332
- // Calculate when the next frame should be rendered
333
- const nextFrame = frames[nextFrameIndex];
334
- if (!nextFrame) {
335
- // No more frames, schedule next animation frame
336
- if (state.isPlaying) {
337
- animationRef.current = requestAnimationFrame(animationLoop);
338
- }
339
- return;
340
- }
341
-
342
- // Check if enough time has passed for the next frame
343
- const frameStartTime = nextFrame.startTime || 0;
344
- if (elapsed >= frameStartTime) {
345
- // Time to render the next frame
346
- dispatch({
347
- type: 'tickStart',
348
- payload: {
349
- currentTime: frameStartTime,
350
- currentFrame: nextFrameIndex,
351
- },
352
- });
353
-
354
- try {
355
- await renderFrame(nextFrameIndex);
356
-
357
- const finalState = {
358
- currentTime: frameStartTime,
359
- currentFrame: nextFrameIndex,
360
- };
361
- if (currentState.requireReset) {
362
- finalState.requireReset = false;
363
- }
364
-
365
- dispatch({ type: 'tickEnd', payload: finalState });
366
- emitEvent('onTick');
367
- } catch (error) {
368
- console.error('Frame rendering error:', error);
369
- }
370
- } else {
371
- // Just update time without rendering
372
- dispatch({ type: 'tick', payload: { currentTime: elapsed } });
373
- }
374
-
375
- // Don't schedule here - let useEffect handle it
376
- // eslint-disable-next-line react-hooks/exhaustive-deps
377
- }, [frames, renderFrame, emitEvent, options.repeat, totalDuration]);
378
-
379
- // Start/stop animation loop based on playing state
380
- useEffect(() => {
381
- let isActive = true;
382
-
383
- const runAnimationLoop = async () => {
384
- // eslint-disable-next-line no-await-in-loop
385
- while (isActive && stateRef.current.isPlaying) {
386
- // eslint-disable-next-line no-await-in-loop
387
- await new Promise((resolve) => {
388
- animationRef.current = requestAnimationFrame(resolve);
389
- });
390
-
391
- if (isActive && stateRef.current.isPlaying) {
392
- // eslint-disable-next-line no-await-in-loop
393
- await animationLoop();
394
- }
395
- }
396
- };
397
-
398
- if (state.isPlaying) {
399
- runAnimationLoop();
400
- } else {
401
- if (animationRef.current) {
402
- cancelAnimationFrame(animationRef.current);
403
- animationRef.current = null;
404
- }
405
- startTimeRef.current = null;
406
- }
407
-
408
- return () => {
409
- isActive = false;
410
- if (animationRef.current) {
411
- cancelAnimationFrame(animationRef.current);
412
- animationRef.current = null;
413
- }
414
- };
415
- }, [state.isPlaying, animationLoop]);
416
-
417
- // If controls are enabled, we need to disable frameBox
418
- if (options.controls) {
419
- options.frameBox.title = null;
420
- options.frameBox.type = null;
421
- options.frameBox.style = {};
422
-
423
- if (options.theme?.background === 'transparent') {
424
- options.frameBox.style.background = 'black';
425
- } else if (options.theme?.background) {
426
- options.frameBox.style.background = options.theme.background;
427
- }
428
-
429
- options.frameBox.style.padding = '10px';
430
- options.frameBox.style.paddingBottom = '40px';
431
- }
432
-
433
- return (
434
- <PlayerRoot className={getPlayerClass(options, state)} ref={container}>
435
- <div className="cover" onClick={onStart} />
436
- <div className="start" onClick={onStart}>
437
- <div className="start-button">
438
- <svg viewBox="0 0 30 30">
439
- <polygon points="6.583,3.186 5,4.004 5,15 26,15 26.483,14.128 " />
440
- <polygon points="6.583,26.814 5,25.996 5,15 26,15 26.483,15.872 " />
441
- <circle cx="26" cy="15" r="1" />
442
- <circle cx="6" cy="4" r="1" />
443
- <circle cx="6" cy="26" r="1" />
444
- </svg>
445
- </div>
446
- </div>
447
-
448
- {/* Hover overlay for playing state */}
449
- {state.isPlaying && state.isStarted && (
450
- <div className="hover-overlay" onClick={onPause}>
451
- <div className="hover-pause-button">
452
- <span className="pause-icon" />
453
- </div>
454
- </div>
455
- )}
456
-
457
- {/* Overlay for paused state */}
458
- {!state.isPlaying && state.isStarted && (
459
- <div className="pause-overlay" onClick={onPlay}>
460
- <div className="overlay-play-button">
461
- <svg viewBox="0 0 30 30">
462
- <polygon points="6.583,3.186 5,4.004 5,15 26,15 26.483,14.128 " />
463
- <polygon points="6.583,26.814 5,25.996 5,15 26,15 26.483,15.872 " />
464
- <circle cx="26" cy="15" r="1" />
465
- <circle cx="6" cy="4" r="1" />
466
- <circle cx="6" cy="26" r="1" />
467
- </svg>
468
- </div>
469
- </div>
470
- )}
471
- <div className="terminal">
472
- <div className={getFrameClass(options)} style={options.frameBox.style || {}}>
473
- <div className="terminal-titlebar">
474
- <div className="buttons">
475
- <div className="close-button" />
476
- <div className="minimize-button" />
477
- <div className="maximize-button" />
478
- </div>
479
- <div className="title">{options.frameBox.title || ''}</div>
480
- </div>
481
- <div className="terminal-body">
482
- <Terminal ref={terminal} options={terminalOptions} />
483
- </div>
484
- </div>
485
- </div>
486
- <div className="controller">
487
- {!state.isPlaying && state.isStarted && (
488
- <div className="play" onClick={onPlay} title="Play">
489
- <span className="icon" />
490
- </div>
491
- )}
492
- {state.isPlaying && (
493
- <div className="pause" onClick={onPause} title="Pause">
494
- <span className="icon" />
495
- </div>
496
- )}
497
- {!state.isPlaying && !state.isStarted && (
498
- <div className="play" onClick={onStart} title="Start">
499
- <span className="icon" />
500
- </div>
501
- )}
502
- <div className="timer">{formatTime(state.currentTime)}</div>
503
- <div className="progressbar-wrapper">
504
- <div className="progressbar" ref={progress} onClick={onJump}>
505
- <div className="progress" style={{ width: `${(state.currentTime / totalDuration) * 100}%` }} />
506
- </div>
507
- </div>
508
- </div>
509
- </PlayerRoot>
510
- );
511
- }
512
-
513
- Player.propTypes = {
514
- frames: PropTypes.array.isRequired,
515
- options: PropTypes.shape({
516
- autoplay: PropTypes.bool,
517
- repeat: PropTypes.bool,
518
- controls: PropTypes.bool,
519
- frameBox: PropTypes.object,
520
- theme: PropTypes.object,
521
- cols: PropTypes.number,
522
- rows: PropTypes.number,
523
- autoplayThreshold: PropTypes.number,
524
- autoplayRootMargin: PropTypes.string,
525
- }).isRequired,
526
- onComplete: PropTypes.func,
527
- onStart: PropTypes.func,
528
- onStop: PropTypes.func,
529
- onPause: PropTypes.func,
530
- onTick: PropTypes.func,
531
- onJump: PropTypes.func,
532
- };
@@ -1,19 +0,0 @@
1
- export { default as Player } from './demo/player';
2
- export { default as AutoPlay } from './demo/autoplay';
3
- export { default as Loop } from './demo/loop';
4
- export { default as AutoPlayLoop } from './demo/autoplay-loop';
5
- export { default as RecordingGuide } from './demo/recording-guide';
6
- export { default as BlockletLogTerminal } from './demo/blocklet-log-terminal';
7
-
8
- export default {
9
- title: 'Data Display/Terminal/Player',
10
-
11
- parameters: {
12
- docs: {
13
- description: {
14
- component:
15
- 'Terminal Player is a react wrapper for `xterm` allowing you to easily render a terminal in the browser.',
16
- },
17
- },
18
- },
19
- };