mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-25 20:18:31 +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
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.20.0",
|
"version": "4.20.1",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
"name": "New API Stress Testing",
|
"name": "New API Stress Testing",
|
||||||
"description": "Send duplicate requests to the new API server for load testing.",
|
"description": "Send duplicate requests to the new API server for load testing.",
|
||||||
"groups": [
|
"groups": [
|
||||||
{"value": true, "weight": 50},
|
{"value": true, "weight": 25},
|
||||||
{"value": false, "weight": 50}
|
{"value": false, "weight": 75}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -358,12 +358,21 @@ export default class Badges extends Module {
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
let title = bd.title || global_badge.title;
|
let title = bd.title || global_badge.title;
|
||||||
|
const tier = bd.tier || global_badge.tier;
|
||||||
|
|
||||||
if ( d.data ) {
|
if ( d.data ) {
|
||||||
if ( d.badge === 'subscriber' ) {
|
if ( d.badge === 'subscriber' ) {
|
||||||
title = this.i18n.t('badges.subscriber.months', '{title} ({count,number} Month{count,en_plural})', {
|
if ( tier > 0 )
|
||||||
title,
|
title = this.i18n.t('badges.subscriber.tier-months', '{title}\n(Tier {tier}, {months,number} Month{months,en_plural})', {
|
||||||
count: d.data
|
title,
|
||||||
});
|
tier,
|
||||||
|
months: d.data
|
||||||
|
});
|
||||||
|
else
|
||||||
|
title = this.i18n.t('badges.subscriber.months', '{title}\n({count,number} Month{count,en_plural})', {
|
||||||
|
title,
|
||||||
|
count: d.data
|
||||||
|
});
|
||||||
} else if ( d.badge === 'founder' ) {
|
} else if ( d.badge === 'founder' ) {
|
||||||
title = this.i18n.t('badges.founder.months', '{title}\n(Subscribed for {count,number} Month{count,en_plural})', {
|
title = this.i18n.t('badges.founder.months', '{title}\n(Subscribed for {count,number} Month{count,en_plural})', {
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -401,6 +401,16 @@ export default class Room {
|
||||||
const sid = data.setID,
|
const sid = data.setID,
|
||||||
bs = b[sid] = b[sid] || {};
|
bs = b[sid] = b[sid] || {};
|
||||||
|
|
||||||
|
if ( sid === 'subscriber' ) {
|
||||||
|
const id = parseInt(data.version, 10);
|
||||||
|
if ( ! isNaN(id) && isFinite(id) ) {
|
||||||
|
data.tier = (id - (id % 1000)) / 1000;
|
||||||
|
if ( data.tier < 0 )
|
||||||
|
data.tier = 0;
|
||||||
|
} else
|
||||||
|
data.tier = 0;
|
||||||
|
}
|
||||||
|
|
||||||
bs[data.version] = data;
|
bs[data.version] = data;
|
||||||
this.badge_count++;
|
this.badge_count++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,14 +81,17 @@ export default class Metadata extends Module {
|
||||||
refresh() { return this.settings.get('metadata.uptime') > 0 },
|
refresh() { return this.settings.get('metadata.uptime') > 0 },
|
||||||
|
|
||||||
setup(data) {
|
setup(data) {
|
||||||
const socket = this.resolve('socket'),
|
const socket = this.resolve('socket');
|
||||||
created_at = data?.meta?.createdAt;
|
let created = data?.channel?.live_since;
|
||||||
|
if ( ! created ) {
|
||||||
|
const created_at = data?.meta?.createdAt;
|
||||||
|
if ( ! created_at )
|
||||||
|
return {};
|
||||||
|
|
||||||
if ( ! created_at )
|
created = new Date(created_at);
|
||||||
return {};
|
}
|
||||||
|
|
||||||
const created = new Date(created_at),
|
const now = Date.now() - socket._time_drift;
|
||||||
now = Date.now() - socket._time_drift;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
created,
|
created,
|
||||||
|
@ -381,7 +384,12 @@ export default class Metadata extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMetadata(keys) {
|
updateMetadata(keys) {
|
||||||
const bar = this.resolve('site.channel_bar');
|
const channel = this.resolve('site.channel');
|
||||||
|
if ( channel )
|
||||||
|
for(const el of channel.InfoBar.instances)
|
||||||
|
channel.updateMetadata(el, keys);
|
||||||
|
|
||||||
|
/*const bar = this.resolve('site.channel_bar');
|
||||||
if ( bar ) {
|
if ( bar ) {
|
||||||
for(const inst of bar.ChannelBar.instances)
|
for(const inst of bar.ChannelBar.instances)
|
||||||
bar.updateMetadata(inst, keys);
|
bar.updateMetadata(inst, keys);
|
||||||
|
@ -391,7 +399,7 @@ export default class Metadata extends Module {
|
||||||
if ( legacy_bar ) {
|
if ( legacy_bar ) {
|
||||||
for(const inst of legacy_bar.ChannelBar.instances)
|
for(const inst of legacy_bar.ChannelBar.instances)
|
||||||
legacy_bar.updateMetadata(inst, keys);
|
legacy_bar.updateMetadata(inst, keys);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderLegacy(key, data, container, timers, refresh_fn) {
|
async renderLegacy(key, data, container, timers, refresh_fn) {
|
||||||
|
@ -465,7 +473,7 @@ export default class Metadata extends Module {
|
||||||
|
|
||||||
if ( def.popup && def.click ) {
|
if ( def.popup && def.click ) {
|
||||||
el = (<div
|
el = (<div
|
||||||
class={`tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat ffz-stat--fix-padding ${border ? 'tw-mg-l-1' : 'tw-mg-l-05 ffz-mg-r--05'}`}
|
class={`tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat ffz-stat--fix-padding ${border ? 'tw-mg-r-1' : 'tw-mg-r-05 ffz-mg-l--05'}`}
|
||||||
data-key={key}
|
data-key={key}
|
||||||
tip_content={null}
|
tip_content={null}
|
||||||
>
|
>
|
||||||
|
@ -490,7 +498,7 @@ export default class Metadata extends Module {
|
||||||
|
|
||||||
} else
|
} else
|
||||||
btn = popup = el = (<button
|
btn = popup = el = (<button
|
||||||
class={`ffz-stat tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative tw-pd-x-05 ffz-stat--fix-padding ${border ? 'tw-border tw-mg-l-1' : 'tw-font-size-5 tw-regular tw-mg-l-05 ffz-mg-r--05'}`}
|
class={`ffz-stat tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative tw-pd-x-05 ffz-stat--fix-padding ${border ? 'tw-border tw-mg-r-1' : 'tw-font-size-5 tw-regular tw-mg-r-05 ffz-mg-l--05'}`}
|
||||||
data-key={key}
|
data-key={key}
|
||||||
tip_content={null}
|
tip_content={null}
|
||||||
>
|
>
|
||||||
|
@ -589,7 +597,7 @@ export default class Metadata extends Module {
|
||||||
icon = (<span class="tw-stat__icon"><figure class={icon} /></span>);
|
icon = (<span class="tw-stat__icon"><figure class={icon} /></span>);
|
||||||
|
|
||||||
el = (<div
|
el = (<div
|
||||||
class="tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat tw-mg-l-1"
|
class="tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat tw-mg-r-1"
|
||||||
data-key={key}
|
data-key={key}
|
||||||
tip_content={null}
|
tip_content={null}
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import BaseSite from '../base';
|
import BaseSite from '../base';
|
||||||
|
|
||||||
import WebMunch from 'utilities/compat/webmunch';
|
import WebMunch from 'utilities/compat/webmunch';
|
||||||
|
import Elemental from 'utilities/compat/elemental';
|
||||||
import Fine from 'utilities/compat/fine';
|
import Fine from 'utilities/compat/fine';
|
||||||
import FineRouter from 'utilities/compat/fine-router';
|
import FineRouter from 'utilities/compat/fine-router';
|
||||||
import Apollo from 'utilities/compat/apollo';
|
import Apollo from 'utilities/compat/apollo';
|
||||||
|
@ -30,6 +31,7 @@ export default class Twilight extends BaseSite {
|
||||||
|
|
||||||
this.inject(WebMunch);
|
this.inject(WebMunch);
|
||||||
this.inject(Fine);
|
this.inject(Fine);
|
||||||
|
this.inject(Elemental);
|
||||||
this.inject('router', FineRouter);
|
this.inject('router', FineRouter);
|
||||||
this.inject(Apollo, false);
|
this.inject(Apollo, false);
|
||||||
this.inject(TwitchData);
|
this.inject(TwitchData);
|
||||||
|
@ -91,6 +93,7 @@ export default class Twilight extends BaseSite {
|
||||||
this.router.on(':route', (route, match) => {
|
this.router.on(':route', (route, match) => {
|
||||||
this.log.info('Navigation', route && route.name, match && match[0]);
|
this.log.info('Navigation', route && route.name, match && match[0]);
|
||||||
this.fine.route(route && route.name);
|
this.fine.route(route && route.name);
|
||||||
|
this.elemental.route(route && route.name);
|
||||||
this.settings.updateContext({
|
this.settings.updateContext({
|
||||||
route,
|
route,
|
||||||
route_data: match
|
route_data: match
|
||||||
|
@ -99,6 +102,7 @@ export default class Twilight extends BaseSite {
|
||||||
|
|
||||||
const current = this.router.current;
|
const current = this.router.current;
|
||||||
this.fine.route(current && current.name);
|
this.fine.route(current && current.name);
|
||||||
|
this.elemental.route(current && current.name);
|
||||||
this.settings.updateContext({
|
this.settings.updateContext({
|
||||||
route: current,
|
route: current,
|
||||||
route_data: this.router.match
|
route_data: this.router.match
|
||||||
|
@ -193,6 +197,12 @@ Twilight.KNOWN_MODULES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Twilight.POPOUT_ROUTES = [
|
||||||
|
'embed-chat',
|
||||||
|
'popout'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
Twilight.CHAT_ROUTES = [
|
Twilight.CHAT_ROUTES = [
|
||||||
'collection',
|
'collection',
|
||||||
'popout',
|
'popout',
|
||||||
|
|
|
@ -5,79 +5,188 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
import { get, has } from 'utilities/object';
|
import { Color } from 'utilities/color';
|
||||||
|
import {debounce} from 'utilities/object';
|
||||||
|
|
||||||
import Twilight from 'site';
|
|
||||||
import { Color } from 'src/utilities/color';
|
|
||||||
|
|
||||||
|
const USER_PAGES = ['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following'];
|
||||||
|
|
||||||
export default class Channel extends Module {
|
export default class Channel extends Module {
|
||||||
|
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
this.should_enable = true;
|
this.should_enable = true;
|
||||||
|
|
||||||
|
this.inject('i18n');
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
this.inject('site.fine');
|
|
||||||
this.inject('site.css_tweaks');
|
this.inject('site.css_tweaks');
|
||||||
|
this.inject('site.fine');
|
||||||
|
this.inject('site.elemental');
|
||||||
|
this.inject('site.twitch_data');
|
||||||
|
this.inject('metadata');
|
||||||
|
this.inject('socket');
|
||||||
|
|
||||||
this.joined_raids = new Set;
|
this.ChannelRoot = this.elemental.define(
|
||||||
|
'channel-root', '.channel-root',
|
||||||
this.settings.add('channel.hosting.enable', {
|
USER_PAGES,
|
||||||
default: true,
|
{attributes: true}, 1
|
||||||
ui: {
|
|
||||||
path: 'Channel > Behavior >> Hosting',
|
|
||||||
title: 'Enable Channel Hosting',
|
|
||||||
component: 'setting-check-box'
|
|
||||||
},
|
|
||||||
changed: val => this.updateChannelHosting(val)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.settings.add('channel.raids.no-autojoin', {
|
|
||||||
default: false,
|
|
||||||
ui: {
|
|
||||||
path: 'Channel > Behavior >> Raids',
|
|
||||||
title: 'Do not automatically join raids.',
|
|
||||||
component: 'setting-check-box'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*this.settings.add('channel.squads.no-autojoin', {
|
|
||||||
default: false,
|
|
||||||
ui: {
|
|
||||||
path: 'Channel > Behavior >> Squads',
|
|
||||||
title: 'Do not automatically redirect to Squad Streams.',
|
|
||||||
component: 'setting-check-box'
|
|
||||||
}
|
|
||||||
});*/
|
|
||||||
|
|
||||||
|
|
||||||
this.ChannelPage = this.fine.define(
|
|
||||||
'channel-page',
|
|
||||||
n => (n.updateHost && n.updateChannel && n.state && has(n.state, 'hostedChannel')) || (n.getHostedChannelLogin && n.handleHostingChange) || (n.onChatHostingChange && n.state && has(n.state, 'hostMode')),
|
|
||||||
['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following', 'mod-view']
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.RaidController = this.fine.define(
|
this.InfoBar = this.elemental.define(
|
||||||
'raid-controller',
|
'channel-info-bar', '.channel-info-content',
|
||||||
n => n.handleLeaveRaid && n.handleJoinRaid,
|
USER_PAGES,
|
||||||
Twilight.CHAT_ROUTES
|
{childNodes: true, subtree: true}, 1
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ChannelContext = this.fine.define(
|
|
||||||
'channel-context',
|
|
||||||
n => n.resetPrivateVariables && n.fetchChannel && n.clearBroadcastSettingsUpdateInterval,
|
|
||||||
['popout', 'embed-chat']
|
|
||||||
);
|
|
||||||
|
|
||||||
/*this.SquadController = this.fine.define(
|
|
||||||
'squad-controller',
|
|
||||||
n => n.onSquadPage && n.isValidSquad && n.handleLeaveSquad,
|
|
||||||
Twilight.CHAT_ROUTES
|
|
||||||
);*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.updateChannelColor();
|
||||||
|
|
||||||
|
this.ChannelRoot.on('mount', this.updateRoot, this);
|
||||||
|
this.ChannelRoot.on('mutate', this.updateRoot, this);
|
||||||
|
this.ChannelRoot.on('unmount', this.removeRoot, this);
|
||||||
|
this.ChannelRoot.each(el => this.updateRoot(el));
|
||||||
|
|
||||||
|
this.InfoBar.on('mount', this.updateBar, this);
|
||||||
|
this.InfoBar.on('mutate', this.updateBar, this);
|
||||||
|
this.InfoBar.on('unmount', this.removeBar, this);
|
||||||
|
this.InfoBar.each(el => this.updateBar(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubscription(login) {
|
||||||
|
if ( this._subbed_login === login )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( this._subbed_login ) {
|
||||||
|
this.socket.unsubscribe(this, `channel.${this._subbed_login}`);
|
||||||
|
this._subbed_login = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( login ) {
|
||||||
|
this.socket.subscribe(this, `channel.${login}`);
|
||||||
|
this._subbed_login = login;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBar(el) {
|
||||||
|
// TODO: Run a data check to abort early if nothing has changed before updating metadata
|
||||||
|
// thus avoiding a potential loop from mutations.
|
||||||
|
if ( ! el._ffz_update )
|
||||||
|
el._ffz_update = debounce(() => requestAnimationFrame(() => this._updateBar(el)), 1000, 2);
|
||||||
|
|
||||||
|
el._ffz_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateBar(el) {
|
||||||
|
if ( el._ffz_cont && ! el.contains(el._ffz_cont) ) {
|
||||||
|
el._ffz_cont.classList.remove('ffz--meta-tray');
|
||||||
|
el._ffz_cont = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! el._ffz_cont ) {
|
||||||
|
const report = el.querySelector('.report-button'),
|
||||||
|
cont = report && report.closest('.tw-flex-wrap.tw-justify-content-end');
|
||||||
|
|
||||||
|
if ( cont && el.contains(cont) ) {
|
||||||
|
el._ffz_cont = cont;
|
||||||
|
cont.classList.add('ffz--meta-tray');
|
||||||
|
|
||||||
|
} else
|
||||||
|
el._ffz_cont = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const react = this.fine.getReactInstance(el),
|
||||||
|
props = react?.memoizedProps?.children?.props;
|
||||||
|
|
||||||
|
if ( ! el._ffz_cont || ! props?.channelID ) {
|
||||||
|
this.updateSubscription(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateSubscription(props.channelLogin);
|
||||||
|
this.updateMetadata(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeBar(el) {
|
||||||
|
this.updateSubscription(null);
|
||||||
|
|
||||||
|
if ( el._ffz_cont )
|
||||||
|
el._ffz_cont.classList.remove('ffz--meta-tray');
|
||||||
|
|
||||||
|
el._ffz_cont = null;
|
||||||
|
if ( el._ffz_meta_timers ) {
|
||||||
|
for(const val of Object.values(el._ffz_meta_timers))
|
||||||
|
clearTimeout(val);
|
||||||
|
|
||||||
|
el._ffz_meta_timers = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
el._ffz_update = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMetadata(el, keys) {
|
||||||
|
const cont = el._ffz_cont,
|
||||||
|
react = this.fine.getReactInstance(el),
|
||||||
|
props = react?.memoizedProps?.children?.props;
|
||||||
|
|
||||||
|
if ( ! cont || ! el.contains(cont) || ! props || ! props.channelID )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( ! keys )
|
||||||
|
keys = this.metadata.keys;
|
||||||
|
else if ( ! Array.isArray(keys) )
|
||||||
|
keys = [keys];
|
||||||
|
|
||||||
|
const timers = el._ffz_meta_timers = el._ffz_meta_timers || {},
|
||||||
|
refresh_fn = key => this.updateMetadata(el, key),
|
||||||
|
data = {
|
||||||
|
channel: {
|
||||||
|
id: props.channelID,
|
||||||
|
login: props.channelLogin,
|
||||||
|
display_name: props.displayName,
|
||||||
|
live: props.isLive,
|
||||||
|
live_since: props.liveSince
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
hosted: {
|
||||||
|
login: props.hostLogin,
|
||||||
|
display_name: props.hostDisplayName
|
||||||
|
},
|
||||||
|
el,
|
||||||
|
getBroadcastID: () => this.getBroadcastID(el, props.channelID)
|
||||||
|
};
|
||||||
|
|
||||||
|
for(const key of keys)
|
||||||
|
this.metadata.renderLegacy(key, data, cont, timers, refresh_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updateRoot(el) {
|
||||||
|
const root = this.fine.getReactInstance(el),
|
||||||
|
channel = root?.return?.memoizedState?.next?.memoizedState?.current?.previousData?.result?.data?.user;
|
||||||
|
|
||||||
|
if ( channel && channel.id ) {
|
||||||
|
this.updateChannelColor(channel.primaryColorHex);
|
||||||
|
|
||||||
|
this.settings.updateContext({
|
||||||
|
channel: channel.login,
|
||||||
|
channelID: channel.id,
|
||||||
|
channelColor: channel.primaryColorHex
|
||||||
|
});
|
||||||
|
|
||||||
|
} else
|
||||||
|
this.removeRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRoot() {
|
||||||
|
this.updateChannelColor();
|
||||||
|
this.settings.updateContext({
|
||||||
|
channel: null,
|
||||||
|
channelID: null,
|
||||||
|
channelColor: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateChannelColor(color) {
|
updateChannelColor(color) {
|
||||||
let parsed = color && Color.RGBA.fromHex(color);
|
let parsed = color && Color.RGBA.fromHex(color);
|
||||||
|
@ -95,343 +204,49 @@ export default class Channel extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBroadcastID(el, channel_id) {
|
||||||
onEnable() {
|
const cache = el._ffz_bcast_cache = el._ffz_bcast_cache || {};
|
||||||
this.updateChannelColor();
|
if ( channel_id === cache.channel_id ) {
|
||||||
|
if ( Date.now() - cache.saved < 60000 )
|
||||||
this.ChannelPage.on('mount', this.wrapChannelPage, this);
|
return Promise.resolve(cache.broadcast_id);
|
||||||
this.RaidController.on('mount', this.wrapRaidController, this);
|
|
||||||
this.RaidController.on('update', this.noAutoRaids, this);
|
|
||||||
|
|
||||||
//this.SquadController.on('mount', this.noAutoSquads, this);
|
|
||||||
//this.SquadController.on('update', this.noAutoSquads, this);
|
|
||||||
|
|
||||||
this.ChannelContext.on('mount', this.onChannelContext, this);
|
|
||||||
this.ChannelContext.on('update', this.onChannelContext, this);
|
|
||||||
this.ChannelContext.on('unmount', this.offChannelContext, this);
|
|
||||||
this.ChannelContext.ready((cls, instances) => {
|
|
||||||
for(const inst of instances)
|
|
||||||
this.onChannelContext(inst);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.RaidController.ready((cls, instances) => {
|
|
||||||
for(const inst of instances)
|
|
||||||
this.wrapRaidController(inst);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ChannelPage.on('mount', this.onChannelMounted, this);
|
|
||||||
|
|
||||||
this.ChannelPage.on('unmount', () => {
|
|
||||||
this.updateChannelColor(null);
|
|
||||||
|
|
||||||
this.settings.updateContext({
|
|
||||||
channel: null,
|
|
||||||
channelID: null,
|
|
||||||
channelColor: null,
|
|
||||||
category: null,
|
|
||||||
categoryID: null,
|
|
||||||
title: null
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ChannelPage.on('update', inst => {
|
|
||||||
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.stream.game', inst) || get('state.channel.broadcastSettings.game', inst),
|
|
||||||
title = get('state.video.title', inst) || get('state.clip.title', inst) || get('state.channel.stream.title', inst) || get('state.channel.broadcastSettings.title', inst);
|
|
||||||
|
|
||||||
const color = get('state.primaryColorHex', inst);
|
|
||||||
this.updateChannelColor(color);
|
|
||||||
|
|
||||||
this.settings.updateContext({
|
|
||||||
channel: get('state.channel.login', inst),
|
|
||||||
channelID: get('state.channel.id', inst),
|
|
||||||
channelColor: color,
|
|
||||||
category: category?.name,
|
|
||||||
categoryID: category?.id,
|
|
||||||
title
|
|
||||||
});
|
|
||||||
|
|
||||||
if ( this.settings.get('channel.hosting.enable') || has(inst.state, 'hostMode') || has(inst.state, 'hostedChannel') )
|
|
||||||
return;
|
|
||||||
|
|
||||||
// We can't do this immediately because the player state
|
|
||||||
// occasionally screws up if we do.
|
|
||||||
setTimeout(() => {
|
|
||||||
const current_channel = inst.props.data && inst.props.data.variables && inst.props.data.variables.currentChannelLogin;
|
|
||||||
if ( current_channel && current_channel !== inst.state.videoPlayerSource ) {
|
|
||||||
inst.ffzExpectedHost = inst.state.videoPlayerSource;
|
|
||||||
inst.ffzOldHostHandler(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ChannelPage.ready((cls, instances) => {
|
|
||||||
for(const inst of instances)
|
|
||||||
this.onChannelMounted(inst);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onChannelContext(inst) {
|
|
||||||
if ( ! inst.state || inst.state.loading )
|
|
||||||
return;
|
|
||||||
|
|
||||||
const channel = inst.state.channel,
|
|
||||||
clip = inst.state.clip,
|
|
||||||
video = inst.state.video,
|
|
||||||
|
|
||||||
category = video?.game || clip?.game || channel?.stream?.game || channel?.broadcastSettings?.game,
|
|
||||||
title = video?.title || clip?.title || channel?.stream?.title || channel?.broadcastSettings?.title || null;
|
|
||||||
|
|
||||||
const color = inst.state?.primaryColorHex;
|
|
||||||
this.updateChannelColor(color);
|
|
||||||
|
|
||||||
this.settings.updateContext({
|
|
||||||
channel: inst.state.channel?.login,
|
|
||||||
channelID: inst.state.channel?.id,
|
|
||||||
channelColor: color,
|
|
||||||
category: category?.name,
|
|
||||||
categoryID: category?.id,
|
|
||||||
title
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
offChannelContext() {
|
|
||||||
this.updateChannelColor(null);
|
|
||||||
this.settings.updateContext({
|
|
||||||
channel: null,
|
|
||||||
channelID: null,
|
|
||||||
category: null,
|
|
||||||
categoryID: null,
|
|
||||||
channelColor: null,
|
|
||||||
title: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onChannelMounted(inst) {
|
|
||||||
this.wrapChannelPage(inst);
|
|
||||||
|
|
||||||
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.stream.game', inst) || get('state.channel.broadcastSettings.game', inst),
|
|
||||||
title = get('state.video.title', inst) || get('state.clip.title', inst) || get('state.channel.stream.title', inst) || get('state.channel.broadcastSettings.title', inst) || null;
|
|
||||||
|
|
||||||
const color = get('state.primaryColorHex', inst);
|
|
||||||
this.updateChannelColor(color);
|
|
||||||
|
|
||||||
this.settings.updateContext({
|
|
||||||
channel: get('state.channel.login', inst),
|
|
||||||
channelID: get('state.channel.id', inst),
|
|
||||||
channelColor: color,
|
|
||||||
category: category?.name,
|
|
||||||
categoryID: category?.id,
|
|
||||||
title
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
wrapRaidController(inst) {
|
|
||||||
if ( inst._ffz_wrapped )
|
|
||||||
return this.noAutoRaids(inst);
|
|
||||||
|
|
||||||
inst._ffz_wrapped = true;
|
|
||||||
|
|
||||||
const t = this,
|
|
||||||
old_handle_join = inst.handleJoinRaid;
|
|
||||||
|
|
||||||
inst.handleJoinRaid = function(event, ...args) {
|
|
||||||
const raid_id = inst.props && inst.props.raid && inst.props.raid.id;
|
|
||||||
if ( event && event.type && raid_id )
|
|
||||||
t.joined_raids.add(raid_id);
|
|
||||||
|
|
||||||
return old_handle_join.call(this, event, ...args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.noAutoRaids(inst);
|
return new Promise(async (s, f) => {
|
||||||
}
|
if ( cache.updating ) {
|
||||||
|
cache.updating.push([s, f]);
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.channel_id = channel_id;
|
||||||
|
cache.updating = [[s,f]];
|
||||||
|
let id, err;
|
||||||
|
|
||||||
noAutoSquads(inst) {
|
|
||||||
if ( this.settings.get('channel.squads.no-autojoin') )
|
|
||||||
setTimeout(() => {
|
|
||||||
if ( inst.isValidSquad() && inst.state && inst.state.hasJoined ) {
|
|
||||||
this.log.info('Automatically opting out of Squad Stream.');
|
|
||||||
inst.handleLeaveSquad();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
noAutoRaids(inst) {
|
|
||||||
if ( this.settings.get('channel.raids.no-autojoin') )
|
|
||||||
setTimeout(() => {
|
|
||||||
if ( inst.props && inst.props.raid && ! inst.isRaidCreator && inst.hasJoinedCurrentRaid ) {
|
|
||||||
const id = inst.props.raid.id;
|
|
||||||
if ( this.joined_raids.has(id) )
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.log.info('Automatically leaving raid:', id);
|
|
||||||
inst.handleLeaveRaid();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
wrapChannelPage(inst) {
|
|
||||||
if ( inst._ffz_hosting_wrapped )
|
|
||||||
return;
|
|
||||||
|
|
||||||
const t = this,
|
|
||||||
new_new_style = inst.updateChannel && has(inst.state, 'hostedChannel'),
|
|
||||||
new_style = ! new_new_style && ! inst.handleHostingChange || has(inst.state, 'hostMode');
|
|
||||||
|
|
||||||
inst.ffzGetChannel = () => {
|
|
||||||
const params = inst.props.match.params
|
|
||||||
if ( ! params )
|
|
||||||
return get('props.data.variables.currentChannelLogin', inst)
|
|
||||||
|
|
||||||
return params.channelName || params.channelLogin
|
|
||||||
}
|
|
||||||
|
|
||||||
inst.ffzOldSetState = inst.setState;
|
|
||||||
inst.setState = function(state, ...args) {
|
|
||||||
try {
|
try {
|
||||||
if ( new_new_style ) {
|
id = await this.twitch_data.getBroadcastID(channel_id);
|
||||||
const expected = inst.ffzGetChannel();
|
} catch(error) {
|
||||||
if ( has(state, 'hostedChannel') ) {
|
id = null;
|
||||||
inst.ffzExpectedHost = state.hostedChannel;
|
err = error;
|
||||||
if ( state.hostedChannel && ! t.settings.get('channel.hosting.enable') ) {
|
|
||||||
state.hostedChannel = null;
|
|
||||||
state.videoPlayerSource = expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
t.settings.updateContext({hosting: !!state.hostedChannel});
|
|
||||||
|
|
||||||
} else if ( has(state, 'videoPlayerSource') ) {
|
|
||||||
if ( state.videoPlayerSource !== expected && ! t.settings.get('channel.hosting.enable') ) {
|
|
||||||
state.videoPlayerSource = expected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if ( new_style ) {
|
|
||||||
const expected = inst.ffzGetChannel();
|
|
||||||
if ( has(state, 'hostMode') ) {
|
|
||||||
inst.ffzExpectedHost = state.hostMode;
|
|
||||||
if ( state.hostMode && ! t.settings.get('channel.hosting.enable') ) {
|
|
||||||
state.hostMode = null;
|
|
||||||
state.videoPlayerSource = expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
t.settings.updateContext({hosting: !!state.hostMode});
|
|
||||||
|
|
||||||
} else if ( has(state, 'videoPlayerSource') ) {
|
|
||||||
if ( state.videoPlayerSource !== expected && ! t.settings.get('channel.hosting.enable') )
|
|
||||||
state.videoPlayerSource = expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if ( ! t.settings.get('channel.hosting.enable') ) {
|
|
||||||
if ( has(state, 'isHosting') )
|
|
||||||
state.isHosting = false;
|
|
||||||
|
|
||||||
if ( has(state, 'videoPlayerSource') )
|
|
||||||
state.videoPlayerSource = inst.ffzGetChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( has(state, 'isHosting') )
|
|
||||||
t.settings.updateContext({hosting: state.isHosting});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
t.log.capture(err, {extra: {props: inst.props, state}});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return inst.ffzOldSetState(state, ...args);
|
const waiters = cache.updating;
|
||||||
}
|
cache.updating = null;
|
||||||
|
|
||||||
inst._ffz_hosting_wrapped = true;
|
if ( cache.channel_id !== channel_id ) {
|
||||||
|
err = new Error('Outdated');
|
||||||
|
cache.channel_id = null;
|
||||||
|
cache.broadcast_id = null;
|
||||||
|
cache.saved = 0;
|
||||||
|
for(const pair of waiters)
|
||||||
|
pair[1](err);
|
||||||
|
|
||||||
if ( new_new_style ) {
|
return;
|
||||||
const hosted = inst.ffzExpectedHost = inst.state.hostedChannel;
|
|
||||||
this.settings.updateContext({hosting: this.settings.get('channel.hosting.enable') && !!hosted});
|
|
||||||
|
|
||||||
if ( hosted && ! this.settings.get('channel.hosting.enable') ) {
|
|
||||||
inst.ffzOldSetState({
|
|
||||||
hostedChannel: null,
|
|
||||||
videoPlayerSource: inst.ffzGetChannel()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if ( new_style ) {
|
cache.broadcast_id = id;
|
||||||
const hosted = inst.ffzExpectedHost = inst.state.hostMode;
|
cache.saved = Date.now();
|
||||||
this.settings.updateContext({hosting: this.settings.get('channel.hosting.enable') && !!inst.state.hostMode});
|
|
||||||
|
|
||||||
if ( hosted && ! this.settings.get('channel.hosting.enable') ) {
|
for(const pair of waiters)
|
||||||
inst.ffzOldSetState({
|
err ? pair[1](err) : pair[0](id);
|
||||||
hostMode: null,
|
});
|
||||||
videoPlayerSource: inst.ffzGetChannel()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
inst.ffzOldGetHostedLogin = () => get('props.data.user.hosting.login', inst) || null;
|
|
||||||
inst.getHostedChannelLogin = function() {
|
|
||||||
return t.settings.get('channel.hosting.enable') ?
|
|
||||||
inst.ffzOldGetHostedLogin() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
inst.ffzOldHostHandler = inst.handleHostingChange;
|
|
||||||
inst.handleHostingChange = function(channel) {
|
|
||||||
inst.ffzExpectedHost = channel;
|
|
||||||
if ( t.settings.get('channel.hosting.enable') )
|
|
||||||
return inst.ffzOldHostHandler(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the current state and disable the current host if needed.
|
|
||||||
inst.ffzExpectedHost = inst.state.isHosting ? inst.state.videoPlayerSource : null;
|
|
||||||
this.settings.updateContext({hosting: this.settings.get('channel.hosting.enable') && inst.state.isHosting});
|
|
||||||
if ( ! this.settings.get('channel.hosting.enable') ) {
|
|
||||||
inst.ffzOldHostHandler(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we force an update so that any child components
|
|
||||||
// receive our updated handler.
|
|
||||||
inst.forceUpdate();
|
|
||||||
this.emit('site:dom-update', 'channel-page', inst);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
updateChannelHosting(val) {
|
|
||||||
if ( val === undefined )
|
|
||||||
val = this.settings.get('channel.hosting.enable');
|
|
||||||
|
|
||||||
let hosting = val;
|
|
||||||
|
|
||||||
for(const inst of this.ChannelPage.instances) {
|
|
||||||
if ( ! inst.ffzExpectedHost )
|
|
||||||
hosting = false;
|
|
||||||
|
|
||||||
if ( has(inst.state, 'hostedChannel') ) {
|
|
||||||
const host = val ? inst.ffzExpectedHost : null,
|
|
||||||
target = host && host.login || inst.ffzGetChannel();
|
|
||||||
|
|
||||||
inst.ffzOldSetState({
|
|
||||||
hostedChannel: host,
|
|
||||||
videoPlayerSource: target
|
|
||||||
});
|
|
||||||
|
|
||||||
} else if ( has(inst.state, 'hostMode') ) {
|
|
||||||
const host = val ? inst.ffzExpectedHost : null,
|
|
||||||
target = host && host.hostedChannel && host.hostedChannel.login || inst.ffzGetChannel();
|
|
||||||
|
|
||||||
inst.ffzOldSetState({
|
|
||||||
hostMode: host,
|
|
||||||
videoPlayerSource: target
|
|
||||||
});
|
|
||||||
|
|
||||||
} else
|
|
||||||
inst.ffzOldHostHandler(val ? inst.ffzExpectedHost : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.settings.updateContext({hosting});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,291 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Channel Bar
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
|
||||||
import {get} from 'utilities/object';
|
|
||||||
import {createElement} from 'utilities/dom';
|
|
||||||
|
|
||||||
//import CHANNEL_QUERY from './channel_header_query.gql';
|
|
||||||
|
|
||||||
|
|
||||||
export default class ChannelBar extends Module {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
|
|
||||||
this.should_enable = true;
|
|
||||||
|
|
||||||
this.inject('i18n');
|
|
||||||
this.inject('settings');
|
|
||||||
this.inject('site.css_tweaks');
|
|
||||||
this.inject('site.fine');
|
|
||||||
this.inject('site.web_munch');
|
|
||||||
this.inject('site.apollo');
|
|
||||||
this.inject('site.twitch_data');
|
|
||||||
this.inject('metadata');
|
|
||||||
this.inject('socket');
|
|
||||||
|
|
||||||
/*this.apollo.registerModifier('ChannelPage_ChannelHeader', CHANNEL_QUERY);
|
|
||||||
this.apollo.registerModifier('ChannelPage_ChannelHeader', data => {
|
|
||||||
const u = data && data.data && data.data.user;
|
|
||||||
if ( u ) {
|
|
||||||
const o = u.profileViewCount = new Number(u.profileViewCount || 0);
|
|
||||||
o.data = deep_copy(u);
|
|
||||||
}
|
|
||||||
}, false);*/
|
|
||||||
|
|
||||||
|
|
||||||
this.settings.add('channel.metadata.force-above', {
|
|
||||||
default: false,
|
|
||||||
ui: {
|
|
||||||
path: 'Channel > Metadata >> Appearance',
|
|
||||||
title: 'Force metadata and tags to the top of the channel information bar.',
|
|
||||||
component: 'setting-check-box'
|
|
||||||
},
|
|
||||||
changed: val => this.css_tweaks.toggle('channel-metadata-top', val)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.VideoBar = this.fine.define(
|
|
||||||
'video-bar',
|
|
||||||
n => n.props && n.props.getLastVideoOffset && n.renderTrackedHighlightButton,
|
|
||||||
['video', 'user-video']
|
|
||||||
);
|
|
||||||
|
|
||||||
this.ChannelBar = this.fine.define(
|
|
||||||
'channel-bar',
|
|
||||||
n => n.getTitle && n.getGame && n.renderGame,
|
|
||||||
['user']
|
|
||||||
);
|
|
||||||
|
|
||||||
this.ModWidget = this.fine.define(
|
|
||||||
'mod-widget',
|
|
||||||
n => n.renderToolbar && n.getToolbarControls && n.childContext,
|
|
||||||
['mod-view']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnable() {
|
|
||||||
this.css_tweaks.toggle('channel-metadata-top', this.settings.get('channel.metadata.force-above'));
|
|
||||||
|
|
||||||
this.on('i18n:update', () => {
|
|
||||||
for(const bar of this.VideoBar.instances)
|
|
||||||
this.updateVideoBar(bar);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ChannelBar.on('unmount', this.unmountChannelBar, this);
|
|
||||||
this.ChannelBar.on('mount', this.updateChannelBar, this);
|
|
||||||
this.ChannelBar.on('update', this.updateChannelBar, this);
|
|
||||||
|
|
||||||
this.ChannelBar.ready((cls, instances) => {
|
|
||||||
for(const inst of instances)
|
|
||||||
this.updateChannelBar(inst);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/*this.ModWidget.on('mount', this.updateModWidget, this);
|
|
||||||
this.ModWidget.on('update', this.updateModWidget, this);
|
|
||||||
|
|
||||||
this.ModWidget.ready((cls, instances) => {
|
|
||||||
for(const inst of instances)
|
|
||||||
this.updateModWidget(inst);
|
|
||||||
});*/
|
|
||||||
|
|
||||||
|
|
||||||
//this.VideoBar.on('unmount', this.unmountVideoBar, this);
|
|
||||||
this.VideoBar.on('mount', this.updateVideoBar, this);
|
|
||||||
this.VideoBar.on('update', this.updateVideoBar, this);
|
|
||||||
|
|
||||||
this.VideoBar.ready((cls, instances) => {
|
|
||||||
for(const inst of instances)
|
|
||||||
this.updateVideoBar(inst);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*updateModWidget(inst) {
|
|
||||||
const container = this.fine.getChildNode(inst);
|
|
||||||
if ( ! container || ! container.querySelector('.video-player-hosting-ui__container') )
|
|
||||||
return;
|
|
||||||
|
|
||||||
const header = container.querySelector('.mod-view-panel-header');
|
|
||||||
if ( ! header )
|
|
||||||
return;
|
|
||||||
|
|
||||||
let cont = header.querySelector('.ffz--stat-container');
|
|
||||||
|
|
||||||
if ( ! cont ) {
|
|
||||||
cont = <div class="ffz--stat-container tw-pd-l-05"></div>;
|
|
||||||
const contcont = header.querySelector(':scope > div:first-child > div');
|
|
||||||
if ( ! contcont )
|
|
||||||
return;
|
|
||||||
|
|
||||||
contcont.appendChild(cont);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.info('mod-widget', inst, cont);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
updateVideoBar(inst) {
|
|
||||||
const container = this.fine.getChildNode(inst),
|
|
||||||
timestamp = container && container.querySelector('[data-test-selector="date"]');
|
|
||||||
|
|
||||||
if ( ! timestamp )
|
|
||||||
return;
|
|
||||||
|
|
||||||
const published = get('props.video.publishedAt', inst);
|
|
||||||
|
|
||||||
if ( ! published )
|
|
||||||
timestamp.classList.toggle('ffz-tooltip', false);
|
|
||||||
else {
|
|
||||||
timestamp.classList.toggle('ffz-tooltip', true);
|
|
||||||
timestamp.dataset.title = this.i18n.t('video.published-on', 'Published on: {date,date}', {date: published});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
updateChannelBar(inst) {
|
|
||||||
const login = get('props.channel.login', inst);
|
|
||||||
if ( login !== inst._ffz_old_login ) {
|
|
||||||
if ( inst._ffz_old_login )
|
|
||||||
this.socket.unsubscribe(inst, `channel.${inst._ffz_old_login}`);
|
|
||||||
|
|
||||||
if ( login )
|
|
||||||
this.socket.subscribe(inst, `channel.${login}`);
|
|
||||||
inst._ffz_old_login = login;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateUptime(inst);
|
|
||||||
this.updateMetadata(inst);
|
|
||||||
}
|
|
||||||
|
|
||||||
unmountChannelBar(inst) {
|
|
||||||
if ( inst._ffz_old_login ) {
|
|
||||||
this.socket.unsubscribe(inst, `channel.${inst._ffz_old_login}`);
|
|
||||||
inst._ffz_old_login = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timers = inst._ffz_meta_timers;
|
|
||||||
if ( timers )
|
|
||||||
for(const key in timers)
|
|
||||||
if ( timers[key] )
|
|
||||||
clearTimeout(timers[key]);
|
|
||||||
|
|
||||||
inst._ffz_meta_timers = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
getBroadcastID(inst) {
|
|
||||||
const current_id = inst.props?.channel?.stream?.id;
|
|
||||||
if ( current_id === inst._ffz_stream_id ) {
|
|
||||||
if ( Date.now() - inst._ffz_broadcast_saved < 60000 )
|
|
||||||
return Promise.resolve(inst._ffz_broadcast_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise(async (s, f) => {
|
|
||||||
if ( inst._ffz_broadcast_updating )
|
|
||||||
return inst._ffz_broadcast_updating.push([s, f]);
|
|
||||||
|
|
||||||
inst._ffz_broadcast_updating = [[s, f]];
|
|
||||||
|
|
||||||
let id, err;
|
|
||||||
|
|
||||||
try {
|
|
||||||
id = await this.twitch_data.getBroadcastID(inst.props.channel.id);
|
|
||||||
} catch(error) {
|
|
||||||
id = null;
|
|
||||||
err = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const waiters = inst._ffz_broadcast_updating;
|
|
||||||
inst._ffz_broadcast_updating = null;
|
|
||||||
|
|
||||||
if ( current_id !== inst.props?.channel?.stream?.id ) {
|
|
||||||
err = new Error('Outdated');
|
|
||||||
inst._ffz_stream_id = null;
|
|
||||||
inst._ffz_broadcast_saved = 0;
|
|
||||||
inst._ffz_broadcast_id = null;
|
|
||||||
|
|
||||||
for(const pair of waiters)
|
|
||||||
pair[1](err);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inst._ffz_broadcast_id = id;
|
|
||||||
inst._ffz_broadcast_saved = Date.now();
|
|
||||||
inst._ffz_stream_id = current_id;
|
|
||||||
|
|
||||||
if ( err ) {
|
|
||||||
for(const pair of waiters)
|
|
||||||
pair[1](err);
|
|
||||||
} else {
|
|
||||||
for(const pair of waiters)
|
|
||||||
pair[0](id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async updateUptime(inst) {
|
|
||||||
const current_id = inst?.props?.channel?.id;
|
|
||||||
if ( current_id === inst._ffz_uptime_id ) {
|
|
||||||
if ( Date.now() - inst._ffz_uptime_saved < 60000 )
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( inst._ffz_uptime_updating )
|
|
||||||
return;
|
|
||||||
|
|
||||||
inst._ffz_uptime_updating = true;
|
|
||||||
inst._ffz_uptime_id = current_id;
|
|
||||||
|
|
||||||
if ( ! current_id )
|
|
||||||
inst._ffz_meta = null;
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
inst._ffz_meta = await this.twitch_data.getStreamMeta(current_id, inst?.props?.channel?.login);
|
|
||||||
} catch(err) {
|
|
||||||
this.log.capture(err);
|
|
||||||
this.log.error('Error fetching uptime:', err);
|
|
||||||
inst._ffz_meta = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inst._ffz_uptime_saved = Date.now();
|
|
||||||
inst._ffz_uptime_updating = false;
|
|
||||||
|
|
||||||
this.updateMetadata(inst);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
updateMetadata(inst, keys) {
|
|
||||||
const container = this.fine.getChildNode(inst),
|
|
||||||
metabar = container?.querySelector?.('.channel-info-bar__viewers-count-wrapper > .tw-flex:last-child');
|
|
||||||
|
|
||||||
if ( ! inst._ffz_mounted || ! metabar )
|
|
||||||
return;
|
|
||||||
|
|
||||||
if ( ! keys )
|
|
||||||
keys = this.metadata.keys;
|
|
||||||
else if ( ! Array.isArray(keys) )
|
|
||||||
keys = [keys];
|
|
||||||
|
|
||||||
const timers = inst._ffz_meta_timers = inst._ffz_meta_timers || {},
|
|
||||||
refresh_func = key => this.updateMetadata(inst, key),
|
|
||||||
data = {
|
|
||||||
channel: inst.props.channel,
|
|
||||||
meta: inst._ffz_meta,
|
|
||||||
hosting: false,
|
|
||||||
legacy: true,
|
|
||||||
_inst: inst,
|
|
||||||
getBroadcastID: () => this.getBroadcastID(inst)
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const key of keys)
|
|
||||||
this.metadata.renderLegacy(key, data, metabar, timers, refresh_func);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,8 +5,7 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import {ColorAdjuster} from 'utilities/color';
|
import {ColorAdjuster} from 'utilities/color';
|
||||||
import {setChildren} from 'utilities/dom';
|
import {get, has, make_enum, shallow_object_equals, set_equals, deep_equals} from 'utilities/object';
|
||||||
import {get, has, make_enum, split_chars, shallow_object_equals, set_equals, deep_equals} from 'utilities/object';
|
|
||||||
import {WEBKIT_CSS as WEBKIT} from 'utilities/constants';
|
import {WEBKIT_CSS as WEBKIT} from 'utilities/constants';
|
||||||
import {FFZEvent} from 'utilities/events';
|
import {FFZEvent} from 'utilities/events';
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ import Input from './input';
|
||||||
import ViewerCards from './viewer_card';
|
import ViewerCards from './viewer_card';
|
||||||
|
|
||||||
|
|
||||||
const REGEX_EMOTES = {
|
/*const REGEX_EMOTES = {
|
||||||
'B-?\\)': ['B)', 'B-)'],
|
'B-?\\)': ['B)', 'B-)'],
|
||||||
'R-?\\)': ['R)', 'R-)'],
|
'R-?\\)': ['R)', 'R-)'],
|
||||||
'[oO](_|\\.)[oO]': ['o_o', 'O_o', 'o_O', 'O_O', 'o.o', 'O.o', 'o.O', 'O.O'],
|
'[oO](_|\\.)[oO]': ['o_o', 'O_o', 'o_O', 'O_O', 'o.o', 'O.o', 'o.O', 'O.O'],
|
||||||
|
@ -42,7 +41,7 @@ const REGEX_EMOTES = {
|
||||||
'\\<\\;\\]': ['<]'],
|
'\\<\\;\\]': ['<]'],
|
||||||
'\\:-?(S|s)': [':s', ':S', ':-s', ':-S'],
|
'\\:-?(S|s)': [':s', ':S', ':-s', ':-S'],
|
||||||
'\\:\\>\\;': [':>']
|
'\\:\\>\\;': [':>']
|
||||||
};
|
};*/
|
||||||
|
|
||||||
|
|
||||||
const MESSAGE_TYPES = make_enum(
|
const MESSAGE_TYPES = make_enum(
|
||||||
|
@ -190,11 +189,13 @@ export default class ChatHook extends Module {
|
||||||
Twilight.CHAT_ROUTES
|
Twilight.CHAT_ROUTES
|
||||||
);
|
);
|
||||||
|
|
||||||
/*this.PinnedCheer = this.fine.define(
|
this.joined_raids = new Set;
|
||||||
'pinned-cheer',
|
|
||||||
n => n.collapseCheer && n.saveRenderedMessageRef,
|
this.RaidController = this.fine.define(
|
||||||
|
'raid-controller',
|
||||||
|
n => n.handleLeaveRaid && n.handleJoinRaid,
|
||||||
Twilight.CHAT_ROUTES
|
Twilight.CHAT_ROUTES
|
||||||
);*/
|
);
|
||||||
|
|
||||||
this.InlineCallout = this.fine.define(
|
this.InlineCallout = this.fine.define(
|
||||||
'inline-callout',
|
'inline-callout',
|
||||||
|
@ -246,6 +247,15 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|
||||||
|
this.settings.add('channel.raids.no-autojoin', {
|
||||||
|
default: false,
|
||||||
|
ui: {
|
||||||
|
path: 'Channel > Behavior >> Raids',
|
||||||
|
title: 'Do not automatically join raids.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('chat.hide-community-highlights', {
|
this.settings.add('chat.hide-community-highlights', {
|
||||||
default: false,
|
default: false,
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -729,6 +739,13 @@ export default class ChatHook extends Module {
|
||||||
this.updateLineBorders();
|
this.updateLineBorders();
|
||||||
this.updateMentionCSS();
|
this.updateMentionCSS();
|
||||||
|
|
||||||
|
this.RaidController.on('mount', this.wrapRaidController, this);
|
||||||
|
this.RaidController.on('update', this.noAutoRaids, this);
|
||||||
|
this.RaidController.ready((cls, instances) => {
|
||||||
|
for(const inst of instances)
|
||||||
|
this.wrapRaidController(inst);
|
||||||
|
});
|
||||||
|
|
||||||
this.InlineCallout.on('mount', this.onInlineCallout, this);
|
this.InlineCallout.on('mount', this.onInlineCallout, this);
|
||||||
this.InlineCallout.on('update', this.onInlineCallout, this);
|
this.InlineCallout.on('update', this.onInlineCallout, this);
|
||||||
this.InlineCallout.ready(() => this.updateInlineCallouts());
|
this.InlineCallout.ready(() => this.updateInlineCallouts());
|
||||||
|
@ -1002,7 +1019,7 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
|
|
||||||
this.ChatContainer.on('mount', this.containerMounted, this);
|
this.ChatContainer.on('mount', this.containerMounted, this);
|
||||||
this.ChatContainer.on('unmount', this.removeRoom, this);
|
this.ChatContainer.on('unmount', this.containerUnmounted, this); //removeRoom, this);
|
||||||
this.ChatContainer.on('update', this.containerUpdated, this);
|
this.ChatContainer.on('update', this.containerUpdated, this);
|
||||||
|
|
||||||
this.ChatContainer.ready((cls, instances) => {
|
this.ChatContainer.ready((cls, instances) => {
|
||||||
|
@ -1048,15 +1065,41 @@ export default class ChatHook extends Module {
|
||||||
for(const inst of instances)
|
for(const inst of instances)
|
||||||
this.containerMounted(inst);
|
this.containerMounted(inst);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*this.PinnedCheer.on('mount', this.fixPinnedCheer, this);
|
wrapRaidController(inst) {
|
||||||
this.PinnedCheer.on('update', this.fixPinnedCheer, this);
|
if ( inst._ffz_wrapped )
|
||||||
|
return this.noAutoRaids(inst);
|
||||||
|
|
||||||
this.PinnedCheer.ready((cls, instances) => {
|
inst._ffz_wrapped = true;
|
||||||
for(const inst of instances)
|
|
||||||
this.fixPinnedCheer(inst);
|
const t = this,
|
||||||
});*/
|
old_handle_join = inst.handleJoinRaid;
|
||||||
|
|
||||||
|
inst.handleJoinRaid = function(event, ...args) {
|
||||||
|
const raid_id = inst.props && inst.props.raid && inst.props.raid.id;
|
||||||
|
if ( event && event.type && raid_id )
|
||||||
|
t.joined_raids.add(raid_id);
|
||||||
|
|
||||||
|
return old_handle_join.call(this, event, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.noAutoRaids(inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
noAutoRaids(inst) {
|
||||||
|
if ( this.settings.get('channel.raids.no-autojoin') )
|
||||||
|
setTimeout(() => {
|
||||||
|
if ( inst.props && inst.props.raid && ! inst.isRaidCreator && inst.hasJoinedCurrentRaid ) {
|
||||||
|
const id = inst.props.raid.id;
|
||||||
|
if ( this.joined_raids.has(id) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.log.info('Automatically leaving raid:', id);
|
||||||
|
inst.handleLeaveRaid();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1583,30 +1626,6 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
cls.prototype._ffz_was_here = true;
|
cls.prototype._ffz_was_here = true;
|
||||||
|
|
||||||
/*cls.prototype.ffzGetEmotes = function() {
|
|
||||||
const emote_map = this.client && this.client.session && this.client.session.emoteMap;
|
|
||||||
if ( this._ffz_cached_map === emote_map )
|
|
||||||
return this._ffz_cached_emotes;
|
|
||||||
|
|
||||||
this._ffz_cached_map = emote_map;
|
|
||||||
const emotes = this._ffz_cached_emotes = {};
|
|
||||||
|
|
||||||
if ( emote_map )
|
|
||||||
for(const emote of Object.values(emote_map))
|
|
||||||
if ( emote ) {
|
|
||||||
const token = emote.token;
|
|
||||||
if ( Array.isArray(REGEX_EMOTES[token]) ) {
|
|
||||||
for(const tok of REGEX_EMOTES[token] )
|
|
||||||
emotes[tok] = emote.id;
|
|
||||||
|
|
||||||
} else
|
|
||||||
emotes[token] = emote.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return emotes;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
cls.prototype._ffzInstall = function() {
|
cls.prototype._ffzInstall = function() {
|
||||||
if ( this._ffz_installed )
|
if ( this._ffz_installed )
|
||||||
return;
|
return;
|
||||||
|
@ -2029,52 +2048,10 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
|
|
||||||
updateChatLines() {
|
updateChatLines() {
|
||||||
//this.PinnedCheer.forceUpdate();
|
|
||||||
this.chat_line.updateLines();
|
this.chat_line.updateLines();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// Pinned Cheers
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/*fixPinnedCheer(inst) {
|
|
||||||
const el = this.fine.getChildNode(inst),
|
|
||||||
container = el && el.querySelector && el.querySelector('.pinned-cheer__headline'),
|
|
||||||
tc = inst.props.topCheer;
|
|
||||||
|
|
||||||
if ( ! container || ! tc )
|
|
||||||
return;
|
|
||||||
|
|
||||||
container.dataset.roomId = inst.props.channelID;
|
|
||||||
container.dataset.room = inst.props.channelLogin && inst.props.channelLogin.toLowerCase();
|
|
||||||
container.dataset.userId = tc.user.userID;
|
|
||||||
container.dataset.user = tc.user.userLogin && tc.user.userLogin.toLowerCase();
|
|
||||||
|
|
||||||
if ( tc.user.color ) {
|
|
||||||
const user_el = container.querySelector('.chat-author__display-name');
|
|
||||||
if ( user_el )
|
|
||||||
user_el.style.color = this.colors.process(tc.user.color);
|
|
||||||
|
|
||||||
const login_el = container.querySelector('.chat-author__intl-login');
|
|
||||||
if ( login_el )
|
|
||||||
login_el.style.color = this.colors.process(tc.user.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bit_el = container.querySelector('.chat-line__message--emote'),
|
|
||||||
cont = bit_el ? bit_el.parentElement.parentElement : container.querySelector('.ffz--pinned-top-emote'),
|
|
||||||
prefix = extractCheerPrefix(tc.messageParts);
|
|
||||||
|
|
||||||
if ( cont && prefix ) {
|
|
||||||
const tokens = this.chat.tokenizeString(`${prefix}${tc.bits}`, tc);
|
|
||||||
|
|
||||||
cont.classList.add('ffz--pinned-top-emote');
|
|
||||||
cont.innerHTML = '';
|
|
||||||
setChildren(cont, this.chat.renderTokens(tokens));
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Room Handling
|
// Room Handling
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
@ -2278,6 +2255,17 @@ export default class ChatHook extends Module {
|
||||||
this.updateRoomBitsConfig(cont, props.bitsConfig);
|
this.updateRoomBitsConfig(cont, props.bitsConfig);
|
||||||
|
|
||||||
if ( props.data ) {
|
if ( props.data ) {
|
||||||
|
if ( Twilight.POPOUT_ROUTES.includes(this.router.current_name) ) {
|
||||||
|
const color = props.data.user?.primaryColorHex;
|
||||||
|
this.resolve('site.channel').updateChannelColor(color);
|
||||||
|
|
||||||
|
this.settings.updateContext({
|
||||||
|
channel: props.channelLogin,
|
||||||
|
channelID: props.channelID,
|
||||||
|
channelColor: color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.chat.badges.updateTwitchBadges(props.data.badges);
|
this.chat.badges.updateTwitchBadges(props.data.badges);
|
||||||
this.updateRoomBadges(cont, props.data.user && props.data.user.broadcastBadges);
|
this.updateRoomBadges(cont, props.data.user && props.data.user.broadcastBadges);
|
||||||
this.updateRoomRules(cont, props.chatRules);
|
this.updateRoomRules(cont, props.chatRules);
|
||||||
|
@ -2285,6 +2273,21 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
containerUnmounted(cont) {
|
||||||
|
if ( Twilight.POPOUT_ROUTES.includes(this.router.current_name) ) {
|
||||||
|
this.resolve('site.channel').updateChannelColor();
|
||||||
|
|
||||||
|
this.settings.updateContext({
|
||||||
|
channel: null,
|
||||||
|
channelID: null,
|
||||||
|
channelColor: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeRoom(cont);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
containerUpdated(cont, props) {
|
containerUpdated(cont, props) {
|
||||||
// If we don't have a room, or if the room ID doesn't match our ID
|
// If we don't have a room, or if the room ID doesn't match our ID
|
||||||
// then we need to just create a new Room because the chat room changed.
|
// then we need to just create a new Room because the chat room changed.
|
||||||
|
@ -2298,6 +2301,17 @@ export default class ChatHook extends Module {
|
||||||
if ( props.bitsConfig !== cont.props.bitsConfig )
|
if ( props.bitsConfig !== cont.props.bitsConfig )
|
||||||
this.updateRoomBitsConfig(cont, props.bitsConfig);
|
this.updateRoomBitsConfig(cont, props.bitsConfig);
|
||||||
|
|
||||||
|
if ( props.data && Twilight.POPOUT_ROUTES.includes(this.router.current_name) ) {
|
||||||
|
const color = props.data.user?.primaryColorHex;
|
||||||
|
this.resolve('site.channel').updateChannelColor(color);
|
||||||
|
|
||||||
|
this.settings.updateContext({
|
||||||
|
channel: props.channelLogin,
|
||||||
|
channelID: props.channelID,
|
||||||
|
channelColor: color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Twitch, React, and Apollo are the trifecta of terror so we
|
// Twitch, React, and Apollo are the trifecta of terror so we
|
||||||
// can't compare the badgeSets property in any reasonable way.
|
// can't compare the badgeSets property in any reasonable way.
|
||||||
// Instead, just check the lengths to see if they've changed
|
// Instead, just check the lengths to see if they've changed
|
||||||
|
@ -2421,40 +2435,4 @@ export function formatBitsConfig(config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*export function findEmotes(msg, emotes) {
|
|
||||||
const out = {};
|
|
||||||
let idx = 0;
|
|
||||||
|
|
||||||
for(const part of msg.split(' ')) {
|
|
||||||
const len = split_chars(part).length;
|
|
||||||
|
|
||||||
if ( has(emotes, part) ) {
|
|
||||||
const em = emotes[part],
|
|
||||||
matches = out[em] = out[em] || [];
|
|
||||||
|
|
||||||
matches.push({
|
|
||||||
startIndex: idx,
|
|
||||||
endIndex: idx + len - 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
idx += len + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
function extractCheerPrefix(parts) {
|
|
||||||
for(const part of parts) {
|
|
||||||
if ( part.type !== 3 || ! part.content.cheerAmount )
|
|
||||||
continue;
|
|
||||||
|
|
||||||
return part.content.alt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -59,6 +59,21 @@ export default class CSSTweaks extends Module {
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
|
|
||||||
|
this.settings.add('metadata.uptime.no-native', {
|
||||||
|
requires: ['metadata.uptime'],
|
||||||
|
default: null,
|
||||||
|
process(ctx, val) {
|
||||||
|
return val == null ? ctx.get('metadata.uptime') !== 0 : val
|
||||||
|
},
|
||||||
|
changed: val => this.toggle('hide-native-uptime', val),
|
||||||
|
ui: {
|
||||||
|
path: 'Channel > Metadata >> Player',
|
||||||
|
title: "Hide Twitch's native Stream Uptime.",
|
||||||
|
description: "By default, this is enabled whenever FFZ's own Stream Uptime display is enabled to avoid redundant information.",
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('layout.use-chat-fix', {
|
this.settings.add('layout.use-chat-fix', {
|
||||||
requires: ['layout.swap-sidebars', 'layout.use-portrait', 'chat.use-width'],
|
requires: ['layout.swap-sidebars', 'layout.use-portrait', 'chat.use-width'],
|
||||||
process(ctx) {
|
process(ctx) {
|
||||||
|
@ -318,6 +333,7 @@ export default class CSSTweaks extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
|
this.toggle('hide-native-uptime', this.settings.get('metadata.uptime.no-native'));
|
||||||
this.toggle('chat-fix', this.settings.get('layout.use-chat-fix'));
|
this.toggle('chat-fix', this.settings.get('layout.use-chat-fix'));
|
||||||
this.toggle('swap-sidebars', this.settings.get('layout.swap-sidebars'));
|
this.toggle('swap-sidebars', this.settings.get('layout.swap-sidebars'));
|
||||||
this.toggle('minimal-navigation', this.settings.get('layout.minimal-navigation'));
|
this.toggle('minimal-navigation', this.settings.get('layout.minimal-navigation'));
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
.ffz--meta-tray {
|
||||||
|
.live-time {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :first-child > :last-child {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,14 +39,14 @@ export default class Game extends SiteModule {
|
||||||
}, false);*/
|
}, false);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
/*modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
||||||
const edges = get('data.game.streams.edges', res);
|
const edges = get('data.game.streams.edges', res);
|
||||||
if ( ! edges || ! edges.length )
|
if ( ! edges || ! edges.length )
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
res.data.game.streams.edges = this.parent.processNodes(edges, true);
|
res.data.game.streams.edges = this.parent.processNodes(edges, true);
|
||||||
return res;
|
return res;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.GameHeader.on('mount', this.updateGameHeader, this);
|
this.GameHeader.on('mount', this.updateGameHeader, this);
|
||||||
|
|
|
@ -54,11 +54,11 @@ export default class Player extends Module {
|
||||||
|
|
||||||
// React Components
|
// React Components
|
||||||
|
|
||||||
this.SquadStreamBar = this.fine.define(
|
/*this.SquadStreamBar = this.fine.define(
|
||||||
'squad-stream-bar',
|
'squad-stream-bar',
|
||||||
n => n.shouldRenderSquadBanner && n.props && n.props.triggerPlayerReposition,
|
n => n.shouldRenderSquadBanner && n.props && n.props.triggerPlayerReposition,
|
||||||
PLAYER_ROUTES
|
PLAYER_ROUTES
|
||||||
);
|
);*/
|
||||||
|
|
||||||
this.PersistentPlayer = this.fine.define(
|
this.PersistentPlayer = this.fine.define(
|
||||||
'persistent-player',
|
'persistent-player',
|
||||||
|
@ -502,7 +502,7 @@ export default class Player extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.settings.add('player.hide-squad-banner', {
|
/*this.settings.add('player.hide-squad-banner', {
|
||||||
default: false,
|
default: false,
|
||||||
ui: {
|
ui: {
|
||||||
path: 'Channel > Appearance >> General',
|
path: 'Channel > Appearance >> General',
|
||||||
|
@ -510,7 +510,7 @@ export default class Player extends Module {
|
||||||
component: 'setting-check-box'
|
component: 'setting-check-box'
|
||||||
},
|
},
|
||||||
changed: () => this.SquadStreamBar.forceUpdate()
|
changed: () => this.SquadStreamBar.forceUpdate()
|
||||||
});
|
});*/
|
||||||
|
|
||||||
this.settings.add('player.hide-mouse', {
|
this.settings.add('player.hide-mouse', {
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -541,7 +541,7 @@ export default class Player extends Module {
|
||||||
|
|
||||||
const t = this;
|
const t = this;
|
||||||
|
|
||||||
this.SquadStreamBar.ready(cls => {
|
/*this.SquadStreamBar.ready(cls => {
|
||||||
const old_should_render = cls.prototype.shouldRenderSquadBanner;
|
const old_should_render = cls.prototype.shouldRenderSquadBanner;
|
||||||
|
|
||||||
cls.prototype.shouldRenderSquadBanner = function(...args) {
|
cls.prototype.shouldRenderSquadBanner = function(...args) {
|
||||||
|
@ -557,7 +557,7 @@ export default class Player extends Module {
|
||||||
|
|
||||||
this.SquadStreamBar.on('mount', this.updateSquadContext, this);
|
this.SquadStreamBar.on('mount', this.updateSquadContext, this);
|
||||||
this.SquadStreamBar.on('update', this.updateSquadContext, this);
|
this.SquadStreamBar.on('update', this.updateSquadContext, this);
|
||||||
this.SquadStreamBar.on('unmount', this.updateSquadContext, this);
|
this.SquadStreamBar.on('unmount', this.updateSquadContext, this);*/
|
||||||
|
|
||||||
|
|
||||||
this.Player.ready((cls, instances) => {
|
this.Player.ready((cls, instances) => {
|
||||||
|
@ -621,12 +621,18 @@ export default class Player extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.prototype.ffzUpdateVolume = function() {
|
cls.prototype.ffzUpdateVolume = function() {
|
||||||
|
if ( document.hidden )
|
||||||
|
return;
|
||||||
|
|
||||||
const player = this.props.mediaPlayerInstance,
|
const player = this.props.mediaPlayerInstance,
|
||||||
video = player?.mediaSinkManager?.video || player?.core?.mediaSinkManager?.video;
|
video = player?.mediaSinkManager?.video || player?.core?.mediaSinkManager?.video;
|
||||||
if ( video ) {
|
if ( video ) {
|
||||||
const volume = video.volume;
|
const volume = video.volume,
|
||||||
if ( ! player.isMuted() && ! video.muted && player.getVolume() !== volume )
|
muted = player.isMuted();
|
||||||
|
if ( ! video.muted && player.getVolume() !== volume ) {
|
||||||
player.setVolume(volume);
|
player.setVolume(volume);
|
||||||
|
player.setMuted(muted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -755,7 +761,9 @@ export default class Player extends Module {
|
||||||
if ( ! player?.isMuted )
|
if ( ! player?.isMuted )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
player.setMuted(! player.isMuted());
|
const muted = ! player.isMuted();
|
||||||
|
player.setMuted(muted);
|
||||||
|
localStorage.setItem('video-muted', JSON.stringify({default: muted}));
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1503,12 +1511,12 @@ export default class Player extends Module {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
repositionPlayer() {
|
repositionPlayer() {
|
||||||
for(const inst of this.SquadStreamBar.instances) {
|
/*for(const inst of this.SquadStreamBar.instances) {
|
||||||
if ( inst?.props?.triggerPlayerReposition ) {
|
if ( inst?.props?.triggerPlayerReposition ) {
|
||||||
inst.props.triggerPlayerReposition();
|
inst.props.triggerPlayerReposition();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSquadContext() {
|
updateSquadContext() {
|
||||||
|
@ -1518,8 +1526,9 @@ export default class Player extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasSquadBar() {
|
get hasSquadBar() {
|
||||||
const inst = this.SquadStreamBar.first;
|
return false;
|
||||||
return inst ? inst.shouldRenderSquadBanner(inst.props) : false
|
/*const inst = this.SquadStreamBar.first;
|
||||||
|
return inst ? inst.shouldRenderSquadBanner(inst.props) : false*/
|
||||||
}
|
}
|
||||||
|
|
||||||
get playerUI() {
|
get playerUI() {
|
||||||
|
|
|
@ -41,11 +41,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz-stat-text {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.ffz-stat--fix-padding {
|
.ffz-stat--fix-padding {
|
||||||
margin-top: -.7rem !important;
|
margin-top: -.7rem !important;
|
||||||
margin-bottom: -.7rem !important;
|
margin-bottom: -.7rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz--meta-tray {
|
||||||
|
& > :first-child {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :nth-child(0n+2) {
|
||||||
|
order: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ffz-stat {
|
.ffz-stat {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
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);
|
this.once('set', fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
each(fn) {
|
||||||
|
for(const inst of this.instances)
|
||||||
|
fn(inst);
|
||||||
|
}
|
||||||
|
|
||||||
_set(cls, instances) {
|
_set(cls, instances) {
|
||||||
if ( this._class )
|
if ( this._class )
|
||||||
throw new Error('already have a class');
|
throw new Error('already have a class');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue