feat: Add MediaSlider and Times to MediaPlayer.v2
MediaPlayer.v2에 필수 기능 추가 (Fullscreen 모드용) 주요 변경사항: - MediaSlider 추가 (비디오 진행 바) - Times 컴포넌트 추가 (현재/전체 시간 표시) - proportionLoaded/Played 상태 관리 - DurationFmt 헬퍼 추가 - Slider 이벤트 핸들러 구현 상태 추가 (9개): - proportionLoaded: 로딩된 비율 - proportionPlayed: 재생된 비율 Import 추가: - MediaSlider, Times, secondsToTime from '../MediaPlayer' - DurationFmt from 'ilib/lib/DurationFmt' - memoize from '@enact/core/util' UI 구조: - controlsContainer - sliderContainer (Times + MediaSlider) - controlsButtons (Play/Pause + Back) 조건부 렌더링: - Modal 모드 (modal=true): 오버레이 없음 - Fullscreen 모드 (modal=false): MediaSlider + Times 표시 CSS 업데이트: - controlsContainer: gradient background - sliderContainer: flex layout - times: min-width 80px - controlsButtons: centered layout
This commit is contained in:
@@ -11,15 +11,18 @@
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import DurationFmt from 'ilib/lib/DurationFmt';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { platform } from '@enact/core/platform';
|
||||
import { memoize } from '@enact/core/util';
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import { Spottable } from '@enact/spotlight/Spottable';
|
||||
import Touchable from '@enact/ui/Touchable';
|
||||
|
||||
import Loader from '../Loader/Loader';
|
||||
import { MediaSlider, Times, secondsToTime } from '../MediaPlayer';
|
||||
import Overlay from './Overlay';
|
||||
import Media from './Media';
|
||||
import TReactPlayer from './TReactPlayer';
|
||||
@@ -35,6 +38,20 @@ const RootContainer = SpotlightContainerDecorator(
|
||||
'div'
|
||||
);
|
||||
|
||||
// DurationFmt memoization
|
||||
const memoGetDurFmt = memoize(
|
||||
() => new DurationFmt({
|
||||
length: 'medium',
|
||||
style: 'clock',
|
||||
useNative: false,
|
||||
})
|
||||
);
|
||||
|
||||
const getDurFmt = () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return memoGetDurFmt();
|
||||
};
|
||||
|
||||
/**
|
||||
* MediaPlayer.v2 컴포넌트
|
||||
*/
|
||||
@@ -96,6 +113,8 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
||||
const [error, setError] = useState(null);
|
||||
const [controlsVisible, setControlsVisible] = useState(false);
|
||||
const [sourceUnavailable, setSourceUnavailable] = useState(true);
|
||||
const [proportionLoaded, setProportionLoaded] = useState(0);
|
||||
const [proportionPlayed, setProportionPlayed] = useState(0);
|
||||
|
||||
// ========== Refs ==========
|
||||
const videoRef = useRef(null);
|
||||
@@ -147,14 +166,21 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
||||
const el = videoRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const newCurrentTime = el.currentTime || 0;
|
||||
const newDuration = el.duration || 0;
|
||||
|
||||
// 상태 업데이트
|
||||
setCurrentTime(el.currentTime || 0);
|
||||
setDuration(el.duration || 0);
|
||||
setCurrentTime(newCurrentTime);
|
||||
setDuration(newDuration);
|
||||
setPaused(el.paused);
|
||||
setLoading(el.loading || false);
|
||||
setError(el.error || null);
|
||||
setSourceUnavailable((el.loading && sourceUnavailable) || el.error);
|
||||
|
||||
// Proportion 계산
|
||||
setProportionLoaded(el.proportionLoaded || 0);
|
||||
setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0);
|
||||
|
||||
// 콜백 호출
|
||||
if (ev.type === 'timeupdate' && onTimeUpdate) {
|
||||
onTimeUpdate(ev);
|
||||
@@ -247,8 +273,31 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
||||
error,
|
||||
playbackRate: videoRef.current?.playbackRate || 1,
|
||||
proportionPlayed: duration > 0 ? currentTime / duration : 0,
|
||||
proportionLoaded,
|
||||
};
|
||||
}, [currentTime, duration, paused, loading, error]);
|
||||
}, [currentTime, duration, paused, loading, error, proportionLoaded]);
|
||||
|
||||
// ========== Slider Event Handlers ==========
|
||||
const handleSliderChange = useCallback(({ value }) => {
|
||||
const time = value * duration;
|
||||
seek(time);
|
||||
}, [duration, seek]);
|
||||
|
||||
const handleKnobMove = useCallback((ev) => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
const seconds = Math.floor(ev.proportion * videoRef.current.duration);
|
||||
if (!isNaN(seconds)) {
|
||||
// Scrub 시 시간 표시 업데이트
|
||||
// 필요시 onScrub 콜백 호출 가능
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSliderKeyDown = useCallback((ev) => {
|
||||
// Spotlight 키 이벤트 처리
|
||||
// 위/아래 키로 controls 이동 등
|
||||
// 기본 동작은 MediaSlider 내부에서 처리
|
||||
}, []);
|
||||
|
||||
// ========== Video Click Handler (Modal 전환) ==========
|
||||
const handleVideoClick = useCallback(() => {
|
||||
@@ -398,9 +447,43 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Simple Controls */}
|
||||
{/* Controls with MediaSlider */}
|
||||
{controlsVisible && !isModal && (
|
||||
<div className={css.simpleControls}>
|
||||
<div className={css.controlsContainer}>
|
||||
{/* Slider Section */}
|
||||
<div className={css.sliderContainer}>
|
||||
{/* Times - Total */}
|
||||
<Times
|
||||
className={css.times}
|
||||
noCurrentTime
|
||||
total={duration}
|
||||
formatter={getDurFmt()}
|
||||
/>
|
||||
|
||||
{/* Times - Current */}
|
||||
<Times
|
||||
className={css.times}
|
||||
noTotalTime
|
||||
current={currentTime}
|
||||
formatter={getDurFmt()}
|
||||
/>
|
||||
|
||||
{/* MediaSlider */}
|
||||
<MediaSlider
|
||||
backgroundProgress={proportionLoaded}
|
||||
disabled={disabled || sourceUnavailable}
|
||||
value={proportionPlayed}
|
||||
visible={controlsVisible}
|
||||
spotlightDisabled={spotlightDisabled}
|
||||
onChange={handleSliderChange}
|
||||
onKnobMove={handleKnobMove}
|
||||
onKeyDown={handleSliderKeyDown}
|
||||
spotlightId={`${spotlightId}-slider`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Buttons Section */}
|
||||
<div className={css.controlsButtons}>
|
||||
<button
|
||||
className={css.playPauseBtn}
|
||||
onClick={(e) => {
|
||||
@@ -427,6 +510,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
|
||||
|
||||
@@ -757,25 +757,43 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ========== MediaPlayer.v2 Simple Controls ==========
|
||||
.simpleControls {
|
||||
// ========== MediaPlayer.v2 Controls ==========
|
||||
.controlsContainer {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20px 40px 30px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 60%, transparent 100%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.times {
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.controlsButtons {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.playPauseBtn {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 32px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 24px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.6);
|
||||
border-radius: 50%;
|
||||
|
||||
Reference in New Issue
Block a user