1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-09-17 02:16:54 +00:00
* Added: Current Title rule for settings profiles. Matches against the title of the current stream or video. Allows for regular expressions.
* API Added: Added several methods for working with polls to the Twitch data module.
* Changed: Move queries into a separate file to reduce initial load file size.
* Fixed: Remove debug logging from stream up-time metadata.
* Fixed: Display the count of new settings on the FFZ menu button when not disabled.
* Maintenance: Updated to the current version of `displacejs`, no longer using a fork.
This commit is contained in:
SirStendec 2019-12-31 17:44:36 -05:00
parent 51eea310a8
commit a4a16af5e1
14 changed files with 464 additions and 102 deletions

View file

@ -204,11 +204,13 @@ export default {
if ( ! key )
return;
this.editing.push({
const out = {
id: generateUUID(),
type: key,
data: maybe_call(type.default, type)
});
};
this.editing.push(out);
},
updateRule(id, data) {

View file

@ -111,8 +111,6 @@ export default class Metadata extends Module {
subtitle: () => this.i18n.t('metadata.uptime.subtitle', 'Uptime'),
tooltip(data) {
console.log('tool-tip');
if ( ! data.created )
return null;

View file

@ -0,0 +1,109 @@
<template>
<section class="tw-flex-grow-1 tw-align-self-start tw-flex tw-align-items-center">
<div class="tw-flex tw-flex-grow-1 tw-align-items-center">
<div class="tw-mg-r-1">
{{ t(type.i18n, type.title) }}
</div>
<div v-if="! is_valid" class="tw-relative tw-tooltip-wrapper tw-mg-r-05">
<figure class="tw-c-text-error ffz-i-attention" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
{{ t('settings.filter.title.warn-invalid', 'This pattern is invalid.') }}
</div>
</div>
<div v-else-if="! is_safe" class="tw-relative tw-tooltip-wrapper tw-mg-r-05">
<figure class="tw-c-text-error ffz-i-attention" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
{{ t('settings.filter.title.warn-complex', 'This pattern is potentially too complex. It will be disabled to avoid client lag or freezing.') }}
</div>
</div>
<input
:id="'title$' + id"
v-model="value.data.title"
type="text"
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-mg-x-1 tw-pd-x-1 tw-pd-y-05 tw-input"
autocapitalize="off"
autocorrect="off"
>
<select
:id="'mode$' + id"
v-model="value.data.mode"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 ffz-min-width-unset"
>
<option value="text">
{{ t('setting.terms.type.text', 'Text') }}
</option>
<option value="glob">
{{ t('setting.terms.type.glob', 'Glob') }}
</option>
<option value="raw">
{{ t('setting.terms.type.regex', 'Regex') }}
</option>
</select>
<div class="tw-flex tw-align-items-center tw-checkbox tw-mg-l-1 tw-mg-y-05">
<input
:id="'sensitive$' + id"
v-model="value.data.sensitive"
type="checkbox"
class="ffz-min-width-unset tw-checkbox__input"
>
<label :for="'sensitive$' + id" class="tw-checkbox__label tw-relative tw-tooltip-wrapper">
Aa
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('settings.filter.title.sensitive', 'Case Sensitive') }}
</div>
</label>
</div>
</div>
</section>
</template>
<script>
import safety from 'safe-regex';
import {glob_to_regex, escape_regex} from 'utilities/object';
let last_id = 0;
export default {
props: ['value', 'type', 'filters', 'context'],
data() {
return {
id: last_id++
}
},
computed: {
regex() {
const mode = this.value.data.mode;
let v = this.value.data.title;
if ( mode === 'text' )
v = escape_regex(v);
else if ( mode === 'glob' )
v = glob_to_regex(v);
return v;
},
is_valid() {
try {
new RegExp(this.regex, `g${this.value.data.sensitive ? '' : 'i'}`);
return true;
} catch(err) {
return false;
}
},
is_safe() {
return safety(this.regex)
}
}
}
</script>

View file

@ -4,8 +4,12 @@
// Profile Filters for Settings
// ============================================================================
import safety from 'safe-regex';
import {glob_to_regex, escape_regex} from 'utilities/object';
import {createTester} from 'utilities/filtering';
// Logical Components
export const Invert = {
@ -282,4 +286,44 @@ export const Category = {
}),
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/category.vue')
}
}
export const Title = {
createTest(config = {}) {
const mode = config.mode;
let title = config.title;
if ( ! title || ! mode )
return () => false;
if ( mode === 'text' )
title = escape_regex(title);
else if ( mode === 'glob' )
title = glob_to_regex(title);
else if ( mode !== 'raw' )
return () => false;
if ( ! safety(title) )
return () => false;
let regex;
try {
regex = new RegExp(title, `g${config.sensitive ? '' : 'i'}`);
} catch(err) {
return () => false;
}
return ctx => ctx.title && regex.test(ctx.title)
},
title: 'Current Title',
i18n: 'settings.filter.title',
default: () => ({
title: '',
mode: 'text',
sensitive: false
}),
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/title.vue')
};

