1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Setting to display Content Flags on directory pages.
* Fixed: The setting to make the player larger on `clips.twitch.tv` pages incorrectly defaulting to true. Closes #1440.
* Fixed: Exception thrown when pressing Escape in chat with a tray open.
* Fixed: Chat not correctly displaying messages until the mouse is moved when using an option to pause chat using mouse movement.
* Fixed: Misspelled entry in the Content Flags list.
* Fixed: Some directory cards not being correctly detected by FFZ.
* Fixed: Color processing throwing an exception if the input value is empty.
* Fixed: Some page elements failing to appear correctly, instead showing an error. Notably this affects the PrattleNot add-on as well as some emote tool-tips. Closes https://github.com/FrankerFaceZ/Add-Ons/issues/193
* Changed: Add a note to the setting to hide extensions that recommends CommanderRoot's Disable Twitch Extensions browser extension.
* Changed: Log a warning when users use external scripts to mess with experiments.
* API Changed: `deep_copy` now correctly handles RegExp, Date, Set, and Map objects.
This commit is contained in:
SirStendec 2023-12-18 17:11:44 -05:00
parent 429553f5d4
commit 80931479c1
14 changed files with 216 additions and 36 deletions

View file

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

View file

@ -20,8 +20,8 @@
"name": "MQTT-Based PubSub",
"description": "An experimental new pubsub system that should be more reliable than the existing socket cluster.",
"groups": [
{"value": true, "weight": 5},
{"value": false, "weight": 95}
{"value": true, "weight": 0},
{"value": false, "weight": 100}
]
}
}
}

View file

@ -4,7 +4,7 @@
// Experiments
// ============================================================================
import {DEBUG, SERVER} from 'utilities/constants';
import {DEBUG, SERVER, SERVER_OR_EXT} from 'utilities/constants';
import Module, { GenericModule } from 'utilities/module';
import {has, deep_copy, fetchJSON} from 'utilities/object';
import { getBuster } from 'utilities/time';
@ -465,11 +465,43 @@ export default class ExperimentManager extends Module<'experiments', ExperimentE
}
private _checkExternalAccess() {
let stack;
try {
stack = new Error().stack;
} catch(err) {
/* :thinking: */
try {
stack = err.stack;
} catch(err_again) { /* aww */ }
}
if ( ! stack )
return;
stack = stack.split(/\s*\n+\s*/g).filter(x => x.startsWith('at '));
let external = false;
for(const line of stack) {
if ( ! line.includes(SERVER_OR_EXT) ) {
external = true;
break;
}
}
if ( external )
this.log.warn('Detected access by external script.');
}
setTwitchOverride(key: string, value: string) {
const overrides = this._getOverrideCookie(),
experiments = overrides.experiments,
disabled = overrides.disabled;
this._checkExternalAccess();
experiments[key] = value;
const idx = disabled.indexOf(key);
@ -489,6 +521,8 @@ export default class ExperimentManager extends Module<'experiments', ExperimentE
const overrides = this._getOverrideCookie(),
experiments = overrides.experiments;
this._checkExternalAccess();
if ( ! has(experiments, key) )
return;

View file

@ -125,6 +125,11 @@ export default class ClipsSite extends BaseSite {
route_data: this.router.match
});
// We need the default to be defined to get the correct value.
this.settings.add('clips.layout.big', {
default: false,
});
this.settings.getChanges('channel.hide-unfollow', val =>
this.css_tweaks.toggleHide('unfollow-button', val));

View file

