mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.22.5
* Added: Setting to hide unrelated results from the FFZ Control Center when searching. This is now enabled by default. * Added: Setting to control whether the height of the Emote Menu is expanded. This is now disabled by default. * Changed: When searching in the FFZ Control Center, pills are displayed in the navigation tree showing how many matching results there are. * Changed: Update the method used for searching for channels and games, hopefully resulting in more accurate results. * Changed: When searching for users in an auto-complete field, display a check-mark for verified users. * Changed: When searching for users in an auto-complete field, respect the user's preference for rounded avatars. * Fixed: Lazy load Markdown when possible to save on initial download size.
This commit is contained in:
parent
ff4bb24a9a
commit
f0d68527b8
21 changed files with 435 additions and 164 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.22.4",
|
||||
"version": "4.22.5",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<markdown :source="t(item.desc_i18n_key, item.description)" />
|
||||
</section>
|
||||
<div
|
||||
v-for="i in item.contents"
|
||||
v-for="i in visibleContents"
|
||||
:key="i.full_key"
|
||||
:class="{'ffz-unmatched-item': showing && ! shouldShow(i)}"
|
||||
>
|
||||
|
@ -34,6 +34,16 @@ export default {
|
|||
return this.shouldShow(this.item);
|
||||
},
|
||||
|
||||
visibleContents() {
|
||||
if ( ! this.item || ! this.item.contents )
|
||||
return [];
|
||||
|
||||
if ( ! this.context.matches_only )
|
||||
return this.item.contents;
|
||||
|
||||
return this.item.contents.filter(item => this.shouldShow(item));
|
||||
},
|
||||
|
||||
classes() {
|
||||
return [
|
||||
'ffz--menu-container',
|
||||
|
|
|
@ -60,20 +60,21 @@
|
|||
<template v-if="! item.contents || ! item.contents.length || item.always_list_pages">
|
||||
<ul class="tw-border-t tw-pd-y-1">
|
||||
<li
|
||||
v-for="i in item.items"
|
||||
v-for="i in visibleItems"
|
||||
:key="i.full_key"
|
||||
:class="{'ffz-unmatched-item': ! shouldShow(i)}"
|
||||
class="tw-pd-x-1"
|
||||
>
|
||||
<a href="#" @click="$emit('change-item', i, false)">
|
||||
{{ t(i.i18n_key, i.title) }}
|
||||
<span v-if="i.unseen" class="tw-pill">{{ i.unseen }}</span>
|
||||
<span v-if="filter" class="ffz-pill ffz-pill--overlay">{{ countMatches(i) }}</span>
|
||||
<span v-else-if="i.unseen" class="ffz-pill ffz-pill--overlay">{{ i.unseen }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div
|
||||
v-for="i in item.contents"
|
||||
v-for="i in visibleContents"
|
||||
:key="i.full_key"
|
||||
:class="{'ffz-unmatched-item': ! shouldShow(i)}"
|
||||
>
|
||||
|
@ -107,6 +108,26 @@ export default {
|
|||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
visibleItems() {
|
||||
if ( ! this.item || ! this.item.items )
|
||||
return [];
|
||||
|
||||
if ( ! this.context.matches_only )
|
||||
return this.item.items;
|
||||
|
||||
return this.item.items.filter(item => this.shouldShow(item));
|
||||
},
|
||||
|
||||
visibleContents() {
|
||||
if ( ! this.item || ! this.item.contents )
|
||||
return [];
|
||||
|
||||
if ( ! this.context.matches_only )
|
||||
return this.item.contents;
|
||||
|
||||
return this.item.contents.filter(item => this.shouldShow(item));
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -118,6 +139,31 @@ export default {
|
|||
return item.no_filter || item.search_terms.includes(this.filter);
|
||||
},
|
||||
|
||||
countMatches(item, seen) {
|
||||
if ( ! this.filter || ! this.filter.length || ! item )
|
||||
return 0;
|
||||
|
||||
if ( seen && seen.has(item) )
|
||||
return 0;
|
||||
|
||||
if ( ! seen )
|
||||
seen = new Set;
|
||||
|
||||
seen.add(item);
|
||||
|
||||
let count = 0;
|
||||
|
||||
for(const key of ['tabs', 'contents', 'items'])
|
||||
if ( item[key] )
|
||||
for(const thing of item[key])
|
||||
count += this.countMatches(thing, seen);
|
||||
|
||||
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
|
||||
count++;
|
||||
|
||||
return count;
|
||||
},
|
||||
|
||||
markSeen(item) {
|
||||
this.$emit('mark-seen', item);
|
||||
},
|
||||
|
|
|
@ -35,7 +35,10 @@
|
|||
<span class="tw-flex-grow-1">
|
||||
{{ t(item.i18n_key, item.title) }}
|
||||
</span>
|
||||
<span v-if="item.pill" class="ffz-pill ffz-pill--overlay">
|
||||
<span v-if="filter" class="ffz-pill ffz-pill--overlay">
|
||||
{{ countMatches(item) }}
|
||||
</span>
|
||||
<span v-else-if="item.pill" class="ffz-pill ffz-pill--overlay">
|
||||
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill) : item.pill }}
|
||||
</span>
|
||||
<span v-else-if="item.unseen" class="ffz-pill ffz-pill--overlay">
|
||||
|
@ -117,6 +120,31 @@ export default {
|
|||
return false;
|
||||
},
|
||||
|
||||
countMatches(item, seen) {
|
||||
if ( ! this.filter || ! this.filter.length || ! item )
|
||||
return 0;
|
||||
|
||||
if ( seen && seen.has(item) )
|
||||
return 0;
|
||||
|
||||
if ( ! seen )
|
||||
seen = new Set;
|
||||
|
||||
seen.add(item);
|
||||
|
||||
let count = 0;
|
||||
|
||||
for(const key of ['tabs', 'contents', 'items'])
|
||||
if ( item[key] )
|
||||
for(const thing of item[key])
|
||||
count += this.countMatches(thing, seen);
|
||||
|
||||
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
|
||||
count++;
|
||||
|
||||
return count;
|
||||
},
|
||||
|
||||
containsCurrent(item) {
|
||||
let i = this.currentItem;
|
||||
while ( i ) {
|
||||
|
|
|
@ -203,6 +203,15 @@ export default class MainMenu extends Module {
|
|||
force_seen: true
|
||||
});
|
||||
|
||||
this.settings.add('ffz.search.matches-only', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Appearance > Control Center >> Search',
|
||||
title: 'Hide items that do not match completely when searching.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.on('settings:added-definition', (key, definition) => {
|
||||
this._addDefinitionToTree(key, definition);
|
||||
this.scheduleUpdate();
|
||||
|
@ -834,6 +843,7 @@ export default class MainMenu extends Module {
|
|||
can_proxy: context._context.can_proxy,
|
||||
proxied: context._context.proxied,
|
||||
has_update: this.has_update,
|
||||
matches_only: settings.get('ffz.search.matches-only'),
|
||||
mod_icons: context.get('context.chat.showModIcons'),
|
||||
|
||||
setProxied: val => {
|
||||
|
@ -946,9 +956,14 @@ export default class MainMenu extends Module {
|
|||
}
|
||||
},
|
||||
|
||||
_update_settings() {
|
||||
_c.matches_only = settings.get('ffz.search.matches-only');
|
||||
},
|
||||
|
||||
_add_user() {
|
||||
this._users++;
|
||||
if ( this._users === 1 ) {
|
||||
settings.on(':changed:ffz.search.matches-only', this._update_settings, this);
|
||||
settings.on(':profile-toggled', this._profile_toggled, this);
|
||||
settings.on(':profile-created', this._profile_created, this);
|
||||
settings.on(':profile-changed', this._profile_changed, this);
|
||||
|
@ -963,6 +978,7 @@ export default class MainMenu extends Module {
|
|||
_remove_user() {
|
||||
this._users--;
|
||||
if ( this._users === 0 ) {
|
||||
settings.off(':changed:ffz.search.matches-only', this._update_settings, this);
|
||||
settings.off(':profile-toggled', this._profile_toggled, this);
|
||||
settings.off(':profile-created', this._profile_created, this);
|
||||
settings.off(':profile-changed', this._profile_changed, this);
|
||||
|
|
|
@ -9,6 +9,7 @@ import {has, maybe_call, once} from 'utilities/object';
|
|||
|
||||
import Tooltip from 'utilities/tooltip';
|
||||
import Module from 'utilities/module';
|
||||
import awaitMD, {getMD} from 'utilities/markdown';
|
||||
|
||||
export default class TooltipProvider extends Module {
|
||||
constructor(...args) {
|
||||
|
@ -60,9 +61,9 @@ export default class TooltipProvider extends Module {
|
|||
this.types.markdown = (target, tip) => {
|
||||
tip.add_class = 'ffz-tooltip--markdown';
|
||||
|
||||
const md = this.getMarkdown();
|
||||
const md = getMD();
|
||||
if ( ! md )
|
||||
return this.loadMarkdown().then(md => md.render(target.dataset.title));
|
||||
return awaitMD().then(md => md.render(target.dataset.title));
|
||||
|
||||
return md.render(target.dataset.title);
|
||||
};
|
||||
|
@ -71,46 +72,8 @@ export default class TooltipProvider extends Module {
|
|||
this.types.html = target => target.dataset.title;
|
||||
|
||||
this.onFSChange = this.onFSChange.bind(this);
|
||||
|
||||
this.loadMarkdown = once(this.loadMarkdown);
|
||||
|
||||
}
|
||||
|
||||
getMarkdown(callback) {
|
||||
if ( this._md )
|
||||
return this._md;
|
||||
|
||||
if ( callback )
|
||||
this.loadMarkdown().then(md => callback(md));
|
||||
}
|
||||
|
||||
async loadMarkdown() { // eslint-disable-line class-methods-use-this
|
||||
if ( this._md )
|
||||
return this._md;
|
||||
|
||||
const [MD, MILA] = await Promise.all([
|
||||
import(/* webpackChunkName: 'markdown' */ 'markdown-it'),
|
||||
import(/* webpackChunkName: 'markdown' */ 'markdown-it-link-attributes')
|
||||
]);
|
||||
|
||||
const md = this._md = new MD.default({
|
||||
html: false,
|
||||
linkify: true
|
||||
});
|
||||
|
||||
md.use(MILA.default, {
|
||||
attrs: {
|
||||
class: 'ffz-tooltip',
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
'data-tooltip-type': 'link'
|
||||
}
|
||||
});
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
const container = this.getRoot();
|
||||
|
||||
|
|
|
@ -26,26 +26,7 @@
|
|||
class="tw-flex-grow-1"
|
||||
@selected="onSelected"
|
||||
>
|
||||
<div class="tw-pd-x-1 tw-pd-y-05">
|
||||
<div class="tw-card tw-relative">
|
||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
|
||||
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden">
|
||||
<aspect :ratio="1/1.33">
|
||||
<img
|
||||
:alt="slot.item.displayName"
|
||||
:src="slot.item.boxArtURL"
|
||||
class="tw-image"
|
||||
>
|
||||
</aspect>
|
||||
</div>
|
||||
<div class="tw-card-body tw-overflow-hidden tw-relative">
|
||||
<p class="tw-pd-x-1">
|
||||
{{ slot.item.displayName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<autocomplete-game :game="slot.item" />
|
||||
</autocomplete>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -28,26 +28,7 @@
|
|||
class="tw-flex-grow-1"
|
||||
@selected="onSelected"
|
||||
>
|
||||
<div class="tw-pd-x-1 tw-pd-y-05">
|
||||
<div class="tw-card tw-relative">
|
||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
|
||||
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden">
|
||||
<aspect :ratio="1">
|
||||
<img
|
||||
:alt="slot.item.displayName"
|
||||
:src="slot.item.profileImageURL"
|
||||
class="tw-image"
|
||||
>
|
||||
</aspect>
|
||||
</div>
|
||||
<div class="tw-card-body tw-overflow-hidden tw-relative">
|
||||
<p class="tw-pd-x-1">
|
||||
{{ slot.item.displayName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<autocomplete-user :user="slot.item" />
|
||||
</autocomplete>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -222,6 +222,15 @@ export default class EmoteMenu extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.emote-menu.tall', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Chat > Emote Menu >> Appearance',
|
||||
title: 'Use extra height for the emote menu.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.emote-menu.show-heading', {
|
||||
default: 1,
|
||||
ui: {
|
||||
|
@ -1066,6 +1075,7 @@ export default class EmoteMenu extends Module {
|
|||
quickNav: t.chat.context.get('chat.emote-menu.show-quick-nav'),
|
||||
animated: t.chat.context.get('chat.emotes.animated'),
|
||||
showHeading: t.chat.context.get('chat.emote-menu.show-heading'),
|
||||
tall: t.chat.context.get('chat.emote-menu.tall'),
|
||||
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
|
||||
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
|
||||
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
|
||||
|
@ -1217,6 +1227,7 @@ export default class EmoteMenu extends Module {
|
|||
t.chat.context.on('changed:chat.emote-menu.show-heading', this.updateSettingState, this);
|
||||
t.chat.context.on('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
|
||||
t.chat.context.on('changed:chat.emote-menu.show-search', this.updateSettingState, this);
|
||||
t.chat.context.on('changed:chat.emote-menu.tall', this.updateSettingState, this);
|
||||
|
||||
window.ffz_menu = this;
|
||||
}
|
||||
|
@ -1231,6 +1242,7 @@ export default class EmoteMenu extends Module {
|
|||
t.chat.context.off('changed:chat.emote-menu.reduced-padding', this.updateSettingState, this);
|
||||
t.chat.context.off('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
|
||||
t.chat.context.off('changed:chat.emote-menu.show-search', this.updateSettingState, this);
|
||||
t.chat.context.off('changed:chat.emote-menu.tall', this.updateSettingState, this);
|
||||
|
||||
if ( window.ffz_menu === this )
|
||||
window.ffz_menu = null;
|
||||
|
@ -1244,7 +1256,8 @@ export default class EmoteMenu extends Module {
|
|||
showHeading: t.chat.context.get('chat.emote-menu.show-heading'),
|
||||
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
|
||||
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
|
||||
showSearch: t.chat.context.get('chat.emote-menu.show-search')
|
||||
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
|
||||
tall: t.chat.context.get('chat.emote-menu.tall')
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2303,7 +2316,7 @@ export default class EmoteMenu extends Module {
|
|||
return (<div class={`tw-block${this.props.visible ? '' : ' tw-hide'}`} style={{display: this.props.visible ? null : 'none !important'}}>
|
||||
<div class="tw-absolute tw-attached tw-attached--right tw-attached--up">
|
||||
<div
|
||||
class={`ffz-balloon ffz-balloon--auto tw-inline-block tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2 ffz--emote-picker${padding ? ' reduced-padding' : ''}`}
|
||||
class={`ffz-balloon ffz-balloon--auto tw-inline-block tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2 ffz--emote-picker${this.state.tall ? ' ffz--emote-picker__tall' : ''}${padding ? ' reduced-padding' : ''}`}
|
||||
data-a-target="emote-picker"
|
||||
role="dialog"
|
||||
>
|
||||
|
|
|
@ -9,7 +9,7 @@ import {SiteModule} from 'utilities/module';
|
|||
import {createElement, ClickOutside, setChildren} from 'utilities/dom';
|
||||
|
||||
import Twilight from 'site';
|
||||
import getMD from 'src/utilities/markdown';
|
||||
import awaitMD, {getMD} from 'utilities/markdown';
|
||||
|
||||
|
||||
export default class MenuButton extends SiteModule {
|
||||
|
@ -292,6 +292,12 @@ export default class MenuButton extends SiteModule {
|
|||
/*if ( ! data.id )
|
||||
data.id = generateUUID();*/
|
||||
|
||||
if ( data.markdown ) {
|
||||
const md = getMD();
|
||||
if ( ! md )
|
||||
return awaitMD().then(() => this.addToast(data));
|
||||
}
|
||||
|
||||
this.toasts.push(data);
|
||||
// TODO: Sort by ending time?
|
||||
if ( this.toasts.length > 5 )
|
||||
|
@ -385,6 +391,9 @@ export default class MenuButton extends SiteModule {
|
|||
}
|
||||
|
||||
let progress_bar = null;
|
||||
const md = data.markdown ? getMD() : null;
|
||||
if ( data.markdown && ! md )
|
||||
return null;
|
||||
|
||||
if ( data.timeout ) {
|
||||
const now = performance.now();
|
||||
|
|
|
@ -318,32 +318,44 @@
|
|||
}
|
||||
}*/
|
||||
|
||||
.whispers-thread .emote-picker-and-button & .emote-picker__tab-content {
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
.emote-picker__nav-content-overflow,
|
||||
.emote-picker__tab-content {
|
||||
height: unset !important;
|
||||
max-height: 50rem;
|
||||
}
|
||||
|
||||
.whispers-thread {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 750px) {
|
||||
&:not(.ffz--emote-picker__tall) {
|
||||
.emote-picker__nav-content-overflow {
|
||||
height: 100% !important;
|
||||
height: 30.5rem !important;
|
||||
}
|
||||
|
||||
.emote-picker__nav-content-overflow-whisper {
|
||||
height: 18rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ffz--emote-picker__tall {
|
||||
.whispers-thread .emote-picker-and-button & .emote-picker__tab-content {
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
.emote-picker__nav-content-overflow,
|
||||
.emote-picker__tab-content {
|
||||
#root & {
|
||||
max-height: calc(100vh - 31rem);
|
||||
height: unset !important;
|
||||
max-height: 50rem;
|
||||
}
|
||||
|
||||
.whispers-thread {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 750px) {
|
||||
.emote-picker__nav-content-overflow {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
max-height: calc(100vh - 26rem);
|
||||
.emote-picker__nav-content-overflow,
|
||||
.emote-picker__tab-content {
|
||||
#root & {
|
||||
max-height: calc(100vh - 31rem);
|
||||
}
|
||||
|
||||
max-height: calc(100vh - 26rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
22
src/std-components/autocomplete-game.vue
Normal file
22
src/std-components/autocomplete-game.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template functional>
|
||||
<div class="tw-pd-x-1 tw-pd-y-05">
|
||||
<div class="tw-card tw-relative">
|
||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
|
||||
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden">
|
||||
<aspect :ratio="1/1.33">
|
||||
<img
|
||||
:alt="props.game.displayName"
|
||||
:src="props.game.boxArtURL"
|
||||
class="tw-image"
|
||||
>
|
||||
</aspect>
|
||||
</div>
|
||||
<div class="tw-card-body tw-overflow-hidden tw-relative">
|
||||
<p class="tw-pd-x-1">
|
||||
{{ props.game.displayName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
35
src/std-components/autocomplete-user.vue
Normal file
35
src/std-components/autocomplete-user.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template functional>
|
||||
<div class="tw-pd-x-1 tw-pd-y-05">
|
||||
<div class="tw-card tw-relative">
|
||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
|
||||
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden tw-avatar">
|
||||
<aspect :ratio="1">
|
||||
<img
|
||||
:alt="props.user.displayName"
|
||||
:src="props.user.profileImageURL"
|
||||
class="tw-image tw-image-avatar tw-border-radius-rounded"
|
||||
>
|
||||
</aspect>
|
||||
</div>
|
||||
<div class="tw-card-body tw-overflow-hidden tw-relative tw-flex tw-align-items-center tw-flex-grow-1">
|
||||
<div
|
||||
v-if="props.user.login && props.user.displayName && props.user.login.trim() !== props.user.displayName.trim().toLowerCase()"
|
||||
class="tw-flex tw-flex-column tw-mg-x-1 tw-flex-grow-1"
|
||||
>
|
||||
<p>{{ props.user.displayName }}</p>
|
||||
<p class="tw-font-size-8 tw-c-text-alt-2">
|
||||
({{ props.user.login }})
|
||||
</p>
|
||||
</div>
|
||||
<p v-else class="tw-mg-x-1 tw-flex-grow-1">
|
||||
{{ props.user.displayName }}
|
||||
</p>
|
||||
<figure
|
||||
v-if="props.user.roles && props.user.roles.isPartner"
|
||||
class="tw-c-text-link ffz-i-verified"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -129,6 +129,11 @@ export default {
|
|||
logger: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
allowFilter: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -172,7 +177,7 @@ export default {
|
|||
if ( this.errored )
|
||||
return null;
|
||||
|
||||
if ( ! this.search || ! this.search.length )
|
||||
if ( ! this.search || ! this.search.length || ! this.allowFilter )
|
||||
return this.cachedItems;
|
||||
|
||||
const needle = this.search.toLowerCase();
|
||||
|
@ -180,6 +185,9 @@ export default {
|
|||
if ( typeof item.displayName === 'string' && item.displayName.toLowerCase().includes(needle) )
|
||||
return true;
|
||||
|
||||
if ( typeof item.login === 'string' && item.login.toLowerCase().includes(needle) )
|
||||
return true;
|
||||
|
||||
if ( typeof item.label === 'string' && item.label.toLowerCase().includes(needle) )
|
||||
return true;
|
||||
|
||||
|
|
|
@ -5,21 +5,31 @@
|
|||
|
||||
<script>
|
||||
|
||||
import getMD from 'utilities/markdown';
|
||||
import awaitMD, {getMD} from 'utilities/markdown';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
source: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
md() {
|
||||
return getMD();
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
md: getMD()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
output() {
|
||||
if ( ! this.md )
|
||||
return '';
|
||||
|
||||
return this.md.render(this.source);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if ( ! this.md )
|
||||
awaitMD().then(md => this.md = md);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
@keyup.down="nextTab"
|
||||
>
|
||||
<div
|
||||
v-for="(i, idx) in item.tabs"
|
||||
v-for="(i, idx) in visibleTabs"
|
||||
:id="'tab-for-' + i.full_key"
|
||||
:key="i.full_key"
|
||||
:aria-selected="selected === idx"
|
||||
|
@ -30,7 +30,8 @@
|
|||
@click="select(idx)"
|
||||
>
|
||||
{{ t(i.i18n_key, i.title) }}
|
||||
<span v-if="i.unseen > 0" class="tw-pill">{{ i.unseen }}</span>
|
||||
<span v-if="filter" class="ffz-pill ffz-pill--overlay">{{ countMatches(i) }}</span>
|
||||
<span v-else-if="i.unseen" class="ffz-pill ffz-pill--overlay">{{ i.unseen }}</span>
|
||||
</div>
|
||||
</header>
|
||||
<section
|
||||
|
@ -45,7 +46,7 @@
|
|||
{{ t(tab.desc_i18n_key, tab.description) }}
|
||||
</section>
|
||||
<div
|
||||
v-for="i in tab.contents"
|
||||
v-for="i in visibleContents"
|
||||
:key="i.full_key"
|
||||
:class="{'ffz-unmatched-item': showing && ! shouldShow(i)}"
|
||||
>
|
||||
|
@ -79,6 +80,26 @@ export default {
|
|||
|
||||
tab() {
|
||||
return this.item.tabs[this.selected];
|
||||
},
|
||||
|
||||
visibleTabs() {
|
||||
if ( ! this.item || ! this.item.tabs )
|
||||
return [];
|
||||
|
||||
if ( ! this.context.matches_only )
|
||||
return this.item.tabs;
|
||||
|
||||
return this.item.tabs.filter(tab => this.shouldShow(tab));
|
||||
},
|
||||
|
||||
visibleContents() {
|
||||
if ( ! this.tab || ! this.tab.contents )
|
||||
return [];
|
||||
|
||||
if ( ! this.context.matches_only )
|
||||
return this.tab.contents;
|
||||
|
||||
return this.tab.contents.filter(item => this.shouldShow(item));
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -97,6 +118,31 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
countMatches(item, seen) {
|
||||
if ( ! this.filter || ! this.filter.length || ! item )
|
||||
return 0;
|
||||
|
||||
if ( seen && seen.has(item) )
|
||||
return 0;
|
||||
|
||||
if ( ! seen )
|
||||
seen = new Set;
|
||||
|
||||
seen.add(item);
|
||||
|
||||
let count = 0;
|
||||
|
||||
for(const key of ['tabs', 'contents', 'items'])
|
||||
if ( item[key] )
|
||||
for(const thing of item[key])
|
||||
count += this.countMatches(thing, seen);
|
||||
|
||||
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
|
||||
count++;
|
||||
|
||||
return count;
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$el.querySelector('header').focus();
|
||||
},
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
query FFZ_SearchCategory($query: String!, $first: Int, $after: String) {
|
||||
searchFor(userQuery: $query, platform: "web", target: {index: GAME, cursor: $after, limit: $first}) {
|
||||
games {
|
||||
query FFZ_SearchCategory($query: String!, $first: Int, $after: Cursor) {
|
||||
searchCategories(query: $query, first: $first, after: $after) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
cursor
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
items {
|
||||
node {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
query FFZ_SearchUser($query: String!, $first: Int, $after: String) {
|
||||
searchFor(userQuery: $query, platform: "web", target: {index: USER, cursor: $after, limit: $first}) {
|
||||
users {
|
||||
query FFZ_SearchUser($query: String!, $first: Int, $after: Cursor) {
|
||||
searchUsers(userQuery: $query, first: $first, after: $after) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
cursor
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
items {
|
||||
node {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
profileImageURL(width: 50)
|
||||
roles {
|
||||
isPartner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,5 +4,8 @@ query FFZ_UserBulk($ids: [ID!], $logins: [String!]) {
|
|||
login
|
||||
displayName
|
||||
profileImageURL(width: 50)
|
||||
roles {
|
||||
isPartner
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
import MD from 'markdown-it';
|
||||
import MILA from 'markdown-it-link-attributes';
|
||||
import {parse as parse_path} from 'utilities/path-parser';
|
||||
|
||||
let MD, MILA, waiters;
|
||||
let _md;
|
||||
|
||||
function loadMD() {
|
||||
if ( MD )
|
||||
return Promise.resolve(MD);
|
||||
|
||||
return new Promise((s,f) => {
|
||||
if ( waiters )
|
||||
return waiters.push([s,f]);
|
||||
|
||||
waiters = [[s,f]];
|
||||
|
||||
Promise.all([
|
||||
import(/* webpackChunkName: 'markdown' */ 'markdown-it'),
|
||||
import(/* webpackChunkName: 'markdown' */ 'markdown-it-link-attributes')
|
||||
]).then(modules => {
|
||||
console.log('loaded', modules);
|
||||
|
||||
MD = modules[0]?.default;
|
||||
MILA = modules[1]?.default;
|
||||
|
||||
const waited = waiters;
|
||||
waiters = null;
|
||||
|
||||
for(const pair of waited)
|
||||
pair[0](MD);
|
||||
|
||||
}).catch(err => {
|
||||
const waited = waiters;
|
||||
waiters = null;
|
||||
|
||||
for(const pair of waited)
|
||||
pair[1](err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function SettingsLinks(md) {
|
||||
const default_render = md.renderer.rules.link_open || this.defaultRender;
|
||||
|
||||
|
@ -73,8 +108,12 @@ SettingsLinks.defaultRender = function(tokens, idx, options, env, self) {
|
|||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
|
||||
export default function getMD() {
|
||||
export default async function awaitMD() {
|
||||
if ( ! _md ) {
|
||||
const MD = await loadMD();
|
||||
if ( ! MD )
|
||||
return null;
|
||||
|
||||
const md = _md = new MD({
|
||||
html: false,
|
||||
linkify: true
|
||||
|
@ -94,3 +133,10 @@ export default function getMD() {
|
|||
return _md;
|
||||
}
|
||||
|
||||
export function getMD(callback) {
|
||||
if ( _md )
|
||||
return _md;
|
||||
|
||||
if ( callback )
|
||||
awaitMD().then(md => callback(md));
|
||||
}
|
|
@ -10,6 +10,15 @@ import {get, debounce} from 'utilities/object';
|
|||
|
||||
const LANGUAGE_MATCHER = /^auto___lang_(\w+)$/;
|
||||
|
||||
/**
|
||||
* PaginatedResult
|
||||
*
|
||||
* @typedef {Object} PaginatedResult
|
||||
* @property {String} cursor A cursor usable to fetch the next page of results
|
||||
* @property {Object[]} items This page of results
|
||||
* @property {Boolean} finished Whether or not we have reached the end of results.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TwitchData is a container for getting different types of Twitch data
|
||||
* @class TwitchData
|
||||
|
@ -99,28 +108,42 @@ export default class TwitchData extends Module {
|
|||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Queries Apollo for categories matching the search argument
|
||||
* @function getMatchingCategories
|
||||
* @memberof TwitchData
|
||||
* @async
|
||||
* Find categories matching the search query
|
||||
*
|
||||
* @param {string} query - query text to match to a category
|
||||
* @returns {Object} a collection of matches for the query string
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* console.log(this.twitch_data.getMatchingCategories("siege"));
|
||||
* @param {String} query The category name to match
|
||||
* @param {Number} [first=15] How many results to return
|
||||
* @param {String} [cursor=null] A cursor, to be used in fetching the
|
||||
* next page of results.
|
||||
* @returns {PaginatedResult} The results
|
||||
*/
|
||||
async getMatchingCategories(query) {
|
||||
async getMatchingCategories(query, first = 15, cursor = null) {
|
||||
const data = await this.queryApollo(
|
||||
await import(/* webpackChunkName: 'queries' */ './data/search-category.gql'),
|
||||
{ query }
|
||||
{
|
||||
query,
|
||||
first,
|
||||
cursor
|
||||
}
|
||||
);
|
||||
|
||||
const items = get('data.searchCategories.edges.@each.node', data) ?? [],
|
||||
needle = query.toLowerCase();
|
||||
|
||||
if ( Array.isArray(items) )
|
||||
items.sort((a,b) => {
|
||||
const a_match = a && (a.name?.toLowerCase?.() === needle || a?.displayName?.toLowerCase?.() === needle),
|
||||
b_match = a && (b.name?.toLowerCase?.() === needle || b?.displayName?.toLowerCase?.() === needle);
|
||||
|
||||
if ( a_match && ! b_match ) return -1;
|
||||
if ( ! a_match && b_match ) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return {
|
||||
cursor: get('data.searchFor.games.cursor', data),
|
||||
items: get('data.searchFor.games.items', data) || [],
|
||||
finished: ! get('data.searchFor.games.pageInfo.hasNextPage', data)
|
||||
cursor: get('data.searchCategories.edges.@last.cursor', data),
|
||||
items,
|
||||
finished: ! get('data.searchCategories.pageInfo.hasNextPage', data),
|
||||
count: get('data.searchCategories.totalCount', data) || 0
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -153,28 +176,42 @@ export default class TwitchData extends Module {
|
|||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Queries Apollo for users matching the search argument
|
||||
* @function getMatchingUsers
|
||||
* @memberof TwitchData
|
||||
* @async
|
||||
* Find users matching the search query.
|
||||
*
|
||||
* @param {string} query - query text to match to a username
|
||||
* @returns {Object} a collection of matches for the query string
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* console.log(this.twitch_data.getMatchingUsers("ninja"));
|
||||
* @param {String} query Text to match in the login or display name
|
||||
* @param {Number} [first=15] How many results to return
|
||||
* @param {String} [cursor=null] A cursor, to be used in fetching the next
|
||||
* page of results.
|
||||
* @returns {PaginatedResult} The results
|
||||
*/
|
||||
async getMatchingUsers(query) {
|
||||
async getMatchingUsers(query, first = 15, cursor = null) {
|
||||
const data = await this.queryApollo(
|
||||
await import(/* webpackChunkName: 'queries' */ './data/search-user.gql'),
|
||||
{ query }
|
||||
{
|
||||
query,
|
||||
first,
|
||||
cursor
|
||||
}
|
||||
);
|
||||
|
||||
const items = get('data.searchUsers.edges.@each.node', data) ?? [],
|
||||
needle = query.toLowerCase();
|
||||
|
||||
if ( Array.isArray(items) )
|
||||
items.sort((a,b) => {
|
||||
const a_match = a && (a.login?.toLowerCase?.() === needle || a?.displayName?.toLowerCase?.() === needle),
|
||||
b_match = a && (b.login?.toLowerCase?.() === needle || b?.displayName?.toLowerCase?.() === needle);
|
||||
|
||||
if ( a_match && ! b_match ) return -1;
|
||||
if ( ! a_match && b_match ) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return {
|
||||
cursor: get('data.searchFor.users.cursor', data),
|
||||
items: get('data.searchFor.users.items', data) || [],
|
||||
finished: ! get('data.searchFor.users.pageInfo.hasNextPage', data)
|
||||
cursor: get('data.searchUsers.edges.@last.cursor', data),
|
||||
items,
|
||||
finished: ! get('data.searchUsers.pageInfo.hasNextPage', data),
|
||||
count: get('data.searchUsers.totalCount', data) || 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue