1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 12:55:55 +00:00
* 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:
SirStendec 2023-08-19 18:08:44 -04:00
parent 7455ec6a2b
commit cef58241d4
10 changed files with 1337 additions and 1069 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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 )

View file

@ -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;

View file

@ -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,

View file

@ -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 [];