From 38653d200f53ae226a62f7b48b764a69f1602f60 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 12 Feb 2025 14:00:36 +0100 Subject: [PATCH] ui: mobile hl player --- .../components/Highlights/HighlightPlayer.tsx | 33 +++-- .../app/components/Session/ClipsPlayer.tsx | 7 +- .../components/Session/MobileClipsPlayer.tsx | 140 ++++++++++++++++++ .../Player/ClipPlayer/ClipPlayerContent.tsx | 6 +- .../ClipPlayer/MobileClipPlayerContent.tsx | 110 ++++++++++++++ .../Player/MobilePlayer/PlayerInst.tsx | 4 +- .../Player/MobilePlayer/ReplayWindow.tsx | 5 +- frontend/app/player/create.ts | 44 ++++-- frontend/app/player/mobile/IOSPlayer.ts | 4 +- frontend/app/player/web/Screen/Screen.ts | 5 +- 10 files changed, 319 insertions(+), 39 deletions(-) create mode 100644 frontend/app/components/Session/MobileClipsPlayer.tsx create mode 100644 frontend/app/components/Session/Player/ClipPlayer/MobileClipPlayerContent.tsx diff --git a/frontend/app/components/Highlights/HighlightPlayer.tsx b/frontend/app/components/Highlights/HighlightPlayer.tsx index 0e5695f29..8888cb3f3 100644 --- a/frontend/app/components/Highlights/HighlightPlayer.tsx +++ b/frontend/app/components/Highlights/HighlightPlayer.tsx @@ -1,6 +1,7 @@ import React from 'react'; import cn from 'classnames'; import ClipsPlayer from '../Session/ClipsPlayer'; +import MobileClipsPlayer from '../Session/MobileClipsPlayer'; import { useStore } from 'App/mstore'; import { Loader } from 'UI'; import { observer } from 'mobx-react-lite'; @@ -18,12 +19,13 @@ function HighlightPlayer({ hlId: string; onClose: () => void; }) { - const { notesStore } = useStore(); + const { notesStore, projectsStore } = useStore(); const [clip, setClip] = React.useState({ sessionId: undefined, range: [], message: '', }); + const isMobile = projectsStore.isMobile; React.useEffect(() => { if (hlId) { @@ -45,7 +47,7 @@ function HighlightPlayer({ if (e.target === e.currentTarget) { onClose(); } - } + }; return (
- + {isMobile ? ( + + ) : ( + + )}
diff --git a/frontend/app/components/Session/ClipsPlayer.tsx b/frontend/app/components/Session/ClipsPlayer.tsx index 8bdd34995..a28d98345 100644 --- a/frontend/app/components/Session/ClipsPlayer.tsx +++ b/frontend/app/components/Session/ClipsPlayer.tsx @@ -28,7 +28,7 @@ interface Props { isHighlight?: boolean; } -function WebPlayer(props: Props) { +function ClipsPlayer(props: Props) { const { clip, currentIndex, isCurrent, onClose, isHighlight } = props; const { sessionStore } = useStore(); const prefetched = sessionStore.prefetched; @@ -101,11 +101,8 @@ function WebPlayer(props: Props) { }, [session, domFiles, prefetched]); const { - firstVisualEvent: visualOffset, - messagesProcessed, tabStates, ready, - playing, } = contextValue.store?.get() || {}; const cssLoading = @@ -156,4 +153,4 @@ function WebPlayer(props: Props) { ); } -export default observer(WebPlayer); +export default observer(ClipsPlayer); diff --git a/frontend/app/components/Session/MobileClipsPlayer.tsx b/frontend/app/components/Session/MobileClipsPlayer.tsx new file mode 100644 index 000000000..f01a2f61a --- /dev/null +++ b/frontend/app/components/Session/MobileClipsPlayer.tsx @@ -0,0 +1,140 @@ +import { createClipPlayer } from 'Player'; +import { makeAutoObservable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import React, { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; + +import { useStore } from 'App/mstore'; +import { Loader } from 'UI'; +import { + IOSPlayerContext, + MobilePlayerContext, + defaultContextValue, +} from './playerContext'; + +import ClipPlayerHeader from 'Components/Session/Player/ClipPlayer/ClipPlayerHeader'; +import MobileClipPlayerContent from 'Components/Session/Player/ClipPlayer/MobileClipPlayerContent'; +import Session from 'Types/session'; +import { sessionService } from '@/services'; + +let playerInst: IOSPlayerContext['player'] | undefined; + +interface Props { + clip: any; + currentIndex: number; + isCurrent: boolean; + autoplay: boolean; + onClose?: () => void; + isHighlight?: boolean; +} + +function MobileClipsPlayer(props: Props) { + const { clip, currentIndex, isCurrent, onClose, isHighlight } = props; + const { sessionStore } = useStore(); + const [windowActive, setWindowActive] = useState(!document.hidden); + const [contextValue, setContextValue] = + // @ts-ignore + useState(defaultContextValue); + const openedAt = React.useRef(); + const [session, setSession] = useState(undefined); + + useEffect(() => { + if (!clip.sessionId) return; + + const fetchSession = async () => { + if (clip.sessionId != null && clip?.sessionId !== '') { + try { + const data = await sessionService.getSessionInfo(clip.sessionId); + setSession(new Session(data)); + } catch (error) { + console.error('Error fetching session data:', error); + } + } else { + console.error('No sessionID in route.'); + } + }; + + void fetchSession(); + }, [clip]); + + React.useEffect(() => { + openedAt.current = Date.now(); + if (windowActive) { + const handleActivation = () => { + if (!document.hidden) { + setWindowActive(true); + document.removeEventListener('visibilitychange', handleActivation); + } + }; + document.addEventListener('visibilitychange', handleActivation); + } + }, []); + + useEffect(() => { + playerInst = undefined; + if (!clip.sessionId || contextValue.player !== undefined || !session) + return; + + // @ts-ignore + sessionStore.setUserTimezone(session?.timezone); + const [PlayerInst, PlayerStore] = createClipPlayer( + session, + (state) => makeAutoObservable(state), + toast, + clip.range, + true, + ); + + setContextValue({ player: PlayerInst, store: PlayerStore }); + playerInst = PlayerInst; + // playerInst.pause(); + }, [session]); + + const { + ready, + } = contextValue.store?.get() || {}; + + useEffect(() => { + if (ready) { + if (!isCurrent) { + contextValue.player?.pause(); + } + } + }, [ready]); + + useEffect(() => { + contextValue.player?.jump(clip.range[0]); + setTimeout(() => { + contextValue.player?.play(); + }, 500); + }, [currentIndex]); + + if (!session || !session?.sessionId) + return ( + + ); + + return ( + + {contextValue.player ? ( + <> + + + + ) : ( + + )} + + ); +} + +export default observer(MobileClipsPlayer); diff --git a/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx b/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx index d4b3cf531..1d7362f3c 100644 --- a/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx +++ b/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx @@ -6,7 +6,6 @@ import { PlayerContext, } from 'Components/Session/playerContext'; import ClipPlayerControls from 'Components/Session/Player/ClipPlayer/ClipPlayerControls'; -import { findDOMNode } from 'react-dom'; import Session from 'Types/session'; import styles from 'Components/Session_/playerBlock.module.css'; import ClipPlayerOverlay from 'Components/Session/Player/ClipPlayer/ClipPlayerOverlay'; @@ -19,6 +18,7 @@ interface Props { autoplay: boolean; isHighlight?: boolean; message?: string; + isMobile?: boolean; } function ClipPlayerContent(props: Props) { @@ -30,9 +30,7 @@ function ClipPlayerContent(props: Props) { React.useEffect(() => { if (!playerContext.player) return; - const parentElement = findDOMNode( - screenWrapper.current - ) as HTMLDivElement | null; + const parentElement = screenWrapper.current if (parentElement && playerContext.player) { playerContext.player?.attach(parentElement); diff --git a/frontend/app/components/Session/Player/ClipPlayer/MobileClipPlayerContent.tsx b/frontend/app/components/Session/Player/ClipPlayer/MobileClipPlayerContent.tsx new file mode 100644 index 000000000..c24707d9a --- /dev/null +++ b/frontend/app/components/Session/Player/ClipPlayer/MobileClipPlayerContent.tsx @@ -0,0 +1,110 @@ +import React, { useEffect } from 'react'; +import cn from 'classnames'; +import stl from 'Components/Session_/Player/player.module.css'; +import { + IOSPlayerContext, + PlayerContext, +} from 'Components/Session/playerContext'; +import ClipPlayerControls from 'Components/Session/Player/ClipPlayer/ClipPlayerControls'; +import Session from 'Types/session'; +import styles from 'Components/Session_/playerBlock.module.css'; +import ClipPlayerOverlay from 'Components/Session/Player/ClipPlayer/ClipPlayerOverlay'; +import { observer } from 'mobx-react-lite'; +import { Icon } from 'UI'; +import ReplayWindow from 'Components/Session/Player/MobilePlayer/ReplayWindow' +import PerfWarnings from "Components/Session/Player/MobilePlayer/PerfWarnings"; +import { useStore } from 'App/mstore' + +interface Props { + session: Session; + range: [number, number]; + autoplay: boolean; + isHighlight?: boolean; + message?: string; + isMobile?: boolean; +} + +function ClipPlayerContent(props: Props) { + const { sessionStore } = useStore(); + const playerContext = React.useContext(PlayerContext); + const screenWrapper = React.useRef(null); + const { time } = playerContext.store.get(); + const { range } = props; + const userDevice = sessionStore.current.userDevice; + const videoURL = sessionStore.current.videoURL; + const screenWidth = sessionStore.current.screenWidth!; + const screenHeight = sessionStore.current.screenHeight!; + const platform = sessionStore.current.platform; + const isAndroid = platform === 'android'; + + React.useEffect(() => { + if (!playerContext.player) return; + + const parentElement = screenWrapper.current + + if (parentElement && playerContext.player) { + playerContext.player.attach(parentElement); + playerContext.player?.play(); + } + }, [playerContext.player]); + + React.useEffect(() => { + playerContext.player.scale(); + }, [playerContext.player]); + + useEffect(() => { + if (time < range[0]) { + playerContext.player?.jump(range[0]); + } + if (time > range[1]) { + playerContext.store.update({ completed: true }); + playerContext.player?.pause(); + } + }, [time]); + + if (!playerContext.player) return null; + + return ( +
+
+
+
+ +
+ + +
+
+
+ {props.isHighlight && props.message ? ( +
+ +
+ {props.message} +
+
+ ) : null} + +
+
+ ); +} + +export default observer(ClipPlayerContent); diff --git a/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx b/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx index b68a6199b..e9150ca76 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx @@ -49,6 +49,7 @@ function Player(props: IProps) { const userDevice = sessionStore.current.userDevice; const videoURL = sessionStore.current.videoURL; const platform = sessionStore.current.platform; + const isAndroid = platform === 'android'; const screenWidth = sessionStore.current.screenWidth!; const screenHeight = sessionStore.current.screenHeight!; const updateLastPlayedSession = sessionStore.updateLastPlayedSession; @@ -63,7 +64,7 @@ function Player(props: IProps) { React.useEffect(() => { updateLastPlayedSession(sessionId); - const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture + const parentElement = screenWrapper.current; //TODO: good architecture if (parentElement && !isAttached) { playerContext.player.attach(parentElement); setAttached(true) @@ -105,7 +106,6 @@ function Player(props: IProps) { document.addEventListener('mouseup', handleMouseUp); }; - const isAndroid = platform === 'android'; return (
@@ -25,7 +26,7 @@ const androidIcon = ` ` -function ReplayWindow({ videoURL, userDevice, screenHeight, screenWidth, isAndroid }: Props) { +function ReplayWindow({ videoURL, userDevice, screenHeight, screenWidth, isAndroid, isClips }: Props) { const playerContext = React.useContext(MobilePlayerContext); const videoRef = React.useRef(); const imageRef = React.useRef(); @@ -111,7 +112,7 @@ function ReplayWindow({ videoURL, userDevice, screenHeight, screenWidth, isAndro icon.id = '___or_mobile-loader-icon'; host.id = '___or_mobile-player'; - playerContext.player.injectPlayer(host); + playerContext.player.injectPlayer(host, isClips); playerContext.player.customScale(styles.shell.width, styles.shell.height); playerContext.player.updateDimensions({ width: styles.screen.width, diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index 8d04f777a..9e18a2266 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -91,20 +91,38 @@ export function createLiveWebPlayer( export function createClipPlayer( session: SessionFilesInfo, - wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore, + wrapStore?: (s: IOSPlayerStore | IWebPlayerStore) => IOSPlayerStore | IWebPlayerStore, uiErrorHandler?: { error: (msg: string) => void }, - range?: [number, number] -): [IWebPlayer, IWebPlayerStore] { - let store: WebPlayerStore = new SimpleStore({ - ...WebPlayer.INITIAL_STATE, - }); - if (wrapStore) { - store = wrapStore(store); - } + range?: [number, number], + isMobile?: boolean, +): [IIosPlayer, IOSPlayerStore] | [IWebPlayer, IWebPlayerStore] { + if (isMobile) { + let store: IOSPlayerStore = new SimpleStore({ + ...IOSPlayer.INITIAL_STATE, + }); + if (wrapStore) { + // @ts-ignore + store = wrapStore(store); + } - const player = new WebPlayer(store, session, false, false, uiErrorHandler); - if (range && range[0] !== range[1]) { - player.toggleRange(range[0], range[1]); + const player = new IOSPlayer(store, session, uiErrorHandler); + if (range && range[0] !== range[1]) { + player.toggleRange(range[0], range[1]); + } + return [player, store] as [IIosPlayer, IOSPlayerStore]; + } else { + let store: WebPlayerStore = new SimpleStore({ + ...WebPlayer.INITIAL_STATE, + }); + if (wrapStore) { + // @ts-ignore + store = wrapStore(store); + } + + const player = new WebPlayer(store, session, false, false, uiErrorHandler); + if (range && range[0] !== range[1]) { + player.toggleRange(range[0], range[1]); + } + return [player, store] as [IWebPlayer, IWebPlayerStore]; } - return [player, store]; } \ No newline at end of file diff --git a/frontend/app/player/mobile/IOSPlayer.ts b/frontend/app/player/mobile/IOSPlayer.ts index c87beb3ad..a01876303 100644 --- a/frontend/app/player/mobile/IOSPlayer.ts +++ b/frontend/app/player/mobile/IOSPlayer.ts @@ -105,9 +105,9 @@ export default class IOSPlayer extends Player { this.screen.updateOverlayStyle(style); } - injectPlayer = (player: HTMLElement) => { + injectPlayer = (player: HTMLElement, stableTop?: boolean) => { this.screen.addToBody(player); - this.screen.addMobileStyles(); + this.screen.addMobileStyles(stableTop); window.addEventListener('resize', () => this.customScale(this.customConstrains.width, this.customConstrains.height) diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index 8399ab867..73ec3bdb0 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -85,9 +85,12 @@ export default class Screen { this.cursor = new Cursor(this.overlay, isMobile); // TODO: move outside } - addMobileStyles() { + addMobileStyles(stableTop?: boolean) { this.iframe.className = styles.mobileIframe; this.screen.className = styles.mobileScreen; + if (stableTop) { + this.screen.style.marginTop = '0px'; + } if (this.document) { Object.assign(this.document?.body.style, { margin: 0, overflow: 'hidden' }) }