mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-10-15 07:21:58 +00:00
4.4.1
* Added: `Current Channel` rule for profiles, to match all pages associated with a certain channel without needing many page rules. * Fixed: Unreadable text in light theme when importing a profile. * Changed: Display a matching page URL in the `Current Page` rule for profiles. * Changed: Do not display an inactive profile warning on the Add-Ons settings page, since those are not affected by profiles. * Changed: Update Vue to a more recent version. * Maintenance: Update the chat types enum based on the latest version of Twitch. * API Added: `TwitchData` module (`site.twitch_data`) for querying Twitch's API for data.
This commit is contained in:
parent
c34b7e30e2
commit
275248ca36
24 changed files with 819 additions and 88 deletions
|
@ -40,12 +40,7 @@ export default class FineRouter extends Module {
|
|||
}
|
||||
|
||||
navigate(route, data, opts) {
|
||||
const r = this.routes[route];
|
||||
if ( ! r )
|
||||
throw new Error(`unable to find route "${route}"`);
|
||||
|
||||
const url = r.url(data, opts);
|
||||
this.history.push(url);
|
||||
this.history.push(this.getURL(route, data, opts));
|
||||
}
|
||||
|
||||
_navigateTo(location) {
|
||||
|
@ -55,6 +50,11 @@ export default class FineRouter extends Module {
|
|||
return;
|
||||
|
||||
this.location = path;
|
||||
this._pickRoute();
|
||||
}
|
||||
|
||||
_pickRoute() {
|
||||
const path = this.location;
|
||||
|
||||
for(const route of this.__routes) {
|
||||
const match = route.regex.exec(path);
|
||||
|
@ -73,6 +73,14 @@ export default class FineRouter extends Module {
|
|||
this.emit(':route', null, null);
|
||||
}
|
||||
|
||||
getURL(route, data, opts) {
|
||||
const r = this.routes[route];
|
||||
if ( ! r )
|
||||
throw new Error(`unable to find route "${route}"`);
|
||||
|
||||
return r.url(data, opts);
|
||||
}
|
||||
|
||||
getRoute(name) {
|
||||
return this.routes[name];
|
||||
}
|
||||
|
@ -92,26 +100,35 @@ export default class FineRouter extends Module {
|
|||
return this.route_names[route];
|
||||
}
|
||||
|
||||
routeName(route, name) {
|
||||
routeName(route, name, process = true) {
|
||||
if ( typeof route === 'object' ) {
|
||||
for(const key in route)
|
||||
if ( has(route, key) )
|
||||
this.routeName(key, route[key]);
|
||||
this.routeName(key, route[key], false);
|
||||
|
||||
if ( process )
|
||||
this.emit(':updated-route-names');
|
||||
return;
|
||||
}
|
||||
|
||||
this.route_names[route] = name;
|
||||
|
||||
if ( process )
|
||||
this.emit(':updated-route-names');
|
||||
}
|
||||
|
||||
route(name, path, sort = true) {
|
||||
route(name, path, process = true) {
|
||||
if ( typeof name === 'object' ) {
|
||||
for(const key in name)
|
||||
if ( has(name, key) )
|
||||
this.route(key, name[key], false);
|
||||
|
||||
if ( sort )
|
||||
if ( process ) {
|
||||
this.__routes.sort((a,b) => b.score - a.score);
|
||||
if ( this.location )
|
||||
this._pickRoute();
|
||||
this.emit(':updated-routes');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -130,7 +147,11 @@ export default class FineRouter extends Module {
|
|||
}
|
||||
|
||||
this.__routes.push(route);
|
||||
if ( sort )
|
||||
if ( process ) {
|
||||
this.__routes.sort((a,b) => b.score - a.score);
|
||||
if ( this.location )
|
||||
this._pickRoute();
|
||||
this.emit(':updated-routes');
|
||||
}
|
||||
}
|
||||
}
|
16
src/utilities/data/search-category.gql
Normal file
16
src/utilities/data/search-category.gql
Normal file
|
@ -0,0 +1,16 @@
|
|||
query FFZ_SearchCategory($query: String!, $first: Int, $after: String) {
|
||||
searchFor(userQuery: $query, platform: "web", target: {index: GAME, cursor: $after, limit: $first}) {
|
||||
games {
|
||||
cursor
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
items {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
boxArtURL(width: 40, height: 56)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
src/utilities/data/search-user.gql
Normal file
16
src/utilities/data/search-user.gql
Normal file
|
@ -0,0 +1,16 @@
|
|||
query FFZ_SearchUser($query: String!, $first: Int, $after: String) {
|
||||
searchFor(userQuery: $query, platform: "web", target: {index: USER, cursor: $after, limit: $first}) {
|
||||
users {
|
||||
cursor
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
items {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
profileImageURL(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/utilities/data/tags-fetch.gql
Normal file
9
src/utilities/data/tags-fetch.gql
Normal file
|
@ -0,0 +1,9 @@
|
|||
query FFZ_FetchTags($ids: [ID!]) {
|
||||
contentTags(ids: $ids) {
|
||||
id
|
||||
isLanguageTag
|
||||
tagName
|
||||
localizedName
|
||||
localizedDescription
|
||||
}
|
||||
}
|
9
src/utilities/data/tags-top.gql
Normal file
9
src/utilities/data/tags-top.gql
Normal file
|
@ -0,0 +1,9 @@
|
|||
query FFZ_TopTags($limit: Int) {
|
||||
topTags(limit: $limit) {
|
||||
id
|
||||
isLanguageTag
|
||||
tagName
|
||||
localizedName
|
||||
localizedDescription
|
||||
}
|
||||
}
|
11
src/utilities/data/user-fetch.gql
Normal file
11
src/utilities/data/user-fetch.gql
Normal file
|
@ -0,0 +1,11 @@
|
|||
query FFZ_FetchUser($id: ID, $login: String) {
|
||||
user(id: $id, login: $login) {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
profileImageURL(width: 50)
|
||||
roles {
|
||||
isPartner
|
||||
}
|
||||
}
|
||||
}
|
306
src/utilities/twitch-data.js
Normal file
306
src/utilities/twitch-data.js
Normal file
|
@ -0,0 +1,306 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Twitch Data
|
||||
// Get data, from Twitch.
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {get, debounce, generateUUID} from 'utilities/object';
|
||||
|
||||
const LANGUAGE_MATCHER = /^auto___lang_(\w+)$/;
|
||||
|
||||
export default class TwitchData extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('site');
|
||||
this.inject('site.apollo');
|
||||
this.inject('site.web_munch');
|
||||
|
||||
this.tag_cache = new Map;
|
||||
this._waiting_tags = new Map;
|
||||
|
||||
this._loadTags = debounce(this._loadTags.bind(this), 50);
|
||||
}
|
||||
|
||||
queryApollo(query, variables, options) {
|
||||
let thing;
|
||||
if ( ! variables && ! options && query.query )
|
||||
thing = query;
|
||||
else {
|
||||
thing = {
|
||||
query,
|
||||
variables
|
||||
};
|
||||
|
||||
if ( options )
|
||||
thing = Object.assign(thing, options);
|
||||
}
|
||||
|
||||
return this.apollo.client.query(thing);
|
||||
}
|
||||
|
||||
get languageCode() {
|
||||
const session = this.site.getSession();
|
||||
return session && session.languageCode || 'en'
|
||||
}
|
||||
|
||||
get locale() {
|
||||
const session = this.site.getSession();
|
||||
return session && session.locale || 'en-US'
|
||||
}
|
||||
|
||||
get searchClient() {
|
||||
if ( this._search )
|
||||
return this._search;
|
||||
|
||||
const apollo = this.apollo.client,
|
||||
core = this.listeners.getCore(),
|
||||
|
||||
search_module = this.web_munch.getModule('algolia-search'),
|
||||
SearchClient = search_module && search_module.a;
|
||||
|
||||
if ( ! SearchClient || ! apollo || ! core )
|
||||
return null;
|
||||
|
||||
this._search = new SearchClient({
|
||||
appId: core.config.algoliaApplicationID,
|
||||
apiKey: core.config.algoliaAPIKey,
|
||||
apolloClient: apollo,
|
||||
logger: core.logger,
|
||||
config: core.config,
|
||||
stats: core.stats
|
||||
});
|
||||
|
||||
return this._search;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Categories
|
||||
// ========================================================================
|
||||
|
||||
async getMatchingCategories(query) {
|
||||
const data = await this.queryApollo(
|
||||
require('./data/search-category.gql'),
|
||||
{ query }
|
||||
);
|
||||
|
||||
return {
|
||||
cursor: get('data.searchFor.games.cursor', data),
|
||||
items: get('data.searchFor.games.items', data) || [],
|
||||
finished: ! get('data.searchFor.games.pageInfo.hasNextPage', data)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Users
|
||||
// ========================================================================
|
||||
|
||||
async getMatchingUsers(query) {
|
||||
const data = await this.queryApollo(
|
||||
require('./data/search-user.gql'),
|
||||
{ query }
|
||||
);
|
||||
|
||||
return {
|
||||
cursor: get('data.searchFor.users.cursor', data),
|
||||
items: get('data.searchFor.users.items', data) || [],
|
||||
finished: ! get('data.searchFor.users.pageInfo.hasNextPage', data)
|
||||
};
|
||||
}
|
||||
|
||||
async getUser(id, login) {
|
||||
const data = await this.queryApollo(
|
||||
require('./data/user-fetch.gql'),
|
||||
{ id, login }
|
||||
);
|
||||
|
||||
return get('data.user', data);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Tags
|
||||
// ========================================================================
|
||||
|
||||
async _loadTags() {
|
||||
if ( this._loading_tags )
|
||||
return;
|
||||
|
||||
this._loading_tags = true;
|
||||
const processing = this._waiting_tags;
|
||||
this._waiting_tags = new Map;
|
||||
|
||||
try {
|
||||
const data = await this.queryApollo(
|
||||
require('./data/tags-fetch.gql'),
|
||||
{
|
||||
ids: [...processing.keys()]
|
||||
}
|
||||
);
|
||||
|
||||
const nodes = get('data.contentTags', data);
|
||||
if ( Array.isArray(nodes) )
|
||||
for(const node of nodes) {
|
||||
const tag = {
|
||||
id: node.id,
|
||||
value: node.id,
|
||||
is_language: node.isLanguageTag,
|
||||
name: node.tagName,
|
||||
label: node.localizedName,
|
||||
description: node.localizedDescription
|
||||
};
|
||||
|
||||
this.tag_cache.set(tag.id, tag);
|
||||
const promises = processing.get(tag.id);
|
||||
if ( promises )
|
||||
for(const pair of promises)
|
||||
pair[0](tag);
|
||||
|
||||
promises.delete(tag.id);
|
||||
}
|
||||
|
||||
for(const promises of processing.values())
|
||||
for(const pair of promises)
|
||||
pair[0](null);
|
||||
|
||||
} catch(err) {
|
||||
for(const promises of processing.values())
|
||||
for(const pair of promises)
|
||||
pair[1](err);
|
||||
}
|
||||
|
||||
this._loading_tags = false;
|
||||
|
||||
if ( this._waiting_tags.size )
|
||||
this._loadTags();
|
||||
}
|
||||
|
||||
getTag(id, want_description = false) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTagImmediate(id, callback, want_description = false) {
|
||||
let out = null;
|
||||
if ( this.tag_cache.has(id) )
|
||||
out = this.tag_cache.get(id);
|
||||
|
||||
if ( ! out || (want_description && ! out.description) )
|
||||
this.getTag(id, want_description).then(tag => callback(id, tag)).catch(err => callback(id, null, err));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async getTopTags(limit = 50) {
|
||||
const data = await this.queryApollo(
|
||||
require('./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);
|
||||
const tag = {
|
||||
id: node.id,
|
||||
value: node.id,
|
||||
is_language: node.isLanguageTag,
|
||||
name: node.tagName,
|
||||
label: node.localizedName,
|
||||
description: node.localizedDescription
|
||||
};
|
||||
|
||||
this.tag_cache.set(tag.id, tag);
|
||||
out.push(tag);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
getLanguagesFromTags(tags, 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;
|
||||
}
|
||||
|
||||
async getMatchingTags(query, locale) {
|
||||
if ( ! locale )
|
||||
locale = this.locale;
|
||||
|
||||
const data = await this.searchClient.queryForType(
|
||||
'tag', query, generateUUID(), {
|
||||
hitsPerPage: 100,
|
||||
facetFilters: [
|
||||
|
||||
],
|
||||
restrictSearchableAttributes: [
|
||||
`localizations.${locale}`,
|
||||
'tag_name'
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
const nodes = get('streamTags.hits', data);
|
||||
if ( ! Array.isArray(nodes) )
|
||||
return [];
|
||||
|
||||
const out = [], seen = new Set;
|
||||
for(const node of nodes) {
|
||||
if ( ! node || seen.has(node.tag_id) )
|
||||
continue;
|
||||
|
||||
seen.add(node.tag_id);
|
||||
if ( ! this.tag_cache.has(node.tag_id) ) {
|
||||
const tag = {
|
||||
id: node.tag_id,
|
||||
value: node.tag_id,
|
||||
is_language: node.tag_name && LANGUAGE_MATCHER.test(node.tag_name),
|
||||
label: node.localizations && (node.localizations[locale] || node.localizations['en-us']) || node.tag_name
|
||||
};
|
||||
|
||||
this.tag_cache.set(tag.id);
|
||||
out.push(tag);
|
||||
|
||||
} else {
|
||||
out.push(this.tag_cache.get(node.tag_id));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue