From 7a3ef9bc2179f30b6c8b3a83c7aad2f1b0a059ae Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 15 Nov 2022 21:01:40 +0100 Subject: [PATCH] trfactoring(frontend/player):phase 1 of componnts decomposition; use store per instance --- .../Session_/Player/Controls/Timeline.js | 2 +- .../app/player/MessageDistributor/Lists.ts | 23 - .../StatedScreen/StatedScreen.ts | 152 ------ .../MessageDistributor/StatedScreen/index.ts | 2 - .../app/player/MessageDistributor/index.js | 2 - frontend/app/player/Player.ts | 281 ----------- .../managers => _common}/ListWalker.ts | 27 +- .../app/player/_common/ListWalkerWithMarks.ts | 31 ++ frontend/app/player/_common/SimpleStore.ts | 15 + .../player/{singletone.js => _singletone.ts} | 109 ++--- .../app/player/{store => _store}/connector.js | 0 frontend/app/player/{store => _store}/duck.js | 10 +- .../app/player/{store => _store}/index.js | 0 .../app/player/{store => _store}/selectors.js | 0 .../app/player/{store => _store}/store.js | 0 frontend/app/player/_web/Lists.ts | 71 +++ .../MessageManager.ts} | 149 +++--- .../Screen/BaseScreen.ts | 16 +- .../StatedScreen => _web}/Screen/Cursor.ts | 0 .../StatedScreen => _web}/Screen/Inspector.js | 0 .../Marker.js => _web/Screen/Marker.ts} | 66 +-- .../StatedScreen => _web}/Screen/Screen.ts | 11 +- .../Screen/cursor.module.css | 0 .../StatedScreen => _web}/Screen/index.js | 0 .../Screen/marker.module.css | 0 .../Screen/screen.module.css | 0 .../StatedScreen => _web}/Screen/types.ts | 0 frontend/app/player/_web/WebPlayer.ts | 178 +++++++ .../assist}/AnnotationCanvas.ts | 0 .../managers => _web/assist}/AssistManager.ts | 78 +-- .../managers => _web/assist}/LocalStream.ts | 0 .../managers/ActivityManager.ts | 8 +- .../managers/DOM/DOMManager.ts | 6 +- .../managers/DOM/FocusManager.ts | 2 +- .../managers/DOM/StylesManager.ts | 6 +- .../managers/DOM/VirtualDOM.ts | 0 .../managers/DOM/safeCSSRules.ts | 0 .../managers/MouseMoveManager.ts | 6 +- .../managers/PagesManager.ts | 8 +- .../managers/PerformanceTrackManager.ts | 2 +- .../managers/ReduxStateManager.ts | 2 +- .../managers/WindowNodeCounter.ts | 0 .../messages/JSONRawMessageReader.ts | 0 .../messages/MFileReader.ts | 0 .../messages/MStreamReader.ts | 0 .../messages/PrimitiveReader.ts | 0 .../messages/RawMessageReader.ts | 0 .../messages/index.ts | 0 .../messages/message.ts | 0 .../messages/raw.ts | 0 .../messages/timed.ts | 0 .../messages/tracker-legacy.ts | 0 .../messages/tracker.ts | 0 .../messages/urlResolve.ts | 0 .../network/crypto.ts | 0 .../network/loadFiles.ts | 0 frontend/app/player/create.ts | 25 + frontend/app/player/index.js | 10 +- frontend/app/player/ios/ImagePlayer.js | 454 ------------------ frontend/app/player/ios/Parser.ts | 34 -- frontend/app/player/ios/PerformanceList.js | 73 --- frontend/app/player/ios/ScreenList.ts | 57 --- frontend/app/player/ios/lists.js | 12 - frontend/app/player/ios/state.js | 112 ----- frontend/app/player/lists/ListReader.js | 124 ----- .../app/player/lists/ListReaderWithRed.js | 48 -- frontend/app/player/lists/index.js | 68 --- frontend/app/player/player/Animator.ts | 172 +++++++ frontend/app/player/player/Player.ts | 125 +++++ frontend/app/player/player/_LSCache.ts | 63 +++ frontend/app/player/player/localStorage.ts | 19 + frontend/app/player/player/types.ts | 21 + 72 files changed, 990 insertions(+), 1690 deletions(-) delete mode 100644 frontend/app/player/MessageDistributor/Lists.ts delete mode 100644 frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts delete mode 100644 frontend/app/player/MessageDistributor/StatedScreen/index.ts delete mode 100644 frontend/app/player/MessageDistributor/index.js delete mode 100644 frontend/app/player/Player.ts rename frontend/app/player/{MessageDistributor/managers => _common}/ListWalker.ts (86%) create mode 100644 frontend/app/player/_common/ListWalkerWithMarks.ts create mode 100644 frontend/app/player/_common/SimpleStore.ts rename frontend/app/player/{singletone.js => _singletone.ts} (69%) rename frontend/app/player/{store => _store}/connector.js (100%) rename frontend/app/player/{store => _store}/duck.js (68%) rename frontend/app/player/{store => _store}/index.js (100%) rename frontend/app/player/{store => _store}/selectors.js (100%) rename frontend/app/player/{store => _store}/store.js (100%) create mode 100644 frontend/app/player/_web/Lists.ts rename frontend/app/player/{MessageDistributor/MessageDistributor.ts => _web/MessageManager.ts} (86%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/BaseScreen.ts (94%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/Cursor.ts (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/Inspector.js (100%) rename frontend/app/player/{MessageDistributor/StatedScreen/Screen/Marker.js => _web/Screen/Marker.ts} (66%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/Screen.ts (91%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/cursor.module.css (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/index.js (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/marker.module.css (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/screen.module.css (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/types.ts (100%) create mode 100644 frontend/app/player/_web/WebPlayer.ts rename frontend/app/player/{MessageDistributor/managers => _web/assist}/AnnotationCanvas.ts (100%) rename frontend/app/player/{MessageDistributor/managers => _web/assist}/AssistManager.ts (87%) rename frontend/app/player/{MessageDistributor/managers => _web/assist}/LocalStream.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/managers/ActivityManager.ts (83%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/DOMManager.ts (99%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/FocusManager.ts (93%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/StylesManager.ts (95%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/VirtualDOM.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/safeCSSRules.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/managers/MouseMoveManager.ts (89%) rename frontend/app/player/{MessageDistributor => _web}/managers/PagesManager.ts (85%) rename frontend/app/player/{MessageDistributor => _web}/managers/PerformanceTrackManager.ts (98%) rename frontend/app/player/{MessageDistributor => _web}/managers/ReduxStateManager.ts (96%) rename frontend/app/player/{MessageDistributor => _web}/managers/WindowNodeCounter.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/JSONRawMessageReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/MFileReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/MStreamReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/PrimitiveReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/RawMessageReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/index.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/message.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/raw.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/timed.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/tracker-legacy.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/tracker.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/urlResolve.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/network/crypto.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/network/loadFiles.ts (100%) create mode 100644 frontend/app/player/create.ts delete mode 100644 frontend/app/player/ios/ImagePlayer.js delete mode 100644 frontend/app/player/ios/Parser.ts delete mode 100644 frontend/app/player/ios/PerformanceList.js delete mode 100644 frontend/app/player/ios/ScreenList.ts delete mode 100644 frontend/app/player/ios/lists.js delete mode 100644 frontend/app/player/ios/state.js delete mode 100644 frontend/app/player/lists/ListReader.js delete mode 100644 frontend/app/player/lists/ListReaderWithRed.js delete mode 100644 frontend/app/player/lists/index.js create mode 100644 frontend/app/player/player/Animator.ts create mode 100644 frontend/app/player/player/Player.ts create mode 100644 frontend/app/player/player/_LSCache.ts create mode 100644 frontend/app/player/player/localStorage.ts create mode 100644 frontend/app/player/player/types.ts diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 3ff810d57..e3ce9788a 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -30,7 +30,7 @@ let debounceTooltipChange = () => null; disabled: state.cssLoading || state.messagesLoading || state.markedTargets, endTime: state.endTime, live: state.live, - notes: state.notes, + notes: state.notes || [], // TODO: implement notes without interaction with Player state })) @connect( (state) => ({ diff --git a/frontend/app/player/MessageDistributor/Lists.ts b/frontend/app/player/MessageDistributor/Lists.ts deleted file mode 100644 index cb7e4d192..000000000 --- a/frontend/app/player/MessageDistributor/Lists.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Message } from './messages' -import ListWalker from './managers/ListWalker'; - -export const LIST_NAMES = ["redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles"] as const; - -export const INITIAL_STATE = {} -LIST_NAMES.forEach(name => { - INITIAL_STATE[`${name}ListNow`] = [] - INITIAL_STATE[`${name}List`] = [] -}) - - -type ListsObject = { - [key in typeof LIST_NAMES[number]]: ListWalker -} - -export function initLists(): ListsObject { - const lists: Partial = {}; - for (var i = 0; i < LIST_NAMES.length; i++) { - lists[LIST_NAMES[i]] = new ListWalker(); - } - return lists as ListsObject; -} diff --git a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts deleted file mode 100644 index 45028f88f..000000000 --- a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts +++ /dev/null @@ -1,152 +0,0 @@ -import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; -import { update, getState } from '../../store'; - -import type { Point } from './Screen/types'; - -function getOffset(el: Element, innerWindow: Window) { - const rect = el.getBoundingClientRect(); - return { - fixedLeft: rect.left + innerWindow.scrollX, - fixedTop: rect.top + innerWindow.scrollY, - rect, - }; -} - -//export interface targetPosition - -interface BoundingRect { - top: number, - left: number, - width: number, - height: number, -} - -export interface MarkedTarget { - boundingRect: BoundingRect, - el: Element, - selector: string, - count: number, - index: number, - active?: boolean, - percent: number -} - -export interface State extends SuperState { - messagesLoading: boolean, - cssLoading: boolean, - markedTargets: MarkedTarget[] | null, - activeTargetIndex: number, -} - -export const INITIAL_STATE: State = { - ...SUPER_INITIAL_STATE, - messagesLoading: false, - cssLoading: false, - markedTargets: null, - activeTargetIndex: 0 -}; - -export default class StatedScreen extends Screen { - constructor() { super(); } - - setMessagesLoading(messagesLoading: boolean) { - this.display(!messagesLoading); - update({ messagesLoading }); - } - - setCSSLoading(cssLoading: boolean) { - this.displayFrame(!cssLoading); - update({ cssLoading }); - } - - setSize({ height, width }: { height: number, width: number }) { - update({ width, height }); - this.scale(); - this.updateMarketTargets() - } - - updateMarketTargets() { - const { markedTargets } = getState(); - if (markedTargets) { - update({ - markedTargets: markedTargets.map((mt: any) => ({ - ...mt, - boundingRect: this.calculateRelativeBoundingRect(mt.el), - })), - }); - } - } - - private calculateRelativeBoundingRect(el: Element): BoundingRect { - if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO - const { top, left, width, height } = el.getBoundingClientRect(); - const s = this.getScale(); - const scrinRect = this.screen.getBoundingClientRect(); - const parentRect = this.parentElement.getBoundingClientRect(); - - return { - top: top*s + scrinRect.top - parentRect.top, - left: left*s + scrinRect.left - parentRect.left, - width: width*s, - height: height*s, - } - } - - setActiveTarget(index: number) { - const window = this.window - const markedTargets: MarkedTarget[] | null = getState().markedTargets - const target = markedTargets && markedTargets[index] - if (target && window) { - const { fixedTop, rect } = getOffset(target.el, window) - const scrollToY = fixedTop - window.innerHeight / 1.5 - if (rect.top < 0 || rect.top > window.innerHeight) { - // behavior hack TODO: fix it somehow when they will decide to remove it from browser api - // @ts-ignore - window.scrollTo({ top: scrollToY, behavior: 'instant' }) - setTimeout(() => { - if (!markedTargets) { return } - update({ - markedTargets: markedTargets.map(t => t === target ? { - ...target, - boundingRect: this.calculateRelativeBoundingRect(target.el), - } : t) - }) - }, 0) - } - - } - update({ activeTargetIndex: index }); - } - - private actualScroll: Point | null = null - setMarkedTargets(selections: { selector: string, count: number }[] | null) { - if (selections) { - const totalCount = selections.reduce((a, b) => { - return a + b.count - }, 0); - const markedTargets: MarkedTarget[] = []; - let index = 0; - selections.forEach((s) => { - const el = this.getElementBySelector(s.selector); - if (!el) return; - markedTargets.push({ - ...s, - el, - index: index++, - percent: Math.round((s.count * 100) / totalCount), - boundingRect: this.calculateRelativeBoundingRect(el), - count: s.count, - }) - }); - - this.actualScroll = this.getCurrentScroll() - update({ markedTargets }); - } else { - if (this.actualScroll) { - this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) - this.actualScroll = null - } - update({ markedTargets: null }); - } - } -} diff --git a/frontend/app/player/MessageDistributor/StatedScreen/index.ts b/frontend/app/player/MessageDistributor/StatedScreen/index.ts deleted file mode 100644 index 0955acffb..000000000 --- a/frontend/app/player/MessageDistributor/StatedScreen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './StatedScreen'; -export * from './StatedScreen'; \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/index.js b/frontend/app/player/MessageDistributor/index.js deleted file mode 100644 index 8502aee50..000000000 --- a/frontend/app/player/MessageDistributor/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './MessageDistributor'; -export * from './MessageDistributor'; diff --git a/frontend/app/player/Player.ts b/frontend/app/player/Player.ts deleted file mode 100644 index 320b141ec..000000000 --- a/frontend/app/player/Player.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { goTo as listsGoTo } from './lists'; -import { update, getState } from './store'; -import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE } from './MessageDistributor/MessageDistributor'; -import { Note } from 'App/services/NotesService'; - -const fps = 60; -const performance = window.performance || { now: Date.now.bind(Date) }; -const requestAnimationFrame = - window.requestAnimationFrame || - // @ts-ignore - window.webkitRequestAnimationFrame || - // @ts-ignore - window.mozRequestAnimationFrame || - // @ts-ignore - window.oRequestAnimationFrame || - // @ts-ignore - window.msRequestAnimationFrame || - ((callback: (args: any) => void) => window.setTimeout(() => { callback(performance.now()); }, 1000 / fps)); -const cancelAnimationFrame = - window.cancelAnimationFrame || - // @ts-ignore - window.mozCancelAnimationFrame || - window.clearTimeout; - -const HIGHEST_SPEED = 16; - - -const SPEED_STORAGE_KEY = "__$player-speed$__"; -const SKIP_STORAGE_KEY = "__$player-skip$__"; -const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__"; -const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__"; -const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__"; -const storedSpeed: number = parseInt(localStorage.getItem(SPEED_STORAGE_KEY) || "") ; -const initialSpeed = [1,2,4,8,16].includes(storedSpeed) ? storedSpeed : 1; -const initialSkip = localStorage.getItem(SKIP_STORAGE_KEY) === 'true'; -const initialSkipToIssue = localStorage.getItem(SKIP_TO_ISSUE_STORAGE_KEY) === 'true'; -const initialAutoplay = localStorage.getItem(AUTOPLAY_STORAGE_KEY) === 'true'; -const initialShowEvents = localStorage.getItem(SHOW_EVENTS_STORAGE_KEY) === 'true'; - -export const INITIAL_STATE = { - ...SUPER_INITIAL_STATE, - time: 0, - playing: false, - completed: false, - endTime: 0, - inspectorMode: false, - live: false, - livePlay: false, - liveTimeTravel: false, - notes: [], -} as const; - - -export const INITIAL_NON_RESETABLE_STATE = { - skip: initialSkip, - skipToIssue: initialSkipToIssue, - autoplay: initialAutoplay, - speed: initialSpeed, - showEvents: initialShowEvents, -} - -export default class Player extends MessageDistributor { - private _animationFrameRequestId: number = 0; - - private _setTime(time: number, index?: number) { - update({ - time, - completed: false, - }); - super.move(time, index); - listsGoTo(time, index); - } - - private _startAnimation() { - let prevTime = getState().time; - let animationPrevTime = performance.now(); - - const nextFrame = (animationCurrentTime: number) => { - const { - speed, - skip, - autoplay, - skipIntervals, - endTime, - live, - livePlay, - disconnected, - messagesLoading, - cssLoading, - } = getState(); - - const diffTime = messagesLoading || cssLoading || disconnected - ? 0 - : Math.max(animationCurrentTime - animationPrevTime, 0) * (live ? 1 : speed); - - let time = prevTime + diffTime; - - const skipInterval = !live && skip && skipIntervals.find((si: Node) => si.contains(time)); // TODO: good skip by messages - if (skipInterval) time = skipInterval.end; - - const fmt = super.getFirstMessageTime(); - if (time < fmt) time = fmt; // ? - - const lmt = super.getLastMessageTime(); - if (livePlay && time < lmt) time = lmt; - if (endTime < lmt) { - update({ - endTime: lmt, - }); - } - - prevTime = time; - animationPrevTime = animationCurrentTime; - - const completed = !live && time >= endTime; - if (completed) { - this._setTime(endTime); - return update({ - playing: false, - completed: true, - }); - } - - // throttle store updates - // TODO: make it possible to change frame rate - if (live && time - endTime > 100) { - update({ - endTime: time, - livePlay: endTime - time < 900 - }); - } - this._setTime(time); - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - }; - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - } - - play() { - cancelAnimationFrame(this._animationFrameRequestId); - update({ playing: true }); - this._startAnimation(); - } - - pause() { - cancelAnimationFrame(this._animationFrameRequestId); - update({ playing: false }) - } - - togglePlay() { - const { playing, completed } = getState(); - if (playing) { - this.pause(); - } else if (completed) { - this._setTime(0); - this.play(); - } else { - this.play(); - } - } - - jump(setTime: number, index: number) { - const { live, liveTimeTravel, endTime } = getState(); - if (live && !liveTimeTravel) return; - const time = setTime ? setTime : getState().time - if (getState().playing) { - cancelAnimationFrame(this._animationFrameRequestId); - // this._animationFrameRequestId = requestAnimationFrame(() => { - this._setTime(time, index); - this._startAnimation(); - // throttilg the redux state update from each frame to nearly half a second - // which is better for performance and component rerenders - update({ livePlay: Math.abs(time - endTime) < 500 }); - //}); - } else { - //this._animationFrameRequestId = requestAnimationFrame(() => { - this._setTime(time, index); - update({ livePlay: Math.abs(time - endTime) < 500 }); - //}); - } - } - - toggleSkip() { - const skip = !getState().skip; - localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`); - update({ skip }); - } - - toggleInspectorMode(flag: boolean, clickCallback?: (args: any) => void) { - 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 }); - } - } - - markTargets(targets: { selector: string, count: number }[] | null) { - this.pause(); - this.setMarkedTargets(targets); - } - - activeTarget(index: number) { - this.setActiveTarget(index); - } - - toggleSkipToIssue() { - const skipToIssue = !getState().skipToIssue; - localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`); - update({ skipToIssue }); - } - - toggleAutoplay() { - const autoplay = !getState().autoplay; - localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`); - update({ autoplay }); - } - - toggleEvents(shouldShow?: boolean) { - const showEvents = shouldShow || !getState().showEvents; - localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`); - update({ showEvents }); - } - - _updateSpeed(speed: number) { - localStorage.setItem(SPEED_STORAGE_KEY, `${speed}`); - update({ speed }); - } - - toggleSpeed() { - const { speed } = getState(); - this._updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 1); - } - - speedUp() { - const { speed } = getState(); - this._updateSpeed(Math.min(HIGHEST_SPEED, speed * 2)); - } - - speedDown() { - const { speed } = getState(); - this._updateSpeed(Math.max(1, speed/2)); - } - - async toggleTimetravel() { - if (!getState().liveTimeTravel) { - return await this.reloadWithUnprocessedFile() - } - } - - jumpToLive() { - cancelAnimationFrame(this._animationFrameRequestId); - this._setTime(getState().endTime); - this._startAnimation(); - update({ livePlay: true }); - } - - toggleUserName(name?: string) { - this.cursor.toggleUserName(name) - } - - injectNotes(notes: Note[]) { - update({ notes }) - } - - filterOutNote(noteId: number) { - const { notes } = getState() - update({ notes: notes.filter((note: Note) => note.noteId !== noteId) }) - } - - clean() { - this.pause(); - super.clean(); - } -} diff --git a/frontend/app/player/MessageDistributor/managers/ListWalker.ts b/frontend/app/player/_common/ListWalker.ts similarity index 86% rename from frontend/app/player/MessageDistributor/managers/ListWalker.ts rename to frontend/app/player/_common/ListWalker.ts index e04c5bb83..92f7585c8 100644 --- a/frontend/app/player/MessageDistributor/managers/ListWalker.ts +++ b/frontend/app/player/_common/ListWalker.ts @@ -1,4 +1,4 @@ -import type { Timed } from '../messages/timed'; +import type { Timed } from './messages/timed'; export default class ListWalker { private p = 0 @@ -79,6 +79,23 @@ export default class ListWalker { return this.p; } + private hasNext() { + return this.p < this.length + } + private hasPrev() { + return this.p > 0 + } + protected moveNext(): T | null { + return this.hasNext() + ? this.list[ this.p++ ] + : null + } + protected movePrev(): T | null { + return this.hasPrev() + ? this.list[ --this.p ] + : null + } + /* Returns last message with the time <= t. Assumed that the current message is already handled so @@ -94,11 +111,11 @@ export default class ListWalker { let changed = false; while (this.p < this.length && this.list[this.p][key] <= val) { - this.p++; + this.moveNext() changed = true; } while (this.p > 0 && this.list[ this.p - 1 ][key] > val) { - this.p--; + this.movePrev() changed = true; } return changed ? this.list[ this.p - 1 ] : null; @@ -112,10 +129,10 @@ export default class ListWalker { const list = this.list while (list[this.p] && list[this.p].time <= t) { - fn(list[ this.p++ ]); + fn(this.moveNext()) } while (fnBack && this.p > 0 && list[ this.p - 1 ].time > t) { - fnBack(list[ --this.p ]); + fnBack(this.movePrev()); } } diff --git a/frontend/app/player/_common/ListWalkerWithMarks.ts b/frontend/app/player/_common/ListWalkerWithMarks.ts new file mode 100644 index 000000000..b2a8f5d3d --- /dev/null +++ b/frontend/app/player/_common/ListWalkerWithMarks.ts @@ -0,0 +1,31 @@ +import type { Timed } from './messages/timed'; +import ListWalker from './ListWalker' + + +type CheckFn = (t: T) => boolean + + +export default class ListWalkerWithMarks extends ListWalker { + private _markCountNow: number = 0 + constructor(private isMarked: CheckFn, initialList?: T[]) { + super(initialList) + } + protected moveNext() { + const val = super.moveNext() + if (val && this.isMarked(val)) { + this._markCountNow++ + } + return val + } + protected movePrev() { + const val = super.movePrev() + if (val && this.isMarked(val)) { + this._markCountNow-- + } + return val + } + get markCountNow(): number { + return this._markCountNow + } + +} \ No newline at end of file diff --git a/frontend/app/player/_common/SimpleStore.ts b/frontend/app/player/_common/SimpleStore.ts new file mode 100644 index 000000000..9e81a79cd --- /dev/null +++ b/frontend/app/player/_common/SimpleStore.ts @@ -0,0 +1,15 @@ + +import { State } from './types' + +// (not a type) +export default class SimpleSore implements State { + constructor(private state: G){} + get(): G { + return this.state + } + update(newState: Partial) { + Object.assign(this.state, newState) + } +} + + diff --git a/frontend/app/player/singletone.js b/frontend/app/player/_singletone.ts similarity index 69% rename from frontend/app/player/singletone.js rename to frontend/app/player/_singletone.ts index feb82ec78..71f29a7ee 100644 --- a/frontend/app/player/singletone.js +++ b/frontend/app/player/_singletone.ts @@ -1,11 +1,40 @@ -import Player from './Player'; -import { update, cleanStore, getState } from './store'; -import { clean as cleanLists } from './lists'; +import WebPlayer from './_web/WebPlayer'; +import reduxStore, {update, cleanStore} from './_store'; -/** @type {Player} */ -let instance = null; +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 initCheck = method => (...args) => { + +const INIT_STATE = { + ...MM_INITIAL_STATE, + ...PLAYER_INITIAL_STATE, +} + + +const myStore: Store = { + get() { + return reduxStore.getState() + }, + update(s) { + update(s) + } +} + +let instance: WebPlayer | null = null; + +export function init(session, config, live = false) { + instance = new WebPlayer(myStore, session, config, live); +} + +export function clean() { + if (instance === null) return; + instance.clean(); + cleanStore() + instance = null; +} + +const initCheck = (method) => (...args) => { if (instance === null) { console.error("Player method called before Player have been initialized."); return; @@ -13,47 +42,7 @@ const initCheck = method => (...args) => { return method(...args); } - -let autoPlay = true; -document.addEventListener("visibilitychange", function() { - if (instance === null) return; - if (document.hidden) { - const { playing } = getState(); - autoPlay = playing - if (playing) { - instance.pause(); - } - } else if (autoPlay) { - instance.play(); - } -}); - -export function init(session, config, live = false) { - const endTime = !live && session.duration.valueOf(); - - instance = new Player(session, config, live); - update({ - initialized: true, - live, - livePlay: live, - endTime, // : 0, //TODO: through initialState - session, - }); - - if (!document.hidden) { - instance.play(); - } -} - -export function clean() { - if (instance === null) return; - instance.clean(); - cleanStore(); - cleanLists(); - instance = null; -} export const jump = initCheck((...args) => instance.jump(...args)); - export const togglePlay = initCheck((...args) => instance.togglePlay(...args)); export const pause = initCheck((...args) => instance.pause(...args)); export const toggleSkip = initCheck((...args) => instance.toggleSkip(...args)); @@ -64,9 +53,22 @@ export const toggleEvents = initCheck((...args) => instance.toggleEvents(...args export const speedUp = initCheck((...args) => instance.speedUp(...args)); export const speedDown = initCheck((...args) => instance.speedDown(...args)); export const attach = initCheck((...args) => instance.attach(...args)); -export const markElement = initCheck((...args) => instance.marker && instance.marker.mark(...args)); +export const markElement = initCheck((...args) => instance.mark(...args)); export const scale = initCheck(() => instance.scale()); +/** @type {WebPlayer.toggleTimetravel} */ +export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args)) export const toggleInspectorMode = initCheck((...args) => instance.toggleInspectorMode(...args)); +export const markTargets = initCheck((...args) => instance.markTargets(...args)) +export const activeTarget =initCheck((...args) => instance.setActiveTarget(...args)) + +export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args)) +export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args)) + +// !not related to player, but rather to the OR platform. +export const injectNotes = () => {} // initCheck((...args) => instance.injectNotes(...args)) +export const filterOutNote = () => {} //initCheck((...args) => instance.filterOutNote(...args)) + + /** @type {Player.assistManager.call} */ export const callPeer = initCheck((...args) => instance.assistManager.call(...args)) /** @type {Player.assistManager.setCallArgs} */ @@ -75,17 +77,10 @@ export const setCallArgs = initCheck((...args) => instance.assistManager.setCall export const initiateCallEnd = initCheck((...args) => instance.assistManager.initiateCallEnd(...args)) export const requestReleaseRemoteControl = initCheck((...args) => instance.assistManager.requestReleaseRemoteControl(...args)) export const releaseRemoteControl = initCheck((...args) => instance.assistManager.releaseRemoteControl(...args)) -export const markTargets = initCheck((...args) => instance.markTargets(...args)) -export const activeTarget = initCheck((...args) => instance.activeTarget(...args)) -export const toggleAnnotation = initCheck((...args) => instance.assistManager.toggleAnnotation(...args)) -/** @type {Player.toggleTimetravel} */ -export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args)) -export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args)) -export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args)) -export const injectNotes = initCheck((...args) => instance.injectNotes(...args)) -export const filterOutNote = initCheck((...args) => instance.filterOutNote(...args)) /** @type {Player.assistManager.toggleVideoLocalStream} */ export const toggleVideoLocalStream = initCheck((...args) => instance.assistManager.toggleVideoLocalStream(...args)) +export const toggleAnnotation = initCheck((...args) => instance.assistManager.toggleAnnotation(...args)) + export const Controls = { jump, @@ -99,4 +94,4 @@ export const Controls = { speedUp, speedDown, callPeer -} +} \ No newline at end of file diff --git a/frontend/app/player/store/connector.js b/frontend/app/player/_store/connector.js similarity index 100% rename from frontend/app/player/store/connector.js rename to frontend/app/player/_store/connector.js diff --git a/frontend/app/player/store/duck.js b/frontend/app/player/_store/duck.js similarity index 68% rename from frontend/app/player/store/duck.js rename to frontend/app/player/_store/duck.js index faf77041c..bcede3b32 100644 --- a/frontend/app/player/store/duck.js +++ b/frontend/app/player/_store/duck.js @@ -1,20 +1,20 @@ import { applyChange, revertChange } from 'deep-diff'; -import { INITIAL_STATE as listsInitialState } from '../lists'; -import { INITIAL_STATE as playerInitialState, INITIAL_NON_RESETABLE_STATE as playerInitialNonResetableState } from '../Player'; + +import { INITIAL_STATE as MM_INITIAL_STATE } from '../_web/MessageManager' +import { INITIAL_STATE as PLAYER_INITIAL_STATE } from '../player/Player' const UPDATE = 'player/UPDATE'; const CLEAN = 'player/CLEAN'; const REDUX = 'player/REDUX'; const resetState = { - ...listsInitialState, - ...playerInitialState, + ...MM_INITIAL_STATE, + ...PLAYER_INITIAL_STATE, initialized: false, }; const initialState = { ...resetState, - ...playerInitialNonResetableState, } export default (state = initialState, action = {}) => { diff --git a/frontend/app/player/store/index.js b/frontend/app/player/_store/index.js similarity index 100% rename from frontend/app/player/store/index.js rename to frontend/app/player/_store/index.js diff --git a/frontend/app/player/store/selectors.js b/frontend/app/player/_store/selectors.js similarity index 100% rename from frontend/app/player/store/selectors.js rename to frontend/app/player/_store/selectors.js diff --git a/frontend/app/player/store/store.js b/frontend/app/player/_store/store.js similarity index 100% rename from frontend/app/player/store/store.js rename to frontend/app/player/_store/store.js diff --git a/frontend/app/player/_web/Lists.ts b/frontend/app/player/_web/Lists.ts new file mode 100644 index 000000000..4f9c236cd --- /dev/null +++ b/frontend/app/player/_web/Lists.ts @@ -0,0 +1,71 @@ +import ListWalker from '../_common/ListWalker'; +import ListWalkerWithMarks from '../_common/ListWalkerWithMarks'; + +import type { Message } from './messages' + +const SIMPLE_LIST_NAMES = [ "event", "redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles"] as const; +const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const; +//const entityNamesSimple = [ "event", "profile" ]; + +const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ]; + +// TODO: provide correct types + +export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => { + state[`${name}List`] = [] + state[`${name}ListNow`] = [] + if (MARKED_LIST_NAMES.includes(name)) { + state[`${name}MarkedCountNow`] = 0 + } + return state +}, {}) + + +type SimpleListsObject = { + [key in typeof SIMPLE_LIST_NAMES[number]]: ListWalker +} +type MarkedListsObject = { + [key in typeof MARKED_LIST_NAMES[number]]: ListWalkerWithMarks +} +type ListsObject = SimpleListsObject & MarkedListsObject + +type InitialLists = { + [key in typeof LIST_NAMES[number]]: any[] +} + +export default class Lists { + lists: ListsObject + constructor(initialLists: Partial = {}) { + const lists: Partial = {} + for (const name of SIMPLE_LIST_NAMES) { + lists[name] = new ListWalker(initialLists[name]) + } + for (const name of MARKED_LIST_NAMES) { + // TODO: provide types + lists[name] = new ListWalkerWithMarks((el) => el.isRed(), initialLists[name]) + } + this.lists = lists as ListsObject + } + + getFullListsState() { + return LIST_NAMES.reduce((state, name) => { + state[`${name}List`] = this.lists[name].list + return state + }, {}) + } + + moveGetState(t: number) { + return LIST_NAMES.reduce((state, name) => { + const lastMsg = this.lists[name].moveGetLast(t) // index: name === 'exceptions' ? undefined : index); + if (lastMsg != null) { + state[`${name}ListNow`] = this.lists[name].listNow + } + return state + }, MARKED_LIST_NAMES.reduce((state, name) => { + state[`${name}RedCountNow`] = this.lists[name].markCountNow // Red --> Marked + return state + }, {}) + ); + } + +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.ts b/frontend/app/player/_web/MessageManager.ts similarity index 86% rename from frontend/app/player/MessageDistributor/MessageDistributor.ts rename to frontend/app/player/_web/MessageManager.ts index 85b01e6a3..e6dc9467c 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.ts +++ b/frontend/app/player/_web/MessageManager.ts @@ -2,37 +2,31 @@ import { Decoder } from "syncod"; import logger from 'App/logger'; -import Resource, { TYPES } from 'Types/session/resource'; // MBTODO: player types? +import Resource, { TYPES } from 'Types/session/resource'; import { TYPES as EVENT_TYPES } from 'Types/session/event'; import Log from 'Types/session/log'; -import { update } from '../store'; import { toast } from 'react-toastify'; -import { - init as initListsDepr, - append as listAppend, - setStartTime as setListsStartTime -} from '../lists'; +import type { Store } from '../player/types'; +import ListWalker from '../_common/ListWalker'; -import StatedScreen from './StatedScreen/StatedScreen'; +import Screen from './Screen/Screen'; -import ListWalker from './managers/ListWalker'; import PagesManager from './managers/PagesManager'; import MouseMoveManager from './managers/MouseMoveManager'; import PerformanceTrackManager from './managers/PerformanceTrackManager'; import WindowNodeCounter from './managers/WindowNodeCounter'; import ActivityManager from './managers/ActivityManager'; -import AssistManager from './managers/AssistManager'; import MFileReader from './messages/MFileReader'; import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; import { decryptSessionBytes } from './network/crypto'; -import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './StatedScreen/StatedScreen'; -import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './managers/AssistManager'; -import { INITIAL_STATE as LISTS_INITIAL_STATE , LIST_NAMES, initLists } from './Lists'; +import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; +import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './assist/AssistManager'; +import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; import type { SkipInterval } from './managers/ActivityManager'; @@ -50,8 +44,16 @@ export interface State extends SuperState, AssistState { domBuildingTime?: any, loadTime?: any, error: boolean, - devtoolsLoading: boolean + devtoolsLoading: boolean, + + liveTimeTravel: boolean, + messagesLoading: boolean, + cssLoading: boolean, + + ready: boolean, + lastMessageTime: number, } + export const INITIAL_STATE: State = { ...SUPER_INITIAL_STATE, ...LISTS_INITIAL_STATE, @@ -60,6 +62,14 @@ export const INITIAL_STATE: State = { skipIntervals: [], error: false, devtoolsLoading: false, + + liveTimeTravel: false, + messagesLoading: false, + cssLoading: false, + get ready() { + return !this.messagesLoading && !this.cssLoading + }, + lastMessageTime: 0, }; @@ -82,7 +92,7 @@ const visualChanges = [ "set_viewport_scroll", ] -export default class MessageDistributor extends StatedScreen { +export default class MessageManager extends Screen { // TODO: consistent with the other data-lists private locationEventManager: ListWalker/**/ = new ListWalker(); private locationManager: ListWalker = new ListWalker(); @@ -95,12 +105,11 @@ export default class MessageDistributor extends StatedScreen { private resizeManager: ListWalker = new ListWalker([]); private pagesManager: PagesManager; private mouseMoveManager: MouseMoveManager; - private assistManager: AssistManager; private scrollManager: ListWalker = new ListWalker(); private readonly decoder = new Decoder(); - private readonly lists = initLists(); + private readonly lists: Lists; private activityManager: ActivityManager | null = null; @@ -109,37 +118,39 @@ export default class MessageDistributor extends StatedScreen { private lastMessageTime: number = 0; private lastMessageInFileTime: number = 0; - constructor(private readonly session: any /*Session*/, config: any, live: boolean) { + constructor( + private readonly session: any /*Session*/, + private readonly state: Store, + config: any, + live: boolean, + ) { super(); this.pagesManager = new PagesManager(this, this.session.isMobile) this.mouseMoveManager = new MouseMoveManager(this); - this.assistManager = new AssistManager(session, this, config); this.sessionStart = this.session.startedAt; if (live) { - initListsDepr({}) - this.assistManager.connect(this.session.agentToken); + this.lists = new Lists() } else { this.activityManager = new ActivityManager(this.session.duration.milliseconds); /* == REFACTOR_ME == */ - const eventList = this.session.events.toJSON(); - - initListsDepr({ - event: eventList, - stack: this.session.stackEvents.toJSON(), - resource: this.session.resources.toJSON(), - }); - + const eventList = session.events.toJSON(); // TODO: fix types for events, remove immutable js eventList.forEach((e: Record) => { if (e.type === EVENT_TYPES.LOCATION) { //TODO type system this.locationEventManager.append(e); } - }); - this.session.errors.forEach((e: Record) => { - this.lists.exceptions.append(e); - }); + }) + + this.lists = new Lists({ + event: eventList, + stack: session.stackEvents.toJSON(), + resource: session.resources.toJSON(), + exceptions: session.errors, + }) + + /* === */ this.loadMessages(); } @@ -187,23 +198,21 @@ export default class MessageDistributor extends StatedScreen { private waitingForFiles: boolean = false private onFileReadSuccess = () => { - const stateToUpdate: {[key:string]: any} = { + const stateToUpdate = { performanceChartData: this.performanceTrackManager.chartData, performanceAvaliability: this.performanceTrackManager.avaliability, + ...this.lists.getFullListsState() } - LIST_NAMES.forEach(key => { - stateToUpdate[ `${ key }List` ] = this.lists[ key ].list - }) if (this.activityManager) { this.activityManager.end() stateToUpdate.skipIntervals = this.activityManager.list } - update(stateToUpdate) + this.state.update(stateToUpdate) } private onFileReadFailed = (e: any) => { logger.error(e) - update({ error: true }) + this.state.update({ error: true }) toast.error('Error requesting a session file') } private onFileReadFinally = () => { @@ -247,14 +256,14 @@ export default class MessageDistributor extends StatedScreen { // load devtools if (this.session.devtoolsURL.length) { - update({ devtoolsLoading: true }) + this.state.update({ devtoolsLoading: true }) loadFiles(this.session.devtoolsURL, createNewParser()) .catch(() => requestEFSDevtools(this.session.sessionId) .then(createNewParser(false)) ) //.catch() // not able to download the devtools file - .finally(() => update({ devtoolsLoading: false })) + .finally(() => this.state.update({ devtoolsLoading: false })) } } @@ -264,7 +273,7 @@ export default class MessageDistributor extends StatedScreen { this.parseAndDistributeMessages(new MFileReader(byteArray, this.sessionStart), onMessage) } const updateState = () => - update({ + this.state.update({ liveTimeTravel: true, }); @@ -304,7 +313,7 @@ export default class MessageDistributor extends StatedScreen { /* == REFACTOR_ME == */ const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index); if (!!lastLoadedLocationMsg) { - setListsStartTime(lastLoadedLocationMsg.time) + // TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time) this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart; } const llEvent = this.locationEventManager.moveGetLast(t, index); @@ -340,15 +349,8 @@ export default class MessageDistributor extends StatedScreen { stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time; } - LIST_NAMES.forEach(key => { - const lastMsg = this.lists[key].moveGetLast(t, key === 'exceptions' ? undefined : index); - if (lastMsg != null) { - // @ts-ignore TODO: fix types - stateToUpdate[`${key}ListNow`] = this.lists[key].listNow; - } - }); - - Object.keys(stateToUpdate).length > 0 && update(stateToUpdate); + this.lists.moveGetState(t) + Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate); /* Sequence of the managers is important here */ // Preparing the size of "screen" @@ -405,6 +407,7 @@ export default class MessageDistributor extends StatedScreen { private distributeMessage(msg: Message, index: number): void { const lastMessageTime = Math.max(msg.time, this.lastMessageTime) this.lastMessageTime = lastMessageTime + this.state.update({ lastMessageTime }) if (visualChanges.includes(msg.tp)) { this.activityManager?.updateAcctivity(msg.time); } @@ -414,15 +417,15 @@ export default class MessageDistributor extends StatedScreen { /* Lists: */ case "console_log": if (msg.level === 'debug') break; - listAppend("log", Log({ + this.lists.lists.log.append(Log({ level: msg.level, value: msg.value, time, index, - })); + })) break; case "fetch": - listAppend("fetch", Resource({ + this.lists.lists.fetch.append(Resource({ method: msg.method, url: msg.url, payload: msg.request, @@ -469,42 +472,42 @@ export default class MessageDistributor extends StatedScreen { decoded = this.decodeStateMessage(msg, ["state", "action"]); logger.log('redux', decoded) if (decoded != null) { - this.lists.redux.append(decoded); + this.lists.lists.redux.append(decoded); } break; case "ng_rx": decoded = this.decodeStateMessage(msg, ["state", "action"]); logger.log('ngrx', decoded) if (decoded != null) { - this.lists.ngrx.append(decoded); + this.lists.lists.ngrx.append(decoded); } break; case "vuex": decoded = this.decodeStateMessage(msg, ["state", "mutation"]); logger.log('vuex', decoded) if (decoded != null) { - this.lists.vuex.append(decoded); + this.lists.lists.vuex.append(decoded); } break; case "zustand": decoded = this.decodeStateMessage(msg, ["state", "mutation"]) logger.log('zustand', decoded) if (decoded != null) { - this.lists.zustand.append(decoded) + this.lists.lists.zustand.append(decoded) } case "mob_x": decoded = this.decodeStateMessage(msg, ["payload"]); logger.log('mobx', decoded) if (decoded != null) { - this.lists.mobx.append(decoded); + this.lists.lists.mobx.append(decoded); } break; case "graph_ql": - this.lists.graphql.append(msg); + this.lists.lists.graphql.append(msg); break; case "profiler": - this.lists.profiles.append(msg); + this.lists.lists.profiles.append(msg); break; default: switch (msg.tp) { @@ -540,11 +543,27 @@ export default class MessageDistributor extends StatedScreen { return this.pagesManager.minTime; } + + setMessagesLoading(messagesLoading: boolean) { + this.display(!messagesLoading); + this.state.update({ messagesLoading }); + } + + setCSSLoading(cssLoading: boolean) { + this.displayFrame(!cssLoading); + this.state.update({ cssLoading }); + } + + private setSize({ height, width }: { height: number, width: number }) { + this.scale({ height, width }); + this.state.update({ width, height }); + + //this.updateMarketTargets() + } + // TODO: clean managers? clean() { - super.clean(); - update(INITIAL_STATE); - this.assistManager.clear(); + this.state.update(INITIAL_STATE); this.incomingMessages.length = 0 } diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts b/frontend/app/player/_web/Screen/BaseScreen.ts similarity index 94% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts rename to frontend/app/player/_web/Screen/BaseScreen.ts index d86284851..67f527e01 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts +++ b/frontend/app/player/_web/Screen/BaseScreen.ts @@ -1,5 +1,4 @@ import styles from './screen.module.css'; -import { getState } from '../../../store'; import type { Point } from './types'; @@ -86,9 +85,6 @@ export default abstract class BaseScreen { parentElement.appendChild(this.screen); this.parentElement = parentElement; - // parentElement.onresize = this.scale; - window.addEventListener('resize', this.scale); - this.scale(); /* == For the Inspecting Document content == */ this.overlay.addEventListener('contextmenu', () => { @@ -197,9 +193,8 @@ export default abstract class BaseScreen { return this.s; } - _scale() { + scale({ height, width }: { height: number, width: number }) { if (!this.parentElement) return; - const { height, width } = getState(); const { offsetWidth, offsetHeight } = this.parentElement; this.s = Math.min(offsetWidth / width, offsetHeight / height); @@ -216,13 +211,4 @@ export default abstract class BaseScreen { this.boundingRect = this.overlay.getBoundingClientRect(); } - - scale = () => { // TODO: solve classes inheritance issues in typescript - this._scale(); - } - - - clean() { - window.removeEventListener('resize', this.scale); - } } diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts b/frontend/app/player/_web/Screen/Cursor.ts similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts rename to frontend/app/player/_web/Screen/Cursor.ts diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Inspector.js b/frontend/app/player/_web/Screen/Inspector.js similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Inspector.js rename to frontend/app/player/_web/Screen/Inspector.js diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js b/frontend/app/player/_web/Screen/Marker.ts similarity index 66% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js rename to frontend/app/player/_web/Screen/Marker.ts index ff1476ac3..4d3fab9b1 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js +++ b/frontend/app/player/_web/Screen/Marker.ts @@ -1,32 +1,32 @@ +import type BaseScreen from './BaseScreen' import styles from './marker.module.css'; -function escapeRegExp(string) { +function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } -function escapeHtml(string) { +function escapeHtml(string: string) { return string.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); } -function safeString(string) { +function safeString(string: string) { return (escapeHtml(escapeRegExp(string))) } export default class Marker { - _target = null; - _selector = null; - _tooltip = null; + private _target: Element | null = null; + private selector: string | null = null; + private tooltip: HTMLDivElement + private marker: HTMLDivElement - constructor(overlay, screen) { - this.screen = screen; - - this._tooltip = document.createElement('div'); - this._tooltip.className = styles.tooltip; - this._tooltip.appendChild(document.createElement('div')); + constructor(overlay: HTMLElement, private readonly screen: BaseScreen) { + this.tooltip = document.createElement('div'); + this.tooltip.className = styles.tooltip; + this.tooltip.appendChild(document.createElement('div')); const htmlStr = document.createElement('div'); htmlStr.innerHTML = 'Right-click > Inspect for more details.'; - this._tooltip.appendChild(htmlStr); + this.tooltip.appendChild(htmlStr); const marker = document.createElement('div'); marker.className = styles.marker; @@ -43,34 +43,34 @@ export default class Marker { marker.appendChild(markerT); marker.appendChild(markerB); - marker.appendChild(this._tooltip); + marker.appendChild(this.tooltip); overlay.appendChild(marker); - this._marker = marker; + this.marker = marker; } get target() { return this._target; } - mark(element) { + mark(element: Element | null) { if (this._target === element) { return; } this._target = element; - this._selector = null; + this.selector = null; this.redraw(); } unmark() { - this.mark(null); + this.mark(null) } - _autodefineTarget() { + private autodefineTarget() { // TODO: put to Screen - if (this._selector) { + if (this.selector) { try { - const fitTargets = this.screen.document.querySelectorAll(this._selector); + const fitTargets = this.screen.document.querySelectorAll(this.selector); if (fitTargets.length === 0) { this._target = null; } else { @@ -90,9 +90,9 @@ export default class Marker { } } - markBySelector(selector) { - this._selector = selector; - this._autodefineTarget(); + markBySelector(selector: string) { + this.selector = selector; + this.autodefineTarget(); this.redraw(); } @@ -116,20 +116,20 @@ export default class Marker { } redraw() { - if (this._selector) { - this._autodefineTarget(); + if (this.selector) { + this.autodefineTarget(); } if (!this._target) { - this._marker.style.display = 'none'; + this.marker.style.display = 'none'; return; } const rect = this._target.getBoundingClientRect(); - this._marker.style.display = 'block'; - this._marker.style.left = rect.left + 'px'; - this._marker.style.top = rect.top + 'px'; - this._marker.style.width = rect.width + 'px'; - this._marker.style.height = rect.height + 'px'; + this.marker.style.display = 'block'; + this.marker.style.left = rect.left + 'px'; + this.marker.style.top = rect.top + 'px'; + this.marker.style.width = rect.width + 'px'; + this.marker.style.height = rect.height + 'px'; - this._tooltip.firstChild.innerHTML = this.getTagString(this._target); + this.tooltip.firstChild.innerHTML = this.getTagString(this._target); } } diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts b/frontend/app/player/_web/Screen/Screen.ts similarity index 91% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts rename to frontend/app/player/_web/Screen/Screen.ts index a0ae8a800..59986423e 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts +++ b/frontend/app/player/_web/Screen/Screen.ts @@ -2,7 +2,6 @@ import Marker from './Marker'; import Cursor from './Cursor'; import Inspector from './Inspector'; // import styles from './screen.module.css'; -// import { getState } from '../../../store'; import BaseScreen from './BaseScreen'; export { INITIAL_STATE } from './BaseScreen'; @@ -12,7 +11,7 @@ export default class Screen extends BaseScreen { public readonly cursor: Cursor; private substitutor: BaseScreen | null = null; private inspector: Inspector | null = null; - private marker: Marker | null = null; + public marker: Marker | null = null; constructor() { super(); this.cursor = new Cursor(this.overlay); @@ -26,14 +25,14 @@ export default class Screen extends BaseScreen { return this.getElementsFromInternalPoint(this.cursor.getPosition()); } - _scale() { - super._scale(); + scale(dims: { height: number, width: number }) { + super.scale(dims) if (this.substitutor) { - this.substitutor._scale(); + this.substitutor.scale(dims) } } - enableInspector(clickCallback: ({ target: Element }) => void): Document | null { + enableInspector(clickCallback: (e: { target: Element }) => void): Document | null { if (!this.parentElement) return null; if (!this.substitutor) { this.substitutor = new Screen(); diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css b/frontend/app/player/_web/Screen/cursor.module.css similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css rename to frontend/app/player/_web/Screen/cursor.module.css diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/index.js b/frontend/app/player/_web/Screen/index.js similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/index.js rename to frontend/app/player/_web/Screen/index.js diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/marker.module.css b/frontend/app/player/_web/Screen/marker.module.css similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/marker.module.css rename to frontend/app/player/_web/Screen/marker.module.css diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.module.css b/frontend/app/player/_web/Screen/screen.module.css similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.module.css rename to frontend/app/player/_web/Screen/screen.module.css diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/types.ts b/frontend/app/player/_web/Screen/types.ts similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/types.ts rename to frontend/app/player/_web/Screen/types.ts diff --git a/frontend/app/player/_web/WebPlayer.ts b/frontend/app/player/_web/WebPlayer.ts new file mode 100644 index 000000000..a48577288 --- /dev/null +++ b/frontend/app/player/_web/WebPlayer.ts @@ -0,0 +1,178 @@ +import type { Store } from '../player/types' +import Player, { State as PlayerState } from '../player/Player' + +import MessageManager from './MessageManager' +import AssistManager from './assist/AssistManager' +import Screen from './Screen/Screen' +import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './MessageManager' + + + +export default class WebPlayer extends Player { + private readonly screen: Screen + private readonly messageManager: MessageManager + + assistManager: AssistManager // public so far + + constructor(private wpState: Store, session, config, live: boolean) { + // TODO: separate screen from manager + const screen = new MessageManager(session, wpState, config, live) + super(wpState, screen) + this.screen = screen + this.messageManager = screen + + // TODO: separate LiveWebPlayer + 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, + livePlay: live, + endTime, // : 0, //TODO: through initialState + }) + + if (live) { + this.assistManager.connect(session.agentToken) + } + } + + attach(parent: HTMLElement) { + this.screen.attach(parent) + window.addEventListener('resize', this.scale) + this.scale() + } + scale = () => { + const { width, height } = this.wpState.get() + this.screen.scale({ width, height }) + } + mark(e: Element) { + this.screen.marker.mark(e) + } + + updateMarketTargets() { + // const { markedTargets } = getState(); + // if (markedTargets) { + // update({ + // markedTargets: markedTargets.map((mt: any) => ({ + // ...mt, + // boundingRect: this.calculateRelativeBoundingRect(mt.el), + // })), + // }); + // } + } + + // private calculateRelativeBoundingRect(el: Element): BoundingRect { + // if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO + // const { top, left, width, height } = el.getBoundingClientRect(); + // const s = this.getScale(); + // const scrinRect = this.screen.getBoundingClientRect(); + // const parentRect = this.parentElement.getBoundingClientRect(); + + // return { + // top: top*s + scrinRect.top - parentRect.top, + // left: left*s + scrinRect.left - parentRect.left, + // width: width*s, + // height: height*s, + // } + // } + + setActiveTarget(index: number) { + // const window = this.window + // const markedTargets: MarkedTarget[] | null = getState().markedTargets + // const target = markedTargets && markedTargets[index] + // if (target && window) { + // const { fixedTop, rect } = getOffset(target.el, window) + // const scrollToY = fixedTop - window.innerHeight / 1.5 + // if (rect.top < 0 || rect.top > window.innerHeight) { + // // behavior hack TODO: fix it somehow when they will decide to remove it from browser api + // // @ts-ignore + // window.scrollTo({ top: scrollToY, behavior: 'instant' }) + // setTimeout(() => { + // if (!markedTargets) { return } + // update({ + // markedTargets: markedTargets.map(t => t === target ? { + // ...target, + // boundingRect: this.calculateRelativeBoundingRect(target.el), + // } : t) + // }) + // }, 0) + // } + + // } + // update({ activeTargetIndex: index }); + } + + // private actualScroll: Point | null = null + setMarkedTargets(selections: { selector: string, count: number }[] | null) { + // if (selections) { + // const totalCount = selections.reduce((a, b) => { + // return a + b.count + // }, 0); + // const markedTargets: MarkedTarget[] = []; + // let index = 0; + // selections.forEach((s) => { + // const el = this.getElementBySelector(s.selector); + // if (!el) return; + // markedTargets.push({ + // ...s, + // el, + // index: index++, + // percent: Math.round((s.count * 100) / totalCount), + // boundingRect: this.calculateRelativeBoundingRect(el), + // count: s.count, + // }) + // }); + // this.actualScroll = this.getCurrentScroll() + // update({ markedTargets }); + // } else { + // if (this.actualScroll) { + // this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) + // this.actualScroll = null + // } + // update({ markedTargets: null }); + // } + } + + 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 }); + // } + } + + async toggleTimetravel() { + if (!this.wpState.get().liveTimeTravel) { + return await this.messageManager.reloadWithUnprocessedFile() + } + } + + toggleUserName(name?: string) { + this.screen.cursor.toggleUserName(name) + } + clean() { + super.clean() + this.assistManager.clean() + window.removeEventListener('resize', this.scale) + } +} + diff --git a/frontend/app/player/MessageDistributor/managers/AnnotationCanvas.ts b/frontend/app/player/_web/assist/AnnotationCanvas.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/AnnotationCanvas.ts rename to frontend/app/player/_web/assist/AnnotationCanvas.ts diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/_web/assist/AssistManager.ts similarity index 87% rename from frontend/app/player/MessageDistributor/managers/AssistManager.ts rename to frontend/app/player/_web/assist/AssistManager.ts index 897d8dfba..1e1274226 100644 --- a/frontend/app/player/MessageDistributor/managers/AssistManager.ts +++ b/frontend/app/player/_web/assist/AssistManager.ts @@ -1,11 +1,10 @@ import type { Socket } from 'socket.io-client'; import type Peer from 'peerjs'; import type { MediaConnection } from 'peerjs'; -import type MessageDistributor from '../MessageDistributor'; -import store from 'App/store'; +import type MessageManager from '../MessageManager'; +import appStore from 'App/store'; import type { LocalStream } from './LocalStream'; -import { update, getState } from '../../store'; -// import { iceServerConfigFromString } from 'App/utils' +import type { Store } from '../../player/types' import AnnotationCanvas from './AnnotationCanvas'; import MStreamReader from '../messages/MStreamReader'; import JSONRawMessageReader from '../messages/JSONRawMessageReader' @@ -76,10 +75,15 @@ export default class AssistManager { private videoStreams: Record = {} // TODO: Session type - constructor(private session: any, private md: MessageDistributor, private config: any) {} + constructor( + private session: any, + private md: MessageManager, + private config: any, + private store: Store, + ) {} private setStatus(status: ConnectionStatus) { - if (getState().peerConnectionStatus === ConnectionStatus.Disconnected && + if (this.store.get().peerConnectionStatus === ConnectionStatus.Disconnected && status !== ConnectionStatus.Connected) { return } @@ -94,7 +98,7 @@ export default class AssistManager { } else { this.md.display(false); } - update({ peerConnectionStatus: status }); + this.store.update({ peerConnectionStatus: status }); } private get peerID(): string { @@ -106,7 +110,7 @@ export default class AssistManager { this.socketCloseTimeout && clearTimeout(this.socketCloseTimeout) if (document.hidden) { this.socketCloseTimeout = setTimeout(() => { - const state = getState() + const state = this.store.get() if (document.hidden && (state.calling === CallingState.NoCall && state.remoteControl === RemoteControlStatus.Enabled)) { this.socket?.close() @@ -134,7 +138,7 @@ export default class AssistManager { } const now = +new Date() - update({ assistStart: now }) + this.store.update({ assistStart: now }) import('socket.io-client').then(({ default: io }) => { if (this.cleaned) { return } @@ -162,7 +166,7 @@ export default class AssistManager { }) socket.on("disconnect", () => { this.toggleRemoteControl(false) - update({ calling: CallingState.NoCall }) + this.store.update({ calling: CallingState.NoCall }) }) socket.on('messages', messages => { jmr.append(messages) // as RawMessage[] @@ -172,7 +176,7 @@ export default class AssistManager { this.setStatus(ConnectionStatus.Connected) // Call State - if (getState().calling === CallingState.Reconnecting) { + if (this.store.get().calling === CallingState.Reconnecting) { this._callSessionPeer() // reconnecting call (todo improve code separation) } } @@ -222,15 +226,15 @@ export default class AssistManager { this.setStatus(ConnectionStatus.Disconnected) }, 30000) - if (getState().remoteControl === RemoteControlStatus.Requesting) { + if (this.store.get().remoteControl === RemoteControlStatus.Requesting) { this.toggleRemoteControl(false) // else its remaining } // Call State - if (getState().calling === CallingState.OnCall) { - update({ calling: CallingState.Reconnecting }) - } else if (getState().calling === CallingState.Requesting){ - update({ calling: CallingState.NoCall }) + if (this.store.get().calling === CallingState.OnCall) { + this.store.update({ calling: CallingState.Reconnecting }) + } else if (this.store.get().calling === CallingState.Requesting){ + this.store.update({ calling: CallingState.NoCall }) } }) socket.on('error', e => { @@ -263,7 +267,7 @@ export default class AssistManager { private onMouseClick = (e: MouseEvent): void => { if (!this.socket) { return; } - if (getState().annotating) { return; } // ignore clicks while annotating + if (this.store.get().annotating) { return; } // ignore clicks while annotating const data = this.md.getInternalViewportCoordinates(e) // const el = this.md.getElementFromPoint(e); // requires requestiong node_id from domManager @@ -299,31 +303,31 @@ export default class AssistManager { this.md.overlay.addEventListener("click", this.onMouseClick) this.md.overlay.addEventListener("wheel", this.onWheel) this.md.toggleRemoteControlStatus(true) - update({ remoteControl: RemoteControlStatus.Enabled }) + 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) - update({ remoteControl: RemoteControlStatus.Disabled }) + this.store.update({ remoteControl: RemoteControlStatus.Disabled }) this.toggleAnnotation(false) } } requestReleaseRemoteControl = () => { if (!this.socket) { return } - const remoteControl = getState().remoteControl + const remoteControl = this.store.get().remoteControl if (remoteControl === RemoteControlStatus.Requesting) { return } if (remoteControl === RemoteControlStatus.Disabled) { - update({ remoteControl: RemoteControlStatus.Requesting }) + this.store.update({ remoteControl: RemoteControlStatus.Requesting }) this.socket.emit("request_control", JSON.stringify({ ...this.session.agentInfo, query: document.location.search })) // setTimeout(() => { - // if (getState().remoteControl !== RemoteControlStatus.Requesting) { return } + // if (this.store.get().remoteControl !== RemoteControlStatus.Requesting) { return } // this.socket?.emit("release_control") - // update({ remoteControl: RemoteControlStatus.Disabled }) + // this.store.update({ remoteControl: RemoteControlStatus.Disabled }) // }, 8000) } else { this.socket.emit("release_control") @@ -416,15 +420,15 @@ export default class AssistManager { private handleCallEnd() { this.callArgs && this.callArgs.onCallEnd() this.callConnection[0] && this.callConnection[0].close() - update({ calling: CallingState.NoCall }) + this.store.update({ calling: CallingState.NoCall }) this.callArgs = null this.toggleAnnotation(false) } public initiateCallEnd = async () => { - this.socket?.emit("call_end", store.getState().getIn([ 'user', 'account', 'name'])) + this.socket?.emit("call_end", appStore.getState().getIn([ 'user', 'account', 'name'])) this.handleCallEnd() - const remoteControl = getState().remoteControl + const remoteControl = this.store.get().remoteControl if (remoteControl === RemoteControlStatus.Enabled) { this.socket.emit("release_control") this.toggleRemoteControl(false) @@ -432,10 +436,10 @@ export default class AssistManager { } private onRemoteCallEnd = () => { - if (getState().calling === CallingState.Requesting) { + if (this.store.get().calling === CallingState.Requesting) { this.callArgs && this.callArgs.onReject() this.callConnection[0] && this.callConnection[0].close() - update({ calling: CallingState.NoCall }) + this.store.update({ calling: CallingState.NoCall }) this.callArgs = null this.toggleAnnotation(false) } else { @@ -487,10 +491,10 @@ export default class AssistManager { /** Connecting to the app user */ private _callSessionPeer() { - if (![CallingState.NoCall, CallingState.Reconnecting].includes(getState().calling)) { return } - update({ calling: CallingState.Connecting }) + if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) { return } + this.store.update({ calling: CallingState.Connecting }) this._peerConnection(this.peerID); - this.socket && this.socket.emit("_agent_name", store.getState().getIn([ 'user', 'account', 'name'])) + this.socket && this.socket.emit("_agent_name", appStore.getState().getIn([ 'user', 'account', 'name'])) } private async _peerConnection(remotePeerId: string) { @@ -509,7 +513,7 @@ export default class AssistManager { }) call.on('stream', stream => { - getState().calling !== CallingState.OnCall && update({ calling: CallingState.OnCall }) + this.store.get().calling !== CallingState.OnCall && this.store.update({ calling: CallingState.OnCall }) this.videoStreams[call.peer] = stream.getVideoTracks()[0] @@ -529,9 +533,9 @@ export default class AssistManager { } toggleAnnotation(enable?: boolean) { - // if (getState().calling !== CallingState.OnCall) { return } + // if (this.store.get().calling !== CallingState.OnCall) { return } if (typeof enable !== "boolean") { - enable = !!getState().annotating + enable = !!this.store.get().annotating } if (enable && !this.annot) { const annot = this.annot = new AnnotationCanvas() @@ -559,11 +563,11 @@ export default class AssistManager { annot.move([ data.x, data.y ]) this.socket.emit("moveAnnotation", [ data.x, data.y ]) }) - update({ annotating: true }) + this.store.update({ annotating: true }) } else if (!enable && !!this.annot) { this.annot.remove() this.annot = null - update({ annotating: false }) + this.store.update({ annotating: false }) } } @@ -577,7 +581,7 @@ export default class AssistManager { /* ==== Cleaning ==== */ private cleaned: boolean = false - clear() { + clean() { this.cleaned = true // sometimes cleaned before modules loaded this.initiateCallEnd(); if (this._peer) { diff --git a/frontend/app/player/MessageDistributor/managers/LocalStream.ts b/frontend/app/player/_web/assist/LocalStream.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/LocalStream.ts rename to frontend/app/player/_web/assist/LocalStream.ts diff --git a/frontend/app/player/MessageDistributor/managers/ActivityManager.ts b/frontend/app/player/_web/managers/ActivityManager.ts similarity index 83% rename from frontend/app/player/MessageDistributor/managers/ActivityManager.ts rename to frontend/app/player/_web/managers/ActivityManager.ts index 412fefdce..32265f924 100644 --- a/frontend/app/player/MessageDistributor/managers/ActivityManager.ts +++ b/frontend/app/player/_web/managers/ActivityManager.ts @@ -1,18 +1,18 @@ -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; class SkipIntervalCls { - constructor(private readonly start = 0, private readonly end = 0) {} + constructor(readonly start = 0, readonly end = 0) {} get time(): number { return this.start; } - contains(ts: number) { + contains(ts) { return ts > this.start && ts < this.end; } } -export type SkipInterval = InstanceType; // exporting only class' type +export type SkipInterval = InstanceType; export default class ActivityManager extends ListWalker { diff --git a/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts b/frontend/app/player/_web/managers/DOM/DOMManager.ts similarity index 99% rename from frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts rename to frontend/app/player/_web/managers/DOM/DOMManager.ts index 2688abb14..370df5c03 100644 --- a/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts +++ b/frontend/app/player/_web/managers/DOM/DOMManager.ts @@ -1,9 +1,9 @@ import logger from 'App/logger'; -import type StatedScreen from '../../StatedScreen'; +import type MessageManager from '../../MessageManager'; import type { Message, SetNodeScroll, CreateElementNode } from '../../messages'; -import ListWalker from '../ListWalker'; +import ListWalker from '../../../_common/ListWalker'; import StylesManager, { rewriteNodeStyleSheet } from './StylesManager'; import FocusManager from './FocusManager'; import { @@ -51,7 +51,7 @@ export default class DOMManager extends ListWalker { constructor( - private readonly screen: StatedScreen, + private readonly screen: MessageManager, private readonly isMobile: boolean, public readonly time: number ) { diff --git a/frontend/app/player/MessageDistributor/managers/DOM/FocusManager.ts b/frontend/app/player/_web/managers/DOM/FocusManager.ts similarity index 93% rename from frontend/app/player/MessageDistributor/managers/DOM/FocusManager.ts rename to frontend/app/player/_web/managers/DOM/FocusManager.ts index 6f80ed16c..629f625e3 100644 --- a/frontend/app/player/MessageDistributor/managers/DOM/FocusManager.ts +++ b/frontend/app/player/_web/managers/DOM/FocusManager.ts @@ -1,7 +1,7 @@ import logger from 'App/logger'; import type { SetNodeFocus } from '../../messages'; import type { VElement } from './VirtualDOM'; -import ListWalker from '../ListWalker'; +import ListWalker from '../../../_common/ListWalker'; const FOCUS_CLASS = "-openreplay-focus" diff --git a/frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts b/frontend/app/player/_web/managers/DOM/StylesManager.ts similarity index 95% rename from frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts rename to frontend/app/player/_web/managers/DOM/StylesManager.ts index b6dddcdd9..1cb49c5dd 100644 --- a/frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts +++ b/frontend/app/player/_web/managers/DOM/StylesManager.ts @@ -1,9 +1,9 @@ -import type StatedScreen from '../../StatedScreen'; +import type MessageManager from '../../MessageManager'; import type { CssInsertRule, CssDeleteRule } from '../../messages'; type CSSRuleMessage = CssInsertRule | CssDeleteRule; -import ListWalker from '../ListWalker'; +import ListWalker from '../../../_common/ListWalker'; const HOVER_CN = "-openreplay-hover"; @@ -26,7 +26,7 @@ export default class StylesManager extends ListWalker { private linkLoadPromises: Array> = []; private skipCSSLinks: Array = []; // should be common for all pages - constructor(private readonly screen: StatedScreen) { + constructor(private readonly screen: MessageManager) { super(); } diff --git a/frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts b/frontend/app/player/_web/managers/DOM/VirtualDOM.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts rename to frontend/app/player/_web/managers/DOM/VirtualDOM.ts diff --git a/frontend/app/player/MessageDistributor/managers/DOM/safeCSSRules.ts b/frontend/app/player/_web/managers/DOM/safeCSSRules.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/DOM/safeCSSRules.ts rename to frontend/app/player/_web/managers/DOM/safeCSSRules.ts diff --git a/frontend/app/player/MessageDistributor/managers/MouseMoveManager.ts b/frontend/app/player/_web/managers/MouseMoveManager.ts similarity index 89% rename from frontend/app/player/MessageDistributor/managers/MouseMoveManager.ts rename to frontend/app/player/_web/managers/MouseMoveManager.ts index ca9e3b740..5dd3160a5 100644 --- a/frontend/app/player/MessageDistributor/managers/MouseMoveManager.ts +++ b/frontend/app/player/_web/managers/MouseMoveManager.ts @@ -1,7 +1,7 @@ -import type StatedScreen from '../StatedScreen'; +import type Screen from '../Screen/Screen'; import type { MouseMove } from '../messages'; -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; const HOVER_CLASS = "-openreplay-hover"; const HOVER_CLASS_DEPR = "-asayer-hover"; @@ -9,7 +9,7 @@ const HOVER_CLASS_DEPR = "-asayer-hover"; export default class MouseMoveManager extends ListWalker { private hoverElements: Array = []; - constructor(private screen: StatedScreen) {super()} + constructor(private screen: Screen) {super()} private updateHover(): void { const curHoverElements = this.screen.getCursorTargets(); diff --git a/frontend/app/player/MessageDistributor/managers/PagesManager.ts b/frontend/app/player/_web/managers/PagesManager.ts similarity index 85% rename from frontend/app/player/MessageDistributor/managers/PagesManager.ts rename to frontend/app/player/_web/managers/PagesManager.ts index 9a4398246..6a3f20f7c 100644 --- a/frontend/app/player/MessageDistributor/managers/PagesManager.ts +++ b/frontend/app/player/_web/managers/PagesManager.ts @@ -1,7 +1,7 @@ -import type StatedScreen from '../StatedScreen'; +import type Screen from '../Screen/Screen'; import type { Message } from '../messages'; -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; import DOMManager from './DOM/DOMManager'; @@ -9,9 +9,9 @@ export default class PagesManager extends ListWalker { private currentPage: DOMManager | null = null private isMobile: boolean; - private screen: StatedScreen; + private screen: Screen; - constructor(screen: StatedScreen, isMobile: boolean) { + constructor(screen: Screen, isMobile: boolean) { super() this.screen = screen this.isMobile = isMobile diff --git a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts b/frontend/app/player/_web/managers/PerformanceTrackManager.ts similarity index 98% rename from frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts rename to frontend/app/player/_web/managers/PerformanceTrackManager.ts index c4a4a8e63..424466b4a 100644 --- a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts +++ b/frontend/app/player/_web/managers/PerformanceTrackManager.ts @@ -1,6 +1,6 @@ import type { PerformanceTrack, SetPageVisibility } from '../messages'; -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; export type PerformanceChartPoint = { time: number, diff --git a/frontend/app/player/MessageDistributor/managers/ReduxStateManager.ts b/frontend/app/player/_web/managers/ReduxStateManager.ts similarity index 96% rename from frontend/app/player/MessageDistributor/managers/ReduxStateManager.ts rename to frontend/app/player/_web/managers/ReduxStateManager.ts index 81638994e..4756b303a 100644 --- a/frontend/app/player/MessageDistributor/managers/ReduxStateManager.ts +++ b/frontend/app/player/_web/managers/ReduxStateManager.ts @@ -1,5 +1,5 @@ // import { applyChange, revertChange } from 'deep-diff'; -// import ListWalker from './ListWalker'; +// import ListWalker from '../../_common/ListWalker'; // import type { Redux } from '../messages'; // export default class ReduxStateManager extends ListWalker { diff --git a/frontend/app/player/MessageDistributor/managers/WindowNodeCounter.ts b/frontend/app/player/_web/managers/WindowNodeCounter.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/WindowNodeCounter.ts rename to frontend/app/player/_web/managers/WindowNodeCounter.ts diff --git a/frontend/app/player/MessageDistributor/messages/JSONRawMessageReader.ts b/frontend/app/player/_web/messages/JSONRawMessageReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/JSONRawMessageReader.ts rename to frontend/app/player/_web/messages/JSONRawMessageReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/MFileReader.ts b/frontend/app/player/_web/messages/MFileReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/MFileReader.ts rename to frontend/app/player/_web/messages/MFileReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/MStreamReader.ts b/frontend/app/player/_web/messages/MStreamReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/MStreamReader.ts rename to frontend/app/player/_web/messages/MStreamReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/PrimitiveReader.ts b/frontend/app/player/_web/messages/PrimitiveReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/PrimitiveReader.ts rename to frontend/app/player/_web/messages/PrimitiveReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/RawMessageReader.ts b/frontend/app/player/_web/messages/RawMessageReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/RawMessageReader.ts rename to frontend/app/player/_web/messages/RawMessageReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/index.ts b/frontend/app/player/_web/messages/index.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/index.ts rename to frontend/app/player/_web/messages/index.ts diff --git a/frontend/app/player/MessageDistributor/messages/message.ts b/frontend/app/player/_web/messages/message.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/message.ts rename to frontend/app/player/_web/messages/message.ts diff --git a/frontend/app/player/MessageDistributor/messages/raw.ts b/frontend/app/player/_web/messages/raw.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/raw.ts rename to frontend/app/player/_web/messages/raw.ts diff --git a/frontend/app/player/MessageDistributor/messages/timed.ts b/frontend/app/player/_web/messages/timed.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/timed.ts rename to frontend/app/player/_web/messages/timed.ts diff --git a/frontend/app/player/MessageDistributor/messages/tracker-legacy.ts b/frontend/app/player/_web/messages/tracker-legacy.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/tracker-legacy.ts rename to frontend/app/player/_web/messages/tracker-legacy.ts diff --git a/frontend/app/player/MessageDistributor/messages/tracker.ts b/frontend/app/player/_web/messages/tracker.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/tracker.ts rename to frontend/app/player/_web/messages/tracker.ts diff --git a/frontend/app/player/MessageDistributor/messages/urlResolve.ts b/frontend/app/player/_web/messages/urlResolve.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/urlResolve.ts rename to frontend/app/player/_web/messages/urlResolve.ts diff --git a/frontend/app/player/MessageDistributor/network/crypto.ts b/frontend/app/player/_web/network/crypto.ts similarity index 100% rename from frontend/app/player/MessageDistributor/network/crypto.ts rename to frontend/app/player/_web/network/crypto.ts diff --git a/frontend/app/player/MessageDistributor/network/loadFiles.ts b/frontend/app/player/_web/network/loadFiles.ts similarity index 100% rename from frontend/app/player/MessageDistributor/network/loadFiles.ts rename to frontend/app/player/_web/network/loadFiles.ts diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts new file mode 100644 index 000000000..c42815eae --- /dev/null +++ b/frontend/app/player/create.ts @@ -0,0 +1,25 @@ +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 WebPlayer from './_web/WebPlayer' + +export function createWebPlayer(session, config): [WebPlayer, Store] { + const store = new SimpleStore({ + ...PLAYER_INITIAL_STATE, + ...MM_INITIAL_STATE, + }) + const player = new WebPlayer(store, session, config, false) + return [player, store] +} + + +export function createLiveWebPlayer(session, config): [WebPlayer, Store] { + const store = new SimpleStore({ + ...PLAYER_INITIAL_STATE, + ...MM_INITIAL_STATE, + }) + const player = new WebPlayer(store, session, config, true) + return [player, store] +} \ No newline at end of file diff --git a/frontend/app/player/index.js b/frontend/app/player/index.js index c49d75294..c40e2f26c 100644 --- a/frontend/app/player/index.js +++ b/frontend/app/player/index.js @@ -1,4 +1,6 @@ -export * from './store'; -export * from './singletone'; -export * from './MessageDistributor/managers/AssistManager'; -export * from './MessageDistributor/managers/LocalStream'; \ No newline at end of file +export * from './_store'; +export * from './_web/assist/AssistManager'; +export * from './_web/assist/LocalStream'; +export * from './_singletone'; + +export * from './create'; \ No newline at end of file diff --git a/frontend/app/player/ios/ImagePlayer.js b/frontend/app/player/ios/ImagePlayer.js deleted file mode 100644 index 1dcf2b9cc..000000000 --- a/frontend/app/player/ios/ImagePlayer.js +++ /dev/null @@ -1,454 +0,0 @@ -import { io } from 'socket.io-client'; -import { makeAutoObservable, autorun } from 'mobx'; -import logger from 'App/logger'; -import { - createPlayerState, - createToolPanelState, - createToggleState, - PLAYING, - PAUSED, - COMPLETED, - SOCKET_ERROR, - - CRASHES, - LOGS, - NETWORK, - PERFORMANCE, - CUSTOM, - EVENTS, // last evemt +clicks -} from "./state"; -import { - createListState, - createScreenListState, -} from './lists'; -import Parser from './Parser'; -import PerformanceList from './PerformanceList'; - -const HIGHEST_SPEED = 3; - - -export default class ImagePlayer { - _screen = null - _wrapper = null - _socket = null - toolPanel = createToolPanelState() - fullscreen = createToggleState() - lists = { - [LOGS]: createListState(), - [NETWORK]: createListState(), - [CRASHES]: createListState(), - [EVENTS]: createListState(), - [CUSTOM]: createListState(), - [PERFORMANCE]: new PerformanceList(), - } - _clicks = createListState() - _screens = createScreenListState() - - constructor(session) { - this.state = createPlayerState({ - endTime: session.duration.valueOf(), - }); - //const canvas = document.createElement("canvas"); - // this._context = canvas.getContext('2d'); - // this._img = new Image(); - // this._img..onerror = function(e){ - // logger.log('Error during loading image:', e); - // }; - // wrapper.appendChild(this._img); - session.crashes.forEach(c => this.lists[CRASHES].append(c)); - session.events.forEach(e => this.lists[EVENTS].append(e)); - session.stackEvents.forEach(e => this.lists[CUSTOM].append(e)); - window.fetch(session.mobsUrl) - .then(r => r.arrayBuffer()) - .then(b => { - new Parser(new Uint8Array(b)).parseEach(m => { - m.time = m.timestamp - session.startedAt; - try { - if (m.tp === "ios_log") { - this.lists[LOGS].append(m); - } else if (m.tp === "ios_network_call") { - this.lists[NETWORK].append(m); - // } else if (m.tp === "ios_custom_event") { - // this.lists[CUSTOM].append(m); - } else if (m.tp === "ios_click_event") { - m.time -= 600; //for graphic initiation - this._clicks.append(m); - } else if (m.tp === "ios_performance_event") { - this.lists[PERFORMANCE].append(m); - } - } catch (e) { - logger.error(e); - } - }); - Object.values(this.lists).forEach(list => list.moveGetLast(0)); // In case of negative values - }) - - if (session.socket == null || typeof session.socket.jwt !== "string" || typeof session.socket.url !== "string") { - logger.error("No socket info found fpr session", session); - return - } - - const options = { - extraHeaders: {Authorization: `Bearer ${session.socket.jwt}`}, - reconnectionAttempts: 5, - //transports: ['websocket'], - } - - const socket = this._socket = io(session.socket.url, options); - socket.on("connect", () => { - logger.log("Socket Connected"); - }); - - socket.on('disconnect', (reason) => { - if (reason === 'io client disconnect') { - return; - } - logger.error("Disconnected. Reason: ", reason) - // if (reason === 'io server disconnect') { - // socket.connect(); - // } - }); - socket.on('connect_error', (e) => { - this.state.setState(SOCKET_ERROR); - logger.error(e) - }); - - socket.on('screen', (time, width, height, binary) => { - //logger.log("New Screen!", time, width, height, binary); - this._screens.insertScreen(time, width, height, binary); - }); - socket.on('buffered', (playTime) => { - if (playTime === this.state.time) { - this.state.setBufferingState(false); - } - logger.log("Play ack!", playTime); - }); - - let startPingInterval; - socket.on('start', () => { - logger.log("Started!"); - clearInterval(startPingInterval) - this.state.setBufferingState(true); - socket.emit("speed", this.state.speed); - this.play(); - }); - startPingInterval = setInterval(() => socket.emit("start"), 1000); - socket.emit("start"); - - window.addEventListener("resize", this.scale); - autorun(this.scale); - } - - _click - _getClickElement() { - if (this._click != null) { - return this._click; - } - const click = document.createElement('div'); - click.style.position = "absolute"; - click.style.background = "#ddd"; - click.style.border = "solid 4px #bbb"; - click.style.borderRadius = "50%"; - click.style.width = "32px"; - click.style.height = "32px"; - click.style.transformOrigin = "center"; - return this._click = click; - } - // More sufficient ways? - _animateClick({ x, y }) { - if (this._screen == null) { - return; - } - const click = this._getClickElement(); - if (click.parentElement == null) { - this._screen.appendChild(click); - } - click.style.transition = "none"; - click.style.left = `${x-18}px`; - click.style.top = `${y-18}px`; - click.style.transform = "scale(1)"; - click.style.opacity = "1"; - setTimeout(() => { - click.style.transition = "all ease-in .5s"; - click.style.transform = "scale(0)"; - click.style.opacity = "0"; - }, 0) - } - - _updateFrame({ image, width, height }) { - // const img = new Image(); - // img.onload = () => { - // this._context.drawImage(img); - // }; - // img.onerror = function(e){ - // logger.log('Error during loading image:', e); - // }; - // this._screen.style.backgroundImage = `url(${binaryToDataURL(binaryArray)})`; - this._canvas.getContext('2d').drawImage(image, 0, 0, this._canvas.width, this._canvas.height); - } - - _setTime(ts) { - ts = Math.max(Math.min(ts, this.state.endTime), 0); - this.state.setTime(ts); - Object.values(this.lists).forEach(list => list.moveGetLast(ts)); - const screen = this._screens.moveGetLast(ts); - if (screen != null) { - const { dataURL, width, height } = screen; - this.state.setSize(width, height); - //imagePromise.then(() => this._updateFrame({ image, width, height })); - //this._screen.style.backgroundImage = `url(${screen.dataURL})`; - screen.loadImage.then(() => this._screen.style.backgroundImage = `url(${screen.dataURL})`); - } - const lastClick = this._clicks.moveGetLast(ts); - if (lastClick != null && lastClick.time > ts - 600) { - this._animateClick(lastClick); - } - } - - attach({ wrapperId, screenId }) { - const screen = document.getElementById(screenId); - if (!screen) { - throw new Error(`ImagePlayer: No screen element found with ID "${screenId}" `); - } - const wrapper = document.getElementById(wrapperId); - if (!wrapper) { - throw new Error(`ImagePlayer: No wrapper element found with ID "${wrapperId}" `); - } - screen.style.backgroundSize = "contain"; - screen.style.backgroundPosition = "center"; - wrapper.style.position = "absolute"; - wrapper.style.transformOrigin = "left top"; - wrapper.style.top = "50%"; - wrapper.style.left = "50%"; - // const canvas = document.createElement('canvas'); - // canvas.style.width = "300px"; - // canvas.style.height = "600px"; - // screen.appendChild(canvas); - // this._canvas = canvas; - this._screen = screen; - this._wrapper = wrapper; - this.scale(); - } - - - get loading() { - return this.state.initializing; - } - - get buffering() { - return this.state.buffering; - } - - // get timeTravelDisabled() { - // return this.state.initializing; - // } - - get controlsDisabled() { - return this.state.initializing; //|| this.state.buffering; - } - - _animationFrameRequestId = null - _stopAnimation() { - cancelAnimationFrame(this._animationFrameRequestId); - } - _startAnimation() { - let prevTime = this.state.time; - let animationPrevTime = performance.now(); - const nextFrame = (animationCurrentTime) => { - const { - speed, - //skip, - //skipIntervals, - endTime, - playing, - buffering, - //live, - //livePlay, - //disconnected, - //messagesLoading, - //cssLoading, - } = this.state; - - const diffTime = !playing || buffering - ? 0 - : Math.max(animationCurrentTime - animationPrevTime, 0) * speed; - - let time = prevTime + diffTime; - - //const skipInterval = skip && skipIntervals.find(si => si.contains(time)); // TODO: good skip by messages - //if (skipInterval) time = skipInterval.end; - - //const fmt = this.getFirstMessageTime(); - //if (time < fmt) time = fmt; // ? - - //const lmt = this.getLastMessageTime(); - //if (livePlay && time < lmt) time = lmt; - // if (endTime < lmt) { - // update({ - // endTime: lmt, - // }); - // } - - prevTime = time; - animationPrevTime = animationCurrentTime; - - const completed = time >= endTime; - if (completed) { - this._setComplete(); - } else { - - // if (live && time > endTime) { - // update({ - // endTime: time, - // }); - // } - this._setTime(time); - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - } - }; - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - } - - - scale = () => { - const { height, width } = this.state; // should be before any return for mobx observing - if (this._wrapper === null) return; - const parent = this._wrapper.parentElement; - if (parent === null) return; - let s = 1; - const { offsetWidth, offsetHeight } = parent; - - s = Math.min(offsetWidth / width, (offsetHeight - 20) / height); - if (s > 1) { - s = 1; - } else { - s = Math.round(s * 1e3) / 1e3; - } - - this._wrapper.style.transform = `scale(${ s }) translate(-50%, -50%)`; - this._wrapper.style.width = width + 'px'; - this._wrapper.style.height = height + 'px'; - // this._canvas.style.width = width + 'px'; - // this._canvas.style.height = height + 'px'; - } - - _setComplete() { - this.state.setStatus(COMPLETED); - this._setTime(this.state.endTime); - if (this._socket != null) { - this._socket.emit("pause"); - } - } - - - _pause() { - this._stopAnimation(); - this.state.setStatus(PAUSED); - } - - pause = () => { - this._pause(); - if (this._socket != null) { - this._socket.emit("pause"); - } - } - - _play() { - if (!this.state.playing) { - this._startAnimation(); - } - this.state.setStatus(PLAYING); - } - play = () => { - this._play() - if (this._socket != null) { - this._socket.emit("resume"); - } - } - - _jump(ts) { - if (this.state.playing) { - this._stopAnimation(); - this._setTime(ts); - this._startAnimation(); - } else { - this._setTime(ts); - this.state.setStatus(PAUSED); // for the case when completed - } - } - jump = (ts) => { - ts = Math.round(ts); // Should be integer - this._jump(ts); - if (this._socket != null) { - this.state.setBufferingState(true); - console.log("Send play on jump!", ts) - this._socket.emit("jump", ts); - } - } - - togglePlay = () => { - if (this.state.playing) { - this.pause() - } else { - if (this.state.completed) { - //this.state.time = 0; - this.jump(0) - } - this.play() - } - } - backTenSeconds = () => { - this.jump(Math.max(this.state.time - 10000, 0)); - } - forthTenSeconds = () => { - this.jump(Math.min(this.state.time + 10000, this.state.endTime)); - } - - _setSpeed(speed) { - if (this._socket != null) { - this._socket.emit("speed", speed); - } - this.state.setSpeed(speed) - } - - toggleSpeed = () => { - const speed = this.state.speed; - this._setSpeed(speed < HIGHEST_SPEED ? speed + 1 : 1); - } - - speedUp = () => { - const speed = this.state.speed; - this._setSpeed(Math.min(HIGHEST_SPEED, speed + 1)); - } - - speedDown = () => { - const speed = this.state.speed; - this._setSpeed(Math.max(1, speed - 1)); - } - - togglePanel = (key) => { - this.toolPanel.toggle(key); - setTimeout(() => this.scale(), 0); - } - - closePanel = () => { - this.toolPanel.close(); - setTimeout(() => this.scale(), 0); - } - - toggleFullscreen = (flag = true) => { - this.fullscreen.toggle(flag); - setTimeout(() => this.scale(), 0); - } - - - clean() { - this._stopAnimation(); - if (this._socket != null) { - //this._socket.emit("close"); - this._socket.close(); - } - this._screens.clean(); - } -} - diff --git a/frontend/app/player/ios/Parser.ts b/frontend/app/player/ios/Parser.ts deleted file mode 100644 index 15b750df6..000000000 --- a/frontend/app/player/ios/Parser.ts +++ /dev/null @@ -1,34 +0,0 @@ -import RawMessageReader from '../MessageDistributor/messages/RawMessageReader'; - - -export default class Parser { - private reader: RawMessageReader - private error: boolean = false - constructor(byteArray) { - this.reader = new RawMessageReader(byteArray) - } - - parseEach(cb) { - while (this.hasNext()) { - const msg = this.next(); - if (msg !== null) { - cb(msg); - } - } - } - - hasNext() { - return !this.error && this.reader.hasNextByte(); - } - - next() { - try { - return this.reader.readMessage() - } catch(e) { - console.warn(e) - this.error = true - return null - } - } - -} \ No newline at end of file diff --git a/frontend/app/player/ios/PerformanceList.js b/frontend/app/player/ios/PerformanceList.js deleted file mode 100644 index 47d7918f3..000000000 --- a/frontend/app/player/ios/PerformanceList.js +++ /dev/null @@ -1,73 +0,0 @@ -import { - createListState, -} from './lists'; - -const MIN_INTERVAL = 500; - - -const NAME_MAP = { - "mainThreadCPU": "cpu", - "batteryLevel": "battery", - "memoryUsage": "memory", -} - -export default class PerformanceList { - _list = createListState() - availability = { - cpu: false, - memory: false, - battery: false, - } - - get list() { - return this._list.list; - } - - get count() { - return this._list.count; - } - - moveGetLast(t) { - this._list.moveGetLast(t); - } - - append(m) { - if (!["mainThreadCPU", "memoryUsage", "batteryLevel", "thermalState", "activeProcessorCount", "isLowPowerModeEnabled"].includes(m.name)) { - return; - } - - let lastPoint = Object.assign({ time: 0, cpu: null, battery: null, memory: null }, this._list.last); - if (this._list.length === 0) { - this._list.append(lastPoint); - } - - if (NAME_MAP[m.name] != null) { - this.availability[ NAME_MAP[m.name] ] = true; - if (lastPoint[NAME_MAP[m.name]] === null) { - this._list.forEach(p => p[NAME_MAP[m.name]] = m.value); - lastPoint[NAME_MAP[m.name]] = m.value; - } - } - - - const newPoint = Object.assign({}, lastPoint, { - time: m.time, - [ NAME_MAP[m.name] || m.name ]: m.value, - }); - - const dif = m.time - lastPoint.time; - const insertCount = Math.floor(dif/MIN_INTERVAL); - for (let i = 0; i < insertCount; i++){ - const evalValue = (key) => lastPoint[key] + Math.floor((newPoint[key]-lastPoint[key])/insertCount*(i + 1)) - this._list.append({ - ...lastPoint, - time: evalValue("time"), - cpu: evalValue("cpu") + (Math.floor(5*Math.random())-2), - battery: evalValue("battery"), - memory: evalValue("memory")*(1 + (0.1*Math.random() - 0.05)), - }); - } - - this._list.append(newPoint); - } -} \ No newline at end of file diff --git a/frontend/app/player/ios/ScreenList.ts b/frontend/app/player/ios/ScreenList.ts deleted file mode 100644 index ef6d1e73f..000000000 --- a/frontend/app/player/ios/ScreenList.ts +++ /dev/null @@ -1,57 +0,0 @@ -import ListWalker from '../MessageDistributor/managers/ListWalker'; - -//URL.revokeObjectURL() !! -function binaryToDataURL(arrayBuffer){ - var blob = new Blob([new Uint8Array(arrayBuffer)], {'type' : 'image/jpeg'}); - return URL.createObjectURL(blob); -} - -function prepareImage(width, height, arrayBuffer) { - const dataURL = binaryToDataURL(arrayBuffer); - return { - loadImage: new Promise(resolve => { - const img = new Image(); - img.onload = function() { - //URL.revokeObjectURL(this.src); - resolve(img); - }; - img.src = dataURL; - }).then(), - dataURL, - }; -} - -export default class ScreenList { - _walker = new ListWalker(); - _insertUnique(m) { - let p = this._walker._list.length; - while (p > 0 && this._walker._list[ p - 1 ].time > m.time) { - p--; - } - if (p > 0 && this._walker._list[ p - 1 ].time === m.time) { - return; - } - this._walker._list.splice(p, 0, m); - } - - moveGetLast(time) { - return this._walker.moveGetLast(time); - } - - insertScreen(time, width, height, arrayBuffer): void { - this._insertUnique({ - time, - width, - height, - ...prepareImage(width, height, arrayBuffer), - //image: new ImageData(new Uint8ClampedArray(arrayBuffer), width, height), - // dataURL: binaryToDataURL(arrayBuffer) - }); - } - - clean() { - this._walker.forEach(m => { - URL.revokeObjectURL(m.dataURL); - }); - } -} \ No newline at end of file diff --git a/frontend/app/player/ios/lists.js b/frontend/app/player/ios/lists.js deleted file mode 100644 index b0ddb9d0d..000000000 --- a/frontend/app/player/ios/lists.js +++ /dev/null @@ -1,12 +0,0 @@ -import { makeAutoObservable } from "mobx" -import ListWalker from '../MessageDistributor/managers/ListWalker'; - -import ScreenList from './ScreenList'; - -export function createListState(list) { - return makeAutoObservable(new ListWalker(list)); -} - -export function createScreenListState() { - return makeAutoObservable(new ScreenList()); -} \ No newline at end of file diff --git a/frontend/app/player/ios/state.js b/frontend/app/player/ios/state.js deleted file mode 100644 index 766ba08fe..000000000 --- a/frontend/app/player/ios/state.js +++ /dev/null @@ -1,112 +0,0 @@ -import { makeAutoObservable } from "mobx" - -//configure ({empceActions: true}) - -export const - NONE = 0, - CRASHES = 1, - NETWORK = 2, - LOGS = 3, - EVENTS = 4, - CUSTOM = 5, - PERFORMANCE = 6; - -export function createToolPanelState() { - return makeAutoObservable({ - key: NONE, - toggle(key) { // auto-bind?? - this.key = this.key === key ? NONE : key; - }, - close() { - this.key = NONE; - }, - }); -} - - -export function createToggleState() { - return makeAutoObservable({ - enabled: false, - toggle(flag) { - this.enabled = typeof flag === 'boolean' - ? flag - : !this.enabled; - }, - enable() { - this.enabled = true; - }, - disable() { - this.enabled = false; - }, - }); -} - -const SPEED_STORAGE_KEY = "__$player-speed$__"; -//const SKIP_STORAGE_KEY = "__$player-skip$__"; -//const initialSkip = !!localStorage.getItem(SKIP_STORAGE_KEY); - -export const - INITIALIZING = 0, - PLAYING = 1, - PAUSED = 2, - COMPLETED = 3, - SOCKET_ERROR = 5; - -export const - PORTRAIT = 1, - LANDSCAPE = 2; - -export function createPlayerState(state) { - const storedSpeed = +localStorage.getItem(SPEED_STORAGE_KEY); - const initialSpeed = [1,2,3].includes(storedSpeed) ? storedSpeed : 1; - - return makeAutoObservable({ - status: INITIALIZING, - _statusSaved: null, - setTime(t) { - this.time = t - }, - time: 0, - endTime: 0, - setStatus(status) { - this.status = status; - }, - get initializing() { - return this.status === INITIALIZING; - }, - get playing() { - return this.status === PLAYING; - }, - get completed() { - return this.status === COMPLETED; - }, - _buffering: false, - get buffering() { - return this._buffering; - }, - setBufferingState(flag = true) { - this._buffering = flag; - }, - speed: initialSpeed, - setSpeed(speed) { - localStorage.setItem(SPEED_STORAGE_KEY, speed); - this.speed = speed; - }, - width: 360, - height: 780, - orientation: PORTRAIT, - get orientationLandscape() { - return this.orientation === LANDSCAPE; - }, - setSize(width, height) { - if (height < 0 || width < 0) { - console.log("Player: wrong non-positive size") - return; - } - this.width = width; - this.height = height; - this.orientation = width > height ? LANDSCAPE : PORTRAIT; - }, - ...state, - }); -} diff --git a/frontend/app/player/lists/ListReader.js b/frontend/app/player/lists/ListReader.js deleted file mode 100644 index 641d3341a..000000000 --- a/frontend/app/player/lists/ListReader.js +++ /dev/null @@ -1,124 +0,0 @@ -export default class ListReader { - _callback; - _p = -1; - _list = []; - _offset = 0; - - constructor(callback = Function.prototype) { - if (typeof callback !== 'function') { - return console.error("List Reader: wrong constructor argument. `callback` must be a function."); - } - this._callback = callback; - } - - static checkItem(item) { - if(typeof item !== 'object' || item === null) { - console.error("List Reader: expected item to be not null object but got ", item); - return false; - } - if (typeof item.time !== 'number') { - console.error("List Reader: expected item to have number property 'time', ", item); - return false; - } - // if (typeof item.index !== 'number') { - // console.error("List Reader: expected item to have number property 'index', ", item); - // return false; - // } // future: All will have index - return true; - } - /* EXTENDABLE METHODS */ - _onIncrement() {} - _onDecrement() {} - _onStartTimeChange() {} - - inc() { - const item = this._list[ ++this._p ]; - this._onIncrement(item); - return item; - } - - dec() { - const item = this._list[ this._p-- ]; - this._onDecrement(item); - return item - } - - get _goToReturn() { - return { listNow: this.listNow }; - } - - goTo(time) { - const prevPointer = this._p; - while (!!this._list[ this._p + 1 ] && this._list[ this._p + 1 ].time <= time) { - this.inc(); - } - while (this._p >= 0 && this._list[ this._p ].time > time) { - this.dec(); - } - if (prevPointer !== this._p) { - //this._notify([ "listNow" ]); - return this._goToReturn; - } - } - - goToIndex(index) { // thinkaboutit - const prevPointer = this._p; - while (!!this._list[ this._p + 1 ] && - this._list[ this._p + 1 ].index <= index - ) { - this.inc(); - } - while (this._p >= 0 && this._list[ this._p ].index > index) { - this.dec(); - } - if (prevPointer !== this._p) { - //this._notify([ "listNow" ]); - return this._goToReturn; - } - } - - // happens rare MBTODO only in class ResourceListReader extends ListReaderWithRed - set startTime(time) { - const prevOffset = this._offset; - const prevPointer = this._p; - this._offset = this._list.findIndex(({ time, duration = 0 }) => time + duration >= time); // TODO: strict for duration rrrrr - this._p = Math.max(this._p, this._offset - 1); - if (prevOffset !== this._offset || prevPointer !== this._p) { - this._notify([ "listNow" ]); - } - this._onStartTimeChange(); - } - - get list() { - return this._list; - } - get count() { - return this._list.length; - } - get listNow() { - return this._list.slice(this._offset, this._p + 1); - } - - set list(_list) { - if (!Array.isArray(_list)) { - console.error("List Reader: wrong list value.", _list) - } - const valid = _list.every(this.constructor.checkItem); - if (!valid) return; - this._list = _list; // future: time + index sort - this._notify([ "list", "count" ]); - } - - append(item) { - if (!this.constructor.checkItem(item)) return; - this._list.push(item); // future: time + index sort - this._notify([ "count" ]); // list is the same by ref, CAREFULL - } - - _notify(propertyList) { - const changedState = {}; - propertyList.forEach(p => changedState[ p ] = this[ p ]); - this._callback(changedState); - } - -} \ No newline at end of file diff --git a/frontend/app/player/lists/ListReaderWithRed.js b/frontend/app/player/lists/ListReaderWithRed.js deleted file mode 100644 index 84da42138..000000000 --- a/frontend/app/player/lists/ListReaderWithRed.js +++ /dev/null @@ -1,48 +0,0 @@ -import ListReader from './ListReader'; - -export default class ListReaderWithRed extends ListReader { - _redCountNow = 0; - - static checkItem(item) { - const superCheckResult = super.checkItem(item); - if (typeof item.isRed !== 'function') { - console.error("List Reader With Red: expected item to have method 'isRed', ", item); - return false; - } - return superCheckResult; - } - - get _goToReturn() { - return { - listNow: this.listNow, - redCountNow: this.redCountNow, - } - } - - _onIncrement(item) { - if (item.isRed()) { - this._redCountNow++; - //this._notify([ "redCountNow" ]); - } - } - - _onDecrement(item) { - if (item.isRed()) { - this._redCountNow--; - //this._notify([ "redCountNow" ]); - } - } - - _onStartTimeChange() { - this._redCountNow = this._list - .slice(this._offset, this._p + 1) - .filter(item => item.isRed()) - .length; - this._notify([ "redCountNow" ]); - } - - get redCountNow() { - return this._redCountNow; - } - -} \ No newline at end of file diff --git a/frontend/app/player/lists/index.js b/frontend/app/player/lists/index.js deleted file mode 100644 index edae90b0c..000000000 --- a/frontend/app/player/lists/index.js +++ /dev/null @@ -1,68 +0,0 @@ -import ListReader from './ListReader'; -import ListReaderWithRed from './ListReaderWithRed'; -import { update as updateStore } from '../store'; - -const l = n => `${ n }List`; -const c = n => `${ n }Count`; -const ln = n => `${ n }ListNow`; -const rcn = n => `${ n }RedCountNow`; - -const entityNamesWithRed = [ "log", "resource", "fetch", "stack" ]; -const entityNamesSimple = [ "event", "profile" ]; -const entityNames = /*[ "redux" ].*/entityNamesWithRed.concat(entityNamesSimple); - -const is = {}; -entityNames.forEach(n => { - is[ l(n) ] = []; - is[ c(n) ] = 0; - is[ ln(n) ] = []; - if (entityNamesWithRed.includes(n)) { - is[ rcn(n) ] = 0; - } -}); -//is["reduxState"] = {}; -//is["reduxFinalStates"] = []; - - -const createCallback = n => { - const entityfy = s => `${ n }${ s[ 0 ].toUpperCase() }${ s.slice(1) }`; - return state => { - if (!state) return; - const namedState = {}; - Object.keys(state).forEach(key => { - namedState[ entityfy(key) ] = state[ key ]; - }); - return updateStore(namedState); - } -} - -let readers = null; - -export function init(lists) { - readers = {}; - entityNamesSimple.forEach(n => readers[ n ] = new ListReader(createCallback(n))); - entityNamesWithRed.forEach(n => readers[ n ] = new ListReaderWithRed(createCallback(n))); - - entityNames.forEach(n => readers[ n ].list = lists[ n ] || []); -} -export function append(name, item) { - readers[ name ].append(item); -} -export function setStartTime(time) { - readers.resource.startTime = time; -} -const byTimeNames = [ "event", "stack" ]; // TEMP -const byIndexNames = entityNames.filter(n => !byTimeNames.includes(n)); -export function goTo(time, index) { - if (readers === null) return; - if (typeof index === 'number') { - byTimeNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goTo(time))); - byIndexNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goToIndex(index))); - } else { - entityNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goTo(time))); - } -} -export function clean() { - entityNames.forEach(n => delete readers[ n ]); -} -export const INITIAL_STATE = is; diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts new file mode 100644 index 000000000..e8b8b7279 --- /dev/null +++ b/frontend/app/player/player/Animator.ts @@ -0,0 +1,172 @@ +import type { Store, Mover, Interval } from './types'; +import * as localStorage from './localStorage'; + +const fps = 60 +const performance: { now: () => number } = window.performance || { now: Date.now.bind(Date) } +const requestAnimationFrame: typeof window.requestAnimationFrame = + window.requestAnimationFrame || + // @ts-ignore + window.webkitRequestAnimationFrame || + // @ts-ignore + window.mozRequestAnimationFrame || + // @ts-ignore + window.oRequestAnimationFrame || + // @ts-ignore + window.msRequestAnimationFrame || + (callback => window.setTimeout(() => { callback(performance.now()) }, 1000 / fps)) +const cancelAnimationFrame = + window.cancelAnimationFrame || + // @ts-ignore + window.mozCancelAnimationFrame || + window.clearTimeout + + +export interface SetState { + time: number + playing: boolean + completed: boolean + endTime: number + live: boolean + livePlay: boolean +} + +export interface GetState extends SetState { + skip: boolean + speed: number + skipIntervals: Interval[] + lastMessageTime: number + ready: boolean +} + +export const INITIAL_STATE: SetState = { + time: 0, + playing: false, + completed: false, + endTime: 0, + live: false, + livePlay: false, +} as const + + +export default class Animator { + private animationFrameRequestId: number = 0 + + constructor(private state: Store, private mm: Mover) {} + + private setTime(time: number) { + this.state.update({ + time, + completed: false, + }) + this.mm.move(time) + } + + private startAnimation() { + let prevTime = this.state.get().time + let animationPrevTime = performance.now() + + const frameHandler = (animationCurrentTime: number) => { + const { + speed, + skip, + skipIntervals, + endTime, + live, + livePlay, + ready, // = messagesLoading || cssLoading || disconnected + lastMessageTime, // should be updated + } = this.state.get() + + const diffTime = !ready + ? 0 + : Math.max(animationCurrentTime - animationPrevTime, 0) * (live ? 1 : speed) + + let time = prevTime + diffTime + + const skipInterval = skip && skipIntervals.find(si => si.contains(time)) // TODO: good skip by messages + if (skipInterval) time = skipInterval.end + + if (time < 0) { time = 0 } // ? + //const fmt = getFirstMessageTime(); + //if (time < fmt) time = fmt; // ? + + + if (livePlay && time < lastMessageTime) { time = lastMessageTime } + if (endTime < lastMessageTime) { + this.state.update({ + endTime: lastMessageTime, + }) + } + + prevTime = time + animationPrevTime = animationCurrentTime + + const completed = !live && time >= endTime + if (completed) { + this.setTime(endTime) + return this.state.update({ + playing: false, + completed: true, + }) + } + + if (live && time > endTime) { + this.state.update({ + endTime: time, + }) + } + this.setTime(time) + this.animationFrameRequestId = requestAnimationFrame(frameHandler) + } + this.animationFrameRequestId = requestAnimationFrame(frameHandler) + } + + play() { + cancelAnimationFrame(this.animationFrameRequestId) + this.state.update({ playing: true }) + this.startAnimation() + } + + pause() { + cancelAnimationFrame(this.animationFrameRequestId) + this.state.update({ playing: false }) + } + + togglePlay() { + const { playing, completed } = this.state.get() + if (playing) { + this.pause() + } else if (completed) { + this.setTime(0) + this.play() + } else { + this.play() + } + } + + // jump by index? + jump(time: number) { + const { live } = this.state.get() + if (live) return + + if (this.state.get().playing) { + cancelAnimationFrame(this.animationFrameRequestId) + this.setTime(time) + this.startAnimation() + this.state.update({ livePlay: time === this.state.get().endTime }) + } else { + this.setTime(time) + this.state.update({ livePlay: time === this.state.get().endTime }) + } + } + + // TODO: clearify logic of live time-travel + jumpToLive() { + cancelAnimationFrame(this.animationFrameRequestId) + this.setTime(this.state.get().endTime) + this.startAnimation() + this.state.update({ livePlay: true }) + } + + +} \ No newline at end of file diff --git a/frontend/app/player/player/Player.ts b/frontend/app/player/player/Player.ts new file mode 100644 index 000000000..057d3752f --- /dev/null +++ b/frontend/app/player/player/Player.ts @@ -0,0 +1,125 @@ +import * as typedLocalStorage from './localStorage'; + +import type { Mover, Cleaner, 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'; + + +/* == separate this == */ +const HIGHEST_SPEED = 16 +const SPEED_STORAGE_KEY = "__$player-speed$__" +const SKIP_STORAGE_KEY = "__$player-skip$__" +const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__" +const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__" +const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__" +const storedSpeed: number = typedLocalStorage.number(SPEED_STORAGE_KEY) +const initialSpeed = [1, 2, 4, 8, 16].includes(storedSpeed) ? storedSpeed : 1 +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 default class Player extends Animator { + constructor(private pState: Store, private manager: Mover & Cleaner) { + super(pState, manager) + + // Autoplay + if (pState.get().autoplay) { + let autoPlay = true; + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + const { playing } = pState.get(); + autoPlay = playing + if (playing) { + this.pause(); + } + } else if (autoPlay) { + this.play(); + } + }) + + if (!document.hidden) { + this.play(); + } + } + } + + + + /* === TODO: incapsulate in LSCache === */ + + toggleAutoplay() { + const autoplay = !this.pState.get().autoplay + localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`); + this.pState.update({ autoplay }) + } + + // Shouldn't it be in the react part as a fully (with localStorage-cache react hook)? + toggleEvents() { + const showEvents = !this.pState.get().showEvents + localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`); + this.pState.update({ showEvents }) + } + + // move to React part? + toggleSkipToIssue() { + const skipToIssue = !this.pState.get().skipToIssue + localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`); + this.pState.update({ skipToIssue }) + } + + toggleSkip() { + const skip = !this.pState.get().skip + localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`); + this.pState.update({ skip }) + } + private updateSpeed(speed: number) { + localStorage.setItem(SPEED_STORAGE_KEY, `${speed}`); + this.pState.update({ speed }) + } + + toggleSpeed() { + const { speed } = this.pState.get() + this.updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 1) + } + + speedUp() { + const { speed } = this.pState.get() + this.updateSpeed(Math.min(HIGHEST_SPEED, speed * 2)) + } + + speedDown() { + const { speed } = this.pState.get() + this.updateSpeed(Math.max(1, speed / 2)) + } + /* === === */ + + // TODO: move theese to React hooks + // injectNotes(notes: Note[]) { + // update({ notes }) + // } + // filterOutNote(noteId: number) { + // const { notes } = getState() + // update({ notes: notes.filter((note: Note) => note.noteId !== noteId) }) + // } + + + clean() { + this.pause() + this.manager.clean() + } + +} \ No newline at end of file diff --git a/frontend/app/player/player/_LSCache.ts b/frontend/app/player/player/_LSCache.ts new file mode 100644 index 000000000..beb48af46 --- /dev/null +++ b/frontend/app/player/player/_LSCache.ts @@ -0,0 +1,63 @@ +import * as lstore from './localStorage' + +import type { SimpleState } from './PlayerState' + +const SPEED_STORAGE_KEY = "__$player-speed$__"; +const SKIP_STORAGE_KEY = "__$player-skip$__"; +const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__"; +const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__"; +const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__"; + + +const storedSpeed = lstore.number(SPEED_STORAGE_KEY, 1) +const initialSpeed = [1,2,4,8,16].includes(storedSpeed) ? storedSpeed : 1; +const initialSkip = lstore.boolean(SKIP_STORAGE_KEY) +const initialSkipToIssue = lstore.boolean(SKIP_TO_ISSUE_STORAGE_KEY) +const initialAutoplay = lstore.boolean(AUTOPLAY_STORAGE_KEY) +const initialShowEvents = lstore.boolean(SHOW_EVENTS_STORAGE_KEY) + +export const INITIAL_STATE = { + skipToIssue: initialSkipToIssue, + autoplay: initialAutoplay, + showEvents: initialShowEvents, + skip: initialSkip, + speed: initialSpeed, +} + +const KEY_MAP = { + speed: SPEED_STORAGE_KEY, + skip: SKIP_STORAGE_KEY, + skipToIssue: SKIP_TO_ISSUE_STORAGE_KEY, + autoplay: AUTOPLAY_STORAGE_KEY, + showEvents: SHOW_EVENTS_STORAGE_KEY, +} + +type KeysOfBoolean = keyof T & keyof { [ K in keyof T as T[K] extends boolean ? K : never ] : K }; + +type Entries = { + [K in keyof T]: [K, T[K]]; +}[keyof T][]; + +export default class LSCache { + constructor(private state: SimpleState, private keyMap: Record, string>) { + } + update(newState: Partial) { + for (let [k, v] of Object.entries(newState) as Entries>) { + if (k in this.keyMap) { + // @ts-ignore TODO: nice typing + //lstore[typeof v](this.keyMap[k], v) + localStorage.setItem(this.keyMap[k], String(v)) + } + } + this.state.update(newState) + } + toggle(key: KeysOfBoolean) { + // @ts-ignore TODO: nice typing + this.update({ + [key]: !this.get()[key] + }) + } + get() { + return this.state.get() + } +} \ No newline at end of file diff --git a/frontend/app/player/player/localStorage.ts b/frontend/app/player/player/localStorage.ts new file mode 100644 index 000000000..03bba1d77 --- /dev/null +++ b/frontend/app/player/player/localStorage.ts @@ -0,0 +1,19 @@ + +export function number(key: string, dflt = 0): number { + const stVal = localStorage.getItem(key) + if (stVal === null) { + return dflt + } + const val = parseInt(stVal) + if (isNaN(val)) { + return dflt + } + return val +} + +export function boolean(key: string, dflt = false): boolean { + return localStorage.getItem(key) === "true" +} +export function string(key: string, dflt = ''): string { + return localStorage.getItem(key) || '' +} \ No newline at end of file diff --git a/frontend/app/player/player/types.ts b/frontend/app/player/player/types.ts new file mode 100644 index 000000000..2ad5032ed --- /dev/null +++ b/frontend/app/player/player/types.ts @@ -0,0 +1,21 @@ + +export interface Mover { + move(time: number): void +} + +export interface Cleaner { + clean(): void +} + +export interface Interval { + contains(t: number): boolean + start: number + end: number +} + +export interface Store { + get(): G + update(state: Partial): void +} + +