Skip to content

Commit

Permalink
better mobile support
Browse files Browse the repository at this point in the history
  • Loading branch information
Shenmin-Z committed Mar 22, 2023
1 parent 3be45d5 commit 1fcc845
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 29 deletions.
2 changes: 1 addition & 1 deletion public/version.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
window.__SRT_VERSION__ = '1.5.6'
window.__SRT_VERSION__ = '1.5.7'
8 changes: 4 additions & 4 deletions src/components/Nav/WaveFormSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const WaveFormOption: FC<WaveFormOptionProps> = ({ type, disabled, setDisabled,
const enableStatus = useSelector(s => s.settings.waveform)
const active = type === enableStatus
const file = useSelector(s => s.files.selected) as string
const videoDuration = doVideoWithDefault(video => video.duration, 0)

let mainText = ''
let subText = ''
Expand All @@ -40,7 +39,7 @@ const WaveFormOption: FC<WaveFormOptionProps> = ({ type, disabled, setDisabled,
worker,
arrayBuffer: ab,
fileName: file,
audioDuration: duration ?? videoDuration,
audioDuration: duration ?? doVideoWithDefault(video => video.duration, 0),
onProgress: s => setStage(s),
})
}
Expand All @@ -51,7 +50,7 @@ const WaveFormOption: FC<WaveFormOptionProps> = ({ type, disabled, setDisabled,
}
case EnableWaveForm.video: {
mainText = i18n('nav.waveform.enable')
subText = i18n('nav.waveform.with_video')
subText = i18n('nav.waveform.with_existing')

cb = async () => {
setStage(StageEnum.decoding)
Expand All @@ -63,7 +62,7 @@ const WaveFormOption: FC<WaveFormOptionProps> = ({ type, disabled, setDisabled,
}
case EnableWaveForm.audio: {
mainText = i18n('nav.waveform.enable')
subText = i18n('nav.waveform.with_audio')
subText = i18n('nav.waveform.with_another')

cb = async () => {
const handles = await showOpenFilePicker({
Expand Down Expand Up @@ -95,6 +94,7 @@ const WaveFormOption: FC<WaveFormOptionProps> = ({ type, disabled, setDisabled,
return (
<div
className={cn(styles['waveform-option'], { [styles['active']]: active })}
data-button-type={type === EnableWaveForm.video ? 'create-waveform' : undefined}
onClick={async () => {
if (enableStatus === type) return
if (disabled) return
Expand Down
13 changes: 10 additions & 3 deletions src/components/Video.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@
.video-controls-top {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
flex-direction: column;
.two-icons {
display: flex;
justify-content: center;
gap: 16px;
}
.touch-top,
.touch-bottom {
flex: 1;
}
}

.float-control {
Expand Down
58 changes: 43 additions & 15 deletions src/components/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
IS_IOS,
toggleFullScreen,
FULLSCREEN_ENABLED,
useTouchEmitter,
} from '../utils'
import { WaveForm } from './WaveForm'
import { confirm, message } from './Modal'
Expand All @@ -34,8 +35,11 @@ export const Video: FC = () => {
const i18n = useI18n()
const dispatch = useDispatch()
const enableStatus = useSelector(s => s.settings.waveform)
const hasVideo = useSelector(s => s.video.hasVideo)
const file = useSelector(s => s.files.selected) as string
const { controlsShow, showForAWhile, showControls, hideControls } = useShowControls()
const { divRef: replayDivRef, emitter: replayEmitter } = useTouchEmitter([hasVideo])
const { divRef: scrollDivRef, emitter: scrollEmitter } = useTouchEmitter([hasVideo])

useEffect(() => {
dispatch(LoadWaveFormPreference(file))
Expand Down Expand Up @@ -98,12 +102,28 @@ export const Video: FC = () => {
}
}, [])

useEffect(() => {
// automatically create wavefrom when: 1. only audio, 2. first time played (current time is 0)
if (audioOnly) {
doVideo(v => {
if (v.currentTime === 0) {
const button = document.querySelector('div[data-button-type="create-waveform"]')
if (button) {
;(button as HTMLDivElement).click()
}
}
})
}
}, [audioOnly])

if (videoUrl) {
return (
<div
className={cn(styles['video-container'], { [styles['has-waveform']]: enableStatus, 'audio-only': audioOnly })}
>
{enableStatus !== EnableWaveForm.disable && <WaveForm key={enableStatus} />}
{enableStatus !== EnableWaveForm.disable && (
<WaveForm key={enableStatus} replayEmitter={replayEmitter} scrollEmitter={scrollEmitter} />
)}
<div className={styles['inner']}>
<video
autoPlay={IS_IOS} // loaddata does not fire on iPhone unless played
Expand Down Expand Up @@ -145,6 +165,8 @@ export const Video: FC = () => {
show={showControls}
hide={hideControls}
hasWaveform={enableStatus !== EnableWaveForm.disable}
replayDivRef={replayDivRef}
scrollDivRef={scrollDivRef}
/>
</div>
</div>
Expand Down Expand Up @@ -191,30 +213,36 @@ interface VideoControlsProps {
show(): void
hide(): void
hasWaveform: boolean
replayDivRef: React.RefObject<HTMLDivElement>
scrollDivRef: React.RefObject<HTMLDivElement>
}

const VideoControls: FC<VideoControlsProps> = ({ shown, show, hide, hasWaveform }) => {
const VideoControls: FC<VideoControlsProps> = ({ shown, show, hide, hasWaveform, replayDivRef, scrollDivRef }) => {
const { hasVideo, playing, total, current } = useSelector(s => s.video)

if (!hasVideo) return null

return (
<div className={cn(styles['video-controls'], { [styles['is-playing']]: playing })}>
<div className={styles['video-controls-top']} onClick={togglePlay}>
<div className={cn('material-icons-outlined', styles['float-control'])}>
{playing ? 'pause_circle' : 'play_circle'}
</div>
{hasWaveform && (
<div
className={cn('material-icons', styles['float-control'])}
onClick={e => {
e.stopPropagation()
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyR' }))
}}
>
replay
<div className={styles['touch-top']} ref={replayDivRef} />
<div className={styles['two-icons']}>
<div className={cn('material-icons-outlined', styles['float-control'])}>
{playing ? 'pause_circle' : 'play_circle'}
</div>
)}
{hasWaveform && (
<div
className={cn('material-icons', styles['float-control'])}
onClick={e => {
e.stopPropagation()
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyR' }))
}}
>
replay
</div>
)}
</div>
<div className={styles['touch-bottom']} ref={scrollDivRef} />
</div>
<div
className={cn(styles['video-controls-bottom'], {
Expand Down
3 changes: 3 additions & 0 deletions src/components/WaveForm.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
visibility: hidden;
}
}
&.instant-scroll {
scroll-behavior: auto;
}

/* firefox-only */
scrollbar-width: thin;
Expand Down
46 changes: 44 additions & 2 deletions src/components/WaveForm.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { FC, useEffect, useState, useRef } from 'react'
import { getSampling, doVideo, doVideoWithDefault, useVideoEvents, PIXELS_PER_SECOND } from '../utils'
import { useSelector } from '../state'
import { TouchEmitterListener, TouchEmitValue } from '../utils'
import cn from 'classnames'
import styles from './WaveForm.module.less'

interface Props {}
interface Props {
replayEmitter: { subscribe: (fn: TouchEmitterListener) => void }
scrollEmitter: { subscribe: (fn: TouchEmitterListener) => void }
}

const WAVEFORM_ID = 'srt-player-waveform'

export const WaveForm: FC<Props> = () => {
export const WaveForm: FC<Props> = ({ replayEmitter, scrollEmitter }) => {
const file = useSelector(s => s.files.selected) as string
const waveformDivRef = useRef<HTMLDivElement>(null)
const [ready, setReady] = useState(false)
Expand Down Expand Up @@ -62,6 +66,19 @@ export const WaveForm: FC<Props> = () => {
}
}, [])

useEffect(() => {
replayEmitter.subscribe(deltaX =>
setReplayPos(p => {
if (deltaX === 'start' || deltaX === 'end') {
return p
} else {
return p + deltaX
}
}),
)
scrollEmitter.subscribe(setScrollPosition)
}, [])

return (
<div
className={cn(styles['waveform'], { [styles['ready']]: ready })}
Expand Down Expand Up @@ -190,3 +207,28 @@ const updatePosition = (() => {
})
}
})()

const setScrollPosition = (() => {
let lastScrollLeft = -1
return (deltaX: TouchEmitValue) => {
const canvasContainer = document.getElementById(WAVEFORM_ID) as HTMLDivElement
if (!canvasContainer) return
const parent = canvasContainer.parentElement as HTMLDivElement
if (deltaX === 'start') {
lastScrollLeft = parent.scrollLeft
parent.classList.add(styles['instant-scroll'])
return
}
if (deltaX === 'end') {
parent.classList.remove(styles['instant-scroll'])
return
}
console.log(lastScrollLeft)
const newScrollLeft = lastScrollLeft - deltaX
if (newScrollLeft < 0) {
return
}
parent.scrollLeft = newScrollLeft
lastScrollLeft = newScrollLeft
}
})()
4 changes: 2 additions & 2 deletions src/locale/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"name": "Waveform",
"disable": "Disable",
"enable": "Enable",
"with_video": "Using existing video file",
"with_audio": "Using extra audio file",
"with_existing": "Using existing file",
"with_another": "Using another file",
"generating": "Generating waveform, this might take a while...",
"done": "Done"
}
Expand Down
4 changes: 2 additions & 2 deletions src/locale/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"name": "波形图",
"disable": "关闭",
"enable": "启用",
"with_video": "从现有视频提取",
"with_audio": "其他视频/音频源",
"with_existing": "从现有文件提取",
"with_another": "其他文件源",
"generating": "波形图生成中,请稍等...",
"done": "完成"
}
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './duration'
export * from './ga'
export * from './toggleFullScreen'
export * from './language'
export * from './touchEmitter'
54 changes: 54 additions & 0 deletions src/utils/touchEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useRef, useState, useEffect } from 'react'

export type TouchEmitValue = number | 'start' | 'end'
export interface TouchEmitterListener {
(deltaX: TouchEmitValue): void
}

export const useTouchEmitter = (deps: any[]) => {
const divRef = useRef<HTMLDivElement>(null)
const [emitter] = useState(() => {
let listener: TouchEmitterListener = () => {}
return {
broadcast(deltaX: TouchEmitValue) {
listener(deltaX)
},
subscribe(fn: TouchEmitterListener) {
listener = fn
},
}
})

useEffect(() => {
const div = divRef.current
if (!div) return
let lastX: number
const onTouchStart = (e: TouchEvent) => {
emitter.broadcast('start')
lastX = e.touches[0].clientX
div.addEventListener('touchmove', onTouchMove)
div.addEventListener('touchend', onTouchFinish)
div.addEventListener('touchcancel', onTouchFinish)
}
const onTouchMove = (e: TouchEvent) => {
e.preventDefault()
const x = e.touches[0].clientX
if (Math.abs(x - lastX) > 1) {
emitter.broadcast(x - lastX)
lastX = x
}
}
const onTouchFinish = () => {
emitter.broadcast('end')
div.removeEventListener('touchmove', onTouchMove)
div.removeEventListener('touchend', onTouchFinish)
div.removeEventListener('touchcancel', onTouchFinish)
}
div.addEventListener('touchstart', onTouchStart)
return () => {
div.removeEventListener('touchstart', onTouchStart)
}
}, deps)

return { divRef, emitter }
}

0 comments on commit 1fcc845

Please sign in to comment.