trfactoring(frontend/player):phase 1 of componnts decomposition; use store per instance
This commit is contained in:
parent
d3d28d705e
commit
7a3ef9bc21
72 changed files with 990 additions and 1690 deletions
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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<any>
|
||||
}
|
||||
|
||||
export function initLists(): ListsObject {
|
||||
const lists: Partial<ListsObject> = {};
|
||||
for (var i = 0; i < LIST_NAMES.length; i++) {
|
||||
lists[LIST_NAMES[i]] = new ListWalker();
|
||||
}
|
||||
return lists as ListsObject;
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from './StatedScreen';
|
||||
export * from './StatedScreen';
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from './MessageDistributor';
|
||||
export * from './MessageDistributor';
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Timed } from '../messages/timed';
|
||||
import type { Timed } from './messages/timed';
|
||||
|
||||
export default class ListWalker<T extends Timed> {
|
||||
private p = 0
|
||||
|
|
@ -79,6 +79,23 @@ export default class ListWalker<T extends Timed> {
|
|||
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<T extends Timed> {
|
|||
|
||||
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<T extends Timed> {
|
|||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
31
frontend/app/player/_common/ListWalkerWithMarks.ts
Normal file
31
frontend/app/player/_common/ListWalkerWithMarks.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Timed } from './messages/timed';
|
||||
import ListWalker from './ListWalker'
|
||||
|
||||
|
||||
type CheckFn<T> = (t: T) => boolean
|
||||
|
||||
|
||||
export default class ListWalkerWithMarks<T extends Timed> extends ListWalker<T> {
|
||||
private _markCountNow: number = 0
|
||||
constructor(private isMarked: CheckFn<T>, 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
|
||||
}
|
||||
|
||||
}
|
||||
15
frontend/app/player/_common/SimpleStore.ts
Normal file
15
frontend/app/player/_common/SimpleStore.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
import { State } from './types'
|
||||
|
||||
// (not a type)
|
||||
export default class SimpleSore<G, S=G> implements State<G, S> {
|
||||
constructor(private state: G){}
|
||||
get(): G {
|
||||
return this.state
|
||||
}
|
||||
update(newState: Partial<S>) {
|
||||
Object.assign(this.state, newState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<PState & MMState> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {}) => {
|
||||
71
frontend/app/player/_web/Lists.ts
Normal file
71
frontend/app/player/_web/Lists.ts
Normal file
|
|
@ -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<any>
|
||||
}
|
||||
type MarkedListsObject = {
|
||||
[key in typeof MARKED_LIST_NAMES[number]]: ListWalkerWithMarks<any>
|
||||
}
|
||||
type ListsObject = SimpleListsObject & MarkedListsObject
|
||||
|
||||
type InitialLists = {
|
||||
[key in typeof LIST_NAMES[number]]: any[]
|
||||
}
|
||||
|
||||
export default class Lists {
|
||||
lists: ListsObject
|
||||
constructor(initialLists: Partial<InitialLists> = {}) {
|
||||
const lists: Partial<ListsObject> = {}
|
||||
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
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<any>/*<LocationEvent>*/ = new ListWalker();
|
||||
private locationManager: ListWalker<SetPageLocation> = new ListWalker();
|
||||
|
|
@ -95,12 +105,11 @@ export default class MessageDistributor extends StatedScreen {
|
|||
private resizeManager: ListWalker<SetViewportSize> = new ListWalker([]);
|
||||
private pagesManager: PagesManager;
|
||||
private mouseMoveManager: MouseMoveManager;
|
||||
private assistManager: AssistManager;
|
||||
|
||||
private scrollManager: ListWalker<SetViewportScroll> = 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<State>,
|
||||
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<string, string>) => {
|
||||
if (e.type === EVENT_TYPES.LOCATION) { //TODO type system
|
||||
this.locationEventManager.append(e);
|
||||
}
|
||||
});
|
||||
this.session.errors.forEach((e: Record<string, string>) => {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = '<b>Right-click > Inspect</b> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
178
frontend/app/player/_web/WebPlayer.ts
Normal file
178
frontend/app/player/_web/WebPlayer.ts
Normal file
|
|
@ -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<MMState & PlayerState>, 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<string, MediaStreamTrack> = {}
|
||||
|
||||
// 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<State>,
|
||||
) {}
|
||||
|
||||
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) {
|
||||
|
|
@ -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<typeof SkipIntervalCls>; // exporting only class' type
|
||||
export type SkipInterval = InstanceType<typeof SkipIntervalCls>;
|
||||
|
||||
|
||||
export default class ActivityManager extends ListWalker<SkipInterval> {
|
||||
|
|
@ -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<Message> {
|
|||
|
||||
|
||||
constructor(
|
||||
private readonly screen: StatedScreen,
|
||||
private readonly screen: MessageManager,
|
||||
private readonly isMobile: boolean,
|
||||
public readonly time: number
|
||||
) {
|
||||
|
|
@ -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"
|
||||
|
|
@ -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<CSSRuleMessage> {
|
|||
private linkLoadPromises: Array<Promise<void>> = [];
|
||||
private skipCSSLinks: Array<string> = []; // should be common for all pages
|
||||
|
||||
constructor(private readonly screen: StatedScreen) {
|
||||
constructor(private readonly screen: MessageManager) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
|
@ -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<MouseMove> {
|
||||
private hoverElements: Array<Element> = [];
|
||||
|
||||
constructor(private screen: StatedScreen) {super()}
|
||||
constructor(private screen: Screen) {super()}
|
||||
|
||||
private updateHover(): void {
|
||||
const curHoverElements = this.screen.getCursorTargets();
|
||||
|
|
@ -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<DOMManager> {
|
|||
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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { PerformanceTrack, SetPageVisibility } from '../messages';
|
||||
|
||||
import ListWalker from './ListWalker';
|
||||
import ListWalker from '../../_common/ListWalker';
|
||||
|
||||
export type PerformanceChartPoint = {
|
||||
time: number,
|
||||
|
|
@ -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<Redux> {
|
||||
25
frontend/app/player/create.ts
Normal file
25
frontend/app/player/create.ts
Normal file
|
|
@ -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<PState & MMState>] {
|
||||
const store = new SimpleStore<PState & MMState>({
|
||||
...PLAYER_INITIAL_STATE,
|
||||
...MM_INITIAL_STATE,
|
||||
})
|
||||
const player = new WebPlayer(store, session, config, false)
|
||||
return [player, store]
|
||||
}
|
||||
|
||||
|
||||
export function createLiveWebPlayer(session, config): [WebPlayer, Store<PState & MMState>] {
|
||||
const store = new SimpleStore<PState & MMState>({
|
||||
...PLAYER_INITIAL_STATE,
|
||||
...MM_INITIAL_STATE,
|
||||
})
|
||||
const player = new WebPlayer(store, session, config, true)
|
||||
return [player, store]
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
export * from './store';
|
||||
export * from './singletone';
|
||||
export * from './MessageDistributor/managers/AssistManager';
|
||||
export * from './MessageDistributor/managers/LocalStream';
|
||||
export * from './_store';
|
||||
export * from './_web/assist/AssistManager';
|
||||
export * from './_web/assist/LocalStream';
|
||||
export * from './_singletone';
|
||||
|
||||
export * from './create';
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
172
frontend/app/player/player/Animator.ts
Normal file
172
frontend/app/player/player/Animator.ts
Normal file
|
|
@ -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<GetState, SetState>, 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 })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
125
frontend/app/player/player/Player.ts
Normal file
125
frontend/app/player/player/Player.ts
Normal file
|
|
@ -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<State>, 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()
|
||||
}
|
||||
|
||||
}
|
||||
63
frontend/app/player/player/_LSCache.ts
Normal file
63
frontend/app/player/player/_LSCache.ts
Normal file
|
|
@ -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<T> = keyof T & keyof { [ K in keyof T as T[K] extends boolean ? K : never ] : K };
|
||||
|
||||
type Entries<T> = {
|
||||
[K in keyof T]: [K, T[K]];
|
||||
}[keyof T][];
|
||||
|
||||
export default class LSCache<G extends Record<string, boolean | number | string> {
|
||||
constructor(private state: SimpleState<G>, private keyMap: Record<keyof Partial<G>, string>) {
|
||||
}
|
||||
update(newState: Partial<G>) {
|
||||
for (let [k, v] of Object.entries(newState) as Entries<Partial<G>>) {
|
||||
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<G>) {
|
||||
// @ts-ignore TODO: nice typing
|
||||
this.update({
|
||||
[key]: !this.get()[key]
|
||||
})
|
||||
}
|
||||
get() {
|
||||
return this.state.get()
|
||||
}
|
||||
}
|
||||
19
frontend/app/player/player/localStorage.ts
Normal file
19
frontend/app/player/player/localStorage.ts
Normal file
|
|
@ -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) || ''
|
||||
}
|
||||
21
frontend/app/player/player/types.ts
Normal file
21
frontend/app/player/player/types.ts
Normal file
|
|
@ -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<G extends Object, S extends Object = G> {
|
||||
get(): G
|
||||
update(state: Partial<S>): void
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue