Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow use shortcut keys without controller #249

Merged
merged 3 commits into from
Jan 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions packages/griffith-utils/src/__tests__/ua.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,30 @@
* @jest-environment jsdom
*/

import ua, {parseUA} from '../ua'

test('ua', () => {
Object.defineProperty(window.navigator, 'userAgent', {
value:
'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Mobile Safari/537.36',
})
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {isMobile, isAndroid, isSafari} = require('../ua')
expect(isMobile).toBe(true)
expect(isAndroid).toBe(true)
expect(isSafari).toBe(false)
expect(ua).toMatchInlineSnapshot(`
Object {
"isAndroid": false,
"isIE": false,
"isMobile": false,
"isSafari": false,
}
`)
})

test('parse', () => {
expect(
parseUA(
'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Mobile Safari/537.36'
)
).toMatchInlineSnapshot(`
Object {
"isAndroid": true,
"isIE": false,
"isMobile": true,
"isSafari": false,
}
`)
})
22 changes: 11 additions & 11 deletions packages/griffith-utils/src/ua.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
export const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)

export const isAndroid = /(android)/i.test(navigator.userAgent)
export function parseUA(userAgent: string) {
return {
isIE: /MSIE|Trident/i.test(userAgent),
isMobile: /iPhone|iPad|iPod|Android/i.test(userAgent),
isAndroid: /(android)/i.test(userAgent),
isSafari: /^((?!chrome|android).)*safari/i.test(userAgent),
}
}

export const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent
export default parseUA(
// TODO: 加一个 context 让各处访问更好
typeof navigator !== 'undefined' ? navigator.userAgent : ''
)

export default {
isMobile,
isAndroid,
isSafari,
}
169 changes: 11 additions & 158 deletions packages/griffith/src/components/Controller.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, {useContext, useEffect, useRef, useState} from 'react'
import React, {useState} from 'react'
import {css} from 'aphrodite/no-important'
import clamp from 'lodash/clamp'
import * as displayIcons from './icons/display/index'
import * as controllerIcons from './icons/controller/index'
import {ProgressDot} from '../types'
import PlayButtonItem from './items/PlayButtonItem'
import TimelineItem from './items/TimelineItem'
Expand All @@ -16,8 +14,6 @@ import PlaybackRateMenuItem from './items/PlaybackRateMenuItem'
import PageFullScreenButtonItem from './items/PageFullScreenButtonItem'
import useHandler from '../hooks/useHandler'
import useBoolean from '../hooks/useBoolean'
import {useActionToastDispatch} from './ActionToast'
import VideoSourceContext from '../contexts/VideoSourceContext'

