mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.53.0
* Added: Setting to hide streams in the directory based upon tags. * Added: Setting to not automatically join raids to specific channels. * Added: Setting to attempt to display Golden Kappa Trains when Hype Trains are otherwise hidden. * Added: Settings profile filter rule for when the window is in fullscreen. * Fixed: Do not activate theater mode settings when in fullscreen. * API Added: The `site.player` module now has a `getUptime` method for getting the uptime of the current stream, if one is available. * API Removed: The tag-related methods of `site.twitch_data`, aside from `getMatchingTags` which now has a different signature. The methods were non-functional due to Twitch removing the relevant endpoints. * Developer: Added a debugging tool for viewing GraphQL queries in Apollo's cache. * Maintenance: Tweak the webpack build to hopefully get Mozilla to stop complaining that their build environment is weird while accusing me of having the weird build environment.
This commit is contained in:
parent
8ac95f3a52
commit
92fcc853a6
26 changed files with 665 additions and 403 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.52.0",
|
||||
"version": "4.53.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -196,8 +196,8 @@
|
|||
|
||||
<script>
|
||||
|
||||
import { sanitize } from 'src/utilities/dom';
|
||||
import { DEBUG, SERVER } from 'utilities/constants';
|
||||
import { highlightJson } from 'utilities/dom';
|
||||
import { deep_copy, generateUUID } from 'utilities/object';
|
||||
import { getBuster } from 'utilities/time';
|
||||
|
||||
|
@ -314,35 +314,8 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
highlightJson(object, depth = 1) {
|
||||
if ( depth > 10 )
|
||||
return `<span class="ffz-ct--obj-literal"><nested>`;
|
||||
|
||||
if (object == null)
|
||||
return `<span class="ffz-ct--literal" depth="${depth}">null</span>`;
|
||||
|
||||
if ( typeof object === 'number' || typeof object === 'boolean' )
|
||||
return `<span class="ffz-ct--literal" depth="${depth}">${object}</span>`;
|
||||
|
||||
if ( typeof object === 'string' )
|
||||
return `<span class=ffz-ct--string depth="${depth}">"${sanitize(object)}"</span>`;
|
||||
|
||||
if ( Array.isArray(object) )
|
||||
return `<span class="ffz-ct--obj-open" depth="${depth}">[</span>`
|
||||
+ object.map(x => this.highlightJson(x, depth + 1)).join(`<span class="ffz-ct--obj-sep" depth="${depth}">, </span>`)
|
||||
+ `<span class="ffz-ct--obj-close" depth="${depth}">]</span>`;
|
||||
|
||||
const out = [];
|
||||
|
||||
for(const [key, val] of Object.entries(object)) {
|
||||
if ( out.length > 0 )
|
||||
out.push(`<span class="ffz-ct--obj-sep" depth="${depth}">, </span>`);
|
||||
|
||||
out.push(`<span class="ffz-ct--obj-key" depth="${depth}">"${sanitize(key)}"</span><span class="ffz-ct--obj-key-sep" depth="${depth}">: </span>`);
|
||||
out.push(this.highlightJson(val, depth + 1));
|
||||
}
|
||||
|
||||
return `<span class="ffz-ct--obj-open" depth="${depth}">{</span>${out.join('')}<span class="ffz-ct--obj-close" depth="${depth}">}</span>`;
|
||||
highlightJson(object, pretty) {
|
||||
return highlightJson(object, pretty);
|
||||
},
|
||||
|
||||
// Samples
|
||||
|
|
160
src/modules/main_menu/components/graphql-inspect.vue
Normal file
160
src/modules/main_menu/components/graphql-inspect.vue
Normal file
|
@ -0,0 +1,160 @@
|
|||
<template>
|
||||
<div class="ffz--graphql-inspector">
|
||||
<div class="tw-flex tw-align-items-start">
|
||||
<label for="selector" class="tw-mg-y-05">
|
||||
{{ t('debug.graphql.query', 'Query:') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-flex tw-flex-column tw-mg-l-05 tw-full-width">
|
||||
<select
|
||||
id="selector"
|
||||
ref="selector"
|
||||
class="tw-full-width tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
|
||||
@change="onSelectChange"
|
||||
>
|
||||
<option
|
||||
v-for="(query, idx) in queries"
|
||||
:key="query.name"
|
||||
:selected="current === query"
|
||||
:value="idx"
|
||||
>
|
||||
{{ query.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="current" class="ffz--example-report">
|
||||
<div class="tw-mg-t-1 tw-c-background-alt-2 tw-font-size-5 tw-pd-y-05 tw-pd-x-1 tw-border-radius-large">
|
||||
<code>{{ current.source }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="current && current.variables" class="tw-mg-t-1">
|
||||
<div v-html="highlightJson(current.variables)" />
|
||||
</div>
|
||||
<div v-if="current && current.result" class="ffz--example-report ffz--tall">
|
||||
<div class="tw-mg-t-1 tw-c-background-alt-2 tw-font-size-5 tw-pd-y-05 tw-pd-x-1 tw-border-radius-large">
|
||||
<code v-html="highlightJson(current.result, true)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highlightJson } from 'utilities/dom';
|
||||
import { deep_copy } from 'utilities/object';
|
||||
|
||||
|
||||
const BAD_KEYS = [
|
||||
'kind',
|
||||
'definitions',
|
||||
'loc'
|
||||
];
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
has_client: false,
|
||||
has_printer: false,
|
||||
queryMap: {},
|
||||
current: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
queries() {
|
||||
const queries = Object.values(this.queryMap);
|
||||
queries.sort((a,b) => a.name.localeCompare(b.name));
|
||||
return queries;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.ffz = this.item.getFFZ();
|
||||
|
||||
this.client = this.ffz.resolve('site.apollo')?.client;
|
||||
this.has_client = !! this.client;
|
||||
|
||||
this.printer = this.ffz.resolve('site.web_munch')?.getModule?.('gql-printer');
|
||||
this.has_printer = !! this.printer;
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.client = null;
|
||||
this.ffz = null;
|
||||
this.has_client = false;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateQueries();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateQueries() {
|
||||
if ( ! this.client )
|
||||
return;
|
||||
|
||||
const map = this.client.queryManager?.queries;
|
||||
|
||||
if ( ! map || ! map.values )
|
||||
return;
|
||||
|
||||
for(const query of map.values()) {
|
||||
if ( ! query?.document )
|
||||
continue;
|
||||
|
||||
let name = guessNameFromDocument(query.document);
|
||||
|
||||
if ( ! name )
|
||||
name = query.observableQuery?.queryName;
|
||||
|
||||
if ( ! this.queryMap[name] )
|
||||
this.$set(this.queryMap, name, {
|
||||
id: query.queryId,
|
||||
name,
|
||||
source: this.printQuery(query.document),
|
||||
variables: null,
|
||||
result: null
|
||||
});
|
||||
|
||||
this.queryMap[name].variables = deep_copy(query.observableQuery?.variables);
|
||||
this.queryMap[name].result = deep_copy(query.observableQuery?.lastResult?.data ?? null);
|
||||
}
|
||||
|
||||
if ( ! this.current )
|
||||
this.current = Object.values(this.queries)[0];
|
||||
},
|
||||
|
||||
highlightJson(object, pretty) {
|
||||
return highlightJson(object, pretty);
|
||||
},
|
||||
|
||||
printQuery(doc) {
|
||||
if ( this.printer )
|
||||
try {
|
||||
return this.printer(doc);
|
||||
} catch(err) {
|
||||
this.ffz.log.warn('Unable to print GQL using gql-printer.', err);
|
||||
}
|
||||
|
||||
return doc.loc?.source?.body;
|
||||
},
|
||||
|
||||
onSelectChange() {
|
||||
const idx = this.$refs.selector.value,
|
||||
item = this.queries[idx];
|
||||
|
||||
this.current = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function guessNameFromDocument(doc) {
|
||||
const keys = Object.keys(doc).filter(key => ! BAD_KEYS.includes(key));
|
||||
if ( keys.length === 1 )
|
||||
return keys[0];
|
||||
}
|
||||
|
||||
</script>
|
134
src/modules/main_menu/components/tag-list-editor.vue
Normal file
134
src/modules/main_menu/components/tag-list-editor.vue
Normal file
|
@ -0,0 +1,134 @@
|
|||
<template lang="html">
|
||||
<section class="ffz--widget ffz--tag-list">
|
||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width">
|
||||
<div class="tw-flex-grow-1 tw-mg-r-05">
|
||||
<autocomplete
|
||||
v-slot="slot"
|
||||
v-model="adding"
|
||||
:input-id="'tag$' + id"
|
||||
:items="fetchTags"
|
||||
:suggest-on-focus="true"
|
||||
:escape-to-clear="false"
|
||||
class="tw-flex-grow-1"
|
||||
/>
|
||||
</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="! value || ! value.length" 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.no-items', 'no items') }}
|
||||
</div>
|
||||
<ul v-else class="ffz--term-list tw-mg-t-05">
|
||||
<li
|
||||
v-for="i in value"
|
||||
:key="i"
|
||||
class="ffz--term ffz--game-term tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width"
|
||||
>
|
||||
<div class="tw-flex-grow-1 tw-mg-r-05">
|
||||
<a
|
||||
v-if="can_link"
|
||||
:href="`/directory/all/tags/${i}`"
|
||||
class="ffz-link"
|
||||
@click.prevent="handleLink(i)"
|
||||
>
|
||||
{{ i }}
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ i }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-flex-shrink-0">
|
||||
<button class="tw-button tw-button--text ffz-il-tooltip__container" @click="remove(i)">
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
import {deep_copy} from 'utilities/object';
|
||||
|
||||
let last_id = 0;
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
id: last_id++,
|
||||
adding: '',
|
||||
can_link: false
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
const ffz = this.context.getFFZ();
|
||||
|
||||
this.loader = ffz.resolve('site.twitch_data');
|
||||
this.router = ffz.resolve('site.router');
|
||||
|
||||
this.can_link = false; //! ffz.resolve('main_menu').exclusive;
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
if ( ! this.adding?.length )
|
||||
return;
|
||||
|
||||
const adding = this.adding.toLowerCase();
|
||||
|
||||
const values = Array.from(this.value);
|
||||
if ( values.includes(adding) )
|
||||
return;
|
||||
|
||||
values.push(adding);
|
||||
this.set(values);
|
||||
},
|
||||
|
||||
remove(item) {
|
||||
const values = Array.from(this.value),
|
||||
idx = values.indexOf(item);
|
||||
|
||||
if ( idx === -1 )
|
||||
return;
|
||||
|
||||
if ( values.length === 1 )
|
||||
this.clear();
|
||||
else {
|
||||
values.splice(idx, 1);
|
||||
this.set(values);
|
||||
}
|
||||
},
|
||||
|
||||
handleLink(i) {
|
||||
//this.router.navigate('dir-game-index', {gameName: i});
|
||||
},
|
||||
|
||||
async fetchTags(query) {
|
||||
if ( ! this.loader )
|
||||
return [];
|
||||
|
||||
const data = await this.loader.getMatchingTags(query);
|
||||
if ( ! Array.isArray(data) )
|
||||
return [];
|
||||
|
||||
return data.map(x => ({name: x}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -151,6 +151,13 @@ export default class MainMenu extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.addUI('debug.graphql-test', {
|
||||
path: 'Debugging > GraphQL >> Inspector',
|
||||
component: 'graphql-inspect',
|
||||
getFFZ: () => this.resolve('core'),
|
||||
force_seen: true
|
||||
});
|
||||
|
||||
this.settings.addUI('faq', {
|
||||
path: 'Home > FAQ @{"profile_warning": false}',
|
||||
component: 'md-page',
|
||||
|
@ -1210,7 +1217,7 @@ export default class MainMenu extends Module {
|
|||
if ( this.dialog.exclusive || this.site?.router?.current_name === 'squad' || this.site?.router?.current_name === 'command-center' )
|
||||
return;
|
||||
|
||||
if ( this.settings.get('context.ui.theatreModeEnabled') )
|
||||
if ( this.settings.get('layout.is-theater-mode') )
|
||||
return;
|
||||
|
||||
this.dialog.toggleSize(e);
|
||||
|
|
|
@ -172,7 +172,11 @@ export const Time = {
|
|||
|
||||
export const TheaterMode = {
|
||||
createTest(config) {
|
||||
return ctx => ctx.ui && ctx.ui.theatreModeEnabled === config;
|
||||
return ctx => {
|
||||
if ( ctx.fullscreen )
|
||||
return config === false;
|
||||
return ctx.ui && ctx.ui.theatreModeEnabled === config;
|
||||
}
|
||||
},
|
||||
|
||||
title: 'Theater Mode',
|
||||
|
@ -183,6 +187,19 @@ export const TheaterMode = {
|
|||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue')
|
||||
};
|
||||
|
||||
export const Fullscreen = {
|
||||
createTest(config) {
|
||||
return ctx => ctx.fullscreen === config;
|
||||
},
|
||||
|
||||
title: 'Fullscreen',
|
||||
i18n: 'settings.filter.fullscreen',
|
||||
|
||||
default: true,
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue')
|
||||
};
|
||||
|
||||
export const Moderator = {
|
||||
createTest(config) {
|
||||
return ctx => ctx.moderator === config;
|
||||
|
|
|
@ -29,6 +29,10 @@ export default class Player extends PlayerBase {
|
|||
return this.settings.get('player.embed-metadata');
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this.parent.data;
|
||||
}
|
||||
|
||||
async getBroadcastID(inst, channel_id) {
|
||||
this.twitch_data = await this.parent.awaitTwitchData();
|
||||
return super.getBroadcastID(inst, channel_id);
|
||||
|
|
|
@ -2214,7 +2214,7 @@ export default class PlayerBase extends Module {
|
|||
else if ( ! Array.isArray(keys) )
|
||||
keys = [keys];
|
||||
|
||||
const source = this.parent.data,
|
||||
const source = this.getData(),
|
||||
user = source?.props?.data?.user;
|
||||
|
||||
const timers = inst._ffz_meta_timers = inst._ffz_meta_timers || {},
|
||||
|
@ -2240,6 +2240,25 @@ export default class PlayerBase extends Module {
|
|||
}
|
||||
|
||||
|
||||
getUptime(inst) {
|
||||
// TODO: Support multiple instances.
|
||||
const source = this.getData(),
|
||||
user = source?.props?.data?.user;
|
||||
|
||||
let created = user?.stream?.createdAt;
|
||||
|
||||
if ( ! created )
|
||||
return null;
|
||||
|
||||
if ( !(created instanceof Date) )
|
||||
created = new Date(created);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
return (now - created.getTime()) / 1000;
|
||||
}
|
||||
|
||||
|
||||
getBroadcastID(inst, channel_id) {
|
||||
if ( ! this.twitch_data )
|
||||
return Promise.resolve(null);
|
||||
|
|
|
@ -106,6 +106,13 @@ export default class Twilight extends BaseSite {
|
|||
window.addEventListener('resize', update_size);
|
||||
update_size();
|
||||
|
||||
const update_fullscreen = () => this.settings.updateContext({
|
||||
fullscreen: !! document.fullscreenElement
|
||||
});
|
||||
|
||||
document.addEventListener('fullscreenchange', update_fullscreen);
|
||||
update_fullscreen();
|
||||
|
||||
// Share Context
|
||||
store.subscribe(() => this.updateContext());
|
||||
this.updateContext();
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// ============================================================================
|
||||
|
||||
import {Color, ColorAdjuster} from 'utilities/color';
|
||||
import {get, has, make_enum, shallow_object_equals, set_equals, deep_equals} from 'utilities/object';
|
||||
import {get, has, make_enum, shallow_object_equals, set_equals, deep_equals, glob_to_regex, escape_regex} from 'utilities/object';
|
||||
import {WEBKIT_CSS as WEBKIT} from 'utilities/constants';
|
||||
import {FFZEvent} from 'utilities/events';
|
||||
import {useFont} from 'utilities/fonts';
|
||||
|
@ -356,6 +356,50 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('channel.raids.blocked-channels', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
always_inherit: true,
|
||||
ui: {
|
||||
path: 'Channel > Behavior >> Raids: Blocked Channels @{"description": "You will not automatically join raids to channels listed here."}',
|
||||
component: 'basic-terms',
|
||||
words: false
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('__filter:channel.raids.blocked-channels', {
|
||||
requires: ['channel.raids.blocked-channels'],
|
||||
equals: 'requirements',
|
||||
process(ctx) {
|
||||
const val = ctx.get('channel.raids.blocked-channels');
|
||||
if ( ! val || ! val.length )
|
||||
return null;
|
||||
|
||||
const out = [];
|
||||
|
||||
for(const item of val) {
|
||||
const t = item.t;
|
||||
let v = item.v;
|
||||
|
||||
if ( t === 'glob' )
|
||||
v = glob_to_regex(v);
|
||||
|
||||
else if ( t !== 'raw' )
|
||||
v = escape_regex(v);
|
||||
|
||||
if ( ! v || ! v.length )
|
||||
continue;
|
||||
|
||||
out.push(v);
|
||||
}
|
||||
|
||||
if ( out.length )
|
||||
return new RegExp(`^(?:${out.join('|')})$`, 'gi');
|
||||
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
||||
this.settings.add('chat.hide-community-highlights', {
|
||||
default: false,
|
||||
ui: {
|
||||
|
@ -402,6 +446,16 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.banners.kappa-train', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Community',
|
||||
title: 'Attempt to always display the Golden Kappa Train, even if other Hype Trains are hidden.',
|
||||
description: '**Note**: This setting is currently theoretical and may not work, or may cause non-Kappa hype trains to appear. Due to the infrequent nature of hype trains, and especially the golden kappa hype train, it is very hard to test.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.banners.drops', {
|
||||
default: true,
|
||||
ui: {
|
||||
|
@ -1570,17 +1624,32 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
|
||||
noAutoRaids(inst) {
|
||||
if ( this.settings.get('channel.raids.no-autojoin') )
|
||||
setTimeout(() => {
|
||||
if ( inst.props && inst.props.raid && ! inst.isRaidCreator && inst.hasJoinedCurrentRaid ) {
|
||||
const id = inst.props.raid.id;
|
||||
if ( this.joined_raids.has(id) )
|
||||
return;
|
||||
if ( inst._ffz_no_raid )
|
||||
return;
|
||||
|
||||
this.log.info('Automatically leaving raid:', id);
|
||||
inst.handleLeaveRaid();
|
||||
inst._ffz_no_raid = setTimeout(() => {
|
||||
inst._ffz_no_raid = null;
|
||||
|
||||
if ( inst.props && inst.props.raid && ! inst.isRaidCreator && inst.hasJoinedCurrentRaid ) {
|
||||
const id = inst.props.raid.id;
|
||||
if ( this.joined_raids.has(id) )
|
||||
return;
|
||||
|
||||
let leave = this.settings.get('channel.raids.no-autojoin');
|
||||
|
||||
if ( ! leave ) {
|
||||
const blocked = this.settings.get('__filter:channel.raids.blocked-channels');
|
||||
if ( blocked && ((inst.props.raid.targetLogin && blocked.test(inst.props.raid.targetLogin)) || (inst.props.raid.targetDisplayName && blocked.test(inst.props.raid.targetDisplayName))) )
|
||||
leave = true;
|
||||
|
||||
if ( ! leave )
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this.log.info('Automatically leaving raid:', id);
|
||||
inst.handleLeaveRaid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleEmoteJail() {
|
||||
|
@ -1611,6 +1680,10 @@ export default class ChatHook extends Module {
|
|||
|
||||
const type = entry.event.type;
|
||||
if ( type && has(types, type) && ! types[type] ) {
|
||||
// Attempt to allow Golden Kappa hype trains?
|
||||
if ( type === 'hype_train' && entry.event.typeDetails === '0' && this.chat.context.get('chat.banners.kappa-train') )
|
||||
continue;
|
||||
|
||||
this.log.info('Removing community highlight: ', type, '#', entry.id);
|
||||
this.community_dispatch({
|
||||
type: 'remove-highlight',
|
||||
|
|
|
@ -304,10 +304,10 @@ export default class CSSTweaks extends Module {
|
|||
});
|
||||
|
||||
this.settings.add('layout.theatre-navigation', {
|
||||
requires: ['context.ui.theatreModeEnabled'],
|
||||
requires: ['layout.is-theater-mode'],
|
||||
default: false,
|
||||
process(ctx, val) {
|
||||
return ctx.get('context.ui.theatreModeEnabled') ? val : false
|
||||
return ctx.get('layout.is-theater-mode') ? val : false
|
||||
},
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Top Navigation',
|
||||
|
|
|
@ -51,7 +51,7 @@ export default class Game extends SiteModule {
|
|||
});
|
||||
|
||||
this.settings.provider.on('changed', key => {
|
||||
if ( key === 'directory.game.blocked-games' || key === 'directory.game.hidden-thumbnails' || key === 'directory.game.blocked-tags' ) {
|
||||
if ( key === 'directory.game.blocked-games' || key === 'directory.game.hidden-thumbnails' ) {
|
||||
this.parent.updateCards();
|
||||
|
||||
for(const inst of this.GameHeader.instances)
|
||||
|
|
|
@ -235,6 +235,17 @@ export default class Directory extends SiteModule {
|
|||
changed: () => this.updateCards()
|
||||
});
|
||||
|
||||
this.settings.add('directory.blocked-tags', {
|
||||
default: [],
|
||||
type: 'basic_array_merge',
|
||||
always_inherit: true,
|
||||
ui: {
|
||||
path: 'Directory > Channels >> Block by Tag',
|
||||
component: 'tag-list-editor'
|
||||
},
|
||||
changed: () => this.updateCards()
|
||||
});
|
||||
|
||||
/*this.settings.add('directory.hide-viewing-history', {
|
||||
default: false,
|
||||
ui: {
|
||||
|
@ -342,7 +353,7 @@ export default class Directory extends SiteModule {
|
|||
let bad_tag = false;
|
||||
|
||||
if ( Array.isArray(tags) ) {
|
||||
const bad_tags = this.settings.provider.get('directory.game.blocked-tags', []);
|
||||
const bad_tags = this.settings.get('directory.blocked-tags', []);
|
||||
if ( bad_tags.length ) {
|
||||
for(const tag of tags) {
|
||||
if ( tag?.id && bad_tags.includes(tag.id) ) {
|
||||
|
@ -380,7 +391,7 @@ export default class Directory extends SiteModule {
|
|||
return;
|
||||
|
||||
const game = props.gameTitle || props.trackingProps?.categoryName || props.trackingProps?.category || props.contextualCardActionProps?.props?.categoryName,
|
||||
tags = props.tagListProps?.tags;
|
||||
tags = props.tagListProps?.freeformTags;
|
||||
|
||||
let bad_tag = false;
|
||||
|
||||
|
@ -388,10 +399,10 @@ export default class Directory extends SiteModule {
|
|||
el.dataset.ffzType = props.streamType;
|
||||
|
||||
if ( Array.isArray(tags) ) {
|
||||
const bad_tags = this.settings.provider.get('directory.game.blocked-tags', []);
|
||||
const bad_tags = this.settings.get('directory.blocked-tags', []);
|
||||
if ( bad_tags.length ) {
|
||||
for(const tag of tags) {
|
||||
if ( tag?.id && bad_tags.includes(tag.id) ) {
|
||||
if ( tag?.name && bad_tags.includes(tag.name.toLowerCase()) ) {
|
||||
bad_tag = true;
|
||||
break;
|
||||
}
|
||||
|
@ -535,53 +546,6 @@ export default class Directory extends SiteModule {
|
|||
}
|
||||
|
||||
|
||||
/*updateAvatar(inst) {
|
||||
const container = this.fine.getChildNode(inst),
|
||||
card = container && container.querySelector && container.querySelector('.preview-card-overlay'),
|
||||
setting = this.settings.get('directory.show-channel-avatars');
|
||||
|
||||
if ( ! card )
|
||||
return;
|
||||
|
||||
const props = inst.props,
|
||||
is_video = props.durationInSeconds != null,
|
||||
src = props.channelImageProps && props.channelImageProps.src;
|
||||
|
||||
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 && 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 = props.channelLogin;
|
||||
inst.ffz_av_src = src;
|
||||
|
||||
const link = props.channelLinkTo && props.channelLinkTo.pathname;
|
||||
|
||||
card.appendChild(<a
|
||||
class="ffz-channel-avatar"
|
||||
href={link}
|
||||
onClick={e => this.routeClick(e, link)} // eslint-disable-line react/jsx-no-bind
|
||||
>
|
||||
<div class={`tw-absolute tw-right-0 tw-border-l tw-c-background-base ${is_video ? 'tw-top-0 tw-border-b' : 'tw-bottom-0 tw-border-t'}`}>
|
||||
<figure class="tw-aspect tw-aspect--align-top">
|
||||
<img src={src} title={props.channelDisplayName} />
|
||||
</figure>
|
||||
</div>
|
||||
</a>);
|
||||
}*/
|
||||
|
||||
|
||||
routeClick(event, url) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
|
|
@ -162,6 +162,15 @@ export default class Layout extends Module {
|
|||
changed: val => this.css_tweaks.toggle('portrait-metadata-top', val)
|
||||
});
|
||||
|
||||
this.settings.add('layout.is-theater-mode', {
|
||||
requires: ['context.ui.theatreModeEnabled', 'context.fullscreen'],
|
||||
process(ctx) {
|
||||
if ( ctx.get('context.fullscreen') )
|
||||
return false;
|
||||
return ctx.get('context.ui.theatreModeEnabled');
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('layout.show-portrait-chat', {
|
||||
requires: ['layout.use-portrait', 'layout.portrait-extra-height', 'layout.portrait-extra-width'],
|
||||
process() {
|
||||
|
@ -172,10 +181,10 @@ export default class Layout extends Module {
|
|||
});
|
||||
|
||||
this.settings.add('layout.portrait-extra-height', {
|
||||
requires: ['context.new_channel', 'context.squad_bar', /*'context.hosting',*/ 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
|
||||
requires: ['context.new_channel', 'context.squad_bar', /*'context.hosting',*/ 'layout.is-theater-mode', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
|
||||
process(ctx) {
|
||||
let height = 0;
|
||||
if ( ctx.get('context.ui.theatreModeEnabled') ) {
|
||||
if ( ctx.get('layout.is-theater-mode') ) {
|
||||
if ( ctx.get('layout.minimal-navigation') )
|
||||
height += 1;
|
||||
|
||||
|
@ -203,9 +212,9 @@ export default class Layout extends Module {
|
|||
})
|
||||
|
||||
this.settings.add('layout.portrait-extra-width', {
|
||||
require: ['layout.side-nav.show', 'context.ui.theatreModeEnabled', 'context.ui.sideNavExpanded'],
|
||||
require: ['layout.side-nav.show', 'layout.is-theater-mode', 'context.ui.sideNavExpanded'],
|
||||
process(ctx) {
|
||||
if ( ! ctx.get('layout.side-nav.show') || ctx.get('context.ui.theatreModeEnabled') )
|
||||
if ( ! ctx.get('layout.side-nav.show') || ctx.get('layout.is-theater-mode') )
|
||||
return 0;
|
||||
|
||||
return ctx.get('context.ui.sideNavExpanded') ? 24 : 5
|
||||
|
|
103
src/sites/twitch-twilight/modules/loadable.jsx
Normal file
103
src/sites/twitch-twilight/modules/loadable.jsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Loadable Stuff
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
|
||||
export default class Loadable extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.web_munch');
|
||||
|
||||
this.LoadableComponent = this.fine.define(
|
||||
'loadable-component',
|
||||
n => n.props?.component && n.props.loader
|
||||
);
|
||||
|
||||
this.overrides = new Map();
|
||||
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.settings.getChanges('chat.hype.show-pinned', val => {
|
||||
this.toggle('PaidPinnedChatMessageList', val);
|
||||
});
|
||||
|
||||
this.LoadableComponent.ready((cls, instances) => {
|
||||
this.log.debug('Found Loadable component wrapper.');
|
||||
|
||||
const t = this,
|
||||
old_render = cls.prototype.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
try {
|
||||
const type = this.props.component;
|
||||
if ( t.overrides.has(type) ) {
|
||||
let cmp = this.state.Component;
|
||||
if ( typeof cmp === 'function' && ! cmp.ffzWrapped ) {
|
||||
const React = t.web_munch.getModule('react'),
|
||||
createElement = React && React.createElement;
|
||||
|
||||
if ( createElement ) {
|
||||
if ( ! cmp.ffzWrapper ) {
|
||||
const th = this;
|
||||
function FFZWrapper(props, state) {
|
||||
if ( t.shouldRender(th.props.component, props, state) )
|
||||
return createElement(cmp, props);
|
||||
return null;
|
||||
}
|
||||
|
||||
FFZWrapper.ffzWrapped = true;
|
||||
FFZWrapper.displayName = `FFZWrapper(${this.props.component})`;
|
||||
cmp.ffzWrapper = FFZWrapper;
|
||||
}
|
||||
|
||||
this.state.Component = cmp.ffzWrapper;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
/* no-op */
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return old_render.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
toggle(cmp, state = null) {
|
||||
const existing = this.overrides.get(cmp) ?? true;
|
||||
|
||||
if ( state == null )
|
||||
state = ! existing;
|
||||
else
|
||||
state = !! state;
|
||||
|
||||
if ( state !== existing ) {
|
||||
this.overrides.set(cmp, state);
|
||||
this.update(cmp);
|
||||
}
|
||||
}
|
||||
|
||||
update(cmp) {
|
||||
for(const inst of this.LoadableComponent.instances) {
|
||||
if ( inst?.props?.component === cmp )
|
||||
inst.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
shouldRender(cmp, props) {
|
||||
return this.overrides.get(cmp) ?? true;
|
||||
}
|
||||
|
||||
}
|
|
@ -41,6 +41,12 @@ export default class Player extends PlayerBase {
|
|||
n => n.state && n.state.playerStyles
|
||||
);*/
|
||||
|
||||
this.DataSource = this.fine.define(
|
||||
'data-source',
|
||||
n => n.consentMetadata && n.onPlaying && n.props && n.props.data,
|
||||
PLAYER_ROUTES
|
||||
);
|
||||
|
||||
this.Player = this.fine.define(
|
||||
'highwind-player',
|
||||
n => n.setPlayerActive && n.props?.playerEvents && n.props?.mediaPlayerInstance,
|
||||
|
@ -108,9 +114,9 @@ export default class Player extends PlayerBase {
|
|||
|
||||
this.settings.add('player.theatre.metadata', {
|
||||
default: false,
|
||||
requires: ['context.route.name'],
|
||||
requires: ['context.route.name', 'layout.is-theater-mode'],
|
||||
process(ctx, val) {
|
||||
if ( ctx.get('context.route.name') === 'video' )
|
||||
if ( ! ctx.get('layout.is-theater-mode') || ctx.get('context.route.name') === 'video' )
|
||||
return false;
|
||||
return val
|
||||
},
|
||||
|
@ -215,6 +221,11 @@ export default class Player extends PlayerBase {
|
|||
}
|
||||
|
||||
|
||||
getData() {
|
||||
return this.DataSource.first;
|
||||
}
|
||||
|
||||
|
||||
tryTheatreMode(inst) {
|
||||
if ( ! inst._ffz_theater_timer )
|
||||
inst._ffz_theater_timer = setTimeout(() => {
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
>
|
||||
<slot :item="item">
|
||||
<div class="tw-pd-x-1 tw-pd-y-05">
|
||||
<span :title="item.title">{{ item.displayName || item.label || item.name }}</span>
|
||||
<span v-if="typeof item === 'string'">{{ item }}</span>
|
||||
<span v-else :title="item.title">{{ item.displayName || item.label || item.name }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
</button>
|
||||
|
|
|
@ -384,18 +384,16 @@ export default class Apollo extends Module {
|
|||
// ========================================================================
|
||||
|
||||
getQuery(operation) {
|
||||
const qm = this.client.queryManager,
|
||||
name_map = qm && qm.queryIdsByName,
|
||||
query_map = qm && qm.queries,
|
||||
query_id = name_map && name_map[operation],
|
||||
query = query_map && query_id && query_map.get(Array.isArray(query_id) ? query_id[0] : query_id);
|
||||
const query_map = this.client.queryManager?.queries;
|
||||
|
||||
if ( ! query_map && ! this.warn_qm ) {
|
||||
this.log.error('Unable to find the Apollo query map. We cannot access data properly.');
|
||||
this.warn_qm = true;
|
||||
if ( ! query_map )
|
||||
return;
|
||||
|
||||
for(const val of query_map.values()) {
|
||||
const obs = val?.observableQuery;
|
||||
if ( obs?.queryName === operation )
|
||||
return obs;
|
||||
}
|
||||
|
||||
return query && query.observableQuery;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
query FFZ_SearchLiveTags($query: String!, $categoryID: ID, $limit: Int) {
|
||||
searchLiveTags(userQuery: $query, categoryID: $categoryID, limit: $limit) {
|
||||
id
|
||||
isAutomated
|
||||
isLanguageTag
|
||||
localizedDescription
|
||||
localizedName
|
||||
scope
|
||||
tagName
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
query FFZ_FetchTags($ids: [ID!]) {
|
||||
contentTags(ids: $ids) {
|
||||
id
|
||||
isAutomated
|
||||
isLanguageTag
|
||||
localizedDescription
|
||||
localizedName
|
||||
scope
|
||||
tagName
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
query FFZ_TopTags($limit: Int) {
|
||||
topTags(limit: $limit) {
|
||||
id
|
||||
isAutomated
|
||||
isLanguageTag
|
||||
localizedDescription
|
||||
localizedName
|
||||
scope
|
||||
tagName
|
||||
}
|
||||
}
|
|
@ -329,4 +329,47 @@ export class ClickOutside {
|
|||
if ( this.el && ! this.el.contains(e.target) )
|
||||
this.cb(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: Rewrite this method to not use raw HTML.
|
||||
|
||||
export function highlightJson(object, pretty = false, depth = 1) {
|
||||
let indent = '', indent_inner = '';
|
||||
if ( pretty ) {
|
||||
indent = ' '.repeat(depth - 1);
|
||||
indent_inner = ' '.repeat(depth);
|
||||
}
|
||||
|
||||
if ( depth > 10 )
|
||||
return `<span class="ffz-ct--obj-literal"><nested></span>`;
|
||||
|
||||
if (object == null)
|
||||
return `<span class="ffz-ct--literal" depth="${depth}">null</span>`;
|
||||
|
||||
if ( typeof object === 'number' || typeof object === 'boolean' )
|
||||
return `<span class="ffz-ct--literal" depth="${depth}">${object}</span>`;
|
||||
|
||||
if ( typeof object === 'string' )
|
||||
return `<span class=ffz-ct--string depth="${depth}">"${sanitize(object)}"</span>`;
|
||||
|
||||
if ( Array.isArray(object) )
|
||||
return `<span class="ffz-ct--obj-open" depth="${depth}">[</span>`
|
||||
+ object.map(x => (pretty ? `\n${indent_inner}` : '') + highlightJson(x, pretty, depth + 1)).join(`<span class="ffz-ct--obj-sep" depth="${depth}">, </span>`)
|
||||
+ (pretty ? `\n${indent}` : '')
|
||||
+ `<span class="ffz-ct--obj-close" depth="${depth}">]</span>`;
|
||||
|
||||
const out = [];
|
||||
|
||||
for(const [key, val] of Object.entries(object)) {
|
||||
if ( out.length > 0 )
|
||||
out.push(`<span class="ffz-ct--obj-sep" depth="${depth}">, </span>`);
|
||||
|
||||
if ( pretty )
|
||||
out.push(`\n${indent_inner}`);
|
||||
out.push(`<span class="ffz-ct--obj-key" depth="${depth}">"${sanitize(key)}"</span><span class="ffz-ct--obj-key-sep" depth="${depth}">: </span>`);
|
||||
out.push(highlightJson(val, pretty, depth + 1));
|
||||
}
|
||||
|
||||
return `<span class="ffz-ct--obj-open" depth="${depth}">{</span>${out.join('')}${pretty ? `\n${indent}` : ''}<span class="ffz-ct--obj-close" depth="${depth}">}</span>`;
|
||||
}
|
|
@ -794,247 +794,6 @@ export default class TwitchData extends Module {
|
|||
// Tags
|
||||
// ========================================================================
|
||||
|
||||
memorizeTag(node, dispatch = true) {
|
||||
// We want properly formed tags.
|
||||
if ( ! node || ! node.id || ! node.tagName || ! node.localizedName )
|
||||
return;
|
||||
|
||||
let tag = this.tag_cache.get(node.id);
|
||||
if ( ! tag ) {
|
||||
const match = node.isLanguageTag && LANGUAGE_MATCHER.exec(node.tagName),
|
||||
lang = match && match[1] || null;
|
||||
|
||||
tag = {
|
||||
id: node.id,
|
||||
value: node.id,
|
||||
is_auto: node.isAutomated,
|
||||
is_language: node.isLanguageTag,
|
||||
language: lang,
|
||||
name: node.tagName,
|
||||
scope: node.scope
|
||||
};
|
||||
|
||||
this.tag_cache.set(node.id, tag);
|
||||
}
|
||||
|
||||
if ( node.localizedName )
|
||||
tag.label = node.localizedName;
|
||||
if ( node.localizedDescription )
|
||||
tag.description = node.localizedDescription;
|
||||
|
||||
if ( dispatch && tag.description && this._waiting_tags.has(tag.id) ) {
|
||||
const promises = this._waiting_tags.get(tag.id);
|
||||
this._waiting_tags.delete(tag.id);
|
||||
for(const pair of promises)
|
||||
pair[0](tag);
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
async _loadTags() {
|
||||
if ( this._loading_tags )
|
||||
return;
|
||||
|
||||
this._loading_tags = true;
|
||||
|
||||
// Get the first 50 tags.
|
||||
const ids = [...this._waiting_tags.keys()].slice(0, 50);
|
||||
|
||||
let nodes
|
||||
|
||||
try {
|
||||
const data = await this.queryApollo(
|
||||
await import(/* webpackChunkName: 'queries' */ './data/tags-fetch.gql'),
|
||||
{
|
||||
ids
|
||||
}
|
||||
);
|
||||
|
||||
nodes = get('data.contentTags', data);
|
||||
|
||||
} catch(err) {
|
||||
for(const id of ids) {
|
||||
const promises = this._waiting_tags.get(id);
|
||||
this._waiting_tags.delete(id);
|
||||
|
||||
for(const pair of promises)
|
||||
pair[1](err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const id_set = new Set(ids);
|
||||
|
||||
if ( Array.isArray(nodes) )
|
||||
for(const node of nodes) {
|
||||
const tag = this.memorizeTag(node, false),
|
||||
promises = this._waiting_tags.get(tag.id);
|
||||
|
||||
this._waiting_tags.delete(tag.id);
|
||||
id_set.delete(tag.id);
|
||||
|
||||
if ( promises )
|
||||
for(const pair of promises)
|
||||
pair[0](tag);
|
||||
}
|
||||
|
||||
for(const id of id_set) {
|
||||
const promises = this._waiting_tags.get(id);
|
||||
this._waiting_tags.delete(id);
|
||||
|
||||
for(const pair of promises)
|
||||
pair[0](null);
|
||||
}
|
||||
|
||||
this._loading_tags = false;
|
||||
|
||||
if ( this._waiting_tags.size )
|
||||
this._loadTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries Apollo for tag information
|
||||
* @function getTag
|
||||
* @memberof TwitchData
|
||||
*
|
||||
* @param {int|string} id - the tag id
|
||||
* @param {bool} [want_description=false] - whether the description is also required
|
||||
* @returns {Promise} tag information
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* this.twitch_data.getTag(50).then(function(returnObj){console.log(returnObj);});
|
||||
*/
|
||||
getTag(id, want_description = false) {
|
||||
// Make sure we weren't accidentally handed a tag object.
|
||||
if ( id && id.id )
|
||||
id = id.id;
|
||||
|
||||
if ( this.tag_cache.has(id) ) {
|
||||
const out = this.tag_cache.get(id);
|
||||
if ( out && (out.description || ! want_description) )
|
||||
return Promise.resolve(out);
|
||||
}
|
||||
|
||||
return new Promise((s, f) => {
|
||||
if ( this._waiting_tags.has(id) )
|
||||
this._waiting_tags.get(id).push([s, f]);
|
||||
else {
|
||||
this._waiting_tags.set(id, [[s, f]]);
|
||||
if ( ! this._loading_tags )
|
||||
this._loadTags();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the tag cache for tag information, queries Apollo on cache miss
|
||||
* @function getTagImmediate
|
||||
* @memberof TwitchData
|
||||
*
|
||||
* @param {int|string} id - the tag id
|
||||
* @param {getTagImmediateCallback} callback - callback function for use when requested tag information is not cached
|
||||
* @param {bool} [want_description=false] - whether the tag description is required
|
||||
* @returns {Object|null} tag information object, or on null, expect callback
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* console.log(this.twitch_data.getTagImmediate(50));
|
||||
*/
|
||||
getTagImmediate(id, callback, want_description = false) {
|
||||
// Make sure we weren't accidentally handed a tag object.
|
||||
if ( id && id.id )
|
||||
id = id.id;
|
||||
|
||||
let out = null;
|
||||
if ( this.tag_cache.has(id) )
|
||||
out = this.tag_cache.get(id);
|
||||
|
||||
if ( (want_description && (! out || ! out.description)) || (! out && callback) ) {
|
||||
const promise = this.getTag(id, want_description);
|
||||
if ( callback )
|
||||
promise.then(tag => callback(id, tag)).catch(err => callback(id, null, err));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function used when getTagImmediate experiences a cache miss
|
||||
* @callback getTagImmediateCallback
|
||||
* @param {int} tag_id - The tag ID number
|
||||
* @param {Object} tag_object - the object containing tag data
|
||||
* @param {Object} [error_object] - returned error information on tag data fetch failure
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get top [n] tags
|
||||
* @function getTopTags
|
||||
* @memberof TwitchData
|
||||
* @async
|
||||
*
|
||||
* @param {int|string} limit=50 - the number of tags to return (can be an integer string)
|
||||
* @returns {string[]} an array containing the top tags up to the limit requested
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* console.log(this.twitch_data.getTopTags(20));
|
||||
*/
|
||||
async getTopTags(limit = 50) {
|
||||
const data = await this.queryApollo(
|
||||
await import(/* webpackChunkName: 'queries' */ './data/tags-top.gql'),
|
||||
{limit}
|
||||
);
|
||||
|
||||
const nodes = get('data.topTags', data);
|
||||
if ( ! Array.isArray(nodes) )
|
||||
return [];
|
||||
|
||||
const out = [], seen = new Set;
|
||||
for(const node of nodes) {
|
||||
if ( ! node || seen.has(node.id) )
|
||||
continue;
|
||||
|
||||
seen.add(node.id);
|
||||
out.push(this.memorizeTag(node));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries tag languages
|
||||
* @function getLanguagesFromTags
|
||||
* @memberof TwitchData
|
||||
*
|
||||
* @param {int[]} tags - an array of tag IDs
|
||||
* @returns {string[]} tag information
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* console.log(this.twitch_data.getLanguagesFromTags([50, 53, 58, 84]));
|
||||
*/
|
||||
getLanguagesFromTags(tags, callback) { // TODO: actually use the callback
|
||||
const out = [],
|
||||
fn = callback ? debounce(() => {
|
||||
this.getLanguagesFromTags(tags, callback);
|
||||
}, 16) : null
|
||||
|
||||
if ( Array.isArray(tags) )
|
||||
for(const tag_id of tags) {
|
||||
const tag = this.getTagImmediate(tag_id, fn);
|
||||
if ( tag && tag.is_language ) {
|
||||
const match = LANGUAGE_MATCHER.exec(tag.name);
|
||||
if ( match )
|
||||
out.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search tags
|
||||
* @function getMatchingTags
|
||||
|
@ -1042,34 +801,28 @@ export default class TwitchData extends Module {
|
|||
* @async
|
||||
*
|
||||
* @param {string} query - the search string
|
||||
* @param {string} [locale] - UNUSED. the locale to return tags from
|
||||
* @param {string} [category=null] - the category to return tags from
|
||||
* @returns {string[]} an array containing tags that match the query string
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* console.log(await this.twitch_data.getMatchingTags("Rainbo"));
|
||||
*/
|
||||
async getMatchingTags(query, locale, category = null) {
|
||||
/*if ( ! locale )
|
||||
locale = this.locale;*/
|
||||
|
||||
async getMatchingTags(query) {
|
||||
const data = await this.queryApollo({
|
||||
query: await import(/* webpackChunkName: 'queries' */ './data/search-tags.gql'),
|
||||
query: await import(/* webpackChunkName: 'queries' */ './data/tag-search.gql'),
|
||||
variables: {
|
||||
query,
|
||||
categoryID: category || null,
|
||||
limit: 100
|
||||
first: 100
|
||||
}
|
||||
});
|
||||
|
||||
const nodes = data?.data?.searchLiveTags;
|
||||
if ( ! Array.isArray(nodes) || ! nodes.length )
|
||||
const edges = data?.data?.searchFreeformTags?.edges;
|
||||
if ( ! Array.isArray(edges) || ! edges.length )
|
||||
return [];
|
||||
|
||||
const out = [];
|
||||
for(const node of nodes) {
|
||||
const tag = this.memorizeTag(node);
|
||||
for(const edge of edges) {
|
||||
const tag = edge?.node?.tagName;
|
||||
if ( tag )
|
||||
out.push(tag);
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ export class Vue extends Module {
|
|||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
vue.mixin({
|
||||
methods: {
|
||||
|
|
|
@ -492,6 +492,10 @@ textarea.ffz-input {
|
|||
}
|
||||
|
||||
.ffz--example-report {
|
||||
&.ffz--tall div {
|
||||
max-height: 60rem;
|
||||
}
|
||||
|
||||
div {
|
||||
max-height: 30rem;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -181,6 +181,21 @@ const config = {
|
|||
: '[name].[contenthash:8].json'
|
||||
}
|
||||
},
|
||||
{
|
||||
// This stupid rule goes out to Mozilla, who consistantly
|
||||
// manage to have this one file not included in the bundle
|
||||
// the same way as every other build on every other machine
|
||||
// out of like twelve I've tested. So fine. We'll do it
|
||||
// your way. Whatever. I don't care.
|
||||
test: /entities.json$/,
|
||||
include: /node_modules/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: (FOR_EXTENSION || DEV_BUILD)
|
||||
? '[name].json'
|
||||
: '[name].[contenthash:8].json'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(?:otf|eot|ttf|woff|woff2)$/,
|
||||
use: [{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue