mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-02 09:08:32 +00:00
4.46.0
* Added: Profile rule to allow profiles to only be active on a specific monitor. This currently relies on a Chromium-specific API and is not available for Firefox users. (Closes #1358) * Added: Simple 'And' profile rule for building complex behaviors without needing a workaround. * Changed: Update the link detection regex to match Twitch's native behavior. (Closes #1353) * Changed: When the add-ons list is being filtered by a search term, display a notice to the user. * Fixed: Unable to set keys for Message Hover actions. (Closes #1316) * Fixed: The Wide emote effect not correctly being disabled. * Fixed: Long emote names causing the favorite button on the emote card to appear incorrectly. * Fixed: Whenever a user's last profile is removed, automatically create a new profile. * Fixed: When scrolling the emote menu, take best efforts to ensure other page elements are not scrolled incorrectly. This notably fixes a scrolling issue on mod view. * Fixed: If users have disabled the `document.fonts` API, do not attempt to filter the list of installed fonts for the font selector controls.
This commit is contained in:
parent
3d4b4f6225
commit
2db7122c1d
10 changed files with 199 additions and 18 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.45.1",
|
"version": "4.46.0",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|
|
@ -22,7 +22,8 @@ const SHRINK_X = MODIFIER_FLAGS.ShrinkX,
|
||||||
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
|
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
|
||||||
//WHITESPACE = /^\s*$/,
|
//WHITESPACE = /^\s*$/,
|
||||||
//LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
|
//LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
|
||||||
NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g,
|
NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*[^\.!,?])?))/g,
|
||||||
|
//OLD_NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g,
|
||||||
//MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex
|
//MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex
|
||||||
MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex
|
MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
|
@ -1275,9 +1276,9 @@ export const AddonEmotes = {
|
||||||
if ( effects ) {
|
if ( effects ) {
|
||||||
this.emotes.ensureEffect(effects);
|
this.emotes.ensureEffect(effects);
|
||||||
|
|
||||||
if ( (effects & SHRINK_X) === SHRINK_X )
|
if ( (effects & SHRINK_X) === SHRINK_X && this.emotes.effects_enabled?.ShrinkX )
|
||||||
style.width *= 0.5;
|
style.width *= 0.5;
|
||||||
if ( (effects & STRETCH_X) === STRETCH_X )
|
if ( (effects & STRETCH_X) === STRETCH_X && this.emotes.effects_enabled?.GrowX )
|
||||||
style.width *= 2;
|
style.width *= 2;
|
||||||
/*if ( (effects & SHRINK_Y) === SHRINK_Y )
|
/*if ( (effects & SHRINK_Y) === SHRINK_Y )
|
||||||
style.height *= 0.5;
|
style.height *= 0.5;
|
||||||
|
@ -1538,11 +1539,11 @@ export const AddonEmotes = {
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
if ( (effects & SHRINK_X) === SHRINK_X ) {
|
if ( (effects & SHRINK_X) === SHRINK_X && this.emotes.effects_enabled?.ShrinkX ) {
|
||||||
style.width *= 0.5;
|
style.width *= 0.5;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if ( (effects & STRETCH_X) === STRETCH_X ) {
|
if ( (effects & STRETCH_X) === STRETCH_X && this.emotes.effects_enabled?.GrowX ) {
|
||||||
style.width *= 2;
|
style.width *= 2;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
>
|
>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-align-left tw-flex-grow-1 tw-ellipsis tw-mg-x-1 tw-mg-y-05 viewer-card__display-name">
|
<div class="tw-align-left tw-flex-grow-1 tw-ellipsis tw-mg-l-1 tw-mg-y-05 viewer-card__display-name">
|
||||||
<h4 class="tw-inline tw-ellipsis" :title="emote ? emote.name : raw_emote.name">
|
<h4 class="tw-inline tw-ellipsis" :title="emote ? emote.name : raw_emote.name">
|
||||||
{{ emote ? emote.name : raw_emote.name }}
|
{{ emote ? emote.name : raw_emote.name }}
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
v-if="canFavorite"
|
v-if="canFavorite"
|
||||||
:data-title="favoriteLabel"
|
:data-title="favoriteLabel"
|
||||||
:aria-label="favoriteLabel"
|
:aria-label="favoriteLabel"
|
||||||
class="viewer-card-drag-cancel tw-align-self-start tw-align-items-center tw-align-middle tw-border-radius-medium tw-button-icon tw-core-button tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ffz-tooltip"
|
class="tw-flex-shrink-0 viewer-card-drag-cancel tw-align-self-start tw-align-items-center tw-align-middle tw-border-radius-medium tw-button-icon tw-core-button tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ffz-tooltip"
|
||||||
@click="toggleFavorite"
|
@click="toggleFavorite"
|
||||||
>
|
>
|
||||||
<span class="tw-button-icon__icon">
|
<span class="tw-button-icon__icon">
|
||||||
|
|
|
@ -746,8 +746,10 @@ export default {
|
||||||
if ( this.$refs.key_meta.checked )
|
if ( this.$refs.key_meta.checked )
|
||||||
i |= 8;
|
i |= 8;
|
||||||
|
|
||||||
this.edit_data.display.hover = this.$refs.key_hover.checked;
|
|
||||||
this.edit_data.display.keys = i;
|
this.edit_data.display.keys = i;
|
||||||
|
|
||||||
|
if ( this.has_hover_modifier )
|
||||||
|
this.edit_data.display.hover = this.$refs.key_hover.checked;
|
||||||
},
|
},
|
||||||
|
|
||||||
edit() {
|
edit() {
|
||||||
|
|
|
@ -63,6 +63,16 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="ready && visible_addons.length !== listed_addons.length" class="tw-align-center tw-pd-1">
|
||||||
|
{{ t('addon.displaying', 'Displaying {visible, number} of {total, plural, one {# add-on} other {# add-ons} }.', {
|
||||||
|
visible: visible_addons.length,
|
||||||
|
total: listed_addons.length
|
||||||
|
}) }}
|
||||||
|
<template v-if="filter && filter.length">
|
||||||
|
{{ t('addon.displaying.filtered', 'The visible add-ons are being filtered by your search. Clear it to view all available add-ons.') }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="visible_unlisted" class="tw-flex tw-align-items-center">
|
<div v-if="visible_unlisted" class="tw-flex tw-align-items-center">
|
||||||
<div class="tw-flex-grow-1" />
|
<div class="tw-flex-grow-1" />
|
||||||
<div
|
<div
|
||||||
|
@ -136,6 +146,10 @@ export default {
|
||||||
return this.sorted_addons.filter(addon => this.shouldShow(addon));
|
return this.sorted_addons.filter(addon => this.shouldShow(addon));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
listed_addons() {
|
||||||
|
return this.sorted_addons.filter(addon => ! addon.unlisted)
|
||||||
|
},
|
||||||
|
|
||||||
sorted_addons() {
|
sorted_addons() {
|
||||||
const addons = this.item.getAddons();
|
const addons = this.item.getAddons();
|
||||||
|
|
||||||
|
|
65
src/settings/components/monitor.vue
Normal file
65
src/settings/components/monitor.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<section class="tw-flex-grow-1 tw-align-self-start">
|
||||||
|
<div class="tw-flex tw-align-items-center">
|
||||||
|
<label :for="'label$' + id">
|
||||||
|
{{ t(type.i18n, type.title) }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
:id="'label$' + id"
|
||||||
|
v-model="value.data.label"
|
||||||
|
class="tw-flex-grow-1 tw-mg-l-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 ffz-select"
|
||||||
|
>
|
||||||
|
<template v-for="mon in monitors">
|
||||||
|
<option :value="mon.label">
|
||||||
|
{{ mon.label }} ({{ mon.width }}×{{ mon.height }})
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
let last_id = 0;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['value', 'type', 'filters', 'context'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
id: last_id++,
|
||||||
|
has_monitors: true,
|
||||||
|
monitors: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.detectMonitors();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async detectMonitors() {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await window.getScreenDetails();
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Unable to get screen details', err);
|
||||||
|
this.has_monitors = false;
|
||||||
|
this.monitors = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.monitors = [];
|
||||||
|
for(const mon of data.screens)
|
||||||
|
this.monitors.push({
|
||||||
|
label: mon.label,
|
||||||
|
width: mon.width,
|
||||||
|
height: mon.height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -37,6 +37,21 @@ export const Invert = {
|
||||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/nested.vue')
|
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/nested.vue')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const And = {
|
||||||
|
createTest(config, rule_types, rebuild) {
|
||||||
|
return createTester(config, rule_types, false, false, rebuild);
|
||||||
|
},
|
||||||
|
|
||||||
|
childRules: true,
|
||||||
|
|
||||||
|
tall: true,
|
||||||
|
title: 'And',
|
||||||
|
i18n: 'settings.filter.and',
|
||||||
|
|
||||||
|
default: () => [],
|
||||||
|
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/nested.vue')
|
||||||
|
};
|
||||||
|
|
||||||
export const Or = {
|
export const Or = {
|
||||||
createTest(config, rule_types, rebuild) {
|
createTest(config, rule_types, rebuild) {
|
||||||
return createTester(config, rule_types, false, true, rebuild);
|
return createTester(config, rule_types, false, true, rebuild);
|
||||||
|
@ -83,7 +98,7 @@ export const Constant = {
|
||||||
default: true,
|
default: true,
|
||||||
|
|
||||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue')
|
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue')
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// Context Stuff
|
// Context Stuff
|
||||||
|
@ -374,4 +389,54 @@ export const Title = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/title.vue')
|
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/title.vue')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Monitor Stuff
|
||||||
|
|
||||||
|
export let Monitor = null;
|
||||||
|
|
||||||
|
if ( window.getScreenDetails ) {
|
||||||
|
|
||||||
|
Monitor = {
|
||||||
|
_used: false,
|
||||||
|
details: undefined,
|
||||||
|
|
||||||
|
used: () => {
|
||||||
|
const out = Monitor._used;
|
||||||
|
Monitor._used = false;
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
createTest(config = {}, _, reload) {
|
||||||
|
if ( ! config.label )
|
||||||
|
return () => false;
|
||||||
|
|
||||||
|
Monitor._used = true;
|
||||||
|
if ( Monitor.details === undefined )
|
||||||
|
FrankerFaceZ.get().resolve('settings').createMonitorUpdate().then(() => {
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Monitor._used = true;
|
||||||
|
const details = Monitor.details,
|
||||||
|
screen = details?.currentScreen;
|
||||||
|
|
||||||
|
if ( ! screen )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return screen.label === config.label;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
default: () => ({
|
||||||
|
label: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
title: 'Current Monitor',
|
||||||
|
i18n: 'settings.filter.monitor',
|
||||||
|
|
||||||
|
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/monitor.vue')
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@ export default class SettingsManager extends Module {
|
||||||
this.filters = {};
|
this.filters = {};
|
||||||
|
|
||||||
for(const key in FILTERS)
|
for(const key in FILTERS)
|
||||||
if ( has(FILTERS, key) )
|
if ( has(FILTERS, key) && FILTERS[key] )
|
||||||
this.filters[key] = FILTERS[key];
|
this.filters[key] = FILTERS[key];
|
||||||
|
|
||||||
|
|
||||||
|
@ -247,8 +247,28 @@ export default class SettingsManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async createMonitorUpdate() {
|
||||||
|
const Monitor = FILTERS?.Monitor;
|
||||||
|
if ( ! Monitor || Monitor.details !== undefined )
|
||||||
|
return;
|
||||||
|
|
||||||
|
Monitor.details = null;
|
||||||
|
try {
|
||||||
|
Monitor.details = await window.getScreenDetails();
|
||||||
|
Monitor.details.addEventListener('currentscreenchange', () => {
|
||||||
|
for(const context of this.__contexts)
|
||||||
|
context.selectProfiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
this.log.error('Unable to get monitor details', err);
|
||||||
|
Monitor.details = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
updateClock() {
|
updateClock() {
|
||||||
const captured = require('./filters').Time.captured();
|
const captured = FILTERS?.Time?.captured?.();
|
||||||
if ( ! captured?.length )
|
if ( ! captured?.length )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -918,7 +938,19 @@ export default class SettingsManager extends Module {
|
||||||
|
|
||||||
|
|
||||||
_saveProfiles() {
|
_saveProfiles() {
|
||||||
this.provider.set('profiles', this.__profiles.filter(prof => ! prof.ephemeral).map(prof => prof.data));
|
const out = this.__profiles.filter(prof => ! prof.ephemeral).map(prof => prof.data);
|
||||||
|
|
||||||
|
// Ensure that we always have a non-ephemeral profile.
|
||||||
|
if ( ! out ) {
|
||||||
|
this.createProfile({
|
||||||
|
name: 'Default Profile',
|
||||||
|
i18n_key: 'setting.profiles.default',
|
||||||
|
description: 'Settings that apply everywhere on Twitch.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.provider.set('profiles', out);
|
||||||
for(const context of this.__contexts)
|
for(const context of this.__contexts)
|
||||||
context.selectProfiles();
|
context.selectProfiles();
|
||||||
|
|
||||||
|
|
|
@ -1212,7 +1212,7 @@ export default class EmoteMenu extends Module {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const el = this.nav_ref?.querySelector?.(`button[data-key="${this.state.active_nav}"]`);
|
const el = this.nav_ref?.querySelector?.(`button[data-key="${this.state.active_nav}"]`);
|
||||||
if ( el )
|
if ( el )
|
||||||
el.scrollIntoView({block: 'nearest'});
|
el.scrollIntoView({block: 'nearest', inline: 'start'});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1342,7 +1342,7 @@ export default class EmoteMenu extends Module {
|
||||||
const el = this.ref?.querySelector?.(`section[data-key="${key}"]`);
|
const el = this.ref?.querySelector?.(`section[data-key="${key}"]`);
|
||||||
if ( el ) {
|
if ( el ) {
|
||||||
this.lock_active = true;
|
this.lock_active = true;
|
||||||
el.scrollIntoView();
|
el.scrollIntoView({block: 'nearest', inline: 'start'});
|
||||||
this.setState({
|
this.setState({
|
||||||
active_nav: key
|
active_nav: key
|
||||||
});
|
});
|
||||||
|
@ -1381,7 +1381,7 @@ export default class EmoteMenu extends Module {
|
||||||
el = set && this.ref?.querySelector?.(`section[data-key="${set.key}"]`);
|
el = set && this.ref?.querySelector?.(`section[data-key="${set.key}"]`);
|
||||||
|
|
||||||
if ( el )
|
if ( el )
|
||||||
el.scrollIntoView();
|
el.scrollIntoView({block: 'nearest', inline: 'start'});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,9 @@ const KNOWN_FONTS = [
|
||||||
'Comic Sans MS',
|
'Comic Sans MS',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const VALID_FONTS = KNOWN_FONTS.filter(font => document.fonts.check(`16px ${font}`)).sort();
|
export const VALID_FONTS = document.fonts?.check
|
||||||
|
? KNOWN_FONTS.filter(font => document.fonts.check(`16px ${font}`)).sort()
|
||||||
|
: KNOWN_FONTS.sort();
|
||||||
|
|
||||||
|
|
||||||
/* Google Font Handling */
|
/* Google Font Handling */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue