1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-28 21:48:31 +00:00

4.0.0-rc3. Display a warning for complex or invalid filtering terms, using the safe-regex NPM module. Add a separate Regex (Word) filtering mode. Fix channel hosting control. Fix hide extensions. Add a fix for bad local echo emote indices for chat. Position the color picker above rather than below for filter terms. Apply a dark theme to the color picker. Rewrite the filter terms editor to use the term editor component for adding a new term.

This commit is contained in:
SirStendec 2018-06-27 14:13:59 -04:00
parent 2a790ad7cd
commit 038270d232
23 changed files with 669 additions and 423 deletions

View file

@ -1,3 +1,14 @@
<div class="list-header">4.0.0-rc3<span>@440f1fb9360578cbde7b</span> <time datetime="2018-05-31">(2018-06-27)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Display a warning if filter terms are invalid or potentially complex.</li>
<li>Changed: Separate <code>Regex</code> and <code>Regex (Word)</code> term modes to allow matching regular expressions without your expression being wrapped in separators.</li>
<li>Fixed: Disable channel hosting now works again.</li>
<li>Fixed: Hide extensions now works again, though it isn't as necessary now that you can hide individual player extensions natively.</li>
<li>Fixed: Twitch's updated chat code not calculating emote positions in locally echoed messages correctly.</li>
<li>Fixed: Position the color picker above the control rather than below when creating highlight terms to avoid the control going out of the window.</li>
<li>Fixed: Apply the dark theme to the color picker.</li>
</ul>
<div class="list-header">4.0.0-rc2<span>@377f701926189263186b</span> <time datetime="2018-05-31">(2018-05-31)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Basic support for custom highlight terms and blocked terms in chat. This system will later be replaced with a more powerful chat filtering system.</li>

4
package-lock.json generated
View file

