diff --git a/package-lock.json b/package-lock.json index 07dba84b..a96a7061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "frankerfacez", - "version": "4.20.14", + "version": "4.20.15", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1847,6 +1847,28 @@ "supports-color": "^5.3.0" } }, + "chartjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/chartjs/-/chartjs-0.3.24.tgz", + "integrity": "sha1-Ot3rWuNgaz6J40bCfVLKFYQW6T0=" + }, + "chartjs-plugin-waterfall": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chartjs-plugin-waterfall/-/chartjs-plugin-waterfall-1.0.3.tgz", + "integrity": "sha1-wcntwyyX913U4qYwdQoKlZM3B0o=", + "requires": { + "lodash.groupby": "^4.6.0", + "lodash.merge": "^4.6.0" + } + }, + "chartjs-plugin-zoom": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-0.7.7.tgz", + "integrity": "sha512-8fOHPPiZTT2+K0w278TQWYs/DtPg06s1OpTqdXxPpdfH7QQbl6Io/WuE1FjPehDWVCxpe3tSTts+dPbxgq2Z5g==", + "requires": { + "hammerjs": "^2.0.8" + } + }, "chokidar": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", @@ -4199,6 +4221,11 @@ "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.3.tgz", "integrity": "sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA==" }, + "hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -5173,6 +5200,16 @@ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, + "lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "lodash.tail": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", diff --git a/package.json b/package.json index ef45b622..d77a768d 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.15", + "version": "4.20.16", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { @@ -65,6 +65,9 @@ }, "dependencies": { "@ffz/icu-msgparser": "^1.0.2", + "chartjs": "^0.3.24", + "chartjs-plugin-waterfall": "^1.0.3", + "chartjs-plugin-zoom": "^0.7.7", "crypto-js": "^3.1.9-1", "dayjs": "^1.8.29", "displacejs": "^1.4.1", diff --git a/src/main.js b/src/main.js index 9f3c6f91..76d8faed 100644 --- a/src/main.js +++ b/src/main.js @@ -16,6 +16,7 @@ import {TranslationManager} from './i18n'; import SocketClient from './socket'; import Site from 'site'; import Vue from 'utilities/vue'; +//import Timing from 'utilities/timing'; class FrankerFaceZ extends Module { constructor() { @@ -29,6 +30,10 @@ class FrankerFaceZ extends Module { this.__state = 0; this.__modules.core = this; + // Timing + //this.inject('timing', Timing); + this.__time('instance'); + // ======================================================================== // Error Reporting and Logging // ======================================================================== diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx index 217c1976..9c9f1ebc 100644 --- a/src/modules/metadata.jsx +++ b/src/modules/metadata.jsx @@ -18,6 +18,7 @@ export default class Metadata extends Module { this.inject('settings'); this.inject('i18n'); + this.inject('tooltips'); this.should_enable = true; this.definitions = {}; @@ -413,6 +414,43 @@ export default class Metadata extends Module { } } + onEnable() { + const md = this.tooltips.types.metadata = target => { + let el = target; + if ( el._ffz_stat ) + el = el._ffz_stat; + else if ( ! el.classList.contains('ffz-stat') ) { + el = target.closest('.ffz-stat'); + target._ffz_stat = el; + } + + if ( ! el ) + return; + + const key = el.dataset.key, + def = this.definitions[key]; + + return maybe_call(def.tooltip, this, el._ffz_data) + }; + + md.onShow = (target, tip) => { + const el = target._ffz_stat || target; + el.tip = tip; + }; + + md.onHide = target => { + const el = target._ffz_stat || target; + el.tip = null; + el.tip_content = null; + } + + md.popperConfig = (target, tip, opts) => { + opts.placement = 'bottom'; + opts.modifiers.flip = {behavior: ['bottom','top']}; + return opts; + } + } + get keys() { return Object.keys(this.definitions); @@ -518,7 +556,8 @@ export default class Metadata extends Module { tip_content={null} > {btn = ( {icon} @@ -526,7 +565,8 @@ export default class Metadata extends Module { )} {popup = ( @@ -538,7 +578,8 @@ export default class Metadata extends Module { } else btn = popup = el = ( @@ -596,7 +637,7 @@ export default class Metadata extends Module { el._ffz_destroy = el._ffz_outside = null; }; - const parent = document.body.querySelector('#root>div') || document.body, + const parent = document.fullscreenElement || document.body.querySelector('#root>div') || document.body, tt = el._ffz_popup = new Tooltip(parent, el, { logger: this.log, i18n: this.i18n, @@ -637,7 +678,8 @@ export default class Metadata extends Module { icon = (); el = ( @@ -668,7 +710,7 @@ export default class Metadata extends Module { subcontainer.appendChild(el); - if ( def.tooltip ) { + /*if ( def.tooltip ) { const parent = document.body.querySelector('#root>div') || document.body; el.tooltip = new Tooltip(parent, el, { logger: this.log, @@ -692,7 +734,7 @@ export default class Metadata extends Module { } } }); - } + }*/ } else { stat = el.querySelector('.ffz-stat-text'); diff --git a/src/modules/tooltips.js b/src/modules/tooltips.js index 4b681d94..a16a3a34 100644 --- a/src/modules/tooltips.js +++ b/src/modules/tooltips.js @@ -93,6 +93,7 @@ export default class TooltipProvider extends Module { onShow: this.delegateOnShow.bind(this), onHide: this.delegateOnHide.bind(this), + popperConfig: this.delegatePopperConfig.bind(this), popper: { placement: 'top', modifiers: { @@ -131,6 +132,16 @@ export default class TooltipProvider extends Module { this.tips.cleanup(); } + delegatePopperConfig(target, tip, pop_opts) { + const type = target.dataset.tooltipType, + handler = this.types[type]; + + if ( handler && handler.popperConfig ) + return handler.popperConfig(target, tip, pop_opts); + + return pop_opts; + } + delegateOnShow(target, tip) { const type = target.dataset.tooltipType, handler = this.types[type]; diff --git a/src/sites/twitch-twilight/modules/channel.js b/src/sites/twitch-twilight/modules/channel.js index 43c55fed..00e87283 100644 --- a/src/sites/twitch-twilight/modules/channel.js +++ b/src/sites/twitch-twilight/modules/channel.js @@ -163,7 +163,7 @@ export default class Channel extends Module { } _updateBar(el) { - if ( el._ffz_cont && ! el.contains(el._ffz_cont) ) { + if ( el._ffz_cont && ! document.contains(el._ffz_cont) ) { el._ffz_cont.classList.remove('ffz--meta-tray'); el._ffz_cont = null; } @@ -227,7 +227,7 @@ export default class Channel extends Module { react = this.fine.getReactInstance(el), props = react?.memoizedProps?.children?.props; - if ( ! cont || ! el.contains(cont) || ! props || ! props.channelID ) + if ( ! cont || ! document.contains(cont) || ! props || ! props.channelID ) return; if ( ! keys ) diff --git a/src/utilities/module.js b/src/utilities/module.js index db21e62b..38ecdf0d 100644 --- a/src/utilities/module.js +++ b/src/utilities/module.js @@ -47,6 +47,7 @@ export class Module extends EventEmitter { this.__state = this.onLoad || this.onEnable ? State.DISABLED : State.ENABLED; + this.__time('instance'); this.emit(':registered'); } @@ -76,6 +77,20 @@ export class Module extends EventEmitter { } + // ======================================================================== + // Timing + // ======================================================================== + + __time(event) { + if ( this.root.timing ) { + if ( typeof event !== 'object' ) + event = {event}; + event.module = this.__path || 'core'; + this.root.timing.addEvent(event); + } + } + + // ======================================================================== // State! Glorious State // ======================================================================== @@ -115,6 +130,7 @@ export class Module extends EventEmitter { chain.push(this); + this.__time('load-start'); this.__load_state = State.LOADING; return this.__load_promise = (async () => { if ( this.load_requires ) { @@ -130,17 +146,21 @@ export class Module extends EventEmitter { await Promise.all(promises); } - if ( this.onLoad ) + if ( this.onLoad ) { + this.__time('load-self'); return this.onLoad(...args); + } })().then(ret => { this.__load_state = State.LOADED; this.__load_promise = null; + this.__time('load-end'); this.emit(':loaded', this); return ret; }).catch(err => { this.__load_state = State.UNLOADED; this.__load_promise = null; + this.__time('load-end'); throw err; }); } @@ -166,7 +186,7 @@ export class Module extends EventEmitter { return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, chain)); chain.push(this); - + this.__time('unload-start'); this.__load_state = State.UNLOADING; return this.__load_promise = (async () => { if ( this.__state !== State.DISABLED ) @@ -185,16 +205,19 @@ export class Module extends EventEmitter { await Promise.all(promises); } + this.__time('unload-self'); return this.onUnload(...args); })().then(ret => { this.__load_state = State.UNLOADED; this.__load_promise = null; + this.__time('unload-end'); this.emit(':unloaded', this); return ret; }).catch(err => { this.__load_state = State.LOADED; this.__load_promise = null; + this.__time('unload-end'); throw err; }); } @@ -217,7 +240,7 @@ export class Module extends EventEmitter { return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, chain)); chain.push(this); - + this.__time('enable-start'); this.__state = State.ENABLING; return this.__state_promise = (async () => { const promises = [], @@ -242,18 +265,22 @@ export class Module extends EventEmitter { } await Promise.all(promises); - if ( this.onEnable ) + if ( this.onEnable ) { + this.__time('enable-self'); return this.onEnable(...args); + } })().then(ret => { this.__state = State.ENABLED; this.__state_promise = null; + this.__time('enable-end'); this.emit(':enabled', this); return ret; }).catch(err => { this.__state = State.DISABLED; this.__state_promise = null; + this.__time('enable-end'); throw err; }); } @@ -279,7 +306,7 @@ export class Module extends EventEmitter { return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, chain)); chain.push(this); - + this.__time('disable-start'); this.__state = State.DISABLING; return this.__state_promise = (async () => { if ( this.__load_state !== State.LOADED ) @@ -300,17 +327,20 @@ export class Module extends EventEmitter { await Promise.all(promises); } + this.__time('disable-self'); return this.onDisable(...args); })().then(ret => { this.__state = State.DISABLED; this.__state_promise = null; + this.__time('disable-end'); this.emit(':disabled', this); return ret; }).catch(err => { this.__state = State.ENABLED; this.__state_promise = null; + this.__time('disable-end'); throw err; }); } diff --git a/src/utilities/timing.js b/src/utilities/timing.js new file mode 100644 index 00000000..a5abdd84 --- /dev/null +++ b/src/utilities/timing.js @@ -0,0 +1,24 @@ +'use strict'; + +// ============================================================================ +// Timing Tracker +// For figuring out FFZ loading +// ============================================================================ + +import Module from 'utilities/module'; + + +export default class Timing extends Module { + constructor(...args) { + super(...args); + + this.events = []; + } + + __time() { /* no-op */ } // eslint-disable-line class-methods-use-this + + addEvent(event) { + event.ts = performance.now(); + this.events.push(event); + } +} \ No newline at end of file diff --git a/src/utilities/tooltip.js b/src/utilities/tooltip.js index 002471ec..100be35a 100644 --- a/src/utilities/tooltip.js +++ b/src/utilities/tooltip.js @@ -291,7 +291,7 @@ export class Tooltip { const use_html = maybe_call(opts.html, null, target, tip), setter = use_html ? 'innerHTML' : 'textContent'; - const pop_opts = Object.assign({ + let pop_opts = Object.assign({ modifiers: { flip: { behavior: ['top', 'bottom', 'left', 'right'] @@ -300,6 +300,9 @@ export class Tooltip { arrowElement: arrow }, opts.popper); + if ( opts.popperConfig ) + pop_opts = opts.popperConfig(target, tip, pop_opts) ?? pop_opts; + pop_opts.onUpdate = tip._on_update = debounce(() => { if ( ! opts.no_auto_remove && ! document.contains(tip.target) ) this.hide(tip);