type ControllerProps = {
standalone?: boolean
Expand All @@ -31,11 +27,11 @@ type ControllerProps = {
isPip: boolean
onDragStart?: () => void
onDragEnd?: () => void
onPlay?: () => void
onPause?: () => void
onTogglePlay?: () => void
onSeek?: (currentTime: number) => void
onQualityChange?: (...args: any[]) => any
onVolumeChange?: (volume: number) => void
onToggleMuted?: () => void
onToggleFullScreen?: () => void
onTogglePageFullScreen?: () => void
onTogglePip?: (...args: any[]) => void
Expand Down Expand Up @@ -89,7 +85,6 @@ function Controller(props: ControllerProps) {
onTogglePageFullScreen,
onTogglePip,
showPip,
standalone,
progressDots,
hiddenPlayButton,
hiddenTimeline,
Expand All @@ -101,166 +96,24 @@ function Controller(props: ControllerProps) {
shouldShowPageFullScreenButton,
onProgressDotHover,
onProgressDotLeave,
onPause,
onPlay,
onTogglePlay,
onSeek,
onToggleMuted,
onVolumeChange,
} = props
const {playbackRates, currentPlaybackRate, setCurrentPlaybackRate} =
useContext(VideoSourceContext)
const actionToastDispatch = useActionToastDispatch()

const [isVolumeHovered, isVolumeHoveredSwitch] = useBoolean()
const [slideTime, setSlideTime] = useState<number>()
const prevVolumeRef = useRef(1)

const rotatePlaybackRate = (dir: 'next' | 'prev') => {
const index = playbackRates?.findIndex(
(x) => x.value === currentPlaybackRate.value
)
if (index >= 0) {
const next = playbackRates[index + (dir === 'next' ? 1 : -1)]
if (next) {
actionToastDispatch({icon: displayIcons.play, label: next.text})
setCurrentPlaybackRate(next)
}
}
}

const handleDragMove = useHandler((slideTime: number) => {
setSlideTime(clamp(slideTime, 0, duration))
})

const handleTogglePlay = () => {
if (isPlaying) {
onPause?.()
} else {
onPlay?.()
}
}

const handleSeek = useHandler((currentTime: number) => {
currentTime = clamp(currentTime, 0, duration)
if (onSeek) {
onSeek(currentTime)
setSlideTime(void 0)
}
onSeek?.(clamp(currentTime, 0, duration))
setSlideTime(void 0)
})

const handleVolumeChange = useHandler((value: number, showToast = false) => {
value = clamp(value, 0, 1)
if (showToast) {
actionToastDispatch({
icon: value ? controllerIcons.volume : controllerIcons.muted,
label: `${(value * 100).toFixed(0)}%`,
})
}
onVolumeChange?.(value)
})

const handleToggleMuted = useHandler((showToast = false) => {
if (volume) {
prevVolumeRef.current = volume
}
handleVolumeChange(volume ? 0 : prevVolumeRef.current, showToast)
})

const handleKeyDown = useHandler((event: KeyboardEvent) => {
// 防止冲突,有修饰键按下时不触发自定义热键
if (event.altKey || event.ctrlKey || event.metaKey) {
return
}
let handled = true

switch (event.key) {
case ' ':
case 'k':
case 'K':
actionToastDispatch({
icon: isPlaying ? displayIcons.pause : displayIcons.play,
})
handleTogglePlay()
break

case 'Enter':
case 'f':
case 'F':
onToggleFullScreen?.()
break
case 'Escape':
if (isPageFullScreen) {
onTogglePageFullScreen?.()
}
break
case 'ArrowLeft':
handleSeek(currentTime - 5)
break

case 'ArrowRight':
handleSeek(currentTime + 5)
break

case 'j':
case 'J':
handleSeek(currentTime - 10)
break

case 'l':
case 'L':
handleSeek(currentTime + 10)
break
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
handleSeek((duration / 10) * Number(event.key))
break

case 'm':
case 'M':
handleToggleMuted(true)
break

case 'ArrowUp':
// 静音状态下调整可能不切换为非静音更好(设置一成临时的,切换后再应用临时状态)
handleVolumeChange(volume + 0.05, true)
break

case 'ArrowDown':
handleVolumeChange(volume - 0.05, true)
break

case '<':
rotatePlaybackRate('prev')
break

case '>':
rotatePlaybackRate('next')
break

default:
handled = false
break
}
if (handled) {
event.preventDefault()
}
})

useEffect(() => {
if (standalone) {
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}
}, [handleKeyDown, standalone])

const displayedCurrentTime = slideTime || currentTime

return (
Expand All @@ -284,7 +137,7 @@ function Controller(props: ControllerProps) {
{!hiddenPlayButton && (
<PlayButtonItem
isPlaying={isPlaying}
onClick={() => handleTogglePlay()}
onClick={() => onTogglePlay?.()}
/>
)}
{hiddenTimeline && <div className={css(styles.timelineHolder)} />}
Expand Down Expand Up @@ -318,8 +171,8 @@ function Controller(props: ControllerProps) {
menuShown={isVolumeHovered}
onMouseEnter={isVolumeHoveredSwitch.on}
onMouseLeave={isVolumeHoveredSwitch.off}
onToggleMuted={handleToggleMuted}
onChange={handleVolumeChange}
onToggleMuted={onToggleMuted}
onChange={onVolumeChange}
/>
)}
</div>
Expand Down
Loading