mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-10-11 13:41:57 +00:00
4.20.1
* Added: Option to hide Twitch's native stream uptime. Enabled by default when using FFZ's own stream uptime. * Fixed: Implement stream metadata for the new Twitch layout. * Fixed: Display the subscription tier in badge tool-tips for Tier 2 and Tier 3 subscriber badges. This release involves significant changes under the hood. Due to Twitch's heavy use of functional React components and effects in recent updates to their site, we need to start listening for DOM nodes rather than components in many cases. To that end, I've implemented the Elemental module to grab elements using MutationObservers. I didn't want to, but this is where we're at. In a future release I'll be using Elemental to add support back to the directory for certain features.
This commit is contained in:
parent
8c9a3aa8a4
commit
ed0577f09e
16 changed files with 719 additions and 824 deletions
302
src/utilities/compat/elemental.js
Normal file
302
src/utilities/compat/elemental.js
Normal file
|
@ -0,0 +1,302 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Elemental
|
||||
// It finds elements.
|
||||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
export default class Elemental extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this._pruneLive = this._pruneLive.bind(this);
|
||||
|
||||
this._wrappers = new Map;
|
||||
|
||||
this._observer = null;
|
||||
this._watching = new Set;
|
||||
this._live_watching = null;
|
||||
}
|
||||
|
||||
|
||||
onDisable() {
|
||||
this._stopWatching();
|
||||
}
|
||||
|
||||
|
||||
define(key, selector, routes, opts = null, limit = 0, timeout = 5000) {
|
||||
if ( this._wrappers.has(key) )
|
||||
return this._wrappers.get(key);
|
||||
|
||||
if ( ! selector || typeof selector !== 'string' || ! selector.length )
|
||||
throw new Error('cannot find definition and no selector provided');
|
||||
|
||||
const wrapper = new ElementalWrapper(key, selector, routes, opts, limit, timeout, this);
|
||||
this._wrappers.set(key, wrapper);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
route(route) {
|
||||
this._route = route;
|
||||
this._timer = Date.now();
|
||||
this._updateLiveWatching();
|
||||
this.checkAll();
|
||||
}
|
||||
|
||||
|
||||
checkAll() {
|
||||
if ( this._watching )
|
||||
for(const watcher of this._watching)
|
||||
watcher.check();
|
||||
}
|
||||
|
||||
|
||||
updateTimeout() {
|
||||
this._timer = Date.now();
|
||||
this._updateLiveWatching();
|
||||
this.checkAll();
|
||||
}
|
||||
|
||||
|
||||
_isActive(watcher, now) {
|
||||
if ( this._route && watcher.routes.length && ! watcher.routes.includes(this._route) )
|
||||
return false;
|
||||
|
||||
if ( watcher.timeout > 0 && (now - this._timer) > watcher.timeout )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
_updateLiveWatching() {
|
||||
if ( this._timeout ) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
|
||||
const lw = this._live_watching = [],
|
||||
now = Date.now();
|
||||
let min_timeout = Number.POSITIVE_INFINITY;
|
||||
|
||||
if ( this._watching )
|
||||
for(const watcher of this._watching)
|
||||
if ( this._isActive(watcher, now) ) {
|
||||
if ( watcher.timeout > 0 && watcher.timeout < min_timeout )
|
||||
min_timeout = watcher.timeout;
|
||||
|
||||
lw.push(watcher);
|
||||
}
|
||||
|
||||
if ( isFinite(min_timeout) )
|
||||
this._timeout = setTimeout(this._pruneLive, min_timeout);
|
||||
|
||||
if ( ! lw.length )
|
||||
this._stopWatching();
|
||||
else if ( ! this._observer )
|
||||
this._startWatching();
|
||||
}
|
||||
|
||||
_pruneLive() {
|
||||
this._updateLiveWatching();
|
||||
}
|
||||
|
||||
_checkWatchers(muts) {
|
||||
for(const watcher of this._live_watching)
|
||||
watcher.checkElements(muts);
|
||||
}
|
||||
|
||||
_startWatching() {
|
||||
if ( ! this._observer && this._live_watching && this._live_watching.length ) {
|
||||
this.log.info('Installing MutationObserver.');
|
||||
|
||||
this._observer = new MutationObserver(mutations => this._checkWatchers(mutations.map(x => x.target)));
|
||||
this._observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_stopWatching() {
|
||||
if ( this._observer ) {
|
||||
this.log.info('Stopping MutationObserver.');
|
||||
this._observer.disconnect();
|
||||
}
|
||||
|
||||
if ( this._timeout ) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
|
||||
this._live_watching = null;
|
||||
this._observer = null;
|
||||
}
|
||||
|
||||
|
||||
listen(inst) {
|
||||
if ( this._watching.has(inst) )
|
||||
return;
|
||||
|
||||
this._watching.add(inst);
|
||||
this._updateLiveWatching();
|
||||
}
|
||||
|
||||
unlisten(inst) {
|
||||
if ( ! this._watching.has(inst) )
|
||||
return;
|
||||
|
||||
this._watching.delete(inst);
|
||||
this._updateLiveWatching();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let elemental_id = 0;
|
||||
|
||||
export class ElementalWrapper extends EventEmitter {
|
||||
constructor(name, selector, routes, opts, limit, timeout, elemental) {
|
||||
super();
|
||||
|
||||
this.id = elemental_id++;
|
||||
this.param = `_ffz$elemental$${this.id}`;
|
||||
this.remove_param = `_ffz$elemental_remove$${this.id}`;
|
||||
this.mut_param = `_ffz$elemental_mutating${this.id}`;
|
||||
|
||||
this._schedule = this._schedule.bind(this);
|
||||
|
||||
this.name = name;
|
||||
this.selector = selector;
|
||||
this.routes = routes || [];
|
||||
this.opts = opts;
|
||||
this.limit = limit;
|
||||
this.timeout = timeout;
|
||||
|
||||
if ( this.opts && ! this.opts.childList && ! this.opts.attributes && ! this.opts.characterData )
|
||||
this.opts.attributes = true;
|
||||
|
||||
this.count = 0;
|
||||
this.instances = new Set;
|
||||
this.observers = new Map;
|
||||
this.elemental = elemental;
|
||||
|
||||
this.check();
|
||||
this.schedule();
|
||||
}
|
||||
|
||||
get atLimit() {
|
||||
return this.limit > 0 && this.count >= this.limit;
|
||||
}
|
||||
|
||||
schedule() {
|
||||
if ( ! this._stimer )
|
||||
this._stimer = setTimeout(this._schedule, 0);
|
||||
}
|
||||
|
||||
_schedule() {
|
||||
clearTimeout(this._stimer);
|
||||
this._stimer = null;
|
||||
|
||||
if ( this.limit === 0 || this.count < this.limit )
|
||||
this.elemental.listen(this);
|
||||
else
|
||||
this.elemental.unlisten(this);
|
||||
}
|
||||
|
||||
check() {
|
||||
const matches = document.querySelectorAll(this.selector);
|
||||
for(const el of matches)
|
||||
this.add(el);
|
||||
}
|
||||
|
||||
checkElements(els) {
|
||||
if ( this.atLimit )
|
||||
return this.schedule();
|
||||
|
||||
for(const el of els) {
|
||||
const matches = el.querySelectorAll(this.selector);
|
||||
for(const match of matches)
|
||||
this.add(match);
|
||||
|
||||
if ( this.atLimit )
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
get first() {
|
||||
for(const el of this.instances)
|
||||
return el;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return Array.from(this.instances);
|
||||
}
|
||||
|
||||
each(fn) {
|
||||
for(const el of this.instances)
|
||||
fn(el);
|
||||
}
|
||||
|
||||
add(el) {
|
||||
if ( this.instances.has(el) )
|
||||
return;
|
||||
|
||||
this.instances.add(el);
|
||||
this.count++;
|
||||
|
||||
const remove_check = new MutationObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if ( ! document.contains(el) )
|
||||
this.remove(el);
|
||||
});
|
||||
});
|
||||
|
||||
remove_check.observe(el.parentNode, {childList: true});
|
||||
el[this.remove_param] = remove_check;
|
||||
|
||||
if ( this.opts ) {
|
||||
const observer = new MutationObserver(muts => {
|
||||
if ( ! document.contains(el) ) {
|
||||
this.remove(el);
|
||||
} else if ( ! this.__running.size )
|
||||
this.emit('mutate', el, muts);
|
||||
});
|
||||
|
||||
observer.observe(el, this.opts);
|
||||
el[this.param] = observer;
|
||||
}
|
||||
|
||||
this.schedule();
|
||||
this.emit('mount', el);
|
||||
}
|
||||
|
||||
remove(el) {
|
||||
const observer = el[this.param];
|
||||
if ( observer ) {
|
||||
observer.disconnect();
|
||||
el[this.param] = null;
|
||||
}
|
||||
|
||||
const remove_check = el[this.remove_param];
|
||||
if ( remove_check ) {
|
||||
remove_check.disconnect();
|
||||
el[this.remove_param] = null;
|
||||
}
|
||||
|
||||
if ( ! this.instances.has(el) )
|
||||
return;
|
||||
|
||||
this.instances.delete(el);
|
||||
this.count--;
|
||||
|
||||
this.schedule();
|
||||
this.emit('unmount', el);
|
||||
}
|
||||
}
|
|
@ -591,6 +591,11 @@ export class FineWrapper extends EventEmitter {
|
|||
this.once('set', fn);
|
||||
}
|
||||
|
||||
each(fn) {
|
||||
for(const inst of this.instances)
|
||||
fn(inst);
|
||||
}
|
||||
|
||||
_set(cls, instances) {
|
||||
if ( this._class )
|
||||
throw new Error('already have a class');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue