Skip to content

Commit

Permalink
feat: add ActionToast for shortcut keys
Browse files Browse the repository at this point in the history
  • Loading branch information
ambar committed Jan 27, 2022
1 parent 044f220 commit d76c811
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 124 deletions.
72 changes: 72 additions & 0 deletions packages/griffith/src/components/ActionToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, {
createContext,
useCallback,
useContext,
useRef,
useState,
} from 'react'
import {css} from 'aphrodite/no-important'
import styles from './Player.styles'
import Icon from './Icon'

type State = {
label?: string
icon?: React.ReactElement
}
type InternalState = {key: string} & State
export type ActionToastDispatch = (s: State) => void

const ActionToastStateContext = createContext<InternalState | void>(void 0)
export const ActionToastDispatchContext = createContext<ActionToastDispatch>(
() => {
// noop
}
)

/** 触发操作反馈 */
export const useActionToastDispatch = () =>
useContext(ActionToastDispatchContext)

/**
* 操作反馈提示(主要提示用户热键或鼠标操作的结果)
*/
export const ActionToastProvider: React.FC = ({children}) => {
const [state, setState] = useState<InternalState>()
const lastKey = useRef(0)
const dispatch = useCallback((state) => {
// 每一次设定必定是创建一个新的提示,key 的变化让元素重新 mount,CSS 动画得以执行
lastKey.current += 1
setState({...state, key: String(lastKey.current)})
}, [])

return (
<ActionToastStateContext.Provider value={state}>
<ActionToastDispatchContext.Provider value={dispatch}>
{children}
</ActionToastDispatchContext.Provider>
</ActionToastStateContext.Provider>
)
}

/** 操作反馈提示的目标渲染位置 */
export const ActionToastOutlet = React.memo(() => {
const state = useContext(ActionToastStateContext)
if (!state) {
return null
}

const {key, icon, label} = state

return (
<div className={css(styles.action)} key={key}>
<div className={css(styles.actionButton, styles.actionButtonAnimated)}>
{icon && <Icon icon={icon} styles={styles.actionIcon} />}
</div>
{label && (
<div className={css(styles.actionLabel, styles.actionLabelAnimation)}>
{label}
</div>
)}
</div>
)
})
109 changes: 38 additions & 71 deletions packages/griffith/src/components/Controller.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, {useEffect, useMemo, useRef, useState} from 'react'
import React, {useEffect, useRef, useState} from 'react'
import {css} from 'aphrodite/no-important'
import debounce from 'lodash/debounce'
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 @@ -15,8 +16,7 @@ import PlaybackRateMenuItem from './items/PlaybackRateMenuItem'
import PageFullScreenButtonItem from './items/PageFullScreenButtonItem'
import useHandler from '../hooks/useHandler'
import useBoolean from '../hooks/useBoolean'

export type ToggleType = 'button' | 'keyCode' | 'video' | null
import {useActionToastDispatch} from './ActionToast'

