refactor(frontend/player):inspector responsibility segregation; renamings, types
This commit is contained in:
parent
094721684a
commit
c5209efd87
22 changed files with 476 additions and 522 deletions
|
|
@ -1,15 +1,9 @@
|
|||
import WebPlayer from './_web/WebPlayer';
|
||||
import reduxStore, {update, cleanStore} from './_store';
|
||||
|
||||
import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager'
|
||||
import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player'
|
||||
import { Store } from './player/types'
|
||||
|
||||
|
||||
const INIT_STATE = {
|
||||
...MM_INITIAL_STATE,
|
||||
...PLAYER_INITIAL_STATE,
|
||||
}
|
||||
import type { State as MMState } from './_web/MessageManager'
|
||||
import type { State as PState } from './player/Player'
|
||||
import type { Store } from './player/types'
|
||||
|
||||
|
||||
const myStore: Store<PState & MMState> = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { applyChange, revertChange } from 'deep-diff';
|
||||
|
||||
import { INITIAL_STATE as MM_INITIAL_STATE } from '../_web/MessageManager'
|
||||
import { INITIAL_STATE as PLAYER_INITIAL_STATE } from '../player/Player'
|
||||
import Player from '../player/Player'
|
||||
|
||||
const UPDATE = 'player/UPDATE';
|
||||
const CLEAN = 'player/CLEAN';
|
||||
|
|
@ -9,7 +9,7 @@ const REDUX = 'player/REDUX';
|
|||
|
||||
const resetState = {
|
||||
...MM_INITIAL_STATE,
|
||||
...PLAYER_INITIAL_STATE,
|
||||
...Player.INITIAL_STATE,
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
57
frontend/app/player/_web/InspectorController.ts
Normal file
57
frontend/app/player/_web/InspectorController.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import Marker from './Screen/Marker'
|
||||
import Inspector from './Screen/Inspector'
|
||||
import Screen from './Screen/Screen'
|
||||
import type { Dimensions } from './Screen/types'
|
||||
|
||||
export default class InspectorController {
|
||||
private substitutor: Screen | null = null
|
||||
private inspector: Inspector | null = null
|
||||
marker: Marker | null = null
|
||||
constructor(private screen: Screen) {}
|
||||
|
||||
scale(dims: Dimensions) {
|
||||
if (this.substitutor) {
|
||||
this.substitutor.scale(dims)
|
||||
}
|
||||
}
|
||||
|
||||
enableInspector(clickCallback: (e: { target: Element }) => void): Document | null {
|
||||
if (!this.screen.parentElement) return null;
|
||||
if (!this.substitutor) {
|
||||
this.substitutor = new Screen()
|
||||
this.marker = new Marker(this.substitutor.overlay, this.substitutor)
|
||||
this.inspector = new Inspector(this.substitutor, this.marker)
|
||||
//this.inspector.addClickListener(clickCallback, true)
|
||||
this.substitutor.attach(this.screen.parentElement)
|
||||
}
|
||||
|
||||
this.substitutor.display(false)
|
||||
|
||||
const docElement = this.screen.document?.documentElement // this.substitutor.document?.importNode(
|
||||
const doc = this.substitutor.document
|
||||
if (doc && docElement) {
|
||||
doc.open()
|
||||
doc.write(docElement.outerHTML)
|
||||
doc.close()
|
||||
|
||||
// TODO! : copy stylesheets & cssRules?
|
||||
}
|
||||
this.screen.display(false);
|
||||
this.inspector.enable(clickCallback);
|
||||
this.substitutor.display(true);
|
||||
return doc;
|
||||
}
|
||||
|
||||
disableInspector() {
|
||||
if (this.substitutor) {
|
||||
const doc = this.substitutor.document;
|
||||
if (doc) {
|
||||
doc.documentElement.innerHTML = "";
|
||||
}
|
||||
this.inspector.clean();
|
||||
this.substitutor.display(false);
|
||||
}
|
||||
this.screen.display(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const;
|
|||
|
||||
const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ];
|
||||
|
||||
// TODO: provide correct types
|
||||
// TODO: provide correct types; maybe use list object itself inside the store
|
||||
|
||||
export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => {
|
||||
state[`${name}List`] = []
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ const visualChanges = [
|
|||
]
|
||||
|
||||
export default class MessageManager extends Screen {
|
||||
// TODO: consistent with the other data-lists
|
||||
private locationEventManager: ListWalker<any>/*<LocationEvent>*/ = new ListWalker();
|
||||
private locationManager: ListWalker<SetPageLocation> = new ListWalker();
|
||||
private loadedLocationManager: ListWalker<SetPageLocation> = new ListWalker();
|
||||
|
|
@ -198,7 +197,7 @@ export default class MessageManager extends Screen {
|
|||
|
||||
private waitingForFiles: boolean = false
|
||||
private onFileReadSuccess = () => {
|
||||
const stateToUpdate = {
|
||||
const stateToUpdate : Partial<State>= {
|
||||
performanceChartData: this.performanceTrackManager.chartData,
|
||||
performanceAvaliability: this.performanceTrackManager.avaliability,
|
||||
...this.lists.getFullListsState()
|
||||
|
|
@ -349,7 +348,7 @@ export default class MessageManager extends Screen {
|
|||
stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time;
|
||||
}
|
||||
|
||||
this.lists.moveGetState(t)
|
||||
Object.assign(stateToUpdate, this.lists.moveGetState(t))
|
||||
Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate);
|
||||
|
||||
/* Sequence of the managers is important here */
|
||||
|
|
@ -535,15 +534,6 @@ export default class MessageManager extends Screen {
|
|||
}
|
||||
}
|
||||
|
||||
getLastMessageTime(): number {
|
||||
return this.lastMessageTime;
|
||||
}
|
||||
|
||||
getFirstMessageTime(): number {
|
||||
return this.pagesManager.minTime;
|
||||
}
|
||||
|
||||
|
||||
setMessagesLoading(messagesLoading: boolean) {
|
||||
this.display(!messagesLoading);
|
||||
this.state.update({ messagesLoading });
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
import styles from './screen.module.css';
|
||||
|
||||
import type { Point } from './types';
|
||||
|
||||
|
||||
export interface State {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
|
||||
|
||||
function getElementsFromInternalPoint(doc: Document, { x, y }: Point): Element[] {
|
||||
// @ts-ignore (IE, Edge)
|
||||
if (typeof doc.msElementsFromRect === 'function') {
|
||||
// @ts-ignore
|
||||
return Array.prototype.slice.call(doc.msElementsFromRect(x,y)) || []
|
||||
}
|
||||
|
||||
if (typeof doc.elementsFromPoint === 'function') {
|
||||
return doc.elementsFromPoint(x, y)
|
||||
}
|
||||
const el = doc.elementFromPoint(x, y)
|
||||
return el ? [ el ] : []
|
||||
}
|
||||
|
||||
function getElementsFromInternalPointDeep(doc: Document, point: Point): Element[] {
|
||||
const elements = getElementsFromInternalPoint(doc, point)
|
||||
// is it performant though??
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements[i]
|
||||
if (isIframe(el)){
|
||||
const iDoc = el.contentDocument
|
||||
if (iDoc) {
|
||||
const iPoint: Point = {
|
||||
x: point.x - el.clientLeft,
|
||||
y: point.y - el.clientTop,
|
||||
}
|
||||
elements.push(...getElementsFromInternalPointDeep(iDoc, iPoint))
|
||||
}
|
||||
}
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
function isIframe(el: Element): el is HTMLIFrameElement {
|
||||
return el.tagName === "IFRAME"
|
||||
}
|
||||
|
||||
export default abstract class BaseScreen {
|
||||
public readonly overlay: HTMLDivElement;
|
||||
|
||||
private readonly iframe: HTMLIFrameElement;
|
||||
protected readonly screen: HTMLDivElement;
|
||||
protected readonly controlButton: HTMLDivElement;
|
||||
protected parentElement: HTMLElement | null = null;
|
||||
|
||||
constructor() {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = styles.iframe;
|
||||
this.iframe = iframe;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = styles.overlay;
|
||||
this.overlay = overlay;
|
||||
|
||||
const screen = document.createElement('div');
|
||||
|
||||
screen.className = styles.screen;
|
||||
screen.appendChild(iframe);
|
||||
screen.appendChild(overlay);
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
attach(parentElement: HTMLElement) {
|
||||
if (this.parentElement) {
|
||||
this.parentElement = undefined
|
||||
console.error("BaseScreen: Trying to attach an attached screen.");
|
||||
}
|
||||
|
||||
parentElement.appendChild(this.screen);
|
||||
|
||||
this.parentElement = parentElement;
|
||||
|
||||
/* == For the Inspecting Document content == */
|
||||
this.overlay.addEventListener('contextmenu', () => {
|
||||
this.overlay.style.display = 'none'
|
||||
const doc = this.document
|
||||
if (!doc) { return }
|
||||
const returnOverlay = () => {
|
||||
this.overlay.style.display = 'block'
|
||||
doc.removeEventListener('mousemove', returnOverlay)
|
||||
doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection
|
||||
}
|
||||
doc.addEventListener('mousemove', returnOverlay)
|
||||
doc.addEventListener('mouseclick', returnOverlay)
|
||||
})
|
||||
}
|
||||
|
||||
toggleRemoteControlStatus(isEnabled: boolean ) {
|
||||
const styles = isEnabled ? { border: '2px dashed blue' } : { border: 'unset'}
|
||||
return Object.assign(this.screen.style, styles)
|
||||
}
|
||||
|
||||
get window(): WindowProxy | null {
|
||||
return this.iframe.contentWindow;
|
||||
}
|
||||
|
||||
get document(): Document | null {
|
||||
return this.iframe.contentDocument;
|
||||
}
|
||||
|
||||
private boundingRect: DOMRect | null = null;
|
||||
private getBoundingClientRect(): DOMRect {
|
||||
if (this.boundingRect === null) {
|
||||
return this.boundingRect = this.overlay.getBoundingClientRect() // expensive operation?
|
||||
}
|
||||
return this.boundingRect
|
||||
}
|
||||
|
||||
getInternalViewportCoordinates({ x, y }: Point): Point {
|
||||
const { x: overlayX, y: overlayY, width } = this.getBoundingClientRect();
|
||||
|
||||
const screenWidth = this.overlay.offsetWidth;
|
||||
|
||||
const scale = screenWidth / width;
|
||||
const screenX = (x - overlayX) * scale;
|
||||
const screenY = (y - overlayY) * scale;
|
||||
|
||||
return { x: Math.round(screenX), y: Math.round(screenY) };
|
||||
}
|
||||
|
||||
getCurrentScroll(): Point {
|
||||
const docEl = this.document?.documentElement
|
||||
const x = docEl ? docEl.scrollLeft : 0
|
||||
const y = docEl ? docEl.scrollTop : 0
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
getInternalCoordinates(p: Point): Point {
|
||||
const { x, y } = this.getInternalViewportCoordinates(p);
|
||||
|
||||
const sc = this.getCurrentScroll()
|
||||
|
||||
return { x: x+sc.x, y: y+sc.y };
|
||||
}
|
||||
|
||||
getElementFromInternalPoint({ x, y }: Point): Element | null {
|
||||
// elementFromPoint && elementFromPoints require viewpoint-related coordinates,
|
||||
// not document-related
|
||||
return this.document?.elementFromPoint(x, y) || null;
|
||||
}
|
||||
|
||||
getElementsFromInternalPoint(point: Point): Element[] {
|
||||
const doc = this.document
|
||||
if (!doc) { return [] }
|
||||
return getElementsFromInternalPointDeep(doc, point)
|
||||
}
|
||||
|
||||
getElementFromPoint(point: Point): Element | null {
|
||||
return this.getElementFromInternalPoint(this.getInternalViewportCoordinates(point));
|
||||
}
|
||||
|
||||
getElementsFromPoint(point: Point): Element[] {
|
||||
return this.getElementsFromInternalPoint(this.getInternalViewportCoordinates(point));
|
||||
}
|
||||
|
||||
getElementBySelector(selector: string): Element | null {
|
||||
if (!selector) return null;
|
||||
try {
|
||||
const safeSelector = selector.replace(/:/g, '\\\\3A ').replace(/\//g, '\\/');
|
||||
return this.document?.querySelector(safeSelector) || null;
|
||||
} catch (e) {
|
||||
console.error("Can not select element. ", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
display(flag: boolean = true) {
|
||||
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({ height, width }: { height: number, width: number }) {
|
||||
if (!this.parentElement) return;
|
||||
const { offsetWidth, offsetHeight } = this.parentElement;
|
||||
|
||||
this.s = Math.min(offsetWidth / width, offsetHeight / height);
|
||||
if (this.s > 1) {
|
||||
this.s = 1;
|
||||
} else {
|
||||
this.s = Math.round(this.s * 1e3) / 1e3;
|
||||
}
|
||||
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';
|
||||
|
||||
this.boundingRect = this.overlay.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,7 @@ import styles from './cursor.module.css';
|
|||
|
||||
export default class Cursor {
|
||||
private readonly cursor: HTMLDivElement;
|
||||
private nameElement: HTMLDivElement;
|
||||
private readonly position: Point = { x: -1, y: -1 }
|
||||
private tagElement: HTMLDivElement;
|
||||
constructor(overlay: HTMLDivElement) {
|
||||
this.cursor = document.createElement('div');
|
||||
this.cursor.className = styles.cursor;
|
||||
|
|
@ -20,10 +19,10 @@ export default class Cursor {
|
|||
}
|
||||
}
|
||||
|
||||
toggleUserName(name?: string) {
|
||||
if (!this.nameElement) {
|
||||
this.nameElement = document.createElement('div')
|
||||
Object.assign(this.nameElement.style, {
|
||||
showTag(tag?: string) {
|
||||
if (!this.tagElement) {
|
||||
this.tagElement = document.createElement('div')
|
||||
Object.assign(this.tagElement.style, {
|
||||
position: 'absolute',
|
||||
padding: '4px 6px',
|
||||
borderRadius: '8px',
|
||||
|
|
@ -34,21 +33,19 @@ export default class Cursor {
|
|||
fontSize: '12px',
|
||||
whiteSpace: 'nowrap',
|
||||
})
|
||||
this.cursor.appendChild(this.nameElement)
|
||||
this.cursor.appendChild(this.tagElement)
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
this.nameElement.style.display = 'none'
|
||||
if (!tag) {
|
||||
this.tagElement.style.display = 'none'
|
||||
} else {
|
||||
this.nameElement.style.display = 'block'
|
||||
const nameStr = name ? name.length > 10 ? name.slice(0, 9) + '...' : name : 'User'
|
||||
this.nameElement.innerHTML = `<span>${nameStr}</span>`
|
||||
this.tagElement.style.display = 'block'
|
||||
const nameStr = tag.length > 10 ? tag.slice(0, 9) + '...' : tag
|
||||
this.tagElement.innerHTML = `<span>${nameStr}</span>`
|
||||
}
|
||||
}
|
||||
|
||||
move({ x, y }: Point) {
|
||||
this.position.x = x;
|
||||
this.position.y = y;
|
||||
this.cursor.style.left = x + 'px';
|
||||
this.cursor.style.top = y + 'px';
|
||||
}
|
||||
|
|
@ -64,8 +61,4 @@ export default class Cursor {
|
|||
// transition
|
||||
// setTransitionSpeed()
|
||||
|
||||
getPosition(): Point {
|
||||
return { x: this.position.x, y: this.position.y };
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import type Screen from './Screen'
|
||||
import type Marker from './Marker'
|
||||
|
||||
//import { select } from 'optimal-select';
|
||||
|
||||
export default class Inspector {
|
||||
//private callbacks;
|
||||
captureCallbacks = [];
|
||||
bubblingCallbacks = [];
|
||||
constructor(screen, marker) {
|
||||
this.screen = screen;
|
||||
this.marker = marker;
|
||||
}
|
||||
export default class Inspector {
|
||||
// private captureCallbacks = [];
|
||||
// private bubblingCallbacks = [];
|
||||
constructor(private screen: Screen, private marker: Marker) {}
|
||||
|
||||
_onMouseMove = (e) => {
|
||||
private onMouseMove = (e: MouseEvent) => {
|
||||
// const { overlay } = this.screen;
|
||||
// if (!overlay.contains(e.target)) {
|
||||
// return;
|
||||
|
|
@ -25,11 +24,11 @@ export default class Inspector {
|
|||
this.marker.mark(target);
|
||||
}
|
||||
|
||||
_onOverlayLeave = () => {
|
||||
private onOverlayLeave = () => {
|
||||
return this.marker.unmark();
|
||||
}
|
||||
|
||||
_onMarkClick = () => {
|
||||
private onMarkClick = () => {
|
||||
let target = this.marker.target;
|
||||
if (!target) {
|
||||
return
|
||||
|
|
@ -57,16 +56,15 @@ export default class Inspector {
|
|||
// }
|
||||
// }
|
||||
|
||||
toggle(flag, clickCallback) {
|
||||
this.clickCallback = clickCallback;
|
||||
if (flag) {
|
||||
this.screen.overlay.addEventListener('mousemove', this._onMouseMove);
|
||||
this.screen.overlay.addEventListener('mouseleave', this._onOverlayLeave);
|
||||
this.screen.overlay.addEventListener('click', this._onMarkClick);
|
||||
} else {
|
||||
this.screen.overlay.removeEventListener('mousemove', this._onMouseMove);
|
||||
this.screen.overlay.removeEventListener('mouseleave', this._onOverlayLeave);
|
||||
this.screen.overlay.removeEventListener('click', this._onMarkClick);
|
||||
}
|
||||
private clickCallback: (e: { target: Element }) => void | null = null
|
||||
enable(clickCallback: Inspector['clickCallback']) {
|
||||
this.screen.overlay.addEventListener('mousemove', this.onMouseMove)
|
||||
this.screen.overlay.addEventListener('mouseleave', this.onOverlayLeave)
|
||||
this.screen.overlay.addEventListener('click', this.onMarkClick)
|
||||
}
|
||||
clean() {
|
||||
this.screen.overlay.removeEventListener('mousemove', this.onMouseMove)
|
||||
this.screen.overlay.removeEventListener('mouseleave', this.onOverlayLeave)
|
||||
this.screen.overlay.removeEventListener('click', this.onMarkClick)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type BaseScreen from './BaseScreen'
|
||||
import type Screen from './Screen'
|
||||
import styles from './marker.module.css';
|
||||
|
||||
function escapeRegExp(string: string) {
|
||||
|
|
@ -19,7 +19,7 @@ export default class Marker {
|
|||
private tooltip: HTMLDivElement
|
||||
private marker: HTMLDivElement
|
||||
|
||||
constructor(overlay: HTMLElement, private readonly screen: BaseScreen) {
|
||||
constructor(overlay: HTMLElement, private readonly screen: Screen) {
|
||||
this.tooltip = document.createElement('div');
|
||||
this.tooltip.className = styles.tooltip;
|
||||
this.tooltip.appendChild(document.createElement('div'));
|
||||
|
|
@ -74,13 +74,14 @@ export default class Marker {
|
|||
if (fitTargets.length === 0) {
|
||||
this._target = null;
|
||||
} else {
|
||||
this._target = fitTargets[0];
|
||||
const cursorTarget = this.screen.getCursorTarget();
|
||||
fitTargets.forEach((target) => {
|
||||
if (target.contains(cursorTarget)) {
|
||||
this._target = target;
|
||||
}
|
||||
});
|
||||
// TODO: fix getCursorTarget()?
|
||||
// this._target = fitTargets[0];
|
||||
// const cursorTarget = this.screen.getCursorTarget();
|
||||
// fitTargets.forEach((target) => {
|
||||
// if (target.contains(cursorTarget)) {
|
||||
// this._target = target;
|
||||
// }
|
||||
// });
|
||||
}
|
||||
} catch (e) {
|
||||
console.info(e);
|
||||
|
|
@ -96,9 +97,9 @@ export default class Marker {
|
|||
this.redraw();
|
||||
}
|
||||
|
||||
getTagString(tag) {
|
||||
const attrs = tag.attributes;
|
||||
let str = `<span style="color:#9BBBDC">${tag.tagName.toLowerCase()}</span>`;
|
||||
private getTagString(el: Element) {
|
||||
const attrs = el.attributes;
|
||||
let str = `<span style="color:#9BBBDC">${el.tagName.toLowerCase()}</span>`;
|
||||
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
let k = attrs[i];
|
||||
|
|
|
|||
|
|
@ -1,83 +1,215 @@
|
|||
import Marker from './Marker';
|
||||
import Cursor from './Cursor';
|
||||
import Inspector from './Inspector';
|
||||
// import styles from './screen.module.css';
|
||||
import BaseScreen from './BaseScreen';
|
||||
import styles from './screen.module.css'
|
||||
import Cursor from './Cursor'
|
||||
|
||||
export { INITIAL_STATE } from './BaseScreen';
|
||||
export type { State } from './BaseScreen';
|
||||
import type { Point, Dimensions } from './types';
|
||||
|
||||
export default class Screen extends BaseScreen {
|
||||
public readonly cursor: Cursor;
|
||||
private substitutor: BaseScreen | null = null;
|
||||
private inspector: Inspector | null = null;
|
||||
public marker: Marker | null = null;
|
||||
constructor() {
|
||||
super();
|
||||
this.cursor = new Cursor(this.overlay);
|
||||
|
||||
export type State = Dimensions
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
|
||||
|
||||
function getElementsFromInternalPoint(doc: Document, { x, y }: Point): Element[] {
|
||||
// @ts-ignore (IE, Edge)
|
||||
if (typeof doc.msElementsFromRect === 'function') {
|
||||
// @ts-ignore
|
||||
return Array.prototype.slice.call(doc.msElementsFromRect(x,y)) || []
|
||||
}
|
||||
|
||||
getCursorTarget() {
|
||||
return this.getElementFromInternalPoint(this.cursor.getPosition());
|
||||
if (typeof doc.elementsFromPoint === 'function') {
|
||||
return doc.elementsFromPoint(x, y)
|
||||
}
|
||||
const el = doc.elementFromPoint(x, y)
|
||||
return el ? [ el ] : []
|
||||
}
|
||||
|
||||
getCursorTargets() {
|
||||
return this.getElementsFromInternalPoint(this.cursor.getPosition());
|
||||
}
|
||||
|
||||
scale(dims: { height: number, width: number }) {
|
||||
super.scale(dims)
|
||||
if (this.substitutor) {
|
||||
this.substitutor.scale(dims)
|
||||
}
|
||||
}
|
||||
|
||||
enableInspector(clickCallback: (e: { target: Element }) => void): Document | null {
|
||||
if (!this.parentElement) return null;
|
||||
if (!this.substitutor) {
|
||||
this.substitutor = new Screen();
|
||||
this.marker = new Marker(this.substitutor.overlay, this.substitutor);
|
||||
this.inspector = new Inspector(this.substitutor, this.marker);
|
||||
//this.inspector.addClickListener(clickCallback, true);
|
||||
this.substitutor.attach(this.parentElement);
|
||||
}
|
||||
|
||||
this.substitutor.display(false);
|
||||
|
||||
const docElement = this.document?.documentElement; // this.substitutor.document?.importNode(
|
||||
const doc = this.substitutor.document;
|
||||
if (doc && docElement) {
|
||||
// doc.documentElement.innerHTML = "";
|
||||
// // Better way?
|
||||
// for (let i = 1; i < docElement.attributes.length; i++) {
|
||||
// const att = docElement.attributes[i];
|
||||
// doc.documentElement.setAttribute(att.name, att.value);
|
||||
// }
|
||||
// for (let i = 1; i < docElement.childNodes.length; i++) {
|
||||
// doc.documentElement.appendChild(docElement.childNodes[i].cloneNode(true));
|
||||
// }
|
||||
doc.open();
|
||||
doc.write(docElement.outerHTML); // Context will be iframe, so instanceof Element won't work
|
||||
doc.close();
|
||||
|
||||
// TODO! : copy stylesheets, check with styles
|
||||
}
|
||||
this.display(false);
|
||||
this.inspector.toggle(true, clickCallback);
|
||||
this.substitutor.display(true);
|
||||
return doc;
|
||||
}
|
||||
|
||||
disableInspector() {
|
||||
if (this.substitutor) {
|
||||
const doc = this.substitutor.document;
|
||||
if (doc) {
|
||||
doc.documentElement.innerHTML = "";
|
||||
function getElementsFromInternalPointDeep(doc: Document, point: Point): Element[] {
|
||||
const elements = getElementsFromInternalPoint(doc, point)
|
||||
// is it performant though??
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements[i]
|
||||
if (isIframe(el)){
|
||||
const iDoc = el.contentDocument
|
||||
if (iDoc) {
|
||||
const iPoint: Point = {
|
||||
x: point.x - el.clientLeft,
|
||||
y: point.y - el.clientTop,
|
||||
}
|
||||
elements.push(...getElementsFromInternalPointDeep(iDoc, iPoint))
|
||||
}
|
||||
this.inspector.toggle(false);
|
||||
this.substitutor.display(false);
|
||||
}
|
||||
this.display(true);
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
function isIframe(el: Element): el is HTMLIFrameElement {
|
||||
return el.tagName === "IFRAME"
|
||||
}
|
||||
|
||||
export default class Screen {
|
||||
readonly overlay: HTMLDivElement
|
||||
readonly cursor: Cursor
|
||||
|
||||
private readonly iframe: HTMLIFrameElement;
|
||||
protected readonly screen: HTMLDivElement;
|
||||
protected readonly controlButton: HTMLDivElement;
|
||||
protected parentElement: HTMLElement | null = null;
|
||||
|
||||
constructor() {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = styles.iframe;
|
||||
this.iframe = iframe;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = styles.overlay;
|
||||
this.overlay = overlay;
|
||||
|
||||
const screen = document.createElement('div');
|
||||
|
||||
screen.className = styles.screen;
|
||||
screen.appendChild(iframe);
|
||||
screen.appendChild(overlay);
|
||||
this.screen = screen;
|
||||
|
||||
this.cursor = new Cursor(this.overlay) // TODO: move outside
|
||||
}
|
||||
|
||||
}
|
||||
attach(parentElement: HTMLElement) {
|
||||
if (this.parentElement) {
|
||||
this.parentElement = undefined
|
||||
console.error("BaseScreen: Trying to attach an attached screen.");
|
||||
}
|
||||
|
||||
parentElement.appendChild(this.screen);
|
||||
|
||||
this.parentElement = parentElement;
|
||||
|
||||
/* == For the Inspecting Document content == */
|
||||
this.overlay.addEventListener('contextmenu', () => {
|
||||
this.overlay.style.display = 'none'
|
||||
const doc = this.document
|
||||
if (!doc) { return }
|
||||
const returnOverlay = () => {
|
||||
this.overlay.style.display = 'block'
|
||||
doc.removeEventListener('mousemove', returnOverlay)
|
||||
doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection
|
||||
}
|
||||
doc.addEventListener('mousemove', returnOverlay)
|
||||
doc.addEventListener('mouseclick', returnOverlay)
|
||||
})
|
||||
}
|
||||
|
||||
toggleBorder(isEnabled: boolean ) {
|
||||
const styles = isEnabled ? { border: '2px dashed blue' } : { border: 'unset'}
|
||||
return Object.assign(this.screen.style, styles)
|
||||
}
|
||||
|
||||
get window(): WindowProxy | null {
|
||||
return this.iframe.contentWindow;
|
||||
}
|
||||
|
||||
get document(): Document | null {
|
||||
return this.iframe.contentDocument;
|
||||
}
|
||||
|
||||
private boundingRect: DOMRect | null = null;
|
||||
private getBoundingClientRect(): DOMRect {
|
||||
if (this.boundingRect === null) {
|
||||
return this.boundingRect = this.overlay.getBoundingClientRect() // expensive operation?
|
||||
}
|
||||
return this.boundingRect
|
||||
}
|
||||
|
||||
getInternalViewportCoordinates({ x, y }: Point): Point {
|
||||
const { x: overlayX, y: overlayY, width } = this.getBoundingClientRect();
|
||||
|
||||
const screenWidth = this.overlay.offsetWidth;
|
||||
|
||||
const scale = screenWidth / width;
|
||||
const screenX = (x - overlayX) * scale;
|
||||
const screenY = (y - overlayY) * scale;
|
||||
|
||||
return { x: Math.round(screenX), y: Math.round(screenY) };
|
||||
}
|
||||
|
||||
getCurrentScroll(): Point {
|
||||
const docEl = this.document?.documentElement
|
||||
const x = docEl ? docEl.scrollLeft : 0
|
||||
const y = docEl ? docEl.scrollTop : 0
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
getInternalCoordinates(p: Point): Point {
|
||||
const { x, y } = this.getInternalViewportCoordinates(p);
|
||||
|
||||
const sc = this.getCurrentScroll()
|
||||
|
||||
return { x: x+sc.x, y: y+sc.y };
|
||||
}
|
||||
|
||||
getElementFromInternalPoint({ x, y }: Point): Element | null {
|
||||
// elementFromPoint && elementFromPoints require viewpoint-related coordinates,
|
||||
// not document-related
|
||||
return this.document?.elementFromPoint(x, y) || null;
|
||||
}
|
||||
|
||||
getElementsFromInternalPoint(point: Point): Element[] {
|
||||
const doc = this.document
|
||||
if (!doc) { return [] }
|
||||
return getElementsFromInternalPointDeep(doc, point)
|
||||
}
|
||||
|
||||
getElementFromPoint(point: Point): Element | null {
|
||||
return this.getElementFromInternalPoint(this.getInternalViewportCoordinates(point));
|
||||
}
|
||||
|
||||
getElementsFromPoint(point: Point): Element[] {
|
||||
return this.getElementsFromInternalPoint(this.getInternalViewportCoordinates(point));
|
||||
}
|
||||
|
||||
getElementBySelector(selector: string): Element | null {
|
||||
if (!selector) return null;
|
||||
try {
|
||||
const safeSelector = selector.replace(/:/g, '\\\\3A ').replace(/\//g, '\\/');
|
||||
return this.document?.querySelector(safeSelector) || null;
|
||||
} catch (e) {
|
||||
console.error("Can not select element. ", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
display(flag: boolean = true) {
|
||||
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({ height, width }: Dimensions) {
|
||||
if (!this.parentElement) return;
|
||||
const { offsetWidth, offsetHeight } = this.parentElement;
|
||||
|
||||
this.s = Math.min(offsetWidth / width, offsetHeight / height);
|
||||
if (this.s > 1) {
|
||||
this.s = 1;
|
||||
} else {
|
||||
this.s = Math.round(this.s * 1e3) / 1e3;
|
||||
}
|
||||
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';
|
||||
|
||||
this.boundingRect = this.overlay.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from './Screen';
|
||||
export * from './Screen';
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface Dimensions {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
23
frontend/app/player/_web/WebLivePlayer.ts
Normal file
23
frontend/app/player/_web/WebLivePlayer.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// import WebPlayer from './WebPlayer'
|
||||
// import AssistManager from './assist/AssistManager'
|
||||
|
||||
|
||||
// export default class WebLivePlayer extends WebPlayer {
|
||||
// assistManager: AssistManager // public so far
|
||||
// constructor(private wpState: Store<MMState & PlayerState>, session, config: RTCIceServer[]) {
|
||||
// super(wpState)
|
||||
// this.assistManager = new AssistManager(session, this.messageManager, config, wpState)
|
||||
// const endTime = !live && session.duration.valueOf()
|
||||
// wpState.update({
|
||||
// //@ts-ignore
|
||||
// initialized: true,
|
||||
// //@ts-ignore
|
||||
// session,
|
||||
|
||||
// live: true,
|
||||
// livePlay: true,
|
||||
// })
|
||||
|
||||
// this.assistManager.connect(session.agentToken)
|
||||
// }
|
||||
// }
|
||||
|
|
@ -2,19 +2,19 @@ import type { Store } from '../player/types'
|
|||
import Player, { State as PlayerState } from '../player/Player'
|
||||
|
||||
import MessageManager from './MessageManager'
|
||||
import InspectorController from './InspectorController'
|
||||
import AssistManager from './assist/AssistManager'
|
||||
import Screen from './Screen/Screen'
|
||||
import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './MessageManager'
|
||||
|
||||
|
||||
import type { State as MMState } from './MessageManager'
|
||||
|
||||
export default class WebPlayer extends Player {
|
||||
private readonly screen: Screen
|
||||
private readonly messageManager: MessageManager
|
||||
private readonly inspectorController: InspectorController
|
||||
protected readonly messageManager: MessageManager
|
||||
|
||||
assistManager: AssistManager // public so far
|
||||
|
||||
constructor(private wpState: Store<MMState & PlayerState>, session, config, live: boolean) {
|
||||
constructor(private wpState: Store<MMState & PlayerState>, session, config: RTCIceServer[], live: boolean) {
|
||||
// TODO: separate screen from manager
|
||||
const screen = new MessageManager(session, wpState, config, live)
|
||||
super(wpState, screen)
|
||||
|
|
@ -24,6 +24,8 @@ export default class WebPlayer extends Player {
|
|||
// TODO: separate LiveWebPlayer
|
||||
this.assistManager = new AssistManager(session, this.messageManager, config, wpState)
|
||||
|
||||
this.inspectorController = new InspectorController(screen)
|
||||
|
||||
|
||||
const endTime = !live && session.duration.valueOf()
|
||||
wpState.update({
|
||||
|
|
@ -50,9 +52,29 @@ export default class WebPlayer extends Player {
|
|||
scale = () => {
|
||||
const { width, height } = this.wpState.get()
|
||||
this.screen.scale({ width, height })
|
||||
this.inspectorController.scale({ width, height })
|
||||
|
||||
// this.updateMarketTargets() ??
|
||||
}
|
||||
|
||||
// Inspector & marker
|
||||
mark(e: Element) {
|
||||
this.screen.marker.mark(e)
|
||||
this.inspectorController.marker?.mark(e)
|
||||
}
|
||||
toggleInspectorMode(flag: boolean, clickCallback) {
|
||||
if (typeof flag !== 'boolean') {
|
||||
const { inspectorMode } = this.wpState.get()
|
||||
flag = !inspectorMode;
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
this.pause()
|
||||
this.wpState.update({ inspectorMode: true })
|
||||
return this.inspectorController.enableInspector(clickCallback);
|
||||
} else {
|
||||
this.inspectorController.disableInspector();
|
||||
this.wpState.update({ inspectorMode: false });
|
||||
}
|
||||
}
|
||||
|
||||
updateMarketTargets() {
|
||||
|
|
@ -109,7 +131,7 @@ export default class WebPlayer extends Player {
|
|||
}
|
||||
|
||||
// private actualScroll: Point | null = null
|
||||
setMarkedTargets(selections: { selector: string, count: number }[] | null) {
|
||||
private setMarkedTargets(selections: { selector: string, count: number }[] | null) {
|
||||
// if (selections) {
|
||||
// const totalCount = selections.reduce((a, b) => {
|
||||
// return a + b.count
|
||||
|
|
@ -132,7 +154,7 @@ export default class WebPlayer extends Player {
|
|||
// update({ markedTargets });
|
||||
// } else {
|
||||
// if (this.actualScroll) {
|
||||
// this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y)
|
||||
// this.screen.window?.scrollTo(this.actualScroll.x, this.actualScroll.y)
|
||||
// this.actualScroll = null
|
||||
// }
|
||||
// update({ markedTargets: null });
|
||||
|
|
@ -140,26 +162,11 @@ export default class WebPlayer extends Player {
|
|||
}
|
||||
|
||||
markTargets(targets: { selector: string, count: number }[] | null) {
|
||||
// this.animator.pause();
|
||||
// this.setMarkedTargets(targets);
|
||||
}
|
||||
|
||||
toggleInspectorMode(flag, clickCallback) {
|
||||
// if (typeof flag !== 'boolean') {
|
||||
// const { inspectorMode } = getState();
|
||||
// flag = !inspectorMode;
|
||||
// }
|
||||
|
||||
// if (flag) {
|
||||
// this.pause()
|
||||
// update({ inspectorMode: true });
|
||||
// return super.enableInspector(clickCallback);
|
||||
// } else {
|
||||
// super.disableInspector();
|
||||
// update({ inspectorMode: false });
|
||||
// }
|
||||
// this.pause();
|
||||
// this.setMarkedTargets(targets);
|
||||
}
|
||||
|
||||
// TODO
|
||||
async toggleTimetravel() {
|
||||
if (!this.wpState.get().liveTimeTravel) {
|
||||
return await this.messageManager.reloadWithUnprocessedFile()
|
||||
|
|
@ -167,7 +174,7 @@ export default class WebPlayer extends Player {
|
|||
}
|
||||
|
||||
toggleUserName(name?: string) {
|
||||
this.screen.cursor.toggleUserName(name)
|
||||
this.screen.cursor.showTag(name)
|
||||
}
|
||||
clean() {
|
||||
super.clean()
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export default class AssistManager {
|
|||
constructor(
|
||||
private session: any,
|
||||
private md: MessageManager,
|
||||
private config: any,
|
||||
private config: RTCIceServer[],
|
||||
private store: Store<State>,
|
||||
) {}
|
||||
|
||||
|
|
@ -302,13 +302,13 @@ export default class AssistManager {
|
|||
this.md.overlay.addEventListener("mousemove", this.onMouseMove)
|
||||
this.md.overlay.addEventListener("click", this.onMouseClick)
|
||||
this.md.overlay.addEventListener("wheel", this.onWheel)
|
||||
this.md.toggleRemoteControlStatus(true)
|
||||
this.md.toggleBorder(true)
|
||||
this.store.update({ remoteControl: RemoteControlStatus.Enabled })
|
||||
} else {
|
||||
this.md.overlay.removeEventListener("mousemove", this.onMouseMove)
|
||||
this.md.overlay.removeEventListener("click", this.onMouseClick)
|
||||
this.md.overlay.removeEventListener("wheel", this.onWheel)
|
||||
this.md.toggleRemoteControlStatus(false)
|
||||
this.md.toggleBorder(false)
|
||||
this.store.update({ remoteControl: RemoteControlStatus.Disabled })
|
||||
this.toggleAnnotation(false)
|
||||
}
|
||||
|
|
@ -354,7 +354,7 @@ export default class AssistManager {
|
|||
const urlObject = new URL(window.env.API_EDP || window.location.origin)
|
||||
return import('peerjs').then(({ default: Peer }) => {
|
||||
if (this.cleaned) {return Promise.reject("Already cleaned")}
|
||||
const peerOpts: any = {
|
||||
const peerOpts: Peer.PeerJSOption = {
|
||||
host: urlObject.hostname,
|
||||
path: '/assist',
|
||||
port: urlObject.port === "" ? (location.protocol === 'https:' ? 443 : 80 ): parseInt(urlObject.port),
|
||||
|
|
@ -362,6 +362,7 @@ export default class AssistManager {
|
|||
if (this.config) {
|
||||
peerOpts['config'] = {
|
||||
iceServers: this.config,
|
||||
//@ts-ignore
|
||||
sdpSemantics: 'unified-plan',
|
||||
iceTransportPolicy: 'relay',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class SkipIntervalCls {
|
|||
get time(): number {
|
||||
return this.start;
|
||||
}
|
||||
contains(ts) {
|
||||
contains(ts: number) {
|
||||
return ts > this.start && ts < this.end;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,50 @@
|
|||
import type Screen from '../Screen/Screen';
|
||||
import type { MouseMove } from '../messages';
|
||||
import type Screen from '../Screen/Screen'
|
||||
import type { Point } from '../Screen/types'
|
||||
import type { MouseMove } from '../messages'
|
||||
|
||||
import ListWalker from '../../_common/ListWalker';
|
||||
import ListWalker from '../../_common/ListWalker'
|
||||
|
||||
const HOVER_CLASS = "-openreplay-hover";
|
||||
const HOVER_CLASS_DEPR = "-asayer-hover";
|
||||
|
||||
export default class MouseMoveManager extends ListWalker<MouseMove> {
|
||||
private hoverElements: Array<Element> = [];
|
||||
private hoverElements: Array<Element> = []
|
||||
|
||||
constructor(private screen: Screen) {super()}
|
||||
|
||||
// private getCursorTarget() {
|
||||
// return this.screen.getElementFromInternalPoint(this.current)
|
||||
// }
|
||||
|
||||
private getCursorTargets() {
|
||||
return this.screen.getElementsFromInternalPoint(this.current)
|
||||
}
|
||||
|
||||
private updateHover(): void {
|
||||
const curHoverElements = this.screen.getCursorTargets();
|
||||
const diffAdd = curHoverElements.filter(elem => !this.hoverElements.includes(elem));
|
||||
const diffRemove = this.hoverElements.filter(elem => !curHoverElements.includes(elem));
|
||||
this.hoverElements = curHoverElements;
|
||||
const curHoverElements = this.getCursorTargets()
|
||||
const diffAdd = curHoverElements.filter(elem => !this.hoverElements.includes(elem))
|
||||
const diffRemove = this.hoverElements.filter(elem => !curHoverElements.includes(elem))
|
||||
this.hoverElements = curHoverElements
|
||||
diffAdd.forEach(elem => {
|
||||
elem.classList.add(HOVER_CLASS)
|
||||
elem.classList.add(HOVER_CLASS_DEPR)
|
||||
});
|
||||
})
|
||||
diffRemove.forEach(elem => {
|
||||
elem.classList.remove(HOVER_CLASS)
|
||||
elem.classList.remove(HOVER_CLASS_DEPR)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.hoverElements = [];
|
||||
this.hoverElements.length = 0
|
||||
}
|
||||
|
||||
move(t: number) {
|
||||
const lastMouseMove = this.moveGetLast(t);
|
||||
if (!!lastMouseMove){
|
||||
this.screen.cursor.move(lastMouseMove);
|
||||
const lastMouseMove = this.moveGetLast(t)
|
||||
if (!!lastMouseMove) {
|
||||
this.screen.cursor.move(lastMouseMove)
|
||||
//window.getComputedStyle(this.screen.getCursorTarget()).cursor === 'pointer' // might nfluence performance though
|
||||
this.updateHover();
|
||||
this.updateHover()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
// import { applyChange, revertChange } from 'deep-diff';
|
||||
// import ListWalker from '../../_common/ListWalker';
|
||||
// import type { Redux } from '../messages';
|
||||
|
||||
// export default class ReduxStateManager extends ListWalker<Redux> {
|
||||
// private state: Object = {}
|
||||
// private finalStates: Object[] = []
|
||||
|
||||
// moveWasUpdated(time, index) {
|
||||
// super.moveApply(
|
||||
// time,
|
||||
// this.onIncrement,
|
||||
// this.onDecrement,
|
||||
// )
|
||||
// }
|
||||
|
||||
// onIncrement = (item) => {
|
||||
// this.processRedux(item, true);
|
||||
// }
|
||||
|
||||
// onDecrement = (item) => {
|
||||
// this.processRedux(item, false);
|
||||
// }
|
||||
|
||||
// private processRedux(action, forward) {
|
||||
// if (forward) {
|
||||
// if (!!action.state) {
|
||||
// this.finalStates.push(this.state);
|
||||
// this.state = JSON.parse(JSON.stringify(action.state)); // Deep clone :(
|
||||
// } else {
|
||||
// action.diff.forEach(d => {
|
||||
// try {
|
||||
// applyChange(this.state, d);
|
||||
// } catch (e) {
|
||||
// //console.warn("Deepdiff error")
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// if (!!action.state) {
|
||||
// this.state = this.finalStates.pop();
|
||||
// } else {
|
||||
// action.diff.forEach(d => {
|
||||
// try {
|
||||
// revertChange(this.state, 1, d); // bad lib :( TODO: write our own diff
|
||||
// } catch (e) {
|
||||
// //console.warn("Deepdiff error")
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,25 +1,33 @@
|
|||
import SimpleStore from './_common/SimpleStore'
|
||||
import type { Store } from './player/types'
|
||||
import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager'
|
||||
import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player'
|
||||
import Player, { State as PState } from './player/Player'
|
||||
|
||||
import WebPlayer from './_web/WebPlayer'
|
||||
|
||||
export function createWebPlayer(session, config): [WebPlayer, Store<PState & MMState>] {
|
||||
const store = new SimpleStore<PState & MMState>({
|
||||
...PLAYER_INITIAL_STATE,
|
||||
type WebPlayerStore = Store<PState & MMState>
|
||||
|
||||
export function createWebPlayer(session, wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] {
|
||||
let store: WebPlayerStore = new SimpleStore<PState & MMState>({
|
||||
...Player.INITIAL_STATE,
|
||||
...MM_INITIAL_STATE,
|
||||
})
|
||||
const player = new WebPlayer(store, session, config, false)
|
||||
if (wrapStore) {
|
||||
store = wrapStore(store)
|
||||
}
|
||||
const player = new WebPlayer(store, session, null, false)
|
||||
return [player, store]
|
||||
}
|
||||
|
||||
|
||||
export function createLiveWebPlayer(session, config): [WebPlayer, Store<PState & MMState>] {
|
||||
const store = new SimpleStore<PState & MMState>({
|
||||
...PLAYER_INITIAL_STATE,
|
||||
export function createLiveWebPlayer(session, config: RTCIceServer[], wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] {
|
||||
let store: WebPlayerStore = new SimpleStore<PState & MMState>({
|
||||
...Player.INITIAL_STATE,
|
||||
...MM_INITIAL_STATE,
|
||||
})
|
||||
if (wrapStore) {
|
||||
store = wrapStore(store)
|
||||
}
|
||||
const player = new WebPlayer(store, session, config, true)
|
||||
return [player, store]
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Store, Mover, Interval } from './types';
|
||||
import type { Store, Moveable, Interval } from './types';
|
||||
import * as localStorage from './localStorage';
|
||||
|
||||
const fps = 60
|
||||
|
|
@ -25,7 +25,6 @@ export interface SetState {
|
|||
time: number
|
||||
playing: boolean
|
||||
completed: boolean
|
||||
endTime: number
|
||||
live: boolean
|
||||
livePlay: boolean
|
||||
}
|
||||
|
|
@ -34,27 +33,27 @@ export interface GetState extends SetState {
|
|||
skip: boolean
|
||||
speed: number
|
||||
skipIntervals: Interval[]
|
||||
lastMessageTime: number
|
||||
endTime: number
|
||||
ready: boolean
|
||||
|
||||
lastMessageTime: number
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: SetState = {
|
||||
time: 0,
|
||||
playing: false,
|
||||
completed: false,
|
||||
endTime: 0,
|
||||
live: false,
|
||||
livePlay: false,
|
||||
} as const
|
||||
|
||||
|
||||
export default class Animator {
|
||||
static INITIAL_STATE: SetState = {
|
||||
time: 0,
|
||||
playing: false,
|
||||
completed: false,
|
||||
live: false,
|
||||
livePlay: false,
|
||||
} as const
|
||||
|
||||
private animationFrameRequestId: number = 0
|
||||
|
||||
constructor(private state: Store<GetState, SetState>, private mm: Mover) {}
|
||||
constructor(private store: Store<GetState>, private mm: Moveable) {}
|
||||
|
||||
private setTime(time: number) {
|
||||
this.state.update({
|
||||
this.store.update({
|
||||
time,
|
||||
completed: false,
|
||||
})
|
||||
|
|
@ -62,7 +61,7 @@ export default class Animator {
|
|||
}
|
||||
|
||||
private startAnimation() {
|
||||
let prevTime = this.state.get().time
|
||||
let prevTime = this.store.get().time
|
||||
let animationPrevTime = performance.now()
|
||||
|
||||
const frameHandler = (animationCurrentTime: number) => {
|
||||
|
|
@ -74,8 +73,9 @@ export default class Animator {
|
|||
live,
|
||||
livePlay,
|
||||
ready, // = messagesLoading || cssLoading || disconnected
|
||||
lastMessageTime, // should be updated
|
||||
} = this.state.get()
|
||||
|
||||
lastMessageTime,
|
||||
} = this.store.get()
|
||||
|
||||
const diffTime = !ready
|
||||
? 0
|
||||
|
|
@ -83,20 +83,22 @@ export default class Animator {
|
|||
|
||||
let time = prevTime + diffTime
|
||||
|
||||
const skipInterval = skip && skipIntervals.find(si => si.contains(time)) // TODO: good skip by messages
|
||||
const skipInterval = skip && skipIntervals.find(si => si.contains(time))
|
||||
if (skipInterval) time = skipInterval.end
|
||||
|
||||
if (time < 0) { time = 0 } // ?
|
||||
//const fmt = getFirstMessageTime();
|
||||
//if (time < fmt) time = fmt; // ?
|
||||
|
||||
|
||||
// if (livePlay && time < endTime) { time = endTime }
|
||||
// === live only
|
||||
if (livePlay && time < lastMessageTime) { time = lastMessageTime }
|
||||
if (endTime < lastMessageTime) {
|
||||
this.state.update({
|
||||
this.store.update({
|
||||
endTime: lastMessageTime,
|
||||
})
|
||||
}
|
||||
// ===
|
||||
|
||||
prevTime = time
|
||||
animationPrevTime = animationCurrentTime
|
||||
|
|
@ -104,17 +106,20 @@ export default class Animator {
|
|||
const completed = !live && time >= endTime
|
||||
if (completed) {
|
||||
this.setTime(endTime)
|
||||
return this.state.update({
|
||||
return this.store.update({
|
||||
playing: false,
|
||||
completed: true,
|
||||
})
|
||||
}
|
||||
|
||||
// === live only
|
||||
if (live && time > endTime) {
|
||||
this.state.update({
|
||||
this.store.update({
|
||||
endTime: time,
|
||||
})
|
||||
}
|
||||
// ===
|
||||
|
||||
this.setTime(time)
|
||||
this.animationFrameRequestId = requestAnimationFrame(frameHandler)
|
||||
}
|
||||
|
|
@ -123,17 +128,17 @@ export default class Animator {
|
|||
|
||||
play() {
|
||||
cancelAnimationFrame(this.animationFrameRequestId)
|
||||
this.state.update({ playing: true })
|
||||
this.store.update({ playing: true })
|
||||
this.startAnimation()
|
||||
}
|
||||
|
||||
pause() {
|
||||
cancelAnimationFrame(this.animationFrameRequestId)
|
||||
this.state.update({ playing: false })
|
||||
this.store.update({ playing: false })
|
||||
}
|
||||
|
||||
togglePlay() {
|
||||
const { playing, completed } = this.state.get()
|
||||
const { playing, completed } = this.store.get()
|
||||
if (playing) {
|
||||
this.pause()
|
||||
} else if (completed) {
|
||||
|
|
@ -146,26 +151,26 @@ export default class Animator {
|
|||
|
||||
// jump by index?
|
||||
jump(time: number) {
|
||||
const { live } = this.state.get()
|
||||
const { live } = this.store.get()
|
||||
if (live) return
|
||||
|
||||
if (this.state.get().playing) {
|
||||
if (this.store.get().playing) {
|
||||
cancelAnimationFrame(this.animationFrameRequestId)
|
||||
this.setTime(time)
|
||||
this.startAnimation()
|
||||
this.state.update({ livePlay: time === this.state.get().endTime })
|
||||
this.store.update({ livePlay: time === this.store.get().endTime })
|
||||
} else {
|
||||
this.setTime(time)
|
||||
this.state.update({ livePlay: time === this.state.get().endTime })
|
||||
this.store.update({ livePlay: time === this.store.get().endTime })
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: clearify logic of live time-travel
|
||||
jumpToLive() {
|
||||
cancelAnimationFrame(this.animationFrameRequestId)
|
||||
this.setTime(this.state.get().endTime)
|
||||
this.setTime(this.store.get().endTime)
|
||||
this.startAnimation()
|
||||
this.state.update({ livePlay: true })
|
||||
this.store.update({ livePlay: true })
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import * as typedLocalStorage from './localStorage';
|
||||
|
||||
import type { Mover, Cleaner, Store } from './types';
|
||||
import type { Moveable, Cleanable, Store } from './types';
|
||||
import Animator from './Animator';
|
||||
import { INITIAL_STATE as ANIMATOR_INITIAL_STATE } from './Animator';
|
||||
import type { GetState as AnimatorGetState, SetState as AnimatorSetState } from './Animator';
|
||||
|
||||
|
||||
|
|
@ -19,21 +18,22 @@ const initialSkip = typedLocalStorage.boolean(SKIP_STORAGE_KEY)
|
|||
const initialSkipToIssue = typedLocalStorage.boolean(SKIP_TO_ISSUE_STORAGE_KEY)
|
||||
const initialAutoplay = typedLocalStorage.boolean(AUTOPLAY_STORAGE_KEY)
|
||||
const initialShowEvents = typedLocalStorage.boolean(SHOW_EVENTS_STORAGE_KEY)
|
||||
export const INITIAL_STATE = {
|
||||
...ANIMATOR_INITIAL_STATE,
|
||||
|
||||
skipToIssue: initialSkipToIssue,
|
||||
showEvents: initialShowEvents,
|
||||
|
||||
autoplay: initialAutoplay,
|
||||
skip: initialSkip,
|
||||
speed: initialSpeed,
|
||||
}
|
||||
export type State = typeof INITIAL_STATE & AnimatorGetState
|
||||
export type State = typeof Player.INITIAL_STATE
|
||||
/* == */
|
||||
|
||||
export default class Player extends Animator {
|
||||
constructor(private pState: Store<State>, private manager: Mover & Cleaner) {
|
||||
static INITIAL_STATE = {
|
||||
...Animator.INITIAL_STATE,
|
||||
skipToIssue: initialSkipToIssue,
|
||||
showEvents: initialShowEvents,
|
||||
|
||||
autoplay: initialAutoplay,
|
||||
skip: initialSkip,
|
||||
speed: initialSpeed,
|
||||
} as const
|
||||
|
||||
constructor(private pState: Store<State & AnimatorGetState>, private manager: Moveable & Cleanable) {
|
||||
super(pState, manager)
|
||||
|
||||
// Autoplay
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
export interface Mover {
|
||||
export interface Moveable {
|
||||
move(time: number): void
|
||||
}
|
||||
|
||||
export interface Cleaner {
|
||||
export interface Cleanable {
|
||||
clean(): void
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue