1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-10 00:00:53 +00:00
* Fixed: Appearance of the page when viewing a Watch Party.
* Fixed: During the initial load, some CSS blocks could be incorrectly injected into the page due to a race condition.
* Fixed: The sample embed in Chat > Appearance >> Rich Content not appearing correctly.
* API Added: New event class `FFZWaitableEvent`, a subclass of `FFZEvent` providing a framework for asynchronous event handlers.
* API Added: `site.channel:update-bar` event, fired whenever the channel info bar is updated.
* API Fixed: `chat.removeTokenizer()`, `chat.removeLinkProvider()`, and `chat.removeRichProvider()` failing to fully remove their respective items.
* API Removed: The `emitAsync` method has been removed from modules. Nothing was using it, and it was problematic due to the concurrent access protection on events. Instead, `FFZWaitableEvent` should be used if asynchronous waiting is necessary.
This commit is contained in:
SirStendec 2023-11-05 14:49:39 -05:00
parent 675512e811
commit a7e131070e
13 changed files with 156 additions and 112 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.58.0", "version": "4.59.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",

View file

@ -9,7 +9,7 @@ let tokenizer;
export default { export default {
props: ['data', 'url', 'events', 'forceFull', 'forceUnsafe', 'forceMedia', 'forceMid', 'noLink', 'noTooltip', 'noElevation', 'noUnsafe'], props: ['data', 'url', 'events', 'forceFull', 'forceUnsafe', 'forceMedia', 'forceShort', 'forceMid', 'noLink', 'noTooltip', 'noElevation', 'noUnsafe'],
data() { data() {
return { return {
@ -256,9 +256,13 @@ export default {
renderBody(h) { renderBody(h) {
let body; let body;
if ( this.forceFull === true || (this.forceFull !== false && this.full) ) if ( this.forceShort )
body = this.short;
else if ( this.forceMid )
body = this.mid;
else if ( this.forceFull || (this.forceFull !== false && this.full) )
body = this.full; body = this.full;
else if ( this.forceMid === true || (this.forceMid !== false && this.mid) ) else if ( this.forceMid || (this.forceMid !== false && this.mid) )
body = this.mid; body = this.mid;
else else
body = this.short; body = this.short;

View file

@ -2316,6 +2316,8 @@ export default class Chat extends Module {
if ( ! tokenizer ) if ( ! tokenizer )
return null; return null;
delete this.tokenizers[type];
if ( tokenizer.tooltip ) if ( tokenizer.tooltip )
delete this.tooltips.types[type]; delete this.tooltips.types[type];
@ -2354,6 +2356,8 @@ export default class Chat extends Module {
if ( ! provider ) if ( ! provider )
return null; return null;
delete this.link_providers[type];
const idx = this.__link_providers.indexOf(provider); const idx = this.__link_providers.indexOf(provider);
if ( idx !== -1 ) if ( idx !== -1 )
this.__link_providers.splice(idx, 1); this.__link_providers.splice(idx, 1);
@ -2389,6 +2393,8 @@ export default class Chat extends Module {
if ( ! provider ) if ( ! provider )
return null; return null;
delete this.rich_providers[type];
const idx = this.__rich_providers.indexOf(provider); const idx = this.__rich_providers.indexOf(provider);
if ( idx !== -1 ) if ( idx !== -1 )
this.__rich_providers.splice(idx, 1); this.__rich_providers.splice(idx, 1);

View file

@ -6,16 +6,19 @@
<chat-rich <chat-rich
:data="data" :data="data"
:url="url" :url="url"
:force-short="true"
/> />
</div> </div>
</template> </template>
<script> <script>
import { maybe_call } from 'utilities/object';
const VIDEOS = [ const VIDEOS = [
'https://www.twitch.tv/dansalvato', 'https://www.twitch.tv/dansalvato',
'https://www.twitch.tv/sirstendec', 'https://www.twitch.tv/sirstendec',
//'https://www.youtube.com/watch?v=BFSWlDpA6C4' 'https://www.youtube.com/watch?v=BFSWlDpA6C4'
]; ];
export default { export default {
@ -29,14 +32,18 @@ export default {
props: ['context', 'item'], props: ['context', 'item'],
data() { data() {
const url = VIDEOS[Math.floor(Math.random() * VIDEOS.length)], let url = maybe_call(this.item.extra.url, this, this.item, this.context);
token = { if ( ! url )
url = VIDEOS[Math.floor(Math.random() * VIDEOS.length)];
const token = {
type: 'link', type: 'link',
force_rich: true, force_rich: true,
is_mail: false, is_mail: false,
url, url,
text: url text: url
}, },
chat = this.item.extra.getChat(); chat = this.item.extra.getChat();
let data = null; let data = null;

View file

@ -483,6 +483,8 @@ export default class Channel extends Module {
this.updateSubscription(props.channelID, props.channelLogin); this.updateSubscription(props.channelID, props.channelLogin);
this.updateMetadata(el); this.updateMetadata(el);
this.emit(':update-bar', el, props, channel);
} }
removeBar(el) { removeBar(el) {

View file

@ -909,6 +909,7 @@ export default class ChatHook extends Module {
this.css_tweaks.toggle('chat-font', size !== 13 || font !== 'inherit'); this.css_tweaks.toggle('chat-font', size !== 13 || font !== 'inherit');
this.css_tweaks.toggle('chat-width', this.settings.get('chat.use-width')); this.css_tweaks.toggle('chat-width', this.settings.get('chat.use-width'));
this.css_tweaks.toggle('chat-fix--watch-party', this.settings.get('context.isWatchParty'));
this.css_tweaks.toggle('emote-alignment-padded', emote_alignment === 1); this.css_tweaks.toggle('emote-alignment-padded', emote_alignment === 1);
this.css_tweaks.toggle('emote-alignment-baseline', emote_alignment === 2); this.css_tweaks.toggle('emote-alignment-baseline', emote_alignment === 2);

View file

@ -72,6 +72,8 @@ export default class CSSTweaks extends Module {
this.chunks = {}; this.chunks = {};
this.chunks_loaded = false; this.chunks_loaded = false;
this._state = {};
// Layout // Layout
this.settings.add('metadata.modview.hide-info', { this.settings.add('metadata.modview.hide-info', {
@ -574,17 +576,33 @@ export default class CSSTweaks extends Module {
} }
async toggle(key, val) { toggle(key, val) {
val = !! val;
if ( (this._state[key] ?? false) === val )
return;
this._state[key] = val;
this._apply(key);
}
_apply(key) {
const val = this._state[key];
if ( ! val ) { if ( ! val ) {
if ( this.style )
this.style.delete(key); this.style.delete(key);
return; return;
} }
if ( ! this.chunks_loaded ) if ( this.style.has(key) )
await this.populate(); return;
if ( ! has(this.chunks, key) ) if ( ! this.chunks_loaded )
throw new Error(`cannot find chunk "${key}"`); return this.populate().then(() => this._apply(key));
if ( ! has(this.chunks, key) ) {
this.log.warn(`Unknown chunk name "${key}" for toggle()`);
return;
}
this.style.set(key, this.chunks[key]); this.style.set(key, this.chunks[key]);
} }

View file

@ -0,0 +1,19 @@
.channel-root__right-column--host-player-above-chat {
transition: none !important;
transform: none !important;
position: initial !important;
}
.toggle-visibility__right-column--expanded {
transform: none !important;
}
body .channel-root--hold-chat + .persistent-player,
body .channel-root--watch-chat + .persistent-player {
width: 100%;
}
.channel-root__info--with-chat .channel-info-content,
.channel-root__player--with-chat {
width: 100% !important;
}

View file

@ -20,6 +20,9 @@ body .channel-root__right-column*/ {
transform: none !important; transform: none !important;
} }
.channel-root--hold-chat+.persistent-player, .channel-root--watch-chat+.persistent-player, .channel-root__info--with-chat .channel-info-content, .channel-root__player--with-chat { .channel-root--hold-chat+.persistent-player,
.channel-root--watch-chat+.persistent-player,
.channel-root__info--with-chat .channel-info-content,
.channel-root__player--with-chat {
width: 100% !important; width: 100% !important;
} }

View file

@ -14,7 +14,7 @@ body .whispers--theatre-mode.whispers--right-column-expanded-beside {
right: var(--ffz-chat-width); right: var(--ffz-chat-width);
} }
body .persistent-player--theatre:not([style*="width: 100%"]), body .persistent-player--theatre:not([style*="width: 100%"]):not([style*="width: 100vw"]),
body .channel-page__video-player--theatre-mode { body .channel-page__video-player--theatre-mode {
width: calc(100% - var(--ffz-chat-width)) !important; width: calc(100% - var(--ffz-chat-width)) !important;
} }

View file

@ -111,7 +111,7 @@ export default class Layout extends Module {
description: 'When enabled, this minimizes the chat header and places the chat input box in line with the chat buttons in order to present a more compact chat able to display more lines with limited vertical space.', description: 'When enabled, this minimizes the chat header and places the chat input box in line with the chat buttons in order to present a more compact chat able to display more lines with limited vertical space.',
component: 'setting-check-box' component: 'setting-check-box'
}, },
changed: val => this.css_tweaks.toggle('portrait-chat', val) //changed: val => this.css_tweaks.toggle('portrait-chat', val)
}) })
this.settings.add('layout.use-portrait', { this.settings.add('layout.use-portrait', {
@ -135,7 +135,7 @@ export default class Layout extends Module {
process(ctx) { process(ctx) {
return ctx.get('layout.use-portrait') && ctx.get('context.ui.rightColumnExpanded'); return ctx.get('layout.use-portrait') && ctx.get('context.ui.rightColumnExpanded');
}, },
changed: val => this.css_tweaks.toggle('portrait', val) //changed: val => this.css_tweaks.toggle('portrait', val)
}); });
this.settings.add('layout.use-portrait-swapped', { this.settings.add('layout.use-portrait-swapped', {
@ -143,7 +143,7 @@ export default class Layout extends Module {
process(ctx) { process(ctx) {
return ctx.get('layout.inject-portrait') && ctx.get('layout.swap-sidebars') return ctx.get('layout.inject-portrait') && ctx.get('layout.swap-sidebars')
}, },
changed: val => this.css_tweaks.toggle('portrait-swapped', val) //changed: val => this.css_tweaks.toggle('portrait-swapped', val)
}); });
this.settings.add('layout.use-portrait-meta', { this.settings.add('layout.use-portrait-meta', {
@ -151,7 +151,7 @@ export default class Layout extends Module {
process(ctx) { process(ctx) {
return ctx.get('layout.inject-portrait') && ctx.get('player.theatre.metadata') return ctx.get('layout.inject-portrait') && ctx.get('player.theatre.metadata')
}, },
changed: val => this.css_tweaks.toggle('portrait-metadata', val) //changed: val => this.css_tweaks.toggle('portrait-metadata', val)
}); });
this.settings.add('layout.use-portrait-meta-top', { this.settings.add('layout.use-portrait-meta-top', {
@ -159,7 +159,7 @@ export default class Layout extends Module {
process(ctx) { process(ctx) {
return ctx.get('layout.use-portrait-meta') && ! ctx.get('layout.portrait-invert') return ctx.get('layout.use-portrait-meta') && ! ctx.get('layout.portrait-invert')
}, },
changed: val => this.css_tweaks.toggle('portrait-metadata-top', val) //changed: val => this.css_tweaks.toggle('portrait-metadata-top', val)
}); });
this.settings.add('layout.is-theater-mode', { this.settings.add('layout.is-theater-mode', {
@ -208,7 +208,7 @@ export default class Layout extends Module {
return height; return height;
}, },
changed: val => this.css_tweaks.setVariable('portrait-extra-height', `${val}rem`) //changed: val => this.css_tweaks.setVariable('portrait-extra-height', `${val}rem`)
}) })
this.settings.add('layout.portrait-extra-width', { this.settings.add('layout.portrait-extra-width', {
@ -220,7 +220,7 @@ export default class Layout extends Module {
return ctx.get('context.ui.sideNavExpanded') ? 24 : 5 return ctx.get('context.ui.sideNavExpanded') ? 24 : 5
}, },
changed: val => this.css_tweaks.setVariable('portrait-extra-width', `${val}rem`) //changed: val => this.css_tweaks.setVariable('portrait-extra-width', `${val}rem`)
}); });
this.settings.add('layout.is-minimal', { this.settings.add('layout.is-minimal', {
@ -237,13 +237,22 @@ export default class Layout extends Module {
this.on(':update-nav', this.updateNavLinks, this); this.on(':update-nav', this.updateNavLinks, this);
this.on(':resize', this.handleResize, this); this.on(':resize', this.handleResize, this);
this.css_tweaks.toggle('portrait-chat', this.settings.get('layout.portrait-min-chat')); this.settings.getChanges('layout.portrait-min-chat', val => this.css_tweaks.toggle('portrait-chat', val));
this.settings.getChanges('layout.inject-portrait', val => this.css_tweaks.toggle('portrait', val));
this.settings.getChanges('layout.use-portrait-swapped', val => this.css_tweaks.toggle('portrait-swapped', val));
this.settings.getChanges('layout.use-portrait-meta', val => this.css_tweaks.toggle('portrait-metadata', val));
this.settings.getChanges('layout.use-portrait-meta-top', val => this.css_tweaks.toggle('portrait-metadata-top', val));
this.settings.getChanges('layout.portrait-extra-width', val => this.css_tweaks.setVariable('portrait-extra-width', `${val}rem`));
this.settings.getChanges('layout.portrait-extra-height', val => this.css_tweaks.setVariable('portrait-extra-height', `${val}rem`));
/*this.css_tweaks.toggle('portrait-chat', this.settings.get('layout.portrait-min-chat'));
this.css_tweaks.toggle('portrait', this.settings.get('layout.inject-portrait')); this.css_tweaks.toggle('portrait', this.settings.get('layout.inject-portrait'));
this.css_tweaks.toggle('portrait-swapped', this.settings.get('layout.use-portrait-swapped')); this.css_tweaks.toggle('portrait-swapped', this.settings.get('layout.use-portrait-swapped'));
this.css_tweaks.toggle('portrait-metadata', this.settings.get('layout.use-portrait-meta')); this.css_tweaks.toggle('portrait-metadata', this.settings.get('layout.use-portrait-meta'));
this.css_tweaks.toggle('portrait-metadata-top', this.settings.get('layout.use-portrait-meta-top')); this.css_tweaks.toggle('portrait-metadata-top', this.settings.get('layout.use-portrait-meta-top'));
this.css_tweaks.setVariable('portrait-extra-width', `${this.settings.get('layout.portrait-extra-width')}rem`); this.css_tweaks.setVariable('portrait-extra-width', `${this.settings.get('layout.portrait-extra-width')}rem`);
this.css_tweaks.setVariable('portrait-extra-height', `${this.settings.get('layout.portrait-extra-height')}rem`); this.css_tweaks.setVariable('portrait-extra-height', `${this.settings.get('layout.portrait-extra-height')}rem`);*/
this.on('site.directory:update-cards', () => { this.on('site.directory:update-cards', () => {
this.SideBar.each(el => this._updateSidebar(el)); this.SideBar.each(el => this._updateSidebar(el));

View file

@ -19,6 +19,8 @@ export default class CSSTweaks extends Module {
this.chunks = {}; this.chunks = {};
this.chunks_loaded = false; this.chunks_loaded = false;
this._state = {};
this.populate = once(this.populate); this.populate = once(this.populate);
} }
@ -43,18 +45,32 @@ export default class CSSTweaks extends Module {
this.style.set(k, `${this.rules[key]}{display:none !important}`); this.style.set(k, `${this.rules[key]}{display:none !important}`);
} }
async toggle(key, val) { toggle(key, val) {
if ( this._state[key] == val )
return;
this._state[key] = val;
this._apply(key);
}
_apply(key) {
const val = this._state[key];
if ( ! val ) { if ( ! val ) {
if ( this._style ) if ( this._style )
this._style.delete(key); this._style.delete(key);
return; return;
} }
if ( ! this.chunks_loaded ) if ( this.style.has(key) )
await this.populate(); return;
if ( ! has(this.chunks, key) ) if ( ! this.chunks_loaded )
throw new Error(`unknown chunk "${key}" for toggle`); return this.populate().then(() => this._apply(key));
if ( ! has(this.chunks, key) ) {
this.log.warn(`Unknown chunk name "${key}" for toggle()`);
return;
}
this.style.set(key, this.chunks[key]); this.style.set(key, this.chunks[key]);
} }

View file

@ -276,6 +276,15 @@ export class EventEmitter {
item[2] = ttl - 1; item[2] = ttl - 1;
} }
// Automatically wait for a promise, if the return value is a promise
// and we're dealing with a waitable event.
if ( ret instanceof Promise ) {
if ( (args[0] instanceof FFZWaitableEvent) )
args[0].waitFor(ret);
else if ( this.log )
this.log.error(`handler for event "${event}" returned a Promise but the event is not an FFZWaitableEvent`);
}
if ( (args[0] instanceof FFZEvent && args[0].propagationStopped) || ret === StopPropagation ) if ( (args[0] instanceof FFZEvent && args[0].propagationStopped) || ret === StopPropagation )
break; break;
} }
@ -306,88 +315,6 @@ export class EventEmitter {
this.__running.delete(event); this.__running.delete(event);
} }
async emitAsync(event, ...args) {
let list = this.__listeners[event];
if ( ! list )
return [];
if ( this.__running.has(event) )
throw new Error(`concurrent access: tried to emit event while event is running`);
// Track removals separately to make iteration over the event list
// much, much simpler.
const removed = new Set,
promises = [];
// Set the current list of listeners to null because we don't want
// to enter some kind of loop if a new listener is added as the result
// of an existing listener.
this.__listeners[event] = null;
this.__running.add(event);
for(const item of list) {
const [fn, ctx] = item;
let ret;
try {
ret = fn.apply(ctx, args);
} catch(err) {
if ( this.log )
this.log.capture(err, {tags: {event}, extra: {args}});
}
if ( !(ret instanceof Promise) )
ret = Promise.resolve(ret);
promises.push(ret.then(r => {
const new_ttl = item[2];
if ( r === Detach )
removed.add(item);
else if ( new_ttl !== false ) {
if ( new_ttl <= 1 )
removed.add(item);
else
item[2] = new_ttl - 1;
}
if ( ret !== Detach )
return ret;
}).catch(err => {
if ( this.log )
this.log.capture(err, {event, args});
return null;
}));
}
const out = await Promise.all(promises);
// Remove any dead listeners from the list.
if ( removed.size ) {
for(const item of removed) {
const idx = list.indexOf(item);
if ( idx !== -1 )
list.splice(idx, 1);
}
}
// Were more listeners added while we were running? Just combine
// the two lists if so.
if ( this.__listeners[event] )
list = list.concat(this.__listeners[event]);
// If we have items, store the list back. Otherwise, mark that we
// have a dead listener.
if ( list.length )
this.__listeners[event] = list;
else {
this.__listeners[event] = null;
this.__dead_events++;
}
this.__running.delete(event);
return out;
}
} }
EventEmitter.Detach = Detach; EventEmitter.Detach = Detach;
@ -417,6 +344,39 @@ export class FFZEvent {
} }
export class FFZWaitableEvent extends FFZEvent {
_wait() {
if ( this.__waiter )
return this.__waiter;
if ( ! this.__promises )
return;
const promises = this.__promises;
this.__promises = null;
return this.__waiter = Promise.all(promises).finally(() => {
this.__waiter = null;
return this._wait();
});
}
_reset() {
super._reset();
this.__waiter = null;
this.__promises = null;
}
waitFor(promise) {
if ( ! this.__promises )
this.__promises = [promise];
else
this.__promises.push(promise);
}
}
export class HierarchicalEventEmitter extends EventEmitter { export class HierarchicalEventEmitter extends EventEmitter {
constructor(name, parent) { constructor(name, parent) {
@ -507,7 +467,6 @@ export class HierarchicalEventEmitter extends EventEmitter {
emit(event, ...args) { return super.emit(this.abs_path(event), ...args) } emit(event, ...args) { return super.emit(this.abs_path(event), ...args) }
emitUnsafe(event, ...args) { return super.emitUnsafe(this.abs_path(event), ...args) } emitUnsafe(event, ...args) { return super.emitUnsafe(this.abs_path(event), ...args) }
emitAsync(event, ...args) { return super.emitAsync(this.abs_path(event), ...args) }
events(include_children) { events(include_children) {
this.__cleanListeners(); this.__cleanListeners();