mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-11 00:20:54 +00:00
4.9.5
* Added: Picture-in-Picture button for the player, when supported by your browser. * Added: Option to disable the `Reset Player` button for the player. * Fixed: Smooth scrolling not functioning once the chat buffer fills. It's still a bit peculiar, but at least it's somewhat working now. Please disable the feature for now if the behavior is bothering you or causing issues. * Fixed: The Featured channels menu not loading, for channels using that FFZ feature. * Fixed: Certain add-on emotes appearing in the emote tab-completion menu multiple times. * Fixed: Cheers not rendering correctly in chat.
This commit is contained in:
parent
9894e9a7cb
commit
512fe8898d
5 changed files with 166 additions and 12 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.9.4",
|
"version": "4.9.5",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1846,6 +1846,8 @@ export default class ChatHook extends Module {
|
||||||
if ( ! this.addRoom(cont, props) )
|
if ( ! this.addRoom(cont, props) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.updateRoomBitsConfig(cont, props.bitsConfig);
|
||||||
|
|
||||||
if ( props.data ) {
|
if ( props.data ) {
|
||||||
this.chat.badges.updateTwitchBadges(props.data.badges);
|
this.chat.badges.updateTwitchBadges(props.data.badges);
|
||||||
this.updateRoomBadges(cont, props.data.user && props.data.user.broadcastBadges);
|
this.updateRoomBadges(cont, props.data.user && props.data.user.broadcastBadges);
|
||||||
|
@ -1864,6 +1866,9 @@ export default class ChatHook extends Module {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( props.bitsConfig !== cont.props.bitsConfig )
|
||||||
|
this.updateRoomBitsConfig(cont, props.bitsConfig);
|
||||||
|
|
||||||
// Twitch, React, and Apollo are the trifecta of terror so we
|
// Twitch, React, and Apollo are the trifecta of terror so we
|
||||||
// can't compare the badgeSets property in any reasonable way.
|
// can't compare the badgeSets property in any reasonable way.
|
||||||
// Instead, just check the lengths to see if they've changed
|
// Instead, just check the lengths to see if they've changed
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default class Scroller extends Module {
|
||||||
|
|
||||||
this.ChatScroller = this.fine.define(
|
this.ChatScroller = this.fine.define(
|
||||||
'chat-scroller',
|
'chat-scroller',
|
||||||
n => n.saveScrollRef && n.handleScrollEvent && ! n.renderLines,
|
n => n.saveScrollRef && n.handleScrollEvent && ! n.renderLines && n.resume,
|
||||||
Twilight.CHAT_ROUTES
|
Twilight.CHAT_ROUTES
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -162,8 +162,23 @@ export default class Scroller extends Module {
|
||||||
|
|
||||||
this.ChatScroller.ready((cls, instances) => {
|
this.ChatScroller.ready((cls, instances) => {
|
||||||
const old_catch = cls.prototype.componentDidCatch,
|
const old_catch = cls.prototype.componentDidCatch,
|
||||||
|
old_snapshot = cls.prototype.getSnapshotBeforeUpdate,
|
||||||
old_render = cls.prototype.render;
|
old_render = cls.prototype.render;
|
||||||
|
|
||||||
|
if ( old_snapshot )
|
||||||
|
cls.prototype.getSnapshotBeforeUpdate = function() {
|
||||||
|
let auto_state;
|
||||||
|
if ( this.state ) {
|
||||||
|
auto_state = this.state.isAutoScrolling;
|
||||||
|
this.state.isAutoScrolling = false;
|
||||||
|
}
|
||||||
|
const out = old_snapshot.call(this);
|
||||||
|
if ( this.state )
|
||||||
|
this.state.isAutoScrolling = auto_state;
|
||||||
|
this._ffz_snapshot = out;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// Try catching errors. With any luck, maybe we can
|
// Try catching errors. With any luck, maybe we can
|
||||||
// recover from the error when we re-build?
|
// recover from the error when we re-build?
|
||||||
cls.prototype.componentDidCatch = function(err, info) {
|
cls.prototype.componentDidCatch = function(err, info) {
|
||||||
|
@ -240,13 +255,23 @@ export default class Scroller extends Module {
|
||||||
inst.smoothScrollBottom();
|
inst.smoothScrollBottom();
|
||||||
else {
|
else {
|
||||||
inst._ffz_one_fast_scroll = false;
|
inst._ffz_one_fast_scroll = false;
|
||||||
|
inst._ffz_snapshot = null;
|
||||||
inst.ffz_oldScroll();
|
inst.ffz_oldScroll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inst.scrollToBottom = function() {
|
inst.scrollToBottom = function() {
|
||||||
if ( inst._ffz_scroll_frame || inst.state.isPaused )
|
// WIP: Trying to fix the scroll position changing so that we can
|
||||||
|
// smooth scroll from the previous position.
|
||||||
|
if ( inst.ffz_smooth_scroll && ! inst._ffz_one_fast_scroll && inst._ffz_snapshot ) {
|
||||||
|
const adjustment = inst._ffz_snapshot && inst._ffz_snapshot.lastLine ? inst._ffz_snapshot.offsetTop - inst._ffz_snapshot.lastLine.offsetTop : 0;
|
||||||
|
if ( inst.scroll && inst.scroll.scrollContent && adjustment > 0 )
|
||||||
|
inst.scroll.scrollContent.scrollTop -= adjustment;
|
||||||
|
}
|
||||||
|
|
||||||
|
inst._ffz_snapshot = null;
|
||||||
|
if ( inst.state.isPaused || inst._ffz_scroll_frame )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this._ffz_scroll_frame = requestAnimationFrame(inst.ffz_doScroll);
|
this._ffz_scroll_frame = requestAnimationFrame(inst.ffz_doScroll);
|
||||||
|
@ -592,7 +617,7 @@ export default class Scroller extends Module {
|
||||||
} else if ( difference > 200 ) {
|
} else if ( difference > 200 ) {
|
||||||
// we are starting to fall behind, speed it up a bit
|
// we are starting to fall behind, speed it up a bit
|
||||||
step += step * Math.floor(difference / 200);
|
step += step * Math.floor(difference / 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
const smoothAnimation = () => {
|
const smoothAnimation = () => {
|
||||||
if ( this.state.isPaused || ! this.state.isAutoScrolling )
|
if ( this.state.isPaused || ! this.state.isAutoScrolling )
|
||||||
|
|
|
@ -57,6 +57,35 @@ export default class Player extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settings.add('player.button.reset', {
|
||||||
|
default: true,
|
||||||
|
ui: {
|
||||||
|
path: 'Player > General >> General',
|
||||||
|
title: 'Add a `Reset Player` button to the player controls.',
|
||||||
|
description: "Double-clicking the Reset Player button destroys and recreates Twitch's player, potentially fixing playback issues without a full page refresh.",
|
||||||
|
component: 'setting-check-box'
|
||||||
|
},
|
||||||
|
changed: () => {
|
||||||
|
for(const inst of this.Player.instances)
|
||||||
|
this.addResetButton(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ( document.pictureInPictureEnabled )
|
||||||
|
this.settings.add('player.button.pip', {
|
||||||
|
default: true,
|
||||||
|
ui: {
|
||||||
|
path: 'Player > General >> General',
|
||||||
|
title: 'Add a `Picture-in-Picture` button to the player controls.',
|
||||||
|
description: "Clicking the PiP button attempts to toggle Picture-in-Picture mode for the player's video.",
|
||||||
|
component: 'setting-check-box'
|
||||||
|
},
|
||||||
|
changed: () => {
|
||||||
|
for(const inst of this.Player.instances)
|
||||||
|
this.addPiPButton(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('player.volume-scroll-steps', {
|
this.settings.add('player.volume-scroll-steps', {
|
||||||
default: 0.1,
|
default: 0.1,
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -436,8 +465,10 @@ export default class Player extends Module {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('i18n:update', () => {
|
this.on('i18n:update', () => {
|
||||||
for(const inst of this.Player.instances)
|
for(const inst of this.Player.instances) {
|
||||||
|
this.addPiPButton(inst);
|
||||||
this.addResetButton(inst);
|
this.addResetButton(inst);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,6 +524,7 @@ export default class Player extends Module {
|
||||||
|
|
||||||
process(inst) {
|
process(inst) {
|
||||||
this.addResetButton(inst);
|
this.addResetButton(inst);
|
||||||
|
this.addPiPButton(inst);
|
||||||
this.addEndedListener(inst);
|
this.addEndedListener(inst);
|
||||||
this.addStateTags(inst);
|
this.addStateTags(inst);
|
||||||
this.addControlVisibility(inst);
|
this.addControlVisibility(inst);
|
||||||
|
@ -503,11 +535,23 @@ export default class Player extends Module {
|
||||||
cleanup(inst) { // eslint-disable-line class-methods-use-this
|
cleanup(inst) { // eslint-disable-line class-methods-use-this
|
||||||
const p = inst.player,
|
const p = inst.player,
|
||||||
pr = inst.playerRef,
|
pr = inst.playerRef,
|
||||||
reset = pr && pr.querySelector('.ffz--player-reset');
|
video = pr && pr.querySelector('video'),
|
||||||
|
reset = pr && pr.querySelector('.ffz--player-reset'),
|
||||||
|
pip = pr && pr.querySelector('.ffz--player-pip');
|
||||||
|
|
||||||
if ( reset )
|
if ( reset )
|
||||||
reset.remove();
|
reset.remove();
|
||||||
|
|
||||||
|
if ( pip )
|
||||||
|
pip.remove();
|
||||||
|
|
||||||
|
if ( video && video._ffz_pip_enter ) {
|
||||||
|
video.removeEventListener('enterpictureinpicture', video._ffz_pip_enter);
|
||||||
|
video.removeEventListener('leavepictureinpicture', video._ffz_pip_exit);
|
||||||
|
video._ffz_pip_enter = null;
|
||||||
|
video._ffz_pip_exit = null;
|
||||||
|
}
|
||||||
|
|
||||||
if ( inst._ffz_on_ended ) {
|
if ( inst._ffz_on_ended ) {
|
||||||
p && off(p, 'ended', inst._ffz_on_ended);
|
p && off(p, 'ended', inst._ffz_on_ended);
|
||||||
inst._ffz_on_ended = null;
|
inst._ffz_on_ended = null;
|
||||||
|
@ -709,11 +753,88 @@ export default class Player extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addPiPButton(inst, tries = 0) {
|
||||||
|
const t = this,
|
||||||
|
el = inst.playerRef && inst.playerRef.querySelector('.player-buttons-right .pl-flex'),
|
||||||
|
container = el && el.parentElement,
|
||||||
|
has_pip = document.pictureInPictureEnabled && this.settings.get('player.button.pip');
|
||||||
|
|
||||||
|
if ( ! container ) {
|
||||||
|
if ( ! has_pip )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( tries < 5 )
|
||||||
|
return setTimeout(this.addPiPButton.bind(this, inst, (tries||0) + 1), 250);
|
||||||
|
|
||||||
|
return this.log.warn('Unable to find container element for PiP Button');
|
||||||
|
}
|
||||||
|
|
||||||
|
let tip, btn = container.querySelector('.ffz--player-pip');
|
||||||
|
if ( ! has_pip ) {
|
||||||
|
if ( btn )
|
||||||
|
btn.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! btn ) {
|
||||||
|
btn = (<button
|
||||||
|
class="player-button player-button--pip ffz--player-pip ffz-i-window-restore"
|
||||||
|
type="button"
|
||||||
|
onClick={t.pipPlayer.bind(t, inst)} // eslint-disable-line react/jsx-no-bind
|
||||||
|
>
|
||||||
|
{tip = <span class="player-tip js-control-tip" />}
|
||||||
|
</button>);
|
||||||
|
|
||||||
|
container.insertBefore(btn, el.nextSibling);
|
||||||
|
|
||||||
|
} else
|
||||||
|
tip = btn.querySelector('.player-tip');
|
||||||
|
|
||||||
|
const pip_active = !!document.pictureInPictureElement
|
||||||
|
|
||||||
|
btn.classList.toggle('ffz-i-window-restore', ! pip_active);
|
||||||
|
btn.classList.toggle('ffz-i-window-maximize', pip_active);
|
||||||
|
|
||||||
|
tip.dataset.tip = this.i18n.t('player.pip_button', 'Click to Toggle Picture-in-Picture');
|
||||||
|
}
|
||||||
|
|
||||||
|
pipPlayer(inst) {
|
||||||
|
const video = inst.playerRef && inst.playerRef.querySelector('video');
|
||||||
|
if ( ! video || ! document.pictureInPictureEnabled )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( ! video._ffz_pip_enter ) {
|
||||||
|
video.addEventListener('enterpictureinpicture', video._ffz_pip_enter = () => {
|
||||||
|
this.addPiPButton(inst);
|
||||||
|
});
|
||||||
|
|
||||||
|
video.addEventListener('leavepictureinpicture', video._ffz_pip_exit = () => {
|
||||||
|
this.addPiPButton(inst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( document.pictureInPictureElement )
|
||||||
|
document.exitPictureInPicture();
|
||||||
|
else
|
||||||
|
video.requestPictureInPicture();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
addResetButton(inst, tries = 0) {
|
addResetButton(inst, tries = 0) {
|
||||||
const t = this,
|
const t = this,
|
||||||
el = inst.playerRef && inst.playerRef.querySelector('.player-buttons-right .pl-flex'),
|
el = inst.playerRef && inst.playerRef.querySelector('.player-buttons-right .pl-flex'),
|
||||||
container = el && el.parentElement;
|
container = el && el.parentElement;
|
||||||
|
|
||||||
|
if ( ! this.settings.get('player.button.reset') ) {
|
||||||
|
if ( container ) {
|
||||||
|
const btn = container.querySelector('.ffz--player-reset');
|
||||||
|
if ( btn )
|
||||||
|
btn.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! container ) {
|
if ( ! container ) {
|
||||||
if ( tries < 5 )
|
if ( tries < 5 )
|
||||||
return setTimeout(this.addResetButton.bind(this, inst, (tries||0) + 1), 250);
|
return setTimeout(this.addResetButton.bind(this, inst, (tries||0) + 1), 250);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
.ffz--player-reset:hover:before {
|
.ffz--player-pip,
|
||||||
color: #a991d4;
|
.ffz--player-reset {
|
||||||
}
|
&:before {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-top: .7rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ffz--player-reset:before {
|
&:hover:before {
|
||||||
font-size: 2rem;
|
color: #a991d4;
|
||||||
margin-top: .7rem;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls-bottom .player-tip {
|
.player-controls-bottom .player-tip {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue