mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 12:55:55 +00:00
4.50.0
* Added: Setting to automatically uncheck the "Featured Clips Only" option when viewing a channel's clips. * Added: Chat actions can now use the `urlencode` formatter. Really this should have been available from the beginning, as it makes the Open URL action much more useful. * Fixed: An issue where certain emotes wouldn't appear in the list of available emotes during tab-completion. * Fixed: An error in the stream latency metadata handler when the video player cannot be accessed. * Fixed: An issue where a user's FFZ settings profiles may become corrupt and prevent the FFZ Control Center from functioning correctly. * Fixed: An issue where ephemeral settings profiles may not initialize correctly. * Fixed: The new Hype Chat up-sell at the top of chat not being hidden. Go away Hype Chat no one likes you. * Changed: Add support for an updated pubsub library. * API Added: Prefix modifier support, to make Lordmau5's life easier with the BTTV Emotes add-on. * API Changed: Attempting to alter settings profiles before the settings module has fully initialized will throw an exception. * Maintenance: Update the pnpm lockfile.
This commit is contained in:
parent
7455ec6a2b
commit
cef58241d4
10 changed files with 1337 additions and 1069 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.49.0",
|
||||
"version": "4.50.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
2097
pnpm-lock.yaml
generated
2097
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -35,7 +35,8 @@ const Flags = make_enum_flags(
|
|||
'Shake',
|
||||
'Cursed',
|
||||
'Jam',
|
||||
'Bounce'
|
||||
'Bounce',
|
||||
'NoSpace'
|
||||
);
|
||||
|
||||
export const MODIFIER_FLAGS = Flags;
|
||||
|
@ -1707,6 +1708,8 @@ export default class Emotes extends Module {
|
|||
animSrc2: emote.animSrc2,
|
||||
animSrcSet2: emote.animSrcSet2,
|
||||
masked: !! emote.mask,
|
||||
mod: emote.modifier,
|
||||
mod_prefix: emote.modifier_prefix,
|
||||
mod_hidden: (emote.modifier_flags & 1) === 1,
|
||||
text: emote.hidden ? '???' : emote.name,
|
||||
length: emote.name.length,
|
||||
|
|
|
@ -1716,7 +1716,12 @@ export const AddonEmotes = {
|
|||
anim = this.context.get('chat.emotes.animated'),
|
||||
out = [];
|
||||
|
||||
let had_prefix_mods = false;
|
||||
let had_no_space = false;
|
||||
let last_token, emote;
|
||||
|
||||
const NoSpace = this.emotes.ModifierFlags?.NoSpace;
|
||||
|
||||
for(const token of tokens) {
|
||||
if ( ! token )
|
||||
continue;
|
||||
|
@ -1741,10 +1746,15 @@ export const AddonEmotes = {
|
|||
emote = emotes[segment];
|
||||
|
||||
// Is this emote a modifier?
|
||||
if ( emote.modifier && last_token && last_token.modifiers && (!text.length || (text.length === 1 && text[0] === '')) ) {
|
||||
if ( emote.modifier && emote.modifier_prefix )
|
||||
had_prefix_mods = true;
|
||||
else if ( emote.modifier && last_token && last_token.modifiers && (!text.length || (text.length === 1 && text[0] === '')) ) {
|
||||
if ( last_token.modifiers.indexOf(emote.token) === -1 ) {
|
||||
if ( emote.modifier_flags )
|
||||
if ( emote.modifier_flags ) {
|
||||
last_token.modifier_flags |= emote.modifier_flags;
|
||||
if ( NoSpace && (emote.modifier_flags & NoSpace) === NoSpace )
|
||||
had_no_space = true;
|
||||
}
|
||||
|
||||
last_token.modifiers.push(
|
||||
Object.assign({
|
||||
|
@ -1790,6 +1800,66 @@ export const AddonEmotes = {
|
|||
}
|
||||
}
|
||||
|
||||
if ( had_prefix_mods ) {
|
||||
// We need to scan through and apply prefix modifiers as appropriate.
|
||||
let last_emote,
|
||||
had_text = false;
|
||||
|
||||
let i = out.length;
|
||||
while(i--) {
|
||||
const token = out[i];
|
||||
|
||||
// Is it a new emote?
|
||||
if ( token.type === 'emote' && ! token.mod ) {
|
||||
last_emote = token;
|
||||
had_text = false;
|
||||
}
|
||||
|
||||
// Is it a prefix mod with a target emote?
|
||||
else if ( last_emote && token.type === 'emote' && token.mod && token.mod_prefix ) {
|
||||
last_emote.modifiers.push(token);
|
||||
if ( token.source_modifier_flags ) {
|
||||
last_emote.modifier_flags |= token.source_modifier_flags;
|
||||
if ( NoSpace && (token.source_modifier_flags & NoSpace) === NoSpace )
|
||||
had_no_space = true;
|
||||
}
|
||||
|
||||
// Remove one or two tokens, depending on if we had a space.
|
||||
// (We should always have a space, but be flexible.)
|
||||
out.splice(i, had_text ? 2 : 1);
|
||||
had_text = false;
|
||||
}
|
||||
|
||||
// Make a note of at most one space.
|
||||
else if ( last_emote && ! had_text && token.type === 'text' && token.text === ' ' ) {
|
||||
had_text = true;
|
||||
}
|
||||
|
||||
// Absolutely anything else means it's a broken sequence.
|
||||
else {
|
||||
last_emote = null;
|
||||
had_text = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( had_no_space ) {
|
||||
// We need to remove prefix spaces before emotes with the no-space effect.
|
||||
let no_space = false;
|
||||
let i = out.length;
|
||||
while(i--) {
|
||||
const token = out[i];
|
||||
if ( token.type === 'emote' && (token.modifier_flags & NoSpace) === NoSpace )
|
||||
no_space = true;
|
||||
else {
|
||||
if ( no_space && token.type === 'text' && token.text === ' ' )
|
||||
out.splice(i, 1);
|
||||
|
||||
no_space = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -362,7 +362,7 @@ export default class Metadata extends Module {
|
|||
}
|
||||
|
||||
// Get the video element.
|
||||
const video = maybe_call(player.getHTMLVideoElement, player);
|
||||
const video = player && maybe_call(player.getHTMLVideoElement, player);
|
||||
stats.avOffset = 0;
|
||||
if ( video?._ffz_context )
|
||||
stats.avOffset = (video._ffz_context_offset ?? 0) + video._ffz_context.currentTime - video.currentTime;
|
||||
|
|
|
@ -184,7 +184,6 @@ export default class SettingsManager extends Module {
|
|||
}
|
||||
|
||||
|
||||
|
||||
addFilter(key, data) {
|
||||
if ( this.filters[key] )
|
||||
return this.log.warn('Tried to add already existing filter', key);
|
||||
|
@ -763,13 +762,22 @@ export default class SettingsManager extends Module {
|
|||
old_ids = new Set(old_profiles.map(x => x.id)),
|
||||
|
||||
new_ids = new Set,
|
||||
changed_ids = new Set,
|
||||
changed_ids = new Set;
|
||||
|
||||
raw_profiles = this.provider.get('profiles', [
|
||||
let raw_profiles = this.provider.get('profiles', [
|
||||
SettingsProfile.Moderation,
|
||||
SettingsProfile.Default
|
||||
]);
|
||||
|
||||
// Sanity check. If we have no profiles, delete the old data.
|
||||
if ( ! raw_profiles?.length ) {
|
||||
this.provider.delete('profiles');
|
||||
raw_profiles = [
|
||||
SettingsProfile.Moderation,
|
||||
SettingsProfile.Default
|
||||
];
|
||||
}
|
||||
|
||||
let reordered = false,
|
||||
changed = false;
|
||||
|
||||
|
@ -851,6 +859,9 @@ export default class SettingsManager extends Module {
|
|||
* @returns {SettingsProfile}
|
||||
*/
|
||||
createProfile(options) {
|
||||
if ( ! this.enabled )
|
||||
throw new Error('Unable to create profile before settings have initialized. Please await enable()');
|
||||
|
||||
let i = 0;
|
||||
while( this.__profile_ids[i] )
|
||||
i++;
|
||||
|
@ -878,6 +889,9 @@ export default class SettingsManager extends Module {
|
|||
* @param {number|SettingsProfile} id - The profile to delete
|
||||
*/
|
||||
deleteProfile(id) {
|
||||
if ( ! this.enabled )
|
||||
throw new Error('Unable to delete profile before settings have initialized. Please await enable()');
|
||||
|
||||
if ( typeof id === 'object' && id.id != null )
|
||||
id = id.id;
|
||||
|
||||
|
@ -905,6 +919,9 @@ export default class SettingsManager extends Module {
|
|||
|
||||
|
||||
moveProfile(id, index) {
|
||||
if ( ! this.enabled )
|
||||
throw new Error('Unable to move profiles before settings have initialized. Please await enable()');
|
||||
|
||||
if ( typeof id === 'object' && id.id )
|
||||
id = id.id;
|
||||
|
||||
|
@ -925,6 +942,9 @@ export default class SettingsManager extends Module {
|
|||
|
||||
|
||||
saveProfile(id) {
|
||||
if ( ! this.enabled )
|
||||
throw new Error('Unable to save profile before settings have initialized. Please await enable()');
|
||||
|
||||
if ( typeof id === 'object' && id.id )
|
||||
id = id.id;
|
||||
|
||||
|
|
|
@ -63,6 +63,10 @@ export default class SettingsProfile extends EventEmitter {
|
|||
|
||||
this.matcher = null;
|
||||
|
||||
// Make sure ephemeral is set first.
|
||||
if ( val.ephemeral )
|
||||
this.ephemeral = true;
|
||||
|
||||
for(const key in val)
|
||||
if ( has(val, key) )
|
||||
this[key] = val[key];
|
||||
|
@ -203,7 +207,7 @@ export default class SettingsProfile extends EventEmitter {
|
|||
}
|
||||
|
||||
set toggled(val) {
|
||||
if ( val === this.toggleState )
|
||||
if ( val === this.toggled )
|
||||
return;
|
||||
|
||||
if ( this.ephemeral )
|
||||
|
|
|
@ -31,6 +31,14 @@ export default class Channel extends Module {
|
|||
this.inject('metadata');
|
||||
this.inject('socket');
|
||||
|
||||
this.settings.add('channel.auto-click-off-featured', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Channel > Behavior >> General',
|
||||
title: 'Automatically un-check "Featured Clips Only" when viewing a channel\'s clips.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('channel.panel-tips', {
|
||||
default: false,
|
||||
|
@ -212,7 +220,26 @@ export default class Channel extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
checkFeaturedClips() {
|
||||
if ( this.router.current_name !== 'user-clips' && this.router.current_name !== 'user-videos' )
|
||||
return;
|
||||
|
||||
if ( this._featured_waiting || ! this.settings.get('channel.auto-click-off-featured') )
|
||||
return;
|
||||
|
||||
this._featured_waiting = this.parent.awaitElement('input#featured-clips-toggle').then(el => {
|
||||
if ( el.checked )
|
||||
el.click();
|
||||
|
||||
this._featured_waiting = false;
|
||||
}).catch(() => {
|
||||
this._featured_waiting = false;
|
||||
});
|
||||
}
|
||||
|
||||
checkNavigation() {
|
||||
this.checkFeaturedClips();
|
||||
|
||||
if ( ! this.settings.get('channel.auto-click-chat') || this.router.current_name !== 'user-home' )
|
||||
return;
|
||||
|
||||
|
|
|
@ -708,12 +708,17 @@ export default class EmoteMenu extends Module {
|
|||
return;
|
||||
|
||||
// Check for magic.
|
||||
let prefix = '';
|
||||
const effects = event.currentTarget.dataset.effects;
|
||||
if ( effects?.length > 0 && effects != '0' && t.emotes.target_emote )
|
||||
prefix = `${t.emotes.target_emote.name} `;
|
||||
let prefix = '', postfix = '';
|
||||
const effects = event.currentTarget.dataset.effects,
|
||||
is_prefix = event.currentTarget.dataset.effectPrefix === 'true';
|
||||
if ( effects?.length > 0 && effects != '0' && t.emotes.target_emote ) {
|
||||
if ( is_prefix )
|
||||
postfix = ` ${t.emotes.target_emote.name}`;
|
||||
else
|
||||
prefix = `${t.emotes.target_emote.name} `;
|
||||
}
|
||||
|
||||
this.props.onClickToken(`${prefix}${event.currentTarget.dataset.name}`);
|
||||
this.props.onClickToken(`${prefix}${event.currentTarget.dataset.name}${postfix}`);
|
||||
}
|
||||
|
||||
keyHeading(event) {
|
||||
|
@ -925,6 +930,7 @@ export default class EmoteMenu extends Module {
|
|||
data-code={emote.code}
|
||||
data-modifiers={modifiers}
|
||||
data-effects={emote.effects}
|
||||
data-effect-prefix={emote.effect_prefix}
|
||||
data-variant={emote.variant}
|
||||
data-no-source={source}
|
||||
data-name={emote.name}
|
||||
|
@ -2519,6 +2525,7 @@ export default class EmoteMenu extends Module {
|
|||
animSrc: emote.animSrc,
|
||||
animSrcSet: emote.animSrcSet,
|
||||
effects: emote.modifier ? emote.modifier_flags : 0,
|
||||
effect_prefix: emote.modifier ? emote.modifier_prefix : false,
|
||||
name: emote.name,
|
||||
favorite: is_fav,
|
||||
locked: locked,
|
||||
|
|
|
@ -45,8 +45,10 @@ export default class Subpump extends Module {
|
|||
}
|
||||
|
||||
onEnable(tries = 0) {
|
||||
const instances = window.__Twitch__pubsubInstances;
|
||||
if ( ! instances ) {
|
||||
const instance = window.__twitch_pubsub_client,
|
||||
instances = window.__Twitch__pubsubInstances;
|
||||
|
||||
if ( ! instance && ! instances ) {
|
||||
if ( tries > 10 )
|
||||
this.log.warn('Unable to find PubSub.');
|
||||
else
|
||||
|
@ -55,52 +57,113 @@ export default class Subpump extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
for(const val of Object.values(instances))
|
||||
if ( val?._client ) {
|
||||
if ( this.instance ) {
|
||||
this.log.warn('Multiple PubSub instances detected. Things might act weird.');
|
||||
continue;
|
||||
}
|
||||
if ( instance ) {
|
||||
this.instance = instance;
|
||||
this.hookClient(instance);
|
||||
}
|
||||
|
||||
this.instance = val;
|
||||
this.hookClient(val._client);
|
||||
}
|
||||
else if ( instances ) {
|
||||
for(const val of Object.values(instances))
|
||||
if ( val?._client ) {
|
||||
if ( this.instance ) {
|
||||
this.log.warn('Multiple PubSub instances detected. Things might act weird.');
|
||||
continue;
|
||||
}
|
||||
|
||||
this.instance = val;
|
||||
this.hookOldClient(val._client);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! this.instance )
|
||||
this.log.warn('Unable to find a PubSub instance.');
|
||||
}
|
||||
|
||||
handleMessage(msg) {
|
||||
try {
|
||||
if ( msg.type === 'MESSAGE' && msg.data?.topic ) {
|
||||
const raw_topic = msg.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: msg.data
|
||||
});
|
||||
|
||||
this.emit(':pubsub-message', event);
|
||||
if ( event.defaultPrevented )
|
||||
return true;
|
||||
|
||||
if ( event._changed )
|
||||
msg.data.message = JSON.stringify(event._obj);
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
this.log.error('Error processing PubSub event.', err);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hookClient(client) {
|
||||
const t = this,
|
||||
orig_message = client.onMessage;
|
||||
|
||||
this.is_old = false;
|
||||
|
||||
client.connection.removeAllListeners('message');
|
||||
|
||||
client.onMessage = function(e) {
|
||||
if ( t.handleMessage(e) )
|
||||
return;
|
||||
|
||||
return orig_message.call(this, e);
|
||||
}
|
||||
|
||||
client.connection.addListener('message', client.onMessage);
|
||||
|
||||
const orig_on = client.listen,
|
||||
orig_off = client.unlisten;
|
||||
|
||||
client.ffz_original_listen = orig_on;
|
||||
client.ffz_original_unlisten = orig_off;
|
||||
|
||||
client.listen = function(opts, fn, ...args) {
|
||||
const topic = opts.topic,
|
||||
has_topic = topic && !! client.topicListeners?._events?.[topic],
|
||||
out = orig_on.call(this, opts, fn, ...args);
|
||||
|
||||
if ( topic && ! has_topic )
|
||||
t.emit(':add-topic', topic);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
client.unlisten = function(topic, fn, ...args) {
|
||||
const has_topic = !! client.topicListeners?._events?.[topic],
|
||||
out = orig_off.call(this, topic, fn, ...args);
|
||||
|
||||
if ( has_topic && ! client.topicListeners?._events?.[topic] )
|
||||
t.emit(':remove-topic', topic);
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
hookOldClient(client) {
|
||||
const t = this,
|
||||
orig_message = client._onMessage;
|
||||
|
||||
this.is_old = true;
|
||||
|
||||
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) {
|
||||
t.log.error('Error processing PubSub event.', err);
|
||||
}
|
||||
if ( t.handleMessage(e) )
|
||||
return;
|
||||
|
||||
return orig_message.call(this, e);
|
||||
};
|
||||
|
@ -133,15 +196,24 @@ export default class Subpump extends Module {
|
|||
}
|
||||
|
||||
inject(topic, message) {
|
||||
const listens = this.instance?._client?._listens;
|
||||
if ( ! listens )
|
||||
if ( ! this.instance )
|
||||
throw new Error('No PubSub instance available');
|
||||
|
||||
listens._trigger(topic, JSON.stringify(message));
|
||||
if ( this.is_old ) {
|
||||
const listens = this.instance._client?._listens;
|
||||
listens._trigger(topic, JSON.stringify(message));
|
||||
} else {
|
||||
this.instance.simulateMessage(topic, JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
get topics() {
|
||||
const events = this.instance?._client?._listens._events;
|
||||
let events;
|
||||
if ( this.is_old )
|
||||
events = this.instance?._client?._listens._events;
|
||||
else
|
||||
events = this.instance?.topicListeners?._events;
|
||||
|
||||
if ( ! events )
|
||||
return [];
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue