1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-28 13:38:30 +00:00
* Changed: Shift-Click the FFZ Control Center icon in the top navigation to open the Control Center in a new window.
* Fixed: Unnecessary localization calls for add-ons.
* Fixed: Show an in-page notification rather than an alert box if the FFZ Control Center fails to load.
* Fixed: Adding an event to an EventListener while the event firing potentially leading to an infinite loop.
* Fixed: Pluralization rules for Ukrainian.
This commit is contained in:
SirStendec 2019-10-07 03:35:53 -04:00
parent 8c7e03119f
commit 02efd61f00
7 changed files with 180 additions and 118 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.13.2", "version": "4.13.3",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {

View file

@ -175,10 +175,17 @@ export default class AddonManager extends Module {
]); ]);
if ( this.i18n.locale !== 'en' ) { if ( this.i18n.locale !== 'en' ) {
terms.add(this.i18n.t(addon.name_i18n, addon.name)); if ( addon.name_i18n )
terms.add(this.i18n.t(addon.short_name_i18n, addon.short_name)); terms.add(this.i18n.t(addon.name_i18n, addon.name));
terms.add(this.i18n.t(addon.author_i18n, addon.author));
terms.add(this.i18n.t(addon.description_i18n, addon.description)); if ( addon.short_name_i18n )
terms.add(this.i18n.t(addon.short_name_i18n, addon.short_name));
if ( addon.author_i18n )
terms.add(this.i18n.t(addon.author_i18n, addon.author));
if ( addon.description_i18n )
terms.add(this.i18n.t(addon.description_i18n, addon.description));
} }
addon.search_terms = [...terms].map(term => term ? term.toLocaleLowerCase() : '').join('\n'); addon.search_terms = [...terms].map(term => term ? term.toLocaleLowerCase() : '').join('\n');

View file

@ -1,7 +1,7 @@
<template lang="html"> <template lang="html">
<div class="ffz--i18n-entry tw-pd-x-1 tw-pd-y-05 tw-border-b"> <div class="ffz--i18n-entry tw-pd-x-1 tw-pd-y-05 tw-border-b">
<div class="tw-flex tw-full-width"> <div class="tw-flex tw-full-width">
<div class="ffz--i18n-sub-entry tw-mg-r-05 tw-flex-grow-1 tw-c-text-alt tw-mg-b-2"> <div class="ffz--i18n-sub-entry tw-mg-r-05 tw-c-text-alt tw-mg-b-2">
<div class="tw-font-size-7 tw-c-text-alt-2 tw-pd-b-05 tw-strong tw-upcase tw-ellipsis" :title="entry.key"> <div class="tw-font-size-7 tw-c-text-alt-2 tw-pd-b-05 tw-strong tw-upcase tw-ellipsis" :title="entry.key">
{{ entry.key }} {{ entry.key }}
</div> </div>
@ -54,7 +54,7 @@
v-if="open" v-if="open"
class="tw-flex tw-full-width tw-mg-t-05" class="tw-flex tw-full-width tw-mg-t-05"
> >
<div class="ffz--i18n-sub-entry tw-mg-r-05 tw-flex-grow-1 tw-c-text-alt tw-mg-b-2"> <div class="ffz--i18n-sub-entry tw-mg-r-05 tw-c-text-alt tw-mg-b-2">
<div class="tw-font-size-7 tw-c-text-alt-2 tw-pd-b-05 tw-strong tw-upcase tw-ellipsis"> <div class="tw-font-size-7 tw-c-text-alt-2 tw-pd-b-05 tw-strong tw-upcase tw-ellipsis">
{{ t('i18n.ui.preview', 'Preview') }} {{ t('i18n.ui.preview', 'Preview') }}
</div> </div>

View file

