mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-01 15:38:31 +00:00
4.20.42
* Added: Settings page for editing the list of categories that are Blocked or have Hidden Thumbnails. * Changed: When popping out the FFZ Control Center, it now remembers which page it was on. * Fixed: Emoji-related features not functioning correctly if the emoji style is set to an invalid value. (Closes #923) * Fixed: Messages not being highlighted when a viewer card is open. (Closes #920) * Fixed: Disable minimal navigation for Watch Parties. (Closes #916) * Fixed: Block and Hide Thumbnails controls not appearing on appropriate directory pages. * Fixed: Hide Thumbnails not working for a category when on that category's directory page. * Fixed: Move up theater metadata to avoid it overlapping with the seek preview when viewing videos. * Fixed: Various instances of `occurred` being spelled incorrectly. * Fixed: Setting incorrectly named `Show Twitch Prime offers.` has been renamed to `Show Prime Gaming Loot.` to reflect Amazon's branding changes. * API Added: `ProviderMixin` for creating Vue components for settings that are set directly to the settings provider. See the new `games-list-editor` for an example. * API Added: Select box and combo box settings now have an optional `no_i18n` property to avoid i18n keys being automatically generated for their entries.
This commit is contained in:
parent
53b22d8a09
commit
b88e6f0f75
17 changed files with 343 additions and 31 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.41",
|
||||
"version": "4.20.42",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -78,6 +78,11 @@ export default class Emoji extends Module {
|
|||
|
||||
this.settings.add('chat.emoji.style', {
|
||||
default: 'twitter',
|
||||
process(ctx, val) {
|
||||
if ( val != 0 && ! IMAGE_PATHS[val] )
|
||||
return 'twitter';
|
||||
return val;
|
||||
},
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Emoji',
|
||||
title: 'Emoji Style',
|
||||
|
|
|
@ -1665,12 +1665,12 @@ export default class Chat extends Module {
|
|||
if ( data.error )
|
||||
data = {
|
||||
v: 5,
|
||||
title: this.i18n.t('card.error', 'An error occured.'),
|
||||
title: this.i18n.t('card.error', 'An error occurred.'),
|
||||
description: data.error,
|
||||
short: {
|
||||
type: 'header',
|
||||
image: {type: 'image', url: ERROR_IMAGE},
|
||||
title: {type: 'i18n', key: 'card.error', phrase: 'An error occured.'},
|
||||
title: {type: 'i18n', key: 'card.error', phrase: 'An error occurred.'},
|
||||
subtitle: data.error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ export default {
|
|||
|
||||
if ( ! response.ok ) {
|
||||
this.uploading = false;
|
||||
this.url = 'An error occured.';
|
||||
this.url = 'An error occurred.';
|
||||
}
|
||||
|
||||
this.url = await response.text();
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
|
||||
<div class="tw-align-center tw-pd-1">
|
||||
<div v-if="error">
|
||||
{{ t('home.changelog.error', 'An error occured loading changes from GitHub.') }}
|
||||
{{ t('home.changelog.error', 'An error occurred loading changes from GitHub.') }}
|
||||
</div>
|
||||
<h1 v-else-if="loading" class="tw-mg-5 ffz-i-zreknarf loading" />
|
||||
<div v-else-if="! more">
|
||||
|
|
157
src/modules/main_menu/components/game-list-editor.vue
Normal file
157
src/modules/main_menu/components/game-list-editor.vue
Normal file
|
@ -0,0 +1,157 @@
|
|||
<template lang="html">
|
||||
<section class="ffz--widget ffz--game-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="'category$' + id"
|
||||
:items="fetchCategories"
|
||||
:suggest-on-focus="true"
|
||||
:escape-to-clear="false"
|
||||
class="tw-flex-grow-1"
|
||||
>
|
||||
<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="tw-card-img tw-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>
|
||||
</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/game/${i}`"
|
||||
class="tw-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 tw-tooltip-wrapper" @click="remove(i)">
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import ProviderMixin from '../provider-mixin';
|
||||
import {deep_copy} from 'utilities/object';
|
||||
|
||||
let last_id = 0;
|
||||
|
||||
export default {
|
||||
mixins: [ProviderMixin],
|
||||
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 = ! ffz.resolve('main_menu').exclusive;
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
if ( ! this.adding?.length )
|
||||
return;
|
||||
|
||||
const values = Array.from(this.value);
|
||||
if ( values.includes(this.adding) )
|
||||
return;
|
||||
|
||||
values.push(this.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 fetchCategories(query) {
|
||||
if ( ! this.loader )
|
||||
return [];
|
||||
|
||||
const data = await this.loader.getMatchingCategories(query);
|
||||
if ( ! data || ! data.items )
|
||||
return [];
|
||||
|
||||
return deep_copy(data.items);
|
||||
},
|
||||
|
||||
onSelected(...args) {
|
||||
console.log('selected', args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -10,7 +10,8 @@ import {get, has, deep_copy} from 'utilities/object';
|
|||
|
||||
import Dialog from 'utilities/dialog';
|
||||
|
||||
import Mixin from './setting-mixin';
|
||||
import SettingMixin from './setting-mixin';
|
||||
import ProviderMixin from './provider-mixin';
|
||||
|
||||
import {parse_path} from 'src/settings';
|
||||
|
||||
|
@ -32,7 +33,8 @@ export default class MainMenu extends Module {
|
|||
|
||||
this.load_requires = ['vue'];
|
||||
|
||||
this.Mixin = Mixin;
|
||||
this.Mixin = this.SettingMixin = SettingMixin;
|
||||
this.ProviderMixin = ProviderMixin;
|
||||
|
||||
//this.should_enable = true;
|
||||
|
||||
|
@ -134,9 +136,9 @@ export default class MainMenu extends Module {
|
|||
this.scheduleUpdate();
|
||||
}
|
||||
|
||||
openPopout() {
|
||||
openPopout(item) {
|
||||
const win = window.open(
|
||||
'https://twitch.tv/popout/frankerfacez/chat?ffz-settings',
|
||||
`https://twitch.tv/popout/frankerfacez/chat?ffz-settings${item ? `=${encodeURIComponent(item)}` : ''}`,
|
||||
'_blank',
|
||||
'resizable=yes,scrollbars=yes,width=850,height=600'
|
||||
);
|
||||
|
@ -633,6 +635,7 @@ export default class MainMenu extends Module {
|
|||
const t = this,
|
||||
Vue = this.vue.Vue,
|
||||
settings = this.settings,
|
||||
provider = settings.provider,
|
||||
context = settings.main_context,
|
||||
[profiles, profile_keys] = this.getProfiles();
|
||||
|
||||
|
@ -665,6 +668,16 @@ export default class MainMenu extends Module {
|
|||
|
||||
getFFZ: () => t.resolve('core'),
|
||||
|
||||
provider: {
|
||||
unwrap: () => provider,
|
||||
get: (...args) => provider.get(...args),
|
||||
set: (...args) => provider.set(...args),
|
||||
delete: (...args) => provider.delete(...args),
|
||||
has: (...args) => provider.has(...args),
|
||||
on: (...args) => provider.on(...args),
|
||||
off: (...args) => provider.off(...args)
|
||||
},
|
||||
|
||||
context: {
|
||||
_users: 0,
|
||||
|
||||
|
@ -924,17 +937,17 @@ export default class MainMenu extends Module {
|
|||
|
||||
close: e => ! this.dialog.exclusive && this.dialog.toggleVisible(e),
|
||||
|
||||
popout: e => {
|
||||
version: window.FrankerFaceZ.version_info,
|
||||
};
|
||||
|
||||
out.popout = e => {
|
||||
if ( this.dialog.exclusive )
|
||||
return;
|
||||
|
||||
this.dialog.toggleVisible(e);
|
||||
if ( ! this.openPopout() )
|
||||
if ( ! this.openPopout(out.currentItem?.full_key) )
|
||||
alert(this.i18n.t('popup.error', 'We tried opening a pop-up window and could not. Make sure to allow pop-ups from Twitch.')); // eslint-disable-line no-alert
|
||||
},
|
||||
|
||||
version: window.FrankerFaceZ.version_info,
|
||||
};
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
108
src/modules/main_menu/provider-mixin.js
Normal file
108
src/modules/main_menu/provider-mixin.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
'use strict';
|
||||
|
||||
import {deep_copy} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: undefined,
|
||||
has_value: false,
|
||||
|
||||
_unseen: false
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
const provider = this.context.provider,
|
||||
setting = this.item.setting;
|
||||
|
||||
provider.on('changed', this._providerChange, this);
|
||||
|
||||
this.has_value = provider.has(setting);
|
||||
if ( this.has_value )
|
||||
this.value = deep_copy(provider.get(setting));
|
||||
else
|
||||
this.value = this.default_value;
|
||||
|
||||
if ( this.item.unseen ) {
|
||||
this._unseen = true;
|
||||
this.item.unseen = 0;
|
||||
}
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
const provider = this.context.provider;
|
||||
|
||||
provider.off('changed', this._providerChange, this);
|
||||
|
||||
this.value = undefined;
|
||||
this.has_value = false;
|
||||
},
|
||||
|
||||
computed: {
|
||||
data() {
|
||||
const data = this.item.data;
|
||||
if ( typeof data === 'function' )
|
||||
return data(this.value);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
unseen() {
|
||||
return this._unseen || this.item.unseen > 0;
|
||||
},
|
||||
|
||||
default_value() {
|
||||
if ( typeof this.item.default === 'function' )
|
||||
return this.item.default(this.context);
|
||||
|
||||
return deep_copy(this.item.default);
|
||||
},
|
||||
|
||||
isDefault() {
|
||||
return ! this.has_value
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
_providerChange(key, val, deleted) {
|
||||
if ( key !== this.item.setting )
|
||||
return;
|
||||
|
||||
if ( deleted ) {
|
||||
this.has_value = false;
|
||||
this.value = this.default_value;
|
||||
} else {
|
||||
this.has_value = true;
|
||||
this.value = deep_copy(val);
|
||||
}
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if ( this.item.process )
|
||||
value = this.item.process(value);
|
||||
|
||||
const provider = this.context.provider,
|
||||
setting = this.item.setting;
|
||||
|
||||
provider.set(setting, value);
|
||||
this.has_value = true;
|
||||
this.value = deep_copy(value);
|
||||
|
||||
if ( this.item.onUIChange )
|
||||
this.item.onUIChange(this.value);
|
||||
},
|
||||
|
||||
clear() {
|
||||
const provider = this.context.provider,
|
||||
setting = this.item.setting;
|
||||
|
||||
provider.delete(setting);
|
||||
this.value = this.default_value;
|
||||
this.has_value = false;
|
||||
|
||||
if ( this.item.onUIChange )
|
||||
this.item.onUIChange(this.value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -565,7 +565,8 @@ export default class SettingsManager extends Module {
|
|||
if ( ! ui.key && ui.title )
|
||||
ui.key = ui.title.toSnakeCase();
|
||||
|
||||
if ( (ui.component === 'setting-select-box' || ui.component === 'setting-combo-box') && Array.isArray(ui.data) ) {
|
||||
if ( (ui.component === 'setting-select-box' || ui.component === 'setting-combo-box') && Array.isArray(ui.data) && ! ui.no_i18n
|
||||
&& key !== 'ffzap.core.highlight_sound' ) { // TODO: Remove workaround.
|
||||
const i18n_base = `${ui.i18n_key || `setting.entry.${key}`}.values`;
|
||||
for(const value of ui.data) {
|
||||
if ( value.i18n_key === undefined && value.value !== undefined )
|
||||
|
|
|
@ -196,7 +196,7 @@ export default class RichContent extends Module {
|
|||
renderBasic() {
|
||||
let title, description;
|
||||
if ( this.state.error ) {
|
||||
title = t.i18n.t('card.error', 'An error occured.');
|
||||
title = t.i18n.t('card.error', 'An error occurred.');
|
||||
description = this.state.error;
|
||||
|
||||
} else if ( this.state.loaded && this.state.has_tokenizer ) {
|
||||
|
|
|
@ -78,7 +78,7 @@ export default class ViewerCards extends Module {
|
|||
else
|
||||
color = 'rgba(128,170,255,0.2)';
|
||||
|
||||
this.css_tweaks.set('viewer-card-highlight', `body .chat-list .chat-line__message:not(.chat-line--inline):nth-child(1n+0)[data-user="${login}"] {
|
||||
this.css_tweaks.set('viewer-card-highlight', `body .chat-room .chat-line__message:not(.chat-line--inline):nth-child(1n+0)[data-user="${login}"] {
|
||||
background-color: ${color} !important;
|
||||
}`);
|
||||
} else
|
||||
|
|
|
@ -241,9 +241,12 @@ export default class CSSTweaks extends Module {
|
|||
});
|
||||
|
||||
this.settings.add('layout.minimal-navigation', {
|
||||
requires: ['layout.theatre-navigation'],
|
||||
requires: ['layout.theatre-navigation', 'context.isWatchParty'],
|
||||
default: false,
|
||||
process(ctx, val) {
|
||||
if ( ctx.get('context.isWatchParty') )
|
||||
return false;
|
||||
|
||||
return ctx.get('layout.theatre-navigation') ?
|
||||
true : val;
|
||||
},
|
||||
|
@ -288,7 +291,7 @@ export default class CSSTweaks extends Module {
|
|||
default: true,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Top Navigation',
|
||||
title: 'Show Twitch Prime offers.',
|
||||
title: 'Show Prime Gaming Loot.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggleHide('prime-offers', !val)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
.channel-info-content > div:first-child,
|
||||
.channel-info-bar {
|
||||
position: fixed;
|
||||
bottom: 10rem;
|
||||
bottom: 17rem;
|
||||
left: 5rem;
|
||||
right: calc(var(--ffz-chat-width) + 40rem);
|
||||
z-index: 3500;
|
||||
|
|
|
@ -21,9 +21,23 @@ export default class Game extends SiteModule {
|
|||
|
||||
this.GameHeader = this.fine.define(
|
||||
'game-header',
|
||||
n => n.props && n.props.data && n.getBannerImage && n.getFollowButton,
|
||||
n => n.props && n.props.data && n.getBannerImage && n.getDirectoryCountAndTags,
|
||||
['dir-game-index', 'dir-community', 'dir-game-videos', 'dir-game-clips', 'dir-game-details']
|
||||
);
|
||||
|
||||
this.settings.addUI('directory.game.blocked-games', {
|
||||
path: 'Directory > Categories @{"description": "Please note that due to limitations in Twitch\'s website, names here must be formatted exactly as displayed in your client. For best results, you can block or unblock categories directly from directory pages."} >> Blocked',
|
||||
component: 'game-list-editor',
|
||||
default: [],
|
||||
onUIChange: () => this.parent.updateCards()
|
||||
});
|
||||
|
||||
this.settings.addUI('directory.game.hidden-thumbnails', {
|
||||
path: 'Directory > Categories >> Hidden Thumbnails',
|
||||
component: 'game-list-editor',
|
||||
default: [],
|
||||
onUIChange: () => this.parent.updateCards()
|
||||
});
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
@ -36,6 +50,15 @@ export default class Game extends SiteModule {
|
|||
})
|
||||
});
|
||||
|
||||
this.settings.provider.on('changed', key => {
|
||||
if ( key === 'directory.game.blocked-games' || key === 'directory.game.hidden-thumbnails' ) {
|
||||
this.parent.updateCards();
|
||||
|
||||
for(const inst of this.GameHeader.instances)
|
||||
this.updateGameHeader(inst);
|
||||
}
|
||||
});
|
||||
|
||||
this.GameHeader.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.updateGameHeader(inst);
|
||||
|
|
|
@ -57,9 +57,10 @@ export default class Directory extends SiteModule {
|
|||
default: 2,
|
||||
|
||||
ui: {
|
||||
path: 'Directory > Channels >> Blocked and Hidden Categories',
|
||||
path: 'Directory > Categories >> Hidden Thumbnail Style @{"sort": 100}',
|
||||
title: 'Hidden Style',
|
||||
component: 'setting-select-box',
|
||||
sort: 100,
|
||||
|
||||
data: [
|
||||
{value: 0, title: 'Replace Image'},
|
||||
|
@ -79,9 +80,10 @@ export default class Directory extends SiteModule {
|
|||
default: false,
|
||||
|
||||
ui: {
|
||||
path: 'Directory > Channels >> Blocked and Hidden Categories',
|
||||
path: 'Directory > Categories >> Hidden Thumbnail Style',
|
||||
title: 'Reveal hidden entries on mouse hover.',
|
||||
component: 'setting-check-box'
|
||||
component: 'setting-check-box',
|
||||
sort: 101
|
||||
},
|
||||
|
||||
changed: val => this.css_tweaks.toggle('dir-reveal', val)
|
||||
|
@ -329,7 +331,7 @@ export default class Directory extends SiteModule {
|
|||
if ( ! props?.channelLogin )
|
||||
return;
|
||||
|
||||
const game = props.gameTitle || props.trackingProps?.categoryName;
|
||||
const game = props.gameTitle || props.trackingProps?.categoryName || props.trackingProps?.category;
|
||||
|
||||
el.classList.toggle('ffz-hide-thumbnail', this.settings.provider.get('directory.game.hidden-thumbnails', []).includes(game));
|
||||
el.dataset.ffzType = props.streamType;
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<div class="tw-flex-grow-1 tw-pd-x-2" />
|
||||
|
||||
<div v-if="user.error">
|
||||
{{ t('featured-follow.error', 'An error occured.') }}
|
||||
{{ t('featured-follow.error', 'An error occurred.') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<button
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class WebMunch extends Module {
|
|||
for(const fn of t._resolve_require)
|
||||
fn(require);
|
||||
} catch(err) {
|
||||
t.log.error('An error occured running require callbacks.', err);
|
||||
t.log.error('An error occurred running require callbacks.', err);
|
||||
}
|
||||
|
||||
t._resolve_require = null;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue