🕐 커밋 시간: 2025. 11. 24. 19:23:39 📊 변경 통계: • 총 파일: 8개 • 추가: +142줄 • 삭제: -31줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/playActions.js ~ com.twin.app.shoptime/src/components/MediaItem/MediaItem.js ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaTitle.js ~ com.twin.app.shoptime/src/components/VideoPlayer/TReactPlayer.jsx ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js ~ com.twin.app.shoptime/src/hooks/useReviews/useReviews.js ~ com.twin.app.shoptime/src/utils/helperMethods.js ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • UI 컴포넌트 아키텍처 개선 • 공통 유틸리티 함수 최적화 • 중간 규모 기능 개선 • 모듈 구조 개선
454 lines
14 KiB
JavaScript
454 lines
14 KiB
JavaScript
/**
|
|
* Item Module of contents list
|
|
*
|
|
* @module MediaItem
|
|
*/
|
|
import React, { useCallback, useEffect, useMemo } from "react";
|
|
|
|
import classNames from "classnames";
|
|
import compose from "ramda/src/compose";
|
|
import { shallowEqual, useSelector } from "react-redux";
|
|
|
|
import { Image } from "@enact/sandstone/Image";
|
|
import { Marquee, MarqueeController } from "@enact/sandstone/Marquee";
|
|
import Spottable from "@enact/spotlight/Spottable";
|
|
import { Cell, Column } from "@enact/ui/Layout";
|
|
import { VoiceControlDecorator } from "@enact/webos/speech";
|
|
|
|
import * as Config from "../../utils/Config";
|
|
// import YOUTUBE_BADGE_LOGO from "../../../assets/list/icon/icon_youtube.png";
|
|
import * as ContentType from "../../utils/Config";
|
|
import * as Utils from "../../utils/helperMethods";
|
|
import { $L } from "../../utils/helperMethods";
|
|
import { SpotlightIds } from "../../utils/SpotlightIds";
|
|
import css from "./MediaItem.module.less";
|
|
|
|
/**
|
|
*
|
|
* @param {itemSize} "small", "big", 'plan'
|
|
* @returns
|
|
*/
|
|
const MediaItemBase = ({
|
|
itemSize,
|
|
supportFavBtn,
|
|
favBtnFocused,
|
|
caption,
|
|
onItemClick,
|
|
forceFocus,
|
|
listspotlightid,
|
|
style,
|
|
onFavBtnFocused,
|
|
itemInfo,
|
|
playing,
|
|
selectMode,
|
|
...rest
|
|
}) => {
|
|
const contentsMyFavorites = useSelector(
|
|
(state) => state.contentsMyFavorites,
|
|
shallowEqual
|
|
);
|
|
const contentInfos = useSelector((state) => state.contentInfos);
|
|
const panelInfo = useSelector((state) => state.panels);
|
|
|
|
const updatedContentInfo = useSelector(
|
|
(state) => state.updatedContentInfo,
|
|
(newState) => {
|
|
if (!itemInfo || !newState) {
|
|
return true;
|
|
}
|
|
if (itemInfo.contentId === newState.contentId) {
|
|
Utils.jsonConcat(itemInfo, newState);
|
|
return false;
|
|
}
|
|
if (itemInfo.youtubeId && itemInfo.youtubeId === newState.youtubeId) {
|
|
Utils.jsonConcat(itemInfo, newState);
|
|
return false;
|
|
}
|
|
if (itemInfo.contentType === ContentType.PLAN) {
|
|
let matched = false;
|
|
if (itemInfo.items) {
|
|
for (let j = 0; j < itemInfo.items.length; j++) {
|
|
if (itemInfo.items[j].contentId === newState.contentId) {
|
|
Utils.jsonConcat(itemInfo.items[j], newState);
|
|
matched = true;
|
|
}
|
|
}
|
|
if (itemInfo.items && itemInfo.items[0]) {
|
|
itemInfo.thumbnailImageUrl = itemInfo.items[0].thumbnailImageUrl;
|
|
itemInfo.postImageUrl = itemInfo.items[0].postImageUrl;
|
|
}
|
|
if (matched) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true; //it's same item. Will not update.
|
|
}
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (itemInfo && itemInfo.contentType === ContentType.PLAN) {
|
|
if (itemInfo.items) {
|
|
for (let j in itemInfo.items) {
|
|
if (
|
|
itemInfo.items[j].contentId &&
|
|
contentInfos[itemInfo.items[j].contentId]
|
|
) {
|
|
Utils.jsonConcat(
|
|
itemInfo.items[j],
|
|
contentInfos[itemInfo.items[j].contentId]
|
|
);
|
|
}
|
|
}
|
|
if (itemInfo.items && itemInfo.items[0]) {
|
|
itemInfo.thumbnailImageUrl = itemInfo.items[0].thumbnailImageUrl;
|
|
itemInfo.postImageUrl = itemInfo.items[0].postImageUrl;
|
|
}
|
|
}
|
|
} else if (
|
|
itemInfo &&
|
|
itemInfo.contentId &&
|
|
contentInfos[itemInfo.contentId]
|
|
) {
|
|
Utils.jsonConcat(itemInfo, contentInfos[itemInfo.contentId]);
|
|
}
|
|
}, [itemInfo, contentInfos, updatedContentInfo]);
|
|
|
|
const iamFavContent = useMemo(() => {
|
|
let ret = false;
|
|
if (listspotlightid === SpotlightIds.LIST_MYFAVORITE) {
|
|
ret = true;
|
|
} else if (
|
|
itemInfo &&
|
|
contentsMyFavorites &&
|
|
contentsMyFavorites.length > 0
|
|
) {
|
|
const id = itemInfo.contentId;
|
|
for (let index in contentsMyFavorites) {
|
|
if (id === contentsMyFavorites[index].contentId) {
|
|
ret = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}, [contentsMyFavorites, itemInfo, listspotlightid]);
|
|
|
|
const onFavMouseEnter = useCallback(() => {
|
|
if (!supportFavBtn) {
|
|
return;
|
|
}
|
|
onFavBtnFocused(true);
|
|
}, [onFavBtnFocused, supportFavBtn]);
|
|
const onFavMouseLeave = useCallback(() => {
|
|
onFavBtnFocused(false);
|
|
}, [onFavBtnFocused]);
|
|
|
|
const onClick = useCallback(
|
|
(ev) => {
|
|
console.log("onClick..", ev);
|
|
if (onItemClick) {
|
|
onItemClick(ev);
|
|
}
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
},
|
|
[onItemClick]
|
|
);
|
|
|
|
let thumbnail = "",
|
|
placeholder = "";
|
|
if (itemSize !== "small" && itemInfo && itemInfo.postImageUrl) {
|
|
thumbnail = itemInfo.postImageUrl;
|
|
} else {
|
|
thumbnail = itemInfo && itemInfo.thumbnailImageUrl;
|
|
}
|
|
placeholder = itemInfo && itemInfo.thumbnailImageUrl;
|
|
|
|
const captionContainer = useCallback(
|
|
(_isYoutubeChannel, _isApp, _isSpecialButton) => {
|
|
const marqueeAlign =
|
|
_isYoutubeChannel || _isApp || _isSpecialButton ? "center" : "left";
|
|
const replaceHtmlEntitesCaption = Utils.replaceHtmlEntites(caption);
|
|
if (!itemInfo || itemInfo.contentType === ContentType.PLAN) {
|
|
return null;
|
|
}
|
|
let splitTitle = [];
|
|
if (Object.keys(itemInfo).includes("dayTitle")) {
|
|
splitTitle = replaceHtmlEntitesCaption.split(":");
|
|
}
|
|
return (
|
|
<Cell
|
|
align="center"
|
|
key="captionContainer"
|
|
className={classNames(
|
|
css.captionContainer,
|
|
_isYoutubeChannel ? css.channel : null,
|
|
_isApp ? css.app : null,
|
|
_isSpecialButton ? css.innerText : null
|
|
)}
|
|
>
|
|
<Marquee
|
|
marqueeDisabled={!forceFocus || favBtnFocused}
|
|
alignment={marqueeAlign}
|
|
key="caption"
|
|
className={css.caption}
|
|
marqueeOn={!favBtnFocused && forceFocus ? "render" : undefined}
|
|
>
|
|
{Object.keys(itemInfo).includes("dayTitle") ? (
|
|
<>
|
|
<span className={css.emphasisFont}>
|
|
{" "}
|
|
{splitTitle[0] + " :"}{" "}
|
|
</span>
|
|
{splitTitle[1]}
|
|
</>
|
|
) : (
|
|
replaceHtmlEntitesCaption
|
|
)}
|
|
</Marquee>
|
|
</Cell>
|
|
);
|
|
},
|
|
[itemInfo, caption, forceFocus, favBtnFocused]
|
|
);
|
|
|
|
const planItemDetail = useCallback(
|
|
(itemSize) => {
|
|
if (itemInfo && itemSize === "plan") {
|
|
return (
|
|
<div className={css.planItemDetail}>
|
|
<Marquee
|
|
marqueeDisabled={!forceFocus}
|
|
onClick={onClick}
|
|
className={css.planTitle}
|
|
marqueeOn={forceFocus ? "render" : undefined}
|
|
>
|
|
{Utils.replaceHtmlEntites(itemInfo.title)}
|
|
</Marquee>
|
|
{itemInfo.items &&
|
|
itemInfo.items.map((item, index) => (
|
|
<div className={css.planList} key={index}>
|
|
<div className={css.plancountcontainer}>
|
|
<div className={css.repeatImage} />
|
|
<div>{item && item.repeatCount}</div>
|
|
</div>
|
|
<Marquee
|
|
marqueeDisabled={!forceFocus}
|
|
className={css.planlisttitle}
|
|
alignment={"left"}
|
|
marqueeOn={forceFocus ? "render" : undefined}
|
|
>
|
|
{item && item.title}
|
|
</Marquee>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
} else {
|
|
return (
|
|
<>
|
|
<div
|
|
className={classNames(
|
|
css.captionContainer,
|
|
isYoutubeChannel ? css.channelPlayIcon : null,
|
|
isMoreButton ? css.moreIcon : null,
|
|
isAddPlanButton ? css.addPlanIcon : null,
|
|
playing ? css.playing : null
|
|
)}
|
|
>
|
|
<Marquee
|
|
className={css.caption}
|
|
marqueeDisabled={!forceFocus}
|
|
onClick={onClick}
|
|
marqueeOn={forceFocus ? "render" : undefined}
|
|
>
|
|
{Utils.replaceHtmlEntites(itemInfo.title)}
|
|
</Marquee>
|
|
</div>
|
|
<div className={css.repeatCountList}>
|
|
<span>{itemInfo.items.length}</span>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
},
|
|
[itemInfo, itemSize, forceFocus, onClick]
|
|
);
|
|
|
|
const playerRepeat = useCallback(() => {
|
|
if (itemInfo) {
|
|
return (
|
|
<div className={css.playerRepeatContainer}>
|
|
<div className={css.repeatImage} />
|
|
<div>{itemInfo.repeatCount}</div>
|
|
</div>
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
}, [itemInfo]);
|
|
|
|
const supportBadge = useMemo(() => {
|
|
let supported = false,
|
|
badgeStyle = {};
|
|
if (itemInfo) {
|
|
if (itemInfo.contentType === ContentType.YOUTUBE_VIDEO) {
|
|
supported = true;
|
|
// badgeStyle = { backgroundImage: `url(${YOUTUBE_BADGE_LOGO})` };
|
|
} else if (itemInfo.contentType === ContentType.CP_CONTENT) {
|
|
supported = true;
|
|
// badgeStyle = { backgroundImage: `url(${itemInfo.badgeIconUrl})` };
|
|
}
|
|
}
|
|
return { supported, badgeStyle };
|
|
}, [itemInfo]);
|
|
|
|
const isYoutubeChannel =
|
|
itemInfo && itemInfo.contentType === ContentType.YOUTUBE_CHANNEL;
|
|
const isYoutubeVideo =
|
|
itemInfo && itemInfo.contentType === ContentType.YOUTUBE_VIDEO;
|
|
const isMoreButton = itemInfo && itemInfo.contentType === ContentType.MORE;
|
|
const isAddPlanButton =
|
|
itemInfo && itemInfo.contentType === ContentType.ADD_PLAN;
|
|
const isSpecialButton = isMoreButton || isAddPlanButton;
|
|
const isApp = itemInfo && itemInfo.contentType === ContentType.APP;
|
|
const isYoutubeAppMoveIcon =
|
|
panelInfo.length > 0 && panelInfo[0].name === "moreformedlist";
|
|
const moveAppIcon =
|
|
itemInfo &&
|
|
(itemInfo.contentType === ContentType.APP ||
|
|
itemInfo.contentType === ContentType.CP_CONTENT);
|
|
const playtime =
|
|
itemInfo && itemInfo.playtime
|
|
? Utils.transSecToText(itemInfo.playtime)
|
|
: null;
|
|
const useNewTodayIcon = Config.USE_NEW_TODAY_ICON;
|
|
return (
|
|
<div
|
|
{...rest}
|
|
className={classNames(
|
|
forceFocus ? css.forceFocus : null,
|
|
css.mediaImage,
|
|
itemSize === "big"
|
|
? css.bigItem
|
|
: itemSize === "plan"
|
|
? css.planItem
|
|
: null
|
|
)}
|
|
style={style}
|
|
onClick={onClick}
|
|
>
|
|
<Column
|
|
className={classNames(
|
|
css.imageItem,
|
|
favBtnFocused ? css.favFocus : null
|
|
)}
|
|
>
|
|
<Cell
|
|
key="thumbContainer"
|
|
className={classNames(
|
|
css.thumbContainer,
|
|
isYoutubeChannel ? css.thumbContaineryoutubeChannel : null,
|
|
isApp ? css.thumbContainerApp : null,
|
|
isSpecialButton ? css.specialBtnContainer : null
|
|
)}
|
|
>
|
|
{thumbnail && (
|
|
<Image
|
|
className={classNames(
|
|
css.image,
|
|
isYoutubeChannel ? css.youtubeChannel : null,
|
|
isApp ? css.applogo : null,
|
|
isYoutubeVideo ? css.youtubeVideo : null
|
|
)}
|
|
placeholder={placeholder}
|
|
src={thumbnail}
|
|
/>
|
|
)}
|
|
{listspotlightid === SpotlightIds.LIST_TODAYTOP && (
|
|
<div
|
|
className={classNames(
|
|
css.top10Number,
|
|
css["index_" + (rest.index + 1)]
|
|
)}
|
|
/>
|
|
)}
|
|
{itemInfo && itemInfo.isToday && (
|
|
<div
|
|
className={classNames(
|
|
css.toDayStr,
|
|
useNewTodayIcon && css.newTodayStr
|
|
)}
|
|
/>
|
|
)}
|
|
{captionContainer(isYoutubeChannel, isApp, isSpecialButton)}
|
|
{!forceFocus && playtime && (
|
|
<div className={classNames(!isYoutubeChannel ? css.subtime : null)}>
|
|
{playtime}
|
|
</div>
|
|
)}
|
|
{supportBadge.supported && (
|
|
<div className={css.badgelogo} style={supportBadge.badgeStyle} />
|
|
)}
|
|
{itemInfo && (!selectMode || isAddPlanButton) && (
|
|
<div
|
|
className={classNames(
|
|
css.playIcon,
|
|
isYoutubeAppMoveIcon
|
|
? css.appIcon
|
|
: isYoutubeChannel
|
|
? css.channelPlayIcon
|
|
: null,
|
|
moveAppIcon ? css.appIcon : null,
|
|
isMoreButton ? css.moreIcon : null,
|
|
isAddPlanButton
|
|
? itemSize === "plan"
|
|
? css.addPlanIcon
|
|
: css.moreIcon
|
|
: null,
|
|
playing ? css.playing : isYoutubeVideo ? css.appIcon : null
|
|
)}
|
|
/>
|
|
)}
|
|
{itemInfo &&
|
|
itemInfo.contentType === ContentType.PLAN &&
|
|
planItemDetail(itemSize)}
|
|
{itemInfo && itemInfo.repeatCount && playerRepeat()}
|
|
</Cell>
|
|
{supportFavBtn && (
|
|
<div
|
|
isfavbtn={"true"}
|
|
className={classNames(
|
|
css.addToFavoriteBtn,
|
|
favBtnFocused ? css.focus : null
|
|
)}
|
|
onMouseEnter={onFavMouseEnter}
|
|
onMouseLeave={onFavMouseLeave}
|
|
>
|
|
<div
|
|
className={classNames(
|
|
css.favIcon,
|
|
iamFavContent ? css.removeFav : null
|
|
)}
|
|
/>
|
|
<div className={css.favText}>
|
|
{iamFavContent
|
|
? $L("Remove from Favorite").toUpperCase()
|
|
: $L("Add to Favorite").toUpperCase()}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Column>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ImageItemDecorator = compose(
|
|
MarqueeController({ marqueeOnFocus: true }),
|
|
VoiceControlDecorator,
|
|
Spottable
|
|
);
|
|
const MediaItem = ImageItemDecorator(MediaItemBase);
|
|
export default MediaItem;
|