@ -332,6 +332,7 @@ export default {
this.listen('i18n:got-keys', this.grabKeys, this); this.listen('i18n:got-keys', this.grabKeys, this);
this.listen('i18n:loaded', this.grabKeys, this); this.listen('i18n:loaded', this.grabKeys, this);
this.listen('i18n:strings-loaded', this.grabKeys, this);
}, },
mounted() { mounted() {
@ -351,6 +352,7 @@ export default {
this.unlisten('i18n:got-keys', this.grabKeys, this); this.unlisten('i18n:got-keys', this.grabKeys, this);
this.unlisten('i18n:loaded', this.grabKeys, this); this.unlisten('i18n:loaded', this.grabKeys, this);
this.unlisten('i18n:strings-loaded', this.grabKeys, this);
}, },
methods: { methods: {

View file

@ -22,6 +22,7 @@ export default class MenuButton extends SiteModule {
this._has_update = false; this._has_update = false;
this._important_update = false; this._important_update = false;
this._new_settings = 0; this._new_settings = 0;
this._error = null;
this.settings.add('ffz.show-new-settings', { this.settings.add('ffz.show-new-settings', {
default: true, default: true,
@ -39,6 +40,22 @@ export default class MenuButton extends SiteModule {
); );
} }
get has_error() {
return this._error != null;
}
get error() {
return this._error;
}
set error(val) {
if ( val === this._error )
return;
this._error = val;
this.update();
}
get new_settings() { get new_settings() {
return this._new_settings; return this._new_settings;
} }
@ -117,6 +134,10 @@ export default class MenuButton extends SiteModule {
} }
update() { update() {
requestAnimationFrame(() => this._update());
}
_update() {
for(const inst of this.NavBar.instances) for(const inst of this.NavBar.instances)
this.updateButton(inst); this.updateButton(inst);
} }
@ -170,7 +191,7 @@ export default class MenuButton extends SiteModule {
<div class="tw-inline-flex tw-relative tw-tooltip-wrapper"> <div class="tw-inline-flex tw-relative tw-tooltip-wrapper">
{btn = (<button {btn = (<button
class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative" class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-core-button tw-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
onClick={e => this.emit(':clicked', e, btn)} // eslint-disable-line react/jsx-no-bind onClick={e => this.handleClick(e, btn)} // eslint-disable-line react/jsx-no-bind
> >
<div class="tw-align-items-center tw-flex tw-flex-grow-0"> <div class="tw-align-items-center tw-flex tw-flex-grow-0">
<span class="tw-button-icon__icon"> <span class="tw-button-icon__icon">
@ -178,7 +199,24 @@ export default class MenuButton extends SiteModule {
</span> </span>
</div> </div>
</button>)} </button>)}
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right"> {this.has_error && (<div class="tw-absolute tw-balloon tw-balloon--down tw-balloon--lg tw-balloon--right tw-block">
<div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-4 tw-pd-1">
<div class="tw-flex tw-align-items-center">
<div class="tw-flex-grow-1">
{ this.error.i18n ? this.i18n.t(this.error.i18n, this.error.text) : this.error.text }
</div>
<button
class="tw-button-icon tw-mg-l-05 tw-relative tw-tooltip-wrapper"
onClick={() => this.error = null} // eslint-disable-line react/jsx-no-bind
>
<span class="tw-button-icon__icon">
<figure class="ffz-i-cancel" />
</span>
</button>
</div>
</div>
</div>)}
{! this.has_error && (<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{this.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')} {this.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
{this.has_update && (<div class="tw-mg-t-1"> {this.has_update && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.update-desc', 'There is an update available. Please refresh your page.')} {this.i18n.t('site.menu_button.update-desc', 'There is an update available. Please refresh your page.')}
@ -198,7 +236,7 @@ export default class MenuButton extends SiteModule {
{this.addons.has_dev && (<div class="tw-mg-t-1"> {this.addons.has_dev && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.addon-dev-desc', 'You have loaded add-on data from a local development server.')} {this.i18n.t('site.menu_button.addon-dev-desc', 'You have loaded add-on data from a local development server.')}
</div>)} </div>)}
</div> </div>)}
</div> </div>
{this.has_update && (<div class="ffz-menu__extra-pill tw-absolute"> {this.has_update && (<div class="ffz-menu__extra-pill tw-absolute">
<div class={`tw-pill ${this.important_update ? 'tw-pill--notification' : ''}`}> <div class={`tw-pill ${this.important_update ? 'tw-pill--notification' : ''}`}>
@ -215,71 +253,22 @@ export default class MenuButton extends SiteModule {
{pill} {pill}
</div> </div>
</div>)} </div>)}
</div>) </div>);
/*el = (<div class="ffz-top-nav tw-align-self-center tw-flex-grow-0 tw-flex-shrink-0 tw-flex-nowrap tw-pd-r-1 tw-pd-l-05">
{btn = (<button
class="tw-button-icon tw-button-icon--overlay tw-button-icon--large"
onClick={e => this.emit(':clicked', e, btn)} //eslint-disable-line react/jsx-no-bind
>
<div class="tw-tooltip-wrapper">
<span class="tw-button-icon__icon">
<figure class="ffz-i-zreknarf" />
</span>
{this.has_update && (<div class="ffz-menu__extra-pill tw-absolute">
<div class={`tw-pill ${this.important_update ? ' tw-pill--notification' : ''}`}>
<figure class="ffz-i-arrows-cw" />
</div>
</div>)}
{!this.has_update && DEBUG && this.addons.has_dev && (<div class="ffz-menu__extra-pill tw-absolute">
<div class="tw-pill">
{this.i18n.t('site.menu_button.dev', 'dev')}
</div>
</div>)}
{!this.has_update && DEBUG && ! this.addons.has_dev && (<div class="ffz-menu__extra-pill tw-absolute">
<div class="tw-pill">
{this.i18n.t('site.menu_button.main-dev', 'm-dev')}
</div>
</div>)}
{!this.has_update && ! DEBUG && this.addons.has_dev && (<div class="ffz-menu__extra-pill tw-absolute">
<div class="tw-pill">
{this.i18n.t('site.menu_button.addon-dev', 'a-dev')}
</div>
</div>)}
{this.has_new && ! pill && (<div class="ffz-menu__pill tw-absolute">
<div class="tw-pill">
{this.i18n.formatNumber(this.new_settings)}
</div>
</div>)}
{pill && (<div class="ffz-menu__pill tw-absolute">
<div class="tw-animation tw-animation--animate tw-animation--duration-medium tw-animation--timing-ease-in tw-animation--bounce-in">
<div class="tw-pill tw-pill--notification">
{pill}
</div>
</div>
</div>)}
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{this.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
{this.has_update && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.update-desc', 'There is an update available. Please refresh your page.')}
</div>)}
{this.has_new && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.new-desc', 'There {count,plural,one {is one new setting} other {are # new settings}}.', {count: this._new_settings})}
</div>)}
{DEBUG && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.main-dev-desc', 'You are running a developer build of FrankerFaceZ.')}
</div>)}
{this.addons.has_dev && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.addon-dev-desc', 'You have loaded add-on data from a local development server.')}
</div>)}
</div>
</div>
</button>)}
</div>);*/
container.insertBefore(el, container.lastElementChild); container.insertBefore(el, container.lastElementChild);
} }
handleClick(event, btn) {
if ( event.shiftKey ) {
if ( DEBUG && event.ctrlKey )
return requestAnimationFrame(() => this.i18n.openUI());
return this.resolve('main_menu').openPopout();
}
this.emit(':clicked', event, btn);
}
loadMenu(event, btn) { loadMenu(event, btn) {
const cl = btn && btn.classList; const cl = btn && btn.classList;
@ -292,10 +281,12 @@ export default class MenuButton extends SiteModule {
}).catch(err => { }).catch(err => {
this.log.capture(err); this.log.capture(err);
// TODO: Show a proper dialog and not an alert.
this.log.error('Error enabling main menu.', err); this.log.error('Error enabling main menu.', err);
alert('There was an error displaying the menu.'); // eslint-disable-line no-alert
this.error = {
i18n: 'site.menu_button.error',
text: 'There was an error loading the FFZ Control Center. Please refresh and try again.'
};
if ( cl ) if ( cl )
cl.remove('loading'); cl.remove('loading');

View file

@ -25,6 +25,7 @@ String.prototype.toSnakeCase = function() {
export class EventEmitter { export class EventEmitter {
constructor() { constructor() {
this.__listeners = {}; this.__listeners = {};
this.__running = new Set;
this.__dead_events = 0; this.__dead_events = 0;
} }
@ -93,6 +94,9 @@ export class EventEmitter {
} }
off(event, fn, ctx) { off(event, fn, ctx) {
if ( this.__running.has(event) )
throw new Error(`concurrent modification: tried removing event listener while event is running`);
let list = this.__listeners[event]; let list = this.__listeners[event];
if ( ! list ) if ( ! list )
return; return;
@ -121,14 +125,23 @@ export class EventEmitter {
} }
emitUnsafe(event, ...args) { emitUnsafe(event, ...args) {
const list = this.__listeners[event]; let list = this.__listeners[event];
if ( ! list ) if ( ! list )
return; 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 // Track removals separately to make iteration over the event list
// much, much simpler. // much, much simpler.
const removed = new Set; const removed = new Set;
// 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) { for(const item of list) {
const [fn, ctx, ttl] = item, const [fn, ctx, ttl] = item,
ret = fn.apply(ctx, args); ret = fn.apply(ctx, args);
@ -146,33 +159,50 @@ export class EventEmitter {
break; break;
} }
// Remove any dead listeners from the list.
if ( removed.size ) { if ( removed.size ) {
// Re-grab the list to make sure it wasn't removed mid-iteration. for(const item of removed) {
const new_list = this.__listeners[event]; const idx = list.indexOf(item);
if ( new_list ) { if ( idx !== -1 )
for(const item of removed) { list.splice(idx, 1);
const idx = new_list.indexOf(item);
if ( idx !== -1 )
new_list.splice(idx, 1);
}
if ( ! list.length ) {
this.__listeners[event] = null;
this.__dead_events++;
}
} }
} }
// 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);
} }
emit(event, ...args) { emit(event, ...args) {
const list = this.__listeners[event]; let list = this.__listeners[event];
if ( ! list ) if ( ! list )
return; 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 // Track removals separately to make iteration over the event list
// much, much simpler. // much, much simpler.
const removed = new Set; const removed = new Set;
// 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) { for(const item of list) {
const [fn, ctx, ttl] = item; const [fn, ctx, ttl] = item;
let ret; let ret;
@ -196,34 +226,51 @@ export class EventEmitter {
break; break;
} }
// Remove any dead listeners from the list.
if ( removed.size ) { if ( removed.size ) {
// Re-grab the list to make sure it wasn't removed mid-iteration. for(const item of removed) {
const new_list = this.__listeners[event]; const idx = list.indexOf(item);
if ( new_list ) { if ( idx !== -1 )
for(const item of removed) { list.splice(idx, 1);
const idx = new_list.indexOf(item);
if ( idx !== -1 )
new_list.splice(idx, 1);
}
if ( ! list.length ) {
this.__listeners[event] = null;
this.__dead_events++;
}
} }
} }
// 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);
} }
async emitAsync(event, ...args) { async emitAsync(event, ...args) {
const list = this.__listeners[event]; let list = this.__listeners[event];
if ( ! list ) if ( ! list )
return []; 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 // Track removals separately to make iteration over the event list
// much, much simpler. // much, much simpler.
const removed = new Set, const removed = new Set,
promises = []; 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) { for(const item of list) {
const [fn, ctx] = item; const [fn, ctx] = item;
let ret; let ret;
@ -260,23 +307,31 @@ export class EventEmitter {
const out = await Promise.all(promises); const out = await Promise.all(promises);
// Remove any dead listeners from the list.
if ( removed.size ) { if ( removed.size ) {
// Re-grab the list to make sure it wasn't removed mid-iteration. for(const item of removed) {
const new_list = this.__listeners[event]; const idx = list.indexOf(item);
if ( new_list ) { if ( idx !== -1 )
for(const item of removed) { list.splice(idx, 1);
const idx = new_list.indexOf(item);
if ( idx !== -1 )
new_list.splice(idx, 1);
}
if ( ! list.length ) {
this.__listeners[event] = null;
this.__dead_events++;
}
} }
} }
// 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; return out;
} }
} }

View file

@ -488,7 +488,7 @@ const CARDINAL_TO_LANG = {
hebrew: ['he'], hebrew: ['he'],
persian: ['fa'], persian: ['fa'],
french: ['fr', 'pt'], french: ['fr', 'pt'],
russian: ['ru'] russian: ['ru','uk']
} }
const CARDINAL_TYPES = { const CARDINAL_TYPES = {
@ -536,7 +536,8 @@ const ORDINAL_TO_LANG = {
hungarian: ['hu'], hungarian: ['hu'],
italian: ['it'], italian: ['it'],
one: ['fr', 'lo', 'ms'], one: ['fr', 'lo', 'ms'],
swedish: ['sv'] swedish: ['sv'],
ukranian: ['uk']
}; };
const ORDINAL_TYPES = { const ORDINAL_TYPES = {
@ -551,6 +552,12 @@ const ORDINAL_TYPES = {
return 5; return 5;
}, },
ukranian(n) {
const n1 = n % 10, n2 = n % 100;
if ( n1 === 3 && n2 !== 13 ) return 3;
return 5;
},
hungarian: n => (n === 1 || n === 5) ? 1 : 5, hungarian: n => (n === 1 || n === 5) ? 1 : 5,
italian: n => (n === 11 || n === 8 || n === 80 || n === 800) ? 4 : 5, italian: n => (n === 11 || n === 8 || n === 80 || n === 800) ? 4 : 5,