1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
Nice.

* Changed: Warn users that they have `Show Mod Icons` disabled within [Chat > Actions](~chat.actions).
* Changed: Blocked Badges, Highlight Badges, and Blocked Types within [Chat > Filtering](~chat.filtering) no longer have a default item. This will hopefully minimize user confusion.
* Changed: Blocked Badges also has a new description telling users that it isn't for hiding badges, with a link to the correct place to change badge visibility.
* Changed: Remove the New Link Tokenization experiment, making it enabled for all users.
* Changed: When navigating within the FFZ Control Center in a pop-out window, update the URL so that it can be shared to link to a specific settings page.
* Changed: Disable the websocket connection for users in the API Links experiment to reduce load on the socket cluster.
* Fixed: Bug with the FFZ Control Center failing to load if experiments haven't been populated correctly.
* Fixed: Badge Visibility not being populated when opening the FFZ Control Center on a page without chat.
* API Added: `<markdown />` now supports a link syntax for navigating to a new section of the FFZ Control Center.
* API Fixed: Better tokenization for settings paths. Brackets can now be used safely in embedded JSON.
* API Fixed: `deep_equals` and `shallow_object_equals` returning false when objects were otherwise equal but had keys in a different order.
This commit is contained in:
SirStendec 2021-02-22 20:11:35 -05:00
parent f5135ad291
commit 77d6cf56d2
27 changed files with 365 additions and 96 deletions

View file

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

View file

