11// SPDX-License-Identifier: MIT
22// Copyright (c) 2022 The Pybricks Authors
33
4+ import './bootloaderInstructions.scss' ;
45import { Callout , Intent } from '@blueprintjs/core' ;
5- import React , { useMemo } from 'react' ;
6+ import classNames from 'classnames' ;
7+ import React , { useEffect , useMemo , useRef , useState } from 'react' ;
68import {
79 pybricksUsbDfuWindowsDriverInstallUrl ,
810 pybricksUsbLinuxUdevRulesUrl ,
911} from '../../app/constants' ;
1012import ExternalLinkIcon from '../../components/ExternalLinkIcon' ;
1113import { Hub , hubHasBluetoothButton , hubHasUSB } from '../../components/hubPicker' ;
1214import { isLinux , isWindows } from '../../utils/os' ;
15+ import cityHubMp4 from './assets/bootloader-cityhub-540.mp4' ;
16+ import cityHubVtt from './assets/bootloader-cityhub-metadata.vtt' ;
17+ import essentialHubMp4 from './assets/bootloader-essentialhub-540.mp4' ;
18+ import essentialHubVtt from './assets/bootloader-essentialhub-metadata.vtt' ;
19+ import inventorHubMp4 from './assets/bootloader-inventorhub-540.mp4' ;
20+ import inventorHubVtt from './assets/bootloader-inventorhub-metadata.vtt' ;
21+ import moveHubMp4 from './assets/bootloader-movehub-540.mp4' ;
22+ import moveHubVtt from './assets/bootloader-movehub-metadata.vtt' ;
23+ import primeHubMp4 from './assets/bootloader-primehub-540.mp4' ;
24+ import primeHubVtt from './assets/bootloader-primehub-metadata.vtt' ;
25+ import technicHubMp4 from './assets/bootloader-technichub-540.mp4' ;
26+ import technicHubVtt from './assets/bootloader-technichub-metadata.vtt' ;
1327import { useI18n } from './i18n' ;
1428
1529type BootloaderInstructionsProps = {
1630 hubType : Hub ;
1731} ;
1832
33+ const videoFileMap : ReadonlyMap < Hub , string > = new Map ( [
34+ [ Hub . City , cityHubMp4 ] ,
35+ [ Hub . Essential , essentialHubMp4 ] ,
36+ [ Hub . Inventor , inventorHubMp4 ] ,
37+ [ Hub . Move , moveHubMp4 ] ,
38+ [ Hub . Prime , primeHubMp4 ] ,
39+ [ Hub . Technic , technicHubMp4 ] ,
40+ ] ) ;
41+
42+ const metadataFileMap : ReadonlyMap < Hub , string > = new Map ( [
43+ [ Hub . City , cityHubVtt ] ,
44+ [ Hub . Essential , essentialHubVtt ] ,
45+ [ Hub . Inventor , inventorHubVtt ] ,
46+ [ Hub . Move , moveHubVtt ] ,
47+ [ Hub . Prime , primeHubVtt ] ,
48+ [ Hub . Technic , technicHubVtt ] ,
49+ ] ) ;
50+
1951/**
2052 * Provides customized instructions on how to enter bootloader mode based
2153 * on the hub type.
@@ -41,6 +73,36 @@ const BootloaderInstructions: React.VoidFunctionComponent<
4173 } ;
4274 } , [ i18n , hubType ] ) ;
4375
76+ const metadataTrackRef = useRef < HTMLTrackElement > ( null ) ;
77+ const [ activeStep , setActiveStep ] = useState ( '' ) ;
78+
79+ useEffect ( ( ) => {
80+ const element = metadataTrackRef . current ;
81+
82+ // istanbul ignore if: should not happen
83+ if ( element === null ) {
84+ return ;
85+ }
86+
87+ // istanbul ignore else: jsdom doesn't support video
88+ if ( process . env . NODE_ENV === 'test' ) {
89+ return ;
90+ }
91+
92+ const handleCueChange = ( e : Event ) => {
93+ const track = e . target as TextTrack ;
94+ setActiveStep ( track . activeCues ?. [ 0 ] ?. id ?? '' ) ;
95+ } ;
96+
97+ element . track . addEventListener ( 'cuechange' , handleCueChange ) ;
98+ element . track . mode = 'hidden' ;
99+
100+ return ( ) => {
101+ element . track . removeEventListener ( 'cuechange' , handleCueChange ) ;
102+ element . track . mode = 'disabled' ;
103+ } ;
104+ } , [ setActiveStep ] ) ;
105+
44106 return (
45107 < >
46108 { hubHasUSB ( hubType ) && isLinux ( ) && (
@@ -69,36 +131,107 @@ const BootloaderInstructions: React.VoidFunctionComponent<
69131 < ExternalLinkIcon />
70132 </ Callout >
71133 ) }
72- < p > { i18n . translate ( 'instruction' ) } </ p >
73- < ol >
74- { hubHasUSB ( hubType ) && < li > { i18n . translate ( 'step.disconnectUsb' ) } </ li > }
75134
76- < li > { i18n . translate ( 'step.powerOff' ) } </ li >
135+ < video
136+ controls
137+ controlsList = "nodownload nofullscreen"
138+ muted
139+ disablePictureInPicture
140+ className = "pb-bootloader-video"
141+ >
142+ < source src = { videoFileMap . get ( hubType ) } type = "video/mp4" />
143+ < track
144+ kind = "metadata"
145+ src = { metadataFileMap . get ( hubType ) }
146+ ref = { metadataTrackRef }
147+ />
148+ </ video >
149+
150+ < div className = "pb-spacer" />
77151
152+ < p >
153+ { i18n . translate ( 'instruction' , {
154+ startPoweredOff : hubHasUSB ( hubType )
155+ ? i18n . translate ( 'startPoweredOff.usb' )
156+ : i18n . translate ( 'startPoweredOff.default' ) ,
157+ } ) }
158+ </ p >
159+ < ol >
78160 { /* City hub has power issues and requires disconnecting motors/sensors */ }
79- { hubType === Hub . City && < li > { i18n . translate ( 'step.disconnectIo' ) } </ li > }
161+ { hubType === Hub . City && (
162+ < li
163+ className = { classNames (
164+ activeStep === 'disconnect-io' && 'pb-active-step' ,
165+ ) }
166+ >
167+ { i18n . translate ( 'step.disconnectIo' ) }
168+ </ li >
169+ ) }
80170
81- < li > { i18n . translate ( 'step.holdButton' , { button } ) } </ li >
171+ < li
172+ className = { classNames (
173+ activeStep === 'hold-button' && 'pb-active-step' ,
174+ ) }
175+ >
176+ { i18n . translate ( 'step.holdButton' , { button } ) }
177+ </ li >
82178
83- { hubHasUSB ( hubType ) && < li > { i18n . translate ( 'step.connectUsb' ) } </ li > }
179+ { /* not strictly necessary, but order is swapped in the video,
180+ so we match it here. */ }
181+ { hubType !== Hub . Essential && hubHasUSB ( hubType ) && (
182+ < li
183+ className = { classNames (
184+ activeStep === 'connect-usb' && 'pb-active-step' ,
185+ ) }
186+ >
187+ { i18n . translate ( 'step.connectUsb' ) }
188+ </ li >
189+ ) }
84190
85- < li >
191+ < li
192+ className = { classNames (
193+ activeStep === 'wait-for-light' && 'pb-active-step' ,
194+ ) }
195+ >
86196 { i18n . translate ( 'step.waitForLight' , {
87197 button,
88198 light,
89199 lightPattern,
90200 } ) }
91201 </ li >
92202
93- < li >
94- { i18n . translate (
95- /* hubs with USB will keep the power on, but other hubs won't */
96- hubHasUSB ( hubType ) ? 'step.releaseButton' : 'step.keepHolding' ,
97- {
203+ { hubType === Hub . Essential && hubHasUSB ( hubType ) && (
204+ < li
205+ className = { classNames (
206+ activeStep === 'connect-usb' && 'pb-active-step' ,
207+ ) }
208+ >
209+ { i18n . translate ( 'step.connectUsb' ) }
210+ </ li >
211+ ) }
212+
213+ { /* hubs with USB will keep the power on, but other hubs won't */ }
214+ { hubHasUSB ( hubType ) ? (
215+ < li
216+ className = { classNames (
217+ activeStep === 'release-button' && 'pb-active-step' ,
218+ ) }
219+ >
220+ { i18n . translate ( 'step.releaseButton' , {
98221 button,
99- } ,
100- ) }
101- </ li >
222+ } ) }
223+ </ li >
224+ ) : (
225+ < li
226+ className = { classNames (
227+ activeStep === 'keep-holding' && 'pb-active-step' ,
228+ ) }
229+ >
230+ { i18n . translate ( 'step.keepHolding' , {
231+ button,
232+ } ) }
233+ </ li >
234+ ) }
102235 </ ol >
103236 </ >
104237 ) ;
0 commit comments