From c5209efd879cb5ac3cbc03d4ce59f8d855252c92 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 12:06:31 +0100 Subject: [PATCH] refactor(frontend/player):inspector responsibility segregation; renamings, types --- frontend/app/player/_singletone.ts | 12 +- frontend/app/player/_store/duck.js | 4 +- .../app/player/_web/InspectorController.ts | 57 ++++ frontend/app/player/_web/Lists.ts | 2 +- frontend/app/player/_web/MessageManager.ts | 14 +- frontend/app/player/_web/Screen/BaseScreen.ts | 214 -------------- frontend/app/player/_web/Screen/Cursor.ts | 29 +- .../Screen/{Inspector.js => Inspector.ts} | 42 ++- frontend/app/player/_web/Screen/Marker.ts | 25 +- frontend/app/player/_web/Screen/Screen.ts | 278 +++++++++++++----- frontend/app/player/_web/Screen/index.js | 2 - frontend/app/player/_web/Screen/types.ts | 9 +- frontend/app/player/_web/WebLivePlayer.ts | 23 ++ frontend/app/player/_web/WebPlayer.ts | 61 ++-- .../app/player/_web/assist/AssistManager.ts | 9 +- .../player/_web/managers/ActivityManager.ts | 2 +- .../player/_web/managers/MouseMoveManager.ts | 39 ++- .../player/_web/managers/ReduxStateManager.ts | 53 ---- frontend/app/player/create.ts | 24 +- frontend/app/player/player/Animator.ts | 69 +++-- frontend/app/player/player/Player.ts | 26 +- frontend/app/player/player/types.ts | 4 +- 22 files changed, 476 insertions(+), 522 deletions(-) create mode 100644 frontend/app/player/_web/InspectorController.ts delete mode 100644 frontend/app/player/_web/Screen/BaseScreen.ts rename frontend/app/player/_web/Screen/{Inspector.js => Inspector.ts} (55%) delete mode 100644 frontend/app/player/_web/Screen/index.js create mode 100644 frontend/app/player/_web/WebLivePlayer.ts delete mode 100644 frontend/app/player/_web/managers/ReduxStateManager.ts diff --git a/frontend/app/player/_singletone.ts b/frontend/app/player/_singletone.ts index 71f29a7ee..c4e0d607b 100644 --- a/frontend/app/player/_singletone.ts +++ b/frontend/app/player/_singletone.ts @@ -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 = { diff --git a/frontend/app/player/_store/duck.js b/frontend/app/player/_store/duck.js index bcede3b32..6cf861ade 100644 --- a/frontend/app/player/_store/duck.js +++ b/frontend/app/player/_store/duck.js @@ -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, }; diff --git a/frontend/app/player/_web/InspectorController.ts b/frontend/app/player/_web/InspectorController.ts new file mode 100644 index 000000000..a78bad006 --- /dev/null +++ b/frontend/app/player/_web/InspectorController.ts @@ -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); + } + +} \ No newline at end of file diff --git a/frontend/app/player/_web/Lists.ts b/frontend/app/player/_web/Lists.ts index 4f9c236cd..4eb5de6db 100644 --- a/frontend/app/player/_web/Lists.ts +++ b/frontend/app/player/_web/Lists.ts @@ -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`] = [] diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts index e6dc9467c..14642c3dc 100644 --- a/frontend/app/player/_web/MessageManager.ts +++ b/frontend/app/player/_web/MessageManager.ts @@ -93,7 +93,6 @@ const visualChanges = [ ] export default class MessageManager extends Screen { - // TODO: consistent with the other data-lists private locationEventManager: ListWalker/**/ = new ListWalker(); private locationManager: ListWalker = new ListWalker(); private loadedLocationManager: ListWalker = new ListWalker(); @@ -198,7 +197,7 @@ export default class MessageManager extends Screen { private waitingForFiles: boolean = false private onFileReadSuccess = () => { - const stateToUpdate = { + const stateToUpdate : Partial= { 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 }); diff --git a/frontend/app/player/_web/Screen/BaseScreen.ts b/frontend/app/player/_web/Screen/BaseScreen.ts deleted file mode 100644 index 67f527e01..000000000 --- a/frontend/app/player/_web/Screen/BaseScreen.ts +++ /dev/null @@ -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(); - } -} diff --git a/frontend/app/player/_web/Screen/Cursor.ts b/frontend/app/player/_web/Screen/Cursor.ts index 54ea414fd..4d8094b4e 100644 --- a/frontend/app/player/_web/Screen/Cursor.ts +++ b/frontend/app/player/_web/Screen/Cursor.ts @@ -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 = `${nameStr}` + this.tagElement.style.display = 'block' + const nameStr = tag.length > 10 ? tag.slice(0, 9) + '...' : tag + this.tagElement.innerHTML = `${nameStr}` } } 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 }; - } - } diff --git a/frontend/app/player/_web/Screen/Inspector.js b/frontend/app/player/_web/Screen/Inspector.ts similarity index 55% rename from frontend/app/player/_web/Screen/Inspector.js rename to frontend/app/player/_web/Screen/Inspector.ts index 98ba5ec0f..b0ea1ca9c 100644 --- a/frontend/app/player/_web/Screen/Inspector.js +++ b/frontend/app/player/_web/Screen/Inspector.ts @@ -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) } } \ No newline at end of file diff --git a/frontend/app/player/_web/Screen/Marker.ts b/frontend/app/player/_web/Screen/Marker.ts index 4d3fab9b1..331995782 100644 --- a/frontend/app/player/_web/Screen/Marker.ts +++ b/frontend/app/player/_web/Screen/Marker.ts @@ -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 = `${tag.tagName.toLowerCase()}`; + private getTagString(el: Element) { + const attrs = el.attributes; + let str = `${el.tagName.toLowerCase()}`; for (let i = 0; i < attrs.length; i++) { let k = attrs[i]; diff --git a/frontend/app/player/_web/Screen/Screen.ts b/frontend/app/player/_web/Screen/Screen.ts index 59986423e..6d28f06fd 100644 --- a/frontend/app/player/_web/Screen/Screen.ts +++ b/frontend/app/player/_web/Screen/Screen.ts @@ -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 } -} \ No newline at end of file + 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(); + } +} diff --git a/frontend/app/player/_web/Screen/index.js b/frontend/app/player/_web/Screen/index.js deleted file mode 100644 index 96f315c68..000000000 --- a/frontend/app/player/_web/Screen/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Screen'; -export * from './Screen'; diff --git a/frontend/app/player/_web/Screen/types.ts b/frontend/app/player/_web/Screen/types.ts index 0fdac4d8b..2c464fdfc 100644 --- a/frontend/app/player/_web/Screen/types.ts +++ b/frontend/app/player/_web/Screen/types.ts @@ -1,4 +1,9 @@ export interface Point { - x: number; - y: number; + x: number + y: number +} + +export interface Dimensions { + width: number + height: number } \ No newline at end of file diff --git a/frontend/app/player/_web/WebLivePlayer.ts b/frontend/app/player/_web/WebLivePlayer.ts new file mode 100644 index 000000000..d7b707b1c --- /dev/null +++ b/frontend/app/player/_web/WebLivePlayer.ts @@ -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, 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) +// } +// } \ No newline at end of file diff --git a/frontend/app/player/_web/WebPlayer.ts b/frontend/app/player/_web/WebPlayer.ts index a48577288..de3dcfa49 100644 --- a/frontend/app/player/_web/WebPlayer.ts +++ b/frontend/app/player/_web/WebPlayer.ts @@ -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, session, config, live: boolean) { + constructor(private wpState: Store, 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() diff --git a/frontend/app/player/_web/assist/AssistManager.ts b/frontend/app/player/_web/assist/AssistManager.ts index 1e1274226..1c9984b51 100644 --- a/frontend/app/player/_web/assist/AssistManager.ts +++ b/frontend/app/player/_web/assist/AssistManager.ts @@ -78,7 +78,7 @@ export default class AssistManager { constructor( private session: any, private md: MessageManager, - private config: any, + private config: RTCIceServer[], private store: Store, ) {} @@ -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', }; diff --git a/frontend/app/player/_web/managers/ActivityManager.ts b/frontend/app/player/_web/managers/ActivityManager.ts index 32265f924..d5e81f62d 100644 --- a/frontend/app/player/_web/managers/ActivityManager.ts +++ b/frontend/app/player/_web/managers/ActivityManager.ts @@ -7,7 +7,7 @@ class SkipIntervalCls { get time(): number { return this.start; } - contains(ts) { + contains(ts: number) { return ts > this.start && ts < this.end; } } diff --git a/frontend/app/player/_web/managers/MouseMoveManager.ts b/frontend/app/player/_web/managers/MouseMoveManager.ts index 5dd3160a5..63b5ca895 100644 --- a/frontend/app/player/_web/managers/MouseMoveManager.ts +++ b/frontend/app/player/_web/managers/MouseMoveManager.ts @@ -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 { - private hoverElements: Array = []; + private hoverElements: Array = [] 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() } } } diff --git a/frontend/app/player/_web/managers/ReduxStateManager.ts b/frontend/app/player/_web/managers/ReduxStateManager.ts deleted file mode 100644 index 4756b303a..000000000 --- a/frontend/app/player/_web/managers/ReduxStateManager.ts +++ /dev/null @@ -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 { -// 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") -// } -// }); -// } -// } -// } -// } diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index c42815eae..4a928e20f 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -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] { - const store = new SimpleStore({ - ...PLAYER_INITIAL_STATE, +type WebPlayerStore = Store + +export function createWebPlayer(session, wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] { + let store: WebPlayerStore = new SimpleStore({ + ...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] { - const store = new SimpleStore({ - ...PLAYER_INITIAL_STATE, +export function createLiveWebPlayer(session, config: RTCIceServer[], wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] { + let store: WebPlayerStore = new SimpleStore({ + ...Player.INITIAL_STATE, ...MM_INITIAL_STATE, }) + if (wrapStore) { + store = wrapStore(store) + } const player = new WebPlayer(store, session, config, true) return [player, store] } \ No newline at end of file diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts index e8b8b7279..871da629a 100644 --- a/frontend/app/player/player/Animator.ts +++ b/frontend/app/player/player/Animator.ts @@ -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, private mm: Mover) {} + constructor(private store: Store, 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 }) } diff --git a/frontend/app/player/player/Player.ts b/frontend/app/player/player/Player.ts index 057d3752f..9b2b98225 100644 --- a/frontend/app/player/player/Player.ts +++ b/frontend/app/player/player/Player.ts @@ -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, 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, private manager: Moveable & Cleanable) { super(pState, manager) // Autoplay diff --git a/frontend/app/player/player/types.ts b/frontend/app/player/player/types.ts index 2ad5032ed..5deb56b06 100644 --- a/frontend/app/player/player/types.ts +++ b/frontend/app/player/player/types.ts @@ -1,9 +1,9 @@ -export interface Mover { +export interface Moveable { move(time: number): void } -export interface Cleaner { +export interface Cleanable { clean(): void }