@ -58,18 +58,17 @@ export default class ExperimentManager extends Module {
getExtraTerms: () => {
const values = [];
for(const [key,val] of Object.entries(this.experiments)) {
values.push(key);
if ( val.name )
values.push(val.name);
if ( val.description )
values.push(val.description);
}
for(const exps of [this.experiments, this.getTwitchExperiments()]) {
if ( ! exps )
continue;
for(const [key, val] of Object.entries(this.getTwitchExperiments())) {
values.push(key);
if ( val.name )
values.push(val.name);
for(const [key, val] of Object.entries(exps)) {
values.push(key);
if ( val.name )
values.push(val.name);
if ( val.description )
values.push(val.description);
}
}
return values;

View file

@ -1,12 +1,4 @@
{
"new_links": {
"name": "New Link Tokenization",
"description": "Update to Twitch's latest link regex. Experiment while this is checked for bugs.",
"groups": [
{"value": true, "weight": 100},
{"value": false, "weight": 0}
]
},
"api_load": {
"name": "New API Stress Testing",
"description": "Send duplicate requests to the new API server for load testing.",

View file

@ -242,7 +242,7 @@ export default class Badges extends Module {
path: 'Chat > Badges >> tabs ~> Visibility',
title: 'Visibility',
component: 'badge-visibility',
data: () => this.getSettingsBadges(true)
getBadges: cb => this.getSettingsBadges(true, cb)
}
});
@ -276,7 +276,7 @@ export default class Badges extends Module {
this.handleClick = this.handleClick.bind(this);
}
getSettingsBadges(include_addons) {
getSettingsBadges(include_addons, callback) {
const twitch = [],
owl = [],
tcon = [],
@ -284,6 +284,16 @@ export default class Badges extends Module {
ffz = [],
addon = [];
const twitch_keys = Object.keys(this.twitch_badges);
if ( ! twitch_keys.length && callback ) {
const td = this.resolve('site.twitch_data');
if ( td )
td.getBadges().then(data => {
this.updateTwitchBadges(data);
callback();
});
}
for(const key in this.twitch_badges)
if ( has(this.twitch_badges, key) ) {
const badge = this.twitch_badges[key],
@ -334,6 +344,9 @@ export default class Badges extends Module {
const badge = this.badges[key],
image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image;
if ( badge.no_visibility )
continue;
(badge.addon ? addon : ffz).push({
id: key,
provider: 'ffz',
@ -1070,6 +1083,9 @@ export function fixBadgeData(badge) {
return badge;
// Click Behavior
if ( ! badge.clickAction && badge.onClickAction )
badge.clickAction = badge.onClickAction;
if ( badge.clickAction === 'VISIT_URL' && badge.clickURL )
badge.click_url = badge.clickURL;

View file

@ -500,7 +500,7 @@ export default class Chat extends Module {
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Chat > Filtering >> Blocked Badges',
path: 'Chat > Filtering >> Blocked Badges @{"description": "**Note:** This section is for filtering messages out of chat from users with specific badges. If you wish to hide a badge, go to [Chat > Badges >> Visibility](~chat.badges.tabs.visibility)."}',
component: 'badge-highlighting',
removable: true,
data: () => this.badges.getSettingsBadges()

View file

@ -12,8 +12,8 @@ import {CATEGORIES} from './emoji';
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
WHITESPACE = /^\s*$/,
LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
//WHITESPACE = /^\s*$/,
//LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g,
//MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex
MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex
@ -148,7 +148,7 @@ export const Links = {
if ( ! tokens || ! tokens.length )
return tokens;
const use_new = this.experiments.getAssignment('new_links');
//const use_new = this.experiments.getAssignment('new_links');
const out = [];
for(const token of tokens) {
@ -157,28 +157,28 @@ export const Links = {
continue;
}
LINK_REGEX.lastIndex = 0;
//LINK_REGEX.lastIndex = 0;
NEW_LINK_REGEX.lastIndex = 0;
const text = token.text;
let idx = 0, match;
if ( use_new ) {
while((match = NEW_LINK_REGEX.exec(text))) {
const nix = match.index;
if ( idx !== nix )
out.push({type: 'text', text: text.slice(idx, nix)});
//if ( use_new ) {
while((match = NEW_LINK_REGEX.exec(text))) {
const nix = match.index;
if ( idx !== nix )
out.push({type: 'text', text: text.slice(idx, nix)});
out.push({
type: 'link',
url: `${match[1] ? '' : 'https://'}${match[0]}`,
is_mail: false,
text: match[0]
});
out.push({
type: 'link',
url: `${match[1] ? '' : 'https://'}${match[0]}`,
is_mail: false,
text: match[0]
});
idx = nix + match[0].length;
}
idx = nix + match[0].length;
}
} else {
/*} else {
while((match = LINK_REGEX.exec(text))) {
const nix = match.index + (match[1] ? match[1].length : 0);
if ( idx !== nix )
@ -195,7 +195,7 @@ export const Links = {
idx = nix + match[2].length;
}
}
}*/
if ( idx < text.length )
out.push({type: 'text', text: text.slice(idx)});

View file

@ -28,6 +28,7 @@
:key="addon.id"
:addon="addon"
:item="item"
:context="context"
@navigate="navigate"
/>
</div>

View file

@ -124,7 +124,7 @@
<script>
export default {
props: ['id', 'addon', 'item'],
props: ['id', 'addon', 'item', 'context'],
data() {
return {

View file

@ -40,7 +40,7 @@ export default {
data() {
return {
default_term: {
v: 'broadcaster',
v: '',
c: '',
remove: false
}

View file

@ -7,6 +7,10 @@
:src="current.image"
class="ffz--badge-term-image"
>
<div
v-else
class="ffz--badge-term-image"
/>
</div>
<div class="tw-flex-grow-1 tw-mg-r-05">
<h4 v-if="! editing && ! current" class="ffz-monospace">
@ -20,6 +24,9 @@
v-model="edit_data.v"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
>
<option v-if="adding" value="">
{{ t('setting.terms.please-select', 'Please select an option.') }}
</option>
<optgroup
v-for="section in badges"
:key="section.title"
@ -69,14 +76,24 @@
</div>
</div>
<div v-if="adding" class="tw-flex-shrink-0">
<button class="tw-button" @click="save">
<button
class="tw-button"
:class="! valid && 'tw-button--disabled'"
:disabled="! valid"
@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__container" @click="save">
<button
class="tw-button tw-button--text tw-tooltip__container"
:class="! valid && 'tw-button--disabled'"
:disabled="! valid"
@click="save"
>
<span class="tw-button__text ffz-i-floppy" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.save', 'Save') }}
@ -163,6 +180,10 @@ export default {
},
computed: {
valid() {
return this.display.v && this.display.v !== '';
},
display() {
return this.editing ? this.edit_data : this.term;
},
@ -207,7 +228,8 @@ export default {
},
save() {
this.$emit('save', this.edit_data);
if ( this.valid )
this.$emit('save', this.edit_data);
this.cancel();
}
}

View file

@ -25,7 +25,7 @@
</div>
<section
v-for="sec in data"
v-for="sec in badges"
:key="sec.title"
class="ffz--menu-container tw-border-t"
>
@ -58,7 +58,7 @@
</header>
<ul class="tw-flex tw-flex-wrap tw-align-content-start">
<li
v-for="i in sort(sec.badges)"
v-for="i in sec.badges"
:key="i.id"
:class="{default: badgeDefault(i.id)}"
class="ffz--badge-info tw-pd-y-1 tw-pd-r-1 tw-flex ffz-checkbox"
@ -115,11 +115,41 @@
import SettingMixin from '../setting-mixin';
import {has} from 'utilities/object';
function sortBadges(items) {
return items.sort((a, b) => {
const an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( an < bn ) return -1;
if ( an > bn ) return 1;
return 0;
});
}
export default {
mixins: [SettingMixin],
props: ['item', 'context'],
data() {
return {
badges: []
}
},
created() {
this.updateBadges();
},
methods: {
updateBadges() {
const badges = this.item.getBadges(() => this.updateBadges());
for(const section of badges) {
section.badges = sortBadges(section.badges);
}
this.badges = badges;
},
badgeChecked(id) {
return ! this.value[id];
},
@ -143,17 +173,6 @@ export default {
this.clear();
else
this.set(val);
},
sort(items) {
return items.sort((a, b) => {
const an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( an < bn ) return -1;
if ( an > bn ) return 1;
return 0;
});
}
}
}

View file

@ -10,6 +10,9 @@
v-model="edit_data.v"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
>
<option v-if="adding" value="">
{{ t('setting.terms.please-select', 'Please select an option.') }}
</option>
<option
v-for="type in types"
:key="type"
@ -20,7 +23,12 @@
</select>
</div>
<div v-if="adding" class="tw-flex-shrink-0">
<button class="tw-button" @click="save">
<button
class="tw-button"
:class="! valid && 'tw-button--disabled'"
:disabled="! valid"
@click="save"
>
<span class="tw-button__text">
{{ t('setting.terms.add-term', 'Add') }}
</span>
@ -94,6 +102,10 @@ export default {
},
computed: {
valid() {
return this.display.v && this.display.v !== '';
},
display() {
return this.editing ? this.edit_data : this.term;
}

View file

@ -36,7 +36,7 @@ export default {
data() {
return {
default_type: {
v: 'Hosted'
v: ''
}
}
},

View file

@ -8,6 +8,14 @@
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
</div>
<div
v-if="(item.warn_icons || (has_icons && item.warn_icons !== false)) && context.mod_icons === false"
class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"
>
<span class="ffz-i-info" />
{{ t('setting.actions.warn-hidden', 'You currently have Mod Icons turned off in your Twitch chat settings, so some actions might be hidden as a result. Use the settings menu in chat to toggle them.') }}
</div>
<div class="tw-pd-b-1 tw-border-b tw-mg-b-1">
<div class="tw-flex tw-flex-wrap tw-align-items-center ffz--inline">
{{ t('setting.actions.preview', 'Preview:') }}

View file

@ -193,14 +193,24 @@ export default {
this.markSeen(item);
if ( item.redirect )
return this.navigate(Array.isArray(item.redirect) ? item.redirect : item.redirect.split(/\./g));
this.currentItem = item;
this.restoredItem = true;
let url;
if ( this.exclusive ) {
url = new URL(location.href);
url.searchParams.set('ffz-settings', item.full_key);
url = url.toString();
}
try {
window.history.replaceState({
...window.history.state,
ffzcc: item.full_key
}, document.title)
}, document.title, url);
} catch(err) {
/* no-op */
}
@ -251,13 +261,34 @@ export default {
break;
}
while(item && item.page)
const tabs = [];
while(item && item.page) {
if ( item.tab && item.parent?.tabs )
tabs.push([item.parent, item.parent.tabs.indexOf(item)]);
item = item.parent;
}
if ( ! item )
return;
this.changeItem(item);
// Asynchronously walk down the tab tree, so that
// we can switch every tab correctly.
if ( tabs.length ) {
const bits = () => {
const latest = tabs.pop();
if ( latest?.[0]?._component )
latest[0]._component.select(latest[1]);
if ( tabs.length )
this.$nextTick(bits);
}
this.$nextTick(bits);
}
}
}
}

View file

@ -1,6 +1,6 @@
<template lang="html">
<div class="ffz--provider tw-pd-t-05">
<div v-if="not_www" class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<div v-if="not_www" class="ffz--notice tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<h3 class="ffz-i-attention">
{{ t('setting.provider.warn-domain.title', 'You\'re far from home!') }}
</h3>
@ -9,12 +9,12 @@
</div>
</div>
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<div class="ffz--notice tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<h3 class="ffz-i-attention">
{{ t('setting.provider.warn.title', 'Be careful!') }}
</h3>
<div>
<markdown :source="t('setting.provider.warn.desc', 'Please close any other Twitch tabs before using this tool. It is **recommended to create a backup** before changing your provider, in case anything happens.')" />
<markdown :source="t('setting.provider.warn.desc', 'Please close any other Twitch tabs before using this tool. It is **recommended to [create a backup](~data_management.backup_and_restore)** before changing your provider, in case anything happens.')" />
</div>
</div>

View file

@ -216,6 +216,15 @@ export default class MainMenu extends Module {
}
mdNavigate(thing) {
const path = thing?.dataset?.settingsLink;
if ( ! path )
return;
this.requestPage(path);
}
updateContext(context) {
if ( ! context )
context = this._context;
@ -732,6 +741,7 @@ export default class MainMenu extends Module {
can_proxy: context._context.can_proxy,
proxied: context._context.proxied,
has_update: this.has_update,
mod_icons: context.get('context.chat.showModIcons'),
setProxied: val => {
this.use_context = val;
@ -828,8 +838,9 @@ export default class MainMenu extends Module {
const profiles = context.manager.__profiles,
ids = this.profiles = context.__profiles.map(profile => profile.id);
this.proxied = this.context.proxied;
this.can_proxy = this.context.can_proxy;
_c.proxied = this.context.proxied;
_c.can_proxy = this.context.can_proxy;
_c.mod_icons = context.get('context.chat.showModIcons');
for(let i=0; i < profiles.length; i++) {
const id = profiles[i].id,

View file

@ -6,6 +6,7 @@
import Module from 'utilities/module';
import {deep_equals, has, debounce, deep_copy} from 'utilities/object';
import {parse as new_parse} from 'utilities/path-parser';
import SettingsProfile from './profile';
import SettingsContext from './context';
@ -913,9 +914,14 @@ export default class SettingsManager extends Module {
}
const PATH_SPLITTER = /(?:^|\s*([~>]+))\s*([^~>@]+)\s*(?:@([^~>]+))?/g;
export function parse_path(path) {
return new_parse(path);
}
/*const PATH_SPLITTER = /(?:^|\s*([~>]+))\s*([^~>@]+)\s*(?:@([^~>]+))?/g;
export function old_parse_path(path) {
const tokens = [];
let match;
@ -928,14 +934,20 @@ export function parse_path(path) {
opts = { key, title, page, tab };
if ( options )
Object.assign(opts, JSON.parse(options));
if ( options ) {
try {
Object.assign(opts, JSON.parse(options));
} catch(err) {
console.warn('Matched segment:', options);
throw err;
}
}
tokens.push(opts);
}
return tokens;
}
}*/
export function format_path_tokens(tokens) {

View file

@ -29,7 +29,7 @@ export default class BitsButton extends Module {
ui: {
path: 'Appearance > Layout >> Top Navigation',
title: 'Show the Get Bits button.',
description: 'By default, this inherits its value from Chat > Bits and Cheering > Display Bits',
description: 'By default, this inherits its value from [Chat > Bits and Cheering > Display Bits](~chat.bits_and_cheering)',
component: 'setting-check-box'
},

View file

@ -298,7 +298,7 @@ export default class ChatHook extends Module {
ui: {
path: 'Chat > Appearance >> Replies',
title: 'Style',
description: `Twitch's default style adds a floating button to the right and displays a notice above messages that are replies. FrankerFaceZ uses an In-Line Chat Action (that can be removed in Chat > Actions > In-Line) and uses an in-line mention to denote replies.`,
description: `Twitch's default style adds a floating button to the right and displays a notice above messages that are replies. FrankerFaceZ uses an In-Line Chat Action (that can be removed in [Chat > Actions > In-Line](~chat.actions.in_line)) and uses an in-line mention to denote replies.`,
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
@ -469,7 +469,7 @@ export default class ChatHook extends Module {
ui: {
path: 'Chat > Appearance >> Community',
title: 'Display Leaderboard',
description: 'The leaderboard shows the top cheerers and sub gifters in a channel.\n\nBy default due to a previous implementation, this inherits its value from Chat > Bits and Cheering > Display Bits.',
description: 'The leaderboard shows the top cheerers and sub gifters in a channel.\n\nBy default due to a previous implementation, this inherits its value from [Chat > Bits and Cheering > Display Bits](~chat.bits_and_cheering).',
component: 'setting-check-box'
}
});

View file

@ -25,6 +25,7 @@ export default class SocketClient extends Module {
super(...args);
this.inject('settings');
this.inject('experiments');
this.settings.addUI('socket.info', {
path: 'Debugging > Socket >> Info @{"sort": -1000}',
@ -99,7 +100,14 @@ export default class SocketClient extends Module {
}
onEnable() { this.connect() }
onEnable() {
// For now, stop connecting to the sockets for people using the
// API links experiment.
if ( this.experiments.getAssignment('api_links') )
return;
this.connect();
}
onDisable() { this.disconnect() }

View file

@ -7,6 +7,99 @@
import MD from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
import {parse as parse_path} from 'utilities/path-parser';
let _md;
function getMD() {
if ( ! _md ) {
const md = _md = new MD({
html: false,
linkify: true
});
md.use(SettingsLinks);
md.use(MILA, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
}
return _md;
}
function SettingsLinks(md) {
const default_render = md.renderer.rules.link_open || this.defaultRender;
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
const token = tokens[idx];
if ( token && token.type === 'link_open' && Array.isArray(token.attrs) ) {
let href;
for(const attr of token.attrs) {
if ( attr[0] === 'href' ) {
href = attr[1];
break;
}
}
if ( href.startsWith('~') ) {
let path;
if ( href === '~' ) {
// We don't have a path, make one from the bits.
let i = idx + 1;
let bits = [];
while(i < tokens.length) {
const tok = tokens[i],
type = tok?.type;
if ( type === 'text' )
bits.push(tok);
else if ( type === 'link_close' )
break;
i++;
}
bits = bits.map(x => x.content).join('');
const toks = parse_path(bits);
path = toks.map(x => x.key).join('.');
} else
path = href.slice(1);
if ( path && path.length ) {
for(const attr of token.attrs) {
if ( attr[0] === 'class' ) {
attr[1] = attr[1].replace(/ffz-tooltip/g, '');
break;
}
}
token.attrs.push([
'data-settings-link',
path
]);
token.attrs.push([
'onclick',
'FrankerFaceZ.get().resolve("main_menu").mdNavigate(this);return false'
]);
}
}
}
return default_render(tokens, idx, options, env, self);
}
}
SettingsLinks.defaultRender = function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
}
export default {
props: {
@ -15,21 +108,7 @@ export default {
computed: {
md() {
const md = new MD({
html: false,
linkify: true
});
md.use(MILA, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
return md;
return getMD();
},
output() {

View file

@ -82,10 +82,20 @@ export default {
}
},
created() {
if ( ! this.item._component )
this.item._component = this;
},
mounted() {
this.markSeen()
},
destroyed() {
if ( this.item._component === this )
this.item._component = null;
},
methods: {
focus() {
this.$el.querySelector('header').focus();

View file

@ -0,0 +1,13 @@
query FFZ_GlobalBadges {
badges {
id
clickURL
onClickAction
title
setID
version
image1x: imageURL(size: NORMAL)
image2x: imageURL(size: DOUBLE)
image4x: imageURL(size: QUADRUPLE)
}
}

View file

@ -197,7 +197,7 @@ export function deep_equals(object, other, ignore_undefined = false, seen, other
const source_keys = Object.keys(object),
dest_keys = Object.keys(other);
if ( ! ignore_undefined && ! array_equals(source_keys, dest_keys) )
if ( ! ignore_undefined && ! set_equals(new Set(source_keys), new Set(dest_keys)) )
return false;
for(const key of source_keys)
@ -216,10 +216,14 @@ export function deep_equals(object, other, ignore_undefined = false, seen, other
export function shallow_object_equals(a, b) {
if ( typeof a !== 'object' || typeof b !== 'object' || ! array_equals(Object.keys(a), Object.keys(b)) )
if ( typeof a !== 'object' || typeof b !== 'object' )
return false;
for(const key in a)
const keys = Object.keys(a);
if ( ! set_equals(new Set(keys), new Set(Object.keys(b))) )
return false;
for(const key of keys)
if ( a[key] !== b[key] )
return false;

View file

@ -158,6 +158,19 @@ export default class TwitchData extends Module {
return this._search;
}
// ========================================================================
// Badges
// ========================================================================
async getBadges() {
const data = await this.queryApollo(
await import(/* webpackChunkName: 'queries' */ './data/global-badges.gql')
);
return get('data.badges', data);
}
// ========================================================================
// Categories
// ========================================================================

View file

@ -18,10 +18,29 @@
.tw-display-inline { display: inline !important }
.tw-width-auto { width: auto !important }
.ffz--notice {
a {
color: var(--color-text-overlay-link);
&:hover, &:hover:focus {
color: var(--color-text-overlay-link-hover);
}
&:focus {
color: var(--color-text-overlay-link-focus);
}
}
}
.ffz-unmatched-item { opacity: 0.25 }
.ffz-monospace { font-family: monospace }
.ffz-bottom-100 { bottom: 100% }
.ffz--badge-term-image {
height: 3.6rem;
width: 3.6rem;
}
.ffz--autocomplete {
.scrollable-area {
max-height: 20rem;