import * as React from 'react'; import { FC, useRef, useEffect, MouseEvent } from 'react'; import { style } from 'typestyle'; import useTapNavigation from '../../../../hooks/useTapNavigation'; import { useStore } from '../../../../services/store'; import { IMedia } from '../../../../types'; import Controls from '../components/Controls'; import MediaContainer from '../components/MediaContainer'; import MediaProgress from '../components/MediaProgress'; import ShopThisLookProductList from '../../../ShopThisLook/ProductList'; import PlayerNavButton from '../../../Icons/PlayerNavButton'; import UserName from '../../../UserName'; import { pillActiveBackgroundColor, pillBackgroundColor, pillFontColor } from '../../../consts'; import CloseBold from '../../../Icons/CloseBold'; import { onKeyDownEventCallback } from '../../../../utils/onKeyDown'; import { useDialogFocusTrap } from '../../../../hooks/useDialogFocusTrap'; import usePlayerNavigation, { PLAYER_ANIMATION_DURATION_SEC, } from '../../../../hooks/usePlayerNavigation'; import useWheelNavigation from '../../../../hooks/useWheelNavigation'; import { useAccessibilityMessage } from '../../../../hooks/useAccessibilityMessage'; import useBorderRadius from '../../../../hooks/useBorderRadius'; import { usePlayerKeyboardNavigation } from '../../../../hooks/usePlayerKeyboardNavigation'; import useMediaCardWidth from '../../../../hooks/useMediaCardWidth'; import useAdjacentCardWidth from '../../../../hooks/useAdjacentCardWidth'; import Spinner from '../../../Icons/Spinner'; interface DesktopPlayerProps { post: IMedia; settings: any; totalPosts: number; onClose: () => void; onPostChange: (post: IMedia | null) => void; classPrefix: string; } /** * Desktop "product_list_player" mode. * Shows a stories-format media player on the left and the product list on the * right, both centered over a full-screen overlay. Shares all navigation * logic with MobilePlayer via the usePlayerNavigation hook. */ const DesktopPlayer: FC = ({ post, settings, totalPosts, onClose, onPostChange, classPrefix, }) => { const store = useStore(); const wrapperRef = useRef(null); const storiesCardRef = useRef(null); const productSideRef = useRef(null); const { containerRef: mediaSideRef, cardWidth } = useMediaCardWidth(post); const swipeDirection: 'horizontal' | 'vertical' = settings.player_swipe_direction === 'horizontal' ? 'horizontal' : 'vertical'; const isVertical = swipeDirection === 'vertical'; const { currentDisplayPost, nextDisplayPost, prevDisplayPost, isMuted, isPlaying, isVisible, isClosing, slideOffset, isSliding, slideAxis, handleClose, handleNextPost, handlePreviousPost, toggleMute, togglePlay, } = usePlayerNavigation({ post, onClose, onPostChange, swipeDirection }); const medias = store.data.content.medias; const mediaBorderRadius = useBorderRadius(200, 200, settings.thumbnail_corners); // Screen-reader live-region announcements when navigating between posts. const { accessibilityMessage, announce } = useAccessibilityMessage(); const isFirstAnnouncementRef = useRef(true); useEffect(() => { if (isFirstAnnouncementRef.current) { isFirstAnnouncementRef.current = false; return; } announce( `Post ${currentDisplayPost.postIndex + 1} of ${medias.length}` + (currentDisplayPost.username ? `, by ${currentDisplayPost.username}` : ''), ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentDisplayPost.id]); useDialogFocusTrap({ active: isVisible, containerRef: wrapperRef }); const { pendingCardWidth, handleNextWithWidth, handlePrevWithWidth } = useAdjacentCardWidth({ containerRef: mediaSideRef, nextPost: nextDisplayPost, prevPost: prevDisplayPost, currentCardWidth: cardWidth, handleNextPost, handlePreviousPost, }); // Mouse-wheel / trackpad scroll navigation (vertical mode only) useWheelNavigation({ containerRef: wrapperRef, onScrollDown: handleNextWithWidth, onScrollUp: handlePrevWithWidth, disabled: !isVertical, excludeRef: productSideRef, }); // Tap navigation on the stories card : only in horizontal mode useTapNavigation({ containerRef: storiesCardRef, onNext: handleNextWithWidth, onPrevious: handlePrevWithWidth, axis: swipeDirection, disabled: isVertical, }); usePlayerKeyboardNavigation({ containerRef: wrapperRef, isVertical, currentPostType: currentDisplayPost.type, handleClose, handleNextPost: handleNextWithWidth, handlePreviousPost: handlePrevWithWidth, togglePlay, toggleMute, }); const animationClass = isClosing ? styles.closing : ''; // Overlay click: left/right in horizontal mode, top/bottom in vertical mode. const handleOverlayClick = (e: MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); if (isVertical) { const y = e.clientY - rect.top; if (y < rect.height / 2) handlePrevWithWidth(); else handleNextWithWidth(); } else { const x = e.clientX - rect.left; if (x < rect.width / 2) handlePrevWithWidth(); else handleNextWithWidth(); } }; return (
{!isVisible && } {isVertical ? ( /* ---- VERTICAL mode: up/down arrows stacked on the right ---- */ <> e.stopPropagation()} > e.stopPropagation()} > ) : ( /* ---- HORIZONTAL mode: left/right arrows ---- */ <> e.stopPropagation()}> e.stopPropagation()}> )}
e.stopPropagation()}> {}} isMuted={isMuted} isPlaying={isPlaying} toggleMute={toggleMute} togglePlay={togglePlay} renderMode='desktop' />
e.stopPropagation()}>
e.stopPropagation()} > {/* Progress bar shown only in HORIZONTAL mode */} {!isVertical ? ( ) : null}
e.stopPropagation()}>
{/* Visually-hidden live region – screen readers announce post changes here */}
{accessibilityMessage}
); }; export default DesktopPlayer; const styles = { /*Overlay*/ wrapper: style({ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', zIndex: 9999, display: 'flex', flexDirection: 'row', alignItems: 'stretch', backgroundColor: 'rgba(0, 0, 0, 0.55)', backdropFilter: 'blur(6px)', '-webkit-backdrop-filter': 'blur(6px)', cursor: 'pointer', outline: 'none', }), closing: style({ opacity: 0, pointerEvents: 'none' as const }), loaderOverlay: style({ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', zIndex: 10, }), closeButton: style({ position: 'absolute', top: 20, left: 20, zIndex: 20, padding: '5px', borderRadius: '50%', height: '40px', width: '40px', backgroundColor: pillBackgroundColor, backdropFilter: 'blur(3px)', '-webkit-backdrop-filter': 'blur(3px)', color: pillFontColor, border: 'none', cursor: 'pointer', outline: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', $nest: { '&:active': { backgroundColor: pillActiveBackgroundColor }, '&:focus-visible': { outline: '2px solid white', outlineOffset: '2px' }, }, }), mediaSide: style({ position: 'relative', flex: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'center', height: '100%', overflow: 'hidden', cursor: 'default', }), /* ---- Horizontal mode arrows (left / right) ---- */ leftArrow: style({ position: 'absolute', left: 20, top: '50%', transform: 'translateY(-50%)', zIndex: 10, cursor: 'default', }), rightArrow: style({ position: 'absolute', right: 20, top: '50%', transform: 'translateY(-50%)', zIndex: 10, cursor: 'default', }), /* ---- Vertical mode arrows (up / down) ---- */ verticalUpArrow: style({ position: 'absolute', right: 20, top: '50%', marginTop: -48, // one arrow-button height above centre zIndex: 10, cursor: 'default', }), verticalDownArrow: style({ position: 'absolute', right: 20, top: '50%', marginTop: 8, // one arrow-button height below centre zIndex: 10, cursor: 'default', }), mediaSideTopRight: style({ position: 'absolute', top: 20, right: 20, zIndex: 10, cursor: 'default', }), mediaSideBottomRight: style({ position: 'absolute', bottom: 20, right: 20, zIndex: 10, cursor: 'default', }), storiesCard: style({ position: 'relative', height: '100%', maxWidth: '80%', overflow: 'hidden', flexShrink: 0, }), productSide: style({ width: '28%', minWidth: 280, maxWidth: 420, height: '100%', flexShrink: 0, position: 'relative', zIndex: 1, cursor: 'default', }), /* Visually hidden but readable by screen readers */ srOnly: style({ position: 'absolute', width: 1, height: 1, padding: 0, margin: -1, overflow: 'hidden', clip: 'rect(0,0,0,0)', whiteSpace: 'nowrap', border: 0, }), };