1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-03 08:28:31 +00:00

Add favorite emotes. Add emote menu sorting. Use a CSS variable for border colors for chat lines. Use fixed emote images for the emote menu.

This commit is contained in:
SirStendec 2018-04-09 19:57:05 -04:00
parent a01b21e9d9
commit 1841ab156c
13 changed files with 450 additions and 99 deletions

View file

@ -1,3 +1,11 @@
<div class="list-header">4.0.0-beta2.3<span>@a07fb33207e6659acc9f</span> <time datetime="2018-04-09">(2018-04-09)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Favorite emotes by Ctrl-Clicking them! ⌘-Click for Mac users.</li>
<li>Added: Open information pages for emotes by Shift-Clicking them.</li>
<li>Added: Sorting options for the emote menu.</li>
<li>Changed: Use cleaned up versions of certain low quality global Twitch emotes in the emote menu.</li>
</ul>
<div class="list-header">4.0.0-beta2.2<span>@201497e9898b452ba698</span> <time datetime="2018-04-08">(2018-04-08)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Support for the old Featured Channels feature.</li>

View file

@ -6,6 +6,7 @@ import Module from 'utilities/module';
import {DEBUG} from 'utilities/constants';
import SettingsManager from './settings/index';
//import ExperimentManager from './experiments';
import {TranslationManager} from './i18n';
import SocketClient from './socket';
import Site from 'site';
@ -34,6 +35,7 @@ class FrankerFaceZ extends Module {
// ========================================================================
this.inject('settings', SettingsManager);
//this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
this.inject('socket', SocketClient);
this.inject('site', Site);
@ -95,7 +97,7 @@ class FrankerFaceZ extends Module {
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: 4, minor: 0, revision: 0, extra: '-beta2.2',
major: 4, minor: 0, revision: 0, extra: '-beta2.3',
build: __webpack_hash__,
toString: () =>
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`

View file

@ -7,8 +7,9 @@
import Module from 'utilities/module';
import {ManagedStyle} from 'utilities/dom';
import {has, timeout, SourcedSet} from 'utilities/object';
import {CLIENT_ID, API_SERVER} from 'utilities/constants';
import {CLIENT_ID, API_SERVER, IS_OSX} from 'utilities/constants';
const MOD_KEY = IS_OSX ? 'metaKey' : 'ctrlKey';
const EXTRA_INVENTORY = [33563];
@ -88,6 +89,10 @@ export default class Emotes extends Module {
component: 'setting-check-box'
}
});
// Because this may be used elsewhere.
this.handleClick = this.handleClick.bind(this);
}
onEnable() {
@ -142,6 +147,116 @@ export default class Emotes extends Module {
}
// ========================================================================
// Favorite Checking
// ========================================================================
toggleFavorite(source, id, value = null) {
const key = `favorite-emotes.${source}`,
p = this.settings.provider,
favorites = p.get(key) || [],
idx = favorites.indexOf(id);
if ( value === null )
value = idx === -1;
if ( value && idx === -1 )
favorites.push(id);
else if ( ! value && idx !== -1 )
favorites.splice(idx, 1);
else
return;
if ( favorites.length )
p.set(key, favorites);
else
p.delete(key);
this.emit(':change-favorite', source, id, value);
}
isFavorite(source, id) {
const favorites = this.settings.provider.get(`favorite-emotes.${source}`);
return favorites && favorites.includes(id);
}
getFavorites(source) {
return this.settings.provider.get(`favorite-emotes.${source}`) || [];
}
handleClick(event) {
const target = event.target,
ds = target && target.dataset;
if ( ! ds )
return;
const provider = ds.provider;
if ( event[MOD_KEY] ) {
// Favoriting Emotes
let source, id;
if ( provider === 'twitch' ) {
source = 'twitch';
id = parseInt(ds.id, 10);
} else if ( provider === 'ffz' ) {
const emote_set = this.emote_sets[ds.set],
emote = emote_set && emote_set.emotes[ds.id];
if ( ! emote )
return;
source = emote_set.source || 'ffz';
id = emote.id;
} else
return;
this.toggleFavorite(source, id);
const tt = target._ffz_tooltip$0;
if ( tt && tt.visible ) {
tt.hide();
setTimeout(() => document.contains(target) && tt.show(), 0);
}
return true;
}
if ( event.shiftKey && this.parent.context.get('chat.click-emotes') ) {
let url;
if ( provider === 'twitch' )
url = `https://twitchemotes.com/emotes/${ds.id}`;
else if ( provider === 'ffz' ) {
const emote_set = this.emote_sets[ds.set],
emote = emote_set && emote_set.emotes[ds.id];
if ( ! emote )
return;
if ( emote.click_url )
url = emote.click_url;
else if ( ! emote_set.source )
url = `https://www.frankerfacez.com/emoticons/${emote.id}`;
}
if ( url ) {
const win = window.open();
win.opener = null;
win.location = url;
}
return true;
}
}
// ========================================================================
// Access
// ========================================================================
@ -413,7 +528,14 @@ export default class Emotes extends Module {
data.id = set_id;
data.emoticons = undefined;
const bad_emotes = [];
for(const emote of ems) {
if ( ! emote.id || ! emote.name || ! emote.urls ) {
bad_emotes.push(emote);
continue;
}
emote.set_id = set_id;
emote.srcSet = `${emote.urls[1]} 1x`;
if ( emote.urls[2] )
@ -443,6 +565,9 @@ export default class Emotes extends Module {
new_ems[emote.id] = emote;
}
if ( bad_emotes.length )
this.log.warn(`Bad Emote Data for Set #${set_id}`, bad_emotes);
data.count = count;
if ( this.style && (css.length || data.css) )

View file

@ -259,6 +259,16 @@ export default class Chat extends Module {
}
});
this.settings.add('chat.click-emotes', {
default: true,
ui: {
path: 'Chat > Behavior >> General',
title: 'Open emote information pages by Shift-Clicking them.',
component: 'setting-check-box'
}
});
this.context.on('changed:theme.is-dark', () => {
for(const room of this.iterateRooms())
room.buildBitsCSS();

View file

@ -7,27 +7,12 @@
import {sanitize, createElement} from 'utilities/dom';
import {has, split_chars} from 'utilities/object';
import {TWITCH_EMOTE_BASE, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants';
const EMOTE_CLASS = 'chat-line__message--emote',
LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g,
TWITCH_BASE = '//static-cdn.jtvnw.net/emoticons/v1/',
REPLACEMENT_BASE = '//cdn.frankerfacez.com/script/replacements/',
REPLACEMENTS = {
15: '15-JKanStyle.png',
16: '16-OptimizePrime.png',
17: '17-StoneLightning.png',
18: '18-TheRinger.png',
//19: '19-PazPazowitz.png',
//20: '20-EagleEye.png',
//21: '21-CougarHunt.png',
22: '22-RedCoat.png',
26: '26-JonCarnage.png',
//27: '27-PicoMause.png',
30: '30-BCWarrior.png',
33: '33-DansGame.png',
36: '36-PJSalt.png'
};
MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g;
// ============================================================================
@ -462,6 +447,7 @@ export const AddonEmotes = {
data-set={token.set}
data-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
onClick={this.emotes.handleClick}
/>);
if ( ! ml )
@ -472,6 +458,7 @@ export const AddonEmotes = {
data-provider={token.provider}
data-id={token.id}
data-set={token.set}
onClick={this.emotes.handleClick}
>
{emote}
{mods.map(t => <span key={t.text}>{this.tokenizers.emote.render(t, createElement)}</span>)}
@ -483,7 +470,7 @@ export const AddonEmotes = {
provider = ds.provider,
modifiers = ds.modifierInfo;
let preview, source, owner, mods;
let preview, source, owner, mods, fav_source, emote_id;
if ( modifiers && modifiers !== 'null' ) {
mods = JSON.parse(modifiers).map(([set_id, emote_id]) => {
@ -499,11 +486,12 @@ export const AddonEmotes = {
}
if ( provider === 'twitch' ) {
const emote_id = parseInt(ds.id, 10),
set_id = this.emotes.getTwitchEmoteSet(emote_id, tip.rerender),
emote_id = parseInt(ds.id, 10);
const set_id = this.emotes.getTwitchEmoteSet(emote_id, tip.rerender),
emote_set = set_id != null && this.emotes.getTwitchSetChannel(set_id, tip.rerender);
preview = `//static-cdn.jtvnw.net/emoticons/v1/${emote_id}/4.0?_=preview`;
fav_source = 'twitch';
if ( emote_set ) {
source = emote_set.c_name;
@ -525,10 +513,14 @@ export const AddonEmotes = {
const emote_set = this.emotes.emote_sets[ds.set],
emote = emote_set && emote_set.emotes[ds.id];
if ( emote_set )
if ( emote_set ) {
source = emote_set.source_line || (`${emote_set.source || 'FFZ'} ${emote_set.title || 'Global'}`);
fav_source = emote_set.source || 'ffz';
}
if ( emote ) {
emote_id = emote.id;
if ( emote.owner )
owner = this.i18n.t(
'emote.owner', 'By: %{owner}',
@ -539,9 +531,12 @@ export const AddonEmotes = {
else if ( emote.urls[2] )
preview = emote.urls[2];
}
}
} else
return;
const name = ds.name || target.alt,
favorite = fav_source && this.emotes.isFavorite(fav_source, emote_id),
hide_source = ds.noSource === 'true';
return [
@ -563,7 +558,9 @@ export const AddonEmotes = {
ds.sellout && (<div class="tw-mg-t-05 tw-border-t tw-pd-t-05">{ds.sellout}</div>),
mods && (<div class="tw-pd-t-1">{mods}</div>)
mods && (<div class="tw-pd-t-1">{mods}</div>),
favorite && (<figure class="ffz--favorite ffz-i-star" />)
];
},
@ -729,8 +726,8 @@ export const TwitchEmotes = {
srcSet = '';
} else {
src = `${TWITCH_BASE}${e_id}/1.0`;
srcSet = `${TWITCH_BASE}${e_id}/1.0 1x, ${TWITCH_BASE}${e_id}/2.0 2x`;
src = `${TWITCH_EMOTE_BASE}${e_id}/1.0`;
srcSet = `${TWITCH_EMOTE_BASE}${e_id}/1.0 1x, ${TWITCH_EMOTE_BASE}${e_id}/2.0 2x`;
}
out.push({

View file

@ -5,7 +5,7 @@
// ============================================================================
import {has, get, once, set_equals} from 'utilities/object';
import {KNOWN_CODES, TWITCH_EMOTE_BASE} from 'utilities/constants';
import {IS_OSX, KNOWN_CODES, TWITCH_EMOTE_BASE, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants';
import Twilight from 'site';
import Module from 'utilities/module';
@ -24,6 +24,34 @@ function maybe_date(val) {
}
const EMOTE_SORTERS = [
function id_asc(a, b) {
return a.id - b.id;
},
function id_desc(a, b) {
return b.id - a.id;
},
function name_asc(a, b) {
const a_n = a.name.toLowerCase(),
b_n = b.name.toLowerCase();
if ( a_n < b_n ) return -1;
if ( a_n > b_n ) return 1;
return a.id - b.id;
},
function name_desc(a, b) {
const a_n = a.name.toLowerCase(),
b_n = b.name.toLowerCase();
if ( a_n > b_n ) return -1;
if ( a_n < b_n ) return 1;
return b.id - a.id;
}
];
function sort_sets(a, b) {
const a_sk = a.sort_key,
b_sk = b.sort_key;
@ -123,13 +151,39 @@ export default class EmoteMenu extends Module {
title: 'Default Tab',
component: 'setting-select-box',
data: [
{value: 'fav', title: 'Favorites'},
{value: 'channel', title: 'Channel'},
{value: 'all', title: 'All'}
{value: 'all', title: 'My Emotes'}
]
}
});
this.settings.add('chat.emote-menu.sort-emotes', {
default: 0,
ui: {
path: 'Chat > Emote Menu >> Sorting',
title: 'Sort Emotes By',
component: 'setting-select-box',
data: [
{value: 0, title: 'Order Added (ID), Ascending'},
{value: 1, title: 'Order Added (ID), Descending'},
{value: 2, title: 'Name, Ascending'},
{value: 3, title: 'Name, Descending'}
]
}
});
this.settings.add('chat.emote-menu.sort-tiers-last', {
default: true,
ui: {
path: 'Chat > Emote Menu >> Sorting',
title: 'List emotes from higher sub tiers last.',
component: 'setting-check-box'
}
});
this.EmoteMenu = this.fine.define(
'chat-emote-menu',
n => n.subscriptionProductHasEmotes,
@ -147,18 +201,24 @@ export default class EmoteMenu extends Module {
this.on('chat.emotes:update-default-sets', this.maybeUpdate, this);
this.on('chat.emotes:update-user-sets', this.maybeUpdate, this);
this.on('chat.emotes:update-room-sets', this.maybeUpdate, this);
this.on('chat.emotes:change-favorite', this.maybeUpdate, this);
this.chat.context.on('changed:chat.emote-menu.enabled', () =>
this.EmoteMenu.forceUpdate());
this.chat.context.on('changed:chat.emote-menu.show-heading', () =>
this.MenuWrapper.forceUpdate());
const fup = () => this.MenuWrapper.forceUpdate();
const rebuild = () => {
for(const inst of this.MenuWrapper.instances)
inst.componentWillReceiveProps(inst.props);
}
this.chat.context.on('changed:chat.emote-menu.show-search', () =>
this.MenuWrapper.forceUpdate());
this.chat.context.on('changed:chat.fix-bad-emotes', rebuild);
this.chat.context.on('changed:chat.emote-menu.sort-emotes', rebuild);
this.chat.context.on('changed:chat.emote-menu.sort-tiers-last', rebuild);
this.chat.context.on('changed:chat.emote-menu.reduced-padding', () =>
this.MenuWrapper.forceUpdate());
this.chat.context.on('changed:chat.emote-menu.show-heading', fup);
this.chat.context.on('changed:chat.emote-menu.show-search', fup);
this.chat.context.on('changed:chat.emote-menu.reduced-padding', fup);
this.chat.context.on('changed:chat.emote-menu.icon', val =>
this.css_tweaks.toggle('emote-menu', val));
@ -214,17 +274,19 @@ export default class EmoteMenu extends Module {
this.MenuEmote = class FFZMenuEmote extends React.Component {
constructor(props) {
super(props);
this.handleClick = props.onClickEmote.bind(this, props.data.name)
this.handleClick = this.handleClick.bind(this);
}
componentWillUpdate() {
this.handleClick = this.props.onClickEmote.bind(this, this.props.data.name);
handleClick(event) {
if ( ! t.emotes.handleClick(event) )
this.props.onClickEmote(this.props.data.name);
}
render() {
const data = this.props.data,
lock = this.props.lock,
locked = this.props.locked,
favorite = data.favorite,
sellout = lock ?
this.props.all_locked ?
@ -253,6 +315,7 @@ export default class EmoteMenu extends Module {
alt={data.name}
/>
</figure>
{favorite && (<figure class="ffz--favorite ffz-i-star" />)}
{locked && (<figure class="ffz-i-lock" />)}
</button>);
}
@ -307,7 +370,7 @@ export default class EmoteMenu extends Module {
const data = this.props.data,
filtered = this.props.filtered;
let show_heading = t.chat.context.get('chat.emote-menu.show-heading');
let show_heading = ! data.is_favorites && t.chat.context.get('chat.emote-menu.show-heading');
if ( show_heading === 2 )
show_heading = ! filtered;
else
@ -541,6 +604,7 @@ export default class EmoteMenu extends Module {
state.filtered_channel_sets = this.filterSets(input, state.channel_sets);
state.filtered_all_sets = this.filterSets(input, state.all_sets);
state.filtered_fav_sets = this.filterSets(input, state.fav_sets);
return state;
}
@ -587,7 +651,8 @@ export default class EmoteMenu extends Module {
data = state.set_data || {},
channel = state.channel_sets = [],
all = state.all_sets = [];
all = state.all_sets = [],
favorites = state.favorites = [];
// If we're still loading, don't set any data.
if ( props.loading || props.error || state.loading )
@ -598,10 +663,32 @@ export default class EmoteMenu extends Module {
if ( state.set_data && this.loadData(false, props, state) )
return state;
// Sorters
const sorter = EMOTE_SORTERS[t.chat.context.get('chat.emote-menu.sort-emotes')],
sort_tiers = t.chat.context.get('chat.emote-menu.sort-tiers-last'),
sort_emotes = (a,b) => {
if ( a.inventory || b.inventory )
return sorter(a,b);
if ( ! a.locked && b.locked ) return -1;
if ( a.locked && ! b.locked ) return 1;
if ( sort_tiers || a.locked || b.locked ) {
if ( a.set_id < b.set_id ) return -1;
if ( a.set_id > b.set_id ) return 1;
}
return sorter(a,b);
}
// Start with the All tab. Some data calculated for
// all is re-used for the Channel tab.
const emote_sets = props.emote_data && props.emote_data.emoteSets,
emote_map = props.emote_data && props.emote_data.emoteMap,
twitch_favorites = t.emotes.getFavorites('twitch'),
twitch_seen_favorites = new Set,
inventory = t.emotes.twitch_inventory_sets || new Set,
grouped_sets = {},
set_ids = new Set;
@ -612,6 +699,7 @@ export default class EmoteMenu extends Module {
continue;
const set_id = parseInt(emote_set.id, 10),
is_inventory = inventory.has(set_id),
set_data = data[set_id] || {},
more_data = t.emotes.getTwitchSetChannel(set_id),
image = set_data.image,
@ -637,7 +725,7 @@ export default class EmoteMenu extends Module {
key = `twitch-${chan.id}`;
else {
if ( inventory.has(set_id) ) {
if ( is_inventory ) {
title = t.i18n.t('emote-menu.inventory', 'Inventory');
key = 'twitch-inventory';
icon = 'inventory';
@ -698,21 +786,47 @@ export default class EmoteMenu extends Module {
for(const emote of emote_set.emotes) {
const id = parseInt(emote.id, 10),
base = `${TWITCH_EMOTE_BASE}${id}`,
name = KNOWN_CODES[emote.token] || emote.token;
name = KNOWN_CODES[emote.token] || emote.token,
mapped = emote_map && emote_map[name],
overridden = mapped && mapped.id != id,
replacement = REPLACEMENTS[id],
is_fav = twitch_favorites.includes(id);
emotes.push({
let src, srcSet;
if ( replacement && t.chat.context.get('chat.fix-bad-emotes') )
src = `${REPLACEMENT_BASE}${replacement}`;
else {
const base = `${TWITCH_EMOTE_BASE}${id}`;
src = `${base}/1.0`;
srcSet = `${src} 1x, ${base}/2.0 2x`
}
const em = {
provider: 'twitch',
id,
set_id,
name,
src: `${base}/1.0`,
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`
});
src,
srcSet,
overridden: overridden ? parseInt(mapped.id,10) : null,
inventory: is_inventory,
favorite: is_fav
};
emotes.push(em);
if ( is_fav && ! twitch_seen_favorites.has(id) ) {
favorites.push(em);
twitch_seen_favorites.add(id);
}
}
if ( emotes.length && ! all.includes(section) )
all.push(section);
if ( emotes.length ) {
emotes.sort(sort_emotes);
if ( ! all.includes(section) )
all.push(section);
}
}
@ -772,25 +886,36 @@ export default class EmoteMenu extends Module {
for(const emote of product.emotes) {
const id = parseInt(emote.id, 10),
base = `${TWITCH_EMOTE_BASE}${id}`,
name = KNOWN_CODES[emote.token] || emote.token;
name = KNOWN_CODES[emote.token] || emote.token,
is_fav = twitch_favorites.includes(id);
emotes.push({
const em = {
provider: 'twitch',
id,
set_id,
name,
locked,
src: `${base}/1.0`,
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`
});
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`,
favorite: is_fav
};
emotes.push(em);
if ( ! locked && is_fav && ! twitch_seen_favorites.has(id) ) {
favorites.push(em);
twitch_seen_favorites.add(id);
}
if ( lock_set )
lock_set.add(id);
}
}
if ( emotes.length )
if ( emotes.length ) {
emotes.sort(sort_emotes);
channel.push(section);
}
}
@ -798,18 +923,23 @@ export default class EmoteMenu extends Module {
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);
ffz_global = t.emotes.getGlobalSetsWithSources(me.id, me.login),
seen_favorites = {};
for(const [emote_set, provider] of ffz_room) {
const section = this.processFFZSet(emote_set, provider);
if ( section )
const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites);
if ( section ) {
section.emotes.sort(sort_emotes);
channel.push(section);
}
}
for(const [emote_set, provider] of ffz_global) {
const section = this.processFFZSet(emote_set, provider);
if ( section )
const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites);
if ( section ) {
section.emotes.sort(sort_emotes);
all.push(section);
}
}
}
@ -820,14 +950,27 @@ export default class EmoteMenu extends Module {
state.has_channel_tab = channel.length > 0;
state.fav_sets = [{
key: 'favorites',
is_favorites: true,
emotes: favorites
}];
// We use this sorter because we don't want things grouped by sets.
favorites.sort(sorter);
return state;
}
processFFZSet(emote_set, provider) { // eslint-disable-line class-methods-use-this
processFFZSet(emote_set, provider, favorites, seen_favorites) { // eslint-disable-line class-methods-use-this
if ( ! emote_set || ! emote_set.emotes )
return null;
const fav_key = emote_set.source || 'ffz',
known_favs = t.emotes.getFavorites(fav_key),
seen_favs = seen_favorites[fav_key] = seen_favorites[fav_key] || new Set;
const pdata = t.emotes.providers.get(provider),
source = pdata && pdata.name ?
(pdata.i18n_key ?
@ -856,16 +999,22 @@ export default class EmoteMenu extends Module {
for(const emote of Object.values(emote_set.emotes))
if ( ! emote.hidden ) {
const em = {
provider: 'ffz',
id: emote.id,
set_id: emote_set.id,
src: emote.urls[1],
srcSet: emote.srcSet,
name: emote.name
};
const is_fav = known_favs.includes(emote.id),
em = {
provider: 'ffz',
id: emote.id,
set_id: emote_set.id,
src: emote.urls[1],
srcSet: emote.srcSet,
name: emote.name,
favorite: is_fav
};
emotes.push(em);
if ( is_fav && ! seen_favs.has(emote.id) ) {
favorites.push(em);
seen_favs.add(emote.id);
}
}
if ( emotes.length )
@ -886,7 +1035,7 @@ export default class EmoteMenu extends Module {
renderError() {
return (<div class="tw-align-center tw-pd-1">
<div class="tw-mg-b-1">
<div class="tw-mg-5">
<div class="tw-mg-2">
<img
src="//cdn.frankerfacez.com/emoticon/26608/2"
srcSet="//cdn.frankerfacez.com/emoticon/26608/2 1x, //cdn.frankerfacez.com/emoticon/26608/4 2x"
@ -904,7 +1053,7 @@ export default class EmoteMenu extends Module {
renderEmpty() { // eslint-disable-line class-methods-use-this
return (<div class="tw-align-center tw-pd-1">
<div class="tw-mg-5">
<div class="tw-mg-2">
<img
src="//cdn.frankerfacez.com/emoticon/26608/2"
srcSet="//cdn.frankerfacez.com/emoticon/26608/2 1x, //cdn.frankerfacez.com/emoticon/26608/4 2x"
@ -912,7 +1061,9 @@ export default class EmoteMenu extends Module {
</div>
{this.state.filtered ?
t.i18n.t('emote-menu.empty-search', 'There are no matching emotes.') :
t.i18n.t('emote-menu.empty', "There's nothing here.")}
this.state.tab === 'fav' ?
t.i18n.t('emote-menu.empty-favs', "You don't have any favorite emotes. To favorite an emote, find it and %{hotkey}-Click it.", {hotkey: IS_OSX ? '⌘' : 'Ctrl'}) :
t.i18n.t('emote-menu.empty', "There's nothing here.")}
</div>)
}
@ -935,6 +1086,9 @@ export default class EmoteMenu extends Module {
tab = 'all';
switch(tab) {
case 'fav':
sets = this.state.filtered_fav_sets;
break;
case 'channel':
sets = this.state.filtered_channel_sets;
break;
@ -984,9 +1138,16 @@ export default class EmoteMenu extends Module {
</div>
</div>)}
<div class="emote-picker__tabs-container tw-flex tw-border-t tw-c-background">
{null && (<div class="ffz-tooltip emote-picker__tab tw-pd-x-1" data-tooltip-type="html" data-title="Favorites">
<div
class={`ffz-tooltip emote-picker__tab tw-pd-x-1${tab === 'fav' ? ' emote-picker__tab--active' : ''}`}
id="emote-picker__fav"
data-tab="fav"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.favorites', 'Favorites')}
onClick={this.clickTab}
>
<figure class="ffz-i-star" />
</div>)}
</div>
{this.state.has_channel_tab && <div
class={`emote-picker__tab tw-pd-x-1${tab === 'channel' ? ' emote-picker__tab--active' : ''}`}
id="emote-picker__channel"
@ -1001,7 +1162,7 @@ export default class EmoteMenu extends Module {
data-tab="all"
onClick={this.clickTab}
>
{t.i18n.t('emote-menu.all', 'All')}
{t.i18n.t('emote-menu.my-emotes', 'My Emotes')}
</div>
<div class="tw-flex-grow-1" />
{!loading && (<div

View file

@ -10,7 +10,7 @@
padding-top: calc(.5rem - 1px) !important;
border-top: 1px solid #aaa;
border-bottom-color: rgba(255,255,255,0.5);
border-bottom-color: var rgba(255,255,255,0.5);
.tw-theme--dark & {
border-top-color: #000;

View file

@ -8,12 +8,7 @@
.chat-line__raid,
.chat-line__subscribe {
padding-top: calc(.5rem - 1px) !important;
border-top: 1px solid #dad8de;
.tw-theme--dark & {
border-top-color: #2c2541;
}
border-top: 1px solid var(--ffz-border-color);
&:first-child {
border-top-color: transparent !important;

View file

@ -8,12 +8,7 @@
.chat-line__raid,
.chat-line__subscribe {
padding-bottom: calc(.5rem - 1px) !important;
border-bottom: 1px solid #dad8de;
.tw-theme--dark & {
border-bottom-color: #2c2541;
}
border-bottom: 1px solid var(--ffz-border-color);
&:last-child:nth-child(odd) {
border-bottom-color: transparent !important;

View file

@ -13,9 +13,11 @@ import THEME_CSS_URL from 'site/styles/theme.scss';
export default class ThemeEngine extends Module {
constructor(...args) {
super(...args);
this.inject('site');
this.inject('settings');
this.inject('site');
this.inject('site.css_tweaks');
this.should_enable = true;
this.settings.add('theme.dark', {
@ -40,7 +42,7 @@ export default class ThemeEngine extends Module {
process(ctx) {
return ctx.get('context.ui.theme') === 1;
},
changed: val => document.body.classList.toggle('tw-theme--dark', val)
changed: () => this.updateCSS()
});
this.settings.add('theme.tooltips-dark', {
@ -53,6 +55,18 @@ export default class ThemeEngine extends Module {
this._style = null;
}
updateCSS() {
const dark = this.settings.get('theme.is-dark'),
gray = this.settings.get('theme.dark');
document.body.classList.toggle('tw-theme--dark', dark);
document.body.classList.toggle('tw-theme--ffz', gray);
this.css_tweaks.setVariable('border-color', dark ? (gray ? '#2a2a2a' : '#2c2541') : '#dad8de');
}
toggleStyle(enable) {
if ( ! this._style ) {
if ( ! enable )
@ -74,11 +88,10 @@ export default class ThemeEngine extends Module {
updateSetting(enable) {
this.toggleStyle(enable);
document.body.classList.toggle('tw-theme--ffz', enable);
this.updateCSS();
}
onEnable() {
this.updateSetting(this.settings.get('theme.dark'));
document.body.classList.toggle('tw-theme--dark', this.settings.get('theme.is-dark'));
}
}

View file

@ -1,7 +1,5 @@
'use strict';
import {has} from 'utilities/object';
export function hue2rgb(p, q, t) {
if ( t < 0 ) t += 1;
if ( t > 1 ) t -= 1;
@ -565,7 +563,7 @@ export class ColorAdjuster {
rebuildContrast() {
this._cache = {};
this._cache = new Map;
const base = RGBAColor.fromCSS(this._base),
lum = base.luminance();
@ -606,8 +604,8 @@ export class ColorAdjuster {
if ( ! color )
return null;
if ( has(this._cache, color) )
return this._cache[color];
if ( this._cache.has(color) )
return this._cache.get(color);
let rgb = RGBAColor.fromCSS(color);
@ -650,7 +648,8 @@ export class ColorAdjuster {
rgb = rgb.brighten(-1);
}
const out = this._cache[color] = rgb.toHex();
const out = rgb.toHex();
this._cache.set(color, out);
return out;
}
}

View file

@ -33,6 +33,25 @@ export const KNOWN_CODES = {
'Gr(a|e)yFace': 'GrayFace'
};
export const REPLACEMENT_BASE = '//cdn.frankerfacez.com/script/replacements/';
export const REPLACEMENTS = {
15: '15-JKanStyle.png',
16: '16-OptimizePrime.png',
17: '17-StoneLightning.png',
18: '18-TheRinger.png',
//19: '19-PazPazowitz.png',
//20: '20-EagleEye.png',
//21: '21-CougarHunt.png',
22: '22-RedCoat.png',
26: '26-JonCarnage.png',
//27: '27-PicoMause.png',
30: '30-BCWarrior.png',
33: '33-DansGame.png',
36: '36-PJSalt.png'
};
export const WS_CLUSTERS = {
Production: [
['wss://catbag.frankerfacez.com/', 0.25],

View file

@ -29,6 +29,33 @@
}
.ffz-i-lock {
position: absolute;
bottom: 0; right: 0;
border-radius: .2rem;
font-size: 1rem;
background-color: rgba(0,0,0,0.5);
color: #fff;
}
.ffz--favorite {
position: absolute;
bottom: 0; right: 0;
font-size: 1rem;
border-radius: .5rem;
color: gold;
background-color: rgba(0,0,0,0.5);
.ffz__tooltip & {
bottom: .1rem; right: .1rem;
padding: .2rem 0;
}
}
.modified-emote {
~ .chat-line__message--emote {
position: relative;