@ -7680,8 +7680,7 @@
"ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"dev": true
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
},
"right-align": {
"version": "0.1.3",
@ -7754,7 +7753,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
"dev": true,
"requires": {
"ret": "0.1.15"
}

View file

@ -58,6 +58,7 @@
"path-to-regexp": "^2.2.1",
"popper.js": "^1.14.3",
"raven-js": "^3.24.2",
"safe-regex": "^1.1.0",
"sortablejs": "^1.7.0",
"vue": "^2.5.16",
"vue-clickaway": "^2.2.2",

View file

@ -6,7 +6,7 @@
import Module from 'utilities/module';
import {createElement, ManagedStyle} from 'utilities/dom';
import {timeout, has, glob_to_regex, escape_regex} from 'utilities/object';
import {timeout, has, glob_to_regex, escape_regex, split_chars} from 'utilities/object';
import Badges from './badges';
import Emotes from './emotes';
@ -111,6 +111,7 @@ export default class Chat extends Module {
this.settings.add('chat.filtering.highlight-basic-terms', {
default: [],
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Chat > Filtering >> Highlight Terms',
component: 'basic-terms',
@ -129,30 +130,44 @@ export default class Chat extends Module {
const colors = new Map;
for(const item of val) {
let list;
const c = item.c || null,
t = item.t;
let v = item.v;
let v = item.v, word = true;
if ( t === 'glob' )
v = glob_to_regex(v);
else if ( t !== 'raw' )
else if ( t === 'raw' )
word = false;
else if ( t !== 'regex' )
v = escape_regex(v);
if ( ! v || ! v.length )
continue;
if ( colors.has(c) )
colors.get(c).push(v);
else
colors.set(c, [v]);
try {
new RegExp(v);
} catch(err) {
continue;
}
if ( colors.has(c) )
colors.get(c)[word ? 0 : 1].push(v);
else {
const vals = [[],[]];
colors.set(c, vals);
vals[word ? 0 : 1].push(v);
}
}
for(const [key, list] of colors)
colors.set(key, new RegExp(`\\b(${list.join('|')})\\b`, 'gi'));
for(const [key, list] of colors) {
if ( list[0].length )
list[1].push(`\\b(?:${list[0].join('|')})\\b`);
colors.set(key, new RegExp(list[1].join('|'), 'gi'));
}
return colors;
}
@ -162,6 +177,7 @@ export default class Chat extends Module {
this.settings.add('chat.filtering.highlight-basic-blocked', {
default: [],
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Chat > Filtering >> Blocked Terms',
component: 'basic-terms'
@ -176,28 +192,34 @@ export default class Chat extends Module {
if ( ! val || ! val.length )
return null;
const out = [];
const out = [[], []];
for(const item of val) {
const t = item.t;
let v = item.v;
let v = item.v, word = true;
if ( t === 'glob' )
v = glob_to_regex(v);
else if ( t !== 'raw' )
else if ( t === 'raw' )
word = false;
else if ( t !== 'regex' )
v = escape_regex(v);
if ( ! v || ! v.length )
continue;
out.push(v);
out[word ? 0 : 1].push(v);
}
if ( ! out.length )
if ( out[0].length )
out[1].push(`\\b(?:${out[0].join('|')})\\b`);
if ( ! out[1].length )
return;
return new RegExp(`\\b(${out.join('|')})\\b`, 'gi');
return new RegExp(out[1].join('|'), 'gi');
}
});
@ -621,7 +643,7 @@ export default class Chat extends Module {
ret = ret.slice(4);
}
idx += ret.length;
idx += split_chars(ret).length;
out.push(ret);
}
}

View file

@ -305,10 +305,10 @@ export const CustomHighlights = {
out.push({
type: 'highlight',
text: match[1]
text: match[0]
});
idx = nix + match[1].length;
idx = nix + match[0].length;
}
if ( idx < text.length )
@ -376,10 +376,10 @@ export const BlockedTerms = {
out.push({
type: 'blocked',
text: match[1]
text: match[0]
});
idx = nix + match[1].length;
idx = nix + match[0].length;
}
if ( idx < text.length )

View file

@ -1,38 +1,15 @@
<template lang="html">
<section class="ffz--widget ffz--basic-terms">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width tw-pd-b-1">
<div class="tw-flex-grow-1">
<input
v-model="new_term"
:placeholder="t('setting.terms.add-placeholder', 'Add a new term')"
type="text"
class="tw-input"
autocapitalize="off"
autocorrect="off"
>
</div>
<div v-if="item.colored" class="tw-flex-shrink-0 tw-mg-l-05">
<color-picker v-model="new_color" :nullable="true" :show-input="false" />
</div>
<div class="tw-flex-shrink-0 tw-mg-x-05">
<select v-model="new_type" class="tw-select ffz-min-width-unset">
<option value="text">{{ t('setting.terms.type.text', 'Text') }}</option>
<option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
<option value="glob">{{ t('setting.terms.type.glob', 'Glob') }}</option>
</select>
</div>
<div class="tw-flex-shrink-0">
<button class="tw-button" @click="add">
<span class="tw-button__text">
{{ t('setting.terms.add-term', 'Add') }}
</span>
</button>
</div>
</div>
<div v-if="! val.length || val.length === 1 && hasInheritance" class="tw-c-text-alt-2 tw-font-size-4 tw-align-center tw-c-text-alt-2 tw-pd-05">
<term-editor
:term="default_term"
:colored="item.colored"
:adding="true"
@save="new_term"
/>
<div v-if="! val.length || val.length === 1 && hasInheritance" class="tw-mg-t-05 tw-c-text-alt-2 tw-font-size-4 tw-align-center tw-c-text-alt-2 tw-pd-05">
{{ t('setting.terms.no-terms', 'no terms are defined in this profile') }}
</div>
<ul v-else class="ffz--term-list">
<ul v-else class="ffz--term-list tw-mg-t-05">
<term-editor
v-for="term in val"
v-if="term.t !== 'inherit'"
@ -59,9 +36,11 @@ export default {
data() {
return {
new_term: '',
new_type: 'text',
new_color: ''
default_term: {
v: '',
t: 'text',
c: ''
}
}
},
@ -84,17 +63,12 @@ export default {
},
methods: {
add() {
const vals = Array.from(this.val);
vals.push({
v: {
t: this.new_type,
v: this.new_term,
c: typeof this.new_color === 'string' ? this.new_color : null
}
});
new_term(term) {
if ( ! term.v )
return;
this.new_term = '';
const vals = Array.from(this.val);
vals.push({v: term});
this.set(deep_copy(vals));
},

View file

@ -38,7 +38,12 @@
</figure>
<figure v-else class="ffz-i-eyedropper" />
</button>
<div v-on-clickaway="closePicker" v-if="open" class="tw-absolute tw-z-default tw-right-0">
<div
v-on-clickaway="closePicker"
v-if="open"
:class="{'ffz-bottom-100': openUp}"
class="tw-absolute tw-z-default tw-balloon--up tw-balloon--right"
>
<chrome-picker :value="colors" @input="onPick" />
</div>
</div>
@ -74,6 +79,10 @@ export default {
default: true
},
showInput: {
type: Boolean,
default: true
},
openUp: {
type: Boolean,
default: false
}

View file

@ -1,6 +1,18 @@
<template lang="html">
<li class="ffz--term">
<div class="ffz--term">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width">
<div v-if="! is_valid" class="tw-tooltip-wrapper tw-mg-r-05">
<figure class="tw-c-text-error ffz-i-attention" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
{{ t('setting.terms.warn-invalid', 'This highlight term is invalid.') }}
</div>
</div>
<div v-if="! is_safe" class="tw-tooltip-wrapper tw-mg-r-05">
<figure class="tw-c-text-hint ffz-i-attention" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
{{ t('setting.terms.warn-complex', 'This highlight term is potentially too complex. It may cause client lag.') }}
</div>
</div>
<div class="tw-flex-grow-1">
<h4 v-if="! editing" class="ffz-monospace">
<pre>{{ term.v }}</pre>
@ -8,7 +20,7 @@
<input
v-else
v-model="edit_data.v"
:placeholder="edit_data.v"
:placeholder="adding ? t('setting.terms.add-placeholder', 'Add a new term') : edit_data.v"
type="text"
class="tw-input"
autocapitalize="off"
@ -16,7 +28,7 @@
>
</div>
<div v-if="colored" class="tw-flex-shrink-0 tw-mg-l-05">
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" />
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" :open-up="true" />
<div v-else-if="term.c" class="ffz-color-preview">
<figure :style="`background-color: ${term.c}`">
&nbsp;
@ -27,11 +39,19 @@
<span v-if="! editing">{{ term_type }}</span>
<select v-else v-model="edit_data.t" class="tw-select ffz-min-width-unset">
<option value="text">{{ t('setting.terms.type.text', 'Text') }}</option>
<option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
<option value="glob">{{ t('setting.terms.type.glob', 'Glob') }}</option>
<option value="regex">{{ t('setting.terms.type.regex-word', 'Regex (Word)') }}</option>
<option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
</select>
</div>
<div v-if="editing" class="tw-flex-shrink-0">
<div v-if="adding" class="tw-flex-shrink-0">
<button class="tw-button" @click="save">
<span class="tw-button__text">
{{ t('setting.terms.add-term', 'Add') }}
</span>
</button>
</div>
<div v-else-if="editing" class="tw-flex-shrink-0">
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="save">
<span class="tw-button__text ffz-i-floppy" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
@ -74,17 +94,36 @@
</button>
</div>
</div>
</li>
</div>
</template>
<script>
import {deep_copy} from 'utilities/object';
import safety from 'safe-regex';
import {deep_copy, glob_to_regex, escape_regex} from 'utilities/object';
export default {
props: ['term', 'colored'],
props: {
term: Object,
colored: {
type: Boolean,
default: false
},
adding: {
type: Boolean,
default: false
}
},
data() {
if ( this.adding )
return {
deleting: false,
editing: true,
edit_data: deep_copy(this.term)
};
return {
deleting: false,
editing: false,
@ -93,6 +132,41 @@ export default {
},
computed: {
is_valid() {
const data = this.editing ? this.edit_data : this.term,
t = data.t;
let v = data.v;
if ( t === 'text' )
v = escape_regex(v);
else if ( t === 'glob' )
v = glob_to_regex(v);
try {
new RegExp(v);
return true;
} catch(err) {
return false;
}
},
is_safe() {
const data = this.editing ? this.edit_data : this.term,
t = data.t;
let v = data.v;
if ( t === 'text' )
v = escape_regex(v);
else if ( t === 'glob' )
v = glob_to_regex(v);
return safety(v);
},
term_type() {
const t = this.term && this.term.t;
if ( t === 'text' )
@ -104,6 +178,9 @@ export default {
else if ( t === 'glob' )
return this.t('setting.terms.type.glob', 'Glob');
else if ( t === 'regex' )
return this.t('setting.terms.type.regex-word', 'Regex (Word)');
return this.t('setting.unknown', 'Unknown Value');
}
},
@ -115,8 +192,13 @@ export default {
},
cancel() {
if ( this.adding ) {
this.edit_data = deep_copy(this.term);
} else {
this.editing = false;
this.edit_data = null;
}
},
save() {

View file

@ -243,7 +243,7 @@ export default class SettingsContext extends EventEmitter {
value = def_default;
if ( type.default )
value = type.default(value);
value = type.default(value, definition, this.manager.log);
}
if ( definition.requires )
@ -269,9 +269,9 @@ export default class SettingsContext extends EventEmitter {
_getRaw(key, type) {
if ( ! type )
throw new Error(`non-existent `)
throw new Error(`non-existent type for ${key}`)
return type.get(key, this.profiles(), this.manager.log);
return type.get(key, this.profiles(), this.manager.definitions.get(key), this.manager.log);
}
/* for(const profile of this.__profiles)
if ( profile.has(key) )

View file

@ -52,7 +52,7 @@ export const array_merge = {
return values;
},
get(key, profiles, log) {
get(key, profiles, definition, log) {
const values = [],
trailing = [],
sources = [];
@ -80,7 +80,7 @@ export const array_merge = {
}
// If we didn't run into an inherit, don't inherit.
if ( ! is_trailing )
if ( ! is_trailing && ! definition.always_inherit )
break;
}

View file

@ -5,6 +5,7 @@
// ============================================================================
import Module from 'utilities/module';
import { has } from 'utilities/object';
export default class Channel extends Module {
@ -41,7 +42,7 @@ export default class Channel extends Module {
this.ChannelPage = this.fine.define(
'channel-page',
n => n.handleHostingChange,
n => n.hostModeFromGraphQL,
['user']
);
@ -70,10 +71,9 @@ export default class Channel extends Module {
// We can't do this immediately because the player state
// occasionally screws up if we do.
setTimeout(() => {
const current_channel = inst.props.data && inst.props.data.variables && inst.props.data.variables.currentChannelLogin;
if ( current_channel && current_channel !== inst.state.videoPlayerSource ) {
inst.ffzExpectedHost = inst.state.videoPlayerSource;
inst.ffzOldHostHandler(null);
if ( inst.state.hostMode ) {
inst.ffzExpectedHost = inst.state.hostMode;
inst.ffzOldSetState({hostMode: null});
}
});
});
@ -107,24 +107,32 @@ export default class Channel extends Module {
const t = this;
inst._ffz_hosting_wrapped = true;
inst.ffzOldHostHandler = inst.handleHostingChange;
inst.handleHostingChange = function(channel) {
inst.ffzExpectedHost = channel;
if ( t.settings.get('channel.hosting.enable') )
return inst.ffzOldHostHandler(channel);
inst.ffzOldSetState = inst.setState;
inst.setState = function(state, ...args) {
try {
if ( has(state, 'hostMode') ) {
inst.ffzExpectedHost = state.hostMode;
if ( state.hostMode && ! t.settings.get('channel.hosting.enable') ) {
state.hostMode = null;
state.videoPlayerSource = inst.props.match.params.channelName;
}
}
// Store the current state and disable the current host if needed.
inst.ffzExpectedHost = inst.state.isHosting ? inst.state.videoPlayerSource : null;
if ( ! this.settings.get('channel.hosting.enable') )
inst.ffzOldHostHandler(null);
} catch(err) {
t.log.capture(err, {extra: {props: inst.props, state}});
}
// Finally, we force an update so that any child components
// receive our updated handler.
inst.forceUpdate();
return inst.ffzOldSetState(state, ...args);
}
inst._ffz_hosting_wrapped = true;
const hosted = inst.ffzExpectedHost = inst.state.hostMode;
if ( hosted && ! this.settings.get('channel.hosting.enable') )
inst.ffzOldSetState({
hostMode: null,
videoPlayerSource: inst.props.match.params.channelName
});
}
@ -132,7 +140,14 @@ export default class Channel extends Module {
if ( val === undefined )
val = this.settings.get('channel.hosting.enable');
for(const inst of this.ChannelPage.instances)
inst.ffzOldHostHandler(val ? inst.ffzExpectedHost : null);
for(const inst of this.ChannelPage.instances) {
const host = val ? inst.ffzExpectedHost : null,
target = host && host.hostedChannel && host.hostedChannel.login || inst.props.match.params.channelName;
inst.ffzOldSetState({
hostMode: host,
videoPlayerSource: target
});
}
}
}

View file

@ -566,6 +566,25 @@ export default class ChatHook extends Module {
}
cls.prototype.ffzGetEmotes = function() {
const emote_sets = this.client && this.client.session && this.client.session.emoteSets;
if ( this._ffz_cached_sets === emote_sets )
return this._ffz_cached_emotes;
this._ffz_cached_sets = emote_sets;
const emotes = this._ffz_cached_emotes = {};
if ( emote_sets )
for(const set of emote_sets)
if ( set && set.emotes )
for(const emote of set.emotes)
if ( emote )
emotes[emote.token] = emote.id;
return emotes;
}
cls.prototype.connectHandlers = function(...args) {
if ( ! this._ffz_init ) {
const i = this;
@ -581,6 +600,42 @@ export default class ChatHook extends Module {
}
}
const old_chat = this.onChatMessageEvent;
this.onChatMessageEvent = function(e) {
if ( e && e.sentByCurrentUser ) {
try {
e.message.user.emotes = findEmotes(
e.message.body,
i.ffzGetEmotes()
);
} catch(err) {
t.log.capture(err, {extra: e});
}
}
return old_chat.call(i, e);
}
const old_action = this.onChatActionEvent;
this.onChatActionEvent = function(e) {
if ( e && e.sentByCurrentUser ) {
try {
e.message.user.emotes = findEmotes(
e.message.body.slice(8, -1),
i.ffzGetEmotes()
);
} catch(err) {
t.log.capture(err, {extra: e});
}
}
return old_action.call(i, e);
}
const old_resub = this.onResubscriptionEvent;
this.onResubscriptionEvent = function(e) {
try {
@ -626,11 +681,11 @@ export default class ChatHook extends Module {
const old_post = this.postMessage;
this.postMessage = function(e) {
const original = this._wrapped;
const original = i._wrapped;
if ( original && ! e._ffz_checked )
return this.postMessageToCurrentChannel(original, e);
return i.postMessageToCurrentChannel(original, e);
return old_post.call(this, e);
return old_post.call(i, e);
}
this._ffz_init = true;
@ -662,11 +717,6 @@ export default class ChatHook extends Module {
message.message = original.action;
else
message.message = original.message.body;
// Twitch doesn't generate a proper emote tag for echoed back
// actions, so we have to regenerate it. Fun. :D
if ( user && user.username === this.userLogin )
message.emotes = findEmotes(message.message, this.selfEmotes);
}
this.postMessage(message);
@ -903,12 +953,14 @@ export function findEmotes(msg, emotes) {
const out = {};
let idx = 0;
console.log('findEmotes', msg, emotes);
for(const part of msg.split(' ')) {
const len = split_chars(part).length;
if ( has(emotes, part) ) {
const em = emotes[part],
matches = out[em.id] = out[em.id] || [];
matches = out[em] = out[em] || [];
matches.push({
startIndex: idx,

View file

@ -244,15 +244,17 @@ export default class ChatLine extends Module {
e('span', {
className: 'chat-line__message--badges'
}, t.chat.badges.render(msg, e)),
e('a', {
className: 'chat-author__display-name notranslate',
e('button', {
className: 'chat-line__username notranslate',
style: { color },
onClick: this.usernameClickHandler, // this.ffz_user_click_handler
}, [
user.userDisplayName,
e('span', {
className: 'chat-author__display-name'
}, user.displayName),
user.isIntl && e('span', {
className: 'chat-author__intl-login'
}, ` (${user.userLogin})`)
}, ` (${user.login})`)
]),
e('span', null, is_action ? ' ' : ': '),
show ?

View file

@ -19,8 +19,8 @@ const CLASSES = {
'prime-offers': '.top-nav__prime',
'player-ext': '.player .extension-overlay',
'player-ext-hover': '.player:not([data-controls="true"]) .extension-overlay',
'player-ext': '.player .extension-taskbar,.player .extension-container',
'player-ext-hover': '.player:not([data-controls="true"]) .extension-container',
'player-event-bar': '.channel-page .live-event-banner-ui__header',
'player-rerun-bar': '.channel-page div.tw-c-text-overlay:not([data-a-target="hosting-ui-header"])',
@ -28,11 +28,8 @@ const CLASSES = {
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2',
'whispers': '.whispers',
'dir-live-ind': '.live-channel-card:not([data-a-target*="host"]) .stream-type-indicator.stream-type-indicator--live',
'boxart-hover': '.tw-card .tw-full-width:hover a[data-a-target="live-channel-card-game-link"]',
'boxart-hide': '.tw-card a[data-a-target="live-channel-card-game-link"]',
'profile-hover-following': '.tw-card .tw-full-width:hover .ffz-channel-avatar',
'profile-hover-game': '.tw-thumbnail-card .tw-card-img:hover .ffz-channel-avatar',
'dir-live-ind': '.live-channel-card:not([data-a-target*="host"]) .stream-type-indicator.stream-type-indicator--live,.stream-thumbnail__card .stream-type-indicator.stream-type-indicator--live',
'profile-hover': '.preview-card .tw-relative:hover .ffz-channel-avatar',
};

View file

@ -18,79 +18,25 @@ export default class BrowsePopular extends SiteModule {
this.inject('settings');
this.apollo.registerModifier('BrowsePage_Popular', BROWSE_POPULAR);
this.ChannelCard = this.fine.define(
'browse-all-channel-card',
n => n.props && n.props.channelName && n.props.linkTo && n.props.linkTo.state && n.props.linkTo.state.medium === 'twitch_browse_directory',
['dir-all']
);
this.apollo.registerModifier('BrowsePage_Popular', res => this.modifyStreams(res), false);
}
onEnable() {
this.ChannelCard.ready((cls, instances) => {
// Popular Directory Channel Cards
this.apollo.ensureQuery(
'BrowsePage_Popular',
'data.streams.edges.node.0.createdAt'
);
for(const inst of instances) this.updateChannelCard(inst);
});
this.ChannelCard.on('update', this.updateChannelCard, this);
this.ChannelCard.on('mount', this.updateChannelCard, this);
this.ChannelCard.on('unmount', this.parent.clearUptime, this);
}
modifyStreams(res) { // eslint-disable-line class-methods-use-this
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
const newStreams = [];
this.log.info('Streams', res);
const edges = get('data.streams.edges', res);
if (!edges) return res;
if ( ! edges || ! edges.length )
return res;
for (let i = 0; i < edges.length; i++) {
const edge = edges[i];
const node = edge.node;
const s = node.viewersCount = new Number(node.viewersCount || 0);
s.profileImageURL = node.broadcaster.profileImageURL;
s.createdAt = node.createdAt;
s.login = node.broadcaster.login;
s.displayName = node.broadcaster.displayName;
if (!node.game || node.game && !blockedGames.includes(node.game.name)) newStreams.push(edge);
}
res.data.streams.edges = newStreams;
res.data.streams.edges = this.parent.processNodes(edges);
return res;
}
updateChannelCard(inst) {
const container = this.fine.getChildNode(inst);
if (!container) return;
if (container.classList.contains('ffz-modified-channel-card')) return;
container.classList.add('ffz-modified-channel-card');
this.parent.updateUptime(inst, 'props.viewerCount.createdAt', '.tw-card-img');
this.parent.addCardAvatar(inst, 'props.viewerCount', '.tw-card');
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
if (inst.props.type === 'watch_party')
container.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts'));
const img = container.querySelector && container.querySelector('.tw-card-img img');
if (img == null) return;
if (hiddenThumbnails.includes(inst.props.gameTitle)) {
img.src = hiddenPreview;
} else {
img.src = inst.props.imageSrc;
}
}
}

View file

@ -68,102 +68,66 @@ export default class Following extends SiteModule {
this.apollo.registerModifier('FollowingHosts_CurrentUser', FOLLOWED_HOSTS);
this.apollo.registerModifier('FollowedChannels', FOLLOWED_CHANNELS);
this.ChannelCard = this.fine.define(
'following-channel-card',
n => n.renderGameBoxArt && n.renderContentType,
['dir-following']
);
this.apollo.registerModifier('FollowedIndex_CurrentUser', res => {
this.modifyLiveUsers(res);
this.modifyLiveHosts(res);
}, false);
this.on('settings:changed:directory.uptime', () => this.ChannelCard.forceUpdate());
this.on('settings:changed:directory.show-channel-avatars', () => this.ChannelCard.forceUpdate());
this.on('settings:changed:directory.show-boxart', () => this.ChannelCard.forceUpdate());
this.on('settings:changed:directory.hide-vodcasts', () => this.ChannelCard.forceUpdate());
this.apollo.registerModifier('FollowedChannels', res => this.modifyLiveUsers(res), false);
this.apollo.registerModifier('FollowingLive_CurrentUser', res => this.modifyLiveUsers(res), false);
this.apollo.registerModifier('FollowingHosts_CurrentUser', res => this.modifyLiveHosts(res), false);
}
modifyLiveUsers(res) {
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
const newStreams = [];
const followedLiveUsers = get('data.currentUser.followedLiveUsers', res);
if (!followedLiveUsers)
const edges = get('data.currentUser.followedLiveUsers.nodes', res);
if ( ! edges || ! edges.length )
return res;
const oldMode = !!followedLiveUsers.nodes;
const edgesOrNodes = followedLiveUsers.nodes || followedLiveUsers.edges;
for (let i = 0; i < edgesOrNodes.length; i++) {
const edge = edgesOrNodes[i],
node = edge.node || edge;
if ( ! node || ! node.stream )
continue;
const s = node.stream.viewersCount = new Number(node.stream.viewersCount || 0);
s.profileImageURL = node.profileImageURL;
s.createdAt = node.stream.createdAt;
if (node.stream.game && hiddenThumbnails.includes(node.stream.game.name)) node.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
if (!node.stream.game || node.stream.game && !blockedGames.includes(node.stream.game.name)) newStreams.push(edge);
}
res.data.currentUser.followedLiveUsers[oldMode ? 'nodes' : 'edges'] = newStreams;
res.data.currentUser.followedLiveUsers.nodes = this.parent.processNodes(edges);
return res;
}
modifyLiveHosts(res) {
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
const blocked_games = this.settings.provider.get('directory.game.blocked-games', []),
do_grouping = this.settings.get('directory.following.group-hosts'),
edges = get('data.currentUser.followedHosts.nodes', res);
this.hosts = {};
const followedHosts = get('data.currentUser.followedHosts', res);
if (!followedHosts)
if ( ! edges || ! edges.length )
return res;
const newHostNodes = [];
const hosts = {},
out = [];
const oldMode = !!followedHosts.nodes;
const edgesOrNodes = followedHosts.nodes || followedHosts.edges;
for(const edge of edges) {
const node = edge.node || edge,
hosted = node.hosting,
stream = hosted && hosted.stream;
for (let i = 0; i < edgesOrNodes.length; i++) {
const edge = edgesOrNodes[i],
node = edge.node || edge;
if ( ! node || ! node.hosting || ! node.hosting.stream )
if ( ! stream || stream.game && blocked_games.includes(stream.game.game) )
continue;
const s = node.hosting.stream.viewersCount = new Number(node.hosting.stream.viewersCount || 0);
s.profileImageURL = node.hosting.profileImageURL;
s.createdAt = node.hosting.stream.createdAt;
const store = {}; // stream.viewersCount = new Number(stream.viewersCount || 0);
store.createdAt = stream.createdAt;
store.title = stream.title;
store.game = stream.game;
if ( do_grouping ) {
const host_nodes = hosts[hosted.login];
if ( host_nodes ) {
host_nodes.push(node);
store.host_nodes = node._ffz_host_nodes = host_nodes;
if (!this.hosts[node.hosting.displayName]) {
this.hosts[node.hosting.displayName] = {
channel: node.hosting.login,
nodes: [node],
channels: [node.displayName]
};
if (node.hosting.stream.game && hiddenThumbnails.includes(node.hosting.stream.game.name)) node.hosting.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
if (!node.hosting.stream.game || node.hosting.stream.game && !blockedGames.includes(node.hosting.stream.game.name)) newHostNodes.push(edge);
} else {
this.hosts[node.hosting.displayName].nodes.push(node);
this.hosts[node.hosting.displayName].channels.push(node.displayName);
}
store.host_nodes = node._ffz_host_nodes = hosts[hosted.login] = [node];
out.push(edge);
}
if (this.settings.get('directory.following.group-hosts')) {
res.data.currentUser.followedHosts[oldMode ? 'nodes' : 'edges'] = newHostNodes;
} else
out.push(edge);
}
res.data.currentUser.followedHosts.nodes = out;
return res;
}
@ -200,19 +164,8 @@ export default class Following extends SiteModule {
}
onEnable() {
this.ChannelCard.ready((cls, instances) => {
this.ensureQueries();
for(const inst of instances) this.updateChannelCard(inst);
});
this.ChannelCard.on('update', inst => {
this.ensureQueries();
this.updateChannelCard(inst)
}, this);
this.ChannelCard.on('mount', this.updateChannelCard, this);
this.ChannelCard.on('unmount', this.parent.clearUptime, this);
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
}

View file

@ -30,7 +30,8 @@ export default class Game extends SiteModule {
onEnable() {
this.GameHeader.ready((cls, instances) => {
for(const inst of instances) this.updateButtons(inst);
for(const inst of instances)
this.updateButtons(inst);
});
this.GameHeader.on('update', this.updateButtons, this);
@ -85,7 +86,7 @@ export default class Game extends SiteModule {
this.i18n.t('directory.show-thumbnails', 'Show Thumbnails') :
this.i18n.t('directory.hide-thumbnails', 'Hide Thumbnails');
this.parent.ChannelCard.forceUpdate();
this.parent.DirectoryCard.forceUpdate();
}
hidden_btn = (<button

View file

@ -13,6 +13,18 @@ import Following from './following';
import Game from './game';
import BrowsePopular from './browse_popular';
export const CARD_CONTEXTS = ((e ={}) => {
e[e.SingleGameList = 1] = 'SingleGameList';
e[e.SingleChannelList = 2] = 'SingleChannelList';
e[e.MixedGameAndChannelList = 3] = 'MixedGameAndChannelList';
return e;
})();
const DIR_ROUTES = ['dir', 'dir-community', 'dir-community-index', 'dir-creative', 'dir-following', 'dir-game-index', 'dir-game-clips', 'dir-game-videos', 'dir-all', 'dir-category', 'user-videos', 'user-clips'];
export default class Directory extends SiteModule {
constructor(...args) {
super(...args);
@ -23,6 +35,7 @@ export default class Directory extends SiteModule {
this.inject('site.router');
this.inject('site.apollo');
this.inject('site.css_tweaks');
this.inject('site.web_munch');
this.inject('i18n');
this.inject('settings');
@ -33,12 +46,17 @@ export default class Directory extends SiteModule {
this.apollo.registerModifier('GamePage_Game', res => this.modifyStreams(res), false);
this.ChannelCard = this.fine.define(
'channel-card',
n => n.props && n.props.streamNode,
['dir-community', 'dir-game-index']
this.DirectoryCard = this.fine.define(
'directory-card',
n => n.renderTitles && n.renderIconicImage,
DIR_ROUTES
);
this.CardWrapper = this.fine.define(
'directory-card-wrapper',
n => n.renderFallback && n.renderStreamFlag,
DIR_ROUTES
);
this.settings.add('directory.uptime', {
default: 1,
@ -56,12 +74,12 @@ export default class Directory extends SiteModule {
]
},
changed: () => this.ChannelCard.forceUpdate()
changed: () => this.DirectoryCard.forceUpdate()
});
this.settings.add('directory.show-channel-avatars', {
default: 0,
default: 1,
ui: {
path: 'Directory > Channels >> Appearance',
@ -78,33 +96,8 @@ export default class Directory extends SiteModule {
},
changed: value => {
this.css_tweaks.toggleHide('profile-hover-following', value === 2);
this.css_tweaks.toggleHide('profile-hover-game', value === 2);
this.ChannelCard.forceUpdate();
}
});
this.settings.add('directory.show-boxart', {
default: 2,
ui: {
path: 'Directory > Channels >> Appearance',
title: 'Show Boxart',
description: 'Display boxart over stream and video thumbnails.',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'Hidden on Hover'},
{value: 2, title: 'Always'}
]
},
changed: value => {
this.css_tweaks.toggleHide('boxart-hide', value === 0);
this.css_tweaks.toggleHide('boxart-hover', value === 1);
this.ChannelCard.forceUpdate();
this.css_tweaks.toggleHide('profile-hover', value === 2);
this.DirectoryCard.forceUpdate();
}
});
@ -130,89 +123,149 @@ export default class Directory extends SiteModule {
component: 'setting-check-box'
},
changed: () => this.ChannelCard.forceUpdate()
changed: () => this.CardWrapper.forceUpdate()
});
}
onEnable() {
const avatars = this.settings.get('directory.show-channel-avatars'),
boxart = this.settings.get('directory.show-boxart');
this.css_tweaks.toggleHide('profile-hover-game', avatars === 2);
this.css_tweaks.toggleHide('profile-hover-following', avatars === 2);
async onEnable() {
this.css_tweaks.toggleHide('profile-hover', this.settings.get('directory.show-channel-avatars') === 2);
this.css_tweaks.toggleHide('dir-live-ind', this.settings.get('directory.hide-live'));
this.css_tweaks.toggleHide('boxart-hide', boxart === 0);
this.css_tweaks.toggleHide('boxart-hover', boxart === 1);
this.ChannelCard.ready((cls, instances) => {
const t = this,
React = await this.web_munch.findModule('react');
const createElement = React && React.createElement;
this.CardWrapper.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
if ( get('props.streamNode.type', this) === 'rerun' && t.settings.get('directory.hide-vodcasts') )
return null;
return old_render.call(this);
}
this.CardWrapper.forceUpdate();
});
this.DirectoryCard.ready(cls => {
const old_render_iconic = cls.prototype.renderIconicImage,
old_render_titles = cls.prototype.renderTitles;
cls.prototype.renderIconicImage = function() {
if ( this.props.context !== CARD_CONTEXTS.SingleChannelList &&
t.settings.get('directory.show-channel-avatars') !== 1 )
return;
return old_render_iconic.call(this);
}
cls.prototype.renderTitles = function() {
const nodes = get('props.currentViewerCount.host_nodes', this);
if ( this.props.hostedByChannelLogin == null || ! nodes || ! nodes.length )
return old_render_titles.call(this);
const channel = nodes[0].hosting,
stream = channel.stream;
return (<div>
<a class="tw-link tw-link--inherit" data-test-selector="preview-card-titles__primary-link">
<h3 class="tw-ellipsis tw-font-size-5 tw-strong" title={stream.title}>{stream.title}</h3>
</a>
<div class="preview-card-titles__subtitle-wrapper">
<div data-test-selector="preview-card-titles__subtitle">
<p class="tw-c-text-alt tw-ellipsis">
<a class="tw-link tw-link--inherit">{channel.displayName}</a> playing <a class="tw-link tw-link--inherit">{stream.game.name}</a>
</p>
</div>
<div data-test-selector="preview-card-titles__subtitle">
<p class="tw-c-text-alt tw-ellipsis">
Hosted by {nodes.length > 1 ? `${nodes.length} channels` : nodes[0].displayName}
</p>
</div>
</div>
</div>);
}
this.DirectoryCard.forceUpdate();
// Game Directory Channel Cards
// TODO: Better query handling.
this.apollo.ensureQuery(
'GamePage_Game',
'data.directory.streams.edges.0.node.createdAt'
);
for(const inst of instances) this.updateChannelCard(inst);
//for(const inst of instances)
// this.updateCard(inst);
});
this.ChannelCard.on('update', this.updateChannelCard, this);
this.ChannelCard.on('mount', this.updateChannelCard, this);
this.ChannelCard.on('unmount', this.clearUptime, this);
this.DirectoryCard.on('update', this.updateCard, this);
this.DirectoryCard.on('mount', this.updateCard, this);
this.DirectoryCard.on('unmount', this.clearCard, this);
// TODO: Queries
}
updateChannelCard(inst) {
updateCard(inst) {
const container = this.fine.getChildNode(inst);
if (!container) return;
if ( ! container )
return;
this.updateUptime(inst, 'props.streamNode.viewersCount.createdAt', '.tw-card-img');
this.addCardAvatar(inst, 'props.streamNode.viewersCount', '.tw-card');
const props = inst.props,
game = props.gameTitle || props.playerMetadataGame,
is_video = props.durationInSeconds != null,
is_host = props.hostedByChannelLogin != null;
const type = get('props.directoryType', inst);
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
container.classList.toggle('ffz-hide-thumbnail', this.settings.provider.get('directory.game.hidden-thumbnails', []).includes(game));
if (get('props.streamNode.type', inst) === 'rerun' || get('props.type', inst) === 'rerun')
container.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts'));
//this.log.info('Card Update', inst.props.channelDisplayName, is_video ? 'Video' : 'Live', is_host ? 'Host' : 'Not-Host', inst);
const img = container.querySelector && container.querySelector('.tw-card-img img');
if (img == null) return;
if (type === 'GAMES' && hiddenThumbnails.includes(get('props.directoryName', inst)) ||
type === 'COMMUNITIES' && hiddenThumbnails.includes(get('props.streamNode.game.name', inst))) {
img.src = hiddenPreview;
} else {
img.src = get('props.streamNode.previewImageURL', inst) || get('props.imageSrc', inst);
this.updateUptime(inst, 'props.currentViewerCount.createdAt');
this.updateAvatar(inst);
}
clearCard(inst) {
this.clearUptime(inst);
}
processNodes(edges, is_game_query = false, blocked_games) {
const out = [];
if ( blocked_games === undefined )
blocked_games = this.settings.provider.get('directory.game.blocked-games', []);
for(const edge of edges) {
const node = edge.node || edge,
store = {}; // node.viewersCount = new Number(node.viewersCount || 0);
store.createdAt = node.createdAt;
store.title = node.title;
store.game = node.game;
if ( is_game_query || (! node.game || node.game && ! blocked_games.includes(node.game.game)) )
out.push(edge);
}
return out;
}
modifyStreams(res) { // eslint-disable-line class-methods-use-this
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
const gamePage = get('data.directory.__typename', res) === 'Game';
this.log.info('Modify Streams', res);
const newStreams = [];
const is_game_query = get('data.directory.__typename', res) === 'Game',
edges = get('data.directory.streams.edges', res);
const edges = get('data.directory.streams.edges', res);
if (!edges) return res;
if ( ! edges || ! edges.length )
return res;
for (let i = 0; i < edges.length; i++) {
const edge = edges[i],
node = edge.node || edge;
const s = node.viewersCount = new Number(node.viewersCount || 0);
s.createdAt = node.createdAt;
if ( node.broadcaster ) {
s.profileImageURL = node.broadcaster.profileImageURL;
s.login = node.broadcaster.login;
s.displayName = node.broadcaster.displayName;
}
if (gamePage || (!node.game || node.game && !blockedGames.includes(node.game.name))) newStreams.push(edge);
}
res.data.directory.streams.edges = newStreams;
res.data.directory.streams.edges = this.processNodes(edges, is_game_query);
return res;
}
@ -233,9 +286,9 @@ export default class Directory extends SiteModule {
}
updateUptime(inst, created_path, selector) {
updateUptime(inst, created_path) {
const container = this.fine.getChildNode(inst),
card = container && container.querySelector && container.querySelector(selector),
card = container && container.querySelector && container.querySelector('.preview-card-overlay'),
setting = this.settings.get('directory.uptime'),
created_at = get(created_path, inst),
up_since = created_at && new Date(created_at),
@ -246,100 +299,93 @@ export default class Directory extends SiteModule {
const up_text = duration_to_string(uptime, false, false, false, setting === 1);
if ( ! inst.ffz_uptime_el || card.querySelector('.ffz-uptime-element') === undefined ) {
card.appendChild(inst.ffz_uptime_el = (<div class="video-preview-card__preview-overlay-stat tw-c-background-overlay tw-c-text-overlay tw-font-size-6 tw-top-0 tw-right-0 tw-z-default tw-inline-flex tw-absolute tw-mg-05 ffz-uptime-element">
<div class="tw-tooltip-wrapper tw-inline-flex">
<div class="tw-stat">
<span class="tw-c-text-live tw-stat__icon">
if ( ! inst.ffz_uptime_el ) {
inst.ffz_uptime_el = card.querySelector('.ffz-uptime-element');
if ( ! inst.ffz_uptime_el )
card.appendChild(inst.ffz_uptime_el = (<div class="ffz-uptime-element tw-absolute tw-right-0 tw-top-0 tw-mg-1">
<div class="tw-tooltip-wrapper">
<div class="preview-card-stat tw-align-items-center tw-border-radi-us-small tw-c-background-overlay tw-c-text-overlay tw-flex tw-font-size-6 tw-justify-content-center">
<div class="tw-flex tw-c-text-live">
<figure class="ffz-i-clock" />
</span>
{inst.ffz_uptime_span = <span class="tw-stat__value" />}
</div>
{inst.ffz_uptime_tt = <div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center" />}
{inst.ffz_uptime_span = <p />}
</div>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center">
{this.i18n.t('metadata.uptime.tooltip', 'Stream Uptime')}
{inst.ffz_uptime_tt = <div class="tw-pd-t-05" />}
</div>
</div>
</div>));
}
if ( ! inst.ffz_update_timer )
inst.ffz_update_timer = setInterval(this.updateUptime.bind(this, inst, created_path, selector), 1000);
inst.ffz_update_timer = setInterval(this.updateUptime.bind(this, inst, created_path), 1000);
inst.ffz_uptime_span.textContent = up_text;
if ( inst.ffz_last_created_at !== created_at ) {
inst.ffz_uptime_tt.innerHTML = `${this.i18n.t(
'metadata.uptime.tooltip',
'Stream Uptime'
)}<div class="pd-t-05">${this.i18n.t(
inst.ffz_uptime_tt.textContent = this.i18n.t(
'metadata.uptime.since',
'(since %{since})',
{since: up_since.toLocaleString()}
)}</div>`;
);
inst.ffz_last_created_at = created_at;
}
}
addCardAvatar(inst, created_path, selector, data) {
updateAvatar(inst) {
const container = this.fine.getChildNode(inst),
card = container && container.querySelector && container.querySelector(selector),
card = container && container.querySelector && container.querySelector('.preview-card-overlay'),
setting = this.settings.get('directory.show-channel-avatars');
if ( ! data )
data = get(created_path, inst);
if ( ! card )
return;
// Get the old element.
const channel_avatar = card.querySelector('.ffz-channel-avatar');
const props = inst.props,
is_video = props.durationInSeconds != null,
src = props.channelImageProps && props.channelImageProps.src;
if ( ! data || ! data.profileImageURL || setting === 0 ) {
if ( channel_avatar !== null )
channel_avatar.remove();
const avatar = card.querySelector('.ffz-channel-avatar');
if ( ! src || setting < 2 || props.context === CARD_CONTEXTS.SingleChannelList ) {
if ( avatar )
avatar.remove();
return;
}
if ( setting !== inst.ffz_av_setting || data.login !== inst.ffz_av_login || data.profileImageURL !== inst.ffz_av_image ) {
if ( channel_avatar )
channel_avatar.remove();
if ( setting === inst.ffz_av_setting && props.channelLogin === inst.ffz_av_login && src === inst.ffz_av_src )
return;
if ( avatar )
avatar.remove();
inst.ffz_av_setting = setting;
inst.ffz_av_login = data.login;
inst.ffz_av_image = data.profileImageURL;
inst.ffz_av_login = props.channelLogin;
inst.ffz_av_src = src;
if ( setting === 1 ) {
const body = card.querySelector('.tw-card-body .tw-flex'),
avatar = (<a
class="ffz-channel-avatar tw-mg-r-05 tw-mg-t-05"
href={`/${data.login}`}
title={data.displayName}
onClick={e => this.hijackUserClick(e, data.login)} // eslint-disable-line react/jsx-no-bind
>
<img src={data.profileImageURL} />
</a>);
body.insertBefore(avatar, body.firstElementChild);
} else if ( setting === 2 || setting === 3 ) {
const avatar_el = (<a
card.appendChild(<a
class="ffz-channel-avatar"
href={`/${data.login}`}
onClick={e => this.hijackUserClick(e, data.login)} // eslint-disable-line react/jsx-no-bind
href={props.channelLinkTo && props.channelLinkTo.pathname}
onClick={e => this.routeClick(e, props.channelLinkTo)} // eslint-disable-line react/jsx-no-bind
>
<div class="live-channel-card__boxart tw-bottom-0 tw-absolute">
<div class={`tw-absolute tw-right-0 tw-border-l tw-c-background ${is_video ? 'tw-top-0 tw-border-b' : 'tw-bottom-0 tw-border-t'}`}>
<figure class="tw-aspect tw-aspect--align-top">
<img src={data.profileImageURL} title={data.displayName} />
<img src={src} title={props.channelDisplayName} />
</figure>
</div>
</a>);
}
const cont = card.querySelector('figure.tw-aspect > div');
if ( cont )
cont.appendChild(avatar_el);
}
}
routeClick(event, route) {
event.preventDefault();
event.stopPropagation();
if ( route && route.pathname )
this.router.history.push(route.pathname);
}
@ -347,7 +393,8 @@ export default class Directory extends SiteModule {
event.preventDefault();
event.stopPropagation();
if (optionalFn) optionalFn();
if ( optionalFn )
optionalFn(event, user);
this.router.navigate('user', { userName: user });
}

View file

@ -39,3 +39,11 @@
.ffz-stat-arrow {
border-left: none !important;
}
.ffz-auto-host-options {
.ffz-channel-avatar {
max-width: 3.2rem;
max-height: 3.2rem;
}
}

View file

@ -106,6 +106,11 @@
}
.chat-line__username:hover {
text-decoration: underline;
}
.ffz--emote-picker {
section:not(.filtered) heading {
cursor: pointer;

View file

@ -1,13 +1,14 @@
.ffz-channel-avatar {
flex-shrink: 0 !important;
.tw-aspect {
width: 4rem;
img {
width: 4rem;
height: 4rem
}
}
.ffz-channel-avatar,
.ffz-uptime-element {
pointer-events: all;
}
// TODO: Color variables
@ -27,6 +28,25 @@
}
.ffz-hide-thumbnail {
.preview-card-thumbnail__image {
&:before {
content: '';
position: absolute;
top: 0; left: 0;
height: 100%;
width: 100%;
background: url("https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg") no-repeat;
background-size: cover;
}
img {
display: none;
}
}
}
.ffz-host-menu {
.scrollable-area {
max-height: 25vh;

View file

@ -7,6 +7,8 @@
@import "./widgets/profile-selector.scss";
@import "./widgets/badge-visibility.scss";
@import "./widgets/color-picker.scss";
.tw-display-inline { display: inline !important }
.tw-width-auto { width: auto !important }
@ -16,6 +18,10 @@
font-family: monospace;
}
.ffz-bottom-100 {
bottom: 100%;
}
.ffz--widget {
input, select {
@ -55,6 +61,7 @@
figure {
width: 3rem;
margin: .4rem;
text-align: center;
height: calc(100% - .8rem);
}

View file

@ -0,0 +1,96 @@
$color: #dad8de;
$bg-color: #17141f;
$border-color: #2c2541;
$input-color: #faf9fa;
$input-bg: #0e0c13;
$input-border: #392e5c;
$input-active-border: #7d5bbe;
.tw-theme--dark {
.vc-sketch {
background: $bg-color;
box-shadow: 0 0 0 1px $border-color,
0 8px 16px rgba($border-color, .15);
}
.vc-sketch-active-color {
box-shadow: inset 0 0 0 1px $border-color,
inset 0 0 4px rgba($border-color, .25);
}
.vc-sketch-field {
.vc-input__input {
background: $input-bg;
color: $input-color;
box-shadow: inset 0 0 0 1px $input-border;
&:focus {
box-shadow: inset 0 0 0 1px $input-active-border,
0 0 6px -2px $input-active-border;
}
}
.vc-input__label {
color: $color;
}
}
.vc-sketch-presets {
border-top-color: $border-color;
}
.vc-sketch-presets-color {
box-shadow: inset 0 0 0 1px $border-color;
}
}
$color: var(--ffz-color-1);
$bg-color: var(--ffz-color-21);
$border-color: var(--ffz-color-20);
$input-color: var(--ffz-color-23);
$input-bg: var(--ffz-color-27);
$input-border: var(--ffz-color-26);
$input-active-border: var(--ffz-color-5);
.tw-theme--dark.tw-theme--ffz {
.vc-sketch {
background: $bg-color;
box-shadow: 0 0 0 1px $border-color,
0 8px 16px var(--ffz-color-37);
}
.vc-sketch-active-color {
box-shadow: inset 0 0 0 1px $border-color,
inset 0 0 4px var(--ffz-color-37);
}
.vc-sketch-field {
.vc-input__input {
background: $input-bg;
color: $input-color;
box-shadow: inset 0 0 0 1px $input-border;
&:focus {
box-shadow: inset 0 0 0 1px $input-active-border,
0 0 6px -2px $input-active-border;
}
}
.vc-input__label {
color: $color;
}
}
.vc-sketch-presets {
border-top-color: $border-color;
}
.vc-sketch-presets-color {
box-shadow: inset 0 0 0 1px $border-color;
}
}