mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.59.0
* 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:
parent
675512e811
commit
a7e131070e
13 changed files with 156 additions and 112 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.58.0",
|
||||
"version": "4.59.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -9,7 +9,7 @@ let tokenizer;
|
|||
|
||||
|
||||
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() {
|
||||
return {
|
||||
|
@ -256,9 +256,13 @@ export default {
|
|||
|
||||
renderBody(h) {
|
||||
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;
|
||||
else if ( this.forceMid === true || (this.forceMid !== false && this.mid) )
|
||||
else if ( this.forceMid || (this.forceMid !== false && this.mid) )
|
||||
body = this.mid;
|
||||
else
|
||||
body = this.short;
|
||||
|
|
|
@ -2316,6 +2316,8 @@ export default class Chat extends Module {
|
|||
if ( ! tokenizer )
|
||||
return null;
|
||||
|
||||
delete this.tokenizers[type];
|
||||
|
||||
if ( tokenizer.tooltip )
|
||||
delete this.tooltips.types[type];
|
||||
|
||||
|
@ -2354,6 +2356,8 @@ export default class Chat extends Module {
|
|||
if ( ! provider )
|
||||
return null;
|
||||
|
||||
delete this.link_providers[type];
|
||||
|
||||
const idx = this.__link_providers.indexOf(provider);
|
||||
if ( idx !== -1 )
|
||||
this.__link_providers.splice(idx, 1);
|
||||
|
@ -2389,6 +2393,8 @@ export default class Chat extends Module {
|
|||
if ( ! provider )
|
||||
return null;
|
||||
|
||||
delete this.rich_providers[type];
|
||||
|
||||
const idx = this.__rich_providers.indexOf(provider);
|
||||
if ( idx !== -1 )
|
||||
this.__rich_providers.splice(idx, 1);
|
||||
|
|
|
@ -6,16 +6,19 @@
|
|||
<chat-rich
|
||||
:data="data"
|
||||
:url="url"
|
||||
:force-short="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { maybe_call } from 'utilities/object';
|
||||
|
||||
const VIDEOS = [
|
||||
'https://www.twitch.tv/dansalvato',
|
||||
'https://www.twitch.tv/sirstendec',
|
||||
//'https://www.youtube.com/watch?v=BFSWlDpA6C4'
|
||||
'https://www.youtube.com/watch?v=BFSWlDpA6C4'
|
||||
];
|
||||
|
||||
export default {
|
||||
|
@ -29,14 +32,18 @@ export default {
|
|||
props: ['context', 'item'],
|
||||
|
||||
data() {
|
||||
const url = VIDEOS[Math.floor(Math.random() * VIDEOS.length)],
|
||||
token = {
|
||||
let url = maybe_call(this.item.extra.url, this, this.item, this.context);
|
||||
if ( ! url )
|
||||
url = VIDEOS[Math.floor(Math.random() * VIDEOS.length)];
|
||||
|
||||
const token = {
|
||||
type: 'link',
|
||||
force_rich: true,
|
||||
is_mail: false,
|
||||
url,
|
||||
text: url
|
||||
},
|
||||
|
||||
chat = this.item.extra.getChat();
|
||||
|
||||
let data = null;
|
||||
|
|
|
@ -483,6 +483,8 @@ export default class Channel extends Module {
|
|||
|
||||
this.updateSubscription(props.channelID, props.channelLogin);
|
||||
this.updateMetadata(el);
|
||||
|
||||
this.emit(':update-bar', el, props, channel);
|
||||
}
|
||||
|
||||
removeBar(el) {
|
||||
|
|
|
@ -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-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-baseline', emote_alignment === 2);
|
||||
|
|
|
@ -72,6 +72,8 @@ export default class CSSTweaks extends Module {
|
|||
this.chunks = {};
|
||||
this.chunks_loaded = false;
|
||||
|
||||
this._state = {};
|
||||
|
||||
// Layout
|
||||
|
||||
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 ) {
|
||||
this.style.delete(key);
|
||||
if ( this.style )
|
||||
this.style.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! this.chunks_loaded )
|
||||
await this.populate();
|
||||
if ( this.style.has(key) )
|
||||
return;
|
||||
|
||||
if ( ! has(this.chunks, key) )
|
||||
throw new Error(`cannot find chunk "${key}"`);
|
||||
if ( ! this.chunks_loaded )
|
||||
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]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -20,6 +20,9 @@ body .channel-root__right-column*/ {
|
|||
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;
|
||||
}
|
|
@ -14,7 +14,7 @@ body .whispers--theatre-mode.whispers--right-column-expanded-beside {
|
|||
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 {
|
||||
width: calc(100% - var(--ffz-chat-width)) !important;
|
||||
}
|
||||
|
|
|
@ -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.',
|
||||
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', {
|
||||
|
@ -135,7 +135,7 @@ export default class Layout extends Module {
|
|||
process(ctx) {
|
||||
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', {
|
||||
|
@ -143,7 +143,7 @@ export default class Layout extends Module {
|
|||
process(ctx) {
|
||||
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', {
|
||||
|
@ -151,7 +151,7 @@ export default class Layout extends Module {
|
|||
process(ctx) {
|
||||
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', {
|
||||
|
@ -159,7 +159,7 @@ export default class Layout extends Module {
|
|||
process(ctx) {
|
||||
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', {
|
||||
|
@ -208,7 +208,7 @@ export default class Layout extends Module {
|
|||
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', {
|
||||
|
@ -220,7 +220,7 @@ export default class Layout extends Module {
|
|||
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', {
|
||||
|
@ -237,13 +237,22 @@ export default class Layout extends Module {
|
|||
this.on(':update-nav', this.updateNavLinks, 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-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-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-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.SideBar.each(el => this._updateSidebar(el));
|
||||
|
|
|
@ -19,6 +19,8 @@ export default class CSSTweaks extends Module {
|
|||
this.chunks = {};
|
||||
this.chunks_loaded = false;
|
||||
|
||||
this._state = {};
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
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 ( this._style )
|
||||
this._style.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! this.chunks_loaded )
|
||||
await this.populate();
|
||||
if ( this.style.has(key) )
|
||||
return;
|
||||
|
||||
if ( ! has(this.chunks, key) )
|
||||
throw new Error(`unknown chunk "${key}" for toggle`);
|
||||
if ( ! this.chunks_loaded )
|
||||
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]);
|
||||
}
|
||||
|
|
|
@ -276,6 +276,15 @@ export class EventEmitter {
|
|||
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 )
|
||||
break;
|
||||
}
|
||||
|
@ -306,88 +315,6 @@ export class EventEmitter {
|
|||
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;
|
||||
|
@ -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 {
|
||||
constructor(name, parent) {
|
||||
|
@ -507,7 +467,6 @@ export class HierarchicalEventEmitter extends EventEmitter {
|
|||
|
||||
emit(event, ...args) { return super.emit(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) {
|
||||
this.__cleanListeners();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue