refactor(frontend/player):inspector responsibility segregation; renamings, types

This commit is contained in:
Alex Kaminskii 2022-11-22 12:06:31 +01:00
parent 094721684a
commit c5209efd87
22 changed files with 476 additions and 522 deletions

View file

@ -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> = {

View file

@ -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,
};

View 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);
}
}

View file

@ -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`] = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
}
getCursorTarget() {
return this.getElementFromInternalPoint(this.cursor.getPosition());
}
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 = "";
}
this.inspector.toggle(false);
this.substitutor.display(false);
}
this.display(true);
}
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)) || []
}
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 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();
}
}

View file

@ -1,2 +0,0 @@
export { default } from './Screen';
export * from './Screen';

View file

@ -1,4 +1,9 @@
export interface Point {
x: number;
y: number;
x: number
y: number
}
export interface Dimensions {
width: number
height: number
}

View 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)
// }
// }

View file

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

View file

@ -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',
};

View file

@ -7,7 +7,7 @@ class SkipIntervalCls {
get time(): number {
return this.start;
}
contains(ts) {
contains(ts: number) {
return ts > this.start && ts < this.end;
}
}

View file

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

View file

@ -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")
// }
// });
// }
// }
// }
// }

View file

@ -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]
}

View file

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

View file

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

View file

@ -1,9 +1,9 @@
export interface Mover {
export interface Moveable {
move(time: number): void
}
export interface Cleaner {
export interface Cleanable {
clean(): void
}