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:
Claude
2025-11-10 08:22:38 +00:00
parent 64d1e553ed
commit 726dcd9381
2 changed files with 137 additions and 35 deletions

View File

@@ -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>

View File

@@ -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%;