@ -526,7 +526,7 @@ export default class PlayerBase extends Module {
ui: {
path: 'Player > General >> Extensions',
title: 'Show Overlay Extensions',
description: 'Note: This feature does not prevent extensions from loading. Hidden extensions are merely invisible. Hiding extensions with this feature will not improve your security.',
description: '**Note**: This feature does not prevent extensions from loading. Hidden extensions are merely invisible. Hiding extensions with this feature will not improve your security. To prevent extensions from loading entirely, we recommend using the [Disable Twitch Extensions browser extension](https://twitch-tools.rootonline.de/disable_twitch_extensions.php) by CommanderRoot.',
component: 'setting-select-box',
data: [
{value: 2, title: 'Never'},
@ -2327,4 +2327,4 @@ export default class PlayerBase extends Module {
return null;
}
}
}

View file

@ -725,7 +725,7 @@ export default class Input extends Module {
if ( inst.props.isShowingEmotePicker )
inst.props.closeEmotePicker();
else if ( inst.props.tray && (! inst.state.value || ! inst.state.value.length) )
inst.closeTray();
inst.props.clearTray();
}
} catch(err) {

View file

@ -582,8 +582,10 @@ export default class Scroller extends Module {
// Pause Stuff
cls.prototype.ffzShouldBePaused = function(since) {
// We may not have moved the mouse. If that's the case,
// since should be zero.
if ( since == null )
since = Date.now() - this.ffz_last_move;
since = Date.now() - (this.ffz_last_move ?? Date.now());
if ( this.state.ffz_scrolled_up )
return true;

View file

@ -24,7 +24,7 @@ export const CONTENT_FLAGS = [
'MatureGame',
'ProfanityVulgarity',
'SexualThemes',
'ViolentGrpahic'
'ViolentGraphic'
];
function formatTerms(data, flags) {
@ -61,7 +61,8 @@ export default class Directory extends Module {
this.inject(Game);
this.DirectoryCard = this.elemental.define(
'directory-card', 'article[data-a-target^="followed-vod-"],article[data-a-target^="card-"],div[data-a-target^="video-tower-card-"] article,div[data-a-target^="clips-card-"] article,.shelf-card__impression-wrapper article,.tw-tower div article',
'directory-card',
'article[data-a-target^="video-carousel-card-"],article[data-a-target^="followed-vod-"],article[data-a-target^="card-"],div[data-a-target^="video-tower-card-"] article,div[data-a-target^="clips-card-"] article,.shelf-card__impression-wrapper article,.tw-tower div article',
DIR_ROUTES, null, 0, 0
);
@ -138,6 +139,17 @@ export default class Directory extends Module {
changed: () => this.updateCards()
});
this.settings.add('directory.show-flags', {
default: false,
ui: {
path: 'Directory > Channels >> Appearance',
title: 'Display Content Flags on channel cards.',
component: 'setting-check-box'
},
changed: () => this.updateCards()
});
/*this.settings.add('directory.show-channel-avatars', {
default: true,
@ -343,9 +355,13 @@ export default class Directory extends Module {
always_inherit: true,
process(ctx, val) {
const out = new Set;
for(const v of val)
if ( v?.v )
out.add(v.v);
for(const v of val) {
let item = v?.v;
if ( item === 'ViolentGrpahic')
item = 'ViolentGraphic';
if ( item )
out.add(item);
}
return out;
},
@ -588,16 +604,34 @@ export default class Directory extends Module {
tags = props.tagListProps?.freeformTags;
const need_flags = this.settings.get('directory.wait-flags'),
show_flags = this.settings.get('directory.show-flags'),
blur_flags = this.settings.get('directory.blur-flags', []),
block_flags = this.settings.get('directory.block-flags', []),
has_flags = blur_flags.size > 0 || block_flags.size > 0;
filter_flags = blur_flags.size > 0 || block_flags.size > 0,
has_flags = show_flags || filter_flags;
if ( el._ffz_flags === undefined && has_flags ) {
el._ffz_flags = null;
this.twitch_data.getStreamFlags(null, props.channelLogin).then(data => {
el._ffz_flags = data ?? [];
this.updateCard(el);
});
// Are we getting a clip, a video, or a stream?
if ( props.slug ) {
// Clip
console.log('need flags for clip', props.slug);
el._ffz_flags = [];
} else if ( props.vodID ) {
// Video
console.log('need flags for vod', props.vodID);
el._ffz_flags = [];
} else {
// Stream?
console.log('need flags for stream', props.channelLogin);
this.twitch_data.getStreamFlags(null, props.channelLogin).then(data => {
el._ffz_flags = data ?? [];
this.updateCard(el);
});
}
}
let bad_tag = false,
@ -623,7 +657,7 @@ export default class Directory extends Module {
}
let should_blur = blur_tag;
if ( need_flags && has_flags && el._ffz_flags == null )
if ( need_flags && filter_flags && el._ffz_flags == null )
should_blur = true;
if ( ! should_blur )
should_blur = this.settings.provider.get('directory.game.hidden-thumbnails', []).includes(game);
@ -682,8 +716,41 @@ export default class Directory extends Module {
hide_container.classList.toggle('tw-hide', should_hide);
this.updateUptime(el, props);
this.updateFlags(el);
}
updateFlags(el) {
if ( ! document.contains(el) )
return this.clearFlags(el);
const setting = this.settings.get('directory.show-flags');
if ( ! setting || ! el._ffz_flags?.length )
return this.clearFlags(el);
const container = this._getTopRightContainer(el);
if ( ! container )
return this.clearFlags(el);
if ( ! el.ffz_flags_el )
container.appendChild(el.ffz_flags_el = (<div class="tw-mg-y-05 ffz-flags-element tw-relative ffz-il-tooltip__container">
<div class="tw-border-radius-small tw-c-background-overlay tw-c-text-overlay tw-flex tw-pd-x-05">
<figure class="ffz-i-flag" />
</div>
{el.ffz_flags_tt = <div class="ffz-il-tooltip ffz-il-tooltip--pre ffz-il-tooltip--down ffz-il-tooltip--align-right" />}
</div>));
el.ffz_flags_tt.textContent = this.i18n.t('metadata.flags.tooltip', 'Intended for certain audiences. May contain:')
+ '\n\n'
+ el._ffz_flags.map(x => x.localizedName).join('\n');
}
clearFlags(el) {
if ( el.ffz_flags_el ) {
el.ffz_flags_el.remove();
el.ffz_flags_tt = null;
}
}
updateCards() {
this.DirectoryCard.each(el => this.updateCard(el));
@ -695,6 +762,32 @@ export default class Directory extends Module {
clearCard(el) {
this.clearUptime(el);
this.clearFlags(el);
const cont = this._getTopRightContainer(el, false);
if ( cont )
cont.remove();
el._ffz_top_right = null;
}
_getTopRightContainer(el, should_create = true) {
let cont = el._ffz_top_right ?? el.querySelector('.ffz-top-right');
if ( cont || ! should_create )
return cont;
const container = el.querySelector('a[data-a-target="preview-card-image-link"] > div');
if ( ! container )
return null;
cont = (<div
data-test-selector="top-right-selector"
class="tw-absolute tw-mg-1 tw-right-0 tw-top-0 ffz-top-right tw-flex tw-flex-column tw-align-items-end"
/>);
el._ffz_top_right = cont;
container.appendChild(cont);
return cont;
}
@ -702,9 +795,12 @@ export default class Directory extends Module {
if ( ! document.contains(el) )
return this.clearUptime(el);
const container = el.querySelector('a[data-a-target="preview-card-image-link"] > div'),
const container = this._getTopRightContainer(el),
setting = this.settings.get('directory.uptime');
//const container = el.querySelector('a[data-a-target="preview-card-image-link"] > div'),
// setting = this.settings.get('directory.uptime');
if ( ! container || setting === 0 || props.viewCount || props.animatedImageProps )
return this.clearUptime(el);
@ -733,8 +829,8 @@ export default class Directory extends Module {
if ( ! el.ffz_uptime_el ) {
el.ffz_uptime_el = container.querySelector('.ffz-uptime-element');
if ( ! el.ffz_uptime_el )
container.appendChild(el.ffz_uptime_el = (<div class="ffz-uptime-element tw-absolute tw-right-0 tw-top-0 tw-mg-1">
<div class="tw-relative ffz-il-tooltip__container">
container.appendChild(el.ffz_uptime_el = (
<div class="ffz-uptime-element tw-relative ffz-il-tooltip__container">
<div class="tw-border-radius-small tw-c-background-overlay tw-c-text-overlay tw-flex tw-pd-x-05">
<div class="tw-flex tw-c-text-live">
<figure class="ffz-i-clock" />
@ -746,7 +842,7 @@ export default class Directory extends Module {
{el.ffz_uptime_tt = <div class="tw-pd-t-05" />}
</div>
</div>
</div>));
));
}
if ( ! el.ffz_uptime_span )

View file

@ -5,6 +5,7 @@
}
.ffz-channel-avatar,
.ffz-flags-element,
.ffz-uptime-element {
pointer-events: all;
}
@ -36,6 +37,9 @@
}
}
.ffz-uptime-element { order: 0; }
.ffz-flags-element { order: 1; }
.ffz-host-menu {
.scrollable-area {
@ -54,4 +58,4 @@
height: 3rem;
}
}
}
}

View file

@ -820,6 +820,9 @@ export class ColorAdjuster {
}
process(color: BaseColor | string, throw_errors = false) {
if ( ! color )
return null;
if ( this._mode === -1 )
return '';
@ -829,9 +832,6 @@ export class ColorAdjuster {
if ( this._mode === 0 )
return color;
if ( ! color )
return null;
if ( this._cache.has(color) )
return this._cache.get(color);

View file

@ -177,18 +177,23 @@ export function createElement(tag: string, props?: any, ...children: DomFragment
if ( lk === 'style' ) {
if ( typeof prop === 'string' )
el.style.cssText = prop;
else
else if ( prop && typeof prop === 'object' )
for(const [key, val] of Object.entries(prop)) {
if ( has(el.style, key) || has(Object.getPrototypeOf(el.style), key) )
(el.style as any)[key] = val;
else
el.style.setProperty(key, prop[key]);
}
else
console.warn('unsupported style value', prop);
} else if ( lk === 'dataset' ) {
for(const k in prop)
if ( has(prop, k) )
el.dataset[camelCase(k)] = prop[k];
if ( prop && typeof prop === 'object' ) {
for(const k in prop)
if ( has(prop, k) )
el.dataset[camelCase(k)] = prop[k];
} else
console.warn('unsupported dataset value', prop);
} else if ( key === 'dangerouslySetInnerHTML' ) {
// React compatibility is cool. SeemsGood

View file

@ -779,6 +779,12 @@ export function deep_copy<T>(object: T, seen?: Set<any>): T {
if ( typeof object === 'function' )
return function(this: ThisParameterType<T>, ...args: any[]) { return object.apply(this, args); } as T // eslint-disable-line no-invalid-this
if ( object instanceof RegExp )
return new RegExp(object.source, object.flags) as T;
if ( object instanceof Date )
return new Date(object) as T;
if ( typeof object !== 'object' )
return object as T;
@ -793,6 +799,30 @@ export function deep_copy<T>(object: T, seen?: Set<any>): T {
if ( Array.isArray(object) )
return object.map(x => deep_copy(x, new Set(seen))) as T;
if ( object instanceof Set ) {
const out = new Set<any>();
for(const item of object) {
if ( typeof item === 'object' )
out.add(deep_copy(item));
else
out.add(item);
}
return out as T;
}
if ( object instanceof Map ) {
const out = new Map<any, any>();
for(const [key, val] of object.entries()) {
let k = typeof key === 'object' ? deep_copy(key) : key,
v = typeof val === 'object' ? deep_copy(val) : val;
out.set(k, v);
}
return out as T;
}
const out: any = {};
for(const [key, val] of Object.entries(object)) {
if ( typeof val === 'object' )

View file

@ -914,11 +914,11 @@ export default class TwitchData extends Module {
f('id and login cannot both be null');
if ( ! this._loading_flags )
this._loadFlags();
this._loadStreamFlags();
})
}
async _loadFlags() {
async _loadStreamFlags() {
if ( this._loading_flags )
return;
@ -1015,7 +1015,7 @@ export default class TwitchData extends Module {
this._loading_flags = false;
if ( this._waiting_flag_ids.size || this._waiting_flag_logins.size )
this._loadFlags();
this._loadStreamFlags();
}

View file

@ -46,6 +46,10 @@
}
&--wrap { white-space: normal; }
&--pre { white-space: pre; }
&--prewrap { white-space: pre-wrap; }
&--20 { width: 20rem; }
&--30 { width: 30rem; }
&--left {
left: auto;
@ -142,4 +146,4 @@
top: -3px;
}
}
}
}