From a503bf923a4df379aa8f55b61a034385f1b0098c Mon Sep 17 00:00:00 2001 From: optrader Date: Sat, 15 Nov 2025 22:03:44 +0900 Subject: [PATCH] [251115] fix: DetailPanel FullScreen Focus Move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ• ์ปค๋ฐ‹ ์‹œ๊ฐ„: 2025. 11. 15. 22:03:44 ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„: โ€ข ์ด ํŒŒ์ผ: 17๊ฐœ โ€ข ์ถ”๊ฐ€: +573์ค„ โ€ข ์‚ญ์ œ: -87์ค„ ๐Ÿ“ ์ถ”๊ฐ€๋œ ํŒŒ์ผ: + com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx ๐Ÿ“ ์ˆ˜์ •๋œ ํŒŒ์ผ: ~ com.twin.app.shoptime/src/actions/panelActions.js ~ com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less ~ com.twin.app.shoptime/src/utils/SpotlightIds.js ~ com.twin.app.shoptime/src/views/HomePanel/BestSeller/BestSeller.jsx ~ com.twin.app.shoptime/src/views/HomePanel/EventPopUpBanner/EventPopUpBanner.jsx ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx ~ com.twin.app.shoptime/src/views/HomePanel/PickedForYou/PickedForYou.jsx ~ com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx ๐Ÿ”ง ํ•จ์ˆ˜ ๋ณ€๊ฒฝ ๋‚ด์šฉ: ๐Ÿ“„ com.twin.app.shoptime/src/actions/panelActions.js (javascript): โœ… Added: updatePanel() ๐Ÿ“„ com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js (javascript): โœ… Added: onSpotlightRight(), onSpotlightUp(), MediaControlsDecoratorHOC(), handleCancel() โŒ Deleted: onSpotlightRight(), onSpotlightUp(), MediaControlsDecoratorHOC(), handleCancel() ๐Ÿ“„ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx (javascript): ๐Ÿ”„ Modified: SpotlightContainerDecorator() ๐Ÿ“„ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less (unknown): โœ… Added: position() โŒ Deleted: position() ๐Ÿ“„ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx (javascript): ๐Ÿ”„ Modified: SpotlightContainerDecorator() ๐Ÿ“„ com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx (javascript): ๐Ÿ”„ Modified: getExpsOrdByLgCatCd() ๐Ÿ“„ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx (javascript): ๐Ÿ”„ Modified: normalizeModalStyle() ๐Ÿ“„ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx (javascript): ๐Ÿ”„ Modified: SpotlightContainerDecorator(), PlayerOverlayContents() ๐Ÿ“„ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx (javascript): โœ… Added: MediaOverlayContents() ๐Ÿ”ง ์ฃผ์š” ๋ณ€๊ฒฝ ๋‚ด์šฉ: โ€ข ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฐœ์„  โ€ข UI ์ปดํฌ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์„  โ€ข ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ์ตœ์ ํ™” --- .../src/actions/panelActions.js | 413 ++++ .../components/MediaPlayer/MediaControls.js | 1948 ++++++++--------- .../VideoPlayer/MediaPlayer.module.less | 3 + .../components/VideoPlayer/MediaPlayer.v2.jsx | 215 +- .../components/VideoPlayer/VideoPlayer.v3.js | 39 +- .../VideoPlayer/VideoPlayer.v3.module.less | 17 +- .../src/utils/SpotlightIds.js | 1 + .../views/HomePanel/BestSeller/BestSeller.jsx | 161 +- .../EventPopUpBanner/EventPopUpBanner.jsx | 176 +- .../views/HomePanel/HomeBanner/RandomUnit.jsx | 16 +- .../HomePanel/PickedForYou/PickedForYou.jsx | 106 +- .../HomePanel/SubCategory/SubCategory.jsx | 172 +- .../src/views/MainView/MainView.jsx | 5 + .../src/views/MediaPanel/MediaPanel.v3.jsx | 2 + .../PlayerOverlay/MediaOverlayContents.jsx | 9 + .../PlayerOverlay/PlayerOverlayContents.jsx | 61 +- .../SearchPanel/SearchResultsNew/ItemCard.jsx | 26 +- 17 files changed, 1878 insertions(+), 1492 deletions(-) create mode 100644 com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx diff --git a/com.twin.app.shoptime/src/actions/panelActions.js b/com.twin.app.shoptime/src/actions/panelActions.js index 9d4cabc7..802248c6 100644 --- a/com.twin.app.shoptime/src/actions/panelActions.js +++ b/com.twin.app.shoptime/src/actions/panelActions.js @@ -11,6 +11,8 @@ export const SOURCE_MENUS = { HOME_ROLLING_UNIT: 'home_rolling_unit', HOME_EVENT_POPUP: 'home_event_popup', HOME_TODAYS_DEAL: 'home_todays_deal', + SEARCH_RESULT: 'search_result', + HOME_GENERAL: 'home_general', THEMED_PRODUCT: 'themed_product', GENERAL_PRODUCT: 'general_product', }; @@ -43,6 +45,417 @@ export const resetPanels = (panels) => ({ payload: panels, }); +/** + * DetailPanel๋กœ ์ด๋™ํ•˜๋Š” ๊ณตํ†ต ์•ก์…˜ ํ•จ์ˆ˜ + * @param {Object} params - ์ด๋™ ํŒŒ๋ผ๋ฏธํ„ฐ + * @param {string} params.patnrId - ํŒŒํŠธ๋„ˆ ID + * @param {string} params.prdtId - ์ƒํ’ˆ ID + * @param {string} [params.curationId] - ํ๋ ˆ์ด์…˜ ID (ํ…Œ๋งˆ ์ƒํ’ˆ์ธ ๊ฒฝ์šฐ) + * @param {string} [params.nowShelf] - ํ˜„์žฌ ์…ธํ”„ ID + * @param {string} [params.type] - ์ƒํ’ˆ ํƒ€์ž… ('theme' ๋“ฑ) + * @param {string} [params.sourceMenu] - ์‹œ์ž‘ ๋ฉ”๋‰ด (SOURCE_MENUS ์ƒ์ˆ˜ ์‚ฌ์šฉ) + * @param {Object} [params.additionalInfo] - ์ถ”๊ฐ€ ์ •๋ณด + * @returns {Function} Redux thunk ํ•จ์ˆ˜ + */ +export const navigateToDetail = ({ + patnrId, + prdtId, + curationId, + nowShelf, + type, + sourceMenu, + additionalInfo = {}, +}) => { + return (dispatch, getState) => { + const panelInfo = { + patnrId, + prdtId, + ...additionalInfo, + }; + + // ์„ ํƒ์  ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค ์ถ”๊ฐ€ + if (curationId) panelInfo.curationId = curationId; + if (nowShelf) panelInfo.nowShelf = nowShelf; + if (type) panelInfo.type = type; + if (sourceMenu) panelInfo.sourceMenu = sourceMenu; + + // ๋กœ๊น… + console.log(`[navigateToDetail] ${sourceMenu || 'unknown'} โ†’ DetailPanel`, { + patnrId, + prdtId, + curationId, + nowShelf, + type, + sourceMenu, + timestamp: Date.now(), + }); + + // sourceMenu์— ๋”ฐ๋ฅธ ์‚ฌ์ „ ์ฒ˜๋ฆฌ + switch (sourceMenu) { + case SOURCE_MENUS.HOME_BEST_SELLER: + case SOURCE_MENUS.HOME_PICKED_FOR_YOU: + case SOURCE_MENUS.HOME_SUB_CATEGORY: + case SOURCE_MENUS.HOME_EVENT_POPUP: + case SOURCE_MENUS.HOME_TODAYS_DEAL: + case SOURCE_MENUS.HOME_RANDOM_UNIT: + case SOURCE_MENUS.HOME_ROLLING_UNIT: + case SOURCE_MENUS.HOME_GENERAL: { + // ๐Ÿ”ฝ ๋ชจ๋“  HomePanel์—์„œ DetailPanel๋กœ ์ด๋™ ์‹œ HomeBanner modal ๋น„๋””์˜ค ์ •์ง€ + const state = getState(); + const playerPanelInfo = state.panels.panels.find( + (p) => p.name === panel_names.PLAYER_PANEL + ); + + // playerPanel์ด ์—†๋Š” ๊ฒฝ์šฐ ๋น„๋””์˜ค ์ •์ง€ ๋กœ์ง ๊ฑด๋„ˆ๋›ฐ๊ธฐ + if (!playerPanelInfo) { + // ๋น„๋””์˜ค๊ฐ€ ์—†์–ด๋„ HomePanel ์ƒํƒœ ์ €์žฅ + dispatch( + updatePanel({ + name: panel_names.HOME_PANEL, + panelInfo: { + lastSelectedProduct: { patnrId, prdtId }, + lastActionSource: sourceMenu, + ...additionalInfo, + }, + }) + ); + panelInfo.fromHome = true; + break; + } + + const isCurrentBannerVideoPlaying = playerPanelInfo.panelInfo?.modal !== false; + + // HomeBanner์˜ modal=true ๋น„๋””์˜ค๊ฐ€ ์žฌ์ƒ ์ค‘์ด๋ฉด ์ •์ง€ + if (isCurrentBannerVideoPlaying) { + // ๐Ÿ”ฝ ๋น„๋””์˜ค ์ƒํƒœ ์ €์žฅ ํ›„ ์ •์ง€ + const { finishVideoPreview } = require('./playActions'); + + // ๋น„๋””์˜ค ๋ณต์›์„ ์œ„ํ•œ ์ƒํƒœ ์ €์žฅ + const videoStateToRestore = { + ...playerPanelInfo.panelInfo, + wasPlaying: true, + restoreOnBack: true, + sourceMenu, + timestamp: Date.now(), + }; + + // HomePanel์— ๋น„๋””์˜ค ๋ณต์› ์ƒํƒœ ์ €์žฅ + dispatch( + updatePanel({ + name: panel_names.HOME_PANEL, + panelInfo: { + videoStateToRestore, + lastSelectedProduct: { patnrId, prdtId }, + lastActionSource: sourceMenu, + ...additionalInfo, + }, + }) + ); + + // ๋น„๋””์˜ค ์ƒํƒœ ์ €์žฅ ํ›„ ์ •์ง€ (๋กœ๊ทธ๋Š” ๊ฐœ๋ฐœ ์‹œ ํ•„์š” ์‹œ ์ฃผ์„ ํ•ด์ œ) + + dispatch(finishVideoPreview()); + } else { + // ๋น„๋””์˜ค๊ฐ€ ์žฌ์ƒ ์ค‘์ด ์•„๋‹ˆ์–ด๋„ HomePanel ์ƒํƒœ ์ €์žฅ + dispatch( + updatePanel({ + name: panel_names.HOME_PANEL, + panelInfo: { + lastSelectedProduct: { patnrId, prdtId }, + lastActionSource: sourceMenu, + ...additionalInfo, + }, + }) + ); + } + + // HomePanel ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋“ค: ๊ธฐ๋ณธ HomePanel ์ƒํƒœ ์ €์žฅ + dispatch( + updatePanel({ + name: panel_names.HOME_PANEL, + panelInfo: { + lastSelectedProduct: { patnrId, prdtId }, + lastActionSource: sourceMenu, + ...additionalInfo, + }, + }) + ); + panelInfo.fromHome = true; + break; + } + + case SOURCE_MENUS.SEARCH_RESULT: + // Search: ํ˜„์žฌ ํŒจ๋„ ์ƒํƒœ ์ €์žฅ (updatePanel) + if (additionalInfo.searchVal && additionalInfo.currentSpot) { + dispatch( + updatePanel({ + name: panel_names.SEARCH_PANEL, + panelInfo: { + searchVal: additionalInfo.searchVal, + currentSpot: additionalInfo.currentSpot, + tab: additionalInfo.tab || 0, + }, + }) + ); + } + panelInfo.fromSearch = true; + panelInfo.searchQuery = additionalInfo.searchVal; + break; + + case SOURCE_MENUS.THEMED_PRODUCT: + // ํ…Œ๋งˆ ์ƒํ’ˆ: ๋ณ„๋„ ์ฒ˜๋ฆฌ ํ•„์š”ํ•  ๊ฒฝ์šฐ + break; + + case SOURCE_MENUS.GENERAL_PRODUCT: + default: + // ์ผ๋ฐ˜ ์ƒํ’ˆ: ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ + break; + } + + // DetailPanel push + dispatch( + pushPanel({ + name: panel_names.DETAIL_PANEL, + panelInfo, + }) + ); + }; +}; + +/** + * ํ…Œ๋งˆ ์ƒํ’ˆ์„ ์œ„ํ•œ DetailPanel ์ด๋™ ํ—ฌํผ ํ•จ์ˆ˜ + * @param {Object} params - ์ด๋™ ํŒŒ๋ผ๋ฏธํ„ฐ + * @returns {Function} Redux thunk + */ +export const navigateToThemeDetail = ({ + patnrId, + prdtId, + curationId, + sourceMenu = SOURCE_MENUS.THEMED_PRODUCT, + ...additionalInfo +}) => { + return navigateToDetail({ + patnrId, + prdtId, + curationId, + type: 'theme', + sourceMenu, + ...additionalInfo, + }); +}; + +/** + * ํ™ˆํŒจ๋„ BestSeller์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromBestSeller = ({ patnrId, prdtId, spotlightId }) => { + return navigateToDetail({ + patnrId, + prdtId, + nowShelf: spotlightId, + sourceMenu: SOURCE_MENUS.HOME_BEST_SELLER, + }); +}; + +/** + * ํ™ˆํŒจ๋„ PickedForYou์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromPickedForYou = ({ patnrId, prdtId, spotlightId }) => { + return navigateToDetail({ + patnrId, + prdtId, + nowShelf: spotlightId, + sourceMenu: SOURCE_MENUS.HOME_PICKED_FOR_YOU, + }); +}; + +/** + * ํ™ˆํŒจ๋„ SubCategory์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromSubCategory = ({ patnrId, prdtId, spotlightId }) => { + return navigateToDetail({ + patnrId, + prdtId, + nowShelf: spotlightId, + sourceMenu: SOURCE_MENUS.HOME_SUB_CATEGORY, + }); +}; + +/** + * ํ™ˆํŒจ๋„ RandomUnit ๋ฐฐ๋„ˆ์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromRandomUnit = ({ patnrId, prdtId, curationId, type = 'product' }) => { + return navigateToDetail({ + patnrId, + prdtId, + curationId, + type: type === 'theme' ? 'theme' : undefined, + sourceMenu: SOURCE_MENUS.HOME_RANDOM_UNIT, + }); +}; + +/** + * ํ™ˆํŒจ๋„ RollingUnit ๋ฐฐ๋„ˆ์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromRollingUnit = ({ patnrId, prdtId, curationId, additionalInfo = {} }) => { + return navigateToDetail({ + patnrId, + prdtId, + curationId, + sourceMenu: SOURCE_MENUS.HOME_ROLLING_UNIT, + ...additionalInfo, + }); +}; + +/** + * ํ™ˆํŒจ๋„ EventPopUpBanner์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromEventPopup = ({ patnrId, prdtId }) => { + return navigateToDetail({ + patnrId, + prdtId, + sourceMenu: SOURCE_MENUS.HOME_EVENT_POPUP, + }); +}; + +/** + * SearchPanel์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ๊ฒ€์ƒ‰ ๋ฐ ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromSearch = ({ + patnrId, + prdtId, + searchQuery, + currentSpot, + additionalInfo = {}, +}) => { + return navigateToDetail({ + patnrId, + prdtId, + sourceMenu: SOURCE_MENUS.SEARCH_RESULT, + additionalInfo: { + searchVal: searchQuery, + currentSpot, + tab: 0, + ...additionalInfo, + }, + }); +}; + +/** + * HomePanel ์ผ๋ฐ˜ ํด๋ฆญ์—์„œ DetailPanel๋กœ ์ด๋™ + * @param {Object} params - ์ƒํ’ˆ ์ •๋ณด + * @returns {Function} Redux thunk + */ +export const navigateFromHomeGeneral = ({ patnrId, prdtId, additionalInfo = {} }) => { + return navigateToDetail({ + patnrId, + prdtId, + sourceMenu: SOURCE_MENUS.HOME_GENERAL, + additionalInfo, + }); +}; + +/** + * DetailPanel์—์„œ ๋Œ์•„์˜ฌ ๋•Œ ๋น„๋””์˜ค ๋ณต์› ํ•จ์ˆ˜ + * HomePanel์— ์ €์žฅ๋œ ๋น„๋””์˜ค ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋ณต์› + * @returns {Function} Redux thunk + */ +export const restoreVideoOnBack = () => { + return (dispatch, getState) => { + const state = getState(); + const panels = state.panels.panels; + + // HomePanel ์ฐพ๊ธฐ + const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL); + const videoStateToRestore = homePanel?.panelInfo?.videoStateToRestore; + + if (!videoStateToRestore || !videoStateToRestore.restoreOnBack) { + return; + } + + // ๋น„๋””์˜ค ๋ณต์› ์‹œ์ž‘ (๋กœ๊ทธ๋Š” ๊ฐœ๋ฐœ ์‹œ ํ•„์š” ์‹œ ์ฃผ์„ ํ•ด์ œ) + + // ๋น„๋””์˜ค ์ƒํƒœ ๋ณต์› + const { startVideoPlayerNew } = require('./playActions'); + + // ๋ณต์›ํ•  ๋น„๋””์˜ค ์ •๋ณด ์ถ”์ถœ + const restoreInfo = { + bannerId: videoStateToRestore.bannerId || videoStateToRestore.playerState?.currentBannerId, + patnrId: videoStateToRestore.patnrId, + showId: videoStateToRestore.showId, + shptmBanrTpNm: videoStateToRestore.shptmBanrTpNm, + lgCatCd: videoStateToRestore.lgCatCd, + modal: true, // HomeBanner๋Š” ํ•ญ์ƒ modal + modalContainerId: videoStateToRestore.modalContainerId, + modalClassName: videoStateToRestore.modalClassName, + chanId: videoStateToRestore.chanId, + }; + + // ๋น„๋””์˜ค ์žฌ์ƒ ์‹œ์ž‘ + dispatch( + startVideoPlayerNew({ + ...restoreInfo, + spotlightDisable: false, + }) + ); + + // ๋ณต์› ์ƒํƒœ ์ •๋ฆฌ + dispatch( + updatePanel({ + name: panel_names.HOME_PANEL, + panelInfo: { + ...homePanel.panelInfo, + videoStateToRestore: { + ...videoStateToRestore, + restoreOnBack: false, // ๋ณต์› ์™„๋ฃŒ ํ›„ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™” + }, + }, + }) + ); + }; +}; + +/** + * DetailPanel ๋‹ซ๊ธฐ ์‹œ ๋น„๋””์˜ค ๋ณต์› ํ™•์ธ ํ•จ์ˆ˜ + * DetailPanel ํŒจ๋„์ด ์ œ๊ฑฐ๋  ๋•Œ ์ž๋™์œผ๋กœ ๋น„๋””์˜ค ๋ณต์› ์‹œ๋„ + * @returns {Function} Redux thunk + */ +export const handleDetailPanelCloseWithVideoRestore = () => { + return (dispatch, getState) => { + const state = getState(); + const panels = state.panels.panels; + + // ํ˜„์žฌ ์ตœ์ƒ๋‹จ ํŒจ๋„์ด DetailPanel์ธ์ง€ ํ™•์ธ + const topPanel = panels[panels.length - 1]; + + if (topPanel?.name === panel_names.DETAIL_PANEL) { + // ๊ธฐ์กด DetailPanel ๋‹ซ๊ธฐ ๋กœ์ง ์ˆ˜ํ–‰ + dispatch({ + type: 'POP_PANEL_WITH_VIDEO_RESTORE', + payload: panel_names.DETAIL_PANEL, + }); + + // ๋น„๋””์˜ค ๋ณต์› ์‹œ๋„ (์•ฝ๊ฐ„์˜ ์ง€์—ฐ ํ›„) + setTimeout(() => { + dispatch(restoreVideoOnBack()); + }, 100); + } + }; +}; + /** * [251114] ๋ช…์‹œ์  ํฌ์ปค์Šค ์ด๋™ * Panel์˜ ๋น„๋™๊ธฐ ์ž‘์—…(useEffect, ํƒ€์ด๋จธ ๋“ฑ)์ด ํฌ์ปค์Šค๋ฅผ ํƒˆ์ทจํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ diff --git a/com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js b/com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js index ab465ca9..6a66af58 100644 --- a/com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js +++ b/com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js @@ -1,989 +1,959 @@ -import React from "react"; -import ReactDOM from "react-dom"; - -import classNames from "classnames"; -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import onlyUpdateForKeys from "recompose/onlyUpdateForKeys"; - -import { off, on } from "@enact/core/dispatcher"; -import { forward } from "@enact/core/handle"; -import hoc from "@enact/core/hoc"; -import ApiDecorator from "@enact/core/internal/ApiDecorator"; -import { is } from "@enact/core/keymap"; -import kind from "@enact/core/kind"; -import { Job } from "@enact/core/util"; -import ActionGuide from "@enact/sandstone/ActionGuide"; -import Button from "@enact/sandstone/Button"; -import $L from "@enact/sandstone/internal/$L"; -import { compareChildren } from "@enact/sandstone/internal/util"; -import Spotlight from "@enact/spotlight"; -import Pause from "@enact/spotlight/Pause"; -import { - SpotlightContainerDecorator, - spotlightDefaultClass, -} from "@enact/spotlight/SpotlightContainerDecorator"; -import Cancelable from "@enact/ui/Cancelable"; -import Slottable from "@enact/ui/Slottable"; - -import ShopTimePlayIcon from "../../../assets/images/btn/btn-video-play-nor@3x.png"; -import ShopTimePauseIcon from "../../../assets/images/btn/btn-voc-pause-nor@3x.png"; -import { SpotlightIds } from "../../utils/SpotlightIds"; -import css from "./MediaControls.module.less"; -import { countReactChildren } from "./util"; - -const OuterContainer = SpotlightContainerDecorator( - { - defaultElement: [`.${spotlightDefaultClass}`], - }, - "div" -); -const Container = SpotlightContainerDecorator( - { - enterTo: "default-element", - }, - "div" -); -const MediaButton = onlyUpdateForKeys([ - "children", - "className", - "disabled", - "icon", - "onClick", - "spotlightDisabled", - "onSpotlightRight", -])(Button); - -const forwardToggleMore = forward("onToggleMore"); - -const animationDuration = 300; - -/** - * A set of components for controlling media playback and rendering additional components. - * - * @class MediaControlsBase - * @memberof sandstone/MediaPlayer - * @ui - * @private - */ -const MediaControlsBase = kind({ - name: "MediaControls", - - // intentionally assigning these props to MediaControls instead of Base (which is private) - propTypes: /** @lends sandstone/MediaPlayer.MediaControls.prototype */ { - /** - * DOM id for the component. - * - * This child component `ActionGuide`'s id is generated from the id. - * - * @type {String} - * @required - * @public - */ - id: PropTypes.string.isRequired, - - /** - * The `aria-label` for the action guide. - * - * @type {String} - * @public - */ - actionGuideAriaLabel: PropTypes.string, - - /** - * The label for the action guide. - * - * @type {String} - * @public - */ - actionGuideLabel: PropTypes.string, - - /** - * These components are placed below the action guide. Typically these will be media playlist controls. - * - * @type {Node} - * @public - */ - bottomComponents: PropTypes.node, - - /** - * Jump backward [icon]{@link sandstone/Icon.Icon} name. Accepts any - * [icon]{@link sandstone/Icon.Icon} component type. - * - * @type {String} - * @default 'jumpbackward' - * @public - */ - jumpBackwardIcon: PropTypes.string, - - /** - * Disables state on the media "jump" buttons; the outer pair. - * - * @type {Boolean} - * @public - */ - jumpBackwardDisabled: PropTypes.bool, - jumpForwardDisabled: PropTypes.bool, - - /** - * Jump forward [icon]{@link sandstone/Icon.Icon} name. Accepts any - * [icon]{@link sandstone/Icon.Icon} component type. - * - * @type {String} - * @default 'jumpforward' - * @public - */ - jumpForwardIcon: PropTypes.string, - - /** - * Disables the media buttons. - * - * @type {Boolean} - * @public - */ - mediaDisabled: PropTypes.bool, - - /** - * When `true`, more components are rendered. This does not indicate the visibility of more components. - * - * @type {Boolean} - * @public - */ - moreComponentsRendered: PropTypes.bool, - - /** - * The spotlight ID for the moreComponent container. - * - * @type {String} - * @public - * @default 'moreComponents' - */ - moreComponentsSpotlightId: PropTypes.string, - - /** - * Removes the "jump" buttons. The buttons that skip forward or backward in the video. - * - * @type {Boolean} - * @public - */ - noJumpButtons: PropTypes.bool, - - /** - * Called when cancel/back key events are fired. - * - * @type {Function} - * @public - */ - onClose: PropTypes.func, - - /** - * Called when the user clicks the JumpBackward button - * - * @type {Function} - * @public - */ - onJumpBackwardButtonClick: PropTypes.func, - - /** - * Called when the user clicks the JumpForward button. - * - * @type {Function} - * @public - */ - onJumpForwardButtonClick: PropTypes.func, - - /** - * Called when the user presses a media control button. - * - * @type {Function} - * @public - */ - onKeyDownFromMediaButtons: PropTypes.func, - - /** - * Called when the user clicks the Play button. - * - * @type {Function} - * @public - */ - onPlayButtonClick: PropTypes.func, - - /** - * `true` when the video is paused. - * - * @type {Boolean} - * @public - */ - paused: PropTypes.bool, - - /** - * A string which is sent to the `pause` icon of the player controls. This can be - * anything that is accepted by [Icon]{@link sandstone/Icon.Icon}. This will be temporarily replaced by - * the [playIcon]{@link sandstone/MediaPlayer.MediaControls.playIcon} when the - * [paused]{@link sandstone/MediaPlayer.MediaControls.paused} boolean is `false`. - * - * @type {String} - * @default 'pause' - * @public - */ - pauseIcon: PropTypes.string, - - /** - * A string which is sent to the `play` icon of the player controls. This can be - * anything that is accepted by {@link sandstone/Icon.Icon}. This will be temporarily replaced by - * the [pauseIcon]{@link sandstone/MediaPlayer.MediaControls.pauseIcon} when the - * [paused]{@link sandstone/MediaPlayer.MediaControls.paused} boolean is `true`. - * - * @type {String} - * @default 'play' - * @public - */ - playIcon: PropTypes.string, - - /** - * Disables the media "play"/"pause" button. - * - * @type {Boolean} - * @public - */ - playPauseButtonDisabled: PropTypes.bool, - - /** - * When `true`, more components are visible. - * - * @type {Boolean} - * @private - */ - showMoreComponents: PropTypes.bool, - - /** - * `true` controls are disabled from Spotlight. - * - * @type {Boolean} - * @public - */ - spotlightDisabled: PropTypes.bool, - - /** - * The spotlight ID for the media controls container. - * - * @type {String} - * @public - * @default 'mediaControls' - */ - spotlightId: PropTypes.string, - - /** - * The visibility of the component. When `false`, the component will be hidden. - * - * @type {Boolean} - * @default true - * @public - */ - visible: PropTypes.bool, - - onActionGuideClick: PropTypes.func, - }, - - defaultProps: { - jumpBackwardIcon: "jumpbackward", - jumpForwardIcon: "jumpforward", - moreComponentsSpotlightId: "moreComponents", - spotlightId: "mediaControls", - pauseIcon: "pause", - playIcon: "play", - visible: true, - }, - - styles: { - css, - className: "controlsFrame", - }, - - computed: { - actionGuideClassName: ({ styler, showMoreComponents }) => - styler.join({ hidden: showMoreComponents }), - actionGuideShowing: ({ bottomComponents, children }) => - countReactChildren(children) || bottomComponents, - className: ({ visible, styler }) => styler.append({ hidden: !visible }), - moreButtonsClassName: ({ styler }) => - styler.join("mediaControls", "moreButtonsComponents"), - moreComponentsRendered: ({ showMoreComponents, moreComponentsRendered }) => - showMoreComponents || moreComponentsRendered, - }, - - render: ({ - actionGuideAriaLabel, - actionGuideLabel, - actionGuideShowing, - children, - id, - jumpBackwardIcon, - jumpBackwardDisabled, - jumpForwardDisabled, - jumpForwardIcon, - bottomComponents, - mediaDisabled, - moreComponentsSpotlightId, - noJumpButtons, - onJumpBackwardButtonClick, - onJumpForwardButtonClick, - onKeyDownFromMediaButtons, - onPlayButtonClick, - paused, - pauseIcon, - playIcon, - playPauseButtonDisabled, - showMoreComponents, - moreComponentsRendered, - moreButtonsClassName, - actionGuideClassName, - spotlightDisabled, - spotlightId, - onActionGuideClick, - videoVerticalVisible, - countryCode, - type, - ...rest - }) => { - delete rest.onClose; - delete rest.visible; - - const onSpotlightRight = (e) => { - e.preventDefault(); - e.stopPropagation(); - - if (videoVerticalVisible && countryCode !== "US") { - Spotlight.focus("tab-0"); - } - Spotlight.focus(SpotlightIds.PLAYER_SUBTITLE_BUTTON); - }; - - const onSpotlightUp = (e) => { - e.preventDefault(); - e.stopPropagation(); - Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); - }; - - return ( - - {type !== "LIVE" && ( - - {/* {noJumpButtons ? null : } */} - - - - {/* {noJumpButtons ? null : } */} - - )} - {actionGuideShowing ? ( - - {actionGuideLabel} - - ) : null} - {moreComponentsRendered ? ( - - {children} -
{bottomComponents}
-
- ) : null} -
- ); - }, -}); - -/** - * Media control behaviors to apply to [MediaControlsBase]{@link sandstone/MediaPlayer.MediaControlsBase}. - * Provides built-in support for showing more components and key handling for basic playback - * controls. - * - * @class MediaControlsDecorator - * @memberof sandstone/MediaPlayer - * @mixes ui/Slottable.Slottable - * @hoc - * @private - */ -const MediaControlsDecorator = hoc((config, Wrapped) => { - // eslint-disable-line no-unused-vars - class MediaControlsDecoratorHOC extends React.Component { - static displayName = "MediaControlsDecorator"; - - static propTypes = - /** @lends sandstone/MediaPlayer.MediaControlsDecorator.prototype */ { - /** - * The label for the action guide. - * - * @type {String} - * @public - */ - actionGuideLabel: PropTypes.string, - - /** - * These components are placed below the children. Typically these will be media playlist items. - * - * @type {Node} - * @public - */ - bottomComponents: PropTypes.node, - - /** - * The number of milliseconds that the player will pause before firing the - * first jump event on a right or left pulse. - * - * @type {Number} - * @default 400 - * @public - */ - initialJumpDelay: PropTypes.number, - - /** - * The number of milliseconds that the player will throttle before firing a - * jump event on a right or left pulse. - * - * @type {Number} - * @default 200 - * @public - */ - jumpDelay: PropTypes.number, - - /** - * Disables the media buttons. - * - * @type {Boolean} - * @public - */ - mediaDisabled: PropTypes.bool, - - /** - * Disables showing more components. - * - * @type {Boolean} - * @public - */ - moreActionDisabled: PropTypes.bool, - - /** - * Setting this to `true` will disable left and right keys for seeking. - * - * @type {Boolean} - * @public - */ - no5WayJump: PropTypes.bool, - - /** - * Called when media fast forwards. - * - * @type {Function} - * @public - */ - onFastForward: PropTypes.func, - - /** - * Called when media jumps. - * - * @type {Function} - * @public - */ - onJump: PropTypes.func, - - /** - * Called when media gets paused. - * - * @type {Function} - * @public - */ - onPause: PropTypes.func, - - /** - * Called when media starts playing. - * - * @type {Function} - * @public - */ - onPlay: PropTypes.func, - - /** - * Called when media rewinds. - * - * @type {Function} - * @public - */ - onRewind: PropTypes.func, - - /** - * Called when the visibility of more components is changed - * - * Event payload includes: - * - * * `type` - Type of event, `'onToggleMore'` - * * `showMoreComponents` - `true` when the components are visible` - * * `liftDistance` - The distance, in pixels, the component animates - * - * @type {Function} - * @public - */ - onToggleMore: PropTypes.func, - - /** - * The video pause state. - * - * @type {Boolean} - * @public - */ - paused: PropTypes.bool, - - /** - * Disables state on the media "play"/"pause" button - * - * @type {Boolean} - * @public - */ - playPauseButtonDisabled: PropTypes.bool, - - /** - * Disables the media playback-rate control via rewind and fast forward keys - * - * @type {Boolean} - * @public - */ - rateChangeDisabled: PropTypes.bool, - - /** - * Registers the MediaControls component with an - * [ApiDecorator]{@link core/internal/ApiDecorator.ApiDecorator}. - * - * @type {Function} - * @private - */ - setApiProvider: PropTypes.func, - - /** - * The visibility of the component. When `false`, the component will be hidden. - * - * @type {Boolean} - * @public - */ - visible: PropTypes.bool, - }; - - static defaultProps = { - initialJumpDelay: 400, - jumpDelay: 200, - }; - - constructor(props) { - super(props); - this.mediaControlsNode = null; - this.moreComponentsNode = null; - - this.actionGuideHeight = 0; - this.animation = null; - this.bottomComponentsHeight = 0; - this.keyLoop = null; - this.pulsingKeyCode = null; - this.pulsing = null; - this.paused = new Pause("MediaPlayer"); - - this.state = { - showMoreComponents: false, - moreComponentsRendered: false, - }; - - if (props.setApiProvider) { - props.setApiProvider(this); - } - } - - static getDerivedStateFromProps(props) { - if (!props.visible) { - return { - showMoreComponents: false, - }; - } - return null; - } - - componentDidMount() { - on("keydown", this.handleKeyDown); - on("keyup", this.handleKeyUp); - on("blur", this.handleBlur, window); - on("wheel", this.handleWheel); - } - - componentDidUpdate(prevProps, prevState) { - // Need to render `moreComponents` to show it. For performance, render `moreComponents` if it is actually shown. - if ( - !prevState.showMoreComponents && - this.state.showMoreComponents && - !this.state.moreComponentsRendered - ) { - this.moreComponentsRenderingJob.startRafAfter(); - } else if ( - prevState.showMoreComponents && - !this.state.showMoreComponents - ) { - this.moreComponentsRenderingJob.stop(); - } - - if ( - (!prevState.moreComponentsRendered && - this.state.moreComponentsRendered) || - (this.state.moreComponentsRendered && - prevProps.bottomComponents !== this.props.bottomComponents) || - !compareChildren(this.props.children, prevProps.children) - ) { - this.calculateMoreComponentsHeight(); - } - - if ( - (this.state.showMoreComponents && - !prevState.moreComponentsRendered && - this.state.moreComponentsRendered) || - (this.state.moreComponentsRendered && - prevState.showMoreComponents !== this.state.showMoreComponents) - ) { - forwardToggleMore( - { - type: "onToggleMore", - showMoreComponents: this.state.showMoreComponents, - liftDistance: this.bottomComponentsHeight - this.actionGuideHeight, - }, - this.props - ); - - if (this.state.showMoreComponents) { - this.moreComponentsNode = - this.moreComponentsNode || - this.mediaControlsNode.querySelector(`.${css.moreComponents}`); - this.paused.pause(); - this.animation = this.moreComponentsNode.animate( - [ - { transform: "none", opacity: 0, offset: 0 }, - { - transform: `translateY(${-this.actionGuideHeight}px)`, - opacity: 1, - offset: 1, - }, - ], - { - duration: animationDuration, - fill: "forwards", - } - ); - this.animation.onfinish = this.handleFinish; - this.animation.oncancel = this.handleCancel; - } else if (this.animation != null) { - this.animation.cancel(); - } - } - - // if media controls disabled, reset key loop - if (!prevProps.mediaDisabled && this.props.mediaDisabled) { - this.stopListeningForPulses(); - this.paused.resume(); - } - } - - componentWillUnmount() { - off("keydown", this.handleKeyDown); - off("keyup", this.handleKeyUp); - off("blur", this.handleBlur, window); - off("wheel", this.handleWheel); - this.stopListeningForPulses(); - this.moreComponentsRenderingJob.stop(); - if (this.animation) { - this.animation.cancel(); - } - } - - moreComponentsRenderingJob = new Job(() => { - this.setState({ - moreComponentsRendered: true, - }); - }); - - calculateMoreComponentsHeight = () => { - if (!this.mediaControlsNode) { - this.bottomComponentsHeight = 0; - return; - } - - const bottomElement = this.mediaControlsNode.querySelector( - `.${css.moreComponents}` - ); - this.bottomComponentsHeight = bottomElement - ? bottomElement.scrollHeight - : 0; - }; - - handleKeyDownFromMediaButtons = (ev) => { - if ( - is("down", ev.keyCode) && - !this.state.showMoreComponents && - !this.props.moreActionDisabled - ) { - this.showMoreComponents(); - ev.stopPropagation(); - } - }; - - handleKeyDown = (ev) => { - const { mediaDisabled, no5WayJump, visible } = this.props; - - const current = Spotlight.getCurrent(); - - if ( - !no5WayJump && - !visible && - !mediaDisabled && - !current && - (is("left", ev.keyCode) || is("right", ev.keyCode)) - ) { - this.paused.pause(); - this.startListeningForPulses(ev.keyCode); - } - }; - - handleKeyUp = (ev) => { - const { - mediaDisabled, - no5WayJump, - rateChangeDisabled, - playPauseButtonDisabled, - } = this.props; - - if (mediaDisabled) return; - - if (!playPauseButtonDisabled) { - if (is("play", ev.keyCode)) { - forward("onPlay", ev, this.props); - } else if (is("pause", ev.keyCode)) { - forward("onPause", ev, this.props); - } - } - - if (!no5WayJump && (is("left", ev.keyCode) || is("right", ev.keyCode))) { - this.stopListeningForPulses(); - this.paused.resume(); - } - - if (!rateChangeDisabled) { - if (is("rewind", ev.keyCode)) { - forward("onRewind", ev, this.props); - } else if (is("fastForward", ev.keyCode)) { - forward("onFastForward", ev, this.props); - } - } - }; - - handleBlur = () => { - this.stopListeningForPulses(); - this.paused.resume(); - }; - - handleWheel = (ev) => { - if ( - !this.state.showMoreComponents && - this.props.visible && - !this.props.moreActionDisabled && - ev.deltaY > 0 - ) { - this.showMoreComponents(); - } - }; - - startListeningForPulses = (keyCode) => { - // Ignore new pulse calls if key code is same, otherwise start new series if we're pulsing - if (this.pulsing && keyCode !== this.pulsingKeyCode) { - this.stopListeningForPulses(); - } - if (!this.pulsing) { - this.pulsingKeyCode = keyCode; - this.pulsing = true; - this.keyLoop = setTimeout( - this.handlePulse, - this.props.initialJumpDelay - ); - forward("onJump", { keyCode }, this.props); - } - }; - - handlePulse = () => { - forward("onJump", { keyCode: this.pulsingKeyCode }, this.props); - this.keyLoop = setTimeout(this.handlePulse, this.props.jumpDelay); - }; - - handlePlayButtonClick = (ev) => { - forward("onPlayButtonClick", ev, this.props); - if (this.props.paused) { - forward("onPlay", ev, this.props); - } else { - forward("onPause", ev, this.props); - } - }; - - stopListeningForPulses() { - this.pulsing = false; - if (this.keyLoop) { - clearTimeout(this.keyLoop); - this.keyLoop = null; - } - } - - getMediaControls = (node) => { - if (!node) { - this.actionGuideHeight = 0; - return; - } - this.mediaControlsNode = ReactDOM.findDOMNode(node); // eslint-disable-line react/no-find-dom-node - - const guideElement = this.mediaControlsNode.querySelector( - `.${css.actionGuide}` - ); - this.actionGuideHeight = guideElement ? guideElement.scrollHeight : 0; - }; - - areMoreComponentsAvailable = () => { - return this.state.showMoreComponents; - }; - - showMoreComponents = () => { - this.setState({ showMoreComponents: true }); - }; - - hideMoreComponents = () => { - this.setState({ showMoreComponents: false }); - }; - - toggleMoreComponents() { - this.setState((prevState) => { - return { - showMoreComponents: !prevState.showMoreComponents, - }; - }); - } - - handleClose = (ev) => { - if (this.props.visible) { - forward("onClose", ev, this.props); - } - }; - - handleFinish = () => { - if (this.state.showMoreComponents) { - this.paused.resume(); - if (!Spotlight.getPointerMode()) { - Spotlight.move("down"); - } - } - }; - - handleCancel = () => { - this.paused.resume(); - }; - onActionGuideClick = () => { - this.showMoreComponents(); - }; - render() { - const props = Object.assign({}, this.props); - delete props.initialJumpDelay; - delete props.jumpDelay; - delete props.moreActionDisabled; - delete props.no5WayJump; - delete props.onFastForward; - delete props.onJump; - delete props.onPause; - delete props.onPlay; - delete props.onRewind; - delete props.onToggleMore; - delete props.rateChangeDisabled; - delete props.setApiProvider; - - return ( - - ); - } - } - - return Slottable({ slots: ["bottomComponents"] }, MediaControlsDecoratorHOC); -}); - -const handleCancel = (ev, { onClose }) => { - if (onClose) { - onClose(ev); - } -}; - -/** - * A set of components for controlling media playback and rendering additional components. - * - * This uses [Slottable]{@link ui/Slottable} to accept the custom tags, `` - * to add components to the bottom of the media controls. Any additional children will be - * rendered into the "more" controls area. Showing the additional components is handled by - * `MediaControls` when the user navigates down from the media buttons. - * - * @class MediaControls - * @memberof sandstone/MediaPlayer - * @mixes ui/Cancelable.Cancelable - * @ui - * @public - */ -const MediaControls = ApiDecorator( - { - api: [ - "areMoreComponentsAvailable", - "showMoreComponents", - "hideMoreComponents", - ], - }, - MediaControlsDecorator( - Cancelable({ modal: true, onCancel: handleCancel }, MediaControlsBase) - ) -); - -MediaControls.defaultSlot = "mediaControlsComponent"; - -export default MediaControls; -export { MediaControls, MediaControlsBase, MediaControlsDecorator }; +import React from 'react'; +import ReactDOM from 'react-dom'; + +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; + +import { off, on } from '@enact/core/dispatcher'; +import { forward } from '@enact/core/handle'; +import hoc from '@enact/core/hoc'; +import ApiDecorator from '@enact/core/internal/ApiDecorator'; +import { is } from '@enact/core/keymap'; +import kind from '@enact/core/kind'; +import { Job } from '@enact/core/util'; +import ActionGuide from '@enact/sandstone/ActionGuide'; +import Button from '@enact/sandstone/Button'; +import $L from '@enact/sandstone/internal/$L'; +import { compareChildren } from '@enact/sandstone/internal/util'; +import Spotlight from '@enact/spotlight'; +import Pause from '@enact/spotlight/Pause'; +import { + SpotlightContainerDecorator, + spotlightDefaultClass, +} from '@enact/spotlight/SpotlightContainerDecorator'; +import Cancelable from '@enact/ui/Cancelable'; +import Slottable from '@enact/ui/Slottable'; + +import ShopTimePlayIcon from '../../../assets/images/btn/btn-video-play-nor@3x.png'; +import ShopTimePauseIcon from '../../../assets/images/btn/btn-voc-pause-nor@3x.png'; +import { SpotlightIds } from '../../utils/SpotlightIds'; +import css from './MediaControls.module.less'; +import { countReactChildren } from './util'; + +const OuterContainer = SpotlightContainerDecorator( + { + defaultElement: [`.${spotlightDefaultClass}`], + }, + 'div' +); +const Container = SpotlightContainerDecorator( + { + enterTo: 'default-element', + }, + 'div' +); +const MediaButton = onlyUpdateForKeys([ + 'children', + 'className', + 'disabled', + 'icon', + 'onClick', + 'spotlightDisabled', + 'onSpotlightRight', +])(Button); + +const forwardToggleMore = forward('onToggleMore'); + +const animationDuration = 300; + +/** + * A set of components for controlling media playback and rendering additional components. + * + * @class MediaControlsBase + * @memberof sandstone/MediaPlayer + * @ui + * @private + */ +const MediaControlsBase = kind({ + name: 'MediaControls', + + // intentionally assigning these props to MediaControls instead of Base (which is private) + propTypes: /** @lends sandstone/MediaPlayer.MediaControls.prototype */ { + /** + * DOM id for the component. + * + * This child component `ActionGuide`'s id is generated from the id. + * + * @type {String} + * @required + * @public + */ + id: PropTypes.string.isRequired, + + /** + * The `aria-label` for the action guide. + * + * @type {String} + * @public + */ + actionGuideAriaLabel: PropTypes.string, + + /** + * The label for the action guide. + * + * @type {String} + * @public + */ + actionGuideLabel: PropTypes.string, + + /** + * These components are placed below the action guide. Typically these will be media playlist controls. + * + * @type {Node} + * @public + */ + bottomComponents: PropTypes.node, + + /** + * Jump backward [icon]{@link sandstone/Icon.Icon} name. Accepts any + * [icon]{@link sandstone/Icon.Icon} component type. + * + * @type {String} + * @default 'jumpbackward' + * @public + */ + jumpBackwardIcon: PropTypes.string, + + /** + * Disables state on the media "jump" buttons; the outer pair. + * + * @type {Boolean} + * @public + */ + jumpBackwardDisabled: PropTypes.bool, + jumpForwardDisabled: PropTypes.bool, + + /** + * Jump forward [icon]{@link sandstone/Icon.Icon} name. Accepts any + * [icon]{@link sandstone/Icon.Icon} component type. + * + * @type {String} + * @default 'jumpforward' + * @public + */ + jumpForwardIcon: PropTypes.string, + + /** + * Disables the media buttons. + * + * @type {Boolean} + * @public + */ + mediaDisabled: PropTypes.bool, + + /** + * When `true`, more components are rendered. This does not indicate the visibility of more components. + * + * @type {Boolean} + * @public + */ + moreComponentsRendered: PropTypes.bool, + + /** + * The spotlight ID for the moreComponent container. + * + * @type {String} + * @public + * @default 'moreComponents' + */ + moreComponentsSpotlightId: PropTypes.string, + + /** + * Removes the "jump" buttons. The buttons that skip forward or backward in the video. + * + * @type {Boolean} + * @public + */ + noJumpButtons: PropTypes.bool, + + /** + * Called when cancel/back key events are fired. + * + * @type {Function} + * @public + */ + onClose: PropTypes.func, + + /** + * Called when the user clicks the JumpBackward button + * + * @type {Function} + * @public + */ + onJumpBackwardButtonClick: PropTypes.func, + + /** + * Called when the user clicks the JumpForward button. + * + * @type {Function} + * @public + */ + onJumpForwardButtonClick: PropTypes.func, + + /** + * Called when the user presses a media control button. + * + * @type {Function} + * @public + */ + onKeyDownFromMediaButtons: PropTypes.func, + + /** + * Called when the user clicks the Play button. + * + * @type {Function} + * @public + */ + onPlayButtonClick: PropTypes.func, + + /** + * `true` when the video is paused. + * + * @type {Boolean} + * @public + */ + paused: PropTypes.bool, + + /** + * A string which is sent to the `pause` icon of the player controls. This can be + * anything that is accepted by [Icon]{@link sandstone/Icon.Icon}. This will be temporarily replaced by + * the [playIcon]{@link sandstone/MediaPlayer.MediaControls.playIcon} when the + * [paused]{@link sandstone/MediaPlayer.MediaControls.paused} boolean is `false`. + * + * @type {String} + * @default 'pause' + * @public + */ + pauseIcon: PropTypes.string, + + /** + * A string which is sent to the `play` icon of the player controls. This can be + * anything that is accepted by {@link sandstone/Icon.Icon}. This will be temporarily replaced by + * the [pauseIcon]{@link sandstone/MediaPlayer.MediaControls.pauseIcon} when the + * [paused]{@link sandstone/MediaPlayer.MediaControls.paused} boolean is `true`. + * + * @type {String} + * @default 'play' + * @public + */ + playIcon: PropTypes.string, + + /** + * Disables the media "play"/"pause" button. + * + * @type {Boolean} + * @public + */ + playPauseButtonDisabled: PropTypes.bool, + + /** + * When `true`, more components are visible. + * + * @type {Boolean} + * @private + */ + showMoreComponents: PropTypes.bool, + + /** + * `true` controls are disabled from Spotlight. + * + * @type {Boolean} + * @public + */ + spotlightDisabled: PropTypes.bool, + + /** + * The spotlight ID for the media controls container. + * + * @type {String} + * @public + * @default 'mediaControls' + */ + spotlightId: PropTypes.string, + + /** + * The visibility of the component. When `false`, the component will be hidden. + * + * @type {Boolean} + * @default true + * @public + */ + visible: PropTypes.bool, + + onActionGuideClick: PropTypes.func, + }, + + defaultProps: { + jumpBackwardIcon: 'jumpbackward', + jumpForwardIcon: 'jumpforward', + moreComponentsSpotlightId: 'moreComponents', + spotlightId: 'mediaControls', + pauseIcon: 'pause', + playIcon: 'play', + visible: true, + }, + + styles: { + css, + className: 'controlsFrame', + }, + + computed: { + actionGuideClassName: ({ styler, showMoreComponents }) => + styler.join({ hidden: showMoreComponents }), + actionGuideShowing: ({ bottomComponents, children }) => + countReactChildren(children) || bottomComponents, + className: ({ visible, styler }) => styler.append({ hidden: !visible }), + moreButtonsClassName: ({ styler }) => styler.join('mediaControls', 'moreButtonsComponents'), + moreComponentsRendered: ({ showMoreComponents, moreComponentsRendered }) => + showMoreComponents || moreComponentsRendered, + }, + + render: ({ + actionGuideAriaLabel, + actionGuideLabel, + actionGuideShowing, + children, + id, + jumpBackwardIcon, + jumpBackwardDisabled, + jumpForwardDisabled, + jumpForwardIcon, + bottomComponents, + mediaDisabled, + moreComponentsSpotlightId, + noJumpButtons, + onJumpBackwardButtonClick, + onJumpForwardButtonClick, + onKeyDownFromMediaButtons, + onPlayButtonClick, + paused, + pauseIcon, + playIcon, + playPauseButtonDisabled, + showMoreComponents, + moreComponentsRendered, + moreButtonsClassName, + actionGuideClassName, + spotlightDisabled, + spotlightId, + onActionGuideClick, + videoVerticalVisible, + countryCode, + type, + ...rest + }) => { + delete rest.onClose; + delete rest.visible; + + const onSpotlightRight = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (videoVerticalVisible && countryCode !== 'US') { + Spotlight.focus('tab-0'); + } + Spotlight.focus(SpotlightIds.PLAYER_SUBTITLE_BUTTON); + }; + + const onSpotlightUp = (e) => { + e.preventDefault(); + e.stopPropagation(); + Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); + }; + + return ( + + {type !== 'LIVE' && ( + + {/* {noJumpButtons ? null : } */} + + + + {/* {noJumpButtons ? null : } */} + + )} + {actionGuideShowing ? ( + + {actionGuideLabel} + + ) : null} + {moreComponentsRendered ? ( + + {children} +
{bottomComponents}
+
+ ) : null} +
+ ); + }, +}); + +/** + * Media control behaviors to apply to [MediaControlsBase]{@link sandstone/MediaPlayer.MediaControlsBase}. + * Provides built-in support for showing more components and key handling for basic playback + * controls. + * + * @class MediaControlsDecorator + * @memberof sandstone/MediaPlayer + * @mixes ui/Slottable.Slottable + * @hoc + * @private + */ +const MediaControlsDecorator = hoc((config, Wrapped) => { + // eslint-disable-line no-unused-vars + class MediaControlsDecoratorHOC extends React.Component { + static displayName = 'MediaControlsDecorator'; + + static propTypes = /** @lends sandstone/MediaPlayer.MediaControlsDecorator.prototype */ { + /** + * The label for the action guide. + * + * @type {String} + * @public + */ + actionGuideLabel: PropTypes.string, + + /** + * These components are placed below the children. Typically these will be media playlist items. + * + * @type {Node} + * @public + */ + bottomComponents: PropTypes.node, + + /** + * The number of milliseconds that the player will pause before firing the + * first jump event on a right or left pulse. + * + * @type {Number} + * @default 400 + * @public + */ + initialJumpDelay: PropTypes.number, + + /** + * The number of milliseconds that the player will throttle before firing a + * jump event on a right or left pulse. + * + * @type {Number} + * @default 200 + * @public + */ + jumpDelay: PropTypes.number, + + /** + * Disables the media buttons. + * + * @type {Boolean} + * @public + */ + mediaDisabled: PropTypes.bool, + + /** + * Disables showing more components. + * + * @type {Boolean} + * @public + */ + moreActionDisabled: PropTypes.bool, + + /** + * Setting this to `true` will disable left and right keys for seeking. + * + * @type {Boolean} + * @public + */ + no5WayJump: PropTypes.bool, + + /** + * Called when media fast forwards. + * + * @type {Function} + * @public + */ + onFastForward: PropTypes.func, + + /** + * Called when media jumps. + * + * @type {Function} + * @public + */ + onJump: PropTypes.func, + + /** + * Called when media gets paused. + * + * @type {Function} + * @public + */ + onPause: PropTypes.func, + + /** + * Called when media starts playing. + * + * @type {Function} + * @public + */ + onPlay: PropTypes.func, + + /** + * Called when media rewinds. + * + * @type {Function} + * @public + */ + onRewind: PropTypes.func, + + /** + * Called when the visibility of more components is changed + * + * Event payload includes: + * + * * `type` - Type of event, `'onToggleMore'` + * * `showMoreComponents` - `true` when the components are visible` + * * `liftDistance` - The distance, in pixels, the component animates + * + * @type {Function} + * @public + */ + onToggleMore: PropTypes.func, + + /** + * The video pause state. + * + * @type {Boolean} + * @public + */ + paused: PropTypes.bool, + + /** + * Disables state on the media "play"/"pause" button + * + * @type {Boolean} + * @public + */ + playPauseButtonDisabled: PropTypes.bool, + + /** + * Disables the media playback-rate control via rewind and fast forward keys + * + * @type {Boolean} + * @public + */ + rateChangeDisabled: PropTypes.bool, + + /** + * Registers the MediaControls component with an + * [ApiDecorator]{@link core/internal/ApiDecorator.ApiDecorator}. + * + * @type {Function} + * @private + */ + setApiProvider: PropTypes.func, + + /** + * The visibility of the component. When `false`, the component will be hidden. + * + * @type {Boolean} + * @public + */ + visible: PropTypes.bool, + }; + + static defaultProps = { + initialJumpDelay: 400, + jumpDelay: 200, + }; + + constructor(props) { + super(props); + this.mediaControlsNode = null; + this.moreComponentsNode = null; + + this.actionGuideHeight = 0; + this.animation = null; + this.bottomComponentsHeight = 0; + this.keyLoop = null; + this.pulsingKeyCode = null; + this.pulsing = null; + this.paused = new Pause('MediaPlayer'); + + this.state = { + showMoreComponents: false, + moreComponentsRendered: false, + }; + + if (props.setApiProvider) { + props.setApiProvider(this); + } + } + + static getDerivedStateFromProps(props) { + if (!props.visible) { + return { + showMoreComponents: false, + }; + } + return null; + } + + componentDidMount() { + on('keydown', this.handleKeyDown); + on('keyup', this.handleKeyUp); + on('blur', this.handleBlur, window); + on('wheel', this.handleWheel); + } + + componentDidUpdate(prevProps, prevState) { + // Need to render `moreComponents` to show it. For performance, render `moreComponents` if it is actually shown. + if ( + !prevState.showMoreComponents && + this.state.showMoreComponents && + !this.state.moreComponentsRendered + ) { + this.moreComponentsRenderingJob.startRafAfter(); + } else if (prevState.showMoreComponents && !this.state.showMoreComponents) { + this.moreComponentsRenderingJob.stop(); + } + + if ( + (!prevState.moreComponentsRendered && this.state.moreComponentsRendered) || + (this.state.moreComponentsRendered && + prevProps.bottomComponents !== this.props.bottomComponents) || + !compareChildren(this.props.children, prevProps.children) + ) { + this.calculateMoreComponentsHeight(); + } + + if ( + (this.state.showMoreComponents && + !prevState.moreComponentsRendered && + this.state.moreComponentsRendered) || + (this.state.moreComponentsRendered && + prevState.showMoreComponents !== this.state.showMoreComponents) + ) { + forwardToggleMore( + { + type: 'onToggleMore', + showMoreComponents: this.state.showMoreComponents, + liftDistance: this.bottomComponentsHeight - this.actionGuideHeight, + }, + this.props + ); + + if (this.state.showMoreComponents) { + this.moreComponentsNode = + this.moreComponentsNode || + this.mediaControlsNode.querySelector(`.${css.moreComponents}`); + this.paused.pause(); + this.animation = this.moreComponentsNode.animate( + [ + { transform: 'none', opacity: 0, offset: 0 }, + { + transform: `translateY(${-this.actionGuideHeight}px)`, + opacity: 1, + offset: 1, + }, + ], + { + duration: animationDuration, + fill: 'forwards', + } + ); + this.animation.onfinish = this.handleFinish; + this.animation.oncancel = this.handleCancel; + } else if (this.animation != null) { + this.animation.cancel(); + } + } + + // if media controls disabled, reset key loop + if (!prevProps.mediaDisabled && this.props.mediaDisabled) { + this.stopListeningForPulses(); + this.paused.resume(); + } + } + + componentWillUnmount() { + off('keydown', this.handleKeyDown); + off('keyup', this.handleKeyUp); + off('blur', this.handleBlur, window); + off('wheel', this.handleWheel); + this.stopListeningForPulses(); + this.moreComponentsRenderingJob.stop(); + if (this.animation) { + this.animation.cancel(); + } + } + + moreComponentsRenderingJob = new Job(() => { + this.setState({ + moreComponentsRendered: true, + }); + }); + + calculateMoreComponentsHeight = () => { + if (!this.mediaControlsNode) { + this.bottomComponentsHeight = 0; + return; + } + + const bottomElement = this.mediaControlsNode.querySelector(`.${css.moreComponents}`); + this.bottomComponentsHeight = bottomElement ? bottomElement.scrollHeight : 0; + }; + + handleKeyDownFromMediaButtons = (ev) => { + if ( + is('down', ev.keyCode) && + !this.state.showMoreComponents && + !this.props.moreActionDisabled + ) { + this.showMoreComponents(); + ev.stopPropagation(); + } + }; + + handleKeyDown = (ev) => { + const { mediaDisabled, no5WayJump, visible } = this.props; + + const current = Spotlight.getCurrent(); + + if ( + !no5WayJump && + !visible && + !mediaDisabled && + !current && + (is('left', ev.keyCode) || is('right', ev.keyCode)) + ) { + this.paused.pause(); + this.startListeningForPulses(ev.keyCode); + } + }; + + handleKeyUp = (ev) => { + const { mediaDisabled, no5WayJump, rateChangeDisabled, playPauseButtonDisabled } = this.props; + + if (mediaDisabled) return; + + if (!playPauseButtonDisabled) { + if (is('play', ev.keyCode)) { + forward('onPlay', ev, this.props); + } else if (is('pause', ev.keyCode)) { + forward('onPause', ev, this.props); + } + } + + if (!no5WayJump && (is('left', ev.keyCode) || is('right', ev.keyCode))) { + this.stopListeningForPulses(); + this.paused.resume(); + } + + if (!rateChangeDisabled) { + if (is('rewind', ev.keyCode)) { + forward('onRewind', ev, this.props); + } else if (is('fastForward', ev.keyCode)) { + forward('onFastForward', ev, this.props); + } + } + }; + + handleBlur = () => { + this.stopListeningForPulses(); + this.paused.resume(); + }; + + handleWheel = (ev) => { + if ( + !this.state.showMoreComponents && + this.props.visible && + !this.props.moreActionDisabled && + ev.deltaY > 0 + ) { + this.showMoreComponents(); + } + }; + + startListeningForPulses = (keyCode) => { + // Ignore new pulse calls if key code is same, otherwise start new series if we're pulsing + if (this.pulsing && keyCode !== this.pulsingKeyCode) { + this.stopListeningForPulses(); + } + if (!this.pulsing) { + this.pulsingKeyCode = keyCode; + this.pulsing = true; + this.keyLoop = setTimeout(this.handlePulse, this.props.initialJumpDelay); + forward('onJump', { keyCode }, this.props); + } + }; + + handlePulse = () => { + forward('onJump', { keyCode: this.pulsingKeyCode }, this.props); + this.keyLoop = setTimeout(this.handlePulse, this.props.jumpDelay); + }; + + handlePlayButtonClick = (ev) => { + forward('onPlayButtonClick', ev, this.props); + if (this.props.paused) { + forward('onPlay', ev, this.props); + } else { + forward('onPause', ev, this.props); + } + }; + + stopListeningForPulses() { + this.pulsing = false; + if (this.keyLoop) { + clearTimeout(this.keyLoop); + this.keyLoop = null; + } + } + + getMediaControls = (node) => { + if (!node) { + this.actionGuideHeight = 0; + return; + } + this.mediaControlsNode = ReactDOM.findDOMNode(node); // eslint-disable-line react/no-find-dom-node + + const guideElement = this.mediaControlsNode.querySelector(`.${css.actionGuide}`); + this.actionGuideHeight = guideElement ? guideElement.scrollHeight : 0; + }; + + areMoreComponentsAvailable = () => { + return this.state.showMoreComponents; + }; + + showMoreComponents = () => { + this.setState({ showMoreComponents: true }); + }; + + hideMoreComponents = () => { + this.setState({ showMoreComponents: false }); + }; + + toggleMoreComponents() { + this.setState((prevState) => { + return { + showMoreComponents: !prevState.showMoreComponents, + }; + }); + } + + handleClose = (ev) => { + if (this.props.visible) { + forward('onClose', ev, this.props); + } + }; + + handleFinish = () => { + if (this.state.showMoreComponents) { + this.paused.resume(); + if (!Spotlight.getPointerMode()) { + Spotlight.move('down'); + } + } + }; + + handleCancel = () => { + this.paused.resume(); + }; + onActionGuideClick = () => { + this.showMoreComponents(); + }; + render() { + const props = Object.assign({}, this.props); + delete props.initialJumpDelay; + delete props.jumpDelay; + delete props.moreActionDisabled; + delete props.no5WayJump; + delete props.onFastForward; + delete props.onJump; + delete props.onPause; + delete props.onPlay; + delete props.onRewind; + delete props.onToggleMore; + delete props.rateChangeDisabled; + delete props.setApiProvider; + + return ( + + ); + } + } + + return Slottable({ slots: ['bottomComponents'] }, MediaControlsDecoratorHOC); +}); + +const handleCancel = (ev, { onClose }) => { + if (onClose) { + onClose(ev); + } +}; + +/** + * A set of components for controlling media playback and rendering additional components. + * + * This uses [Slottable]{@link ui/Slottable} to accept the custom tags, `` + * to add components to the bottom of the media controls. Any additional children will be + * rendered into the "more" controls area. Showing the additional components is handled by + * `MediaControls` when the user navigates down from the media buttons. + * + * @class MediaControls + * @memberof sandstone/MediaPlayer + * @mixes ui/Cancelable.Cancelable + * @ui + * @public + */ +const MediaControls = ApiDecorator( + { + api: ['areMoreComponentsAvailable', 'showMoreComponents', 'hideMoreComponents'], + }, + MediaControlsDecorator(Cancelable({ modal: true, onCancel: handleCancel }, MediaControlsBase)) +); + +MediaControls.defaultSlot = 'mediaControlsComponent'; + +export default MediaControls; +export { MediaControls, MediaControlsBase, MediaControlsDecorator }; diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less index 24e091a8..09630954 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less +++ b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less @@ -588,6 +588,8 @@ .overlay { .position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0); + pointer-events: auto; + z-index: 10; } @keyframes spin { 0% { @@ -714,6 +716,7 @@ .controlsHandleAbove { pointer-events: none; + z-index: -1; .position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0); } diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx index 7480d61e..19f1fe78 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx +++ b/com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx @@ -9,7 +9,15 @@ * - ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ ์šฐ์„  */ -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 DurationFmt from 'ilib/lib/DurationFmt'; import PropTypes from 'prop-types'; @@ -33,7 +41,7 @@ import { setMediaControlToggle, startMediaAutoClose, stopMediaAutoClose, - resetMediaAutoClose + resetMediaAutoClose, } from '../../actions/mediaOverlayActions'; import css from './MediaPlayer.module.less'; @@ -49,11 +57,12 @@ const RootContainer = SpotlightContainerDecorator( // DurationFmt memoization const memoGetDurFmt = memoize( - () => new DurationFmt({ - length: 'medium', - style: 'clock', - useNative: false, - }) + () => + new DurationFmt({ + length: 'medium', + style: 'clock', + useNative: false, + }) ); const getDurFmt = () => { @@ -68,7 +77,7 @@ const getDurFmt = () => { format: (time) => { if (!time || !time.millisecond) return '00:00'; return secondsToTime(Math.floor(time.millisecond / 1000)); - } + }, }; } }; @@ -159,7 +168,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => { try { // URL ํŒŒ์‹ฑ ์‹œ๋„ const url = new URL(src); - return ['youtube.com', 'youtu.be', 'm.youtube.com'].some(domain => + return ['youtube.com', 'youtu.be', 'm.youtube.com'].some((domain) => url.hostname.includes(domain) ); } catch { @@ -231,65 +240,75 @@ const MediaPlayerV2 = forwardRef((props, ref) => { } }, [ActualVideoComponent]); - const handleUpdate = useCallback((ev) => { - const el = videoRef.current; - if (!el) return; + const handleUpdate = useCallback( + (ev) => { + const el = videoRef.current; + if (!el) return; - const newCurrentTime = el.currentTime || 0; - const newDuration = el.duration || 0; + const newCurrentTime = el.currentTime || 0; + const newDuration = el.duration || 0; - // ์ƒํƒœ ์—…๋ฐ์ดํŠธ - setCurrentTime(newCurrentTime); - setDuration(newDuration); - setPaused(el.paused); - setLoading(el.loading || false); - setError(el.error || null); + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setCurrentTime(newCurrentTime); + setDuration(newDuration); + setPaused(el.paused); + setLoading(el.loading || false); + setError(el.error || null); - // ํ•จ์ˆ˜ํ˜• ์—…๋ฐ์ดํŠธ๋กœ stale closure ๋ฐฉ์ง€ - setSourceUnavailable((prevUnavailable) => - (el.loading && prevUnavailable) || el.error - ); + // ํ•จ์ˆ˜ํ˜• ์—…๋ฐ์ดํŠธ๋กœ stale closure ๋ฐฉ์ง€ + setSourceUnavailable((prevUnavailable) => (el.loading && prevUnavailable) || el.error); - // Proportion ๊ณ„์‚ฐ - updateProportionLoaded(); // ํ”Œ๋žซํผ๋ณ„ ๊ณ„์‚ฐ ํ•จ์ˆ˜ ํ˜ธ์ถœ - setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0); + // Proportion ๊ณ„์‚ฐ + updateProportionLoaded(); // ํ”Œ๋žซํผ๋ณ„ ๊ณ„์‚ฐ ํ•จ์ˆ˜ ํ˜ธ์ถœ + setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0); - // ์ฝœ๋ฐฑ ํ˜ธ์ถœ - if (ev.type === 'timeupdate' && onTimeUpdate) { - onTimeUpdate(ev); - } - if (ev.type === 'loadeddata' && onLoadedData) { - onLoadedData(ev); - } - if (ev.type === 'loadedmetadata' && onLoadedMetadata) { - onLoadedMetadata(ev); - } - if (ev.type === 'durationchange' && onDurationChange) { - onDurationChange(ev); - } - }, [onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange, updateProportionLoaded]); + // ์ฝœ๋ฐฑ ํ˜ธ์ถœ + if (ev.type === 'timeupdate' && onTimeUpdate) { + onTimeUpdate(ev); + } + if (ev.type === 'loadeddata' && onLoadedData) { + onLoadedData(ev); + } + if (ev.type === 'loadedmetadata' && onLoadedMetadata) { + onLoadedMetadata(ev); + } + if (ev.type === 'durationchange' && onDurationChange) { + onDurationChange(ev); + } + }, + [onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange, updateProportionLoaded] + ); - const handleEnded = useCallback((e) => { - if (onEnded) { - onEnded(e); - } - }, [onEnded]); + const handleEnded = useCallback( + (e) => { + if (onEnded) { + onEnded(e); + } + }, + [onEnded] + ); - const handleErrorEvent = useCallback((e) => { - setError(e); - if (onError) { - onError(e); - } - }, [onError]); + const handleErrorEvent = useCallback( + (e) => { + setError(e); + if (onError) { + onError(e); + } + }, + [onError] + ); // ========== Controls Management ========== - const showControls = useCallback((timeout = 3000) => { - if (disabled || isModal) return; + const showControls = useCallback( + (timeout = 3000) => { + if (disabled || isModal) return; - console.log('๐ŸŽฌ [MediaPlayer.v2] showControls called, dispatching setMediaControlShow'); - dispatch(setMediaControlShow()); - dispatch(startMediaAutoClose(timeout)); - }, [disabled, isModal, dispatch]); + console.log('๐ŸŽฌ [MediaPlayer.v2] showControls called, dispatching setMediaControlShow'); + dispatch(setMediaControlShow()); + dispatch(startMediaAutoClose(timeout)); + }, + [disabled, isModal, dispatch] + ); const hideControls = useCallback(() => { console.log('๐ŸŽฌ [MediaPlayer.v2] hideControls called, dispatching setMediaControlHide'); @@ -351,10 +370,13 @@ const MediaPlayerV2 = forwardRef((props, ref) => { }, [currentTime, duration, paused, loading, error, proportionLoaded]); // ========== Slider Event Handlers ========== - const handleSliderChange = useCallback(({ value }) => { - const time = value * duration; - seek(time); - }, [duration, seek]); + const handleSliderChange = useCallback( + ({ value }) => { + const time = value * duration; + seek(time); + }, + [duration, seek] + ); const handleKnobMove = useCallback((ev) => { if (!videoRef.current) return; @@ -459,17 +481,21 @@ const MediaPlayerV2 = forwardRef((props, ref) => { }, [dispatch]); // ========== Imperative Handle (API) ========== - useImperativeHandle(ref, () => ({ - play, - pause, - seek, - getMediaState, - showControls, - hideControls, - toggleControls, - areControlsVisible: () => controlsVisible, - getVideoNode: () => videoRef.current, - }), [play, pause, seek, getMediaState, showControls, hideControls, toggleControls, controlsVisible]); + useImperativeHandle( + ref, + () => ({ + play, + pause, + seek, + getMediaState, + showControls, + hideControls, + toggleControls, + areControlsVisible: () => controlsVisible, + getVideoNode: () => videoRef.current, + }), + [play, pause, seek, getMediaState, showControls, hideControls, toggleControls, controlsVisible] + ); // ========== setApiProvider ํ˜ธ์ถœ ========== useEffect(() => { @@ -486,7 +512,16 @@ const MediaPlayerV2 = forwardRef((props, ref) => { getVideoNode: () => videoRef.current, }); } - }, [setApiProvider, play, pause, seek, getMediaState, showControls, hideControls, toggleControls]); + }, [ + setApiProvider, + play, + pause, + seek, + getMediaState, + showControls, + hideControls, + toggleControls, + ]); // ========== Video Props ========== const videoProps = useMemo(() => { @@ -535,7 +570,19 @@ const MediaPlayerV2 = forwardRef((props, ref) => { ...baseProps, ref: assignVideoNode, }; - }, [ActualVideoComponent, assignVideoNode, src, paused, loop, muted, handleLoadStart, handleUpdate, handleEnded, handleErrorEvent, reactPlayerConfig]); + }, [ + ActualVideoComponent, + assignVideoNode, + src, + paused, + loop, + muted, + handleLoadStart, + handleUpdate, + handleEnded, + handleErrorEvent, + reactPlayerConfig, + ]); // ========== Spotlight Handler ========== const handleSpotlightFocus = useCallback(() => { @@ -564,18 +611,13 @@ const MediaPlayerV2 = forwardRef((props, ref) => { > {/* Video Element */} {ActualVideoComponent === Media ? ( - - {children} - + {children} ) : ( )} {/* Overlay */} - + {/* Loading + Thumbnail */} {loading && thumbnailUrl && ( <> @@ -594,12 +636,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => { {/* Slider Section */}
{/* Times - Total */} - + {/* Times - Current */} { onSpotlightRight={handleSpotlightFocus} onSpotlightLeft={handleSpotlightFocus} onClick={handleSpotlightFocus} - spotlightDisabled={controlsVisible || shouldDisableSpotlight} + spotlightDisabled={controlsVisible || shouldDisableSpotlight || !isModal} /> ); diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js index a2d32cdf..6951f74d 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js @@ -724,6 +724,7 @@ const VideoPlayerBase = class extends React.Component { belowContentsVisible: PropTypes.bool, tabContainerVersion: PropTypes.number, tabIndexV2: PropTypes.number, + overlayContentsComponent: PropTypes.elementType, dispatch: PropTypes.func, }; @@ -1121,6 +1122,8 @@ const VideoPlayerBase = class extends React.Component { return; } + Spotlight.setPointerMode(false); + // this.startDelayedFeedbackHide(); // this.startDelayedTitleHide(); @@ -1426,6 +1429,15 @@ const VideoPlayerBase = class extends React.Component { this.showControlsFromPointer ); + onVideoClickCapture = (ev) => { + if (!this.state.mediaControlsVisible && !this.props.panelInfo?.modal) { + this.activityDetected(); + this.onVideoClick(); + ev.stopPropagation(); + ev.preventDefault(); + } + }; + handleControlsHandleAboveHoldPulse = () => { if (shouldJump(this.props, this.state)) { this.handleJump({ @@ -1997,6 +2009,11 @@ const VideoPlayerBase = class extends React.Component { // Player Interaction events // onVideoClick = () => { + console.log('[VideoPlayer] onVideoClick', { + controlsVisible: this.state.mediaControlsVisible, + panelInfoModal: this.props.panelInfo?.modal, + pointerMode: Spotlight.getPointerMode(), + }); // tabContainerVersion === 2์ผ ๋•Œ belowContentsVisible๋„ ํ•จ๊ป˜ ํ† ๊ธ€ if (this.props.tabContainerVersion === 2 && this.props.setBelowContentsVisible) { const willShowControls = !this.state.mediaControlsVisible; @@ -2012,7 +2029,18 @@ const VideoPlayerBase = class extends React.Component { return; } + const willShowControls = !this.state.mediaControlsVisible; this.toggleControls(); + if (willShowControls && !this.props.panelInfo?.modal) { + this.restoreOverlayFocus(); + } + }; + + restoreOverlayFocus = () => { + setTimeout(() => { + Spotlight.setPointerMode(false); + Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); + }); }; onSliderChange = ({ value }) => { @@ -2216,6 +2244,7 @@ const VideoPlayerBase = class extends React.Component { setSideContentsVisible, belowContentsVisible, tabContainerVersion, + overlayContentsComponent, disclaimer, liveTotalTime, currentLiveTimeSeconds, @@ -2264,6 +2293,7 @@ const VideoPlayerBase = class extends React.Component { delete mediaProps.belowContentsVisible; delete mediaProps.tabContainerVersion; delete mediaProps.tabIndexV2; + delete mediaProps.overlayContentsComponent; mediaProps.autoPlay = !noAutoPlay; mediaProps.className = type !== 'MEDIA' ? css.video : css.media; @@ -2272,6 +2302,9 @@ const VideoPlayerBase = class extends React.Component { mediaProps.onLoadStart = this.handleLoadStart; mediaProps.onUpdate = this.handleEvent; mediaProps.ref = this.setVideoRef; + if (!panelInfo.modal) { + mediaProps.tabIndex = -1; + } //yhcho ReactPlayer if ((typeof window === 'object' && !window.PalmSystem) || isYoutube) { @@ -2287,6 +2320,7 @@ const VideoPlayerBase = class extends React.Component { } const controlsAriaProps = this.getControlsAriaProps(); + const OverlayContents = overlayContentsComponent || PlayerOverlayContents; let proportionSelection = selection; if (proportionSelection != null && this.state.duration) { @@ -2356,6 +2390,7 @@ const VideoPlayerBase = class extends React.Component { modalClassName )} onClick={this.activityDetected} + onClickCapture={this.onVideoClickCapture} ref={this.setPlayerRef} spotlightDisabled={spotlightDisabled} spotlightId={spotlightId} @@ -2439,7 +2474,7 @@ const VideoPlayerBase = class extends React.Component { css.controlFeedbackBtnLayer + (this.state.infoVisible ? ' ' + css.lift : '') } > - diff --git a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less index 9e2af48c..57000fb7 100644 --- a/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less +++ b/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less @@ -594,9 +594,11 @@ } } - .overlay { - .position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0); - } + .overlay { + .position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0); + pointer-events: auto; + z-index: 10; + } @keyframes spin { 0% { transform: rotate(0.25turn); @@ -720,10 +722,11 @@ } } - .controlsHandleAbove { - pointer-events: none; - .position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0); - } + .controlsHandleAbove { + pointer-events: none; + z-index: -1; + .position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0); + } // Skin colors .applySkins({ diff --git a/com.twin.app.shoptime/src/utils/SpotlightIds.js b/com.twin.app.shoptime/src/utils/SpotlightIds.js index 0d45a646..12262c60 100644 --- a/com.twin.app.shoptime/src/utils/SpotlightIds.js +++ b/com.twin.app.shoptime/src/utils/SpotlightIds.js @@ -42,6 +42,7 @@ export const SpotlightIds = { PLAYER_TAB_BUTTON: 'playerTabArrow', PLAYER_BACK_BUTTON: 'player-back-button', PLAYER_SUBTITLE_BUTTON: 'player-subtitlebutton', + PLAYER_PLAY_BUTTON: 'player-play-button', // searchPanel SEARCH_THEME: 'search_theme', diff --git a/com.twin.app.shoptime/src/views/HomePanel/BestSeller/BestSeller.jsx b/com.twin.app.shoptime/src/views/HomePanel/BestSeller/BestSeller.jsx index ee16c131..daa3b37d 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/BestSeller/BestSeller.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/BestSeller/BestSeller.jsx @@ -1,28 +1,13 @@ -import React, { - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - useDispatch, - useSelector, -} from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import Spotlight from '@enact/spotlight'; -import { - SpotlightContainerDecorator, -} from '@enact/spotlight/SpotlightContainerDecorator'; +import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; import Spottable from '@enact/spotlight/Spottable'; -import { - pushPanel, - updatePanel, -} from '../../../actions/panelActions'; -import { - navigateToDetailFromHome, -} from '../../../actions/panelNavigationActions'; +import { pushPanel, updatePanel, navigateFromBestSeller } from '../../../actions/panelActions'; +import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions'; import SectionTitle from '../../../components/SectionTitle/SectionTitle'; import Tag from '../../../components/TItemCard/Tag'; import TItemCard from '../../../components/TItemCard/TItemCard'; @@ -30,23 +15,13 @@ import TItemCardNew from '../../../components/TItemCard/TItemCard.new'; import TScroller from '../../../components/TScroller/TScroller'; import useScrollReset from '../../../hooks/useScrollReset'; import useScrollTo from '../../../hooks/useScrollTo'; -import { - LOG_CONTEXT_NAME, - LOG_MESSAGE_ID, - panel_names, -} from '../../../utils/Config'; -import { - $L, - scaleW, -} from '../../../utils/helperMethods'; +import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config'; +import { $L, scaleW } from '../../../utils/helperMethods'; import { SpotlightIds } from '../../../utils/SpotlightIds'; import css from './BestSeller.module.less'; -const SpottableComponent = Spottable("div"); -const Container = SpotlightContainerDecorator( - { enterTo: "last-focused" }, - "div" -); +const SpottableComponent = Spottable('div'); +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const BestSeller = ({ order, @@ -58,25 +33,15 @@ const BestSeller = ({ shelfTitle, }) => { const { getScrollTo, scrollLeft } = useScrollTo(); - const { handleScrollReset, handleStopScrolling } = useScrollReset( - scrollLeft, - true - ); + const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true); const dispatch = useDispatch(); const { cursorVisible } = useSelector((state) => state.common.appStatus); - const bestSellerDatas = useSelector( - (state) => state.product.bestSellerData?.bestSeller - ); - - const bestSellerNewDatas = useSelector( - (state) => - state.foryou?.recommendInfo?.recommendProduct - ); - + const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller); + const bestSellerNewDatas = useSelector((state) => state.foryou?.recommendInfo?.recommendProduct); const [drawChk, setDrawChk] = useState(false); const [firstChk, setFirstChk] = useState(0); @@ -84,59 +49,57 @@ const BestSeller = ({ const [bestInfos, setBestInfos] = useState(null); const [bestItemNewData, setBestItemNewData] = useState([]); - useEffect(()=>{ + useEffect(() => { setBestInfos( - bestSellerNewDatas?.filter( - (item) => item.recommendTpCd === "BESTSELLER" - ) || [] // ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋นˆ ๋ฐฐ์—ด ์„ค์ • - ) - },[bestSellerNewDatas]) + bestSellerNewDatas?.filter((item) => item.recommendTpCd === 'BESTSELLER') || [] // ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋นˆ ๋ฐฐ์—ด ์„ค์ • + ); + }, [bestSellerNewDatas]); useEffect(() => { if (!bestInfos || bestInfos.length === 0) { - const baseData = bestSellerDatas?.map((item) => ({ - ...item, - foryou: false, - })) || []; + const baseData = + bestSellerDatas?.map((item) => ({ + ...item, + foryou: false, + })) || []; setBestItemNewData(baseData); return; } - - const recommendedData = bestInfos[0].productInfos?.map((item) => ({ - ...item, - foryou: true, - })) || []; - - const recommendedPrdtIds = new Set(recommendedData.map(item => item.prdtId)); - - const baseData = bestSellerDatas?.map((item) => ({ - ...item, - foryou: recommendedPrdtIds.has(item.prdtId), - })) || []; - + + const recommendedData = + bestInfos[0].productInfos?.map((item) => ({ + ...item, + foryou: true, + })) || []; + + const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId)); + + const baseData = + bestSellerDatas?.map((item) => ({ + ...item, + foryou: recommendedPrdtIds.has(item.prdtId), + })) || []; + setBestItemNewData(baseData); }, [bestSellerDatas, bestInfos]); const orderStyle = useMemo(() => ({ order: order }), [order]); - useEffect(() => { + useEffect(() => { setDrawChk(true); }, [bestSellerNewDatas]); const handleCardClick = useCallback( (patnrId, prdtId) => () => { dispatch( - pushPanel({ - name: panel_names.DETAIL_PANEL, - panelInfo: { - patnrId: patnrId, - prdtId: prdtId, - nowShelf: spotlightId, - }, + navigateFromBestSeller({ + patnrId, + prdtId, + spotlightId, }) ); }, - [dispatch] + [dispatch, spotlightId] ); const handleMoreCardClick = useCallback(() => { @@ -144,7 +107,7 @@ const BestSeller = ({ pushPanel({ name: panel_names.TRENDING_NOW_PANEL, panelInfo: { - pageName: "BS", + pageName: 'BS', focusedContainerId: SpotlightIds.TRENDING_NOW_BEST_SELLER, }, }) @@ -171,21 +134,18 @@ const BestSeller = ({ if (firstChk === 0 && itemIndex === 0) { const c = Spotlight.getCurrent(); if (c) { - let cAriaLabel = c.getAttribute("aria-label"); - cAriaLabel = "Best Seller, Heading 1," + cAriaLabel; - c.setAttribute("aria-label", cAriaLabel); + let cAriaLabel = c.getAttribute('aria-label'); + cAriaLabel = 'Best Seller, Heading 1,' + cAriaLabel; + c.setAttribute('aria-label', cAriaLabel); } setFirstChk(1); } else if (firstChk === 1 && itemIndex === 0) { const c = Spotlight.getCurrent(); if (c) { - let cAriaLabel = c.getAttribute("aria-label"); + let cAriaLabel = c.getAttribute('aria-label'); if (cAriaLabel) { - const newcAriaLabel = cAriaLabel.replace( - "Best Seller, Heading 1,", - "" - ); - c.setAttribute("aria-label", newcAriaLabel); + const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', ''); + c.setAttribute('aria-label', newcAriaLabel); } } } else { @@ -219,7 +179,6 @@ const BestSeller = ({ handleShelfFocus(); } }, [handleShelfFocus]); - return ( @@ -262,15 +221,15 @@ const BestSeller = ({ ) => { const rankText = rankOrd === 1 - ? rankOrd + "st" + ? rankOrd + 'st' : rankOrd === 2 - ? rankOrd + "nd" + ? rankOrd + 'nd' : rankOrd === 3 - ? rankOrd + "rd" - : rankOrd + "th"; + ? rankOrd + 'rd' + : rankOrd + 'th'; return ( - {foryou === true && } + {foryou === true && } ); } @@ -308,7 +267,7 @@ const BestSeller = ({
@@ -318,4 +277,4 @@ const BestSeller = ({ ); }; -export default BestSeller; \ No newline at end of file +export default BestSeller; diff --git a/com.twin.app.shoptime/src/views/HomePanel/EventPopUpBanner/EventPopUpBanner.jsx b/com.twin.app.shoptime/src/views/HomePanel/EventPopUpBanner/EventPopUpBanner.jsx index ff2833d3..f310ecab 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/EventPopUpBanner/EventPopUpBanner.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/EventPopUpBanner/EventPopUpBanner.jsx @@ -1,57 +1,38 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch, useSelector } from 'react-redux'; -import Spotlight from "@enact/spotlight"; -import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator"; +import Spotlight from '@enact/spotlight'; +import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; -import { clearSMS } from "../../../actions/appDataActions"; -import { - alertToast, - setHidePopup, - setShowPopup, -} from "../../../actions/commonActions"; +import { clearSMS } from '../../../actions/appDataActions'; +import { alertToast, setHidePopup, setShowPopup } from '../../../actions/commonActions'; import { clearGetProductCouponDownload, getProductCouponDownload, -} from "../../../actions/couponActions"; -import { setEventPopClickInfo } from "../../../actions/eventActions"; -import { sendLogGNB, sendLogShopByMobile } from "../../../actions/logActions"; -import { pushPanel } from "../../../actions/panelActions"; -import { startVideoPlayer } from "../../../actions/playActions"; -import MobileSendPopUp from "../../../components/MobileSend/MobileSendPopUp"; -import TPopUp from "../../../components/TPopUp/TPopUp"; -import { launchMembershipApp } from "../../../lunaSend"; -import { - ACTIVE_POPUP, - LOG_MENU, - LOG_TP_NO, - MYINFO_TABS, - panel_names, -} from "../../../utils/Config"; -import { $L, formatLocalDateTime } from "../../../utils/helperMethods"; -import css from "../EventPopUpBanner/EventPopUpBanner.module.less"; +} from '../../../actions/couponActions'; +import { setEventPopClickInfo } from '../../../actions/eventActions'; +import { sendLogGNB, sendLogShopByMobile } from '../../../actions/logActions'; +import { pushPanel, navigateFromEventPopup } from '../../../actions/panelActions'; +import { startVideoPlayer } from '../../../actions/playActions'; +import MobileSendPopUp from '../../../components/MobileSend/MobileSendPopUp'; +import TPopUp from '../../../components/TPopUp/TPopUp'; +import { launchMembershipApp } from '../../../lunaSend'; +import { ACTIVE_POPUP, LOG_MENU, LOG_TP_NO, MYINFO_TABS, panel_names } from '../../../utils/Config'; +import { $L, formatLocalDateTime } from '../../../utils/helperMethods'; +import css from '../EventPopUpBanner/EventPopUpBanner.module.less'; -const Container = SpotlightContainerDecorator( - { enterTo: "last-focused" }, - "div" -); +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); export default function EventPopUpBanner() { const dispatch = useDispatch(); - const eventPopInfosData = useSelector( - (state) => state.event.eventData.eventPopInfo - ); + const eventPopInfosData = useSelector((state) => state.event.eventData.eventPopInfo); const popupVisible = useSelector((state) => state.common.popup.popupVisible); const activePopup = useSelector((state) => state.common.popup.activePopup); - const userNumber = useSelector( - (state) => state.common?.appStatus.loginUserData.userNumber - ); + const userNumber = useSelector((state) => state.common?.appStatus.loginUserData.userNumber); const smsSuccess = useSelector((state) => state.appData.sendSms.retCode); - const couponDownloadSuccess = useSelector( - (state) => state.coupon?.couponDownloadSuccess - ); + const couponDownloadSuccess = useSelector((state) => state.coupon?.couponDownloadSuccess); const timerRef = useRef(); const shopByMobileLogRef = useRef(null); @@ -61,7 +42,7 @@ export default function EventPopUpBanner() { const evntTpCd = eventPopInfosData?.evntTpCd; const evntNm = eventPopInfosData?.evntNm; - const [smsTpCd, setSmsTpCd] = useState(""); + const [smsTpCd, setSmsTpCd] = useState(''); const [eventPopData, setEventPopData] = useState({}); useEffect(() => { @@ -77,12 +58,12 @@ export default function EventPopUpBanner() { const handleApply = useCallback(() => { if (eventPopData) { - dispatch(setEventPopClickInfo({ evntApplcnFlag: "Y", evntId })); + dispatch(setEventPopClickInfo({ evntApplcnFlag: 'Y', evntId })); switch (evntTpCd) { - case "EVT00102": // curation - case "EVT00104": // curation + prize - case "EVT00107": { + case 'EVT00102': // curation + case 'EVT00104': // curation + prize + case 'EVT00107': { // curation + billing coupon dispatch(setHidePopup()); dispatch( @@ -96,43 +77,43 @@ export default function EventPopUpBanner() { ); break; } - case "EVT00105": { + case 'EVT00105': { // coupon Only - setSmsTpCd("APP00212"); + setSmsTpCd('APP00212'); dispatch(setShowPopup(ACTIVE_POPUP.smsPopup)); const params = { - befPrice: "", - curationId: eventPopData?.curationId ?? "", - curationNm: eventPopData?.curationNm ?? "", - evntId: eventPopData?.evntId ?? "", - evntNm: eventPopData?.evntNm ?? "", - lastPrice: "", - lgCatCd: "", - lgCatNm: "", - liveFlag: "N", + befPrice: '', + curationId: eventPopData?.curationId ?? '', + curationNm: eventPopData?.curationNm ?? '', + evntId: eventPopData?.evntId ?? '', + evntNm: eventPopData?.evntNm ?? '', + lastPrice: '', + lgCatCd: '', + lgCatNm: '', + liveFlag: 'N', locDt: formatLocalDateTime(new Date()), logTpNo: LOG_TP_NO.SHOP_BY_MOBILE.SHOP_BY_MOBILE, - mbphNoFlag: "N", + mbphNoFlag: 'N', patncNm: eventPopData?.patncNm, patnrId: eventPopData?.patnrId, - prdtId: "", - prdtNm: "", - revwGrd: "", - rewdAplyFlag: "N", - shopByMobileFlag: "Y", - shopTpNm: "popevent", - showId: "", - showNm: "", - trmsAgrFlag: "N", - tsvFlag: "N", + prdtId: '', + prdtNm: '', + revwGrd: '', + rewdAplyFlag: 'N', + shopByMobileFlag: 'Y', + shopTpNm: 'popevent', + showId: '', + showNm: '', + trmsAgrFlag: 'N', + tsvFlag: 'N', }; dispatch(sendLogShopByMobile(params)); shopByMobileLogRef.current = params; break; } - case "EVT00108": { + case 'EVT00108': { // Direct+Billng+Coupon if (!userNumber) { dispatch(setShowPopup(ACTIVE_POPUP.loginPopup)); @@ -152,7 +133,7 @@ export default function EventPopUpBanner() { useEffect(() => { if (couponDownloadSuccess === 0) { - dispatch(alertToast($L("Downloading coupon complete."))); + dispatch(alertToast($L('Downloading coupon complete.'))); if (eventPopData && eventPopData?.shptmLnkInfo?.shptmLnkTpCd) { dispatch(setHidePopup()); navigateToLinkByTypeCode(); @@ -174,7 +155,7 @@ export default function EventPopUpBanner() { const handleSkip = useCallback(() => { if (eventPopInfosData) { - dispatch(setEventPopClickInfo({ evntApplcnFlag: "N", evntId })); + dispatch(setEventPopClickInfo({ evntApplcnFlag: 'N', evntId })); dispatch(setHidePopup()); } @@ -185,7 +166,7 @@ export default function EventPopUpBanner() { useEffect(() => { timerRef.current = setTimeout(() => { - Spotlight.focus("tPopupBtn1"); + Spotlight.focus('tPopupBtn1'); }, 100); return () => clearTimeout(timerRef.current); }, []); @@ -200,23 +181,23 @@ export default function EventPopUpBanner() { if (eventPopData && eventPopData.shptmLnkInfo) { switch (eventPopData.shptmLnkInfo.shptmLnkTpCd) { - case "EVT00201": + case 'EVT00201': break; - case "EVT00202": + case 'EVT00202': dispatch( pushPanel({ name: panel_names.FEATURED_BRANDS_PANEL, panelInfo: { - from: "gnb", + from: 'gnb', patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId, }, }) ); break; - case "EVT00203": + case 'EVT00203': dispatch(pushPanel({ name: panel_names.TRENDING_NOW_PANEL })); break; - case "EVT00204": + case 'EVT00204': pushPanel({ name: panel_names.HOT_PICKS_PANEL, panelInfo: { @@ -225,7 +206,7 @@ export default function EventPopUpBanner() { }, }); break; - case "EVT00205": + case 'EVT00205': dispatch( pushPanel({ name: panel_names.ON_SALE_PANEL, @@ -233,7 +214,7 @@ export default function EventPopUpBanner() { }) ); break; - case "EVT00206": + case 'EVT00206': dispatch( pushPanel({ name: panel_names.CATEGORY_PANEL, @@ -243,28 +224,25 @@ export default function EventPopUpBanner() { }) ); break; - case "EVT00207": + case 'EVT00207': dispatch( - pushPanel({ - name: panel_names.DETAIL_PANEL, - panelInfo: { - patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId, - prdtId: eventPopData.shptmLnkInfo?.lnkPrdtId, - }, + navigateFromEventPopup({ + patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId, + prdtId: eventPopData.shptmLnkInfo?.lnkPrdtId, }) ); break; - case "EVT00208": + case 'EVT00208': dispatch( startVideoPlayer({ modal: false, patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId, showId: eventPopData.shptmLnkInfo?.lnkBrdcId, - shptmBanrTpNm: "VOD", + shptmBanrTpNm: 'VOD', }) ); break; - case "EVT00209": + case 'EVT00209': dispatch( pushPanel({ name: panel_names.THEME_CURATION_PANEL, @@ -274,12 +252,12 @@ export default function EventPopUpBanner() { }) ); break; - case "EVT00210": + case 'EVT00210': dispatch( pushPanel({ name: panel_names.MY_PAGE_PANEL, panelInfo: { - menuNm: "My Info", + menuNm: 'My Info', menuOrd: 3, tabForced: MYINFO_TABS.COUPON, }, @@ -307,8 +285,8 @@ export default function EventPopUpBanner() { onClose={handleSkip} onClick={handleApply} hasButton - button1Text={$L("Apply Now")} - button2Text={$L("Skip")} + button1Text={$L('Apply Now')} + button2Text={$L('Skip')} className={css.eventPopup} > {eventPopInfosData && ( @@ -322,10 +300,8 @@ export default function EventPopUpBanner() { )} {activePopup === ACTIVE_POPUP.loginPopup && ( @@ -345,10 +321,10 @@ export default function EventPopUpBanner() { kind="textPopup" hasText open={popupVisible} - text={$L("Would you like to sign in?")} + text={$L('Would you like to sign in?')} hasButton - button1Text={$L("OK")} - button2Text={$L("CANCEL")} + button1Text={$L('OK')} + button2Text={$L('CANCEL')} onClick={handleLogin} onClose={onClose} > diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx index 2d103bc5..c562fc03 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx @@ -17,7 +17,7 @@ import liveShow from '../../../../assets/images/tag-liveshow.png'; import { changeAppStatus } from '../../../actions/commonActions'; import { updateHomeInfo, setVideoTransitionLock } from '../../../actions/homeActions'; import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions'; -import { pushPanel } from '../../../actions/panelActions'; +import { pushPanel, navigateFromRandomUnit } from '../../../actions/panelActions'; import { finishVideoPreview, startVideoPlayer, @@ -462,17 +462,11 @@ export default function RandomUnit({ // ํˆฌ๋ฐ์ด์ฆˆ๋”œ ํด๋ฆญ const todayDealClick = useCallback(() => { - if (playerPanelInfo?.modal !== false) { - finishAndUnlock(); - } - dispatch( - pushPanel({ - name: panel_names.DETAIL_PANEL, - panelInfo: { - patnrId: randomData.patnrId, - prdtId: randomData.prdtId, - }, + navigateFromRandomUnit({ + patnrId: randomData.patnrId, + prdtId: randomData.prdtId, + type: 'product', }) ); diff --git a/com.twin.app.shoptime/src/views/HomePanel/PickedForYou/PickedForYou.jsx b/com.twin.app.shoptime/src/views/HomePanel/PickedForYou/PickedForYou.jsx index b6f3ccf4..0af0bf9e 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/PickedForYou/PickedForYou.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/PickedForYou/PickedForYou.jsx @@ -1,41 +1,24 @@ -import React, { - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - useDispatch, - useSelector, -} from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import Spotlight from '@enact/spotlight'; -import { - SpotlightContainerDecorator, -} from '@enact/spotlight/SpotlightContainerDecorator'; +import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; import Spottable from '@enact/spotlight/Spottable'; -import { pushPanel } from '../../../actions/panelActions'; +import { pushPanel, navigateFromPickedForYou } from '../../../actions/panelActions'; import SectionTitle from '../../../components/SectionTitle/SectionTitle'; import TItemCardNew from '../../../components/TItemCard/TItemCard.new'; import TScroller from '../../../components/TScroller/TScroller'; import useScrollReset from '../../../hooks/useScrollReset'; import useScrollTo from '../../../hooks/useScrollTo'; -import { - LOG_CONTEXT_NAME, - LOG_MESSAGE_ID, - panel_names, -} from '../../../utils/Config'; +import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config'; import { $L } from '../../../utils/helperMethods'; import { SpotlightIds } from '../../../utils/SpotlightIds'; import css from './PickedForYou.module.less'; -const SpottableComponent = Spottable("div"); -const Container = SpotlightContainerDecorator( - { enterTo: "last-focused" }, - "div" -); +const SpottableComponent = Spottable('div'); +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const PickedForYou = ({ order, @@ -46,36 +29,25 @@ const PickedForYou = ({ shelfLocation, shelfTitle, }) => { - const { getScrollTo, scrollLeft } = useScrollTo(); - const { handleScrollReset, handleStopScrolling } = useScrollReset( - scrollLeft, - true - ); + const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true); const dispatch = useDispatch(); const { cursorVisible } = useSelector((state) => state.common.appStatus); - const justForYouDatasNew = useSelector( - (state) => state.foryou?.recommendInfo?.recommendProduct - ) - + const justForYouDatasNew = useSelector((state) => state.foryou?.recommendInfo?.recommendProduct); + const [drawChk, setDrawChk] = useState(false); const [firstChk, setFirstChk] = useState(0); const [pickedForYou, setPickedForYou] = useState(null); - - const orderStyle = useMemo(() => ({ order: order }), [order]); - useEffect(()=>{ - setPickedForYou( - justForYouDatasNew?.filter( - (item) => item.recommendTpCd === "PICKEDFORYOU" - ) - ) - },[justForYouDatasNew]) - + const orderStyle = useMemo(() => ({ order: order }), [order]); + useEffect(() => { + setPickedForYou(justForYouDatasNew?.filter((item) => item.recommendTpCd === 'PICKEDFORYOU')); + }, [justForYouDatasNew]); + useEffect(() => { setDrawChk(true); }, [justForYouDatasNew]); @@ -83,17 +55,14 @@ const PickedForYou = ({ const handleCardClick = useCallback( (patnrId, prdtId) => () => { dispatch( - pushPanel({ - name: panel_names.DETAIL_PANEL, - panelInfo: { - patnrId: patnrId, - prdtId: prdtId, - nowShelf: spotlightId, - }, + navigateFromPickedForYou({ + patnrId, + prdtId, + spotlightId, }) ); }, - [dispatch] + [dispatch, spotlightId] ); const handleMoreCardClick = useCallback(() => { @@ -101,7 +70,7 @@ const PickedForYou = ({ pushPanel({ name: panel_names.TRENDING_NOW_PANEL, panelInfo: { - pageName: "BS", + pageName: 'BS', focusedContainerId: SpotlightIds.TRENDING_NOW_BEST_SELLER, }, }) @@ -128,21 +97,18 @@ const PickedForYou = ({ if (firstChk === 0 && itemIndex === 0) { const c = Spotlight.getCurrent(); if (c) { - let cAriaLabel = c.getAttribute("aria-label"); - cAriaLabel = "Best Seller, Heading 1," + cAriaLabel; - c.setAttribute("aria-label", cAriaLabel); + let cAriaLabel = c.getAttribute('aria-label'); + cAriaLabel = 'Best Seller, Heading 1,' + cAriaLabel; + c.setAttribute('aria-label', cAriaLabel); } setFirstChk(1); } else if (firstChk === 1 && itemIndex === 0) { const c = Spotlight.getCurrent(); if (c) { - let cAriaLabel = c.getAttribute("aria-label"); + let cAriaLabel = c.getAttribute('aria-label'); if (cAriaLabel) { - const newcAriaLabel = cAriaLabel.replace( - "Best Seller, Heading 1,", - "" - ); - c.setAttribute("aria-label", newcAriaLabel); + const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', ''); + c.setAttribute('aria-label', newcAriaLabel); } } } else { @@ -186,7 +152,7 @@ const PickedForYou = ({ onFocus={_handleShelfFocus} > @@ -198,26 +164,26 @@ const PickedForYou = ({ noScrollByWheel > {pickedForYou && - pickedForYou?.[0] && + pickedForYou?.[0] && pickedForYou?.[0].productInfos.map( ( { prdtId, imgUrl, priceInfo, - prdtNm, + prdtNm, patnrId, offerInfo, brndNm, patncNm, catNm, - euEnrgLblInfos + euEnrgLblInfos, }, itemIndex ) => { return ( @@ -250,7 +216,7 @@ const PickedForYou = ({ diff --git a/com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx b/com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx index 20f95377..8aa77e27 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx @@ -1,46 +1,28 @@ -import React, { - memo, - useCallback, - useEffect, - useState, -} from 'react'; +import React, { memo, useCallback, useEffect, useState } from 'react'; -import { - useDispatch, - useSelector, -} from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; -import { - SpotlightContainerDecorator, -} from '@enact/spotlight/SpotlightContainerDecorator'; +import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; import Spottable from '@enact/spotlight/Spottable'; import { setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import { sendLogCuration } from '../../../actions/logActions'; import { getSubCategory } from '../../../actions/mainActions'; -import { pushPanel } from '../../../actions/panelActions'; +import { pushPanel, navigateFromSubCategory } from '../../../actions/panelActions'; import Tag from '../../../components/TItemCard/Tag'; import TItemCardNew from '../../../components/TItemCard/TItemCard.new'; import TScroller from '../../../components/TScroller/TScroller'; import usePrevious from '../../../hooks/usePrevious'; import useScrollReset from '../../../hooks/useScrollReset'; import useScrollTo from '../../../hooks/useScrollTo'; -import { - LOG_CONTEXT_NAME, - LOG_MESSAGE_ID, - LOG_TP_NO, - panel_names, -} from '../../../utils/Config'; +import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, LOG_TP_NO, panel_names } from '../../../utils/Config'; import { SpotlightIds } from '../../../utils/SpotlightIds'; import CategoryNav from '../../HomePanel/SubCategory/CategoryNav/CategoryNav'; import css from '../../HomePanel/SubCategory/SubCategory.module.less'; -const SpottableComponent = Spottable("div"); -const Container = SpotlightContainerDecorator({ enterTo: null }, "div"); -const ContainerBasic = SpotlightContainerDecorator( - { enterTo: "last-focused" }, - "div" -); +const SpottableComponent = Spottable('div'); +const Container = SpotlightContainerDecorator({ enterTo: null }, 'div'); +const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const getExpsOrdByLgCatCd = (array, value) => { const expsOrd = array.findIndex(({ lgCatCd }) => value === lgCatCd) + 1; @@ -58,21 +40,12 @@ export default memo(function SubCategory({ }) { const dispatch = useDispatch(); const { getScrollTo, scrollLeft } = useScrollTo(); - const { handleScrollReset, handleStopScrolling } = useScrollReset( - scrollLeft, - false - ); + const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, false); - const categoryInfos = useSelector( - (state) => state.home.menuData?.data?.homeCategory - ); - const categoryItemInfos = useSelector( - (state) => state.main.subCategoryData?.categoryItemInfos - ); + const categoryInfos = useSelector((state) => state.home.menuData?.data?.homeCategory); + const categoryItemInfos = useSelector((state) => state.main.subCategoryData?.categoryItemInfos); - const foruItemInfos = useSelector( - (state) => state.main.recommendProduct[0]?.productInfos - ); + const foruItemInfos = useSelector((state) => state.main.recommendProduct[0]?.productInfos); const nowMenu = useSelector((state) => state.common.menu.nowMenu); @@ -110,9 +83,9 @@ export default memo(function SubCategory({ getSubCategory( { lgCatCd: currentLgCatCd, - pageSize: "10", - tabType: "CAT00102", - filterType: "CAT00202", + pageSize: '10', + tabType: 'CAT00102', + filterType: 'CAT00202', recommendIncFlag: 'Y', }, 1 @@ -124,7 +97,7 @@ export default memo(function SubCategory({ }, [currentLgCatCd, dispatch, firstChk]); useEffect(() => { - if (!nowMenuRef.current || !nowMenuRef.current.startsWith("Home")) { + if (!nowMenuRef.current || !nowMenuRef.current.startsWith('Home')) { return; } @@ -179,40 +152,47 @@ export default memo(function SubCategory({ const handleCardClick = useCallback( (patnrId, prdtId) => () => { dispatch( - pushPanel({ - name: panel_names.DETAIL_PANEL, - panelInfo: { patnrId, prdtId, nowShelf: spotlightId }, + navigateFromSubCategory({ + patnrId, + prdtId, + spotlightId, }) ); }, [dispatch, spotlightId] ); - const handleScrollRight = useCallback((e) => { - const container = e.currentTarget?.parentNode; - const x = container.scrollWidth - container.clientWidth; + const handleScrollRight = useCallback( + (e) => { + const container = e.currentTarget?.parentNode; + const x = container.scrollWidth - container.clientWidth; - setTimeout(() => scrollLeft({ x, animate: true })); - }, [scrollLeft]); + setTimeout(() => scrollLeft({ x, animate: true })); + }, + [scrollLeft] + ); - const handleMoreCardClick = useCallback((e) => { - const lgCatCd = e.currentTarget?.getAttribute("data-catcd-num"); - const lgCatNm = e.currentTarget?.getAttribute("data-catcd-nm"); - const tab = 0; + const handleMoreCardClick = useCallback( + (e) => { + const lgCatCd = e.currentTarget?.getAttribute('data-catcd-num'); + const lgCatNm = e.currentTarget?.getAttribute('data-catcd-nm'); + const tab = 0; - if (lgCatCd && lgCatNm) { - dispatch( - pushPanel({ - name: panel_names.CATEGORY_PANEL, - panelInfo: { - lgCatCd, - lgCatNm, - tab, - }, - }) - ); - } - }, [dispatch]); + if (lgCatCd && lgCatNm) { + dispatch( + pushPanel({ + name: panel_names.CATEGORY_PANEL, + panelInfo: { + lgCatCd, + lgCatNm, + tab, + }, + }) + ); + } + }, + [dispatch] + ); const _handleItemFocus = useCallback(() => { if (handleItemFocus) { @@ -228,35 +208,34 @@ export default memo(function SubCategory({ useEffect(() => { if (!foruItemInfos || foruItemInfos.length === 0) { - const baseData = categoryItemInfos?.subCatItemList?.map((item) => ({ - ...item, - foryou: false, - })) || []; + const baseData = + categoryItemInfos?.subCatItemList?.map((item) => ({ + ...item, + foryou: false, + })) || []; setCategoryItemNewData(baseData); return; } - const recommendedData = foruItemInfos?.map((item) => ({ - ...item, - foryou: true, - })) || []; - - const recommendedPrdtIds = new Set(recommendedData.map(item => item.prdtId)); - - const baseData = categoryItemInfos?.subCatItemList?.map((item) => ({ - ...item, - foryou: recommendedPrdtIds.has(item.prdtId), - })) || []; - + const recommendedData = + foruItemInfos?.map((item) => ({ + ...item, + foryou: true, + })) || []; + + const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId)); + + const baseData = + categoryItemInfos?.subCatItemList?.map((item) => ({ + ...item, + foryou: recommendedPrdtIds.has(item.prdtId), + })) || []; + setCategoryItemNewData([...baseData]); }, [categoryItemInfos?.subCatItemList, foruItemInfos]); return ( - + { return ( - {foryou === true && } + {foryou === true && } ); } @@ -333,7 +307,7 @@ export default memo(function SubCategory({ ); -}); \ No newline at end of file +}); diff --git a/com.twin.app.shoptime/src/views/MainView/MainView.jsx b/com.twin.app.shoptime/src/views/MainView/MainView.jsx index f8fb1d5a..fae595f7 100644 --- a/com.twin.app.shoptime/src/views/MainView/MainView.jsx +++ b/com.twin.app.shoptime/src/views/MainView/MainView.jsx @@ -186,6 +186,10 @@ export default function MainView({ className, initService }) { const isHomeOnTop = useMemo(() => { return !mainIndex && (panels.length <= 0 || (panels.length === 1 && panels[0].panelInfo.modal)); }, [mainIndex, panels]); + const hasDetailPanel = useMemo( + () => panels.some((panel) => panel?.name === Config.panel_names.DETAIL_PANEL), + [panels] + ); const onPreImageLoadComplete = useCallback(() => { // console.log('MainView onPreImageLoadComplete'); @@ -257,6 +261,7 @@ export default function MainView({ className, initService }) { return ( <> {(isHomeOnTop || + hasDetailPanel || (panels.length === 1 && (panels[0]?.name === Config.panel_names.PLAYER_PANEL || panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW || diff --git a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx index e3edabe8..1ae72ccc 100644 --- a/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx +++ b/com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx @@ -61,6 +61,7 @@ import TPopUp from '../../components/TPopUp/TPopUp'; import Media from '../../components/VideoPlayer/Media'; import TReactPlayer from '../../components/VideoPlayer/TReactPlayer'; import { VideoPlayer } from '../../components/VideoPlayer/VideoPlayer.v3'; +import MediaOverlayContents from '../PlayerPanel/PlayerOverlay/MediaOverlayContents'; import usePrevious from '../../hooks/usePrevious'; import useWhyDidYouUpdate from '../../hooks/useWhyDidYouUpdate'; import * as Config from '../../utils/Config'; @@ -2244,6 +2245,7 @@ const MediaPanel = React.forwardRef( spotlightId={ panelInfo.modal ? 'modal-video-player' : panelInfo.modalContainerId || spotlightId } + overlayContentsComponent={MediaOverlayContents} handleIndicatorDownClick={handleIndicatorDownClick} handleIndicatorUpClick={handleIndicatorUpClick} onError={mediainfoHandler} diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx new file mode 100644 index 00000000..29ef1d9c --- /dev/null +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import PlayerOverlayContents from './PlayerOverlayContents'; + +function MediaOverlayContents(props) { + return ; +} + +export default MediaOverlayContents; diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx index b991504b..6206b3d7 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx @@ -37,11 +37,11 @@ function PlayerOverlayContents({ handleIndicatorDownClick, tabContainerVersion, tabIndexV2, + forceShowMediaOverlay = false, }) { const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd); const dispatch = useDispatch(); const onClickBack = (ev) => { - // TabContainerV2๊ฐ€ ํ‘œ์‹œ๋œ ์ƒํƒœ์—์„œ ๋ฐฑ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ๋ฐฉ์ง€ // (Overlay์˜ onClick์œผ๋กœ ์ „ํŒŒ๋˜์–ด toggleControls()๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ๊ฒƒ์„ ๋ง‰์Œ) if (tabContainerVersion === 2 && belowContentsVisible) { @@ -107,11 +107,15 @@ function PlayerOverlayContents({ if (type === 'LIVE') { return Spotlight.focus('videoIndicator-down-button'); } - Spotlight.focus('videoPlayer_mediaControls'); + return Spotlight.focus(SpotlightIds.PLAYER_PLAY_BUTTON); }, [type] ); + const onSpotlightMoveSubtitleButton = useCallback(() => { + return Spotlight.focus('player-subtitlebutton'); + }, []); + const onSpotlightMoveSlider = useCallback( (e) => { if (type === 'VOD') { @@ -154,9 +158,41 @@ function PlayerOverlayContents({ return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); }, []); + const handleOverlayKeyDownCapture = useCallback( + (ev) => { + const currentId = Spotlight.getCurrent()?.getAttribute('data-spotlight-id'); + if (ev.keyCode === 40 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return onSpotlightMoveMediaButton(ev); + } + if (ev.keyCode === 39 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return onSpotlightMoveSubtitleButton(ev); + } + if (ev.keyCode === 37 && currentId === SpotlightIds.PLAYER_SUBTITLE_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return onSpotlightMoveMediaButton(ev); + } + if (ev.keyCode === 38 && currentId === SpotlightIds.PLAYER_PLAY_BUTTON) { + ev.preventDefault(); + ev.stopPropagation(); + return onSpotlightMoveBackButton(); + } + }, + [onSpotlightMoveBackButton, onSpotlightMoveMediaButton, onSpotlightMoveSubtitleButton] + ); + + const shouldShowExtendedControls = useMemo( + () => forceShowMediaOverlay || type !== 'MEDIA', + [forceShowMediaOverlay, type] + ); + const currentSideButtonStatus = useMemo(() => { if ( - type !== 'MEDIA' && + shouldShowExtendedControls && !panelInfo?.modal && !sideContentsVisible && tabContainerVersion === 1 @@ -164,7 +200,7 @@ function PlayerOverlayContents({ return true; } return false; - }, [type, panelInfo, sideContentsVisible, tabContainerVersion]); + }, [shouldShowExtendedControls, panelInfo, sideContentsVisible, tabContainerVersion]); const noLiveContentsVisible = useMemo(() => { if (!Array.isArray(playListInfo) || playListInfo.length === 0) { @@ -181,8 +217,8 @@ function PlayerOverlayContents({ return ( <> - - {type !== 'MEDIA' && playListInfo.length > 1 && noLiveContentsVisible && ( + + {shouldShowExtendedControls && playListInfo.length > 1 && noLiveContentsVisible && ( <>
@@ -247,8 +283,10 @@ function PlayerOverlayContents({ )} onClick={handleSubtitleOnClick} spotlightId="player-subtitlebutton" - onSpotlightUp={onSpotlightMoveTabButton} - onSpotlightLeft={onSpotlightMoveMediaButton} + onSpotlightUp={onSpotlightMoveBackButton} + onSpotlightLeft={onSpotlightMoveBackButton} + onSpotlightRight={onSpotlightMoveMediaButton} + onSpotlightDown={onSpotlightMoveMediaButton} aria-label="Caption" />
@@ -263,6 +301,8 @@ function PlayerOverlayContents({ ? onSpotlightMoveBelowTab : onSpotlightMoveMediaButton } + onSpotlightRight={onSpotlightMoveSubtitleButton} + onSpotlightUp={onSpotlightMoveSubtitleButton} aria-label="Video Player Close" ref={backBtnRef} /> @@ -312,7 +352,8 @@ const propsAreEqual = (prev, next) => { prev.sideContentsVisible === next.sideContentsVisible && prev.belowContentsVisible === next.belowContentsVisible && prev.tabContainerVersion === next.tabContainerVersion && - prev.tabIndexV2 === next.tabIndexV2 + prev.tabIndexV2 === next.tabIndexV2 && + prev.forceShowMediaOverlay === next.forceShowMediaOverlay ); }; diff --git a/com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx b/com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx index 2a8aeb77..722e1ecc 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx @@ -4,11 +4,7 @@ import { useDispatch } from 'react-redux'; import Spotlight from '@enact/spotlight'; -import { - pushPanel, - updatePanel, -} from '../../../actions/panelActions'; -import { navigateToDetailPanel } from '../../../actions/panelNavigationActions'; +import { pushPanel, updatePanel, navigateFromSearch } from '../../../actions/panelActions'; import TItemCardNew from '../../../components/TItemCard/TItemCard.new'; import TScroller from '../../../components/TScroller/TScroller'; import { panel_names } from '../../../utils/Config'; @@ -49,14 +45,16 @@ const ItemCard = ({ onClick, itemInfo, searchQuery }) => { onClick(ev); } - // ์ˆœ์ฐจ ๋„ค๋น„๊ฒŒ์ด์…˜ ์‚ฌ์šฉ (Chrome 68 ํ˜ธํ™˜) - dispatch(navigateToDetailPanel( - patnrId, - prdtId, - searchQuery, - currentSpot, - { tab: 0 } - )); + // ํ†ตํ•ฉ๋œ navigateToDetail ์‚ฌ์šฉ + dispatch( + navigateFromSearch({ + patnrId, + prdtId, + searchQuery, + currentSpot, + additionalInfo: { tab: 0 }, + }) + ); }, [onClick, dispatch, searchQuery] ); @@ -65,7 +63,7 @@ const ItemCard = ({ onClick, itemInfo, searchQuery }) => { <> {itemInfo.map((item, index) => { - const { thumbnail, title, dcPrice, price, soldout, contentId,euEnrgLblInfos } = item; + const { thumbnail, title, dcPrice, price, soldout, contentId, euEnrgLblInfos } = item; const tokens = contentId && contentId.split('_'); const patnrId = tokens?.[4] || ''; const prdtId = tokens?.[5] || '';