View file

@ -127,12 +127,14 @@ export default class Channel extends Module {
channelID: null,
channelColor: null,
category: null,
categoryID: null
categoryID: null,
title: null
});
});
this.ChannelPage.on('update', inst => {
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.broadcastSettings.game', inst);
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.stream.game', inst) || get('state.channel.broadcastSettings.game', inst),
title = get('state.video.title', inst) || get('state.clip.title', inst) || get('state.channel.stream.title', inst) || get('state.channel.broadcastSettings.title', inst);
const color = get('state.primaryColorHex', inst);
this.updateChannelColor(color);
@ -142,7 +144,8 @@ export default class Channel extends Module {
channelID: get('state.channel.id', inst),
channelColor: color,
category: category?.name,
categoryID: category?.id
categoryID: category?.id,
title
});
if ( this.settings.get('channel.hosting.enable') || has(inst.state, 'hostMode') || has(inst.state, 'hostedChannel') )
@ -174,7 +177,8 @@ export default class Channel extends Module {
clip = inst.state.clip,
video = inst.state.video,
category = video?.game || clip?.game || channel?.stream?.game || channel?.broadcastSettings?.game;
category = video?.game || clip?.game || channel?.stream?.game || channel?.broadcastSettings?.game,
title = video?.title || clip?.title || channel?.stream?.title || channel?.broadcastSettings?.title || null;
const color = inst.state?.primaryColorHex;
this.updateChannelColor(color);
@ -184,7 +188,8 @@ export default class Channel extends Module {
channelID: inst.state.channel?.id,
channelColor: color,
category: category?.name,
categoryID: category?.id
categoryID: category?.id,
title
});
}
@ -195,7 +200,8 @@ export default class Channel extends Module {
channelID: null,
category: null,
categoryID: null,
channelColor: null
channelColor: null,
title: null
});
}
@ -203,7 +209,8 @@ export default class Channel extends Module {
onChannelMounted(inst) {
this.wrapChannelPage(inst);
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.broadcastSettings.game', inst);
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.stream.game', inst) || get('state.channel.broadcastSettings.game', inst),
title = get('state.video.title', inst) || get('state.clip.title', inst) || get('state.channel.stream.title', inst) || get('state.channel.broadcastSettings.title', inst) || null;
const color = get('state.primaryColorHex', inst);
this.updateChannelColor(color);
@ -213,7 +220,8 @@ export default class Channel extends Module {
channelID: get('state.channel.id', inst),
channelColor: color,
category: category?.name,
categoryID: category?.id
categoryID: category?.id,
title
});
}

View file

