feat (frontend-player): heatmaps player markup + overlay refactor

This commit is contained in:
ShiKhu 2021-07-23 19:41:54 +08:00
parent 0e3fa5fd8e
commit ebcf5ad655
22 changed files with 297 additions and 199 deletions

View file

@ -1 +0,0 @@
export { default } from './AutoplayTimer'

View file

@ -73,7 +73,7 @@ function getStorageName(type) {
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,

View file

@ -0,0 +1,77 @@
import React, {useEffect} from 'react';
import { connectPlayer, markTargets } from 'Player';
import { getStatusText } from 'Player/MessageDistributor/managers/AssistManager';
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
import AutoplayTimer from './Overlay/AutoplayTimer';
import PlayIconLayer from './Overlay/PlayIconLayer';
import LiveStatusText from './Overlay/LiveStatusText';
import Loader from './Overlay/Loader';
import ElementsMarker from './Overlay/ElementsMarker';
interface Props {
playing: boolean,
completed: boolean,
inspectorMode: boolean,
messagesLoading: boolean,
loading: boolean,
live: boolean,
liveStatusText: string,
autoplay: boolean,
markedTargets: MarkedTarget[] | null,
nextId: string,
togglePlay: () => void,
}
function Overlay({
playing,
completed,
inspectorMode,
messagesLoading,
loading,
live,
liveStatusText,
autoplay,
markedTargets,
nextId,
togglePlay,
}: Props) {
useEffect(() =>{
setTimeout(() => markTargets([{ selector: 'div', count:6}]), 5000)
setTimeout(() => markTargets(null), 8000)
},[])
const showAutoplayTimer = !live && completed && autoplay && nextId
const showPlayIconLayer = !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer;
const showLiveStatusText = live && liveStatusText && !loading;
return (
<>
{ showAutoplayTimer && <AutoplayTimer /> }
{ showLiveStatusText &&
<LiveStatusText text={liveStatusText} />
}
{ messagesLoading && <Loader/> }
{ showPlayIconLayer &&
<PlayIconLayer playing={playing} togglePlay={togglePlay} />
}
{ markedTargets && <ElementsMarker targets={ markedTargets } />
}
</>
);
}
export default connectPlayer(state => ({
playing: state.playing,
messagesLoading: state.messagesLoading,
loading: state.messagesLoading || state.cssLoading,
completed: state.completed,
autoplay: state.autoplay,
live: state.live,
liveStatusText: getStatusText(state.peerConnectionStatus),
markedTargets: state.markedTargets
}))(Overlay);

View file

@ -0,0 +1,3 @@
.overlayBg {
background-color: rgba(255, 255, 255, 0.8);
}

View file

@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react'
import cn from 'classnames';
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom';
import { Button, Link } from 'UI'
import { session as sessionRoute, withSiteId } from 'App/routes'
import stl from './AutoplayTimer.css'
import { withRouter } from 'react-router-dom';
import stl from './AutoplayTimer.css';
import clsOv from './overlay.css';
function AutoplayTimer({ nextId, siteId, history }) {
let timer
@ -33,7 +35,7 @@ function AutoplayTimer({ nextId, siteId, history }) {
return ''
return (
<div className={stl.overlay}>
<div className={ cn(clsOv.overlay, stl.overlayBg) } >
<div className="border p-6 shadow-lg bg-white rounded">
<div className="py-4">Next recording will be played in {counter}s</div>
<div className="flex items-center">

View file

@ -0,0 +1,9 @@
import React from 'react';
import Marker from './ElementsMarker/Marker';
export default function ElementsMarker({ targets }) {
return targets.map(t => <Marker target={t} />)
}

View file

@ -0,0 +1,5 @@
.marker {
position: absolute;
border: 2px dashed;
z-index: 9999;
}

View file

@ -0,0 +1,21 @@
import React, { useState, useEffect } from 'react';
import { Popup } from 'UI';
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
import stl from './Marker.css';
interface Props {
target: MarkedTarget;
}
export default function Marker({ target }: Props) {
const style = {
top: `${ target.boundingRect.top }px`,
left: `${ target.boundingRect.left }px`,
width: `${ target.boundingRect.width }px`,
height: `${ target.boundingRect.height }px`,
}
return <div className={ stl.marker } style={ style } />
}

View file

@ -0,0 +1,4 @@
.text {
color: $gray-light;
font-size: 40px;
}

View file

@ -0,0 +1,11 @@
import React from 'react';
import stl from './LiveStatusText.css';
import ovStl from './overlay.css';
interface Props {
text: string;
}
export default function LiveStatusText({ text }: Props) {
return <div className={ovStl.overlay}><span className={stl.text}>{text}</span></div>
}

View file

@ -0,0 +1,7 @@
import React from 'react';
import { Loader } from 'UI';
import ovStl from './overlay.css';
export default function OverlayLoader() {
return <div className={ovStl.overlay}><Loader loading /></div>
}

View file

@ -0,0 +1,17 @@
.iconWrapper {
background-color: rgba(0, 0, 0, 0.1);
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
opacity: 0;
transition: all .2s; /* Animation */
}
.zoomIcon {
opacity: 1;
transform: scale(1.8);
transition: all .8s;
}

View file

@ -0,0 +1,38 @@
import React, { useState, useCallback } from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
import cls from './PlayIconLayer.css';
import clsOv from './overlay.css';
interface Props {
togglePlay: () => void,
playing: boolean,
}
export default function PlayIconLayer({ playing, togglePlay }: Props) {
const [ showPlayOverlayIcon, setShowPlayOverlayIcon ] = useState(false);
const togglePlayAnimated = useCallback(() => {
setShowPlayOverlayIcon(true);
togglePlay();
setTimeout(
() => setShowPlayOverlayIcon(false),
800,
);
}, []);
return (
<div className={ clsOv.overlay } onClick={ togglePlayAnimated }>
<div
className={ cn(cls.iconWrapper, {
[ cls.zoomIcon ]: showPlayOverlayIcon
}) }
>
<Icon
name={ playing ? "play" : "pause" }
color="gray-medium"
size={30}
/>
</div>
</div>
)
}

View file

@ -1,8 +1,3 @@
.wrapper {
width: 30%;
height: 30%;
}
.overlay {
position: absolute;
top: 0;
@ -13,5 +8,4 @@
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
}
}

View file

@ -2,29 +2,17 @@ import { connect } from 'react-redux';
import { findDOMNode } from 'react-dom';
import cn from 'classnames';
import { Loader, IconButton, EscapeButton } from 'UI';
import { hide as hideTargetDefiner, toggleInspectorMode } from 'Duck/components/targetDefiner';
import { hide as hideTargetDefiner } from 'Duck/components/targetDefiner';
import { fullscreenOff } from 'Duck/components/player';
import withOverlay from 'Components/hocs/withOverlay';
import { attach as attachPlayer, Controls as PlayerControls, connectPlayer } from 'Player';
import Controls from './Controls';
import Overlay from './Overlay';
import stl from './player.css';
import AutoplayTimer from '../AutoplayTimer';
import EventsToggleButton from '../../Session/EventsToggleButton';
import { getStatusText } from 'Player/MessageDistributor/managers/AssistManager';
const ScreenWrapper = withOverlay()(React.memo(() => <div className={ stl.screenWrapper } />));
@connectPlayer(state => ({
playing: state.playing,
loading: state.messagesLoading,
disconnected: state.disconnected,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode,
removeOverlay: !state.messagesLoading && state.inspectorMode || state.live,
completed: state.completed,
autoplay: state.autoplay,
live: state.live,
liveStatusText: getStatusText(state.peerConnectionStatus),
}))
@connect(state => ({
//session: state.getIn([ 'sessions', 'current' ]),
@ -32,15 +20,9 @@ const ScreenWrapper = withOverlay()(React.memo(() => <div className={ stl.screen
nextId: state.getIn([ 'sessions', 'nextId' ]),
}), {
hideTargetDefiner,
toggleInspectorMode: () => toggleInspectorMode(false),
fullscreenOff,
})
export default class Player extends React.PureComponent {
state = {
showPlayOverlayIcon: false,
startedToPlayAt: Date.now(),
};
screenWrapper = React.createRef();
componentDidMount() {
@ -48,69 +30,14 @@ export default class Player extends React.PureComponent {
attachPlayer(parentElement);
}
componentDidUpdate(prevProps) {
if (prevProps.targetSelector !== this.props.targetSelector) {
PlayerControls.mark(this.props.targetSelector);
}
if (prevProps.playing !== this.props.playing) {
if (this.props.playing) {
this.setState({ startedToPlayAt: Date.now() });
} else {
this.updateWatchingTime();
}
}
}
componentWillUnmount() {
if (this.props.playing) {
this.updateWatchingTime();
}
}
updateWatchingTime() {
const diff = Date.now() - this.state.startedToPlayAt;
}
// onTargetClick = (targetPath) => {
// const { targetCustomList, location } = this.props;
// const targetCustomFromList = targetCustomList !== this.props.targetSelector
// .find(({ path }) => path === targetPath);
// const target = targetCustomFromList
// ? targetCustomFromList.set('location', location)
// : { path: targetPath, isCustom: true, location };
// this.props.showTargetDefiner(target);
// }
togglePlay = () => {
this.setState({ showPlayOverlayIcon: true });
PlayerControls.togglePlay();
setTimeout(
() => this.setState({ showPlayOverlayIcon: false }),
800,
);
}
render() {
const {
showPlayOverlayIcon,
} = this.state;
const {
className,
playing,
disabled,
removeOverlay,
bottomBlockIsActive,
loading,
disconnected,
fullscreen,
fullscreenOff,
completed,
autoplay,
nextId,
live,
liveStatusText,
} = this.props;
return (
@ -120,40 +47,12 @@ export default class Player extends React.PureComponent {
>
{ fullscreen &&
<EscapeButton onClose={ fullscreenOff } />
// <IconButton
// size="18"
// className="ml-auto mb-5"
// style={{ marginTop: '-5px' }}
// onClick={ fullscreenOff }
// size="small"
// icon="close"
// label="Esc"
// />
}
{!live && !fullscreen && <EventsToggleButton /> }
<div className="relative flex-1">
{ (!removeOverlay || live && liveStatusText) &&
<div
className={ stl.overlay }
onClick={ disabled ? null : this.togglePlay }
>
{ live && liveStatusText
? <span className={stl.liveStatusText}>{liveStatusText}</span>
: <Loader loading={ loading } />
}
{ !live &&
<div
className={ cn(stl.iconWrapper, {
[ stl.zoomIcon ]: showPlayOverlayIcon
}) }
>
<div className={ playing ? stl.playIcon : stl.pauseIcon } />
</div>
}
</div>
}
{ completed && autoplay && nextId && <AutoplayTimer /> }
<ScreenWrapper
<div className="relative flex-1 overflow-hidden">
<Overlay nextId={nextId} togglePlay={PlayerControls.togglePlay} />
<div
className={ stl.screenWrapper }
ref={ this.screenWrapper }
/>
</div>

View file

@ -1,5 +1,3 @@
@import 'icons.css';
.playerBody {
background: $white;
/* border-radius: 3px; */
@ -25,61 +23,8 @@
font-weight: 200;
color: $gray-medium;
}
.overlay {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
/* &[data-protect] {
pointer-events: none;
background: $white;
opacity: 0.3;
}
*/
& .iconWrapper {
background-color: rgba(0, 0, 0, 0.1);
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
opacity: 0;
transition: all .2s; /* Animation */
}
& .zoomIcon {
opacity: 1;
transform: scale(1.8);
transition: all .8s;
}
& .playIcon {
@mixin icon play, $gray-medium, 30px;
}
& .pauseIcon {
@mixin icon pause, $gray-medium, 30px;
}
}
.playerView {
position: relative;
flex: 1;
}
.inspectorMode {
z-index: 99991 !important;
}
.liveStatusText {
color: $gray-light;
font-size: 40px;
}

View file

@ -1,3 +1,4 @@
import React from 'react';
import cn from 'classnames';
import SVG from 'UI/SVG';
import styles from './icon.css';

View file

@ -306,9 +306,7 @@ export default class MessageDistributor extends StatedScreen {
this.pagesManager.moveReady(t).then(() => {
const lastScroll = this.scrollManager.moveToLast(t, index);
// @ts-ignore ??can't see double inheritance
if (!!lastScroll && this.window) {
// @ts-ignore
this.window.scrollTo(lastScroll.x, lastScroll.y);
}
// Moving mouse and setting :hover classes on ready view
@ -479,7 +477,6 @@ export default class MessageDistributor extends StatedScreen {
// TODO: clean managers?
clean() {
// @ts-ignore
super.clean();
//if (this._socket) this._socket.close();
update(INITIAL_STATE);

View file

@ -18,7 +18,7 @@ export const INITIAL_STATE: State = {
export default abstract class BaseScreen {
public readonly overlay: HTMLDivElement;
private readonly iframe: HTMLIFrameElement;
private readonly _screen: HTMLDivElement;
protected readonly screen: HTMLDivElement;
protected parentElement: HTMLElement | null = null;
constructor() {
const iframe = document.createElement('iframe');
@ -44,7 +44,7 @@ export default abstract class BaseScreen {
screen.className = styles.screen;
screen.appendChild(iframe);
screen.appendChild(overlay);
this._screen = screen;
this.screen = screen;
}
attach(parentElement: HTMLElement) {
@ -52,7 +52,7 @@ export default abstract class BaseScreen {
throw new Error("BaseScreen: Trying to attach an attached screen.");
}
parentElement.appendChild(this._screen);
parentElement.appendChild(this.screen);
this.parentElement = parentElement;
// parentElement.onresize = this.scale;
@ -115,29 +115,37 @@ export default abstract class BaseScreen {
return this.getElementsFromInternalPoint(this.getInternalCoordinates(point));
}
getElementBySelector(selector: string): Element | null {
return this.document?.querySelector(selector) || null;
}
display(flag: boolean = true) {
this._screen.style.display = flag ? '' : 'none';
this.screen.style.display = flag ? '' : 'none';
}
displayFrame(flag: boolean = true) {
this.iframe.style.display = flag ? '' : 'none';
}
private s: number = 1;
getScale() {
return this.s;
}
_scale() {
if (!this.parentElement) return;
let s = 1;
const { height, width } = getState();
const { offsetWidth, offsetHeight } = this.parentElement;
s = Math.min(offsetWidth / width, offsetHeight / height);
if (s > 1) {
s = 1;
this.s = Math.min(offsetWidth / width, offsetHeight / height);
if (this.s > 1) {
this.s = 1;
} else {
s = Math.round(s * 1e3) / 1e3;
this.s = Math.round(this.s * 1e3) / 1e3;
}
this._screen.style.transform = `scale(${ s }) translate(-50%, -50%)`;
this._screen.style.width = width + 'px';
this._screen.style.height = height + 'px';
this.screen.style.transform = `scale(${ this.s }) translate(-50%, -50%)`;
this.screen.style.width = width + 'px';
this.screen.style.height = height + 'px';
this.iframe.style.width = width + 'px';
this.iframe.style.height = height + 'px';

View file

@ -1,12 +1,29 @@
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen';
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen';
import { update, getState } from '../../store';
//export interface targetPosition
interface BoundingRect {
top: number,
left: number,
width: number,
height: number,
}
export interface MarkedTarget {
boundingRect: BoundingRect,
el: Element,
selector: string,
count: number,
}
export interface State extends SuperState {
messagesLoading: boolean,
cssLoading: boolean,
disconnected: boolean,
userPageLoading: boolean,
markedTargets: MarkedTarget[] | null
}
export const INITIAL_STATE: State = {
@ -15,40 +32,78 @@ export const INITIAL_STATE: State = {
cssLoading: false,
disconnected: false,
userPageLoading: false,
}
markedTargets: [],
};
export default class StatedScreen extends Screen {
constructor() { super(); }
setMessagesLoading(messagesLoading: boolean) {
// @ts-ignore
this.display(!messagesLoading);
update({ messagesLoading });
}
setCSSLoading(cssLoading: boolean) {
// @ts-ignore
this.displayFrame(!cssLoading);
update({ cssLoading });
}
setDisconnected(disconnected: boolean) {
if (!getState().live) return; //?
// @ts-ignore
this.display(!disconnected);
update({ disconnected });
}
setUserPageLoading(userPageLoading: boolean) {
// @ts-ignore
this.display(!userPageLoading);
update({ userPageLoading });
}
setSize({ height, width }: { height: number, width: number }) {
update({ width, height });
// @ts-ignore
this.scale();
const { markedTargets } = getState();
if (markedTargets) {
update({
markedTargets: markedTargets.map(mt => ({
...mt,
boundingRect: this.calculateRelativeBoundingRect(mt.el),
})),
});
}
}
private calculateRelativeBoundingRect(el: Element): BoundingRect {
if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO
const { top, left, width, height } = el.getBoundingClientRect();
const s = this.getScale();
const scrinRect = this.screen.getBoundingClientRect();
const parentRect = this.parentElement.getBoundingClientRect();
return {
top: top*s + scrinRect.top - parentRect.top,
left: left*s + scrinRect.left - parentRect.left,
width: width*s,
height: height*s,
}
}
setMarkedTargets(selections: { selector: string, count: number }[] | null) {
if (selections) {
const targets: MarkedTarget[] = [];
selections.forEach(s => {
const el = this.getElementBySelector(s.selector);
if (!el) return;
targets.push({
...s,
el,
boundingRect: this.calculateRelativeBoundingRect(el),
})
});
update({ markedTargets: targets });
} else {
update({ markedTargets: null });
}
}
}

View file

@ -1,6 +1,6 @@
import { goTo as listsGoTo } from './lists';
import { update, getState } from './store';
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './MessageDistributor';
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './MessageDistributor/MessageDistributor';
const fps = 60;
const performance = window.performance || { now: Date.now.bind(Date) };
@ -35,7 +35,7 @@ const initialSkipToIssue = !!localStorage.getItem(SKIP_TO_ISSUE_STORAGE_KEY);
const initialAutoplay = !!localStorage.getItem(AUTOPLAY_STORAGE_KEY);
const initialShowEvents = !!localStorage.getItem(SHOW_EVENTS_STORAGE_KEY);
export const INITIAL_STATE: SuperState = {
export const INITIAL_STATE = {
...SUPER_INITIAL_STATE,
time: 0,
playing: false,
@ -191,6 +191,11 @@ export default class Player extends MessageDistributor {
update({ inspectorMode: false });
}
}
markTargets(targets: { selector: string, count: number }[] | null) {
this.pause();
this.setMarkedTargets(targets);
}
toggleSkipToIssue() {
const skipToIssue = !getState().skipToIssue;

View file

@ -69,6 +69,7 @@ export const markElement = initCheck((...args) => instance.marker && instance.ma
export const scale = initCheck(() => instance.scale());
export const toggleInspectorMode = initCheck((...args) => instance.toggleInspectorMode(...args));
export const callPeer = initCheck((...args) => instance.assistManager.call(...args))
export const markTargets = initCheck((...args) => instance.markTargets(...args))
export const Controls = {
jump,