new icons, add fullscreen + time + fix play

This commit is contained in:
Samuel Newman
2024-07-31 17:40:24 +02:00
parent e07fddd7b9
commit 40f65858af
10 changed files with 115 additions and 32 deletions
@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 391 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM4 13a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 369 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4ZM14 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 247 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4Zm2 1v14h2V5H6Zm8-1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Zm2 1v14h2V5h-2Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 315 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 184 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.507 2.13a1 1 0 0 1 1.008.013l15 9a1 1 0 0 1 0 1.714l-15 9A1 1 0 0 1 5 21V3a1 1 0 0 1 .507-.87ZM7 4.766v14.468L19.056 12 7 4.766Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+8
View File
@@ -1,5 +1,13 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ArrowsDiagonalOut_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM4 13a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z',
})
export const ArrowsDiagonalIn_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z',
})
export const ArrowsDiagonalOut_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z',
})
+8
View File
@@ -1,5 +1,13 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Pause_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4Zm2 1v14h2V5H6Zm8-1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Zm2 1v14h2V5h-2Z',
})
export const Pause_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4ZM14 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Z',
})
export const Pause_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Zm7 1a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Z',
})
+8
View File
@@ -1,5 +1,13 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Play_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M5.507 2.13a1 1 0 0 1 1.008.013l15 9a1 1 0 0 1 0 1.714l-15 9A1 1 0 0 1 5 21V3a1 1 0 0 1 .507-.87ZM7 4.766v14.468L19.056 12 7 4.766Z',
})
export const Play_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z',
})
export const Play_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z',
})
@@ -5,14 +5,18 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import Hls from 'hls.js'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a, useTheme, web} from '#/alf'
import {Button} from '#/components/Button'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {ArrowsDiagonalOut_Stroke2_Corner2_Rounded as FullscreenIcon} from '#/components/icons/ArrowsDiagonal'
import {
ArrowsDiagonalIn_Stroke2_Corner0_Rounded as ArrowsInIcon,
ArrowsDiagonalOut_Stroke2_Corner0_Rounded as ArrowsOutIcon,
} from '#/components/icons/ArrowsDiagonal'
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
import {Pause_Filled_Corner2_Rounded as PauseIcon} from '#/components/icons/Pause'
import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {Text} from '#/components/Typography'
export function VideoEmbedInner({
active,
@@ -127,22 +131,11 @@ export function VideoPlayer({
style={[
a.w_full,
a.rounded_sm,
// TODO: get from embed metadata
// max should be 1 / 1
{aspectRatio: 16 / 9},
a.overflow_hidden,
]}>
<div
style={{
position: 'absolute',
top: 5,
right: 5,
borderRadius: '50%',
width: 20,
height: 20,
background: active ? 'red' : 'blue',
opacity: 0.8,
pointerEvents: 'none',
}}
/>
<div
ref={containerRef}
style={{width: '100%', height: '100%', display: 'flex'}}>
@@ -157,7 +150,7 @@ export function VideoPlayer({
/>
<video
ref={ref}
style={a.flex_1}
style={{width: '100%', height: '100%', objectFit: 'contain'}}
playsInline
preload="none"
loop
@@ -202,6 +195,7 @@ function Controls({
onIn: onMouseEnter,
onOut: onMouseLeave,
} = useInteractionState()
const isFullscreen = useFullscreen()
const {state: hasFocus, onIn: onFocus, onOut: onBlur} = useInteractionState()
useEffect(() => {
@@ -231,6 +225,8 @@ function Controls({
}
}, [togglePlayPause, setActive, setFocused, active, focused])
const showControls = hovered || hasFocus || !playing
return (
<div
style={{
@@ -263,10 +259,14 @@ function Controls({
a.px_sm,
a.pt_sm,
a.pb_md,
a.gap_sm,
a.gap_md,
a.flex_row,
{backgroundColor: 'rgba(0, 0, 0, 0.5)'},
hovered || hasFocus ? {opacity: 1} : {opacity: 0},
a.align_center,
web({
background:
'linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4))',
}),
showControls ? {opacity: 1} : {opacity: 0},
]}>
<Button
label={_(playing ? msg`Pause` : msg`Play`)}
@@ -281,6 +281,9 @@ function Controls({
)}
</Button>
<View style={a.flex_1} />
<Text style={{color: t.palette.white}}>
{formatTime(currentTime)} / {formatTime(duration)}
</Text>
<Button
label={_(muted ? msg`Unmute` : msg`Mute`)}
onPress={() => {
@@ -312,10 +315,14 @@ function Controls({
variant="ghost"
shape="round"
size="medium">
<FullscreenIcon fill={t.palette.white} width={20} />
{isFullscreen ? (
<ArrowsInIcon fill={t.palette.white} width={20} />
) : (
<ArrowsOutIcon fill={t.palette.white} width={20} />
)}
</Button>
</View>
{(hovered || hasFocus || !focused) && (
{(showControls || !focused) && (
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
@@ -348,30 +355,42 @@ function Controls({
)
}
function formatTime(time: number) {
if (isNaN(time)) {
return '--'
}
const minutes = Math.floor(time / 60)
const seconds = String(time % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
const [playing, setPlaying] = useState(false)
const [muted, setMuted] = useState(true)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const playWhenReadyRef = useRef(false)
useEffect(() => {
if (!ref.current) return
let current = ref.current
// Initial values
setCurrentTime(current.currentTime || 0)
setDuration(current.duration || 0)
setCurrentTime(Math.round(current.currentTime) || 0)
setDuration(Math.round(current.duration) || 0)
setMuted(current.muted)
setPlaying(!current.paused)
const handleTimeUpdate = () => {
if (!current) return
setCurrentTime(current.currentTime || 0)
setCurrentTime(Math.round(current.currentTime) || 0)
}
const handleDurationChange = () => {
if (!current) return
setDuration(current.duration || 0)
setDuration(Math.round(current.duration) || 0)
}
const handlePlay = () => {
@@ -387,6 +406,14 @@ function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
setMuted(current.muted)
}
const handleCanPlay = () => {
if (!current) return
if (playWhenReadyRef.current) {
current.play()
playWhenReadyRef.current = false
}
}
ref.current.addEventListener('timeupdate', handleTimeUpdate)
ref.current.addEventListener('durationchange', handleDurationChange)
ref.current.addEventListener('play', handlePlay)
@@ -394,6 +421,7 @@ function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
ref.current.addEventListener('volumechange', handleVolumeChange)
ref.current.addEventListener('ended', handlePause)
ref.current.addEventListener('error', handlePause)
ref.current.addEventListener('canplay', handleCanPlay)
return () => {
current.removeEventListener('timeupdate', handleTimeUpdate)
@@ -403,17 +431,22 @@ function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
current.removeEventListener('volumechange', handleVolumeChange)
current.removeEventListener('ended', handlePause)
current.removeEventListener('error', handlePause)
current.removeEventListener('canplay', handleCanPlay)
}
}, [ref])
const play = useCallback(() => {
if (!ref.current) return
const promise = ref.current.play()
if (promise !== undefined) {
promise.catch(error => {
console.error('Error playing video:', error)
})
if (ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
playWhenReadyRef.current = true
} else {
const promise = ref.current.play()
if (promise !== undefined) {
promise.catch(error => {
console.error('Error playing video:', error)
})
}
}
}, [ref])
@@ -464,3 +497,23 @@ function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
toggleMute,
}
}
function useFullscreen() {
const [isFullscreen, setIsFullscreen] = useState(
Boolean(document.fullscreenElement),
)
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(Boolean(document.fullscreenElement))
}
document.addEventListener('fullscreenchange', handleFullscreenChange)
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
}
}, [])
return isFullscreen
}