@ -145,6 +145,9 @@ export default class MenuButton extends SiteModule {
if ( typeof val === 'number' )
return this.i18n.formatNumber(val);
if ( val == null && this.has_new )
return this.i18n.formatNumber(this._new_settings);
return val;
}
@ -300,7 +303,7 @@ export default class MenuButton extends SiteModule {
{this.has_update && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.update-desc', 'There is an update available. Please refresh your page.')}
</div>)}
{this.has_new && (<div class="tw-mg-t-1">
{this._new_settings > 0 && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.new-desc', 'There {count,plural,one {is one new setting} other {are # new settings}}.', {count: this._new_settings})}
</div>)}
{this.has_strings && (<div class="tw-mg-t-1">

View file

@ -6,6 +6,7 @@
.ffz-menu__pill {
top: -.5rem;
right: 0;
pointer-events: none;
}
.ffz-menu__extra-pill {

View file

@ -0,0 +1,8 @@
mutation FFZ_ArchivePoll($id: ID!) {
archivePoll(input: {pollID: $id}) {
poll {
id
status
}
}
}

View file

@ -0,0 +1,12 @@
mutation FFZ_CreatePoll($input: CreatePollInput!) {
createPoll(input: $input) {
poll {
id
status
choices {
id
title
}
}
}
}

View file

@ -0,0 +1,25 @@
query FFZ_FetchPoll($id: ID!) {
poll(id: $id) {
id
status
title
ownedBy {
id
login
displayName
}
endedAt
startedAt
totalVoters
choices {
id
title
votes {
id
base
bits
total
}
}
}
}

View file

@ -0,0 +1,8 @@
mutation FFZ_TerminatePoll($id: ID!) {
terminatePoll(input: {pollID: $id}) {
poll {
id
status
}
}
}

View file

@ -89,6 +89,23 @@ export default class TwitchData extends Module {
return this.apollo.client.query(thing);
}
mutate(mutation, variables, options) {
let thing;
if ( ! variables && ! options && mutation.mutation )
thing = mutation;
else {
thing = {
mutation,
variables
};
if ( options )
thing = Object.assign(thing, options);
}
return this.apollo.client.mutate(thing);
}
get languageCode() {
const session = this.site.getSession();
return session && session.languageCode || 'en'
@ -130,7 +147,7 @@ export default class TwitchData extends Module {
async getMatchingCategories(query) {
const data = await this.queryApollo(
require('./data/search-category.gql'),
await import(/* webpackChunkName: 'queries' */ './data/search-category.gql'),
{ query }
);
@ -143,7 +160,7 @@ export default class TwitchData extends Module {
async getCategory(id, name) {
const data = await this.queryApollo(
require('./data/category-fetch.gql'),
await import(/* webpackChunkName: 'queries' */ './data/category-fetch.gql'),
{ id, name }
);
@ -157,7 +174,7 @@ export default class TwitchData extends Module {
async getMatchingUsers(query) {
const data = await this.queryApollo(
require('./data/search-user.gql'),
await import(/* webpackChunkName: 'queries' */ './data/search-user.gql'),
{ query }
);
@ -170,7 +187,7 @@ export default class TwitchData extends Module {
async getUser(id, login) {
const data = await this.queryApollo(
require('./data/user-fetch.gql'),
await import(/* webpackChunkName: 'queries' */ './data/user-fetch.gql'),
{ id, login }
);
@ -179,7 +196,7 @@ export default class TwitchData extends Module {
async getLastBroadcast(id, login) {
const data = await this.queryApollo(
require('./data/last-broadcast.gql'),
await import(/* webpackChunkName: 'queries' */ './data/last-broadcast.gql'),
{ id, login }
);
@ -192,7 +209,7 @@ export default class TwitchData extends Module {
async getBroadcastID(id, login) {
const data = await this.queryApollo({
query: require('./data/broadcast-id.gql'),
query: await import(/* webpackChunkName: 'queries' */ './data/broadcast-id.gql'),
variables: {
id,
login
@ -203,6 +220,77 @@ export default class TwitchData extends Module {
}
// ========================================================================
// Polls
// ========================================================================
async getPoll(poll_id) {
const data = await this.queryApollo({
query: await import(/* webpackChunkName: 'queries' */ './data/poll-get.gql'),
variables: {
id: poll_id
}
});
return get('data.poll', data);
}
async createPoll(channel_id, title, choices, options = {}) {
if ( typeof title !== 'string' )
throw new TypeError('title must be string');
if ( ! Array.isArray(choices) || choices.some(x => typeof x !== 'string') )
throw new TypeError('choices must be array of strings');
let bits = options.bits || 0,
duration = options.duration || 60;
if ( typeof bits !== 'number' || bits < 0 )
bits = 0;
if ( typeof duration !== 'number' || duration < 0 )
duration = 60;
const data = await this.mutate({
mutation: await import(/* webpackChunkName: 'queries' */ './data/poll-create.gql'),
variables: {
input: {
bitsCost: bits,
bitsVoting: bits > 0,
choices: choices.map(x => ({title: x})),
durationSeconds: duration,
ownedBy: `${channel_id}`,
subscriberMultiplier: options.subscriberMultiplier || false,
subscriberOnly: options.subscriberOnly || false,
title
}
}
});
return get('data.createPoll.poll', data);
}
async archivePoll(poll_id) {
const data = await this.mutate({
mutation: await import(/* webpackChunkName: 'queries' */ './data/poll-archive.gql'),
variables: {
id: poll_id
}
});
return get('data.archivePoll.poll', data);
}
async terminatePoll(poll_id) {
const data = await this.mutate({
mutation: await import(/* webpackChunkName: 'queries' */ './data/poll-terminate.gql'),
variables: {
id: poll_id
}
});
return get('data.terminatePoll.poll', data);
}
// ========================================================================
// Stream Up-Type (Uptime and Type, for Directory Purposes)
// ========================================================================
@ -242,7 +330,7 @@ export default class TwitchData extends Module {
try {
const data = await this.queryApollo({
query: require('./data/stream-fetch.gql'),
query: await import(/* webpackChunkName: 'queries' */ './data/stream-fetch.gql'),
variables: {
ids: ids.length ? ids : null,
logins: logins.length ? logins : null
@ -376,7 +464,7 @@ export default class TwitchData extends Module {
try {
const data = await this.queryApollo(
require('./data/tags-fetch.gql'),
await import(/* webpackChunkName: 'queries' */ './data/tags-fetch.gql'),
{
ids
}
@ -467,7 +555,7 @@ export default class TwitchData extends Module {
async getTopTags(limit = 50) {
const data = await this.queryApollo(
require('./data/tags-top.gql'),
await import(/* webpackChunkName: 'queries' */ './data/tags-top.gql'),
{limit}
);