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 React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import DurationFmt from 'ilib/lib/DurationFmt';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { platform } from '@enact/core/platform';
|
import { platform } from '@enact/core/platform';
|
||||||
|
import { memoize } from '@enact/core/util';
|
||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
|
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
|
||||||
import { Spottable } from '@enact/spotlight/Spottable';
|
import { Spottable } from '@enact/spotlight/Spottable';
|
||||||
import Touchable from '@enact/ui/Touchable';
|
import Touchable from '@enact/ui/Touchable';
|
||||||
|
|
||||||
import Loader from '../Loader/Loader';
|
import Loader from '../Loader/Loader';
|
||||||
|
import { MediaSlider, Times, secondsToTime } from '../MediaPlayer';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
import Media from './Media';
|
import Media from './Media';
|
||||||
import TReactPlayer from './TReactPlayer';
|
import TReactPlayer from './TReactPlayer';
|
||||||
@@ -35,6 +38,20 @@ const RootContainer = SpotlightContainerDecorator(
|
|||||||
'div'
|
'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 컴포넌트
|
* MediaPlayer.v2 컴포넌트
|
||||||
*/
|
*/
|
||||||
@@ -96,6 +113,8 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [controlsVisible, setControlsVisible] = useState(false);
|
const [controlsVisible, setControlsVisible] = useState(false);
|
||||||
const [sourceUnavailable, setSourceUnavailable] = useState(true);
|
const [sourceUnavailable, setSourceUnavailable] = useState(true);
|
||||||
|
const [proportionLoaded, setProportionLoaded] = useState(0);
|
||||||
|
const [proportionPlayed, setProportionPlayed] = useState(0);
|
||||||
|
|
||||||
// ========== Refs ==========
|
// ========== Refs ==========
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
@@ -147,14 +166,21 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
|||||||
const el = videoRef.current;
|
const el = videoRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
|
const newCurrentTime = el.currentTime || 0;
|
||||||
|
const newDuration = el.duration || 0;
|
||||||
|
|
||||||
// 상태 업데이트
|
// 상태 업데이트
|
||||||
setCurrentTime(el.currentTime || 0);
|
setCurrentTime(newCurrentTime);
|
||||||
setDuration(el.duration || 0);
|
setDuration(newDuration);
|
||||||
setPaused(el.paused);
|
setPaused(el.paused);
|
||||||
setLoading(el.loading || false);
|
setLoading(el.loading || false);
|
||||||
setError(el.error || null);
|
setError(el.error || null);
|
||||||
setSourceUnavailable((el.loading && sourceUnavailable) || el.error);
|
setSourceUnavailable((el.loading && sourceUnavailable) || el.error);
|
||||||
|
|
||||||
|
// Proportion 계산
|
||||||
|
setProportionLoaded(el.proportionLoaded || 0);
|
||||||
|
setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0);
|
||||||
|
|
||||||
// 콜백 호출
|
// 콜백 호출
|
||||||
if (ev.type === 'timeupdate' && onTimeUpdate) {
|
if (ev.type === 'timeupdate' && onTimeUpdate) {
|
||||||
onTimeUpdate(ev);
|
onTimeUpdate(ev);
|
||||||
@@ -247,8 +273,31 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
|||||||
error,
|
error,
|
||||||
playbackRate: videoRef.current?.playbackRate || 1,
|
playbackRate: videoRef.current?.playbackRate || 1,
|
||||||
proportionPlayed: duration > 0 ? currentTime / duration : 0,
|
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 전환) ==========
|
// ========== Video Click Handler (Modal 전환) ==========
|
||||||
const handleVideoClick = useCallback(() => {
|
const handleVideoClick = useCallback(() => {
|
||||||
@@ -398,34 +447,69 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Simple Controls */}
|
{/* Controls with MediaSlider */}
|
||||||
{controlsVisible && !isModal && (
|
{controlsVisible && !isModal && (
|
||||||
<div className={css.simpleControls}>
|
<div className={css.controlsContainer}>
|
||||||
<button
|
{/* Slider Section */}
|
||||||
className={css.playPauseBtn}
|
<div className={css.sliderContainer}>
|
||||||
onClick={(e) => {
|
{/* Times - Total */}
|
||||||
e.stopPropagation();
|
<Times
|
||||||
if (paused) {
|
className={css.times}
|
||||||
play();
|
noCurrentTime
|
||||||
} else {
|
total={duration}
|
||||||
pause();
|
formatter={getDurFmt()}
|
||||||
}
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{paused ? '▶' : '⏸'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{onBackButton && (
|
{/* 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
|
<button
|
||||||
className={css.backBtn}
|
className={css.playPauseBtn}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onBackButton(e);
|
if (paused) {
|
||||||
|
play();
|
||||||
|
} else {
|
||||||
|
pause();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
← Back
|
{paused ? '▶' : '⏸'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
|
{onBackButton && (
|
||||||
|
<button
|
||||||
|
className={css.backBtn}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onBackButton(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|||||||
@@ -757,25 +757,43 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== MediaPlayer.v2 Simple Controls ==========
|
// ========== MediaPlayer.v2 Controls ==========
|
||||||
.simpleControls {
|
.controlsContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 40px;
|
bottom: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translateX(-50%);
|
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;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 10;
|
|
||||||
padding: 20px;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playPauseBtn {
|
.playPauseBtn {
|
||||||
width: 80px;
|
width: 60px;
|
||||||
height: 80px;
|
height: 60px;
|
||||||
font-size: 32px;
|
font-size: 24px;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border: 2px solid rgba(255, 255, 255, 0.6);
|
border: 2px solid rgba(255, 255, 255, 0.6);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
Reference in New Issue
Block a user