mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.48.0
* Fixed: The `/ffz reload` command is now `/ffz:reload` to remove an undesirable behavior with Twitch's completion handling when backspacing. * Fixed: Spaces being included in links when they shouldn't be. * Fixed: Previews of emotes in chat when typing their names directly. * Changed: Initial work on tracking the audio/video de-sync when using audio APIs for the compressor. This appears as a value in the stream latency metadata tool-tip, but currently drifts whenever the player is paused. * Changed: Initial work on allowing the extension to be loaded from a bundled extension. * API Changed: The load tracker now returns a list of reported loading keys when firing events.
This commit is contained in:
parent
67c64e6f7e
commit
109898a10d
13 changed files with 120 additions and 35 deletions
|
@ -34,6 +34,7 @@ module.exports = {
|
|||
'__version_minor__': false,
|
||||
'__version_patch__': false,
|
||||
'__version_prerelease__': false,
|
||||
'__extension__': false,
|
||||
'FrankerFaceZ': false
|
||||
},
|
||||
'rules': {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.47.1",
|
||||
"version": "4.48.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import { SERVER } from 'utilities/constants';
|
||||
import { EXTENSION, SERVER_OR_EXT } from 'utilities/constants';
|
||||
import { createElement } from 'utilities/dom';
|
||||
import { timeout, has } from 'utilities/object';
|
||||
import { getBuster } from 'utilities/time';
|
||||
|
@ -69,15 +69,16 @@ export default class AddonManager extends Module {
|
|||
off: (...args) => this.off(...args)
|
||||
});
|
||||
|
||||
this.settings.add('addons.dev.server', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Add-Ons >> Development',
|
||||
title: 'Use Local Development Server',
|
||||
description: 'Attempt to load add-ons from local development server on port 8001.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
if ( ! EXTENSION )
|
||||
this.settings.add('addons.dev.server', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Add-Ons >> Development',
|
||||
title: 'Use Local Development Server',
|
||||
description: 'Attempt to load add-ons from local development server on port 8001.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.on('i18n:update', this.rebuildAddonSearch, this);
|
||||
|
||||
|
@ -151,9 +152,13 @@ export default class AddonManager extends Module {
|
|||
|
||||
async loadAddonData() {
|
||||
const [cdn_data, local_data] = await Promise.all([
|
||||
fetchJSON(`${SERVER}/script/addons.json?_=${getBuster(30)}`),
|
||||
this.settings.get('addons.dev.server') ?
|
||||
fetchJSON(`https://localhost:8001/script/addons.json?_=${getBuster()}`) : null
|
||||
fetchJSON(`${SERVER_OR_EXT}/addons.json?_=${getBuster(30)}`),
|
||||
|
||||
// Do not attempt to load local add-ons if using the extension, as
|
||||
// loading external code is against the policy of basically everyone.
|
||||
(! EXTENSION && this.settings.get('addons.dev.server'))
|
||||
? fetchJSON(`https://localhost:8001/script/addons.json?_=${getBuster()}`)
|
||||
: null
|
||||
]);
|
||||
|
||||
if ( Array.isArray(cdn_data) )
|
||||
|
@ -469,7 +474,7 @@ export default class AddonManager extends Module {
|
|||
document.head.appendChild(createElement('script', {
|
||||
id: `ffz-loaded-addon-${addon.id}`,
|
||||
type: 'text/javascript',
|
||||
src: addon.src || `${addon.dev ? 'https://localhost:8001' : SERVER}/script/addons/${addon.id}/script.js?_=${getBuster(30)}`,
|
||||
src: addon.src || `${addon.dev ? 'https://localhost:8001/script' : SERVER_OR_EXT}/addons/${addon.id}/script.js?_=${getBuster(30)}`,
|
||||
crossorigin: 'anonymous'
|
||||
}));
|
||||
|
||||
|
|
25
src/entry_ext.js
Normal file
25
src/entry_ext.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* eslint strict: off */
|
||||
'use strict';
|
||||
(() => {
|
||||
// Don't run on certain sub-domains.
|
||||
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
|
||||
return;
|
||||
|
||||
const HOST = location.hostname,
|
||||
SERVER = __EXTENSION_PATH__,
|
||||
script = document.createElement('script');
|
||||
|
||||
let FLAVOR =
|
||||
HOST.includes('player') ? 'player' :
|
||||
HOST.includes('clips') ? 'clips' :
|
||||
(location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon');
|
||||
|
||||
if (FLAVOR === 'clips' && location.pathname === '/embed')
|
||||
FLAVOR = 'player';
|
||||
|
||||
script.id = 'ffz-script';
|
||||
script.async = true;
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.src = `${SERVER}/${FLAVOR}.js?_=${Date.now()}`;
|
||||
document.head.appendChild(script);
|
||||
})();
|
|
@ -67,9 +67,11 @@ export default class LoadTracker extends Module {
|
|||
data.success = true;
|
||||
|
||||
if ( ! data.pending.size ) {
|
||||
this.log.debug('complete', type, Object.keys(data.timers));
|
||||
const keys = Object.keys(data.timers);
|
||||
|
||||
this.log.debug('complete', type, keys);
|
||||
if ( data.success )
|
||||
this.emit(`:complete:${type}`);
|
||||
this.emit(`:complete:${type}`, keys);
|
||||
this.pending_loads.delete(type);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1271,7 +1271,7 @@ export default class Chat extends Module {
|
|||
|
||||
this.on('chat:get-tab-commands', event => {
|
||||
event.commands.push({
|
||||
name: 'ffz reload',
|
||||
name: 'ffz:reload',
|
||||
description: this.i18n.t('chat.command.reload', 'Reload FFZ and add-on chat data (emotes, badges, etc.)'),
|
||||
permissionLevel: 0,
|
||||
ffz_group: 'FrankerFaceZ'
|
||||
|
@ -1292,11 +1292,11 @@ export default class Chat extends Module {
|
|||
this.emit('chat:reload-data');
|
||||
});
|
||||
|
||||
this.on('load_tracker:complete:chat-data', () => {
|
||||
this.on('load_tracker:complete:chat-data', (list) => {
|
||||
if ( this.triggered_reload ) {
|
||||
const sc = this.resolve('site.chat');
|
||||
if ( sc?.addNotice )
|
||||
sc.addNotice('*', this.i18n.t('chat.command.reload.done', 'FFZ has finished reloading data.'));
|
||||
sc.addNotice('*', this.i18n.t('chat.command.reload.done', 'FFZ has finished reloading data. (Sources: {list})', {list: list.join(', ')}));
|
||||
}
|
||||
|
||||
this.triggered_reload = false;
|
||||
|
|
|
@ -22,7 +22,7 @@ const SHRINK_X = MODIFIER_FLAGS.ShrinkX,
|
|||
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
|
||||
//WHITESPACE = /^\s*$/,
|
||||
//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./#%&@()\-+=:?~]*[^\s\.!,?])?))/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 = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex
|
||||
|
|
|
@ -361,6 +361,12 @@ export default class Metadata extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
// Get the video element.
|
||||
const video = maybe_call(player.getHTMLVideoElement, player);
|
||||
stats.avOffset = 0;
|
||||
if ( video?._ffz_context )
|
||||
stats.avOffset = (video._ffz_context_offset ?? 0) + video._ffz_context.currentTime - video.currentTime;
|
||||
|
||||
let tampered = false;
|
||||
try {
|
||||
const url = player.core.state.path;
|
||||
|
@ -493,6 +499,14 @@ export default class Metadata extends Module {
|
|||
stats
|
||||
);
|
||||
|
||||
const desync = data.avOffset !== 0
|
||||
? (<div>{this.i18n.t(
|
||||
'metadata.player-stats.av-offset',
|
||||
'A/V Offset: {avOffset, number} seconds',
|
||||
stats
|
||||
)}</div>)
|
||||
: null;
|
||||
|
||||
if ( data.old )
|
||||
return [
|
||||
delayed,
|
||||
|
@ -510,6 +524,7 @@ export default class Metadata extends Module {
|
|||
<div class="tw-pd-t-05">
|
||||
{video_info}
|
||||
</div>,
|
||||
desync,
|
||||
tampered
|
||||
];
|
||||
|
||||
|
@ -522,6 +537,7 @@ export default class Metadata extends Module {
|
|||
<div class="tw-pd-t-05">
|
||||
{video_info}
|
||||
</div>,
|
||||
desync,
|
||||
tampered
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2227,7 +2227,7 @@ export default class ChatHook extends Module {
|
|||
inst.sendMessage = function(msg, extra) {
|
||||
msg = msg.replace(/\s+/g, ' ');
|
||||
|
||||
if ( msg.startsWith('/ffz') ) {
|
||||
if ( msg.startsWith('/ffz:') ) {
|
||||
msg = msg.slice(5).trim();
|
||||
const idx = msg.indexOf(' ');
|
||||
let subcmd;
|
||||
|
@ -2258,7 +2258,7 @@ export default class ChatHook extends Module {
|
|||
else
|
||||
inst.addMessage({
|
||||
type: t.chat_types.Notice,
|
||||
message: t.i18n.t('chat.ffz-command.invalid', 'No such command: /ffz {subcmd}', {subcmd})
|
||||
message: t.i18n.t('chat.ffz-command.invalid', 'No such command: /ffz:{subcmd}', {subcmd})
|
||||
});
|
||||
|
||||
return false;
|
||||
|
|
|
@ -402,13 +402,24 @@ export default class Input extends Module {
|
|||
}
|
||||
|
||||
checkForPreviews(inst, node) {
|
||||
for(const el of node.querySelectorAll?.('span[data-a-target="chat-input-emote-preview"][aria-describedby]') ?? []) {
|
||||
// We can't find the tooltip element directly (without digging into React tree at least)
|
||||
// So instead just find the relevant images in the document. This shouldn't happen TOO
|
||||
// frequently, with any luck, so the performance impact should be small.
|
||||
if ( node.querySelector?.('span[data-a-target="chat-input-emote-preview"]') ) {
|
||||
for(const target of document.querySelectorAll('.tw-tooltip-layer img.chat-line__message--emote')) {
|
||||
if ( target && target.src.startsWith('https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__') )
|
||||
this.updatePreview(inst, target);
|
||||
}
|
||||
}
|
||||
|
||||
// This no longer works because they removed aria-describedby
|
||||
/*for(const el of node.querySelectorAll?.('span[data-a-target="chat-input-emote-preview"][aria-describedby]') ?? []) {
|
||||
const cont = document.getElementById(el.getAttribute('aria-describedby')),
|
||||
target = cont && cont.querySelector('img.chat-line__message--emote');
|
||||
|
||||
if ( target && target.src.startsWith('https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__') )
|
||||
this.updatePreview(inst, target);
|
||||
}
|
||||
}*/
|
||||
|
||||
for(const target of node.querySelectorAll?.('img.chat-line__message--emote')) {
|
||||
if ( target && (target.dataset.ffzId || target.src.startsWith('https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__')) )
|
||||
|
|
|
@ -4,7 +4,11 @@ import {make_enum} from 'utilities/object';
|
|||
|
||||
|
||||
export const DEBUG = localStorage.ffzDebugMode === 'true' && document.body.classList.contains('ffz-dev');
|
||||
export const EXTENSION = document.body.classList.contains('ffz-ext') && !!__extension__;
|
||||
export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com';
|
||||
export const SERVER_OR_EXT = EXTENSION
|
||||
? __extension__
|
||||
: `${SERVER}/script`;
|
||||
|
||||
export const CLIENT_ID = 'a3bc9znoz6vi8ozsoca0inlcr4fcvkl';
|
||||
export const API_SERVER = '//api.frankerfacez.com';
|
||||
|
|
|
@ -8,6 +8,7 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
|||
|
||||
const VERSION = semver.parse(require('./package.json').version);
|
||||
const PRODUCTION = process.env.NODE_ENV === 'production';
|
||||
const FOR_EXTENSION = !! process.env.FFZ_EXTENSION;
|
||||
|
||||
const ENTRY_POINTS = {
|
||||
bridge: './src/bridge.js',
|
||||
|
@ -36,7 +37,9 @@ module.exports = {
|
|||
}
|
||||
],
|
||||
output: {
|
||||
chunkFilename: '[name].[chunkhash].js',
|
||||
chunkFilename: FOR_EXTENSION
|
||||
? '[name].js'
|
||||
: '[name].[chunkhash].js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
jsonpFunction: 'ffzWebpackJsonp',
|
||||
crossOriginLoading: 'anonymous'
|
||||
|
@ -58,7 +61,10 @@ module.exports = {
|
|||
__version_major__: VERSION.major,
|
||||
__version_minor__: VERSION.minor,
|
||||
__version_patch__: VERSION.patch,
|
||||
__version_prerelease__: VERSION.prerelease
|
||||
__version_prerelease__: VERSION.prerelease,
|
||||
__extension__: FOR_EXTENSION
|
||||
? JSON.stringify(process.env.FFZ_EXTENSION)
|
||||
: false
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
|
@ -67,7 +73,9 @@ module.exports = {
|
|||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: PRODUCTION ? '[name].[hash].css' : '[name].css'
|
||||
name: (! FOR_EXTENSION && PRODUCTION)
|
||||
? '[name].[hash].css'
|
||||
: '[name].css'
|
||||
}
|
||||
}, {
|
||||
loader: 'extract-loader'
|
||||
|
@ -89,7 +97,9 @@ module.exports = {
|
|||
type: 'javascript/auto',
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: PRODUCTION ? '[name].[hash].json' : '[name].json'
|
||||
name: (! FOR_EXTENSION && PRODUCTION)
|
||||
? '[name].[hash].json'
|
||||
: '[name].json'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -123,7 +133,9 @@ module.exports = {
|
|||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: PRODUCTION ? '[name].[hash].[ext]' : '[name].[ext]'
|
||||
name: (! FOR_EXTENSION && PRODUCTION)
|
||||
? '[name].[hash].[ext]'
|
||||
: '[name].[ext]'
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
|
|
@ -12,13 +12,16 @@ const Terser = require('terser');
|
|||
// Get Git info
|
||||
|
||||
const commit_hash = require('child_process').execSync('git rev-parse HEAD').toString().trim();
|
||||
const FOR_EXTENSION = !! process.env.FFZ_EXTENSION;
|
||||
|
||||
/* global module Buffer */
|
||||
|
||||
const minifier = content => {
|
||||
const text = content.toString('utf8');
|
||||
let text = content.toString('utf8');
|
||||
if ( FOR_EXTENSION )
|
||||
text = text.replace('__EXTENSION_PATH__', JSON.stringify(process.env.FFZ_EXTENSION));
|
||||
const minified = Terser.minify(text);
|
||||
return (minified && minified.code) ? Buffer.from(minified.code) : content;
|
||||
return (minified && minified.code) ? Buffer.from(minified.code) : Buffer.from(text);
|
||||
};
|
||||
|
||||
module.exports = merge(common, {
|
||||
|
@ -45,7 +48,9 @@ module.exports = merge(common, {
|
|||
}),
|
||||
new CopyPlugin([
|
||||
{
|
||||
from: './src/entry.js',
|
||||
from: FOR_EXTENSION
|
||||
? './src/entry_ext.js'
|
||||
: './src/entry.js',
|
||||
to: 'script.min.js',
|
||||
transform: minifier
|
||||
}
|
||||
|
@ -62,7 +67,11 @@ module.exports = merge(common, {
|
|||
],
|
||||
|
||||
output: {
|
||||
publicPath: '//cdn.frankerfacez.com/static/',
|
||||
filename: '[name].[hash].js'
|
||||
publicPath: FOR_EXTENSION
|
||||
? process.env.FFZ_EXTENSION
|
||||
: '//cdn.frankerfacez.com/static/',
|
||||
filename: FOR_EXTENSION
|
||||
? '[name].js'
|
||||
: '[name].[hash].js'
|
||||
}
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue