1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05: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",
"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",

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

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.',
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));

View file

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

View file

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