diff --git a/fontello.config.json b/fontello.config.json index f284dc20..370a7dfd 100644 --- a/fontello.config.json +++ b/fontello.config.json @@ -881,6 +881,20 @@ "search": [ "fx" ] + }, + { + "uid": "3c736f432d2e3ec0d6b5d2f193b93345", + "css": "artist", + "code": 59470, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M200 500V535C200 685 320 800 465 800H535A95 95 0 0 0 630 705V690A150 150 0 0 1 680 585L770 500C790 480 800 455 800 430V400L880 320C895 355 900 390 900 430A200 200 0 0 1 835 575L750 660A50 50 0 0 0 735 690V705A195 195 0 0 1 535 900H470C270 900 105 735 105 535V500A400 400 0 0 1 505 100H575C675 100 760 140 825 210L805 225 705 325 605 405V450A150 150 0 0 1 455 600H300L335 565C350 550 360 525 360 500A150 150 0 0 1 510 350H545L625 250 650 225 660 215C630 205 605 200 570 200H500A300 300 0 0 0 200 500Z", + "width": 1000 + }, + "search": [ + "artist" + ] } ] } \ No newline at end of file diff --git a/package.json b/package.json index e2bf3c11..605972af 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.43.0", + "version": "4.44.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/res/font/ffz-fontello.eot b/res/font/ffz-fontello.eot index 22ce8442..9797c95a 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 84288f8a..89ee7da8 100644 --- a/res/font/ffz-fontello.svg +++ b/res/font/ffz-fontello.svg @@ -162,6 +162,8 @@ + + diff --git a/res/font/ffz-fontello.ttf b/res/font/ffz-fontello.ttf index 9c1a5f7e..d3501979 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 c2e1efb3..5492c85c 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 442e500a..58c24309 100644 Binary files a/res/font/ffz-fontello.woff2 and b/res/font/ffz-fontello.woff2 differ diff --git a/src/experiments.json b/src/experiments.json index 1fe1346c..a7ed1cce 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -3,8 +3,8 @@ "name": "Modular Chat Line Rendering", "description": "Enable a newer, modular chat line renderer.", "groups": [ - {"value": true, "weight": 20}, - {"value": false, "weight": 80} + {"value": true, "weight": 80}, + {"value": false, "weight": 20} ] }, "api_load": { diff --git a/src/modules/chat/actions/components/edit-chat.vue b/src/modules/chat/actions/components/edit-chat.vue index dae26f97..da13a63c 100644 --- a/src/modules/chat/actions/components/edit-chat.vue +++ b/src/modules/chat/actions/components/edit-chat.vue @@ -1,42 +1,68 @@ diff --git a/src/modules/chat/actions/types.jsx b/src/modules/chat/actions/types.jsx index 949cfb08..42e355d9 100644 --- a/src/modules/chat/actions/types.jsx +++ b/src/modules/chat/actions/types.jsx @@ -238,7 +238,10 @@ export const open_url = { editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-url.vue'), title: 'Open URL', - description: '{options.url}', + description(data) { + return data.options.url; + }, + description_i18n: null, can_self: true, @@ -287,7 +290,20 @@ export const chat = { }, title: 'Chat Command', - description: '{options.command}', + description(data) { + if ( data.options.paste ) + return this.t('chat.actions.chat.desc.paste', 'Paste into chat: {cmd}', {cmd: data.options.command}) + + const target = data.options.target ?? ''; + + return this.t('chat.actions.chat.desc.target', 'Send in {target}: {cmd}', { + cmd: data.options.command, + target: /^\s*$/.test(target) + ? this.t('chat.actions.chat.desc.current', 'current channel') + : target + }); + }, + description_i18n: null, can_self: true, @@ -295,10 +311,15 @@ export const chat = { tooltip(data) { const msg = this.replaceVariables(data.options.command, data); + let target = this.replaceVariables(data.options.target ?? '', data); + if ( /^\s*$/.test(target) ) + target = null; return [ (
{ // eslint-disable-line react/jsx-key - this.i18n.t('chat.actions.chat', 'Chat Command') + target + ? this.i18n.t('chat.actions.chat.with-target', 'Chat Command in Channel: {target}', {target}) + : this.i18n.t('chat.actions.chat', 'Chat Command') }
), (
{ // eslint-disable-line react/jsx-key msg @@ -308,10 +329,14 @@ export const chat = { click(event, data) { const msg = this.replaceVariables(data.options.command, data); + let target = this.replaceVariables(data.options.target ?? '', data); + if ( data.options.paste || /^\s*$/.test(target) ) + target = data.room.login; + if ( data.options.paste ) - this.pasteMessage(data.room.login, msg); + this.pasteMessage(target, msg); else - this.sendMessage(data.room.login, msg); + this.sendMessage(target, msg); } } diff --git a/src/modules/chat/emote_info.gql b/src/modules/chat/emote_info.gql index 10b5232e..27f19241 100644 --- a/src/modules/chat/emote_info.gql +++ b/src/modules/chat/emote_info.gql @@ -1,6 +1,11 @@ query FFZ_GetEmoteInfo($id: ID!) { emote(id: $id) { id + artist { + id + login + displayName + } owner { id login diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 689fcdda..aa854f7f 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -23,10 +23,10 @@ const Flags = make_enum_flags( 'FlipX', 'FlipY', 'GrowX', - 'GrowY', - 'ShrinkX', - 'ShrinkY', - 'Rotate45', + 'Slide', + 'Appear', + 'Leave', + 'Rotate', 'Rotate90', 'Greyscale', 'Sepia', @@ -42,6 +42,70 @@ export const MODIFIER_FLAGS = Flags; export const MODIFIER_KEYS = Object.values(MODIFIER_FLAGS).filter(x => typeof x === 'number'); +const APPEAR_FRAMES = [ + [0, -18, 0, 0], + [19.99, -18, 0, 0], + [20, -18, 0.1, 0], + [25, -16, 0.2, 0.6], + [30, -14, 0.3, -4], + [35, -12, 0.4, 0.6], + [40, -10, 0.5, -4], + [45, -8, 0.6, 2], + [50, -6, 0.7, -3], + [55, -4, 0.8, 2], + [60, -2, 0.9, -3], + [65, 0, 1, 0], + [100, 0, 1, 0] +]; + +const LEAVE_FRAMES = [ + [0, 0, 1, 0], + [39.99, 1, 1, 0], + [40, 0, -.9, .9, -3], + [45, -2, -.8, .8, 2], + [50, -4, -.7, .7, -3], + [55, -6, -.6, .6, 2], + [60, -8, -.5, .5, -4], + [65, -10, -.4, .4, .6], + [70, -12, -.3, .3, -4], + [75, -14, -.2, .2, .6], + [80, 16, -.1, .1, 0], + [85, -18, -0.01, 0, 0], + [100, -18, -0.01, 0, 0] +]; + + +function appearLeaveToKeyframes(source, multi = 1, offset = 0, has_var = false) { + const out = []; + + for(const line of source) { + const pct = (line[0] * multi) + offset; + + let vr, tx, scale, ty; + vr = has_var ? `var(--ffz-effect-transforms) ` : ''; + tx = line[1] === 0 ? '' : `translateX(${line[1]}px) `; + + if ( line.length === 4 ) { + scale = `scale(${line[2]})`; + ty = line[3] === 0 ? '' : ` translateY(${line[3]}px)`; + + } else { + const sx = line[2], + sy = line[3]; + + scale = `scale(${sx}, ${sy})`; + + ty = line[4] === 0 ? '' : ` translateY(${line[4]}px)`; + } + + out.push(`\t${pct}% { transform:${vr}${tx}${scale}${ty}; }`); + } + + return out.join('\n'); +} + + + const EFFECT_STYLES = [ { setting: 'FlipX', @@ -66,24 +130,87 @@ const EFFECT_STYLES = [ title: 'Stretch Horizontal' }, { - setting: 'ShrinkY', - flags: Flags.ShrinkY, - title: 'Squish Vertical' + setting: 'Slide', + flags: Flags.Slide, + //not_flags: Flags.Rotate, + title: 'Slide Animation', + as_background: true, + animation: 'ffz-effect-slide var(--ffz-speed-x) linear infinite', + raw: `@keyframes ffz-effect-slide { +0% { background-position-x: 0; } +100% { background-position-x: calc(-1 * var(--ffz-width)); } +}` }, { - setting: 'GrowY', - flags: Flags.GrowY, - title: 'Stretch Vertical' + setting: 'Appear', + flags: Flags.Appear, + not_flags: Flags.Leave, + title: 'Appear Animation', + animation: 'ffz-effect-appear 3s infinite linear', + animationTransform: 'ffz-effect-appear-transform 3s linear infinite', + raw: `@keyframes ffz-effect-appear { +${appearLeaveToKeyframes(APPEAR_FRAMES)} +} +@keyframes ffz-effect-appear-transform { +${appearLeaveToKeyframes(APPEAR_FRAMES, 1, 0, true)} +}` + }, + { + setting: 'Leave', + flags: Flags.Leave, + not_flags: Flags.Appear, + title: 'Leave Animation', + animation: 'ffz-effect-leave 3s infinite linear', + animationTransform: 'ffz-effect-leave-transform 3s infinite linear', + raw: `@keyframes ffz-effect-leave { +${appearLeaveToKeyframes(LEAVE_FRAMES)} +} +@keyframes ffz-effect-leave-transform { +${appearLeaveToKeyframes(LEAVE_FRAMES, 1, 0, true)} +}` + }, + { + setting: [ + 'Appear', + 'Leave' + ], + flags: Flags.Appear | Flags.Leave, + animation: 'ffz-effect-in-out 6s infinite linear', + animationTransform: 'ffz-effect-in-out-transform 6s linear infinite', + raw: `@keyframes ffz-effect-in-out { +${appearLeaveToKeyframes(APPEAR_FRAMES, 0.5, 0)} +${appearLeaveToKeyframes(LEAVE_FRAMES, 0.5, 50)} +} +@keyframes ffz-effect-in-out-transform { +${appearLeaveToKeyframes(APPEAR_FRAMES, 0.5, 0, true)} +${appearLeaveToKeyframes(LEAVE_FRAMES, 0.5, 50, true)} +}` + }, + { + setting: 'Rotate', + flags: Flags.Rotate, + not_flags: Flags.Slide, + title: 'Rotate Animation', + no_wide: true, + animation: 'ffz-effect-rotate 1.5s infinite linear', + animationTransform: 'ffz-effect-rotate-transform 1.5s infinite linear', + raw: `@keyframes ffz-effect-rotate { +0% { transform: rotate(0deg); } +100% { transform: rotate(360deg); } +} +@keyframes ffz-effect-rotate-transform { +0% { transform: var(--ffz-effect-transforms) rotate(0deg); } +100% { transform: var(--ffz-effect-transforms) rotate(360deg); } +}` }, /*{ - setting: 'Rotate45', - flags: MODIFIER_FLAGS.Rotate45, - title: 'Rotate 45 Degrees' - }, - { - setting: 'Rotate90', - flags: MODIFIER_FLAGS.Rotate90, - title: 'Rotate 90 Degrees' + setting: [ + 'Slide', + 'Rotate' + ], + flags: Flags.Rotate | Flags.Slide, + // Sync up the speed for slide and rotate if both are applied. + animation: 'ffz-effect-slide calc(1.5 * var(--ffz-speed-x)) linear infinite' }, { setting: 'Greyscale', @@ -242,7 +369,7 @@ const EFFECT_STYLES = [ function generateBaseFilterCss() { const out = [ - `.modified-emote[data-effects] > img { + `.modified-emote[data-effects] > .chat-line__message--emote { --ffz-effect-filters: none; --ffz-effect-transforms: initial; --ffz-effect-animations: initial; @@ -314,6 +441,7 @@ export default class Emotes extends Module { this.twitch_inventory_sets = new Set; //(EXTRA_INVENTORY); this.__twitch_emote_to_set = {}; this.__twitch_set_to_channel = {}; + this.__twitch_emote_to_artist = {}; // Bulk data structure for collections applied to a lot of users. // This lets us avoid allocating lots of individual user @@ -548,6 +676,9 @@ export default class Emotes extends Module { if ( (flags & input.flags) !== input.flags ) continue; + if ( input.not_flags && (flags & input.not_flags) === input.not_flags ) + continue; + if ( input.animation ) animations.push(input); @@ -584,7 +715,7 @@ export default class Emotes extends Module { if ( ! filter && ! transform && ! animation ) return null; - return `.modified-emote[data-effects="${flags}"] > img {${filter ? ` + return `.modified-emote[data-effects="${flags}"] > .chat-line__message--emote {${filter ? ` --ffz-effect-filters: ${filter}; filter: var(--ffz-effect-filters);` : ''}${transformOrigin ? ` transform-origin: ${transformOrigin};` : ''}${transform ? ` @@ -602,6 +733,9 @@ export default class Emotes extends Module { this.effects_enabled = {}; this.activeEffectStyles = []; + this.activeAsBackgroundMask = 0; + this.activeNoWideMask = 0; + for(const input of EFFECT_STYLES) { if ( input.setting && ! Array.isArray(input.setting) ) this.effects_enabled[input.setting] = this.parent.context.get(`chat.effects.${input.setting}`); @@ -619,8 +753,14 @@ export default class Emotes extends Module { } else if ( input.setting ) enabled = this.effects_enabled[input.setting]; - if ( enabled ) + if ( enabled ) { this.activeEffectStyles.push(input); + + if ( input.as_background ) + this.activeAsBackgroundMask = this.activeAsBackgroundMask | input.flags; + if ( input.no_wide ) + this.activeNoWideMask = this.activeNoWideMask | input.flags; + } } this.effect_style.clear(); @@ -820,10 +960,14 @@ export default class Emotes extends Module { this.settings.provider.set(key, favs); } - handleClick(event) { + handleClick(event, favorite_only = false) { const target = event.target, ds = target && target.dataset; + /*const modified = target.closest('.modified-emote'); + if ( modified && modified !== target ) + return;*/ + if ( ! ds ) return; @@ -928,9 +1072,14 @@ export default class Emotes extends Module { return true; } + if ( favorite_only ) + return false; + const evt = new FFZEvent({ provider, id: ds.id, + set: ds.set, + name: ds.name || target.alt, source: event }); @@ -1504,6 +1653,18 @@ export default class Emotes extends Module { } } + // Check to see if this emote applies any effects with as_background. + /*let as_background = false; + if ( emote.modifier_flags ) { + for(const input of EFFECT_STYLES) + if ( (emote.modifier_flags & input.flags) === input.flags ) { + if ( input.as_background ) { + as_background = true; + break; + } + } + }*/ + emote.token = { type: 'emote', id: emote.id, @@ -1524,7 +1685,8 @@ export default class Emotes extends Module { length: emote.name.length, height: emote.height, width: emote.width, - source_modifier_flags: emote.modifier_flags ?? 0 + source_modifier_flags: emote.modifier_flags ?? 0, + //effect_bg: as_background }; if ( has(MODIFIERS, emote.id) ) @@ -1746,7 +1908,7 @@ export default class Emotes extends Module { } if ( emote.modifier && emote.mask?.[1] ) { - output = (output || '') + `.modified-emote[data-modifiers~="${emote.id}"] > img { + output = (output || '') + `.modified-emote[data-modifiers~="${emote.id}"] > .chat-line__message--emote { -webkit-mask-image: url("${emote.mask[1]}"); -webkit-mask-position: center center; }` @@ -1790,9 +1952,10 @@ export default class Emotes extends Module { this.__twitch_set_to_channel[set_id] = channel; } - _getTwitchEmoteSet(emote_id) { + _getTwitchEmoteSet(emote_id, need_artist = false) { const tes = this.__twitch_emote_to_set, - tsc = this.__twitch_set_to_channel; + tsc = this.__twitch_set_to_channel, + tsa = this.__twitch_emote_to_artist; if ( typeof emote_id === 'number' ) { if ( isNaN(emote_id) || ! isFinite(emote_id) ) @@ -1801,7 +1964,7 @@ export default class Emotes extends Module { emote_id = `${emote_id}`; } - if ( has(tes, emote_id) ) { + if ( has(tes, emote_id) && (! need_artist || has(tsa, emote_id)) ) { const val = tes[emote_id]; if ( Array.isArray(val) ) return new Promise(s => val.push(s)); @@ -1829,6 +1992,10 @@ export default class Emotes extends Module { if ( emote ) { set_id = emote.setID; + if ( emote.id && ! has(tsa, emote.id) ) { + tsa[emote.id] = emote.artist; + } + if ( set_id && ! has(tsc, set_id) ) { const type = determineEmoteType(emote); @@ -1860,6 +2027,28 @@ export default class Emotes extends Module { return promise; } + _getTwitchEmoteArtist(emote_id) { + const tsa = this.__twitch_emote_to_artist; + + if ( has(tsa, emote_id) ) + return Promise.resolve(tsa[emote_id]); + + return this._getTwitchEmoteSet(emote_id, true) + .then(() => tsa[emote_id]) + .catch(() => { + tsa[emote_id] = null; + return null; + }); + } + + getTwitchEmoteArtist(emote_id, callback) { + const promise = this._getTwitchEmoteArtist(emote_id); + if ( callback ) + promise.then(callback); + else + return promise; + } + _getTwitchSetChannel(set_id) { const tsc = this.__twitch_set_to_channel; diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index e202094d..40ec9d16 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -1864,7 +1864,7 @@ export default class Chat extends Module { className: 'chat-author__intl-login' }, ` (${user.login})`)); - return [out]; + return out; } diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 86083dc2..d742ebbb 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -7,17 +7,16 @@ import {sanitize, createElement} from 'utilities/dom'; import {has, getTwitchEmoteURL, split_chars, getTwitchEmoteSrcSet} from 'utilities/object'; -import {EmoteTypes, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants'; +import {EmoteTypes, REPLACEMENT_BASE, REPLACEMENTS, WEIRD_EMOTE_SIZES} 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; + SLIDE_X = MODIFIER_FLAGS.Slide, + STRETCH_X = MODIFIER_FLAGS.GrowX; + //SHRINK_Y = MODIFIER_FLAGS.ShrinkY, + //STRETCH_Y = MODIFIER_FLAGS.GrowY, const EMOTE_CLASS = 'chat-image chat-line__message--emote', @@ -1227,7 +1226,10 @@ export const AddonEmotes = { effects = token.modifier_flags, is_big = (token.big && ! token.can_big && token.height); - if ( effects || ml ) { + let as_bg = (this.emotes.activeAsBackgroundMask & effects) !== 0; + let no_wide = (this.emotes.activeNoWideMask & effects) !== 0; + + if ( no_wide || effects || ml ) { // We need to calculate the size of the emote and the biggest // modifier so that everything can be nicely centered. if ( token.provider === 'emoji' ) { @@ -1243,7 +1245,7 @@ export const AddonEmotes = { height: size }; } else { - const factor = big ? 2 : 1; + const factor = token.big ? 2 : 1; style = { width: token.width * factor, height: token.height * factor @@ -1255,6 +1257,9 @@ export const AddonEmotes = { } for(const mod of mods) { + if ( mod.effect_bg ) + as_bg = true; + if ( ! mod.hidden && mod.set !== 'info' ) { const factor = mod.big ? 2 : 1, width = mod.width * factor, @@ -1274,27 +1279,71 @@ export const AddonEmotes = { style.width *= 0.5; if ( (effects & STRETCH_X) === STRETCH_X ) style.width *= 2; - if ( (effects & SHRINK_Y) === SHRINK_Y ) + /*if ( (effects & SHRINK_Y) === SHRINK_Y ) style.height *= 0.5; if ( (effects & STRETCH_Y) === STRETCH_Y ) - style.height *= 2; + style.height *= 2;*/ - style.width = Math.min(style.width, big ? 256 : 128); - style.height = Math.min(style.height, big ? 80 : 40); - - if ( style.width > outerStyle.width ) - outerStyle.width = style.width; - if ( style.height > outerStyle.height ) - outerStyle.height = style.height; + style.width = Math.min(style.width, token.big ? 256 : 128); + style.height = Math.min(style.height, token.big ? 80 : 40); } + if ( no_wide ) { + const limit = token.big ? 64 : 32; + if ( style.width > limit ) { + const factor = limit / style.width; + style.width *= factor; + style.height *= factor; + } + } + + if ( style.width > outerStyle.width ) + outerStyle.width = style.width; + if ( style.height > outerStyle.height ) + outerStyle.height = style.height; + if ( style.width !== outerStyle.width ) style.marginLeft = (outerStyle.width - style.width) / 2; if ( style.height !== outerStyle.height ) style.marginTop = (outerStyle.height - style.height) / 2; + + if ( effects ) { + if ( (effects & SLIDE_X) === SLIDE_X ) { + style['--ffz-width'] = `${style.width}px`; + style['--ffz-speed-x'] = `${0.5 * (style.width / (token.big ? 64 : 32))}s`; + } + } } - const emote = ( x.id).join(' ') : null} + data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null} + onClick={this.emotes.handleClick} + >
{ token.text }
); + } + + else + emote = ( x.id).join(' ') : null} data-effects={effects ? effects : undefined} - onClick={this.emotes.handleClick} + //onClick={this.emotes.handleClick} > {emote} {mods.map(t => { - if ( (t.source_modifier_flags & 1) === 1 || t.set === 'info') + if (t.set === 'info') return null; - return - {this.tokenizers.emote.render.call(this, t, createElement, true)} - + if ((t.source_modifier_flags & 1) === 1 && t.text) + return null; + // This is currently weird and breaks copy/paste + // so since it doesn't *fix* copy/paste just leave + // it out for now. + //return
{` ${t.text}`}
; + return {this.tokenizers.emote.render.call(this, t, createElement, true)} })} ); }, @@ -1350,7 +1403,7 @@ export const AddonEmotes = { provider = ds.provider, modifiers = ds.modifierInfo; - let name, preview, source, owner, mods, fav_source, emote_id, + let name, preview, source, artist, owner, mods, fav_source, emote_id, plain_name = false; const hide_source = ds.noSource === 'true'; @@ -1377,11 +1430,15 @@ export const AddonEmotes = { if ( provider === 'twitch' ) { emote_id = ds.id; const set_id = hide_source ? null : await this.emotes.getTwitchEmoteSet(emote_id), - emote_set = set_id != null && await this.emotes.getTwitchSetChannel(set_id); + emote_set = set_id != null && await this.emotes.getTwitchSetChannel(set_id), + raw_artist = hide_source ? null : await this.emotes.getTwitchEmoteArtist(emote_id); preview = `${getTwitchEmoteURL(ds.id, 4, true, true)}?_=preview`; fav_source = 'twitch'; + if ( raw_artist ) + artist = raw_artist.displayName || raw_artist.login; + if ( emote_set ) { const type = emote_set.type; if ( type === EmoteTypes.Global ) { @@ -1435,6 +1492,9 @@ export const AddonEmotes = { if ( emote ) { emote_id = emote.id; + if ( emote.artist ) + artist = emote.artist.display_name || emote.artist.name; + if ( emote.owner ) owner = this.i18n.t( 'emote.owner', 'By: {owner}', @@ -1467,6 +1527,15 @@ export const AddonEmotes = { height: (target.height ?? 28) * 2 }; + let outerStyle = { + width: style.width, + height: style.height + }; + + + let as_bg = (this.emotes.activeAsBackgroundMask & effects) !== 0; + let no_wide = (this.emotes.activeNoWideMask & effects) !== 0; + let changed = false; if ( (effects & SHRINK_X) === SHRINK_X ) { @@ -1477,14 +1546,14 @@ export const AddonEmotes = { style.width *= 2; changed = true; } - if ( (effects & SHRINK_Y) === SHRINK_Y ) { + /*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 ) @@ -1493,9 +1562,41 @@ export const AddonEmotes = { style.height = 160; } + if ( no_wide ) { + const limit = 64; + if ( style.width > limit ) { + const factor = limit / style.width; + style.width *= factor; + style.height *= factor; + } + } + + if ( style.width > outerStyle.width ) + outerStyle.width = style.width; + if ( style.height > outerStyle.height ) + outerStyle.height = style.height; + + if ( style.width !== outerStyle.width ) + style.marginLeft = (outerStyle.width - style.width) / 2; + if ( style.height !== outerStyle.height ) + style.marginTop = (outerStyle.height - style.height) / 2; + + if ( (effects & SLIDE_X) === SLIDE_X ) { + style['--ffz-width'] = `${style.width}px`; + style['--ffz-speed-x'] = `${0.5 * style.width / 64}s`; + } + style.width = `${style.width}px`; style.height = `${style.height}px`; + outerStyle.width = `${outerStyle.width}px`; + outerStyle.height = `${outerStyle.height}px`; + + if ( as_bg ) { + style.backgroundImage = `url("${target.src}")`; + style.backgroundSize = '100%'; + } + // Whip up a special preview. preview = (
- + {as_bg + ?
+ : + }
); } @@ -1572,6 +1679,13 @@ export const AddonEmotes = { {owner}
), + artist && this.context.get('tooltip.emote-sources') && (
+ {this.i18n.t( + 'emote.artist', 'Artist: {artist}', + {artist} + )} +
), + ds.sellout && (
{ds.sellout}
), mods && (
{mods}
), @@ -1901,6 +2015,11 @@ export const TwitchEmotes = { } } + const sizes = WEIRD_EMOTE_SIZES[e_id]; + + const width = sizes ? sizes[0] : 28, + height = sizes ? sizes[1] : 28; + out.push({ type: 'emote', id: e_id, @@ -1916,8 +2035,8 @@ export const TwitchEmotes = { anim, big, can_big, - width: 28, - height: 28, // Not always accurate but close enough. + width, + height, text: text.slice(e_start - t_start, e_end - t_start).join(''), modifiers: [], modifier_flags: 0 diff --git a/src/modules/emote_card/components/card.vue b/src/modules/emote_card/components/card.vue new file mode 100644 index 00000000..cc694842 --- /dev/null +++ b/src/modules/emote_card/components/card.vue @@ -0,0 +1,338 @@ + + + \ No newline at end of file diff --git a/src/modules/emote_card/components/report-form.vue b/src/modules/emote_card/components/report-form.vue new file mode 100644 index 00000000..2c841b28 --- /dev/null +++ b/src/modules/emote_card/components/report-form.vue @@ -0,0 +1,257 @@ +