ui: mobile hl player
This commit is contained in:
parent
3be8e8092d
commit
38653d200f
10 changed files with 319 additions and 39 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
140
frontend/app/components/Session/MobileClipsPlayer.tsx
Normal file
140
frontend/app/components/Session/MobileClipsPlayer.tsx
Normal 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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue