1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 05:15:54 +00:00
* Added: Setting to disable Channel Hosting.
* Added: Setting to hide streams tagged as Promoted from the directory.
* Fixed: Tool-tips not appearing when an element is open in fullscreen.
* Fixed: Bug when destroying a tool-tip instance.
* Fixed: Directory cards not updating with FFZ features on the front page.

* API Added: `Subpump` module for manipulating Twitch PubSub events.
* API Changed: Add-Ons can now define custom settings profile filters.
This commit is contained in:
SirStendec 2020-07-18 15:44:02 -04:00
parent ced61d2bfc
commit 22c60050e0
17 changed files with 356 additions and 96 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.20.10",
"version": "4.20.11",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0",
"scripts": {

View file

@ -334,7 +334,7 @@ export default class Actions extends Module {
target._ffz_destroy = target._ffz_outside = target._ffz_on_destroy = null;
}
const parent = document.body.querySelector('#root>div') || document.body,
const parent = document.fullscreenElement || document.body.querySelector('#root>div') || document.body,
tt = target._ffz_popup = new Tooltip(parent, target, {
logger: this.log,
manual: true,
@ -807,8 +807,8 @@ export default class Actions extends Module {
return this.log.warn(`No click handler for action provider "${data.action}"`);
}
if ( target._ffz_tooltip$0 )
target._ffz_tooltip$0.hide();
if ( target._ffz_tooltip )
target._ffz_tooltip.hide();
return data.definition.click.call(this, event, data);
}
@ -827,8 +827,8 @@ export default class Actions extends Module {
if ( target.classList.contains('disabled') )
return;
if ( target._ffz_tooltip$0 )
target._ffz_tooltip$0.hide();
if ( target._ffz_tooltip )
target._ffz_tooltip.hide();
if ( ! data.definition.context && ! data.definition.uses_reason )
return;
@ -847,8 +847,8 @@ export default class Actions extends Module {
event.preventDefault();
const target = event.target;
if ( target._ffz_tooltip$0 )
target._ffz_tooltip$0.hide();
if ( target._ffz_tooltip )
target._ffz_tooltip.hide();
this.renderUserContext(target, actions);
}

View file

@ -386,7 +386,7 @@ export default class Emotes extends Module {
return;
this.toggleFavorite(source, id);
const tt = target._ffz_tooltip$0;
const tt = target._ffz_tooltip;
if ( tt && tt.visible ) {
tt.hide();
setTimeout(() => document.contains(target) && tt.show(), 0);

View file

@ -56,7 +56,7 @@ export default class Overrides extends Module {
v.$destroy();
}
const parent = document.body.querySelector('#root>div') || document.body;
const parent = document.fullscreenElement || document.body.querySelector('#root>div') || document.body;
popup = new Tooltip(parent, [], {
logger: this.log,

View file

@ -144,8 +144,10 @@ export default {
props: ['item', 'context'],
data() {
const settings = this.context.getFFZ().resolve('settings');
return {
filters: deep_copy(require('src/settings/filters.js')),
filters: deep_copy(settings.filters),
old_name: null,
old_desc: null,

View file

@ -59,16 +59,30 @@ export default class TooltipProvider extends Module {
this.types.text = target => sanitize(target.dataset.title);
this.types.html = target => target.dataset.title;
this.onFSChange = this.onFSChange.bind(this);
}
onEnable() {
const container = document.querySelector('.sunlight-root') || document.querySelector('#root>div') || document.querySelector('#root') || document.querySelector('.clips-root') || document.body;
window.addEventListener('fullscreenchange', this.onFSChange);
// is_minimal = false; //container && container.classList.contains('twilight-minimal-root');
this.tips = new Tooltip(container, 'ffz-tooltip', {
this.container = container;
this.tip_element = container;
this.tips = this._createInstance(container);
this.on(':cleanup', this.cleanup);
}
_createInstance(container) {
return new Tooltip(container, 'ffz-tooltip', {
html: true,
i18n: this.i18n,
live: true,
delayHide: this.checkDelayHide.bind(this),
delayShow: this.checkDelayShow.bind(this),
@ -99,10 +113,20 @@ export default class TooltipProvider extends Module {
this.emit(':leave', target, tip, event);
}
});
this.on(':cleanup', this.cleanup);
}
onFSChange() {
const tip_element = document.fullscreenElement || this.container;
if ( tip_element !== this.tip_element ) {
this.tips.destroy();
this.tip_element = tip_element;
this.tips = this._createInstance(tip_element);
}
}
cleanup() {
this.tips.cleanup();
}

View file

@ -12,6 +12,8 @@ import SettingsProfile from './profile';
import SettingsContext from './context';
import MigrationManager from './migration';
import * as FILTERS from './filters';
// ============================================================================
// SettingsManager
@ -38,12 +40,19 @@ export default class SettingsManager extends Module {
this.ui_structures = new Map;
this.definitions = new Map;
// Filters
this.filters = {};
for(const key in FILTERS)
if ( has(FILTERS, key) )
this.filters[key] = FILTERS[key];
// Create our provider as early as possible.
const provider = this.provider = this._createProvider();
this.log.info(`Using Provider: ${provider.constructor.name}`);
provider.on('changed', this._onProviderChange, this);
this.migrations = new MigrationManager(this);
// Also create the main context as early as possible.
@ -63,6 +72,20 @@ export default class SettingsManager extends Module {
this.enable();
}
addFilter(key, data) {
if ( this.filters[key] )
return this.log.warn('Tried to add already existing filter', key);
this.filters[key] = data;
this.updateRoutes();
}
getFilterBasicEditor() { // eslint-disable-line class-methods-use-this
return () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue')
}
generateLog() {
const out = [];
for(const [key, value] of this.main_context.__cache.entries())

View file

@ -58,7 +58,7 @@ export default class SettingsProfile extends EventEmitter {
matches(context) {
if ( ! this.matcher )
this.matcher = createTester(this.context, require('./filters'));
this.matcher = createTester(this.context, this.manager.filters);
return this.matcher(context);
}

View file

@ -12,6 +12,7 @@ import Fine from 'utilities/compat/fine';
import FineRouter from 'utilities/compat/fine-router';
import Apollo from 'utilities/compat/apollo';
import TwitchData from 'utilities/twitch-data';
import Subpump from 'utilities/compat/subpump';
import Switchboard from './switchboard';
@ -36,6 +37,7 @@ export default class Twilight extends BaseSite {
this.inject(Apollo, false);
this.inject(TwitchData);
this.inject(Switchboard);
this.inject(Subpump);
this._dom_updates = [];
}

View file

@ -7,6 +7,7 @@
import Module from 'utilities/module';
import { Color } from 'utilities/color';
import {debounce} from 'utilities/object';
import { valueToNode } from 'C:/Users/Stendec/AppData/Local/Microsoft/TypeScript/3.8/node_modules/@babel/types/lib/index';
const USER_PAGES = ['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following'];
@ -22,6 +23,7 @@ export default class Channel extends Module {
this.inject('settings');
this.inject('site.css_tweaks');
this.inject('site.elemental');
this.inject('site.subpump');
this.inject('site.fine');
this.inject('site.router');
this.inject('site.twitch_data');
@ -37,11 +39,16 @@ export default class Channel extends Module {
}
});
/*this.SideNav = this.elemental.define(
'side-nav', '.side-bar-contents .side-nav-section:first-child',
null,
{childNodes: true, subtree: true}, 1
);*/
this.settings.add('channel.hosting.enable', {
default: true,
ui: {
path: 'Channel > Behavior >> Hosting',
title: 'Enable Channel Hosting',
component: 'setting-check-box'
},
changed: val => ! val && this.InfoBar.each(el => this.updateBar(el))
});
this.ChannelRoot = this.elemental.define(
'channel-root', '.channel-root',
@ -59,10 +66,6 @@ export default class Channel extends Module {
onEnable() {
this.updateChannelColor();
//this.SideNav.on('mount', this.updateHidden, this);
//this.SideNav.on('mutate', this.updateHidden, this);
//this.SideNav.each(el => this.updateHidden(el));
this.ChannelRoot.on('mount', this.updateRoot, this);
this.ChannelRoot.on('mutate', this.updateRoot, this);
this.ChannelRoot.on('unmount', this.removeRoot, this);
@ -73,10 +76,13 @@ export default class Channel extends Module {
this.InfoBar.on('unmount', this.removeBar, this);
this.InfoBar.each(el => this.updateBar(el));
this.subpump.on(':pubsub-message', this.onPubSub, this);
this.router.on(':route', route => {
if ( route?.name === 'user' )
setTimeout(this.maybeClickChat.bind(this), 1000);
}, this);
this.maybeClickChat();
}
@ -88,21 +94,50 @@ export default class Channel extends Module {
}
}
/*updateHidden(el) { // eslint-disable-line class-methods-use-this
if ( ! el._ffz_raf )
el._ffz_raf = requestAnimationFrame(() => {
el._ffz_raf = null;
const nodes = el.querySelectorAll('.side-nav-card');
for(const node of nodes) {
const react = this.fine.getReactInstance(node),
props = react?.return?.return?.return?.memoizedProps;
const offline = props?.offline ?? node.querySelector('.side-nav-card__avatar--offline') != null;
node.classList.toggle('ffz--offline-side-nav', offline);
setHost(channel_id, channel_login, target_id, target_login) {
const topic = `stream-chat-room-v1.${channel_id}`;
this.subpump.inject(topic, {
type: 'host_target_change',
data: {
channel_id,
channel_login,
target_channel_id: target_id || null,
target_channel_login: target_login || null,
previous_target_channel_id: null,
num_viewers: 0
}
});
}*/
this.subpump.inject(topic, {
type: 'host_target_change_v2',
data: {
channel_id,
channel_login,
target_channel_id: target_id || null,
target_channel_login: target_login || null,
previous_target_channel_id: null,
num_viewers: 0
}
});
}
onPubSub(event) {
if ( event.prefix !== 'stream-chat-room-v1' || this.settings.get('channel.hosting.enable') )
return;
const type = event.message.type;
if ( type === 'host_target_change' || type === 'host_target_change_v2' ) {
this.log.info('Nulling Host Target Change', type);
event.message.data.target_channel_id = null;
event.message.data.target_channel_login = null;
event.message.data.previous_target_channel_id = null;
event.message.data.num_viewers = 0;
event.markChanged();
}
}
updateSubscription(login) {
if ( this._subbed_login === login )
@ -154,6 +189,9 @@ export default class Channel extends Module {
return;
}
if ( ! this.settings.get('channel.hosting.enable') && props.hostLogin )
this.setHost(props.channelID, props.channelLogin, null, null);
this.updateSubscription(props.channelLogin);
this.updateMetadata(el);
}

View file

@ -1155,7 +1155,7 @@ export default class EmoteMenu extends Module {
/*clickRefresh(event) {
const target = event.currentTarget,
tt = target && target._ffz_tooltip$0;
tt = target && target._ffz_tooltip;
if ( tt && tt.hide )
tt.hide();

View file

@ -19,9 +19,9 @@ export const CARD_CONTEXTS = ((e ={}) => {
})();
const CREATIVE_ID = 488191;
//const CREATIVE_ID = 488191;
const DIR_ROUTES = ['dir', 'dir-community', 'dir-community-index', 'dir-creative', 'dir-following', 'dir-game-index', 'dir-game-clips', 'dir-game-videos', 'dir-all', 'dir-category', 'user-videos', 'user-clips'];
const DIR_ROUTES = ['front-page', 'dir', 'dir-community', 'dir-community-index', 'dir-creative', 'dir-following', 'dir-game-index', 'dir-game-clips', 'dir-game-videos', 'dir-all', 'dir-category', 'user-videos', 'user-clips'];
export default class Directory extends SiteModule {
@ -53,7 +53,6 @@ export default class Directory extends SiteModule {
DIR_ROUTES
);
this.settings.add('directory.hidden.style', {
default: 2,
@ -133,6 +132,18 @@ export default class Directory extends SiteModule {
changed: value => this.css_tweaks.toggleHide('dir-live-ind', value)
});
this.settings.add('directory.hide-promoted', {
default: false,
ui: {
path: 'Directory > Channels >> Appearance',
title: 'Do not show Promoted streams in the directory.',
component: 'setting-check-box'
},
changed: () => this.updateCards()
});
this.settings.add('directory.hide-vodcasts', {
default: false,
@ -324,7 +335,8 @@ export default class Directory extends SiteModule {
el.dataset.ffzType = props.streamType;
const should_hide = (props.streamType === 'rerun' && this.settings.get('directory.hide-vodcasts')) ||
(props.context != null && props.context !== CARD_CONTEXTS.SingleGameList && this.settings.provider.get('directory.game.blocked-games', []).includes(game));
(props.context != null && props.context !== CARD_CONTEXTS.SingleGameList && this.settings.provider.get('directory.game.blocked-games', []).includes(game)) ||
(props.sourceType === 'PROMOTION' && this.settings.get('directory.hide-promoted'));
let hide_container = el.closest('.tw-tower > div');
if ( ! hide_container )

View file

@ -92,7 +92,7 @@ export default {
methods: {
clickWithTip(event, fn, ...args) {
const el = event.target,
tip = el && el._ffz_tooltip$0,
tip = el && el._ffz_tooltip,
visible = tip && tip.visible;
visible && tip.hide();

View file

@ -434,8 +434,8 @@ export default class MenuButton extends SiteModule {
setChildren(toggle, this.renderButtonIcon(profile));
toggle.dataset.title = this.renderButtonTip(profile);
if ( toggle['_ffz_tooltip$0']?.rerender )
toggle['_ffz_tooltip$0'].rerender();
if ( toggle['_ffz_tooltip']?.rerender )
toggle['_ffz_tooltip'].rerender();
this.emit('tooltips:cleanup');

View file

@ -502,16 +502,6 @@ export default class Player extends Module {
}
});
/*this.settings.add('player.hide-squad-banner', {
default: false,
ui: {
path: 'Channel > Appearance >> General',
title: 'Hide the Squad Streaming Bar',
component: 'setting-check-box'
},
changed: () => this.SquadStreamBar.forceUpdate()
});*/
this.settings.add('player.hide-mouse', {
default: true,
ui: {
@ -541,25 +531,6 @@ export default class Player extends Module {
const t = this;
/*this.SquadStreamBar.ready(cls => {
const old_should_render = cls.prototype.shouldRenderSquadBanner;
cls.prototype.shouldRenderSquadBanner = function(...args) {
if ( t.settings.get('player.hide-squad-banner') )
return false;
return old_should_render.call(this, ...args);
}
this.SquadStreamBar.forceUpdate();
this.updateSquadContext();
});
this.SquadStreamBar.on('mount', this.updateSquadContext, this);
this.SquadStreamBar.on('update', this.updateSquadContext, this);
this.SquadStreamBar.on('unmount', this.updateSquadContext, this);*/
this.Player.ready((cls, instances) => {
const old_attach = cls.prototype.maybeAttachDomEventListeners;
@ -677,24 +648,8 @@ export default class Player extends Module {
}
cls.prototype.ffzStopAutoplay = function() {
if ( t.settings.get('player.no-autoplay') || (! t.settings.get('player.home.autoplay') && t.router.current.name === 'front-page') ) {
const player = this.props.mediaPlayerInstance,
events = this.props.playerEvents;
if ( player && player.pause && player.getPlayerState?.() === 'Playing' )
player.pause();
else if ( events ) {
const immediatePause = () => {
if ( this.props.mediaPlayerInstance?.pause ) {
this.props.mediaPlayerInstance.pause();
off(events, 'Playing', immediatePause);
}
}
t.log.info('Unable to immediately pause. Listening for playing event.');
on(events, 'Playing', immediatePause);
}
}
if ( t.settings.get('player.no-autoplay') || (! t.settings.get('player.home.autoplay') && t.router.current.name === 'front-page') )
this.stopPlayer(this.props.mediaPlayerInstance, this.props.playerEvents, this);
}
cls.prototype.ffzScheduleState = function() {
@ -858,6 +813,8 @@ export default class Player extends Module {
this.tryTheatreMode(inst);
});
this.PlayerSource.on('mount', this.checkCarousel, this);
this.PlayerSource.on('update', this.checkCarousel, this);
this.on('i18n:update', () => {
for(const inst of this.Player.instances) {
@ -867,6 +824,46 @@ export default class Player extends Module {
}
stopPlayer(player, events, inst) {
if ( player && player.pause && (player.getPlayerState?.() || player.core?.getPlayerState?.()) === 'Playing' )
player.pause();
else if ( events && ! events._ffz_stopping ) {
events._ffz_stopping = true;
const immediatePause = () => {
if ( inst.props.mediaPlayerInstance?.pause ) {
inst.props.mediaPlayerInstance.pause();
off(events, 'Playing', immediatePause);
events._ffz_stopping = false;
}
}
this.log.info('Unable to immediately pause. Listening for playing event.');
on(events, 'Playing', immediatePause);
}
}
checkCarousel(inst) {
if ( this.settings.get('channel.hosting.enable') )
return;
if ( inst.props?.playerType === 'channel_home_carousel' ) {
if ( inst.props.content?.hostChannel === inst._ffz_cached_login )
return;
inst._ffz_cached_login = inst.props.content?.hostChannel;
if ( ! inst._ffz_cached_login )
return;
const player = inst.props.mediaPlayerInstance,
events = inst.props.playerEvents;
this.stopPlayer(player, events, inst);
}
}
updateAutoPlaybackRate(inst, val) {
const player = inst.props?.mediaPlayerInstance;
if ( ! player )

View file

@ -0,0 +1,151 @@
'use strict';
// ============================================================================
// Subpump
// It controls Twitch PubSub.
// ============================================================================
import Module from 'utilities/module';
import { FFZEvent } from 'utilities/events';
export class PubSubEvent extends FFZEvent {
constructor(data) {
super(data);
this._obj = undefined;
this._changed = false;
}
markChanged() {
this._changed = true;
}
get topic() {
return this.event.topic;
}
get message() {
if ( this._obj === undefined )
this._obj = JSON.parse(this.event.message);
return this._obj;
}
set message(val) {
this._obj = val;
this._changed = true;
}
}
export default class Subpump extends Module {
constructor(...args) {
super(...args);
this.instance = null;
}
onEnable(tries = 0) {
const instances = window.__Twitch__pubsubInstances;
if ( ! instances ) {
if ( tries > 10 )
this.log.warn('Unable to find PubSub.');
else
new Promise(r => setTimeout(r, 50)).then(() => this.onEnable(tries + 1));
return;
}
for(const [key, val] of Object.entries(instances))
if ( val?._client ) {
if ( this.instance ) {
this.log.warn('Multiple PubSub instances detected. Things might act weird.');
continue;
}
this.instance = val;
this.hookClient(val._client);
}
if ( ! this.instance )
this.log.warn('Unable to find a PubSub instance.');
}
hookClient(client) {
const t = this,
orig_message = client._onMessage;
client._unbindPrimary(client._primarySocket);
client._onMessage = function(e) {
try {
if ( e.type === 'MESSAGE' && e.data?.topic ) {
const raw_topic = e.data.topic,
idx = raw_topic.indexOf('.'),
prefix = idx === -1 ? raw_topic : raw_topic.slice(0, idx),
trail = idx === -1 ? '' : raw_topic.slice(idx + 1);
const event = new PubSubEvent({
prefix,
trail,
event: e.data
});
t.emit(':pubsub-message', event);
if ( event.defaultPrevented )
return;
if ( event._changed )
e.data.message = JSON.stringify(event._obj);
}
} catch(err) {
this.log.error('Error processing PubSub event.', err);
}
return orig_message.call(this, e);
};
client._bindPrimary(client._primarySocket);
const listener = client._listens,
orig_on = listener.on,
orig_off = listener.off;
listener.on = function(topic, fn, ctx) {
const has_topic = !! listener._events?.[topic],
out = orig_on.call(this, topic, fn, ctx);
if ( ! has_topic )
t.emit(':add-topic', topic)
return out;
}
listener.off = function(topic, fn) {
const has_topic = !! listener._events?.[topic],
out = orig_off.call(this, topic, fn);
if ( has_topic && ! listener._events?.[topic] )
t.emit(':remove-topic', topic);
return out;
}
}
inject(topic, message) {
const listens = this.instance?._client?._listens;
if ( ! listens )
throw new Error('No PubSub instance available');
listens._trigger(topic, JSON.stringify(message));
}
get topics() {
const events = this.instance?._client?._listens._events;
if ( ! events )
return [];
return Object.keys(events);
}
}

View file

@ -105,8 +105,8 @@ export class Tooltip {
if ( this.options.manual ) {
// Do nothing~!
} else if ( this.live || this.elements.size > 5 ) {
parent.removeEventListener('mouseover', this._onMouseOver);
parent.removeEventListener('mouseout', this._onMouseOut);
this.parent.removeEventListener('mouseover', this._onMouseOver);
this.parent.removeEventListener('mouseout', this._onMouseOut);
} else
for(const el of this.elements) {
el.removeEventListener('mouseenter', this._onMouseOver);
@ -119,6 +119,7 @@ export class Tooltip {
this.hide(tip);
el[this._accessor] = null;
el._ffz_tooltip = null;
}
this.elements = null;
@ -205,12 +206,18 @@ export class Tooltip {
target = tip.target;
this.elements.add(target);
target._ffz_tooltip = tip;
// Set this early in case content uses it early.
tip._promises = [];
tip.waitForDom = () => tip.element ? Promise.resolve() : new Promise(s => {tip._promises.push(s)});
tip.update = () => tip._update(); // tip.popper && tip.popper.scheduleUpdate();
tip.show = () => this.show(tip);
tip.show = () => {
let tip = target[this._accessor];
if ( ! tip )
tip = target[this._accessor] = {target};
this.show(tip);
};
tip.hide = () => this.hide(tip);
tip.rerender = () => {
if ( tip.visible ) {
@ -385,6 +392,10 @@ export class Tooltip {
if ( this.live && this.elements )
this.elements.delete(tip.target);
if ( tip.target._ffz_tooltip === tip )
tip.target._ffz_tooltip = null;
tip.target[this._accessor] = null;
tip._update = tip.rerender = tip.update = noop;
tip.element = null;
tip.visible = false;