ui: mobile hl player

This commit is contained in:
nick-delirium 2025-02-12 14:00:36 +01:00
parent 3be8e8092d
commit 38653d200f
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
10 changed files with 319 additions and 39 deletions

View file

@ -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<Clip>({
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 (
<div
className={
@ -62,14 +64,25 @@ function HighlightPlayer({
style={{ width: 960 }}
>
<Loader loading={notesStore.loading}>
<ClipsPlayer
isHighlight
onClose={onClose}
clip={clip}
currentIndex={0}
isCurrent={true}
autoplay={false}
/>
{isMobile ? (
<MobileClipsPlayer
isHighlight
onClose={onClose}
clip={clip}
currentIndex={0}
isCurrent={true}
autoplay={false}
/>
) : (
<ClipsPlayer
isHighlight
onClose={onClose}
clip={clip}
currentIndex={0}
isCurrent={true}
autoplay={false}
/>
)}
</Loader>
</div>
</div>

View file

@ -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);

View file

@ -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<IOSPlayerContext>(defaultContextValue);
const openedAt = React.useRef<number>();
const [session, setSession] = useState<Session | undefined>(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 (
<Loader
size={75}
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translateX(-50%)',
height: 75,
}}
/>
);
return (
<MobilePlayerContext.Provider value={contextValue}>
{contextValue.player ? (
<>
<ClipPlayerHeader isHighlight={isHighlight} onClose={onClose} range={clip.range} session={session!} />
<MobileClipPlayerContent message={clip.message} isHighlight={isHighlight} autoplay={props.autoplay} range={clip.range} session={session!} />
</>
) : (
<Loader />
)}
</MobilePlayerContext.Provider>
);
}
export default observer(MobileClipsPlayer);

View file

@ -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);

View file

@ -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<IOSPlayerContext>(PlayerContext);
const screenWrapper = React.useRef<HTMLDivElement>(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 (
<div
className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}
>
<div className={cn(stl.playerBody, 'flex-1 flex flex-col relative')}>
<div className={cn(stl.playerBody, 'flex flex-1 flex-col relative')}>
<div className="relative flex-1 overflow-hidden group">
<ClipPlayerOverlay autoplay={props.autoplay} />
<div
className={cn(stl.screenWrapper, stl.checkers)}
ref={screenWrapper}
data-openreplay-obscured
style={{ height: '500px' }}
>
<ReplayWindow
videoURL={videoURL}
userDevice={userDevice}
isAndroid={isAndroid}
screenWidth={screenWidth}
screenHeight={screenHeight}
isClips={true}
/>
<PerfWarnings userDevice={userDevice} />
</div>
</div>
</div>
{props.isHighlight && props.message ? (
<div className={'shadow-inner p-3 flex gap-2 w-full items-center'} style={{ background: 'rgba(252, 193, 0, 0.2)' }}>
<Icon name="chat-square-quote" color="inherit" size={18} />
<div className={'leading-none font-medium'}>
{props.message}
</div>
</div>
) : null}
<ClipPlayerControls
session={props.session}
range={props.range}
/>
</div>
</div>
);
}
export default observer(ClipPlayerContent);

View file

@ -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 (
<div
className={cn(stl.playerBody, 'flex-1 flex flex-col relative', fullscreen && 'pb-2')}

View file

@ -10,6 +10,7 @@ interface Props {
isAndroid: boolean;
screenWidth: number;
screenHeight: number;
isClips?: boolean;
}
const appleIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="102" height="102" fill="white" viewBox="0 0 16 16">
@ -25,7 +26,7 @@ const androidIcon = `<svg fill="#78C257" xmlns="http://www.w3.org/2000/svg" widt
</svg>
`
function ReplayWindow({ videoURL, userDevice, screenHeight, screenWidth, isAndroid }: Props) {
function ReplayWindow({ videoURL, userDevice, screenHeight, screenWidth, isAndroid, isClips }: Props) {
const playerContext = React.useContext<IOSPlayerContext>(MobilePlayerContext);
const videoRef = React.useRef<HTMLVideoElement>();
const imageRef = React.useRef<HTMLImageElement>();
@ -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,

View file

@ -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<WebState>({
...WebPlayer.INITIAL_STATE,
});
if (wrapStore) {
store = wrapStore(store);
}
range?: [number, number],
isMobile?: boolean,
): [IIosPlayer, IOSPlayerStore] | [IWebPlayer, IWebPlayerStore] {
if (isMobile) {
let store: IOSPlayerStore = new SimpleStore<IosState>({
...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<WebState>({
...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];
}

View file

@ -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)

View file

@ -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' })
}