diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 12d02d25..872d7089 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ["https://www.frankerfacez.com/donate"] +custom: ["https://www.frankerfacez.com/subscribe"] diff --git a/fontello.config.json b/fontello.config.json index 7f83c666..f284dc20 100644 --- a/fontello.config.json +++ b/fontello.config.json @@ -867,6 +867,20 @@ "css": "doc-text", "code": 61686, "src": "fontawesome" + }, + { + "uid": "090b7864c67408ce29c67a49429b17a7", + "css": "fx", + "code": 59469, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M435.1 30C411.8 34 382.3 46.4 357.4 62.8 339.8 74.4 313.5 100.9 301.3 119.5 284.3 145.2 268.2 183 257.7 222.2L252.8 240.2 212.5 240.5 172.4 241 163.9 273.9C159.2 292.1 155.4 307.8 155.4 308.6 155.3 310.1 164.1 310.5 194.8 310.5 221 310.5 234.4 311 234.4 312 234.4 315.4 169.5 572.6 165.4 585.8 158.6 606.7 151.8 620.2 146.2 623.7 135.2 630.3 118.7 631.2 79.5 627.5 49.1 624.6 38.7 625.2 26.7 630.6-5.7 645.1-8.9 689.1 20.9 709.6 32.8 717.8 43.7 721 62.3 721.7 112.1 723.9 160.4 702.7 202.9 660.1 224.1 638.8 237 621.1 250.8 594 272 552.2 272.8 549.5 321.5 350.2L331.3 310.3 396.1 311.1C431.5 311.6 468.6 312.5 478.4 312.9L496.1 313.8 504.3 349.8C509 369.7 512.7 387 512.7 388.2 512.7 390.5 484.9 432.3 471.7 449.7 460.5 464.2 438.3 489.3 436.2 489.3 435.4 489.3 432.1 486.9 428.9 484 411.3 468.2 389.1 467.7 372.2 482.8 361.5 492.5 356.8 502.9 356.8 517.1 356.8 527.1 357.6 529.8 361.5 538 369.1 553.3 381.9 562.5 400.6 566.5 414.6 569.2 428.6 567.5 444.7 561 468.3 551.7 492.5 529.8 520.3 493.2 528.1 483 534.8 475 535.3 475.8 535.5 476.4 537.6 481.3 539.6 486.8 550.3 515.3 573.8 553.3 591.8 571.4 611 590.8 636.3 600.7 666.5 600.7 684.1 600.7 695.5 597.4 710.4 587.7 732.3 573.5 743.6 560.4 747.8 544.2 752.5 526 749.3 513.3 736.8 501 727.9 491.9 721.4 489.3 708.7 489.3 702.7 489.3 698.4 490.4 691.7 493.8 680.7 499.2 676.3 504.2 669.1 519.6 666.1 526.3 663.1 531.7 662.5 531.7 656.8 531.7 643.8 516.2 635.2 499.5 626.8 483.4 625.8 479.9 613.8 423.5L603.5 375.9 608.8 367.8C625.8 342.2 649.2 317.3 661.4 311.9 665.6 310 675.3 308.1 686.7 306.7 697 305.6 709.1 303.7 713.8 302.5 733.4 297.5 746.9 281.4 747.5 262.2 748.4 229.1 718.8 208.4 683.3 217.5 655.7 224.7 616.6 252.5 593.3 281.8 589.6 286.4 586.4 290 585.9 290 585.5 290 583.4 284.2 581.2 277.1 573.9 253.9 563.1 240.7 548.3 237.5 543.3 236.4 515 236.7 452.8 238.2 404 239.4 360.8 240.2 356.7 239.9L349.2 239.5 355.4 213.1C367.1 163.2 377.9 136.2 392.1 121.9 399 115 400.3 114.3 406.3 114.3 409.9 114.3 422 115.9 433.2 118.1 481.8 127 494.5 125.5 510.6 109 528.1 91 526.8 61.8 507.6 44.1 491.7 29.6 466.6 24.6 435.1 30Z", + "width": 1000 + }, + "search": [ + "fx" + ] } ] } \ No newline at end of file diff --git a/res/font/ffz-fontello.eot b/res/font/ffz-fontello.eot index 7c5b8e98..22ce8442 100644 Binary files a/res/font/ffz-fontello.eot and b/res/font/ffz-fontello.eot differ diff --git a/res/font/ffz-fontello.svg b/res/font/ffz-fontello.svg index c9388b4e..84288f8a 100644 --- a/res/font/ffz-fontello.svg +++ b/res/font/ffz-fontello.svg @@ -1,7 +1,7 @@ -Copyright (C) 2022 by original authors @ fontello.com +Copyright (C) 2023 by original authors @ fontello.com @@ -160,6 +160,8 @@ + + diff --git a/res/font/ffz-fontello.ttf b/res/font/ffz-fontello.ttf index 6d4383ec..9c1a5f7e 100644 Binary files a/res/font/ffz-fontello.ttf and b/res/font/ffz-fontello.ttf differ diff --git a/res/font/ffz-fontello.woff b/res/font/ffz-fontello.woff index 2085249f..c2e1efb3 100644 Binary files a/res/font/ffz-fontello.woff and b/res/font/ffz-fontello.woff differ diff --git a/res/font/ffz-fontello.woff2 b/res/font/ffz-fontello.woff2 index ab4a6bc2..442e500a 100644 Binary files a/res/font/ffz-fontello.woff2 and b/res/font/ffz-fontello.woff2 differ diff --git a/src/clips.js b/src/clips.js index a720da2f..cdcfc726 100644 --- a/src/clips.js +++ b/src/clips.js @@ -13,6 +13,7 @@ import SettingsManager from './settings/index'; import AddonManager from './addons'; import ExperimentManager from './experiments'; import {TranslationManager} from './i18n'; +import StagingSelector from './staging'; import Site from './sites/clips'; import Tooltips from 'src/modules/tooltips'; @@ -52,6 +53,7 @@ class FrankerFaceZ extends Module { this.inject('settings', SettingsManager); this.inject('experiments', ExperimentManager); this.inject('i18n', TranslationManager); + this.inject('staging', StagingSelector); this.inject('site', Site); this.inject('addons', AddonManager); diff --git a/src/i18n.js b/src/i18n.js index 27114c4f..6680b851 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -800,6 +800,10 @@ export class TranslationManager extends Module { return this._.formatNumber(...args); } + formatCurrency(...args) { + return this._.formatCurrency(...args); + } + formatDuration(...args) { return this._.formatDuration(...args); } diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index 34138a54..ddd530fb 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -995,7 +995,7 @@ export default class Badges extends Module { name = badge?.name; let c = 0; - if ( name === 'supporter' || name === 'bot' ) { + if ( name === 'supporter' || name === 'subwoofer' || name === 'bot' ) { this.setBulk('ffz-global', badge_id, data.users[badge_id].map(x => String(x))); /*this.supporter_id = badge_id; for(const user_id of data.users[badge_id]) @@ -1032,8 +1032,8 @@ export default class Badges extends Module { data.replaces = true; } - if ( ! data.addon && (data.name === 'developer' || data.name === 'supporter') ) - data.click_url = 'https://www.frankerfacez.com/donate'; + if ( ! data.addon && (data.name === 'developer' || data.name === 'subwoofer' || data.name === 'supporter') ) + data.click_url = 'https://www.frankerfacez.com/subscribe'; } if ( generate_css ) diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 838c91f2..0165c4fa 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -11,6 +11,7 @@ import {NEW_API, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POINTS_SETS, TWI import GET_EMOTE from './emote_info.gql'; import GET_EMOTE_SET from './emote_set_info.gql'; +import { FFZEvent } from 'src/utilities/events'; const HoverRAF = Symbol('FFZ:Hover:RAF'); const HoverState = Symbol('FFZ:Hover:State'); @@ -32,7 +33,9 @@ export const MODIFIER_FLAGS = make_enum_flags( 'Rainbow', 'HyperRed', 'Shake', - 'Photocopy' + 'Photocopy', + 'Jam', + 'Bounce' ); export const MODIFIER_KEYS = Object.values(MODIFIER_FLAGS).filter(x => typeof x === 'number'); @@ -48,19 +51,19 @@ const MODIFIER_FLAG_CSS = { }, ShrinkX: { title: 'Squish Horizontal', - transform: 'scaleX(0.5)' + //transform: 'scaleX(0.5)' }, GrowX: { title: 'Stretch Horizontal', - transform: 'scaleX(2)' + //transform: 'scaleX(2) translateX(25%)' }, ShrinkY: { title: 'Squish Vertical', - transform: 'scaleY(0.5)' + //transform: 'scaleY(0.5)' }, GrowY: { title: 'Stretch Vertical', - transform: 'scaleY(2)' + //transform: 'scaleY(2) translateY(25%)' }, Rotate45: { title: 'Rotate 45 Degrees', @@ -127,27 +130,89 @@ const MODIFIER_FLAG_CSS = { }` }, Photocopy: { - title: 'Photocopy', - filter: 'grayscale(1) brightness(0.65) contrast(5)' + title: 'Cursed', + filter: 'grayscale(1) brightness(0.7) contrast(2.5)' + }, + Jam: { + title: 'Jam Animation', + animation: 'ffz-effect-jam 0.6s linear infinite', + animationTransform: 'ffz-effect-jam-transform 0.6s linear infinite', + raw: `@keyframes ffz-effect-jam { +0% { transform: translate(-2px, -2px) rotate(-6deg); } +10% { transform: translate(-1.5px, -2px) rotate(-8deg); } +20% { transform: translate(1px, -1.5px) rotate(-8deg); } +30% { transform: translate(3px, 2.5px) rotate(-6deg); } +40% { transform: translate(3px, 4px) rotate(-2deg); } +50% { transform: translate(2px, 4px) rotate(3deg); } +60% { transform: translate(1px, 4px) rotate(3deg); } +70% { transform: translate(-0.5px, 3px) rotate(2deg); } +80% { transform: translate(-1.25px, 1px) rotate(0deg); } +90% { transform: translate(-1.75px, -0.5px) rotate(-2deg); } +100% { transform: translate(-2px, -2px) rotate(-5deg); } +} +@keyframes ffz-effect-jam-transform { +0% { transform: var(--ffz-effect-transforms) translate(-2px, -2px) rotate(-6deg); } +10% { transform: var(--ffz-effect-transforms) translate(-1.5px, -2px) rotate(-8deg); } +20% { transform: var(--ffz-effect-transforms) translate(1px, -1.5px) rotate(-8deg); } +30% { transform: var(--ffz-effect-transforms) translate(3px, 2.5px) rotate(-6deg); } +40% { transform: var(--ffz-effect-transforms) translate(3px, 4px) rotate(-2deg); } +50% { transform: var(--ffz-effect-transforms) translate(2px, 4px) rotate(3deg); } +60% { transform: var(--ffz-effect-transforms) translate(1px, 4px) rotate(3deg); } +70% { transform: var(--ffz-effect-transforms) translate(-0.5px, 3px) rotate(2deg); } +80% { transform: var(--ffz-effect-transforms) translate(-1.25px, 1px) rotate(0deg); } +90% { transform: var(--ffz-effect-transforms) translate(-1.75px, -0.5px) rotate(-2deg); } +100% { transform: var(--ffz-effect-transforms) translate(-2px, -2px) rotate(-5deg); } +}`, + }, + Bounce: { + animation: 'ffz-effect-bounce 0.5s linear infinite', + animationTransform: 'ffz-effect-bounce-transform 0.5s linear infinite', + transformOrigin: 'bottom center', + raw: `@keyframes ffz-effect-bounce { +0% { transform: scale(0.8, 1); } +10% { transform: scale(0.9, 0.8); } +20% { transform: scale(1, 0.4); } +25% { transform: scale(1.2, 0.3); } +25.001% { transform: scale(-1.2, 0.3); } +30% { transform: scale(-1, 0.4); } +40% { transform: scale(-0.9, 0.8); } +50% { transform: scale(-0.8, 1); } +60% { transform: scale(-0.9, 0.8); } +70% { transform: scale(-1, 0.4); } +75% { transform: scale(-1.2, 0.3); } +75.001% { transform: scale(1.2, 0.3); } +80% { transform: scale(1, 0.4); } +90% { transform: scale(0.9, 0.8); } +100% { transform: scale(0.8, 1); } +} +@keyframes ffz-effect-bounce-transform { +0% { transform: scale(0.8, 1) var(--ffz-effect-transforms); } +10% { transform: scale(0.9, 0.8) var(--ffz-effect-transforms); } +20% { transform: scale(1, 0.4) var(--ffz-effect-transforms); } +25% { transform: scale(1.2, 0.3) var(--ffz-effect-transforms); } +25.001% { transform: scale(-1.2, 0.3) var(--ffz-effect-transforms); } +30% { transform: scale(-1, 0.4) var(--ffz-effect-transforms); } +40% { transform: scale(-0.9, 0.8) var(--ffz-effect-transforms); } +50% { transform: scale(-0.8, 1) var(--ffz-effect-transforms); } +60% { transform: scale(-0.9, 0.8) var(--ffz-effect-transforms); } +70% { transform: scale(-1, 0.4) var(--ffz-effect-transforms); } +75% { transform: scale(-1.2, 0.3) var(--ffz-effect-transforms); } +75.001% { transform: scale(1.2, 0.3) var(--ffz-effect-transforms); } +80% { transform: scale(1, 0.4) var(--ffz-effect-transforms); } +90% { transform: scale(0.9, 0.8) var(--ffz-effect-transforms); } +100% { transform: scale(0.8, 1) var(--ffz-effect-transforms); } +}` } }; function generateBaseFilterCss() { - console.log('flags', MODIFIER_FLAGS); - console.log('css', MODIFIER_FLAG_CSS); - const out = [ `.modified-emote[data-effects] > img { --ffz-effect-filters: none; --ffz-effect-transforms: initial; --ffz-effect-animations: initial; -}`/*, - `.modified-emote[data-effects] > img { - filter: var(--ffz-effect-filters); - transform: var(--ffz-effect-transforms); - animation: var(--ffz-effect-animations); -}`*/ +}` ]; for(const [key, val] of Object.entries(MODIFIER_FLAG_CSS)) { @@ -223,6 +288,7 @@ export default class Emotes extends Module { this.pending_effects = new Set(); this.applyEffects = this.applyEffects.bind(this); + this.sub_sets = new SourcedSet; this.default_sets = new SourcedSet; this.global_sets = new SourcedSet; @@ -436,7 +502,7 @@ export default class Emotes extends Module { if ( ! this.parent.context.get('chat.effects.enable') ) return null; - let filter, transform, animation, animations = []; + let filter, transformOrigin, transform, animation, animations = []; for(const key of MODIFIER_KEYS) { if ( (flags & key) !== key || ! this.effects_enabled[key] ) @@ -454,6 +520,9 @@ export default class Emotes extends Module { ? `${filter} ${input.filter}` : input.filter; + if ( input.transformOrigin ) + transformOrigin = input.transformOrigin; + if ( input.transform ) transform = transform ? `${transform} ${input.transform}` @@ -481,7 +550,8 @@ export default class Emotes extends Module { return `.modified-emote[data-effects="${flags}"] > img {${filter ? ` --ffz-effect-filters: ${filter}; - filter: var(--ffz-effect-filters);` : ''}${transform ? ` + filter: var(--ffz-effect-filters);` : ''}${transformOrigin ? ` + transform-origin: ${transformOrigin};` : ''}${transform ? ` --ffz-effect-transforms: ${transform}; transform: var(--ffz-effect-transforms);` : ''}${animation ? ` --ffz-effect-animations: ${animation}; @@ -694,7 +764,6 @@ export default class Emotes extends Module { this.settings.provider.set(key, favs); } - handleClick(event) { const target = event.target, ds = target && target.dataset; @@ -710,7 +779,7 @@ export default class Emotes extends Module { let url; if ( provider === 'twitch' ) { - url = `https://twitchemotes.com/emotes/${ds.id}`; + url = null; // = `https://twitchemotes.com/emotes/${ds.id}`; if ( click_sub ) { const apollo = this.resolve('site.apollo'); @@ -803,6 +872,16 @@ export default class Emotes extends Module { return true; } + const evt = new FFZEvent({ + provider, + id: ds.id, + source: event + }); + + this.emit('chat.emotes:click', evt); + if ( evt.defaultPrevented ) + return true; + if ( provider === 'twitch' && this.parent.context.get('chat.emote-dialogs') ) { const fine = this.resolve('site.fine'); if ( ! fine ) @@ -836,6 +915,55 @@ export default class Emotes extends Module { // Access // ======================================================================== + getTargetEmote() { + const me = this.resolve('site').getUser(), + Input = me ? this.resolve('site.chat.input') : null, + entered = Input ? Input.getInput() : null; + + const menu = this.resolve('site.chat.emote_menu')?.MenuWrapper?.first, + emote_sets = menu?.getAllSets?.(), + emotes = emote_sets + ? emote_sets.map(x => x.emotes).flat().filter(x => ! x.effects) + : null; + + if ( entered && emotes ) { + // Okay this is gonna be oof. + const name_map = {}; + for(let i = 0; i < emotes.length; i++) + if ( ! name_map[emotes[i].name] ) + name_map[emotes[i].name] = i; + + const words = entered.split(' '); + let i = words.length; + while(i--) { + const word = words[i]; + if ( name_map[word] != null ) + return emotes[name_map[word]]; + } + } + + // Random emote + if ( emotes && emotes.length ) { + const idx = Math.floor(Math.random() * emotes.length), + emote = emotes[idx]; + + return emote; + } + + // Return LaterSooner + return { + provider: 'ffz', + set_id: 3, + id: 149346, + name: 'LaterSooner', + src: 'https://cdn.frankerfacez.com/emote/149346/1', + srcSet: 'https://cdn.frankerfacez.com/emote/149346/1 1x, https://cdn.frankerfacez.com/emote/149346/2 2x, https://cdn.frankerfacez.com/emote/149346/4 4x', + width: 25, + height: 32 + } + } + + getSetIDs(user_id, user_login, room_id, room_login) { const room = this.parent.getRoom(room_id, room_login, true), room_user = room && room.getUser(user_id, user_login, true), @@ -949,6 +1077,21 @@ export default class Emotes extends Module { .map(([set_id, source]) => [this.emote_sets[set_id], source]); } + + getSubSetIDsWithSources() { + const out = [], seen = new Set; + + this._withSources(out, seen, this.sub_sets); + + return out; + } + + getSubSetsWithSources() { + return this.getSubSetIDsWithSources() + .map(([set_id, source]) => [this.emote_sets[set_id], source]); + } + + getGlobalSetIDs(user_id, user_login) { const user = this.parent.getUser(user_id, user_login, true); @@ -1073,6 +1216,45 @@ export default class Emotes extends Module { return false; } + addSubSet(provider, set_id, data) { + if ( typeof set_id === 'number' ) + set_id = `${set_id}`; + + let changed = false, added = false; + if ( ! this.sub_sets.sourceIncludes(provider, set_id) ) { + changed = ! this.sub_sets.includes(set_id); + this.sub_sets.push(provider, set_id); + added = true; + } + + if ( data ) + this.loadSetData(set_id, data); + + if ( changed ) { + this.refSet(set_id); + this.emit(':update-sub-sets', provider, set_id, true); + } + + return added; + } + + removeSubSet(provider, set_id) { + if ( typeof set_id === 'number' ) + set_id = `${set_id}`; + + if ( this.sub_sets.sourceIncludes(provider, set_id) ) { + this.sub_sets.remove(provider, set_id); + if ( ! this.sub_sets.includes(set_id) ) { + this.unrefSet(set_id); + this.emit(':update-sub-sets', provider, set_id, false); + } + + return true; + } + + return false; + } + refSet(set_id) { this._set_refs[set_id] = (this._set_refs[set_id] || 0) + 1; if ( this._set_timers[set_id] ) { @@ -1101,7 +1283,7 @@ export default class Emotes extends Module { } catch(err) { /* do nothing */ } try { - response = await fetch(`${this.staging.api}/v1/set/global${this.staging.active ? '/ids' : ''}`) + response = await fetch(`${this.staging.api}/v1/set/global/ids`) } catch(err) { tries++; if ( tries < 10 ) @@ -1127,8 +1309,12 @@ export default class Emotes extends Module { this.addDefaultSet('ffz-global', set_id); for(const set_id in sets) - if ( has(sets, set_id) ) + if ( has(sets, set_id) ) { + const id = sets[set_id]?.id; this.loadSetData(set_id, sets[set_id]); + if ( id && ! data.default_sets.includes(id) ) + this.addSubSet('ffz-global', set_id); + } if ( data.user_ids ) this.loadSetUserIds(data.user_ids); @@ -1262,6 +1448,7 @@ export default class Emotes extends Module { text: emote.hidden ? '???' : emote.name, length: emote.name.length, height: emote.height, + width: emote.width, source_modifier_flags: emote.modifier_flags ?? 0 }; diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index c54c50f0..e890a7c7 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -10,6 +10,15 @@ import {has, getTwitchEmoteURL, split_chars, getTwitchEmoteSrcSet} from 'utiliti import {EmoteTypes, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants'; import {CATEGORIES, JOINER_REPLACEMENT} from './emoji'; +import { MODIFIER_FLAGS } from './emotes'; + +const SHRINK_X = MODIFIER_FLAGS.ShrinkX, + STRETCH_X = MODIFIER_FLAGS.GrowX, + SHRINK_Y = MODIFIER_FLAGS.ShrinkY, + STRETCH_Y = MODIFIER_FLAGS.GrowY, + ROTATE_45 = MODIFIER_FLAGS.Rotate45, + ROTATE_90 = MODIFIER_FLAGS.Rotate90; + const EMOTE_CLASS = 'chat-image chat-line__message--emote', //WHITESPACE = /^\s*$/, @@ -1213,12 +1222,45 @@ export const AddonEmotes = { hoverSrcSet = big ? token.animSrcSet2 : token.animSrcSet; } + let style = undefined; + const effects = token.modifier_flags, + is_big = (token.big && ! token.can_big && token.height); + + if ( effects ) { + this.emotes.ensureEffect(effects); + style = { + width: is_big ? token.width * 2 : token.width, + height: is_big ? token.height * 2 : token.height + } + + if ( (effects & SHRINK_X) === SHRINK_X ) + style.width *= 0.5; + if ( (effects & STRETCH_X) === STRETCH_X ) + style.width *= 2; + if ( (effects & SHRINK_Y) === SHRINK_Y ) + style.height *= 0.5; + if ( (effects & STRETCH_Y) === STRETCH_Y ) + style.height *= 2; + + if ( (effects & ROTATE_90) === ROTATE_90 ) { + const w = style.width; + style.width = style.height; + style.height = w; + } + + if ( style.width > 128 ) + style.width = 128; + if ( style.height > 40 ) + style.height = 40; + } + const mods = token.modifiers || [], ml = mods.length, emote = ({token.text}); - if ( ! ml ) { + if ( ! ml && ! token.modifier_flags ) { if ( wrapped ) return emote; return (
{emote}
); } - const effects = token.modifier_flags; - if ( effects ) - this.emotes.ensureEffect(effects); - return (
x.id).join(' ') : null} data-effects={effects ? effects : undefined} onClick={this.emotes.handleClick} @@ -1369,6 +1408,76 @@ export const AddonEmotes = { else if ( emote.urls[2] ) preview = emote.urls[2]; } + + if ( ds.effects && emote.modifier && emote.modifier_flags ) { + owner = null; + + const effects = emote.modifier_flags; + this.emotes.ensureEffect(effects); + + const target = this.emotes.getTargetEmote(); + + let style = { + width: (target.width ?? 28) * 2, + height: (target.height ?? 28) * 2 + }; + + let changed = false; + + if ( (effects & SHRINK_X) === SHRINK_X ) { + style.width *= 0.5; + changed = true; + } + if ( (effects & STRETCH_X) === STRETCH_X ) { + style.width *= 2; + changed = true; + } + if ( (effects & SHRINK_Y) === SHRINK_Y ) { + style.height *= 0.5; + changed = true; + } + if ( (effects & STRETCH_Y) === STRETCH_Y ) { + style.height *= 2; + changed = true; + } + + if ( changed ) { + if ( style.width > 512 ) + style.width = 512; + if ( style.height > 160 ) + style.height = 160; + } + + style.width = `${style.width}px`; + style.height = `${style.height}px`; + + // Whip up a special preview. + preview = (
+ + +
+ +
+
); + } } } else if ( provider === 'emoji' ) { @@ -1750,6 +1859,7 @@ export const TwitchEmotes = { anim, big, can_big, + width: 28, height: 28, // Not always accurate but close enough. text: text.slice(e_start - t_start, e_end - t_start).join(''), modifiers: [], diff --git a/src/modules/main_menu/faq.md b/src/modules/main_menu/faq.md index acfcb0de..4c2f9e0a 100644 --- a/src/modules/main_menu/faq.md +++ b/src/modules/main_menu/faq.md @@ -13,7 +13,7 @@ Due to performance problems with our current website, we have to use caching on * *I don't want the `FFZ Supporter` badge.* -Users can toggle the visibility of their supporter badge at: [https://www.frankerfacez.com/donate](https://www.frankerfacez.com/donate) +Users can toggle the visibility of their FFZ badges at: [https://www.frankerfacez.com/settings/profile](https://www.frankerfacez.com/settings/profile) * *I can see my emotes, but someone in chat said they can't.* diff --git a/src/player.js b/src/player.js index 71e6a7b3..f200553f 100644 --- a/src/player.js +++ b/src/player.js @@ -13,6 +13,7 @@ import SettingsManager from './settings/index'; import AddonManager from './addons'; import ExperimentManager from './experiments'; import {TranslationManager} from './i18n'; +import StagingSelector from './staging'; import Site from './sites/player'; class FrankerFaceZ extends Module { @@ -49,6 +50,7 @@ class FrankerFaceZ extends Module { this.inject('settings', SettingsManager); this.inject('experiments', ExperimentManager); this.inject('i18n', TranslationManager); + this.inject('staging', StagingSelector); this.inject('site', Site); this.inject('addons', AddonManager); diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index 498aea3c..851f7129 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -1443,6 +1443,47 @@ export default class PlayerBase extends Module { tip.textContent = label; } + replaceVideoElement(player, video) { + const new_vid = createElement('video'), + vol = video?._ffz_pregain_volume ?? video?.volume ?? player.getVolume(), + muted = player.isMuted(); + + new_vid._ffz_gain_value = video._ffz_gain_value; + new_vid._ffz_state = video._ffz_state; + new_vid._ffz_toggled = video._ffz_toggled; + new_vid._ffz_maybe_compress = video._ffz_compressed; + new_vid.volume = vol; + if ( muted ) + new_vid.muted = true; + new_vid.playsInline = true; + + this.installPlaybackRate(new_vid); + video.replaceWith(new_vid); + player.attachHTMLVideoElement(new_vid); + return new_vid; + } + + hookPlayerLoad(player) { + if ( ! player || player._ffz_load ) + return; + + player._ffz_load = player.load; + + player.load = (...args) => { + try { + const video = player.getHTMLVideoElement(); + if ( video?._ffz_compressor && player.attachHTMLVideoElement ) { + this.log.info('Recreating video element due to player load with compressor installed.'); + this.replaceVideoElement(player, video); + } + } catch(err) { + t.log.error('Error while handling player load.', err); + } + + return player._ffz_load(...args); + } + } + compressPlayer(inst, e) { const player = inst.props.mediaPlayerInstance, core = player.core || player, @@ -1451,6 +1492,21 @@ export default class PlayerBase extends Module { if ( ! video || ! HAS_COMPRESSOR ) return; + // Backup the player load method. + this.hookPlayerLoad(player); + + // Backup and replace the setSrc method. + if ( ! inst._ffz_setSrc ) { + inst._ffz_setSrc = inst.setSrc; + inst.setSrc = async function(...args) { + console.log('setSrc', args); + const vid = inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video; + if ( vid && vid._ffz_compressor ) + await this.resetPlayer(inst); + return inst._ffz_setSrc(...args); + } + } + // Backup the setVolume method. if ( ! core._ffz_setVolume ) { core._ffz_setVolume = core.setVolume; @@ -2028,7 +2084,7 @@ export default class PlayerBase extends Module { } - resetPlayer(inst, e) { + async resetPlayer(inst, e) { const player = inst ? ((inst.mediaSinkManager || inst.core?.mediaSinkManager) ? inst : inst?.props?.mediaPlayerInstance) : null; if ( e ) { @@ -2064,40 +2120,20 @@ export default class PlayerBase extends Module { const video = player.mediaSinkManager?.video || player.core?.mediaSinkManager?.video; if ( video?._ffz_compressor && player.attachHTMLVideoElement ) { - const new_vid = createElement('video'), - vol = video?._ffz_pregain_volume ?? video?.volume ?? player.getVolume(), - muted = player.isMuted(); - - new_vid._ffz_gain_value = video._ffz_gain_value; - new_vid._ffz_state = video._ffz_state; - new_vid._ffz_toggled = video._ffz_toggled; + const new_vid = this.replaceVideoElement(player, video); new_vid._ffz_maybe_compress = true; - new_vid.volume = muted ? 0 : vol; - new_vid.playsInline = true; - - this.installPlaybackRate(new_vid); - video.replaceWith(new_vid); - player.attachHTMLVideoElement(new_vid); - setTimeout(() => { - player.setVolume(vol); - player.setMuted(muted); - - //localStorage.volume = vol; - //localStorage.setItem('video-muted', JSON.stringify({default: muted})); - }, 0); } this.PlayerSource.check(); for(const inst of this.PlayerSource.instances) { if ( ! player || player === inst.props?.mediaPlayerInstance ) - inst.setSrc({isNewMediaPlayerInstance: false}); + await inst.setSrc({isNewMediaPlayerInstance: false}); } if ( position > 0 ) setTimeout(() => player.seekTo(position), 250); } - addMetadata(inst) { if ( ! this.metadata ) return; diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index fc3fa409..fc9568c2 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -150,6 +150,7 @@ export default class EmoteMenu extends Module { constructor(...args) { super(...args); + this.inject('staging'); this.inject('settings'); this.inject('i18n'); this.inject('chat'); @@ -793,7 +794,7 @@ export default class EmoteMenu extends Module { let source = data.source_i18n ? t.i18n.t(data.source_i18n, data.source) : data.source; if ( source == null ) - source = 'FrankerFaceZ'; + source = 'FFZ'; return (
{show_heading ? ( @@ -833,7 +834,9 @@ export default class EmoteMenu extends Module { let sellout = ''; if ( emote_lock ) { - if ( emote_lock.id === 'cheer' ) { + if ( emote_lock.id === 'subwoofer' ) { + sellout = t.i18n.t('emote-menu.emote-subwoofer', 'Become an FFZ Subwoofer to unlock this emote.'); + } else if ( emote_lock.id === 'cheer' ) { sellout = t.i18n.t('emote-menu.emote-cheer', 'Cheer an additional {bits_remaining, plural, one {# bit} other {# bits}} to unlock this emote.', emote_lock); } else if ( emote_lock.id === 'follower' ) { sellout = t.i18n.t('emote-menu.emote-follower', 'Follow {user} to unlock this emote in their channel.', emote_lock); @@ -886,6 +889,7 @@ export default class EmoteMenu extends Module { data-set={emote.set_id} data-code={emote.code} data-modifiers={modifiers} + data-effects={emote.effects} data-variant={emote.variant} data-no-source={source} data-name={emote.name} @@ -917,16 +921,27 @@ export default class EmoteMenu extends Module { if ( ! data.all_locked || ! data.locks ) return null; - const lock = data.locks[this.state.unlocked], - locks = Object.values(data.locks).filter(x => x.id !== 'cheer'); + let lock = data.locks[this.state.unlocked], + locks = Object.values(data.locks).filter(x => x.id !== 'cheer'), + has_ffz = locks.filter(x => x.is_ffz).length > 0; + + if ( ! lock && data.locks.length === 1 ) + lock = data.locks[0]; if ( ! locks.length ) return null; return (
- {lock ? - t.i18n.t('emote-menu.sub-unlock', 'Subscribe for {price} to unlock {count, plural, one {# emote} other {# emotes}}', {price: lock.price, count: lock.emotes.size}) : - t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')} + {has_ffz + ? t.i18n.t('emote-menu.ffz-unlock', 'This feature is available to FFZ Subwoofers.') + : (lock + ? t.i18n.t('emote-menu.sub-unlock', 'Subscribe for {price} to unlock {count, plural, one {# emote} other {# emotes}}', {price: lock.price, count: lock.emotes.size}) + : t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes') + ) + } + {has_ffz && this.props.ffz_sub_data?.has_free_sub + ?
{t.i18n.t('emote-menu.free-sub.about', 'As thanks for supporting us in the past, you can get one month of FFZ Subwoofer for free.')}
+ : null} @@ -1092,8 +1110,13 @@ export default class EmoteMenu extends Module { tone: t.settings.provider.get('emoji-tone', null) } - if ( props.visible ) + if ( props.visible ) { this.loadData(); + if ( this.state.wants_plan_info ) + this.loadFFZPlanData(); + if ( this.state.wants_resub_info ) + this.loadFFZSubData(); + } this.rebuildData(); @@ -1310,6 +1333,9 @@ export default class EmoteMenu extends Module { case 'channel': sets = this.state.filtered_channel_sets; break; + case 'effects': + sets = this.state.filtered_effect_sets; + break; case 'emoji': sets = this.state.filtered_emoji_sets; break; @@ -1437,6 +1463,46 @@ export default class EmoteMenu extends Module { return true; } + loadFFZPlanData(force = false, props, state) { + state = state || this.state; + if ( ! state || state.ffz_plan_loading ) + return false; + + if ( state.ffz_sub_data && ! force ) + return false; + + this.setState({ffz_plan_loading: true}, () => { + t.getFFZSubPrices().then(d => { + this.setState(this.filterState(this.state.filter, this.buildState( + this.props, + Object.assign({}, this.state, {ffz_plan_data: d, ffz_plan_loading: false}) + ))); + }) + }); + + return true; + } + + loadFFZSubData(force = false, props, state) { + state = state || this.state; + if ( ! state || state.ffz_loading ) + return false; + + if ( state.ffz_sub_data && ! force ) + return false; + + this.setState({ffz_loading: true}, () => { + t.getFFZSubData().then(d => { + this.setState(this.filterState(this.state.filter, this.buildState( + this.props, + Object.assign({}, this.state, {ffz_sub_data: d, ffz_loading: false}) + ))); + }) + }); + + return true; + } + filterState(input, old_state, visibility_control) { const state = Object.assign({}, old_state); @@ -1449,6 +1515,7 @@ export default class EmoteMenu extends Module { state.filtered = input && input.length > 0 && input !== ':' || false; state.filtered_channel_sets = this.filterSets(input, state.channel_sets, visibility_control); + state.filtered_effect_sets = this.filterSets(input, state.effect_sets, visibility_control); state.filtered_all_sets = this.filterSets(input, state.all_sets, visibility_control); state.filtered_fav_sets = this.filterSets(input, state.fav_sets, visibility_control); state.filtered_emoji_sets = this.filterSets(input, state.emoji_sets, visibility_control); @@ -1619,6 +1686,13 @@ export default class EmoteMenu extends Module { return state; } + getAllSets() { + return [ + ...(this.state.channel_sets || []), + ...(this.state.effect_sets || []), + ...(this.state.all_sets || []) + ]; + } getSorter() { // eslint-disable-line class-methods-use-this return EMOTE_SORTERS[t.chat.context.get('chat.emote-menu.sort-emotes')] || EMOTE_SORTERS[0] || (() => 0); @@ -1631,6 +1705,7 @@ export default class EmoteMenu extends Module { modifiers = state.emote_modifiers = {}, channel = state.channel_sets = [], all = state.all_sets = [], + effects = state.effect_sets = [], favorites = state.favorites = []; // If we're still loading, don't set any data. @@ -1987,8 +2062,8 @@ export default class EmoteMenu extends Module { hide_button: true, emotes: lock_set = new Set() } - else - section.all_locked = false; + /*else + section.all_locked = false;*/ let order = 0; for(const emote of local.emotes) { @@ -2178,56 +2253,106 @@ export default class EmoteMenu extends Module { } } + let wants_resub_info = false, + wants_plan_info = false; // Finally, emotes added by FrankerFaceZ. if ( t.chat.context.get('chat.emotes.enabled') > 1 ) { const me = t.site.getUser(); - if ( me ) { - const ffz_room = t.emotes.getRoomSetsWithSources(me.id, me.login, props.channel_id, null), - ffz_global = t.emotes.getGlobalSetsWithSources(me.id, me.login), - seen_favorites = {}; - let grouped_sets = {}; + const ffz_room = t.emotes.getRoomSetsWithSources(me?.id, me?.login, props.channel_id, null), + ffz_subs = t.emotes.getSubSetsWithSources(), + ffz_global = t.emotes.getGlobalSetsWithSources(me?.id, me?.login), + seen_sets = new Set(), + seen_favorites = {}; - for(const [emote_set, provider] of ffz_room) { - const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets); - if ( section ) { - section.emotes.sort(sort_emotes); + let grouped_sets = {}; - if ( ! channel.includes(section) ) - channel.push(section); - } + for(const [emote_set, provider] of ffz_room) { + if ( seen_sets.has(emote_set) ) + continue; + seen_sets.add(emote_set); + + const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets); + if ( section ) { + section.emotes.sort(sort_emotes); + + if ( ! channel.includes(section) ) + channel.push(section); } + } - grouped_sets = {}; + grouped_sets = {}; - for(const [emote_set, provider] of ffz_global) { - const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets); - if ( section ) { - section.emotes.sort(sort_emotes); + const global_set_ids = ffz_global.map(x => x?.[0]?.id); - if ( ! all.includes(section) ) - all.push(section); + for(const [emote_set, provider] of ffz_subs) { + if ( seen_sets.has(emote_set) ) + continue; + seen_sets.add(emote_set); - if ( ! channel.includes(section) && maybe_call(section.force_global, this, emote_set, props.channel_data && props.channel_data.user, me) ) - channel.push(section); - } + const locked = ! global_set_ids.includes(emote_set.id); + + wants_resub_info = true; + + const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets, locked, state); + if ( section ) { + section.emotes.sort(sort_emotes); + + if ( ! effects.includes(section) && section.has_effects ) + effects.push(section); + else if ( ! all.includes(section) ) + all.push(section); + } + } + + grouped_sets = {}; + + for(const [emote_set, provider] of ffz_global) { + if ( seen_sets.has(emote_set) ) + continue; + seen_sets.add(emote_set); + + const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets); + if ( section ) { + section.emotes.sort(sort_emotes); + + if ( ! effects.includes(section) && section.has_effects ) + effects.push(section); + + else if ( ! all.includes(section) ) + all.push(section); + + if ( ! channel.includes(section) && maybe_call(section.force_global, this, emote_set, props.channel_data && props.channel_data.user, me) ) + channel.push(section); } } } + // Load FFZ sub data. + state.wants_resub_info = wants_resub_info; + state.wants_plan_info = wants_plan_info; + + if ( this.props.visible ) { + if ( wants_plan_info ) + this.loadFFZPlanData(); + if ( wants_resub_info ) + this.loadFFZSubData(); + } // Sort Sets channel.sort(sort_sets); + effects.sort(sort_sets); all.sort(sort_sets); state.has_channel_tab = channel.length > 0; + state.has_effect_Tab = effects.length > 0; return this.buildEmoji(state); } - processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets) { // eslint-disable-line class-methods-use-this + processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets, locked = false, state) { // eslint-disable-line class-methods-use-this if ( ! emote_set || ! emote_set.emotes ) return null; @@ -2242,7 +2367,7 @@ export default class EmoteMenu extends Module { (pdata.i18n_key ? t.i18n.t(pdata.i18n_key, pdata.name, pdata) : pdata.name) : - emote_set.source || 'FrankerFaceZ', + emote_set.source || 'FFZ', title = provider === 'main' ? t.i18n.t('emote-menu.main-set', 'Channel Emotes') : @@ -2252,7 +2377,7 @@ export default class EmoteMenu extends Module { if ( sort_key == null ) sort_key = emote_set.title.toLowerCase().includes('global') ? 100 : 0; - let section, emotes; + let section, emotes, locks; if ( grouped_sets[key] ) { section = grouped_sets[key]; @@ -2277,10 +2402,31 @@ export default class EmoteMenu extends Module { title, source, emotes, - force_global: emote_set.force_global + force_global: emote_set.force_global, + all_locked: true } } + // Try to get resub info. + const resub = (state || this.state)?.ffz_sub_data?.sets?.[emote_set.id]; + if ( resub ) { + section.renews = resub.next_bill_date; + section.ends = resub.expires_at; + } + + if ( locked ) { + section.locks = section.locks || {}; + section.locks[emote_set.id] = { + set_id: emote_set.id, + id: 'subwoofer', + is_ffz: true, + price: 'More Info', + url: 'https://www.frankerfacez.com/subscribe', + emotes: locks = new Set() + } + } else + section.all_locked = false; + for(const emote of Object.values(emote_set.emotes)) if ( ! emote.hidden ) { const is_fav = known_favs.includes(emote.id), @@ -2292,18 +2438,27 @@ export default class EmoteMenu extends Module { srcSet: emote.srcSet, animSrc: emote.animSrc, animSrcSet: emote.animSrcSet, + effects: emote.modifier ? emote.modifier_flags : 0, name: emote.name, favorite: is_fav, + locked: locked, hidden: known_hidden.includes(emote.id), height: emote.height, width: emote.width }; emotes.push(em); - if ( is_fav && ! seen_favs.has(emote.id) ) { + + if ( ! locked && is_fav && ! seen_favs.has(emote.id) ) { favorites.push(em); seen_favs.add(emote.id); } + + if ( locked ) + locks.add(emote.id); + + if ( emote.modifier && emote.modifier_flags ) + section.has_effects = true; } if ( emotes.length ) @@ -2320,7 +2475,11 @@ export default class EmoteMenu extends Module { componentDidUpdate(old_props) { if ( this.props.visible && ! old_props.visible ) { this.loadData(); - return; + + if ( this.state.wants_plan_info ) + this.loadFFZPlanData(); + if ( this.state.wants_resub_info ) + this.loadFFZSubData(); } if ( ! this.props.visible && old_props.visible ) { @@ -2406,23 +2565,25 @@ export default class EmoteMenu extends Module { if ( ! loading ) this.loadedOnce = true; - let tab, sets, is_emoji, is_favs; + let tab, sets, is_emoji, is_favs, is_effect; if ( no_tabs ) { sets = [ this.state.filtered_fav_sets, this.state.filtered_channel_sets, + this.state.filtered_effect_sets, this.state.filtered_all_sets, this.state.filtered_emoji_sets ].flat(); } else { tab = this.state.tab || t.chat.context.get('chat.emote-menu.default-tab'); - if ( (tab === 'channel' && ! this.state.has_channel_tab) || (tab === 'emoji' && ! this.state.has_emoji_tab) ) + if ( (tab === 'effect' && ! this.state.has_effect_Tab) || (tab === 'channel' && ! this.state.has_channel_tab) || (tab === 'emoji' && ! this.state.has_emoji_tab) ) tab = 'all'; is_emoji = tab === 'emoji'; is_favs = tab === 'fav'; + is_effect = tab === 'effect'; switch(tab) { case 'fav': @@ -2431,6 +2592,9 @@ export default class EmoteMenu extends Module { case 'channel': sets = this.state.filtered_channel_sets; break; + case 'effect': + sets = this.state.filtered_effect_sets; + break; case 'emoji': sets = this.state.filtered_emoji_sets; break; @@ -2467,6 +2631,7 @@ export default class EmoteMenu extends Module { key: data.key, idx, data, + ffz_sub_data: this.state.ffz_sub_data, emote_modifiers: this.state.emote_modifiers, animated: this.state.animated, combineTabs: this.state.combineTabs, @@ -2606,6 +2771,20 @@ export default class EmoteMenu extends Module {
} + {this.state.has_effect_Tab &&
+ +
}