diff --git a/package.json b/package.json index 136ee5a4..866d5da6 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.9.4", + "version": "4.9.5", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index a2451e7a..b039bab6 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -1846,6 +1846,8 @@ export default class ChatHook extends Module { if ( ! this.addRoom(cont, props) ) return; + this.updateRoomBitsConfig(cont, props.bitsConfig); + if ( props.data ) { this.chat.badges.updateTwitchBadges(props.data.badges); this.updateRoomBadges(cont, props.data.user && props.data.user.broadcastBadges); @@ -1864,6 +1866,9 @@ export default class ChatHook extends Module { return; } + if ( props.bitsConfig !== cont.props.bitsConfig ) + this.updateRoomBitsConfig(cont, props.bitsConfig); + // Twitch, React, and Apollo are the trifecta of terror so we // can't compare the badgeSets property in any reasonable way. // Instead, just check the lengths to see if they've changed diff --git a/src/sites/twitch-twilight/modules/chat/scroller.js b/src/sites/twitch-twilight/modules/chat/scroller.js index bc964235..d05011a9 100644 --- a/src/sites/twitch-twilight/modules/chat/scroller.js +++ b/src/sites/twitch-twilight/modules/chat/scroller.js @@ -29,7 +29,7 @@ export default class Scroller extends Module { this.ChatScroller = this.fine.define( 'chat-scroller', - n => n.saveScrollRef && n.handleScrollEvent && ! n.renderLines, + n => n.saveScrollRef && n.handleScrollEvent && ! n.renderLines && n.resume, Twilight.CHAT_ROUTES ); @@ -162,8 +162,23 @@ export default class Scroller extends Module { this.ChatScroller.ready((cls, instances) => { const old_catch = cls.prototype.componentDidCatch, + old_snapshot = cls.prototype.getSnapshotBeforeUpdate, 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 // recover from the error when we re-build? cls.prototype.componentDidCatch = function(err, info) { @@ -240,13 +255,23 @@ export default class Scroller extends Module { inst.smoothScrollBottom(); else { inst._ffz_one_fast_scroll = false; + inst._ffz_snapshot = null; inst.ffz_oldScroll(); } } } 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; this._ffz_scroll_frame = requestAnimationFrame(inst.ffz_doScroll); @@ -592,7 +617,7 @@ export default class Scroller extends Module { } else if ( difference > 200 ) { // we are starting to fall behind, speed it up a bit step += step * Math.floor(difference / 200); - } + } const smoothAnimation = () => { if ( this.state.isPaused || ! this.state.isAutoScrolling ) diff --git a/src/sites/twitch-twilight/modules/player.jsx b/src/sites/twitch-twilight/modules/player.jsx index 67d2e738..6711d0e7 100644 --- a/src/sites/twitch-twilight/modules/player.jsx +++ b/src/sites/twitch-twilight/modules/player.jsx @@ -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', { default: 0.1, ui: { @@ -436,8 +465,10 @@ export default class Player extends Module { }); this.on('i18n:update', () => { - for(const inst of this.Player.instances) + for(const inst of this.Player.instances) { + this.addPiPButton(inst); this.addResetButton(inst); + } }); } @@ -493,6 +524,7 @@ export default class Player extends Module { process(inst) { this.addResetButton(inst); + this.addPiPButton(inst); this.addEndedListener(inst); this.addStateTags(inst); this.addControlVisibility(inst); @@ -503,11 +535,23 @@ export default class Player extends Module { cleanup(inst) { // eslint-disable-line class-methods-use-this const p = inst.player, 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 ) 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 ) { p && off(p, 'ended', inst._ffz_on_ended); 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 = (); + + 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) { const t = this, el = inst.playerRef && inst.playerRef.querySelector('.player-buttons-right .pl-flex'), 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 ( tries < 5 ) return setTimeout(this.addResetButton.bind(this, inst, (tries||0) + 1), 250); diff --git a/src/sites/twitch-twilight/styles/player.scss b/src/sites/twitch-twilight/styles/player.scss index b41af3e3..746f860d 100644 --- a/src/sites/twitch-twilight/styles/player.scss +++ b/src/sites/twitch-twilight/styles/player.scss @@ -1,10 +1,13 @@ -.ffz--player-reset:hover:before { - color: #a991d4; -} +.ffz--player-pip, +.ffz--player-reset { + &:before { + font-size: 2rem; + margin-top: .7rem; + } -.ffz--player-reset:before { - font-size: 2rem; - margin-top: .7rem; + &:hover:before { + color: #a991d4; + } } .player-controls-bottom .player-tip {