type ControllerProps = {
standalone?: boolean
Expand All @@ -30,8 +30,8 @@ type ControllerProps = {
isPip: boolean
onDragStart?: () => void
onDragEnd?: () => void
onPlay?: (type: ToggleType) => void
onPause?: (type: ToggleType) => void
onPlay?: () => void
onPause?: () => void
onSeek?: (currentTime: number) => void
onQualityChange?: (...args: any[]) => any
onVolumeChange?: (volume: number) => void
Expand Down Expand Up @@ -74,7 +74,6 @@ Controller.defaultProps = {

function Controller(props: ControllerProps) {
const {
show,
isPlaying = false,
buffered,
duration,
Expand Down Expand Up @@ -106,21 +105,20 @@ function Controller(props: ControllerProps) {
onSeek,
onVolumeChange,
} = props
const actionToastDispatch = useActionToastDispatch()
const [isVolumeHovered, isVolumeHoveredSwitch] = useBoolean()
const [isVolumeDragging, isVolumeDraggingSwitch] = useBoolean()
const [isVolumeKeyboard, isVolumeKeyboardSwitch] = useBoolean()
const [slideTime, setSlideTime] = useState<number>()
const prevVolumeRef = useRef(1)

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

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

Expand All @@ -132,30 +130,22 @@ function Controller(props: ControllerProps) {
}
})

const handleVolumeChange = useHandler((volume: number) => {
volume = clamp(volume, 0, 1)
onVolumeChange?.(volume)
})

const handleToggleMuted = useHandler(() => {
if (volume) {
prevVolumeRef.current = volume
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)}%`,
})
}
handleVolumeChange(volume ? 0 : prevVolumeRef.current)
onVolumeChange?.(value)
})

const handleVolumeDragStart = useHandler(() => {
const handleToggleMuted = useHandler((showToast = false) => {
if (volume) {
prevVolumeRef.current = volume
}

isVolumeDraggingSwitch.on()
onDragStart?.()
})

const handleVolumeDragEnd = useHandler(() => {
isVolumeDraggingSwitch.off()
onDragEnd?.()
handleVolumeChange(volume ? 0 : prevVolumeRef.current, showToast)
})

const handleKeyDown = useHandler((event: KeyboardEvent) => {
Expand All @@ -168,11 +158,16 @@ function Controller(props: ControllerProps) {
switch (event.key) {
case ' ':
case 'k':
handleTogglePlay('keyCode')
case 'K':
actionToastDispatch({
icon: isPlaying ? displayIcons.pause : displayIcons.play,
})
handleTogglePlay()
break

case 'Enter':
case 'f':
case 'F':
onToggleFullScreen?.()
break
case 'Escape':
Expand All @@ -189,10 +184,12 @@ function Controller(props: ControllerProps) {
break

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

case 'l':
case 'L':
handleSeek(currentTime + 10)
break
case '0':
Expand All @@ -205,28 +202,21 @@ function Controller(props: ControllerProps) {
case '7':
case '8':
case '9':
if (show) {
const nextTime = (duration / 10) * Number(event.key)
handleSeek(nextTime)
}
handleSeek((duration / 10) * Number(event.key))
break

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

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

case 'ArrowDown':
if (volume) {
prevVolumeRef.current = volume
}
handleVolumeChange(volume - 0.05)
handleVolumeChange(volume - 0.05, true)
break

default:
Expand All @@ -238,33 +228,14 @@ function Controller(props: ControllerProps) {
}
})

const handleKeyUp = useHandler((event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
handleVolumeKeyboard()
break
}
})

const handleVolumeKeyboard = useMemo(
() =>
debounce(() => {
isVolumeKeyboardSwitch.off()
}, 1000),
[isVolumeKeyboardSwitch]
)

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

const displayedCurrentTime = slideTime || currentTime

Expand All @@ -289,7 +260,7 @@ function Controller(props: ControllerProps) {
{!hiddenPlayButton && (
<PlayButtonItem
isPlaying={isPlaying}
onClick={() => handleTogglePlay('button')}
onClick={() => handleTogglePlay()}
/>
)}
{hiddenTimeline && <div className={css(styles.timelineHolder)} />}
Expand Down Expand Up @@ -320,14 +291,10 @@ function Controller(props: ControllerProps) {
{!hiddenVolumeItem && (
<VolumeItem
volume={volume}
menuShown={
isVolumeHovered || isVolumeDragging || isVolumeKeyboard
}
menuShown={isVolumeHovered}
onMouseEnter={isVolumeHoveredSwitch.on}
onMouseLeave={isVolumeHoveredSwitch.off}
onToggleMuted={handleToggleMuted}
onDragStart={handleVolumeDragStart}
onDragEnd={handleVolumeDragEnd}
onChange={handleVolumeChange}
/>
)}
Expand Down
24 changes: 24 additions & 0 deletions packages/griffith/src/components/Player.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ const actionAnimation = {
},
}

const actionLabelAnimation = {
'90%': {
opacity: 1,
},
'100%': {
opacity: 0,
},
}

const fadeinAnimation = {
'0%': {
opacity: 0,
Expand Down Expand Up @@ -60,6 +69,7 @@ export default StyleSheet.create({
},

actionButton: {
color: '#fff',
width: '4.5em',
height: '4.5em',
},
Expand All @@ -70,6 +80,12 @@ export default StyleSheet.create({
animationFillMode: 'both',
},

actionLabelAnimation: {
animationName: actionLabelAnimation,
animationDuration: '600ms',
animationFillMode: 'both',
},

actionIcon: {
width: '4.5em',
height: '4.5em',
Expand All @@ -79,6 +95,14 @@ export default StyleSheet.create({
},
},

actionLabel: {
padding: '.3em .5em',
textAlign: 'center',
color: '#fff',
background: 'rgba(0,0,0,0.5)',
borderRadius: 5,
},

video: {
width: '100%',
height: '100%',
Expand Down
Loading

0 comments on commit d76c811

Please sign in to comment.