1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-08 07:10:54 +00:00
* Added: Show the FFZ menu button on new dashboard pages.
* Fixed: Synchronize settings with the `dashboard.twitch.tv` subdomain.
* Fixed: Performance issue with metadata tool-tips being calculated too frequently.
* Fixed: Metadata not appearing in theater mode when portrait mode is enabled.
* API Added: Chat Action types can now override rendering.
This commit is contained in:
SirStendec 2019-11-25 17:50:20 -05:00
parent 347919c51a
commit ff0f0ea074
14 changed files with 317 additions and 42 deletions

View file

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

View file

@ -82,7 +82,9 @@ class FFZBridge extends Module {
ffz_type: 'loaded',
data: out
});
}
} else if ( msg.ffz_type === 'change' )
this.onChange(msg);
}
send(msg) { // eslint-disable-line class-methods-use-this
@ -91,6 +93,17 @@ class FFZBridge extends Module {
} catch(err) { this.log.error('send error', err); /* no-op */ }
}
onChange(msg) {
const key = msg.key,
value = msg.value,
deleted = msg.deleted;
if ( deleted )
this.settings.provider.delete(key);
else
this.settings.provider.set(key, value);
}
onProviderChange(key, value, deleted) {
this.send({
ffz_type: 'change',

View file

@ -437,12 +437,11 @@ export default class Actions extends Module {
if ( ! data || ! data.action || ! data.appearance )
continue;
const ap = data.appearance || {},
disp = data.display || {},
let ap = data.appearance || {};
const disp = data.display || {},
act = this.actions[data.action];
def = this.renderers[ap.type];
if ( ! def || disp.disabled ||
if ( ! act || disp.disabled ||
(disp.mod_icons != null && disp.mod_icons !== !!mod_icons) ||
(disp.mod != null && disp.mod !== (current_user ? !!current_user.mod : false)) ||
(disp.staff != null && disp.staff !== (current_user ? !!current_user.staff : false)) ||
@ -453,12 +452,23 @@ export default class Actions extends Module {
(disp.followersOnly != null && disp.followersOnly !== current_room.followersOnly) )
continue;
if ( act.override_appearance ) {
const out = act.override_appearance.call(this, Object.assign({}, ap), data, null, current_room, current_user, mod_icons);
if ( out )
ap = out;
}
const def = this.renderers[ap.type];
if ( ! def )
continue;
const has_color = def.colored && ap.color,
disabled = maybe_call(act.disabled, this, data, null, current_room, current_user, mod_icons) || false,
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
contents = def.render.call(this, ap, createElement, color);
actions.push(<button
class={`ffz-tooltip tw-pd-x-05 ffz-mod-icon mod-icon tw-c-text-alt-2${has_color ? ' colored' : ''}`}
class={`ffz-tooltip tw-pd-x-05 ffz-mod-icon mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}`}
data-tooltip-type="action"
data-action={data.action}
data-options={data.options ? JSON.stringify(data.options) : null}
@ -552,19 +562,29 @@ export default class Actions extends Module {
} else if ( ! data.action || ! data.appearance )
continue;
const ap = data.appearance || {},
disp = data.display || {},
let ap = data.appearance || {};
const disp = data.display || {},
act = this.actions[data.action];
def = this.renderers[ap.type];
if ( ! def || disp.disabled ||
if ( ! act || disp.disabled ||
(disp.mod_icons != null && disp.mod_icons !== !!mod_icons) ||
(disp.mod != null && disp.mod !== (current_level > msg_level)) ||
(disp.staff != null && disp.staff !== (u ? !!u.staff : false)) ||
(disp.deleted != null && disp.deleted !== !!msg.deleted) )
continue;
if ( act.override_appearance ) {
const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, r, u, mod_icons);
if ( out )
ap = out;
}
const def = this.renderers[ap.type];
if ( ! def )
continue;
const has_color = def.colored && ap.color,
disabled = maybe_call(act.disabled, this, data, msg, r, u, mod_icons) || false,
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
contents = def.render.call(this, ap, createElement, color);
@ -572,7 +592,8 @@ export default class Actions extends Module {
lines.push(line = []);
const btn = (<button
class="ffz-tooltip ffz-tooltip--no-mouse tw-button tw-button--text"
class={`ffz-tooltip ffz-tooltip--no-mouse tw-button tw-button--text${disabled ? ' tw-button--disabled disabled' : ''}`}
disabled={disabled}
data-tooltip-type="action"
data-action={data.action}
data-options={data.options ? JSON.stringify(data.options) : null}
@ -631,20 +652,30 @@ export default class Actions extends Module {
if ( ! data.action || ! data.appearance )
continue;
const ap = data.appearance || {},
disp = data.display || {},
let ap = data.appearance || {};
const disp = data.display || {},
keys = disp.keys,
act = this.actions[data.action];
def = this.renderers[ap.type];
if ( ! def || disp.disabled ||
if ( ! act || disp.disabled ||
(disp.mod_icons != null && disp.mod_icons !== !!mod_icons) ||
(disp.mod != null && disp.mod !== (current_level > msg_level)) ||
(disp.staff != null && disp.staff !== (current_user ? !!current_user.staff : false)) ||
(disp.deleted != null && disp.deleted !== !!msg.deleted) )
continue;
if ( act.override_appearance ) {
const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons);
if ( out )
ap = out;
}
const def = this.renderers[ap.type];
if ( ! def )
continue;
const has_color = def.colored && ap.color,
disabled = maybe_call(act.disabled, this, data, msg, current_room, current_user, mod_icons) || false,
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
contents = def.render.call(this, ap, createElement, color);
@ -655,7 +686,8 @@ export default class Actions extends Module {
had_action = true;
list.push(<button
class={`ffz-tooltip ffz-mod-icon mod-icon tw-c-text-alt-2${has_color ? ' colored' : ''}${keys ? ` ffz-modifier-${keys}` : ''}`}
class={`ffz-tooltip ffz-mod-icon mod-icon tw-c-text-alt-2${disabled ? ' disabled' : ''}${has_color ? ' colored' : ''}${keys ? ` ffz-modifier-${keys}` : ''}`}
disabled={disabled}
data-tooltip-type="action"
data-action={data.action}
data-options={data.options ? JSON.stringify(data.options) : null}
@ -799,6 +831,9 @@ export default class Actions extends Module {
if ( ! data )
return;
if ( target.classList.contains('disabled') )
return;
if ( ! data.definition.click ) {
if ( data.definition.context )
return this.handleContext(event);
@ -823,6 +858,9 @@ export default class Actions extends Module {
if ( ! data )
return;
if ( target.classList.contains('disabled') )
return;
if ( target._ffz_tooltip$0 )
target._ffz_tooltip$0.hide();

View file

@ -111,6 +111,8 @@ export default class Metadata extends Module {
subtitle: () => this.i18n.t('metadata.uptime.subtitle', 'Uptime'),
tooltip(data) {
console.log('tool-tip');
if ( ! data.created )
return null;
@ -444,8 +446,7 @@ export default class Metadata extends Module {
if ( ! label )
return destroy();
const tooltip = maybe_call(def.tooltip, this, data),
order = maybe_call(def.order, this, data),
const order = maybe_call(def.order, this, data),
color = maybe_call(def.color, this, data) || '';
if ( ! el ) {
@ -468,7 +469,7 @@ export default class Metadata extends Module {
el = (<div
class={`tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat ffz-stat--fix-padding ${border ? 'tw-mg-l-1' : 'tw-mg-l-05 ffz-mg-r--05'}`}
data-key={key}
tip_content={tooltip}
tip_content={null}
>
{btn = (<button
class={`tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-core-button tw-core-button--padded tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ${border ? 'tw-border-l tw-border-t tw-border-b' : 'tw-font-size-5 tw-regular'}`}
@ -493,7 +494,7 @@ export default class Metadata extends Module {
btn = popup = el = (<button
class={`ffz-stat tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-top-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--text ${inherit ? 'ffz-c-text-inherit' : 'tw-c-text-base'} tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative tw-pd-x-05 ffz-stat--fix-padding ${border ? 'tw-border tw-mg-l-1' : 'tw-font-size-5 tw-regular tw-mg-l-05 ffz-mg-r--05'}`}
data-key={key}
tip_content={tooltip}
tip_content={null}
>
<div class="tw-align-items-center tw-flex tw-flex-grow-0 tw-justify-center">
{icon}
@ -592,7 +593,7 @@ export default class Metadata extends Module {
el = (<div
class="tw-align-items-center tw-inline-flex tw-relative tw-tooltip-wrapper ffz-stat tw-stat tw-mg-l-1"
data-key={key}
tip_content={tooltip}
tip_content={null}
>
{icon}
{stat = <span class={`${icon ? 'tw-mg-l-05 ' : ''}ffz-stat-text tw-stat__value`} />}
@ -627,9 +628,12 @@ export default class Metadata extends Module {
logger: this.log,
live: false,
html: true,
content: () => el.tip_content,
content: () => maybe_call(def.tooltip, this, el._ffz_data),
onShow: (t, tip) => el.tip = tip,
onHide: () => el.tip = null,
onHide: () => {
el.tip = null;
el.tip_content = null;
},
popper: {
placement: 'bottom',
modifiers: {
@ -655,10 +659,12 @@ export default class Metadata extends Module {
if ( el._ffz_order !== order )
el.style.order = el._ffz_order = order;
if ( el.tip_content !== tooltip ) {
el.tip_content = tooltip;
if ( el.tip )
if ( el.tip ) {
const tooltip = maybe_call(def.tooltip, this, data);
if ( el.tip_content !== tooltip ) {
el.tip_content = tooltip;
setChildren(el.tip.element, tooltip);
}
}
}

View file

@ -180,12 +180,14 @@ export class LocalStorageProvider extends SettingsProvider {
this._cached.set(key, value);
localStorage.setItem(this.prefix + key, JSON.stringify(value));
this.broadcast({type: 'set', key});
this.emit('set', key, value, false);
}
delete(key) {
this._cached.delete(key);
localStorage.removeItem(this.prefix + key);
this.broadcast({type: 'delete', key});
this.emit('set', key, undefined, true);
}
has(key) {

View file

@ -42,8 +42,11 @@ export default class Twilight extends BaseSite {
this.populateModules();
this.web_munch.known(Twilight.KNOWN_MODULES);
this.router.route(Twilight.ROUTES);
this.router.routeName(Twilight.ROUTE_NAMES);
this.router.route(Twilight.DASH_ROUTES, 'dashboard.twitch.tv');
}
onEnable() {
@ -205,7 +208,8 @@ Twilight.CHAT_ROUTES = [
'dash',
'embed-chat',
'squad',
'command-center'
'command-center',
'dash-stream-manager'
];
@ -220,6 +224,47 @@ Twilight.ROUTE_NAMES = {
};
Twilight.SUNLIGHT_ROUTES = [
'dash-stream-manager',
'dash-channel-analytics',
'dash-stream-summary',
'dash-achievements',
'dash-roles',
'dash-activity',
'dash-channel-points',
'dash-video-producer',
'dash-edit-video',
'dash-collections',
'dash-edit-collection',
'dash-clips',
'dash-settings-moderation',
'dash-settings-channel',
'dash-settings-revenue',
'dash-extensions',
'dash-streaming-tools'
];
Twilight.DASH_ROUTES = {
'dash-stream-manager': '/u/:userName/stream-manager',
'dash-channel-analytics': '/u/:userName/channel-analytics',
'dash-stream-summary': '/u/:userName/stream-summary',
'dash-achievements': '/u/:userName/achievements',
'dash-roles': '/u/:userName/community/roles',
'dash-activity': '/u/:userName/community/activity',
'dash-channel-points': '/u/:userName/community/channel-points',
'dash-video-producer': '/u/:userName/content/video-producer',
'dash-edit-video': '/u/:userName/content/video-producer/edit/:videoID',
'dash-collections': '/u/:userName/content/collections',
'dash-edit-collection': '/u/:userName/content/collections/:collectionID',
'dash-clips': '/u/:userName/content/clips',
'dash-settings-moderation': '/u/:userName/settings/moderation',
'dash-settings-channel': '/u/:userName/settings/channel',
'dash-settings-revenue': '/u/:userName/settings/revenue',
'dash-extensions': '/u/:userName/extensions',
'dash-streaming-tools': '/u/:userName/broadcast',
};
Twilight.ROUTES = {
'front-page': '/',
'collection': '/collections/:collectionID',

View file

@ -0,0 +1,3 @@
.channel-root__scroll-area--theatre-mode .channel-info-bar {
bottom: calc(10rem + var(--ffz-chat-height)) !important;
}

View file

@ -0,0 +1,3 @@
.channel-root__scroll-area--theatre-mode .channel-info-bar {
right: 40rem !important;
}

View file

@ -101,6 +101,22 @@ export default class Layout extends Module {
changed: val => this.css_tweaks.toggle('portrait-swapped', val)
});
this.settings.add('layout.use-portrait-meta', {
requires: ['layout.inject-portrait', 'player.theatre.metadata'],
process(ctx) {
return ctx.get('layout.inject-portrait') && ctx.get('player.theatre.metadata')
},
changed: val => this.css_tweaks.toggle('portrait-metadata', val)
});
this.settings.add('layout.use-portrait-meta-top', {
requires: ['layout.use-portrait-meta', 'layout.portrait-invert'],
process(ctx) {
return ctx.get('layout.use-portrait-meta') && ! ctx.get('layout.portrait-invert')
},
changed: val => this.css_tweaks.toggle('portrait-metadata-top', val)
});
this.settings.add('layout.show-portrait-chat', {
requires: ['layout.use-portrait', 'layout.portrait-extra-height', 'layout.portrait-extra-width'],
process() {
@ -168,6 +184,8 @@ export default class Layout extends Module {
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`);

View file

@ -8,6 +8,9 @@ import {DEBUG} from 'utilities/constants';
import {SiteModule} from 'utilities/module';
import {createElement, ClickOutside, setChildren} from 'utilities/dom';
import Twilight from 'site';
export default class MenuButton extends SiteModule {
constructor(...args) {
super(...args);
@ -35,6 +38,12 @@ export default class MenuButton extends SiteModule {
changed: () => this.update()
});
this.SunlightDash = this.fine.define(
'sunlight-dash',
n => n.getIsChannelEditor && n.getIsChannelModerator && n.getIsAdsEnabled && n.getIsSquadStreamsEnabled,
Twilight.SUNLIGHT_ROUTES
);
this.NavBar = this.fine.define(
'nav-bar',
n => n.renderOnsiteNotifications && n.renderTwitchPrimeCrown
@ -171,12 +180,14 @@ export default class MenuButton extends SiteModule {
for(const inst of this.MultiController.instances)
this.updateButton(inst);
for(const inst of this.SunlightDash.instances)
this.updateButton(inst);
}
onEnable() {
this.NavBar.ready(() => this.update());
this.NavBar.on('mount', this.updateButton, this);
this.NavBar.on('update', this.updateButton, this);
@ -188,6 +199,10 @@ export default class MenuButton extends SiteModule {
this.MultiController.on('mount', this.updateButton, this);
this.MultiController.on('update', this.updateButton, this);
this.SunlightDash.ready(() => this.update());
this.SunlightDash.on('mount', this.updateButton, this);
this.SunlightDash.on('update', this.updateButton, this);
this.on(':clicked', () => this.important_update = false);
this.once(':clicked', this.loadMenu);
@ -201,6 +216,7 @@ export default class MenuButton extends SiteModule {
updateButton(inst) {
const root = this.fine.getChildNode(inst);
let is_squad = false,
is_sunlight = false,
container = root && root.querySelector('.top-nav__menu');
if ( ! container ) {
@ -219,6 +235,12 @@ export default class MenuButton extends SiteModule {
is_squad = true;
}
if ( ! container && inst.getIsAdsEnabled ) {
container = root && root.querySelector('.sunlight-top-nav > .tw-flex');
if ( container )
is_sunlight = true;
}
if ( ! container )
return;
@ -242,7 +264,7 @@ export default class MenuButton extends SiteModule {
extra_pill = this.formatExtraPill();
el = (<div
class="ffz-top-nav tw-align-self-center tw-flex-grow-0 tw-flex-nowrap tw-flex-shrink-0 tw-mg-x-05 tw-relative"
class={`ffz-top-nav tw-align-self-center tw-flex-grow-0 tw-flex-nowrap tw-flex-shrink-0 tw-relative ${is_sunlight ? 'tw-mg-l-05 tw-mg-r-2' : 'tw-mg-x-05'}`}
>
<div class="tw-inline-flex tw-relative tw-tooltip-wrapper">
{btn = (<button

View file

@ -0,0 +1,104 @@
'use strict';
// ============================================================================
// Settings Sync
// ============================================================================
import Module from 'utilities/module';
import {createElement} from 'utilities/dom';
export default class SettingsSync extends Module {
constructor(...args) {
super(...args);
this.should_enable = window.location.host !== 'www.twitch.tv';
this.inject('settings');
}
onEnable() {
const frame = this.frame = createElement('iframe');
frame.src = '//www.twitch.tv/p/ffz_bridge/';
frame.id = 'ffz-settings-bridge';
frame.style.width = 0;
frame.style.height = 0;
this.settings.provider.on('set', this.onProviderSet, this);
window.addEventListener('message', this.onMessage.bind(this));
document.body.appendChild(frame);
}
send(msg) {
try {
this.frame.contentWindow.postMessage(msg, '*');
} catch(err) { this.log.error('send error', err); /* no-op */ }
}
onMessage(event) {
const msg = event.data;
if ( ! msg || ! msg.ffz_type )
return;
if ( msg.ffz_type === 'ready' )
this.send({ffz_type: 'load'});
else if ( msg.ffz_type === 'loaded' )
this.onLoad(msg.data);
else if ( msg.ffz_type === 'change' )
this.onChange(msg);
else
this.log.info('Unknown Message', msg.ffz_type, msg);
}
onLoad(data) {
if ( ! data )
return;
const provider = this.settings.provider,
old_keys = new Set(provider.keys());
this.skip = true;
for(const [key, value] of Object.entries(data)) {
old_keys.delete(key);
if ( provider.get(key) === value )
continue;
provider.set(key, value);
provider.emit('changed', key, value, false);
}
for(const key of old_keys) {
provider.delete(key);
provider.emit('changed', key, undefined, true);
}
this.skip = false;
}
onProviderSet(key, value, deleted) {
if ( this.skip )
return;
this.send({
ffz_type: 'change',
key,
value,
deleted
});
}
onChange(msg) {
const key = msg.key,
value = msg.value,
deleted = msg.deleted;
this.skip = true;
if ( deleted )
this.settings.provider.delete(key);
else
this.settings.provider.set(key, value);
this.skip = false;
this.settings.provider.emit('changed', key, value, deleted, true);
}
}

View file

@ -88,6 +88,11 @@
text-align: center;
display: inline-flex;
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
& + .ffz-mod-icon {
margin-left: 1px;
}
@ -125,8 +130,8 @@
}
.ffz--action &,
.tw-interactable:hover &,
&:hover {
.tw-interactable:hover &:not(.disabled),
&:not(.disabled):hover {
.tw-root--theme-dark &, & {
&.tw-c-text-alt-2 {
color: inherit !important;

View file

@ -45,18 +45,25 @@ export default class FineRouter extends Module {
_navigateTo(location) {
this.log.debug('New Location', location);
const path = location.pathname;
if ( path === this.location )
const host = window.location.host,
path = location.pathname;
if ( path === this.location && host === this.domain )
return;
this.location = path;
this.domain = host;
this._pickRoute();
}
_pickRoute() {
const path = this.location;
const path = this.location,
host = this.domain;
for(const route of this.__routes) {
if ( route.domain && route.domain !== host )
continue;
const match = route.regex.exec(path);
if ( match ) {
this.log.debug('Matching Route', route, match);
@ -105,12 +112,19 @@ export default class FineRouter extends Module {
}
getRouteNames() {
for(const route of Object.keys(this.getRoutes()))
this.getRouteName(route);
return this.route_names;
}
getRouteName(route) {
if ( ! this.route_names[route] )
this.route_names[route] = route.replace(/(^|-)([a-z])/g, (_, spacer, letter) => `${spacer ? ' ' : ''}${letter.toLocaleUpperCase()}`);
this.route_names[route] = route
.replace(/^dash-([a-z])/, (_, letter) =>
`Dashboard: ${letter.toLocaleUpperCase()}`)
.replace(/(^|-)([a-z])/g, (_, spacer, letter) =>
`${spacer ? ' ' : ''}${letter.toLocaleUpperCase()}`);
return this.route_names[route];
}
@ -132,11 +146,12 @@ export default class FineRouter extends Module {
this.emit(':updated-route-names');
}
route(name, path, process = true) {
route(name, path, domain = null, process = true) {
if ( typeof name === 'object' ) {
domain = path;
for(const key in name)
if ( has(name, key) )
this.route(key, name[key], false);
this.route(key, name[key], domain, false);
if ( process ) {
this.__routes.sort((a,b) => b.score - a.score);
@ -157,6 +172,7 @@ export default class FineRouter extends Module {
name,
parts,
score,
domain,
regex: tokensToRegExp(parts),
url: tokensToFunction(parts)
}

View file

@ -8,7 +8,7 @@ const ATTRS = [
'challenge', 'charset', 'checked', 'cite', 'class', 'code', 'codebase',
'color', 'cols', 'colspan', 'content', 'contenteditable', 'contextmenu',
'controls', 'coords', 'crossorigin', 'data', 'data-*', 'datetime',
'default', 'defer', 'dir', 'dirname', 'disabled', 'download', 'draggable',
'default', 'defer', 'dir', 'dirname', 'download', 'draggable',
'dropzone', 'enctype', 'for', 'form', 'formaction', 'headers', 'height',
'hidden', 'high', 'href', 'hreflang', 'http-equiv', 'icon', 'id',
'integrity', 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang',