diff --git a/.gitignore b/.gitignore
index 0fb636f2..04755b58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules
npm-debug.log
build
-Extension Building
\ No newline at end of file
+Extension Building
+.idea
\ No newline at end of file
diff --git a/script.js b/script.js
index 8d5221ad..41edb476 100644
--- a/script.js
+++ b/script.js
@@ -190,7 +190,7 @@ FFZ.prototype._legacy_parse_donors = function(data) {
this.log("Added donor badge to " + utils.number_commas(count) + " users.");
}
-},{"./constants":3,"./utils":24}],2:[function(require,module,exports){
+},{"./constants":3,"./utils":26}],2:[function(require,module,exports){
var FFZ = window.FrankerFaceZ;
@@ -277,16 +277,33 @@ module.exports = {
CHAT_BUTTON: '' + SVGPATH + ' ',
GEAR: ' ',
- HEART: ' '
+ HEART: ' ',
+ EMOTE: ' '
}
},{}],4:[function(require,module,exports){
var FFZ = window.FrankerFaceZ;
// -----------------------
-// Developer Mode Command
+// Developer Mode
// -----------------------
+FFZ.settings_info.developer_mode = {
+ type: "boolean",
+ value: false,
+ storage_key: "ffzDebugMode",
+
+ visible: function() { return this.settings.developer_mode || (Date.now() - parseInt(localStorage.ffzLastDevMode || "0")) < 604800000; },
+ category: "Debugging",
+ name: "Developer Mode",
+ help: "Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",
+
+ on_update: function() {
+ localStorage.ffzLastDevMode = Date.now();
+ }
+ };
+
+
FFZ.ffz_commands.developer_mode = function(room, args) {
var enabled, args = args && args.length ? args[0].toLowerCase() : null;
if ( args == "y" || args == "yes" || args == "true" || args == "on" )
@@ -295,9 +312,9 @@ FFZ.ffz_commands.developer_mode = function(room, args) {
enabled = false;
if ( enabled === undefined )
- return "Developer Mode is currently " + (localStorage.ffzDebugMode == "true" ? "enabled." : "disabled.");
+ return "Developer Mode is currently " + (this.settings.developer_mode ? "enabled." : "disabled.");
- localStorage.ffzDebugMode = enabled;
+ this.settings.set("developer_mode", enabled);
return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser.";
}
@@ -319,7 +336,10 @@ FFZ.prototype.setup_chatview = function() {
// For some reason, this doesn't work unless we create an instance of the
// chat view and then destroy it immediately.
- Chat.create().destroy();
+ try {
+ Chat.create().destroy();
+ } catch(err) { }
+
// Modify all existing Chat views.
for(var key in Ember.View.views) {
@@ -381,6 +401,80 @@ var FFZ = window.FrankerFaceZ,
reg_escape = function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+
+ SEPARATORS = "[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]",
+ SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"),
+
+ quote_attr = function(attr) {
+ return (attr + '')
+ .replace(/&/g, "&")
+ .replace(/'/g, "'")
+ .replace(/"/g, """)
+ .replace(//g, ">");
+ },
+
+
+ TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/",
+ build_srcset = function(id) {
+ return TWITCH_BASE + id + "/1.0 1x, " + TWITCH_BASE + id + "/2.0 2x, " + TWITCH_BASE + id + "/3.0 4x";
+ },
+
+
+ data_to_tooltip = function(data) {
+ var output = "
Emoticon " + data.code + " ",
+ set = data.set,
+ set_type = data.set_type;
+
+ if ( set_type === undefined )
+ set_type = "Channel";
+
+ if ( ! set )
+ return data.code;
+
+ else if ( set == "00000turbo" || set == "turbo" ) {
+ set = "Twitch Turbo";
+ set_type = null;
+ }
+
+ if ( ! set_type )
+ output += '' + set + ' ';
+ else
+ output += "" + set_type + " " + set + " ";
+
+ return '';
+ },
+
+ build_tooltip = function(id) {
+ var emote_data = this._twitch_emotes[id],
+ set = emote_data ? emote_data.set : null;
+
+ if ( ! emote_data )
+ return "???";
+
+ if ( typeof emote_data == "string" )
+ return emote_data;
+
+ if ( emote_data.tooltip )
+ return emote_data.tooltip;
+
+ return emote_data.tooltip = data_to_tooltip(emote_data);
+ },
+
+ load_emote_data = function(id, code, success, data) {
+ if ( ! success )
+ return;
+
+ if ( code )
+ data.code = code;
+
+ this._twitch_emotes[id] = data;
+ var tooltip = build_tooltip.bind(this)(id);
+
+ var images = document.querySelectorAll('img[emote-id="' + id + '"]');
+ for(var x=0; x < images.length; x++)
+ images[x].title = tooltip;
};
@@ -400,6 +494,37 @@ FFZ.settings_info.capitalize = {
};
+FFZ.settings_info.banned_words = {
+ type: "button",
+ value: [],
+
+ category: "Chat",
+ visible: function() { return ! this.has_bttv },
+
+ name: "Banned Words",
+ help: "Set a list of words that will be locally removed from chat messages.",
+
+ method: function() {
+ var old_val = this.settings.banned_words.join(", "),
+ new_val = prompt("Banned Words\n\nPlease enter a comma-separated list of words that you would like to be removed from chat messages.", old_val);
+
+ if ( new_val === null || new_val === undefined )
+ return;
+
+ new_val = new_val.trim().split(SPLITTER);
+ var vals = [];
+
+ for(var i=0; i < new_val.length; i++)
+ new_val[i] && vals.push(new_val[i]);
+
+ if ( vals.length == 1 && vals[0] == "disable" )
+ vals = [];
+
+ this.settings.set("banned_words", vals);
+ }
+ };
+
+
FFZ.settings_info.keywords = {
type: "button",
value: [],
@@ -418,12 +543,16 @@ FFZ.settings_info.keywords = {
return;
// Split them up.
- new_val = new_val.trim().split(/\W*,\W*/);
+ new_val = new_val.trim().split(SPLITTER);
+ var vals = [];
- if ( new_val.length == 1 && (new_val[0] == "" || new_val[0] == "disable") )
- new_val = [];
+ for(var i=0; i < new_val.length; i++)
+ new_val[i] && vals.push(new_val[i]);
- this.settings.set("keywords", new_val);
+ if ( vals.length == 1 && vals[0] == "disable" )
+ vals = [];
+
+ this.settings.set("keywords", vals);
}
};
@@ -484,6 +613,10 @@ FFZ.prototype.setup_line = function() {
document.head.appendChild(s);
+ // Emoticon Data
+ this._twitch_emotes = {};
+
+
this.log("Hooking the Ember Line controller.");
var Line = App.__container__.resolve('controller:line'),
@@ -495,6 +628,7 @@ FFZ.prototype.setup_line = function() {
var tokens = this._super();
try {
+ tokens = f._remove_banned(tokens);
tokens = f._emoticonize(this, tokens);
var user = f.get_user();
@@ -521,11 +655,12 @@ FFZ.prototype.setup_line = function() {
this._super();
try {
var el = this.get('element'),
- user = this.get('context.model.from'),
- room = this.get('context.parentController.content.id'),
- color = this.get('context.model.color'),
+ controller = this.get('context'),
+ user = controller.get('model.from'),
+ room = controller.get('parentController.content.id'),
+ color = controller.get('model.color'),
- row_type = this.get('context.model.ffz_alternate');
+ row_type = controller.get('model.ffz_alternate');
// Color Processing
@@ -588,6 +723,107 @@ FFZ.prototype.setup_line = function() {
// Mark that we've checked this message for mentions.
this.set('context.model.ffz_notified', true);
+
+ // Banned Links
+ var bad_links = el.querySelectorAll('a.deleted-link');
+ for(var i=0; i < bad_links.length; i++) {
+ var link = bad_links[i];
+
+ link.addEventListener("click", function(e) {
+ if ( ! this.classList.contains("deleted-link") )
+ return true;
+
+ // Get the URL
+ var href = this.getAttribute('data-url'),
+ link = href;
+
+ // Delete Old Stuff
+ this.classList.remove('deleted-link');
+ this.removeAttribute("data-url");
+ this.removeAttribute("title");
+ this.removeAttribute("original-title");
+
+ // Process URL
+ if ( href.indexOf("@") > -1 && (-1 === href.indexOf("/") || href.indexOf("@") < href.indexOf("/")) )
+ href = "mailto:" + href;
+ else if ( ! href.match(/^https?:\/\//) )
+ href = "http://" + href;
+
+ // Set up the Link
+ this.href = href;
+ this.target = "_new";
+ this.textContent = link;
+
+ // Stop from Navigating
+ e.preventDefault();
+ });
+
+ // Also add a nice tooltip.
+ jQuery(link).tipsy();
+ }
+
+
+ // Enhanced Emotes
+ var images = el.querySelectorAll('img');
+ for(var i=0; i < images.length; i++) {
+ var img = images[i],
+ name = img.alt,
+ match = /\/emoticons\/v1\/(\d+)\/1\.0/.exec(img.src),
+ id = match ? parseInt(match[1]) : null;
+
+ if ( id !== null ) {
+ // High-DPI Images
+ img.setAttribute('srcset', build_srcset(id));
+ img.setAttribute('emote-id', id);
+
+ // Source Lookup
+ var emote_data = f._twitch_emotes[id];
+ if ( emote_data ) {
+ if ( typeof emote_data != "string" )
+ img.title = emote_data.tooltip;
+
+ } else {
+ f._twitch_emotes[id] = img.alt;
+ f.ws_send("twitch_emote", id, load_emote_data.bind(f, id, img.alt));
+ }
+
+ jQuery(img).tipsy({html:true});
+
+ } else if ( img.getAttribute('data-ffz-emote') ) {
+ var data = JSON.parse(decodeURIComponent(img.getAttribute('data-ffz-emote'))),
+ id = data && data[0] || null,
+ set_id = data && data[1] || null,
+
+ set = f.emote_sets[set_id],
+ emote = set ? set.emotes[id] : null,
+
+ set_name = set.id,
+ set_type = "FFZ Channel";
+
+ if ( set.id == "global" ) {
+ set_name = "FrankerFaceZ Global";
+ set_type = null;
+
+ } else if ( set.id == "globalevent" ) {
+ set_name = "FrankerFaceZ Event";
+ set_type = null;
+
+ } else if ( f.feature_friday && set.id == f.feature_friday.set )
+ set_name = "Feature Friday - " + f.feature_friday.channel;
+
+ img.title = data_to_tooltip({
+ code: emote.hidden ? "???" : emote.name,
+ set: set_name,
+ set_type: set_type
+ });
+
+ jQuery(img).tipsy({html:true});
+
+ } else
+ jQuery(img).tipsy();
+ }
+
+
} catch(err) {
try {
f.error("LineView didInsertElement: " + err);
@@ -666,7 +902,6 @@ FFZ.prototype._handle_color = function(color) {
}
-
// ---------------------
// Capitalization
// ---------------------
@@ -676,7 +911,7 @@ FFZ._cap_fetching = 0;
FFZ.get_capitalization = function(name, callback) {
// Use the BTTV code if it's present.
- if ( window.BetterTTV )
+ if ( window.BetterTTV && BetterTTV.chat && BetterTTV.chat.helpers.lookupDisplayName )
return BetterTTV.chat.helpers.lookupDisplayName(name);
if ( ! name )
@@ -692,15 +927,14 @@ FFZ.get_capitalization = function(name, callback) {
return old_data[0];
}
- if ( FFZ._cap_fetching < 5 ) {
+ if ( FFZ._cap_fetching < 25 ) {
FFZ._cap_fetching++;
- Twitch.api.get("users/" + name)
- .always(function(data) {
- var cap_name = data.display_name || name;
- FFZ.capitalization[name] = [cap_name, Date.now()];
- FFZ._cap_fetching--;
- typeof callback === "function" && callback(cap_name);
- });
+ FFZ.get().ws_send("get_display_name", name, function(success, data) {
+ var cap_name = success ? data : name;
+ FFZ.capitalization[name] = [cap_name, Date.now()];
+ FFZ._cap_fetching--;
+ typeof callback === "function" && callback(cap_name);
+ });
}
return old_data ? old_data[0] : name;
@@ -709,7 +943,7 @@ FFZ.get_capitalization = function(name, callback) {
FFZ.prototype.capitalize = function(view, user) {
var name = FFZ.get_capitalization(user, this.capitalize.bind(this, view));
- if ( name )
+ if ( name && view )
view.$('.from').text(name);
}
@@ -724,8 +958,21 @@ FFZ._get_regex = function(word) {
return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig");
}
-FFZ._mentions_to_regex = function(list) {
- return FFZ._regex_cache[list] = FFZ._regex_cache[list] || RegExp("\\b(?:" + _.chain(list).map(reg_escape).value().join("|") + ")\\b", "ig");
+FFZ._words_to_regex = function(list) {
+ var regex = FFZ._regex_cache[list];
+ if ( ! regex ) {
+ var reg = "";
+ for(var i=0; i < list.length; i++) {
+ if ( ! list[i] )
+ continue;
+
+ reg += (reg ? "|" : "") + reg_escape(list[i]);
+ }
+
+ regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig");
+ }
+
+ return regex;
}
@@ -737,24 +984,72 @@ FFZ.prototype._mentionize = function(controller, tokens) {
if ( typeof tokens == "string" )
tokens = [tokens];
- var regex = FFZ._mentions_to_regex(mention_words);
+ var regex = FFZ._words_to_regex(mention_words),
+ new_tokens = [];
- return _.chain(tokens).map(function(token) {
- if ( !_.isString(token) )
- return token;
- else if ( !token.match(regex) )
- return [token];
+ for(var i=0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if ( ! _.isString(token) ) {
+ new_tokens.push(token);
+ continue;
+ }
- return _.zip(
- _.map(token.split(regex), _.identity),
- _.map(token.match(regex), function(e) {
- return {
- mentionedUser: e,
- own: false
- };
- })
- );
- }).flatten().compact().value();
+ if ( ! token.match(regex) ) {
+ new_tokens.push(token);
+ continue;
+ }
+
+ token = token.replace(regex, function(all, prefix, match) {
+ new_tokens.push(prefix);
+ new_tokens.push({
+ mentionedUser: match,
+ own: false
+ });
+
+ return "";
+ });
+
+ if ( token )
+ new_tokens.push(token);
+ }
+
+ return new_tokens;
+}
+
+
+// ---------------------
+// Banned Words
+// ---------------------
+
+FFZ.prototype._remove_banned = function(tokens) {
+ var banned_words = this.settings.banned_words;
+ if ( ! banned_words || ! banned_words.length )
+ return tokens;
+
+ if ( typeof tokens == "string" )
+ tokens = [tokens];
+
+ var regex = FFZ._words_to_regex(banned_words),
+ new_tokens = [];
+
+ for(var i=0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if ( ! _.isString(token ) ) {
+ if ( token.emoticonSrc && regex.test(token.altText) )
+ new_tokens.push(token.altText.replace(regex, "$1***"));
+ else if ( token.isLink && regex.test(token.href) )
+ new_tokens.push({
+ mentionedUser: '<banned link> ',
+ own: true
+ });
+ else
+ new_tokens.push(token);
+
+ } else
+ new_tokens.push(token.replace(regex, "$1***"));
+ }
+
+ return new_tokens;
}
@@ -799,7 +1094,7 @@ FFZ.prototype._emoticonize = function(controller, tokens) {
// emoticon.
_.each(emotes, function(emote) {
//var eo = {isEmoticon:true, cls: emote.klass};
- var eo = {isEmoticon:true, cls: emote.klass, emoticonSrc: emote.url, altText: (emote.hidden ? "???" : emote.name)};
+ var eo = {isEmoticon:true, cls: emote.klass, emoticonSrc: emote.url + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), altText: (emote.hidden ? "???" : emote.name)};
tokens = _.compact(_.flatten(_.map(tokens, function(token) {
if ( _.isObject(token) )
@@ -817,7 +1112,7 @@ FFZ.prototype._emoticonize = function(controller, tokens) {
return tokens;
}
-},{"../utils":24}],7:[function(require,module,exports){
+},{"../utils":26}],7:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
utils = require("../utils"),
@@ -851,7 +1146,7 @@ FFZ.settings_info.enhanced_moderation = {
category: "Chat",
name: "Enhanced Moderation",
- help: "Use /p, /t, /u and /b in chat to moderator, or use hotkeys with moderation cards."
+ help: "Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards."
};
@@ -1056,7 +1351,7 @@ FFZ.chat_commands.b.enabled = function() { return this.settings.enhanced_moderat
FFZ.chat_commands.u = function(room, args) {
if ( ! args || ! args.length )
- return "Unban Usage: /b username [more usernames separated by spaces]";
+ return "Unban Usage: /u username [more usernames separated by spaces]";
if ( args.length > 10 )
return "Please only unban up to 10 users at once.";
@@ -1069,7 +1364,7 @@ FFZ.chat_commands.u = function(room, args) {
}
FFZ.chat_commands.u.enabled = function() { return this.settings.enhanced_moderation; }
-},{"../utils":24}],8:[function(require,module,exports){
+},{"../utils":26}],8:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg,
MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/,
@@ -1437,7 +1732,7 @@ FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) {
output.css = data || null;
return this._load_room_json(room_id, callback, output);
}
-},{"../constants":3,"../utils":24}],9:[function(require,module,exports){
+},{"../constants":3,"../utils":26}],9:[function(require,module,exports){
var FFZ = window.FrankerFaceZ;
@@ -1717,7 +2012,7 @@ FFZ.prototype._legacy_load_css = function(set_id, callback, data) {
margins = check_margins(margins, height);
var hidden = path.substr(path.lastIndexOf("/") + 1, 1) === ".",
id = ++f._last_emote_id,
- emote = {id: id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra};
+ emote = {id: id, set_id: set_id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra};
emotes[id] = emote;
return "";
@@ -1733,7 +2028,7 @@ FFZ.prototype._legacy_load_css = function(set_id, callback, data) {
this._load_set_json(set_id, callback, output);
}
-},{"./constants":3,"./utils":24}],11:[function(require,module,exports){
+},{"./constants":3,"./utils":26}],11:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/;
@@ -1975,7 +2270,7 @@ FFZ.get = function() { return FFZ.instance; }
// Version
var VER = FFZ.version_info = {
- major: 3, minor: 1, revision: 0,
+ major: 3, minor: 2, revision: 1,
toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
}
@@ -2058,9 +2353,12 @@ FFZ.prototype.get_user = function() {
//require('./templates');
+// Import these first to set up data structures
+require('./ui/menu');
require('./settings');
-
require('./socket');
+
+
require('./emoticons');
require('./badges');
@@ -2087,8 +2385,9 @@ require('./ui/notifications');
require('./ui/viewer_count');
require('./ui/menu_button');
-require('./ui/menu');
require('./ui/races');
+require('./ui/my_emotes');
+require('./ui/about_page');
require('./commands');
@@ -2151,6 +2450,7 @@ FFZ.prototype.setup_ember = function(delay) {
this.setup_notifications();
this.setup_css();
this.setup_menu();
+ this.setup_my_emotes();
this.setup_races();
this.find_bttv(10);
@@ -2163,7 +2463,7 @@ FFZ.prototype.setup_ember = function(delay) {
this.log("Initialization complete in " + duration + "ms");
}
-},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/moderation-card":7,"./ember/room":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./ui/menu":18,"./ui/menu_button":19,"./ui/notifications":20,"./ui/races":21,"./ui/styles":22,"./ui/viewer_count":23}],14:[function(require,module,exports){
+},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/moderation-card":7,"./ember/room":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./ui/about_page":18,"./ui/menu":19,"./ui/menu_button":20,"./ui/my_emotes":21,"./ui/notifications":22,"./ui/races":23,"./ui/styles":24,"./ui/viewer_count":25}],14:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
constants = require('./constants');
@@ -2312,10 +2612,17 @@ FFZ.prototype._update_ff_name = function(name) {
}
},{"./constants":3}],15:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
+ constants = require("./constants");
make_ls = function(key) {
return "ffz_setting_" + key;
+ },
+
+ toggle_setting = function(swit, key) {
+ var val = ! this.settings.get(key);
+ this.settings.set(key, val);
+ swit.classList.toggle('active', val);
};
@@ -2332,8 +2639,11 @@ FFZ.prototype.load_settings = function() {
this.settings = {};
for(var key in FFZ.settings_info) {
- var ls_key = make_ls(key),
- info = FFZ.settings_info[key],
+ if ( ! FFZ.settings_info.hasOwnProperty(key) )
+ continue;
+
+ var info = FFZ.settings_info[key],
+ ls_key = info.storage_key || make_ls(key),
val = info.hasOwnProperty("value") ? info.value : undefined;
if ( localStorage.hasOwnProperty(ls_key) ) {
@@ -2353,10 +2663,143 @@ FFZ.prototype.load_settings = function() {
this.settings.del = this._setting_del.bind(this);
// Listen for Changes
- window.addEventListener("storage", this._setting_update.bind(this));
+ window.addEventListener("storage", this._setting_update.bind(this), false);
}
+// --------------------
+// Menu Page
+// --------------------
+
+FFZ.menu_pages.settings = {
+ render: function(view, container) {
+ var settings = {},
+ categories = [];
+ for(var key in FFZ.settings_info) {
+ if ( ! FFZ.settings_info.hasOwnProperty(key) )
+ continue;
+
+ var info = FFZ.settings_info[key],
+ cat = info.category || "Miscellaneous",
+ cs = settings[cat];
+
+ if ( info.visible !== undefined && info.visible !== null ) {
+ var visible = info.visible;
+ if ( typeof info.visible == "function" )
+ visible = info.visible.bind(this)();
+
+ if ( ! visible )
+ continue;
+ }
+
+ if ( ! cs ) {
+ categories.push(cat);
+ cs = settings[cat] = [];
+ }
+
+ cs.push([key, info]);
+ }
+
+ categories.sort(function(a,b) {
+ var a = a.toLowerCase(),
+ b = b.toLowerCase();
+
+ if ( a === "Debugging" )
+ a = "zzz" + a;
+
+ if ( b === "Debugging" )
+ b = "zzz" + b;
+
+ if ( a < b ) return -1;
+ else if ( a > b ) return 1;
+ return 0;
+ });
+
+ for(var ci=0; ci < categories.length; ci++) {
+ var category = categories[ci],
+ cset = settings[category],
+
+ menu = document.createElement('div'),
+ heading = document.createElement('div');
+
+ heading.className = 'heading';
+ menu.className = 'chat-menu-content';
+ heading.innerHTML = category;
+ menu.appendChild(heading);
+
+ cset.sort(function(a,b) {
+ var a = a[1],
+ b = b[1],
+
+ at = a.type,
+ bt = b.type,
+
+ an = a.name.toLowerCase(),
+ bn = b.name.toLowerCase();
+
+ if ( at < bt ) return -1;
+ else if ( at > bt ) return 1;
+
+ else if ( an < bn ) return -1;
+ else if ( an > bn ) return 1;
+
+ return 0;
+ });
+
+ for(var i=0; i < cset.length; i++) {
+ var key = cset[i][0],
+ info = cset[i][1],
+ el = document.createElement('p'),
+ val = this.settings.get(key);
+
+ el.className = 'clearfix';
+
+ if ( info.type == "boolean" ) {
+ var swit = document.createElement('a'),
+ label = document.createElement('span');
+
+ swit.className = 'switch';
+ swit.classList.toggle('active', val);
+ swit.innerHTML = " ";
+
+ label.className = 'switch-label';
+ label.innerHTML = info.name;
+
+ el.appendChild(swit);
+ el.appendChild(label);
+
+ swit.addEventListener("click", toggle_setting.bind(this, swit, key));
+
+ } else {
+ el.classList.add("option");
+ var link = document.createElement('a');
+ link.innerHTML = info.name;
+ link.href = "#";
+ el.appendChild(link);
+
+ link.addEventListener("click", info.method.bind(this));
+ }
+
+ if ( info.help ) {
+ var help = document.createElement('span');
+ help.className = 'help';
+ help.innerHTML = info.help;
+ el.appendChild(help);
+ }
+
+ menu.appendChild(el);
+ }
+
+ container.appendChild(menu);
+ }
+ },
+
+ name: "Settings",
+ icon: constants.GEAR,
+ sort_order: 99999
+ };
+
+
// --------------------
// Tracking Updates
// --------------------
@@ -2375,6 +2818,22 @@ FFZ.prototype._setting_update = function(e) {
val = undefined,
info = FFZ.settings_info[key];
+ if ( ! info ) {
+ // Try iterating to find the key.
+ for(key in FFZ.settings_info) {
+ if ( ! FFZ.settings_info.hasOwnProperty(key) )
+ continue;
+
+ info = FFZ.settings_info[key];
+ if ( info.storage_key == ls_key )
+ break;
+ }
+
+ // Not us.
+ if ( info.storage_key != ls_key )
+ return;
+ }
+
this.log("Updated Setting: " + key);
try {
@@ -2405,8 +2864,8 @@ FFZ.prototype._setting_get = function(key) {
FFZ.prototype._setting_set = function(key, val) {
- var ls_key = make_ls(key),
- info = FFZ.settings_info[key],
+ var info = FFZ.settings_info[key],
+ ls_key = info.storage_key || make_ls(key),
jval = JSON.stringify(val);
this.settings[key] = val;
@@ -2424,8 +2883,8 @@ FFZ.prototype._setting_set = function(key, val) {
FFZ.prototype._setting_del = function(key) {
- var ls_key = make_ls(key),
- info = FFZ.settings_info[key],
+ var info = FFZ.settings_info[key],
+ ls_key = info.storage_key || make_ls(key),
val = undefined;
if ( localStorage.hasOwnProperty(ls_key) )
@@ -2443,7 +2902,7 @@ FFZ.prototype._setting_del = function(key) {
this.log('Error running updater for setting "' + key + '": ' + err);
}
}
-},{}],16:[function(require,module,exports){
+},{"./constants":3}],16:[function(require,module,exports){
Array.prototype.equals = function (array) {
// if the other array is a falsy value, return
if (!array)
@@ -2510,7 +2969,7 @@ FFZ.prototype.ws_create = function() {
// Send the current rooms.
for(var room_id in f.rooms)
- f.ws_send("sub", room_id);
+ f.rooms.hasOwnProperty(room_id) && f.ws_send("sub", room_id);
// Send any pending commands.
var pending = f._ws_pending;
@@ -2536,8 +2995,11 @@ FFZ.prototype.ws_create = function() {
}
// We never ever want to not have a socket.
- if ( f._ws_delay < 30000 )
+ if ( f._ws_delay < 60000 )
f._ws_delay += 5000;
+ else
+ // Randomize delay.
+ f._ws_delay = (Math.floor(Math.random()*60)+30)*1000;
setTimeout(f.ws_create.bind(f), f._ws_delay);
}
@@ -2597,7 +3059,131 @@ FFZ.prototype.ws_send = function(func, data, callback, can_wait) {
this._ws_sock.send(request + " " + func + data);
return request;
}
+
+// ----------------
+// Authorization
+// ----------------
+
+FFZ.ws_commands.do_authorize = function(data) {
+ // Try finding a channel we can send on.
+ var conn;
+ for(var room_id in this.rooms) {
+ if ( ! this.rooms.hasOwnProperty(room_id) )
+ continue;
+
+ var r = this.rooms[room_id];
+ if ( r && r.room && !r.room.get('roomProperties.eventchat') && !r.room.get('isGroupRoom') && r.room.tmiRoom ) {
+ var c = r.room.tmiRoom._getConnection();
+ if ( c.isConnected ) {
+ conn = c;
+ break;
+ }
+ }
+ }
+
+ if ( conn )
+ conn._send("PRIVMSG #frankerfacezauthorizer :AUTH " + data);
+ else
+ // Try again shortly.
+ setTimeout(FFZ.ws_commands.do_authorize.bind(this, data), 5000);
+}
},{}],18:[function(require,module,exports){
+var FFZ = window.FrankerFaceZ,
+ constants = require("../constants");
+
+
+// -------------------
+// About Page
+// -------------------
+
+FFZ.menu_pages.about = {
+ name: "About FrankerFaceZ",
+ icon: constants.HEART,
+ sort_order: 998,
+
+ render: function(view, container) {
+ var room = this.rooms[view.get("context.currentRoom.id")],
+ has_emotes = false, f = this;
+
+ // Check for emoticons.
+ if ( room && room.sets.length ) {
+ for(var i=0; i < room.sets.length; i++) {
+ var set = this.emote_sets[room.sets[i]];
+ if ( set && set.count > 0 ) {
+ has_emotes = true;
+ break;
+ }
+ }
+ }
+
+ // Heading
+ var heading = document.createElement('div'),
+ content = '';
+
+ content += "FrankerFaceZ ";
+ content += 'new ways to woof
';
+
+ heading.className = 'chat-menu-content center';
+ heading.innerHTML = content;
+ container.appendChild(heading);
+
+
+ // Advertising
+ var btn_container = document.createElement('div'),
+ ad_button = document.createElement('a'),
+ message = "To use custom emoticons in " + (has_emotes ? "this channel" : "tons of channels") + ", get FrankerFaceZ from http://www.frankerfacez.com";
+
+ ad_button.className = 'button primary';
+ ad_button.innerHTML = "Advertise in Chat";
+ ad_button.addEventListener('click', this._add_emote.bind(this, view, message));
+
+ btn_container.appendChild(ad_button);
+
+ // Donate
+ var donate_button = document.createElement('a');
+
+ donate_button.className = 'button ffz-donate';
+ donate_button.href = "http://www.frankerfacez.com/donate.html";
+ donate_button.target = "_new";
+ donate_button.innerHTML = "Donate";
+
+ btn_container.appendChild(donate_button);
+ btn_container.className = 'chat-menu-content center';
+ container.appendChild(btn_container);
+
+ // Credits
+ var credits = document.createElement('div');
+
+ content = '';
+ content += 'Developers ';
+ content += 'Dan Salvato ';
+ content += 'Stendec ';
+
+ content += 'Version ' + FFZ.version_info + ' Logs ';
+
+ credits.className = 'chat-menu-content center';
+ credits.innerHTML = content;
+
+ // Make the Logs button functional.
+ var getting_logs = false;
+ credits.querySelector('#ffz-debug-logs').addEventListener('click', function() {
+ if ( getting_logs )
+ return;
+
+ getting_logs = true;
+ f._pastebin(f._log_data.join("\n"), function(url) {
+ getting_logs = false;
+ if ( ! url )
+ alert("There was an error uploading the FrankerFaceZ logs.");
+ else
+ prompt("Your FrankerFaceZ logs have been uploaded to the URL:", url);
+ });
+ });
+
+ container.appendChild(credits);
+ }
+}
+},{"../constants":3}],19:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
constants = require('../constants');
@@ -2660,23 +3246,43 @@ FFZ.prototype.build_ui_popup = function(view) {
menu.className = 'menu clearfix';
inner.appendChild(menu);
- var el = document.createElement('li');
- el.className = 'title';
- el.innerHTML = "FrankerFaceZ ";
- menu.appendChild(el);
-
- el.addEventListener("click", this._add_emote.bind(this, view, "To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com"));
+ var heading = document.createElement('li');
+ heading.className = 'title';
+ heading.innerHTML = "" + (constants.DEBUG ? "[DEV] " : "") + "FrankerFaceZ ";
+ menu.appendChild(heading);
var sub_container = document.createElement('div');
sub_container.className = 'ffz-ui-menu-page';
inner.appendChild(sub_container);
+ var menu_pages = [];
for(var key in FFZ.menu_pages) {
+ if ( ! FFZ.menu_pages.hasOwnProperty(key) )
+ continue;
+
var page = FFZ.menu_pages[key];
if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)()))) )
continue;
- var el = document.createElement('li'),
+ menu_pages.push([page.sort_order || 0, key, page]);
+ }
+
+ menu_pages.sort(function(a,b) {
+ if ( a[0] < b[0] ) return 1;
+ else if ( a[0] > b[0] ) return -1;
+
+ var al = a[1].toLowerCase(),
+ bl = b[1].toLowerCase();
+
+ if ( al < bl ) return 1;
+ if ( al > bl ) return -1;
+ return 0;
+ });
+
+ for(var i=0; i < menu_pages.length; i++) {
+ var key = menu_pages[i][1],
+ page = menu_pages[i][2],
+ el = document.createElement('li'),
link = document.createElement('a');
el.className = 'item';
@@ -2684,6 +3290,8 @@ FFZ.prototype.build_ui_popup = function(view) {
link.title = page.name;
link.innerHTML = page.icon;
+ jQuery(link).tipsy();
+
link.addEventListener("click", this._ui_change_page.bind(this, view, menu, sub_container, key));
el.appendChild(link);
@@ -2703,6 +3311,7 @@ FFZ.prototype.build_ui_popup = function(view) {
FFZ.prototype._ui_change_page = function(view, menu, container, page) {
this._last_page = page;
container.innerHTML = "";
+ container.setAttribute('data-page', page);
var els = menu.querySelectorAll('li.active');
for(var i=0; i < els.length; i++)
@@ -2718,137 +3327,48 @@ FFZ.prototype._ui_change_page = function(view, menu, container, page) {
}
-// --------------------
-// Settings Page
-// --------------------
-
-FFZ.menu_pages.settings = {
- render: function(view, container) {
- var settings = {},
- categories = [];
- for(var key in FFZ.settings_info) {
- var info = FFZ.settings_info[key],
- cat = info.category || "Miscellaneous",
- cs = settings[cat];
-
- if ( info.visible !== undefined && info.visible !== null ) {
- var visible = info.visible;
- if ( typeof info.visible == "function" )
- visible = info.visible.bind(this)();
-
- if ( ! visible )
- continue;
- }
-
- if ( ! cs ) {
- categories.push(cat);
- cs = settings[cat] = [];
- }
-
- cs.push([key, info]);
- }
-
- categories.sort(function(a,b) {
- var a = a.toLowerCase(),
- b = b.toLowerCase();
-
- if ( a < b ) return -1;
- else if ( a > b ) return 1;
- return 0;
- });
-
- for(var ci=0; ci < categories.length; ci++) {
- var category = categories[ci],
- cset = settings[category],
-
- menu = document.createElement('div'),
- heading = document.createElement('div');
-
- heading.className = 'heading';
- menu.className = 'chat-menu-content';
- heading.innerHTML = category;
- menu.appendChild(heading);
-
- cset.sort(function(a,b) {
- var ai = a[1],
- bi = b[1],
-
- an = ai.name.toLowerCase(),
- bn = bi.name.toLowerCase();
-
- if ( an < bn ) return -1;
- else if ( an > bn ) return 1;
- return 0;
- });
-
-
- for(var i=0; i < cset.length; i++) {
- var key = cset[i][0],
- info = cset[i][1],
- el = document.createElement('p'),
- val = this.settings.get(key);
-
- el.className = 'clearfix';
-
- if ( info.type == "boolean" ) {
- var swit = document.createElement('a'),
- label = document.createElement('span');
-
- swit.className = 'switch';
- swit.classList.toggle('active', val);
- swit.innerHTML = " ";
-
- label.className = 'switch-label';
- label.innerHTML = info.name;
-
- el.appendChild(swit);
- el.appendChild(label);
-
- swit.addEventListener("click", this._ui_toggle_setting.bind(this, swit, key));
-
- } else {
- el.classList.add("option");
- var link = document.createElement('a');
- link.innerHTML = info.name;
- link.href = "#";
- el.appendChild(link);
-
- link.addEventListener("click", info.method.bind(this));
- }
-
- if ( info.help ) {
- var help = document.createElement('span');
- help.className = 'help';
- help.innerHTML = info.help;
- el.appendChild(help);
- }
-
- menu.appendChild(el);
- }
-
- container.appendChild(menu);
- }
- },
-
- name: "Settings",
- icon: constants.GEAR
- };
-
-
-FFZ.prototype._ui_toggle_setting = function(swit, key) {
- var val = ! this.settings.get(key);
- this.settings.set(key, val);
- swit.classList.toggle('active', val);
-}
-
-
// --------------------
// Favorites Page
// --------------------
+FFZ.prototype._tokenize_message = function(message, room_id) {
+ var lc = App.__container__.lookup('controller:line'),
+ rc = App.__container__.lookup('controller:room'),
+ room = this.rooms[room_id],
+ user = this.get_user();
+
+ if ( ! lc || ! rc || ! room )
+ return [message];
+
+ rc.set('model', room.room);
+ lc.set('parentController', rc);
+
+ var model = {
+ from: user && user.login || "FrankerFaceZ",
+ message: message,
+ tags: {
+ emotes: room.room.tmiSession._emotesParser.parseEmotesTag(message)
+ }
+ };
+
+ lc.set('model', model);
+
+ var tokens = lc.get('tokenizedMessage');
+
+ lc.set('model', null);
+ rc.set('model', null);
+ lc.set('parentController', null);
+
+ return tokens;
+}
+
+
/*FFZ.menu_pages.favorites = {
render: function(view, container) {
-
+ // Get the current room.
+ var room_id = view.get('controller.currentRoom.id');
+
+
},
name: "Favorites",
@@ -2866,35 +3386,8 @@ FFZ.menu_pages.channel = {
var room_id = view.get('controller.currentRoom.id'),
room = this.rooms[room_id];
- //this.track('trackEvent', 'Menu', 'Open', room_id);
-
- // Add the header and ad button.
- /*var btn = document.createElement('a');
- btn.className = 'button glyph-only ffz-button';
- btn.title = 'Advertise for FrankerFaceZ in chat!';
- btn.href = '#';
- btn.innerHTML = ' ';
-
- var hdr = document.createElement('div');
- hdr.className = 'list-header first';
- hdr.appendChild(btn);
- hdr.appendChild(document.createTextNode('FrankerFaceZ'));
- inner.appendChild(hdr);*/
-
- var c = this._emotes_for_sets(inner, view, room && room.menu_sets || []);
-
- /*if ( ! this._ws_exists ) {
- btn.className = "button ffz-button primary";
- btn.innerHTML = "Server Error";
- btn.title = "FFZ Server Error";
- btn.addEventListener('click', alert.bind(window, "The FrankerFaceZ client was unable to create a WebSocket to communicate with the FrankerFaceZ server.\n\nThis is most likely due to your browser's configuration either disabling WebSockets entirely or limiting the number of simultaneous connections. Please ensure that WebSockets have not been disabled."));
-
- } else {
- if ( c === 0 )
- btn.addEventListener('click', this._add_emote.bind(this, view, "To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com"));
- else
- btn.addEventListener('click', this._add_emote.bind(this, view, "To view this channel's emoticons, get FrankerFaceZ from http://www.frankerfacez.com"));
- }*/
+ // Basic Emote Sets
+ this._emotes_for_sets(inner, view, room && room.menu_sets || []);
// Feature Friday!
this._feature_friday_ui(room_id, inner, view);
@@ -2930,6 +3423,9 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) {
continue;
for(var eid in set.emotes) {
+ if ( ! set.emotes.hasOwnProperty(eid) )
+ continue;
+
var emote = set.emotes[eid];
if ( !set.emotes.hasOwnProperty(eid) || emote.hidden )
continue;
@@ -2956,15 +3452,25 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) {
FFZ.prototype._add_emote = function(view, emote) {
- var room = view.get('controller.currentRoom'),
- current_text = room.get('messageToSend') || '';
+ var input_el, text, room;
- if ( current_text && current_text.substr(-1) !== " " )
- current_text += ' ';
+ if ( this.has_bttv ) {
+ input_el = view.get('element').querySelector('textarea');
+ text = input_el.value;
- room.set('messageToSend', current_text + (emote.name || emote));
+ } else {
+ room = view.get('controller.currentRoom');
+ text = room.get('messageToSend') || '';
+ }
+
+ text += (text && text.substr(-1) !== " " ? " " : "") + (emote.name || emote);
+
+ if ( input_el )
+ input_el.value = text;
+ else
+ room.set('messageToSend', text);
}
-},{"../constants":3}],19:[function(require,module,exports){
+},{"../constants":3}],20:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
constants = require('../constants');
@@ -3015,7 +3521,307 @@ FFZ.prototype.update_ui_link = function(link) {
link.classList.toggle('dark', dark);
link.classList.toggle('blue', blue);
}
-},{"../constants":3}],20:[function(require,module,exports){
+},{"../constants":3}],21:[function(require,module,exports){
+var FFZ = window.FrankerFaceZ,
+ constants = require("../constants"),
+
+ TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/",
+ BANNED_SETS = {"00000turbo":true},
+
+
+ get_emotes = function(ffz) {
+ var Chat = App.__container__.lookup('controller:chat'),
+ room_id = Chat.get('currentRoom.id'),
+ room = ffz.rooms[room_id],
+ tmiSession = room ? room.room.tmiSession : null,
+
+ set_ids = tmiSession && tmiSession._emotesParser && tmiSession._emotesParser.emoticonSetIds || "0",
+ user = ffz.get_user(),
+ user_sets = user && ffz.users[user.login] && ffz.users[user.login].sets || [];
+
+ // Remove the 'default' set.
+ set_ids = set_ids.split(",").removeObject("0")
+
+ return [set_ids, user_sets];
+ };
+
+
+// -------------------
+// Initialization
+// -------------------
+
+FFZ.prototype.setup_my_emotes = function() {
+ this._twitch_emote_sets = {};
+ this._twitch_set_to_channel = {};
+
+ if ( localStorage.ffzTwitchSets ) {
+ try {
+ this._twitch_set_to_channel = JSON.parse(localStorage.ffzTwitchSets);
+ } catch(err) { }
+ }
+}
+
+
+// -------------------
+// Menu Page
+// -------------------
+
+FFZ.menu_pages.my_emotes = {
+ name: "My Emoticons",
+ icon: constants.EMOTE,
+
+ visible: function() {
+ var emotes = get_emotes(this);
+ return emotes[0].length > 0 || emotes[1].length > 0;
+ },
+
+ render: function(view, container) {
+ var emotes = get_emotes(this), f = this;
+
+ new RSVP.Promise(function(done) {
+ var needed_sets = [];
+ for(var i=0; i < emotes[0].length; i++) {
+ var set_id = emotes[0][i];
+ if ( ! f._twitch_emote_sets[set_id] )
+ needed_sets.push(set_id);
+ }
+
+ RSVP.all([
+ new RSVP.Promise(function(d) {
+ if ( ! needed_sets.length )
+ return d();
+
+ Twitch.api.get("chat/emoticon_images", {emotesets: needed_sets.join(",")}, {version: 3})
+ .done(function(data) {
+ if ( data.emoticon_sets ) {
+ for(var set_id in data.emoticon_sets) {
+ if ( ! data.emoticon_sets.hasOwnProperty(set_id) )
+ continue;
+
+ var set = f._twitch_emote_sets[set_id] = f._twitch_emote_sets[set_id] || {};
+ set.emotes = data.emoticon_sets[set_id];
+ set.source = "Twitch";
+ }
+ }
+ d();
+ }).fail(function() {
+ d();
+ });
+ }),
+ new RSVP.Promise(function(d) {
+ if ( ! needed_sets.length )
+ return d();
+
+ var promises = [],
+ old_needed = needed_sets,
+ handle_set = function(id, name) {
+ var set = f._twitch_emote_sets[id] = f._twitch_emote_sets[id] || {};
+
+ if ( !name || BANNED_SETS[name] )
+ return;
+
+ if ( name == "turbo" ) {
+ set.channel = "Twitch Turbo";
+ set.badge = "//cdn.frankerfacez.com/script/turbo_badge.png";
+ return;
+ }
+
+ // Badge Lookup
+ promises.push(new RSVP.Promise(function(set, name, dn) {
+ Twitch.api.get("chat/" + name + "/badges", null, {version: 3})
+ .done(function(data) {
+ if ( data.subscriber && data.subscriber.image )
+ set.badge = data.subscriber.image;
+ dn();
+ }).fail(dn)}.bind(this,set,name)));
+
+ // Mess Up Capitalization
+ var lname = name.toLowerCase(),
+ old_data = FFZ.capitalization[lname];
+ if ( old_data && Date.now() - old_data[1] < 3600000 ) {
+ set.channel = old_data[0];
+ return;
+ }
+
+ promises.push(new RSVP.Promise(function(set, lname, name, dn) {
+ if ( ! f.ws_send("get_display_name", lname, function(success, data) {
+ var cap_name = success ? data : name;
+ FFZ.capitalization[lname] = [cap_name, Date.now()];
+ set.channel = cap_name;
+ dn();
+ }) ) {
+ // Can't use socket.
+ set.channel = name;
+ dn();
+ }
+
+ // Timeout
+ setTimeout(function(set,name,dn) {
+ if ( ! set.channel )
+ set.channel = name;
+ dn();
+ }.bind(this,set,name,dn), 5000);
+ }.bind(this, set, lname, name)));
+ },
+ handle_promises = function() {
+ if ( promises.length )
+ RSVP.all(promises).then(d,d);
+ else
+ d();
+ };
+
+ // Process all the sets we already have.
+ needed_sets = [];
+ for(var i=0;i 0 ) {
+ f.ws_send("twitch_sets", needed_sets, function(success, data) {
+ needed_sets = [];
+ if ( success ) {
+ for(var set_id in data) {
+ if ( ! data.hasOwnProperty(set_id) )
+ continue;
+
+ f._twitch_set_to_channel[set_id] = data[set_id];
+ handle_set(set_id, data[set_id]);
+ }
+
+ localStorage.ffzTwitchSets = JSON.stringify(f._twitch_set_to_channel);
+ }
+
+ handle_promises();
+ });
+
+ // Timeout!
+ setTimeout(function() {
+ if ( needed_sets.length )
+ handle_promises();
+ }, 5000);
+
+ } else
+ handle_promises();
+ })
+ ]).then(function() {
+ var sets = {};
+ for(var i=0; i < emotes[0].length; i++) {
+ var set_id = emotes[0][i];
+ if ( f._twitch_emote_sets[set_id] )
+ sets[set_id] = f._twitch_emote_sets[set_id];
+ }
+ done(sets);
+ }, function() { done({}); })
+ }).then(function(twitch_sets) {
+ try {
+
+ // Don't override a different page. We can wait.
+ if ( container.getAttribute('data-page') != "my_emotes" )
+ return;
+
+ container.innerHTML = "";
+
+ var ffz_sets = {},
+ sets = [];
+
+ for(var set_id in twitch_sets) {
+ if ( ! twitch_sets.hasOwnProperty(set_id) )
+ continue;
+
+ var set = twitch_sets[set_id];
+ if ( set.channel && set.emotes && set.emotes.length )
+ sets.push([1, set.channel, set]);
+ }
+
+ sets.sort(function(a,b) {
+ if ( a[0] < b[0] ) return -1;
+ else if ( a[0] > b[0] ) return 1;
+
+ var an = a[1].toLowerCase(),
+ bn = b[1].toLowerCase();
+
+ if ( an === "twitch turbo" )
+ an = "zzz" + an;
+
+ if ( bn === "twitch turbo" )
+ bn = "zzz" + bn;
+
+ if ( an < bn ) return -1;
+ else if ( an > bn ) return 1;
+ return 0;
+ });
+
+ for(var i=0; i < sets.length; i++) {
+ var set = sets[i][2],
+ heading = document.createElement('div'),
+ menu = document.createElement('div');
+
+ heading.className = 'heading';
+ heading.innerHTML = '' + set.source + ' ' + FFZ.get_capitalization(set.channel);
+ if ( set.badge )
+ heading.style.backgroundImage = 'url("' + set.badge + '")';
+
+ menu.className = 'emoticon-grid';
+ menu.appendChild(heading);
+
+ for(var x=0; x < set.emotes.length; x++) {
+ var emote = set.emotes[x];
+
+ var s = document.createElement('span');
+ s.className = 'emoticon tooltip';
+ s.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")';
+
+ var img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)';
+ s.style.backgroundImage = '-webkit-' + img_set;
+ s.style.backgroundImage = '-moz-' + img_set;
+ s.style.backgroundImage = '-ms-' + img_set;
+ s.style.backgroundImage = img_set;
+
+ s.title = emote.code;
+ s.addEventListener('click', f._add_emote.bind(f, view, emote.code));
+ menu.appendChild(s);
+ }
+
+ container.appendChild(menu);
+ }
+
+ if ( ! sets.length ) {
+ var menu = document.createElement('div');
+
+ menu.className = 'chat-menu-content center';
+ menu.innerHTML = "Error Loading Subscriptions";
+
+ container.appendChild(menu);
+ }
+
+ } catch(err) {
+ f.log("My Emotes Menu Error", err);
+ container.innerHTML = "";
+
+ var menu = document.createElement('div'),
+ heading = document.createElement('div'),
+ p = document.createElement('p');
+
+ heading.className = 'heading';
+ heading.innerHTML = 'Error Loading Menu';
+ menu.appendChild(heading);
+
+ p.className = 'clearfix';
+ p.textContent = err;
+ menu.appendChild(p);
+
+ menu.className = 'chat-menu-content';
+ container.appendChild(menu);
+ }
+ });
+ }
+ };
+
+},{"../constants":3}],22:[function(require,module,exports){
var FFZ = window.FrankerFaceZ;
@@ -3164,7 +3970,7 @@ FFZ.prototype.show_message = function(message) {
closeWith: ["button"]
}).show();
}
-},{}],21:[function(require,module,exports){
+},{}],23:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
utils = require('../utils');
@@ -3469,7 +4275,7 @@ FFZ.prototype._update_race = function(not_timer) {
}
}
}
-},{"../utils":24}],22:[function(require,module,exports){
+},{"../utils":26}],24:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
constants = require('../constants');
@@ -3480,7 +4286,7 @@ FFZ.prototype.setup_css = function() {
s.id = "ffz-ui-css";
s.setAttribute('rel', 'stylesheet');
- s.setAttribute('href', constants.SERVER + "script/style.css");
+ s.setAttribute('href', constants.SERVER + "script/style.css?_=" + Date.now());
document.head.appendChild(s);
jQuery.noty.themes.ffzTheme = {
@@ -3494,7 +4300,7 @@ FFZ.prototype.setup_css = function() {
}
};
}
-},{"../constants":3}],23:[function(require,module,exports){
+},{"../constants":3}],25:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
constants = require('../constants'),
utils = require('../utils');
@@ -3531,7 +4337,7 @@ FFZ.ws_commands.viewers = function(data) {
jQuery(view_count).tipsy();
}
}
-},{"../constants":3,"../utils":24}],24:[function(require,module,exports){
+},{"../constants":3,"../utils":26}],26:[function(require,module,exports){
var FFZ = window.FrankerFaceZ,
constants = require('./constants');
@@ -3576,8 +4382,7 @@ var sanitize_cache = {},
rgb[i] = Math.pow( ((rgb[i]+0.055)/1.055), 2.4 );
}
}
- var l = (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]);
- return l;
+ return (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]);
};
diff --git a/script.min.js b/script.min.js
index a28ffcc7..88ebedd1 100644
--- a/script.min.js
+++ b/script.min.js
@@ -1,2 +1,3 @@
-!function(e){!function t(e,n,i){function o(r,a){if(!n[r]){if(!e[r]){var l="function"==typeof require&&require;if(!a&&l)return l(r,!0);if(s)return s(r,!0);throw new Error("Cannot find module '"+r+"'")}var c=n[r]={exports:{}};e[r][0].call(c.exports,function(t){var n=e[r][1][t];return o(n?n:t)},c,c.exports,t,e,n,i)}return n[r].exports}for(var s="function"==typeof require&&require,r=0;re?this._legacy_add_donors(e):void 0):void 0})},n.prototype._legacy_parse_donors=function(e){var t=0;if(null!=e)for(var n=e.trim().split(/\W+/),i=0;i50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var i=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/unmod "+o)}return"Sent unmod command for "+i+" users."},t.ffz_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.ffz_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var n=this.get_user();if(!n||!n.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var i=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/mod "+o)}return"Sent mod command for "+i+" users."},t.ffz_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var n=' ',i="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:i,SERVER:i?"//localhost:8000/":"//cdn.frankerfacez.com/",SVGPATH:n,ZREKNARF:''+n+" ",CHAT_BUTTON:''+n+" ",GEAR:' ',HEART:' '}},{}],4:[function(){var t=e.FrankerFaceZ;t.ffz_commands.developer_mode=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Developer Mode is currently "+("true"==localStorage.ffzDebugMode?"enabled.":"disabled."):(localStorage.ffzDebugMode=n,"Developer Mode is now "+(n?"enabled":"disabled")+". Please refresh your browser.")},t.ffz_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[function(){var t=e.FrankerFaceZ;t.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e),e.create().destroy();for(var t in Ember.View.views)if(Ember.View.views.hasOwnProperty(t)){var n=Ember.View.views[t];if(n instanceof e){this.log("Adding UI link manually to Chat view.",n);try{n.$(".textarea-contain").append(this.build_ui_link(n))}catch(i){this.error("setup: build_ui_link: "+i)}}}},t.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.$()&&this.$(".textarea-contain").append(t.build_ui_link(this))}catch(e){t.error("didInsertElement: build_ui_link: "+e)}},willClearRender:function(){this._super();try{this.$(".ffz-ui-toggle").remove()}catch(e){t.error("willClearRender: remove ui link: "+e)}},ffzUpdateLink:Ember.observer("controller.currentRoom",function(){try{t.update_ui_link()}catch(e){t.error("ffzUpdateLink: update_ui_link: "+e)}})})}},{}],6:[function(t){var n=e.FrankerFaceZ,i=t("../utils"),o=function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")};n.settings_info.capitalize={type:"boolean",value:!0,category:"Chat",visible:function(){return!this.has_bttv},name:"Username Capitalization",help:"Display names in chat with proper capitalization."},n.settings_info.keywords={type:"button",value:[],category:"Chat",visible:function(){return!this.has_bttv},name:"Highlight Keywords",help:"Set additional keywords that will be highlighted in chat.",method:function(){var e=this.settings.keywords.join(", "),t=prompt("Highlight Keywords\n\nPlease enter a comma-separated list of words that you would like to be highlighted in chat.",e);null!==t&&void 0!==t&&(t=t.trim().split(/\W*,\W*/),1!=t.length||""!=t[0]&&"disable"!=t[0]||(t=[]),this.settings.set("keywords",t))}},n.settings_info.fix_color={type:"boolean",value:!1,category:"Chat",visible:function(){return!this.has_bttv},name:"Adjust Username Colors",help:"Ensure that username colors contrast with the background enough to be readable.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-colors",e)}},n.settings_info.chat_rows={type:"boolean",value:!1,category:"Chat",visible:function(){return!this.has_bttv},name:"Chat Line Backgrounds",help:"Display alternating background colors for lines in chat.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-background",e)}},n.prototype.setup_line=function(){document.body.classList.toggle("ffz-chat-colors",!this.has_bttv&&this.settings.fix_color),document.body.classList.toggle("ffz-chat-background",!this.has_bttv&&this.settings.chat_rows),this._colors={},this._last_row={};var t=this._fix_color_style=document.createElement("style");t.id="ffz-style-username-colors",t.type="text/css",document.head.appendChild(t),this.log("Hooking the Ember Line controller.");var i=App.__container__.resolve("controller:line"),o=this;i.reopen({tokenizedMessage:function(){var e=this._super();try{e=o._emoticonize(this,e);var t=o.get_user();t&&this.get("model.from")==t.login||(e=o._mentionize(this,e))}catch(n){try{o.error("LineController tokenizedMessage: "+n)}catch(n){}}return e}.property("model.message","isModeratorOrHigher")}),this.log("Hooking the Ember Line view.");var i=App.__container__.resolve("view:line");i.reopen({didInsertElement:function(){this._super();try{var t=this.get("element"),i=this.get("context.model.from"),s=this.get("context.parentController.content.id"),r=this.get("context.model.color"),a=this.get("context.model.ffz_alternate");r&&o._handle_color(r),void 0===a&&(a=o._last_row[s]=o._last_row.hasOwnProperty(s)?!o._last_row[s]:!1,this.set("context.model.ffz_alternate",a)),t.classList.toggle("ffz-alternate",a),t.setAttribute("data-room",s),t.setAttribute("data-sender",i),o.render_badge(this),o.settings.capitalize&&o.capitalize(this,i);var l=t.querySelector("span.mentioned");if(l&&(t.classList.add("ffz-mentioned"),!document.hasFocus()&&!this.get("context.model.ffz_notified")&&o.settings.highlight_notifications)){var c=n.get_capitalization(s),d=n.get_capitalization(i),u=c,h=this.get("context.model.message");this.get("context.parentController.content.isGroupRoom")&&(u=this.get("context.parentController.content.tmiRoom.displayName")),h="action"==this.get("context.model.style")?"* "+d+" "+h:d+": "+h,o.show_notification(h,"Twitch Chat Mention in "+u,c,6e4,e.focus.bind(e))}this.set("context.model.ffz_notified",!0)}catch(p){try{o.error("LineView didInsertElement: "+p)}catch(p){}}}});var s=this.get_user();s&&s.name&&(n.capitalization[s.login]=[s.name,Date.now()])},n.prototype._handle_color=function(e){if(e&&!this._colors[e]){this._colors[e]=!0;var t=parseInt(e.substr(1),16),n=[t>>16,t>>8&255,255&t],o=i.get_luminance(n),s="",r='span[style="color:'+e+'"]',a=!1;if(o>.3){a=!0;for(var l=255,c=n;l--&&(c=i.darken(c),!(i.get_luminance(c)<=.3)););s+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+r+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+r+" { color: "+i.rgb_to_css(c)+" !important; }\n"}else s+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+r+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+r+" { color: "+e+" !important; }\n";if(.1>o){a=!0;for(var l=255,c=n;l--&&(c=i.brighten(c),!(i.get_luminance(c)>=.1)););s+=".ffz-chat-colors .theatre .chat-container .chat-line "+r+", .ffz-chat-colors .chat-container.dark .chat-line "+r+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+r+" { color: "+i.rgb_to_css(c)+" !important; }\n"}else s+=".ffz-chat-colors .theatre .chat-container .chat-line "+r+", .ffz-chat-colors .chat-container.dark .chat-line "+r+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+r+" { color: "+e+" !important; }\n";a&&(this._fix_color_style.innerHTML+=s)}},n.capitalization={},n._cap_fetching=0,n.get_capitalization=function(t,i){if(e.BetterTTV)return BetterTTV.chat.helpers.lookupDisplayName(t);if(!t)return t;if(t=t.toLowerCase(),"jtv"==t||"twitchnotify"==t)return t;var o=n.capitalization[t];return o&&Date.now()-o[1]<36e5?o[0]:(n._cap_fetching<5&&(n._cap_fetching++,Twitch.api.get("users/"+t).always(function(e){var o=e.display_name||t;n.capitalization[t]=[o,Date.now()],n._cap_fetching--,"function"==typeof i&&i(o)})),o?o[0]:t)},n.prototype.capitalize=function(e,t){var i=n.get_capitalization(t,this.capitalize.bind(this,e));i&&e.$(".from").text(i)},n._regex_cache={},n._get_regex=function(e){return n._regex_cache[e]=n._regex_cache[e]||RegExp("\\b"+o(e)+"\\b","ig")},n._mentions_to_regex=function(e){return n._regex_cache[e]=n._regex_cache[e]||RegExp("\\b(?:"+_.chain(e).map(o).value().join("|")+")\\b","ig")},n.prototype._mentionize=function(e,t){var i=this.settings.keywords;if(!i||!i.length)return t;"string"==typeof t&&(t=[t]);var o=n._mentions_to_regex(i);return _.chain(t).map(function(e){return _.isString(e)?e.match(o)?_.zip(_.map(e.split(o),_.identity),_.map(e.match(o),function(e){return{mentionedUser:e,own:!1}})):[e]:e}).flatten().compact().value()},n.prototype._emoticonize=function(e,t){var n=e.get("parentController.model.id"),i=e.get("model.from"),o=this,s=this.getEmotes(i,n),r=[];return _.each(s,function(e){var n=o.emote_sets[e];n&&_.each(n.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&r.push(e)})}),r.length?("string"==typeof t&&(t=[t]),_.each(r,function(e){var n={isEmoticon:!0,cls:e.klass,emoticonSrc:e.url,altText:e.hidden?"???":e.name};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var i=t.split(e.regex),o=[];return i.forEach(function(e,t){o.push(e),t!==i.length-1&&o.push(n)}),o})))}),t):t}},{"../utils":24}],7:[function(t){var n=e.FrankerFaceZ,i=t("../utils"),o={ESC:27,P:80,B:66,T:84},s=[["5m",300],["10m",600],["1hr",3600],["12hr",43200],["24hr",86400]],r=' ',a=' ';n.settings_info.enhanced_moderation={type:"boolean",value:!1,visible:function(){return!this.has_bttv},category:"Chat",name:"Enhanced Moderation",help:"Use /p, /t, /u and /b in chat to moderator, or use hotkeys with moderation cards."},n.prototype.setup_mod_card=function(){this.log("Hooking the Ember Moderation Card view.");var e=App.__container__.resolve("view:moderation-card"),t=this;e.reopen({didInsertElement:function(){this._super();try{if(t.has_bttv||!t.settings.enhanced_moderation)return;var e=this.get("element"),n=this.get("context");if(e.classList.add("ffz-moderation-card"),n.get("parentController.model.isModeratorOrHigher")){e.classList.add("ffz-is-mod"),e.setAttribute("tabindex",1),e.addEventListener("keyup",function(e){var t=e.keyCode||e.which,i=n.get("model.user.id"),s=n.get("parentController.model");if(t==o.P)s.send("/timeout "+i+" 1");else if(t==o.B)s.send("/ban "+i);else if(t==o.T)s.send("/timeout "+i+" 600");else if(t!=o.ESC)return;n.send("hideModOverlay")});var l=document.createElement("div");l.className="interface clearfix";var c=function(e){var t=n.get("model.user.id"),i=n.get("parentController.model");i.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},d=function(e,t){var n=document.createElement("button");return n.className="button",n.innerHTML=e,n.title="Timeout User for "+i.number_commas(t)+" Second"+(1!=t?"s":""),600===t?n.title="(T)"+n.title.substr(1):1===t&&(n.title="(P)urge - "+n.title),jQuery(n).tipsy(),n.addEventListener("click",c.bind(this,t)),n};l.appendChild(d("Purge",1));var u=document.createElement("span");u.className="right",l.appendChild(u);for(var h=0;h button");v&&"button"==v.className&&(v.innerHTML=r,v.classList.add("glyph-only"),v.classList.add("message"),v.title="Message User",jQuery(v).tipsy()),this.$().draggable({start:function(){e.focus()}}),e.focus()}catch(b){try{t.error("ModerationCardView didInsertElement: "+b)}catch(b){}}}})},n.chat_commands.purge=n.chat_commands.p=function(e,t){if(!t||!t.length)return"Purge Usage: /p username [more usernames separated by spaces]";if(t.length>10)return"Please only purge up to 10 users at once.";for(var n=0;n10)return"Please only ban up to 10 users at once.";for(var n=0;n10)return"Please only unban up to 10 users at once.";for(var n=0;nn?this._legacy_add_room(e,t,n):void 0)})},n.prototype._legacy_load_room_css=function(e,t,n){var r=e,a=r.match(s);a&&a[1]&&(r=a[1]);var l={id:e,menu_sets:[r],sets:[r],moderator_badge:null,css:null};return n&&(n=n.replace(i,"").trim()),n&&(n=n.replace(o,function(e,t){return l.moderator_badge||"modicon.png"!==t.substr(-11)?e:(l.moderator_badge=t,"")})),l.css=n||null,this._load_room_json(e,t,l)}},{"../constants":3,"../utils":24}],9:[function(){var t=e.FrankerFaceZ;t.prototype.setup_viewers=function(){this.log("Hooking the Ember Viewers controller.");var e=App.__container__.resolve("controller:viewers");this._modify_viewers(e)},t.prototype._modify_viewers=function(e){var n=this;e.reopen({lines:function(){var e=this._super();try{var i=[],o={},s=null,r=App.__container__.lookup("controller:channel"),a=this.get("parentController.model.id"),l=r&&r.get("id");if(l){var c=r.get("display_name");c&&(t.capitalization[l]=[c,Date.now()])}a!=l&&(l=null);for(var d=0;dn?this._legacy_load_set(e,t,n):"function"==typeof t&&t(!1))})},n.prototype._legacy_load_css=function(e,t,n){var s={},r={id:e,emotes:s,extra_css:null},a=this;n=n.replace(i,function(e,t,n,i,o,r,c,d){o=parseInt(o),r=parseInt(r),c=l(c,o);var u="."===i.substr(i.lastIndexOf("/")+1,1),h=++a._last_emote_id,p={id:h,hidden:u,name:n,height:o,width:r,url:i,margins:c,extra_css:d};return s[h]=p,""}).trim(),n&&n.replace(o,function(e,t){r.icon||"modicon.png"!==t.substr(-11)||(r.icon=t)}),this._load_set_json(e,t,r)}},{"./constants":3,"./utils":24}],11:[function(){var t=e.FrankerFaceZ,n=/(\sdata-sender="[^"]*"(?=>))/;t.prototype.find_bttv=function(t,n){return this.has_bttv=!1,e.BTTVLOADED?this.setup_bttv(n||0):void(n>=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(n||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,document.body.classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background");var t=BetterTTV.chat.helpers.sendMessage,i=this;BetterTTV.chat.helpers.sendMessage=function(e){var n=e.split(" ",1)[0].toLowerCase();return"/ffz"!==n?t(e):void i.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var o,s=BetterTTV.chat.handlers.privmsg;BetterTTV.chat.handlers.privmsg=function(e,t){o=e;var n=s(e,t);return o=null,n};var r=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,s,a,l){i.bttv_badges(l);var c=r(e,t,s,a,l);return c.replace(n,'$1 data-room="'+o+'"')};var a,l=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,n,i){a=e;var o=l(e,t,n,i);return a=null,o};var c=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var n=c(e,t),s=i.getEmotes(a,o),t=[];return _.each(s,function(e){var o=i.emote_sets[e];o&&_.each(o.emotes,function(e){_.any(n,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=[' '],i=n;if(n=[],!i||!i.length)return n;for(var o=0;o=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(n||0)+t),t))},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),n=e?e.login:null,i=App.__container__.lookup("controller:chat"),o=i?i.get("currentRoom.id"):null,s=this.getEmotes(n,o),r=[],a=0;a=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(n||0)+t),t)))},n.prototype.setup_ember=function(t){var i=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_mod_card(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_races(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var o=e.performance&&performance.now?performance.now():Date.now(),s=o-i;this.log("Initialization complete in "+s+"ms")}},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/moderation-card":7,"./ember/room":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./ui/menu":18,"./ui/menu_button":19,"./ui/notifications":20,"./ui/races":21,"./ui/styles":22,"./ui/viewer_count":23}],14:[function(t){var n=e.FrankerFaceZ,i=t("./constants");n.prototype.feature_friday=null,n.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(i.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},n.ws_commands.reload_ff=function(){this.check_ff()},n.prototype._feature_friday_ui=function(e,t,n){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,n,[this.feature_friday.set],"Feature Friday");var i=App.__container__.lookup("controller:channel");if(!i||i.get("id")!=this.feature_friday.channel){var o=this.feature_friday,s=document.createElement("div"),r=document.createElement("a");s.className="chat-menu-content",s.style.textAlign="center";var a=o.display_name+(o.live?" is live now!":"");r.className="button primary",r.classList.toggle("live",o.live),r.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),r.href="http://www.twitch.tv/"+o.channel,r.title=a,r.target="_new",r.innerHTML=""+a+" ",s.appendChild(r),t.appendChild(s)}}},n.prototype._load_ff=function(e){if(this.feature_friday){this.global_sets.removeObject(this.feature_friday.set);var t=this.emote_sets[this.feature_friday.set];t&&(t.global=!1),this.feature_friday=null,this.update_ui_link()}e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,display_name:n.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.load_set(e.set,this._update_ff_set.bind(this)),this._update_ff_live())},n.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},n.prototype._update_ff_set=function(e,t){t&&(t.global=!0)},n.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],15:[function(){var t=e.FrankerFaceZ,n=function(e){return"ffz_setting_"+e};t.settings_info={},t.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var i in t.settings_info){var o=n(i),s=t.settings_info[i],r=s.hasOwnProperty("value")?s.value:void 0;if(localStorage.hasOwnProperty(o))try{r=JSON.parse(localStorage.getItem(o))}catch(a){this.log('Error loading value for "'+i+'": '+a)}this.settings[i]=r}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this))},t.prototype._setting_update=function(n){if(n||(n=e.event),this.log("Storage Event",n),n.key&&"ffz_setting_"===n.key.substr(0,12)){var i=n.key,o=i.substr(12),s=void 0,r=t.settings_info[o];this.log("Updated Setting: "+o);try{s=JSON.parse(n.newValue)}catch(a){this.log('Error loading new value for "'+o+'": '+a),s=r.value||void 0}if(this.settings[o]=s,r.on_update)try{r.on_update.bind(this)(s,!1)}catch(a){this.log('Error running updater for setting "'+o+'": '+a)}}},t.prototype._setting_get=function(e){return this.settings[e]},t.prototype._setting_set=function(e,i){var o=n(e),s=t.settings_info[e],r=JSON.stringify(i);if(this.settings[e]=i,localStorage.setItem(o,r),this.log('Changed Setting "'+e+'" to: '+r),s.on_update)try{s.on_update.bind(this)(i,!0)}catch(a){this.log('Error running updater for setting "'+e+'": '+a)}},t.prototype._setting_del=function(e){var i=n(e),o=t.settings_info[e],s=void 0;if(localStorage.hasOwnProperty(i)&&localStorage.removeItem(i),delete this.settings[e],o&&(s=this.settings[e]=o.hasOwnProperty("value")?o.value:void 0),o.on_update)try{o.on_update.bind(this)(s,!0)}catch(r){this.log('Error running updater for setting "'+e+'": '+r)}}},{}],16:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,n=this.length;n>t;t++)if(this[t]instanceof Array&&e[t]instanceof Array){if(!this[t].equals(e[t]))return!1}else if(this[t]!=e[t])return!1;return!0}},{}],17:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var e,n=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{e=this._ws_sock=new WebSocket("ws://ffz.stendec.me/")}catch(i){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+i)}this._ws_exists=!0,e.onopen=function(){n._ws_open=!0,n._ws_delay=0,n.log("Socket connected.");var e=n.get_user();e&&n.ws_send("setuser",e.login);for(var t in n.rooms)n.ws_send("sub",t);var i=n._ws_pending;n._ws_pending=[];for(var o=0;oFrankerFaceZ",s.appendChild(a),a.addEventListener("click",this._add_emote.bind(this,e,"To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com"));var l=document.createElement("div");l.className="ffz-ui-menu-page",o.appendChild(l);for(var c in n.menu_pages){var d=n.menu_pages[c];if(d&&(!d.hasOwnProperty("visible")||d.visible&&("function"!=typeof d.visible||d.visible.bind(this)()))){var a=document.createElement("li"),u=document.createElement("a");a.className="item",a.id="ffz-menu-page-"+c,u.title=d.name,u.innerHTML=d.icon,u.addEventListener("click",this._ui_change_page.bind(this,e,s,l,c)),a.appendChild(u),s.appendChild(a)}}this._ui_change_page(e,s,l,this._last_page||"channel"),this._popup=i,l.style.maxHeight=Math.max(100,e.$().height()-162)+"px",e.$(".chat-interface").append(i)},n.prototype._ui_change_page=function(e,t,i,o){this._last_page=o,i.innerHTML="";for(var s=t.querySelectorAll("li.active"),r=0;re?-1:e>t?1:0});for(var d=0;do?-1:o>s?1:0});for(var f=0;f",b.className="switch-label",b.innerHTML=r.name,m.appendChild(v),m.appendChild(b),v.addEventListener("click",this._ui_toggle_setting.bind(this,v,s))}else{m.classList.add("option");var y=document.createElement("a");y.innerHTML=r.name,y.href="#",m.appendChild(y),y.addEventListener("click",r.method.bind(this))}if(r.help){var w=document.createElement("span");w.className="help",w.innerHTML=r.help,m.appendChild(w)}p.appendChild(m)}t.appendChild(p)}},name:"Settings",icon:i.GEAR},n.prototype._ui_toggle_setting=function(e,t){var n=!this.settings.get(t);this.settings.set(t,n),e.classList.toggle("active",n)},n.menu_pages.channel={render:function(e,t){{var n=e.get("controller.currentRoom.id"),i=this.rooms[n];this._emotes_for_sets(t,e,i&&i.menu_sets||[])}this._feature_friday_ui(n,t,e)},name:"Channel",icon:i.ZREKNARF},n.prototype._emotes_for_sets=function(e,t,n,i,o){if(null!=i){var s=document.createElement("div");s.className="list-header",s.appendChild(document.createTextNode(i)),o&&s.appendChild(o),e.appendChild(s)}var r=document.createElement("div"),a=0;r.className="emoticon-grid";for(var l=0;l0){o=!0;break}}e.classList.toggle("no-emotes",!o),e.classList.toggle("live",a),e.classList.toggle("dark",s),e.classList.toggle("blue",r)}}},{"../constants":3}],20:[function(){var t=e.FrankerFaceZ;t.prototype.setup_notifications=function(){this.log("Adding event handler for window focus."),e.addEventListener("focus",this.clear_notifications.bind(this))},t.settings_info.highlight_notifications={type:"boolean",value:!1,category:"Chat",visible:function(){return!this.has_bttv},name:"Highlight Notifications",help:"Display notifications when a highlighted word appears in chat in an unfocused tab.",on_update:function(e,t){if(e&&t){if("denied"===Notification.permission)return this.log("Notifications have been denied by the user."),void this.settings.set("highlight_notifications",!1);if("granted"!==Notification.permission){var n=this;Notification.requestPermission(function(e){"denied"===e&&(n.log("Notifications have been denied by the user."),n.settings.set("highlight_notifications",!1))})}}}},t.ws_commands.message=function(e){this.show_message(e)},t._notifications={},t._last_notification=0,t.prototype.clear_notifications=function(){for(var e in t._notifications){var n=t._notifications[e];if(n)try{n.close()}catch(i){}}t._notifications={},t._last_notification=0},t.prototype.show_notification=function(e,n,i,o,s,r){var a=Notification.permission;if("denied "===a)return!1;if("granted"===a){n=n||"FrankerFaceZ",o=o||1e4;var l={lang:"en-US",dir:"ltr",body:e,tag:i||"FrankerFaceZ",icon:"http://cdn.frankerfacez.com/icon32.png"},c=this,d=new Notification(n,l),u=t._last_notification++;return t._notifications[u]=d,d.addEventListener("click",function(){delete t._notifications[u],s&&s.bind(c)()}),d.addEventListener("close",function(){delete t._notifications[u],r&&r.bind(c)()}),void("number"==typeof o&&d.addEventListener("show",function(){setTimeout(function(){delete t._notifications[u],d.close()},o)}))}var c=this;Notification.requestPermission(function(){c.show_notification(e,n,i)})},t.prototype.show_message=function(t){e.noty({text:t,theme:"ffzTheme",layout:"bottomCenter",closeWith:["button"]}).show()}},{}],21:[function(t){var n=e.FrankerFaceZ,i=t("../utils");n.prototype.setup_races=function(){this.log("Initializing race support."),this.srl_races={}},n.settings_info.srl_races={type:"boolean",value:!0,category:"Channel Metadata",name:"SRL Race Information",help:'Display information about SpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},n.ws_on_close.push(function(){var e=App.__container__.lookup("controller:channel"),t=e.get("id"),n=!1;for(var i in this.srl_races)delete this.srl_races[i],i==t&&(n=!0);n&&this.rebuild_race_ui()}),n.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),n=t.get("id"),i=!1,o=0;o',r.addEventListener("click",this.build_race_popup.bind(this)),s.appendChild(r),o.appendChild(s),this._update_race(!0)},n.prototype._race_kill=function(){this._race_timer&&(clearTimeout(this._race_timer),delete this._race_timer),delete this._race_game,delete this._race_goal},n.prototype.build_race_popup=function(){var e=this._popup;if(!e||(e.parentElement.removeChild(e),delete this._popup,this._popup_kill&&this._popup_kill(),delete this._popup_kill,"ffz-race-popup"!=e.id)){var t=document.querySelector("#ffz-ui-race");if(t){var i=t.querySelector(".button"),o=i.offsetLeft+i.offsetWidth,s=t.getAttribute("data-channel"),r=this.srl_races[s],e=document.createElement("div"),a="";e.id="ffz-race-popup",e.className=(o>=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var l="http://kadgar.net/live",c=!1;for(var d in r.entrants){var u=r.entrants[d].state;r.entrants.hasOwnProperty(d)&&r.entrants[d].channel&&("racing"==u||"entered"==u)&&(l+="/"+r.entrants[d].channel,c=!0)}var h=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,p=App.__container__.lookup("controller:channel"),_=p?p.get("display_name"):n.get_capitalization(s),f=encodeURIComponent("I'm watching "+_+" race "+r.goal+" in "+r.game+" on SpeedRunsLive!");a='',a+='",a+='
',a+='',a+='SRL ',c&&(a+=' Multitwitch '),a+="
",e.innerHTML=a,t.appendChild(e),this._update_race(!0)}}},n.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var n=t.getAttribute("data-channel"),o=this.srl_races[n];if(!o)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var s=o.twitch_entrants[n],r=o.entrants[s],a=t.querySelector("#ffz-race-popup"),l=Date.now()/1e3,c=Math.floor(l-o.time);if(t.querySelector(".logo").innerHTML=i.placement(r),a){var d=a.querySelector("tbody"),u=a.querySelector(".heading span"),h=a.querySelector(".heading div");d.innerHTML="";var p=[],_=!0;for(var f in o.entrants)o.entrants.hasOwnProperty(f)&&("racing"==o.entrants[f].state&&(_=!1),p.push(o.entrants[f]));p.sort(function(e,t){var n=e.place||9999,i=t.place||9999,o=e.time||c,s=t.time||c;return("forfeit"==e.state||"dq"==e.state)&&(n=1e4),("forfeit"==t.state||"dq"==t.state)&&(i=1e4),i>n?-1:n>i?1:e.namet.name?1:s>o?-1:o>s?1:void 0});for(var m=0;m'+f.display_name+"",v=f.channel?' ':"",b=f.hitbox?' ':"",y=c?i.time_to_string(f.time||c):"",w=i.place_string(f.place),k=f.comment?i.sanitize(f.comment):"";d.innerHTML+="'+w+" "+g+" "+v+b+' '+("forfeit"==f.state?"Forfeit":y)+" "}if(this._race_game!=o.game||this._race_goal!=o.goal){this._race_game=o.game,this._race_goal=o.goal;var x=i.sanitize(o.game),z=i.sanitize(o.goal);h.innerHTML=''+x+" Goal: "+z}c?_?u.innerHTML="Done":(u.innerHTML=i.time_to_string(c),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):u.innerHTML="Entry Open"}}}},{"../utils":24}],22:[function(t){var n=e.FrankerFaceZ,i=t("../constants");n.prototype.setup_css=function(){this.log("Injecting main FrankerFaceZ CSS.");var e=this._main_style=document.createElement("link");e.id="ffz-ui-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",i.SERVER+"script/style.css"),document.head.appendChild(e),jQuery.noty.themes.ffzTheme={name:"ffzTheme",style:function(){this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type)},callback:{onShow:function(){},onClose:function(){}}}}},{"../constants":3}],23:[function(t){var n=e.FrankerFaceZ,i=t("../constants"),o=t("../utils");n.ws_commands.viewers=function(e){var t=e[0],n=e[1],s=App.__container__.lookup("controller:channel"),r=s&&s.get&&s.get("id");if(r===t){var a=document.querySelector(".channel-stats .ffz.stat"),l=i.ZREKNARF+" "+o.number_commas(n);if(a)a.innerHTML=l;else{var c=document.querySelector(".channel-stats");if(!c)return;a=document.createElement("span"),a.className="ffz stat",a.title="Viewers with FrankerFaceZ",a.innerHTML=l,c.appendChild(a),jQuery(a).tipsy()}}}},{"../constants":3,"../utils":24}],24:[function(t,n){var i=(e.FrankerFaceZ,t("./constants"),{}),o=document.createElement("span"),s=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"},r=function(e,t){t=0===t?0:t||1,t=Math.round(255*-(t/100));var n=Math.max(0,Math.min(255,e[0]-t)),i=Math.max(0,Math.min(255,e[1]-t)),o=Math.max(0,Math.min(255,e[2]-t));return[n,i,o]},a=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},l=function(e,t){return t=0===t?0:t||1,r(e,-t)},c=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;tr;(l||n)&&(l&&(i=i.substr(0,r)+i.substr(a+s.length)),n&&(i+=o+n+s),e.innerHTML=i)},get_luminance:c,brighten:r,darken:l,rgb_to_css:a,number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:s,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?s(e.place):""},sanitize:function(e){var t=i[e];return t||(o.textContent=e,t=i[e]=o.innerHTML,o.innerHTML=""),t},time_to_string:function(e){var t=e%60,n=Math.floor(e/60),i=Math.floor(n/60);return n%=60,(10>i?"0":"")+i+":"+(10>n?"0":"")+n+":"+(10>t?"0":"")+t}}},{"./constants":3}]},{},[13]),e.ffz=new FrankerFaceZ}(window);
\ No newline at end of file
+!function(e){!function t(e,n,o){function i(s,a){if(!n[s]){if(!e[s]){var l="function"==typeof require&&require;if(!a&&l)return l(s,!0);if(r)return r(s,!0);throw new Error("Cannot find module '"+s+"'")}var c=n[s]={exports:{}};e[s][0].call(c.exports,function(t){var n=e[s][1][t];return i(n?n:t)},c,c.exports,t,e,n,o)}return n[s].exports}for(var r="function"==typeof require&&require,s=0;se?this._legacy_add_donors(e):void 0):void 0})},n.prototype._legacy_parse_donors=function(e){var t=0;if(null!=e)for(var n=e.trim().split(/\W+/),o=0;o50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var o=t.length;t.length;){var i=t.shift();e.room.tmiRoom.sendMessage("/unmod "+i)}return"Sent unmod command for "+o+" users."},t.ffz_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.ffz_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var n=this.get_user();if(!n||!n.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var o=t.length;t.length;){var i=t.shift();e.room.tmiRoom.sendMessage("/mod "+i)}return"Sent mod command for "+o+" users."},t.ffz_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var n=' ',o="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:o,SERVER:o?"//localhost:8000/":"//cdn.frankerfacez.com/",SVGPATH:n,ZREKNARF:''+n+" ",CHAT_BUTTON:''+n+" ",GEAR:' ',HEART:' ',EMOTE:' '}},{}],4:[function(){var t=e.FrankerFaceZ;t.settings_info.developer_mode={type:"boolean",value:!1,storage_key:"ffzDebugMode",visible:function(){return this.settings.developer_mode||Date.now()-parseInt(localStorage.ffzLastDevMode||"0")<6048e5},category:"Debugging",name:"Developer Mode",help:"Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",on_update:function(){localStorage.ffzLastDevMode=Date.now()}},t.ffz_commands.developer_mode=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Developer Mode is currently "+(this.settings.developer_mode?"enabled.":"disabled."):(this.settings.set("developer_mode",n),"Developer Mode is now "+(n?"enabled":"disabled")+". Please refresh your browser.")},t.ffz_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[function(){var t=e.FrankerFaceZ;t.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e);try{e.create().destroy()}catch(t){}for(var n in Ember.View.views)if(Ember.View.views.hasOwnProperty(n)){var o=Ember.View.views[n];if(o instanceof e){this.log("Adding UI link manually to Chat view.",o);try{o.$(".textarea-contain").append(this.build_ui_link(o))}catch(t){this.error("setup: build_ui_link: "+t)}}}},t.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.$()&&this.$(".textarea-contain").append(t.build_ui_link(this))}catch(e){t.error("didInsertElement: build_ui_link: "+e)}},willClearRender:function(){this._super();try{this.$(".ffz-ui-toggle").remove()}catch(e){t.error("willClearRender: remove ui link: "+e)}},ffzUpdateLink:Ember.observer("controller.currentRoom",function(){try{t.update_ui_link()}catch(e){t.error("ffzUpdateLink: update_ui_link: "+e)}})})}},{}],6:[function(t){var n=e.FrankerFaceZ,o=t("../utils"),i=function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},r="[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]",s=new RegExp(r+"*,"+r+"*"),a=function(e){return(e+"").replace(/&/g,"&").replace(/'/g,"'").replace(/"/g,""").replace(//g,">")},l="http://static-cdn.jtvnw.net/emoticons/v1/",c=function(e){return l+e+"/1.0 1x, "+l+e+"/2.0 2x, "+l+e+"/3.0 4x"},u=function(e){var t="Emoticon "+e.code+" ",n=e.set,o=e.set_type;return void 0===o&&(o="Channel"),n?(("00000turbo"==n||"turbo"==n)&&(n="Twitch Turbo",o=null),t+=o?""+o+" "+n+" ":''+n+" ",'"):e.code},d=function(e){{var t=this._twitch_emotes[e];t?t.set:null}return t?"string"==typeof t?t:t.tooltip?t.tooltip:t.tooltip=u(t):"???"},h=function(e,t,n,o){if(n){t&&(o.code=t),this._twitch_emotes[e]=o;for(var i=d.bind(this)(e),r=document.querySelectorAll('img[emote-id="'+e+'"]'),s=0;s-1&&(-1===t.indexOf("/")||t.indexOf("@")>16,t>>8&255,255&t],i=o.get_luminance(n),r="",s='span[style="color:'+e+'"]',a=!1;if(i>.3){a=!0;for(var l=255,c=n;l--&&(c=o.darken(c),!(o.get_luminance(c)<=.3)););r+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+s+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+s+" { color: "+o.rgb_to_css(c)+" !important; }\n"}else r+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+s+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+s+" { color: "+e+" !important; }\n";if(.1>i){a=!0;for(var l=255,c=n;l--&&(c=o.brighten(c),!(o.get_luminance(c)>=.1)););r+=".ffz-chat-colors .theatre .chat-container .chat-line "+s+", .ffz-chat-colors .chat-container.dark .chat-line "+s+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+s+" { color: "+o.rgb_to_css(c)+" !important; }\n"}else r+=".ffz-chat-colors .theatre .chat-container .chat-line "+s+", .ffz-chat-colors .chat-container.dark .chat-line "+s+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+s+" { color: "+e+" !important; }\n";a&&(this._fix_color_style.innerHTML+=r)}},n.capitalization={},n._cap_fetching=0,n.get_capitalization=function(t,o){if(e.BetterTTV&&BetterTTV.chat&&BetterTTV.chat.helpers.lookupDisplayName)return BetterTTV.chat.helpers.lookupDisplayName(t);if(!t)return t;if(t=t.toLowerCase(),"jtv"==t||"twitchnotify"==t)return t;var i=n.capitalization[t];return i&&Date.now()-i[1]<36e5?i[0]:(n._cap_fetching<25&&(n._cap_fetching++,n.get().ws_send("get_display_name",t,function(e,i){var r=e?i:t;n.capitalization[t]=[r,Date.now()],n._cap_fetching--,"function"==typeof o&&o(r)})),i?i[0]:t)},n.prototype.capitalize=function(e,t){var o=n.get_capitalization(t,this.capitalize.bind(this,e));o&&e&&e.$(".from").text(o)},n._regex_cache={},n._get_regex=function(e){return n._regex_cache[e]=n._regex_cache[e]||RegExp("\\b"+i(e)+"\\b","ig")},n._words_to_regex=function(e){var t=n._regex_cache[e];if(!t){for(var o="",s=0;s<banned link> ',own:!0}:s)}return i},n.prototype._emoticonize=function(e,t){var n=e.get("parentController.model.id"),o=e.get("model.from"),i=this,r=this.getEmotes(o,n),s=[];return _.each(r,function(e){var n=i.emote_sets[e];n&&_.each(n.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&s.push(e)})}),s.length?("string"==typeof t&&(t=[t]),_.each(s,function(e){var n={isEmoticon:!0,cls:e.klass,emoticonSrc:e.url+'" data-ffz-emote="'+encodeURIComponent(JSON.stringify([e.id,e.set_id])),altText:e.hidden?"???":e.name};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var o=t.split(e.regex),i=[];return o.forEach(function(e,t){i.push(e),t!==o.length-1&&i.push(n)}),i})))}),t):t}},{"../utils":26}],7:[function(t){var n=e.FrankerFaceZ,o=t("../utils"),i={ESC:27,P:80,B:66,T:84},r=[["5m",300],["10m",600],["1hr",3600],["12hr",43200],["24hr",86400]],s=' ',a=' ';n.settings_info.enhanced_moderation={type:"boolean",value:!1,visible:function(){return!this.has_bttv},category:"Chat",name:"Enhanced Moderation",help:"Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards."},n.prototype.setup_mod_card=function(){this.log("Hooking the Ember Moderation Card view.");var e=App.__container__.resolve("view:moderation-card"),t=this;e.reopen({didInsertElement:function(){this._super();try{if(t.has_bttv||!t.settings.enhanced_moderation)return;var e=this.get("element"),n=this.get("context");if(e.classList.add("ffz-moderation-card"),n.get("parentController.model.isModeratorOrHigher")){e.classList.add("ffz-is-mod"),e.setAttribute("tabindex",1),e.addEventListener("keyup",function(e){var t=e.keyCode||e.which,o=n.get("model.user.id"),r=n.get("parentController.model");if(t==i.P)r.send("/timeout "+o+" 1");else if(t==i.B)r.send("/ban "+o);else if(t==i.T)r.send("/timeout "+o+" 600");else if(t!=i.ESC)return;n.send("hideModOverlay")});var l=document.createElement("div");l.className="interface clearfix";var c=function(e){var t=n.get("model.user.id"),o=n.get("parentController.model");o.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},u=function(e,t){var n=document.createElement("button");return n.className="button",n.innerHTML=e,n.title="Timeout User for "+o.number_commas(t)+" Second"+(1!=t?"s":""),600===t?n.title="(T)"+n.title.substr(1):1===t&&(n.title="(P)urge - "+n.title),jQuery(n).tipsy(),n.addEventListener("click",c.bind(this,t)),n};l.appendChild(u("Purge",1));var d=document.createElement("span");d.className="right",l.appendChild(d);for(var h=0;h button");v&&"button"==v.className&&(v.innerHTML=s,v.classList.add("glyph-only"),v.classList.add("message"),v.title="Message User",jQuery(v).tipsy()),this.$().draggable({start:function(){e.focus()}}),e.focus()}catch(b){try{t.error("ModerationCardView didInsertElement: "+b)}catch(b){}}}})},n.chat_commands.purge=n.chat_commands.p=function(e,t){if(!t||!t.length)return"Purge Usage: /p username [more usernames separated by spaces]";if(t.length>10)return"Please only purge up to 10 users at once.";for(var n=0;n10)return"Please only ban up to 10 users at once.";for(var n=0;n10)return"Please only unban up to 10 users at once.";for(var n=0;nn?this._legacy_add_room(e,t,n):void 0)})},n.prototype._legacy_load_room_css=function(e,t,n){var s=e,a=s.match(r);
+a&&a[1]&&(s=a[1]);var l={id:e,menu_sets:[s],sets:[s],moderator_badge:null,css:null};return n&&(n=n.replace(o,"").trim()),n&&(n=n.replace(i,function(e,t){return l.moderator_badge||"modicon.png"!==t.substr(-11)?e:(l.moderator_badge=t,"")})),l.css=n||null,this._load_room_json(e,t,l)}},{"../constants":3,"../utils":26}],9:[function(){var t=e.FrankerFaceZ;t.prototype.setup_viewers=function(){this.log("Hooking the Ember Viewers controller.");var e=App.__container__.resolve("controller:viewers");this._modify_viewers(e)},t.prototype._modify_viewers=function(e){var n=this;e.reopen({lines:function(){var e=this._super();try{var o=[],i={},r=null,s=App.__container__.lookup("controller:channel"),a=this.get("parentController.model.id"),l=s&&s.get("id");if(l){var c=s.get("display_name");c&&(t.capitalization[l]=[c,Date.now()])}a!=l&&(l=null);for(var u=0;un?this._legacy_load_set(e,t,n):"function"==typeof t&&t(!1))})},n.prototype._legacy_load_css=function(e,t,n){var r={},s={id:e,emotes:r,extra_css:null},a=this;n=n.replace(o,function(t,n,o,i,s,c,u,d){s=parseInt(s),c=parseInt(c),u=l(u,s);var h="."===i.substr(i.lastIndexOf("/")+1,1),m=++a._last_emote_id,p={id:m,set_id:e,hidden:h,name:o,height:s,width:c,url:i,margins:u,extra_css:d};return r[m]=p,""}).trim(),n&&n.replace(i,function(e,t){s.icon||"modicon.png"!==t.substr(-11)||(s.icon=t)}),this._load_set_json(e,t,s)}},{"./constants":3,"./utils":26}],11:[function(){var t=e.FrankerFaceZ,n=/(\sdata-sender="[^"]*"(?=>))/;t.prototype.find_bttv=function(t,n){return this.has_bttv=!1,e.BTTVLOADED?this.setup_bttv(n||0):void(n>=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(n||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,document.body.classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background");var t=BetterTTV.chat.helpers.sendMessage,o=this;BetterTTV.chat.helpers.sendMessage=function(e){var n=e.split(" ",1)[0].toLowerCase();return"/ffz"!==n?t(e):void o.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var i,r=BetterTTV.chat.handlers.privmsg;BetterTTV.chat.handlers.privmsg=function(e,t){i=e;var n=r(e,t);return i=null,n};var s=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,r,a,l){o.bttv_badges(l);var c=s(e,t,r,a,l);return c.replace(n,'$1 data-room="'+i+'"')};var a,l=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,n,o){a=e;var i=l(e,t,n,o);return a=null,i};var c=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var n=c(e,t),r=o.getEmotes(a,i),t=[];return _.each(r,function(e){var i=o.emote_sets[e];i&&_.each(i.emotes,function(e){_.any(n,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=[' '],o=n;if(n=[],!o||!o.length)return n;for(var i=0;i=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(n||0)+t),t))},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),n=e?e.login:null,o=App.__container__.lookup("controller:chat"),i=o?o.get("currentRoom.id"):null,r=this.getEmotes(n,i),s=[],a=0;a=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(n||0)+t),t)))},n.prototype.setup_ember=function(t){var o=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_mod_card(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_my_emotes(),this.setup_races(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var i=e.performance&&performance.now?performance.now():Date.now(),r=i-o;this.log("Initialization complete in "+r+"ms")}},{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/moderation-card":7,"./ember/room":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./settings":15,"./shims":16,"./socket":17,"./ui/about_page":18,"./ui/menu":19,"./ui/menu_button":20,"./ui/my_emotes":21,"./ui/notifications":22,"./ui/races":23,"./ui/styles":24,"./ui/viewer_count":25}],14:[function(t){var n=e.FrankerFaceZ,o=t("./constants");n.prototype.feature_friday=null,n.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(o.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},n.ws_commands.reload_ff=function(){this.check_ff()},n.prototype._feature_friday_ui=function(e,t,n){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,n,[this.feature_friday.set],"Feature Friday");var o=App.__container__.lookup("controller:channel");if(!o||o.get("id")!=this.feature_friday.channel){var i=this.feature_friday,r=document.createElement("div"),s=document.createElement("a");r.className="chat-menu-content",r.style.textAlign="center";var a=i.display_name+(i.live?" is live now!":"");s.className="button primary",s.classList.toggle("live",i.live),s.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),s.href="http://www.twitch.tv/"+i.channel,s.title=a,s.target="_new",s.innerHTML=""+a+" ",r.appendChild(s),t.appendChild(r)}}},n.prototype._load_ff=function(e){if(this.feature_friday){this.global_sets.removeObject(this.feature_friday.set);var t=this.emote_sets[this.feature_friday.set];t&&(t.global=!1),this.feature_friday=null,this.update_ui_link()}e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,display_name:n.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.load_set(e.set,this._update_ff_set.bind(this)),this._update_ff_live())},n.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},n.prototype._update_ff_set=function(e,t){t&&(t.global=!0)},n.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],15:[function(t){var n=e.FrankerFaceZ,o=t("./constants");make_ls=function(e){return"ffz_setting_"+e},toggle_setting=function(e,t){var n=!this.settings.get(t);this.settings.set(t,n),e.classList.toggle("active",n)},n.settings_info={},n.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var t in n.settings_info)if(n.settings_info.hasOwnProperty(t)){var o=n.settings_info[t],i=o.storage_key||make_ls(t),r=o.hasOwnProperty("value")?o.value:void 0;if(localStorage.hasOwnProperty(i))try{r=JSON.parse(localStorage.getItem(i))}catch(s){this.log('Error loading value for "'+t+'": '+s)}this.settings[t]=r}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this),!1)},n.menu_pages.settings={render:function(e,t){var o={},i=[];for(var r in n.settings_info)if(n.settings_info.hasOwnProperty(r)){var s=n.settings_info[r],a=s.category||"Miscellaneous",l=o[a];if(void 0!==s.visible&&null!==s.visible){var c=s.visible;if("function"==typeof s.visible&&(c=s.visible.bind(this)()),!c)continue}l||(i.push(a),l=o[a]=[]),l.push([r,s])}i.sort(function(e,t){var e=e.toLowerCase(),t=t.toLowerCase();return"Debugging"===e&&(e="zzz"+e),"Debugging"===t&&(t="zzz"+t),t>e?-1:e>t?1:0});for(var u=0;un?-1:n>o?1:r>i?-1:i>r?1:0});for(var f=0;f ",b.className="switch-label",b.innerHTML=s.name,_.appendChild(v),_.appendChild(b),v.addEventListener("click",toggle_setting.bind(this,v,r))}else{_.classList.add("option");var y=document.createElement("a");y.innerHTML=s.name,y.href="#",_.appendChild(y),y.addEventListener("click",s.method.bind(this))}if(s.help){var w=document.createElement("span");w.className="help",w.innerHTML=s.help,_.appendChild(w)}m.appendChild(_)}t.appendChild(m)}},name:"Settings",icon:o.GEAR,sort_order:99999},n.prototype._setting_update=function(t){if(t||(t=e.event),this.log("Storage Event",t),t.key&&"ffz_setting_"===t.key.substr(0,12)){var o=t.key,i=o.substr(12),r=void 0,s=n.settings_info[i];if(!s){for(i in n.settings_info)if(n.settings_info.hasOwnProperty(i)&&(s=n.settings_info[i],s.storage_key==o))break;if(s.storage_key!=o)return}this.log("Updated Setting: "+i);try{r=JSON.parse(t.newValue)}catch(a){this.log('Error loading new value for "'+i+'": '+a),r=s.value||void 0}if(this.settings[i]=r,s.on_update)try{s.on_update.bind(this)(r,!1)}catch(a){this.log('Error running updater for setting "'+i+'": '+a)}}},n.prototype._setting_get=function(e){return this.settings[e]},n.prototype._setting_set=function(e,t){var o=n.settings_info[e],i=o.storage_key||make_ls(e),r=JSON.stringify(t);if(this.settings[e]=t,localStorage.setItem(i,r),this.log('Changed Setting "'+e+'" to: '+r),o.on_update)try{o.on_update.bind(this)(t,!0)}catch(s){this.log('Error running updater for setting "'+e+'": '+s)}},n.prototype._setting_del=function(e){var t=n.settings_info[e],o=t.storage_key||make_ls(e),i=void 0;if(localStorage.hasOwnProperty(o)&&localStorage.removeItem(o),delete this.settings[e],t&&(i=this.settings[e]=t.hasOwnProperty("value")?t.value:void 0),t.on_update)try{t.on_update.bind(this)(i,!0)}catch(r){this.log('Error running updater for setting "'+e+'": '+r)}}},{"./constants":3}],16:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,n=this.length;n>t;t++)if(this[t]instanceof Array&&e[t]instanceof Array){if(!this[t].equals(e[t]))return!1}else if(this[t]!=e[t])return!1;return!0}},{}],17:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var e,n=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{e=this._ws_sock=new WebSocket("ws://ffz.stendec.me/")}catch(o){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+o)}this._ws_exists=!0,e.onopen=function(){n._ws_open=!0,n._ws_delay=0,n.log("Socket connected.");var e=n.get_user();e&&n.ws_send("setuser",e.login);for(var t in n.rooms)n.rooms.hasOwnProperty(t)&&n.ws_send("sub",t);var o=n._ws_pending;n._ws_pending=[];for(var i=0;i0){i=!0;break}}var l=document.createElement("div"),c="";c+="FrankerFaceZ ",c+='new ways to woof
',l.className="chat-menu-content center",l.innerHTML=c,t.appendChild(l);var u=document.createElement("div"),d=document.createElement("a"),h="To use custom emoticons in "+(i?"this channel":"tons of channels")+", get FrankerFaceZ from http://www.frankerfacez.com";d.className="button primary",d.innerHTML="Advertise in Chat",d.addEventListener("click",this._add_emote.bind(this,e,h)),u.appendChild(d);var m=document.createElement("a");m.className="button ffz-donate",m.href="http://www.frankerfacez.com/donate.html",m.target="_new",m.innerHTML="Donate",u.appendChild(m),u.className="chat-menu-content center",t.appendChild(u);var p=document.createElement("div");c='',c+='Developers ',c+='Dan Salvato ',c+='Stendec ',c+='Version '+n.version_info+' Logs ',p.className="chat-menu-content center",p.innerHTML=c;var f=!1;p.querySelector("#ffz-debug-logs").addEventListener("click",function(){f||(f=!0,r._pastebin(r._log_data.join("\n"),function(e){f=!1,e?prompt("Your FrankerFaceZ logs have been uploaded to the URL:",e):alert("There was an error uploading the FrankerFaceZ logs.")}))}),t.appendChild(p)}}},{"../constants":3}],19:[function(t){var n=e.FrankerFaceZ,o=t("../constants");n.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var n,o=e._popup;o&&(o=jQuery(o),n=o.parent(),n.is(t.target)||0!==n.has(t.target).length||(o.remove(),delete e._popup,e._popup_kill&&e._popup_kill(),delete e._popup_kill))})},n.menu_pages={},n.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),delete this._popup,this._popup_kill&&this._popup_kill(),void delete this._popup_kill;var i=document.createElement("div"),r=document.createElement("div"),s=document.createElement("ul"),a=this.has_bttv?BetterTTV.settings.get("darkenedMode"):!1;i.className="emoticon-selector chat-menu ffz-ui-popup",r.className="emoticon-selector-box dropmenu",i.appendChild(r),i.classList.toggle("dark",a),s.className="menu clearfix",r.appendChild(s);var l=document.createElement("li");l.className="title",l.innerHTML=""+(o.DEBUG?"[DEV] ":"")+"FrankerFaceZ ",s.appendChild(l);var c=document.createElement("div");c.className="ffz-ui-menu-page",r.appendChild(c);var u=[];for(var d in n.menu_pages)if(n.menu_pages.hasOwnProperty(d)){var h=n.menu_pages[d];h&&(!h.hasOwnProperty("visible")||h.visible&&("function"!=typeof h.visible||h.visible.bind(this)()))&&u.push([h.sort_order||0,d,h])}u.sort(function(e,t){if(e[0]t[0])return-1;var n=e[1].toLowerCase(),o=t[1].toLowerCase();return o>n?1:n>o?-1:0});for(var m=0;m0){i=!0;break}}e.classList.toggle("no-emotes",!i),e.classList.toggle("live",a),e.classList.toggle("dark",r),e.classList.toggle("blue",s)}}},{"../constants":3}],21:[function(t){var n=e.FrankerFaceZ,o=t("../constants"),i="http://static-cdn.jtvnw.net/emoticons/v1/",r={"00000turbo":!0},s=function(e){var t=App.__container__.lookup("controller:chat"),n=t.get("currentRoom.id"),o=e.rooms[n],i=o?o.room.tmiSession:null,r=i&&i._emotesParser&&i._emotesParser.emoticonSetIds||"0",s=e.get_user(),a=s&&e.users[s.login]&&e.users[s.login].sets||[];return r=r.split(",").removeObject("0"),[r,a]};n.prototype.setup_my_emotes=function(){if(this._twitch_emote_sets={},this._twitch_set_to_channel={},localStorage.ffzTwitchSets)try{this._twitch_set_to_channel=JSON.parse(localStorage.ffzTwitchSets)}catch(e){}},n.menu_pages.my_emotes={name:"My Emoticons",icon:o.EMOTE,visible:function(){var e=s(this);return e[0].length>0||e[1].length>0},render:function(e,t){var o=s(this),a=this;new RSVP.Promise(function(e){for(var t=[],i=0;i0?(a.ws_send("twitch_sets",t,function(e,n){if(t=[],e){for(var o in n)n.hasOwnProperty(o)&&(a._twitch_set_to_channel[o]=n[o],s(o,n[o]));localStorage.ffzTwitchSets=JSON.stringify(a._twitch_set_to_channel)}l()}),setTimeout(function(){t.length&&l()},5e3)):l()})]).then(function(){for(var t={},n=0;nt[0])return 1;var n=e[1].toLowerCase(),o=t[1].toLowerCase();return"twitch turbo"===n&&(n="zzz"+n),"twitch turbo"===o&&(o="zzz"+o),o>n?-1:n>o?1:0});for(var c=0;c'+l.source+""+n.get_capitalization(l.channel),l.badge&&(u.style.backgroundImage='url("'+l.badge+'")'),d.className="emoticon-grid",d.appendChild(u);for(var h=0;hSpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},n.ws_on_close.push(function(){var e=App.__container__.lookup("controller:channel"),t=e.get("id"),n=!1;for(var o in this.srl_races)delete this.srl_races[o],o==t&&(n=!0);n&&this.rebuild_race_ui()}),n.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),n=t.get("id"),o=!1,i=0;i',s.addEventListener("click",this.build_race_popup.bind(this)),r.appendChild(s),i.appendChild(r),this._update_race(!0)},n.prototype._race_kill=function(){this._race_timer&&(clearTimeout(this._race_timer),delete this._race_timer),delete this._race_game,delete this._race_goal},n.prototype.build_race_popup=function(){var e=this._popup;if(!e||(e.parentElement.removeChild(e),delete this._popup,this._popup_kill&&this._popup_kill(),delete this._popup_kill,"ffz-race-popup"!=e.id)){var t=document.querySelector("#ffz-ui-race");if(t){var o=t.querySelector(".button"),i=o.offsetLeft+o.offsetWidth,r=t.getAttribute("data-channel"),s=this.srl_races[r],e=document.createElement("div"),a="";e.id="ffz-race-popup",e.className=(i>=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var l="http://kadgar.net/live",c=!1;for(var u in s.entrants){var d=s.entrants[u].state;s.entrants.hasOwnProperty(u)&&s.entrants[u].channel&&("racing"==d||"entered"==d)&&(l+="/"+s.entrants[u].channel,c=!0)}var h=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,m=App.__container__.lookup("controller:channel"),p=m?m.get("display_name"):n.get_capitalization(r),f=encodeURIComponent("I'm watching "+p+" race "+s.goal+" in "+s.game+" on SpeedRunsLive!");a='',a+='",a+='
',a+='',a+='SRL ',c&&(a+=' Multitwitch '),a+="
",e.innerHTML=a,t.appendChild(e),this._update_race(!0)}}},n.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var n=t.getAttribute("data-channel"),i=this.srl_races[n];if(!i)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var r=i.twitch_entrants[n],s=i.entrants[r],a=t.querySelector("#ffz-race-popup"),l=Date.now()/1e3,c=Math.floor(l-i.time);if(t.querySelector(".logo").innerHTML=o.placement(s),a){var u=a.querySelector("tbody"),d=a.querySelector(".heading span"),h=a.querySelector(".heading div");u.innerHTML="";var m=[],p=!0;for(var f in i.entrants)i.entrants.hasOwnProperty(f)&&("racing"==i.entrants[f].state&&(p=!1),m.push(i.entrants[f]));m.sort(function(e,t){var n=e.place||9999,o=t.place||9999,i=e.time||c,r=t.time||c;return("forfeit"==e.state||"dq"==e.state)&&(n=1e4),("forfeit"==t.state||"dq"==t.state)&&(o=1e4),o>n?-1:n>o?1:e.namet.name?1:r>i?-1:i>r?1:void 0});for(var _=0;_'+f.display_name+"",v=f.channel?' ':"",b=f.hitbox?' ':"",y=c?o.time_to_string(f.time||c):"",w=o.place_string(f.place),F=f.comment?o.sanitize(f.comment):"";u.innerHTML+="'+w+" "+g+" "+v+b+' '+("forfeit"==f.state?"Forfeit":y)+" "}if(this._race_game!=i.game||this._race_goal!=i.goal){this._race_game=i.game,this._race_goal=i.goal;var k=o.sanitize(i.game),E=o.sanitize(i.goal);h.innerHTML=''+k+" Goal: "+E}c?p?d.innerHTML="Done":(d.innerHTML=o.time_to_string(c),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):d.innerHTML="Entry Open"}}}},{"../utils":26}],24:[function(t){var n=e.FrankerFaceZ,o=t("../constants");n.prototype.setup_css=function(){this.log("Injecting main FrankerFaceZ CSS.");var e=this._main_style=document.createElement("link");e.id="ffz-ui-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",o.SERVER+"script/style.css?_="+Date.now()),document.head.appendChild(e),jQuery.noty.themes.ffzTheme={name:"ffzTheme",style:function(){this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type)},callback:{onShow:function(){},onClose:function(){}}}}},{"../constants":3}],25:[function(t){var n=e.FrankerFaceZ,o=t("../constants"),i=t("../utils");n.ws_commands.viewers=function(e){var t=e[0],n=e[1],r=App.__container__.lookup("controller:channel"),s=r&&r.get&&r.get("id");if(s===t){var a=document.querySelector(".channel-stats .ffz.stat"),l=o.ZREKNARF+" "+i.number_commas(n);if(a)a.innerHTML=l;else{var c=document.querySelector(".channel-stats");if(!c)return;a=document.createElement("span"),a.className="ffz stat",a.title="Viewers with FrankerFaceZ",a.innerHTML=l,c.appendChild(a),jQuery(a).tipsy()}}}},{"../constants":3,"../utils":26}],26:[function(t,n){var o=(e.FrankerFaceZ,t("./constants"),{}),i=document.createElement("span"),r=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"},s=function(e,t){t=0===t?0:t||1,t=Math.round(255*-(t/100));var n=Math.max(0,Math.min(255,e[0]-t)),o=Math.max(0,Math.min(255,e[1]-t)),i=Math.max(0,Math.min(255,e[2]-t));return[n,o,i]},a=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},l=function(e,t){return t=0===t?0:t||1,s(e,-t)},c=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;ts;(l||n)&&(l&&(o=o.substr(0,s)+o.substr(a+r.length)),n&&(o+=i+n+r),e.innerHTML=o)},get_luminance:c,brighten:s,darken:l,rgb_to_css:a,number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:r,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?r(e.place):""},sanitize:function(e){var t=o[e];return t||(i.textContent=e,t=o[e]=i.innerHTML,i.innerHTML=""),t},time_to_string:function(e){var t=e%60,n=Math.floor(e/60),o=Math.floor(n/60);return n%=60,(10>o?"0":"")+o+":"+(10>n?"0":"")+n+":"+(10>t?"0":"")+t}}},{"./constants":3}]},{},[13]),e.ffz=new FrankerFaceZ}(window);
\ No newline at end of file
diff --git a/src/constants.js b/src/constants.js
index 295da7bf..5dbf3598 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -10,5 +10,6 @@ module.exports = {
CHAT_BUTTON: '' + SVGPATH + ' ',
GEAR: ' ',
- HEART: ' '
+ HEART: ' ',
+ EMOTE: ' '
}
\ No newline at end of file
diff --git a/src/debug.js b/src/debug.js
index b7aadf46..355c87e9 100644
--- a/src/debug.js
+++ b/src/debug.js
@@ -2,9 +2,25 @@ var FFZ = window.FrankerFaceZ;
// -----------------------
-// Developer Mode Command
+// Developer Mode
// -----------------------
+FFZ.settings_info.developer_mode = {
+ type: "boolean",
+ value: false,
+ storage_key: "ffzDebugMode",
+
+ visible: function() { return this.settings.developer_mode || (Date.now() - parseInt(localStorage.ffzLastDevMode || "0")) < 604800000; },
+ category: "Debugging",
+ name: "Developer Mode",
+ help: "Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",
+
+ on_update: function() {
+ localStorage.ffzLastDevMode = Date.now();
+ }
+ };
+
+
FFZ.ffz_commands.developer_mode = function(room, args) {
var enabled, args = args && args.length ? args[0].toLowerCase() : null;
if ( args == "y" || args == "yes" || args == "true" || args == "on" )
@@ -13,9 +29,9 @@ FFZ.ffz_commands.developer_mode = function(room, args) {
enabled = false;
if ( enabled === undefined )
- return "Developer Mode is currently " + (localStorage.ffzDebugMode == "true" ? "enabled." : "disabled.");
+ return "Developer Mode is currently " + (this.settings.developer_mode ? "enabled." : "disabled.");
- localStorage.ffzDebugMode = enabled;
+ this.settings.set("developer_mode", enabled);
return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser.";
}
diff --git a/src/ember/chatview.js b/src/ember/chatview.js
index 2068d173..f1d9377e 100644
--- a/src/ember/chatview.js
+++ b/src/ember/chatview.js
@@ -13,7 +13,10 @@ FFZ.prototype.setup_chatview = function() {
// For some reason, this doesn't work unless we create an instance of the
// chat view and then destroy it immediately.
- Chat.create().destroy();
+ try {
+ Chat.create().destroy();
+ } catch(err) { }
+
// Modify all existing Chat views.
for(var key in Ember.View.views) {
diff --git a/src/ember/line.js b/src/ember/line.js
index bac5479f..b52d42c4 100644
--- a/src/ember/line.js
+++ b/src/ember/line.js
@@ -3,6 +3,80 @@ var FFZ = window.FrankerFaceZ,
reg_escape = function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ },
+
+ SEPARATORS = "[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]",
+ SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"),
+
+ quote_attr = function(attr) {
+ return (attr + '')
+ .replace(/&/g, "&")
+ .replace(/'/g, "'")
+ .replace(/"/g, """)
+ .replace(//g, ">");
+ },
+
+
+ TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/",
+ build_srcset = function(id) {
+ return TWITCH_BASE + id + "/1.0 1x, " + TWITCH_BASE + id + "/2.0 2x, " + TWITCH_BASE + id + "/3.0 4x";
+ },
+
+
+ data_to_tooltip = function(data) {
+ var output = "Emoticon " + data.code + " ",
+ set = data.set,
+ set_type = data.set_type;
+
+ if ( set_type === undefined )
+ set_type = "Channel";
+
+ if ( ! set )
+ return data.code;
+
+ else if ( set == "00000turbo" || set == "turbo" ) {
+ set = "Twitch Turbo";
+ set_type = null;
+ }
+
+ if ( ! set_type )
+ output += '' + set + ' ';
+ else
+ output += "" + set_type + " " + set + " ";
+
+ return '';
+ },
+
+ build_tooltip = function(id) {
+ var emote_data = this._twitch_emotes[id],
+ set = emote_data ? emote_data.set : null;
+
+ if ( ! emote_data )
+ return "???";
+
+ if ( typeof emote_data == "string" )
+ return emote_data;
+
+ if ( emote_data.tooltip )
+ return emote_data.tooltip;
+
+ return emote_data.tooltip = data_to_tooltip(emote_data);
+ },
+
+ load_emote_data = function(id, code, success, data) {
+ if ( ! success )
+ return;
+
+ if ( code )
+ data.code = code;
+
+ this._twitch_emotes[id] = data;
+ var tooltip = build_tooltip.bind(this)(id);
+
+ var images = document.querySelectorAll('img[emote-id="' + id + '"]');
+ for(var x=0; x < images.length; x++)
+ images[x].title = tooltip;
};
@@ -22,6 +96,37 @@ FFZ.settings_info.capitalize = {
};
+FFZ.settings_info.banned_words = {
+ type: "button",
+ value: [],
+
+ category: "Chat",
+ visible: function() { return ! this.has_bttv },
+
+ name: "Banned Words",
+ help: "Set a list of words that will be locally removed from chat messages.",
+
+ method: function() {
+ var old_val = this.settings.banned_words.join(", "),
+ new_val = prompt("Banned Words\n\nPlease enter a comma-separated list of words that you would like to be removed from chat messages.", old_val);
+
+ if ( new_val === null || new_val === undefined )
+ return;
+
+ new_val = new_val.trim().split(SPLITTER);
+ var vals = [];
+
+ for(var i=0; i < new_val.length; i++)
+ new_val[i] && vals.push(new_val[i]);
+
+ if ( vals.length == 1 && vals[0] == "disable" )
+ vals = [];
+
+ this.settings.set("banned_words", vals);
+ }
+ };
+
+
FFZ.settings_info.keywords = {
type: "button",
value: [],
@@ -40,12 +145,16 @@ FFZ.settings_info.keywords = {
return;
// Split them up.
- new_val = new_val.trim().split(/\W*,\W*/);
+ new_val = new_val.trim().split(SPLITTER);
+ var vals = [];
- if ( new_val.length == 1 && (new_val[0] == "" || new_val[0] == "disable") )
- new_val = [];
+ for(var i=0; i < new_val.length; i++)
+ new_val[i] && vals.push(new_val[i]);
- this.settings.set("keywords", new_val);
+ if ( vals.length == 1 && vals[0] == "disable" )
+ vals = [];
+
+ this.settings.set("keywords", vals);
}
};
@@ -106,6 +215,10 @@ FFZ.prototype.setup_line = function() {
document.head.appendChild(s);
+ // Emoticon Data
+ this._twitch_emotes = {};
+
+
this.log("Hooking the Ember Line controller.");
var Line = App.__container__.resolve('controller:line'),
@@ -117,6 +230,7 @@ FFZ.prototype.setup_line = function() {
var tokens = this._super();
try {
+ tokens = f._remove_banned(tokens);
tokens = f._emoticonize(this, tokens);
var user = f.get_user();
@@ -143,11 +257,12 @@ FFZ.prototype.setup_line = function() {
this._super();
try {
var el = this.get('element'),
- user = this.get('context.model.from'),
- room = this.get('context.parentController.content.id'),
- color = this.get('context.model.color'),
+ controller = this.get('context'),
+ user = controller.get('model.from'),
+ room = controller.get('parentController.content.id'),
+ color = controller.get('model.color'),
- row_type = this.get('context.model.ffz_alternate');
+ row_type = controller.get('model.ffz_alternate');
// Color Processing
@@ -210,6 +325,107 @@ FFZ.prototype.setup_line = function() {
// Mark that we've checked this message for mentions.
this.set('context.model.ffz_notified', true);
+
+ // Banned Links
+ var bad_links = el.querySelectorAll('a.deleted-link');
+ for(var i=0; i < bad_links.length; i++) {
+ var link = bad_links[i];
+
+ link.addEventListener("click", function(e) {
+ if ( ! this.classList.contains("deleted-link") )
+ return true;
+
+ // Get the URL
+ var href = this.getAttribute('data-url'),
+ link = href;
+
+ // Delete Old Stuff
+ this.classList.remove('deleted-link');
+ this.removeAttribute("data-url");
+ this.removeAttribute("title");
+ this.removeAttribute("original-title");
+
+ // Process URL
+ if ( href.indexOf("@") > -1 && (-1 === href.indexOf("/") || href.indexOf("@") < href.indexOf("/")) )
+ href = "mailto:" + href;
+ else if ( ! href.match(/^https?:\/\//) )
+ href = "http://" + href;
+
+ // Set up the Link
+ this.href = href;
+ this.target = "_new";
+ this.textContent = link;
+
+ // Stop from Navigating
+ e.preventDefault();
+ });
+
+ // Also add a nice tooltip.
+ jQuery(link).tipsy();
+ }
+
+
+ // Enhanced Emotes
+ var images = el.querySelectorAll('img');
+ for(var i=0; i < images.length; i++) {
+ var img = images[i],
+ name = img.alt,
+ match = /\/emoticons\/v1\/(\d+)\/1\.0/.exec(img.src),
+ id = match ? parseInt(match[1]) : null;
+
+ if ( id !== null ) {
+ // High-DPI Images
+ img.setAttribute('srcset', build_srcset(id));
+ img.setAttribute('emote-id', id);
+
+ // Source Lookup
+ var emote_data = f._twitch_emotes[id];
+ if ( emote_data ) {
+ if ( typeof emote_data != "string" )
+ img.title = emote_data.tooltip;
+
+ } else {
+ f._twitch_emotes[id] = img.alt;
+ f.ws_send("twitch_emote", id, load_emote_data.bind(f, id, img.alt));
+ }
+
+ jQuery(img).tipsy({html:true});
+
+ } else if ( img.getAttribute('data-ffz-emote') ) {
+ var data = JSON.parse(decodeURIComponent(img.getAttribute('data-ffz-emote'))),
+ id = data && data[0] || null,
+ set_id = data && data[1] || null,
+
+ set = f.emote_sets[set_id],
+ emote = set ? set.emotes[id] : null,
+
+ set_name = set.id,
+ set_type = "FFZ Channel";
+
+ if ( set.id == "global" ) {
+ set_name = "FrankerFaceZ Global";
+ set_type = null;
+
+ } else if ( set.id == "globalevent" ) {
+ set_name = "FrankerFaceZ Event";
+ set_type = null;
+
+ } else if ( f.feature_friday && set.id == f.feature_friday.set )
+ set_name = "Feature Friday - " + f.feature_friday.channel;
+
+ img.title = data_to_tooltip({
+ code: emote.hidden ? "???" : emote.name,
+ set: set_name,
+ set_type: set_type
+ });
+
+ jQuery(img).tipsy({html:true});
+
+ } else
+ jQuery(img).tipsy();
+ }
+
+
} catch(err) {
try {
f.error("LineView didInsertElement: " + err);
@@ -288,7 +504,6 @@ FFZ.prototype._handle_color = function(color) {
}
-
// ---------------------
// Capitalization
// ---------------------
@@ -298,7 +513,7 @@ FFZ._cap_fetching = 0;
FFZ.get_capitalization = function(name, callback) {
// Use the BTTV code if it's present.
- if ( window.BetterTTV )
+ if ( window.BetterTTV && BetterTTV.chat && BetterTTV.chat.helpers.lookupDisplayName )
return BetterTTV.chat.helpers.lookupDisplayName(name);
if ( ! name )
@@ -314,15 +529,14 @@ FFZ.get_capitalization = function(name, callback) {
return old_data[0];
}
- if ( FFZ._cap_fetching < 5 ) {
+ if ( FFZ._cap_fetching < 25 ) {
FFZ._cap_fetching++;
- Twitch.api.get("users/" + name)
- .always(function(data) {
- var cap_name = data.display_name || name;
- FFZ.capitalization[name] = [cap_name, Date.now()];
- FFZ._cap_fetching--;
- typeof callback === "function" && callback(cap_name);
- });
+ FFZ.get().ws_send("get_display_name", name, function(success, data) {
+ var cap_name = success ? data : name;
+ FFZ.capitalization[name] = [cap_name, Date.now()];
+ FFZ._cap_fetching--;
+ typeof callback === "function" && callback(cap_name);
+ });
}
return old_data ? old_data[0] : name;
@@ -331,7 +545,7 @@ FFZ.get_capitalization = function(name, callback) {
FFZ.prototype.capitalize = function(view, user) {
var name = FFZ.get_capitalization(user, this.capitalize.bind(this, view));
- if ( name )
+ if ( name && view )
view.$('.from').text(name);
}
@@ -346,8 +560,21 @@ FFZ._get_regex = function(word) {
return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig");
}
-FFZ._mentions_to_regex = function(list) {
- return FFZ._regex_cache[list] = FFZ._regex_cache[list] || RegExp("\\b(?:" + _.chain(list).map(reg_escape).value().join("|") + ")\\b", "ig");
+FFZ._words_to_regex = function(list) {
+ var regex = FFZ._regex_cache[list];
+ if ( ! regex ) {
+ var reg = "";
+ for(var i=0; i < list.length; i++) {
+ if ( ! list[i] )
+ continue;
+
+ reg += (reg ? "|" : "") + reg_escape(list[i]);
+ }
+
+ regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig");
+ }
+
+ return regex;
}
@@ -359,24 +586,72 @@ FFZ.prototype._mentionize = function(controller, tokens) {
if ( typeof tokens == "string" )
tokens = [tokens];
- var regex = FFZ._mentions_to_regex(mention_words);
+ var regex = FFZ._words_to_regex(mention_words),
+ new_tokens = [];
- return _.chain(tokens).map(function(token) {
- if ( !_.isString(token) )
- return token;
- else if ( !token.match(regex) )
- return [token];
+ for(var i=0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if ( ! _.isString(token) ) {
+ new_tokens.push(token);
+ continue;
+ }
- return _.zip(
- _.map(token.split(regex), _.identity),
- _.map(token.match(regex), function(e) {
- return {
- mentionedUser: e,
- own: false
- };
- })
- );
- }).flatten().compact().value();
+ if ( ! token.match(regex) ) {
+ new_tokens.push(token);
+ continue;
+ }
+
+ token = token.replace(regex, function(all, prefix, match) {
+ new_tokens.push(prefix);
+ new_tokens.push({
+ mentionedUser: match,
+ own: false
+ });
+
+ return "";
+ });
+
+ if ( token )
+ new_tokens.push(token);
+ }
+
+ return new_tokens;
+}
+
+
+// ---------------------
+// Banned Words
+// ---------------------
+
+FFZ.prototype._remove_banned = function(tokens) {
+ var banned_words = this.settings.banned_words;
+ if ( ! banned_words || ! banned_words.length )
+ return tokens;
+
+ if ( typeof tokens == "string" )
+ tokens = [tokens];
+
+ var regex = FFZ._words_to_regex(banned_words),
+ new_tokens = [];
+
+ for(var i=0; i < tokens.length; i++) {
+ var token = tokens[i];
+ if ( ! _.isString(token ) ) {
+ if ( token.emoticonSrc && regex.test(token.altText) )
+ new_tokens.push(token.altText.replace(regex, "$1***"));
+ else if ( token.isLink && regex.test(token.href) )
+ new_tokens.push({
+ mentionedUser: ' <banned link> ',
+ own: true
+ });
+ else
+ new_tokens.push(token);
+
+ } else
+ new_tokens.push(token.replace(regex, "$1***"));
+ }
+
+ return new_tokens;
}
@@ -421,7 +696,7 @@ FFZ.prototype._emoticonize = function(controller, tokens) {
// emoticon.
_.each(emotes, function(emote) {
//var eo = {isEmoticon:true, cls: emote.klass};
- var eo = {isEmoticon:true, cls: emote.klass, emoticonSrc: emote.url, altText: (emote.hidden ? "???" : emote.name)};
+ var eo = {isEmoticon:true, cls: emote.klass, emoticonSrc: emote.url + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), altText: (emote.hidden ? "???" : emote.name)};
tokens = _.compact(_.flatten(_.map(tokens, function(token) {
if ( _.isObject(token) )
diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js
index af74e27b..cb5d6412 100644
--- a/src/ember/moderation-card.js
+++ b/src/ember/moderation-card.js
@@ -31,7 +31,7 @@ FFZ.settings_info.enhanced_moderation = {
category: "Chat",
name: "Enhanced Moderation",
- help: "Use /p, /t, /u and /b in chat to moderator, or use hotkeys with moderation cards."
+ help: "Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards."
};
@@ -236,7 +236,7 @@ FFZ.chat_commands.b.enabled = function() { return this.settings.enhanced_moderat
FFZ.chat_commands.u = function(room, args) {
if ( ! args || ! args.length )
- return "Unban Usage: /b username [more usernames separated by spaces]";
+ return "Unban Usage: /u username [more usernames separated by spaces]";
if ( args.length > 10 )
return "Please only unban up to 10 users at once.";
diff --git a/src/emoticons.js b/src/emoticons.js
index eaf37801..07d811d5 100644
--- a/src/emoticons.js
+++ b/src/emoticons.js
@@ -179,7 +179,7 @@ FFZ.prototype._legacy_load_css = function(set_id, callback, data) {
margins = check_margins(margins, height);
var hidden = path.substr(path.lastIndexOf("/") + 1, 1) === ".",
id = ++f._last_emote_id,
- emote = {id: id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra};
+ emote = {id: id, set_id: set_id, hidden: hidden, name: name, height: height, width: width, url: path, margins: margins, extra_css: extra};
emotes[id] = emote;
return "";
diff --git a/src/main.js b/src/main.js
index 15ef7fb2..98ce3dab 100644
--- a/src/main.js
+++ b/src/main.js
@@ -22,7 +22,7 @@ FFZ.get = function() { return FFZ.instance; }
// Version
var VER = FFZ.version_info = {
- major: 3, minor: 1, revision: 0,
+ major: 3, minor: 2, revision: 1,
toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
}
@@ -105,9 +105,12 @@ FFZ.prototype.get_user = function() {
//require('./templates');
+// Import these first to set up data structures
+require('./ui/menu');
require('./settings');
-
require('./socket');
+
+
require('./emoticons');
require('./badges');
@@ -134,8 +137,9 @@ require('./ui/notifications');
require('./ui/viewer_count');
require('./ui/menu_button');
-require('./ui/menu');
require('./ui/races');
+require('./ui/my_emotes');
+require('./ui/about_page');
require('./commands');
@@ -198,6 +202,7 @@ FFZ.prototype.setup_ember = function(delay) {
this.setup_notifications();
this.setup_css();
this.setup_menu();
+ this.setup_my_emotes();
this.setup_races();
this.find_bttv(10);
diff --git a/src/settings.js b/src/settings.js
index 286684d4..3019720e 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -1,8 +1,15 @@
var FFZ = window.FrankerFaceZ,
+ constants = require("./constants");
make_ls = function(key) {
return "ffz_setting_" + key;
+ },
+
+ toggle_setting = function(swit, key) {
+ var val = ! this.settings.get(key);
+ this.settings.set(key, val);
+ swit.classList.toggle('active', val);
};
@@ -19,8 +26,11 @@ FFZ.prototype.load_settings = function() {
this.settings = {};
for(var key in FFZ.settings_info) {
- var ls_key = make_ls(key),
- info = FFZ.settings_info[key],
+ if ( ! FFZ.settings_info.hasOwnProperty(key) )
+ continue;
+
+ var info = FFZ.settings_info[key],
+ ls_key = info.storage_key || make_ls(key),
val = info.hasOwnProperty("value") ? info.value : undefined;
if ( localStorage.hasOwnProperty(ls_key) ) {
@@ -40,10 +50,143 @@ FFZ.prototype.load_settings = function() {
this.settings.del = this._setting_del.bind(this);
// Listen for Changes
- window.addEventListener("storage", this._setting_update.bind(this));
+ window.addEventListener("storage", this._setting_update.bind(this), false);
}
+// --------------------
+// Menu Page
+// --------------------
+
+FFZ.menu_pages.settings = {
+ render: function(view, container) {
+ var settings = {},
+ categories = [];
+ for(var key in FFZ.settings_info) {
+ if ( ! FFZ.settings_info.hasOwnProperty(key) )
+ continue;
+
+ var info = FFZ.settings_info[key],
+ cat = info.category || "Miscellaneous",
+ cs = settings[cat];
+
+ if ( info.visible !== undefined && info.visible !== null ) {
+ var visible = info.visible;
+ if ( typeof info.visible == "function" )
+ visible = info.visible.bind(this)();
+
+ if ( ! visible )
+ continue;
+ }
+
+ if ( ! cs ) {
+ categories.push(cat);
+ cs = settings[cat] = [];
+ }
+
+ cs.push([key, info]);
+ }
+
+ categories.sort(function(a,b) {
+ var a = a.toLowerCase(),
+ b = b.toLowerCase();
+
+ if ( a === "Debugging" )
+ a = "zzz" + a;
+
+ if ( b === "Debugging" )
+ b = "zzz" + b;
+
+ if ( a < b ) return -1;
+ else if ( a > b ) return 1;
+ return 0;
+ });
+
+ for(var ci=0; ci < categories.length; ci++) {
+ var category = categories[ci],
+ cset = settings[category],
+
+ menu = document.createElement('div'),
+ heading = document.createElement('div');
+
+ heading.className = 'heading';
+ menu.className = 'chat-menu-content';
+ heading.innerHTML = category;
+ menu.appendChild(heading);
+
+ cset.sort(function(a,b) {
+ var a = a[1],
+ b = b[1],
+
+ at = a.type,
+ bt = b.type,
+
+ an = a.name.toLowerCase(),
+ bn = b.name.toLowerCase();
+
+ if ( at < bt ) return -1;
+ else if ( at > bt ) return 1;
+
+ else if ( an < bn ) return -1;
+ else if ( an > bn ) return 1;
+
+ return 0;
+ });
+
+ for(var i=0; i < cset.length; i++) {
+ var key = cset[i][0],
+ info = cset[i][1],
+ el = document.createElement('p'),
+ val = this.settings.get(key);
+
+ el.className = 'clearfix';
+
+ if ( info.type == "boolean" ) {
+ var swit = document.createElement('a'),
+ label = document.createElement('span');
+
+ swit.className = 'switch';
+ swit.classList.toggle('active', val);
+ swit.innerHTML = " ";
+
+ label.className = 'switch-label';
+ label.innerHTML = info.name;
+
+ el.appendChild(swit);
+ el.appendChild(label);
+
+ swit.addEventListener("click", toggle_setting.bind(this, swit, key));
+
+ } else {
+ el.classList.add("option");
+ var link = document.createElement('a');
+ link.innerHTML = info.name;
+ link.href = "#";
+ el.appendChild(link);
+
+ link.addEventListener("click", info.method.bind(this));
+ }
+
+ if ( info.help ) {
+ var help = document.createElement('span');
+ help.className = 'help';
+ help.innerHTML = info.help;
+ el.appendChild(help);
+ }
+
+ menu.appendChild(el);
+ }
+
+ container.appendChild(menu);
+ }
+ },
+
+ name: "Settings",
+ icon: constants.GEAR,
+ sort_order: 99999
+ };
+
+
// --------------------
// Tracking Updates
// --------------------
@@ -62,6 +205,22 @@ FFZ.prototype._setting_update = function(e) {
val = undefined,
info = FFZ.settings_info[key];
+ if ( ! info ) {
+ // Try iterating to find the key.
+ for(key in FFZ.settings_info) {
+ if ( ! FFZ.settings_info.hasOwnProperty(key) )
+ continue;
+
+ info = FFZ.settings_info[key];
+ if ( info.storage_key == ls_key )
+ break;
+ }
+
+ // Not us.
+ if ( info.storage_key != ls_key )
+ return;
+ }
+
this.log("Updated Setting: " + key);
try {
@@ -92,8 +251,8 @@ FFZ.prototype._setting_get = function(key) {
FFZ.prototype._setting_set = function(key, val) {
- var ls_key = make_ls(key),
- info = FFZ.settings_info[key],
+ var info = FFZ.settings_info[key],
+ ls_key = info.storage_key || make_ls(key),
jval = JSON.stringify(val);
this.settings[key] = val;
@@ -111,8 +270,8 @@ FFZ.prototype._setting_set = function(key, val) {
FFZ.prototype._setting_del = function(key) {
- var ls_key = make_ls(key),
- info = FFZ.settings_info[key],
+ var info = FFZ.settings_info[key],
+ ls_key = info.storage_key || make_ls(key),
val = undefined;
if ( localStorage.hasOwnProperty(ls_key) )
diff --git a/src/socket.js b/src/socket.js
index 406c99fd..ab9c9f9d 100644
--- a/src/socket.js
+++ b/src/socket.js
@@ -38,7 +38,7 @@ FFZ.prototype.ws_create = function() {
// Send the current rooms.
for(var room_id in f.rooms)
- f.ws_send("sub", room_id);
+ f.rooms.hasOwnProperty(room_id) && f.ws_send("sub", room_id);
// Send any pending commands.
var pending = f._ws_pending;
@@ -64,8 +64,11 @@ FFZ.prototype.ws_create = function() {
}
// We never ever want to not have a socket.
- if ( f._ws_delay < 30000 )
+ if ( f._ws_delay < 60000 )
f._ws_delay += 5000;
+ else
+ // Randomize delay.
+ f._ws_delay = (Math.floor(Math.random()*60)+30)*1000;
setTimeout(f.ws_create.bind(f), f._ws_delay);
}
@@ -124,4 +127,32 @@ FFZ.prototype.ws_send = function(func, data, callback, can_wait) {
this._ws_sock.send(request + " " + func + data);
return request;
+}
+
+// ----------------
+// Authorization
+// ----------------
+
+FFZ.ws_commands.do_authorize = function(data) {
+ // Try finding a channel we can send on.
+ var conn;
+ for(var room_id in this.rooms) {
+ if ( ! this.rooms.hasOwnProperty(room_id) )
+ continue;
+
+ var r = this.rooms[room_id];
+ if ( r && r.room && !r.room.get('roomProperties.eventchat') && !r.room.get('isGroupRoom') && r.room.tmiRoom ) {
+ var c = r.room.tmiRoom._getConnection();
+ if ( c.isConnected ) {
+ conn = c;
+ break;
+ }
+ }
+ }
+
+ if ( conn )
+ conn._send("PRIVMSG #frankerfacezauthorizer :AUTH " + data);
+ else
+ // Try again shortly.
+ setTimeout(FFZ.ws_commands.do_authorize.bind(this, data), 5000);
}
\ No newline at end of file
diff --git a/src/ui/about_page.js b/src/ui/about_page.js
new file mode 100644
index 00000000..398ed2a6
--- /dev/null
+++ b/src/ui/about_page.js
@@ -0,0 +1,95 @@
+var FFZ = window.FrankerFaceZ,
+ constants = require("../constants");
+
+
+// -------------------
+// About Page
+// -------------------
+
+FFZ.menu_pages.about = {
+ name: "About FrankerFaceZ",
+ icon: constants.HEART,
+ sort_order: 998,
+
+ render: function(view, container) {
+ var room = this.rooms[view.get("context.currentRoom.id")],
+ has_emotes = false, f = this;
+
+ // Check for emoticons.
+ if ( room && room.sets.length ) {
+ for(var i=0; i < room.sets.length; i++) {
+ var set = this.emote_sets[room.sets[i]];
+ if ( set && set.count > 0 ) {
+ has_emotes = true;
+ break;
+ }
+ }
+ }
+
+ // Heading
+ var heading = document.createElement('div'),
+ content = '';
+
+ content += "FrankerFaceZ ";
+ content += 'new ways to woof
';
+
+ heading.className = 'chat-menu-content center';
+ heading.innerHTML = content;
+ container.appendChild(heading);
+
+
+ // Advertising
+ var btn_container = document.createElement('div'),
+ ad_button = document.createElement('a'),
+ message = "To use custom emoticons in " + (has_emotes ? "this channel" : "tons of channels") + ", get FrankerFaceZ from http://www.frankerfacez.com";
+
+ ad_button.className = 'button primary';
+ ad_button.innerHTML = "Advertise in Chat";
+ ad_button.addEventListener('click', this._add_emote.bind(this, view, message));
+
+ btn_container.appendChild(ad_button);
+
+ // Donate
+ var donate_button = document.createElement('a');
+
+ donate_button.className = 'button ffz-donate';
+ donate_button.href = "http://www.frankerfacez.com/donate.html";
+ donate_button.target = "_new";
+ donate_button.innerHTML = "Donate";
+
+ btn_container.appendChild(donate_button);
+ btn_container.className = 'chat-menu-content center';
+ container.appendChild(btn_container);
+
+ // Credits
+ var credits = document.createElement('div');
+
+ content = '';
+ content += 'Developers ';
+ content += 'Dan Salvato ';
+ content += 'Stendec ';
+
+ content += 'Version ' + FFZ.version_info + ' Logs ';
+
+ credits.className = 'chat-menu-content center';
+ credits.innerHTML = content;
+
+ // Make the Logs button functional.
+ var getting_logs = false;
+ credits.querySelector('#ffz-debug-logs').addEventListener('click', function() {
+ if ( getting_logs )
+ return;
+
+ getting_logs = true;
+ f._pastebin(f._log_data.join("\n"), function(url) {
+ getting_logs = false;
+ if ( ! url )
+ alert("There was an error uploading the FrankerFaceZ logs.");
+ else
+ prompt("Your FrankerFaceZ logs have been uploaded to the URL:", url);
+ });
+ });
+
+ container.appendChild(credits);
+ }
+}
\ No newline at end of file
diff --git a/src/ui/menu.js b/src/ui/menu.js
index 32d56173..bdd2ba57 100644
--- a/src/ui/menu.js
+++ b/src/ui/menu.js
@@ -60,23 +60,43 @@ FFZ.prototype.build_ui_popup = function(view) {
menu.className = 'menu clearfix';
inner.appendChild(menu);
- var el = document.createElement('li');
- el.className = 'title';
- el.innerHTML = "FrankerFaceZ ";
- menu.appendChild(el);
-
- el.addEventListener("click", this._add_emote.bind(this, view, "To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com"));
+ var heading = document.createElement('li');
+ heading.className = 'title';
+ heading.innerHTML = "" + (constants.DEBUG ? "[DEV] " : "") + "FrankerFaceZ ";
+ menu.appendChild(heading);
var sub_container = document.createElement('div');
sub_container.className = 'ffz-ui-menu-page';
inner.appendChild(sub_container);
+ var menu_pages = [];
for(var key in FFZ.menu_pages) {
+ if ( ! FFZ.menu_pages.hasOwnProperty(key) )
+ continue;
+
var page = FFZ.menu_pages[key];
if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)()))) )
continue;
- var el = document.createElement('li'),
+ menu_pages.push([page.sort_order || 0, key, page]);
+ }
+
+ menu_pages.sort(function(a,b) {
+ if ( a[0] < b[0] ) return 1;
+ else if ( a[0] > b[0] ) return -1;
+
+ var al = a[1].toLowerCase(),
+ bl = b[1].toLowerCase();
+
+ if ( al < bl ) return 1;
+ if ( al > bl ) return -1;
+ return 0;
+ });
+
+ for(var i=0; i < menu_pages.length; i++) {
+ var key = menu_pages[i][1],
+ page = menu_pages[i][2],
+ el = document.createElement('li'),
link = document.createElement('a');
el.className = 'item';
@@ -84,6 +104,8 @@ FFZ.prototype.build_ui_popup = function(view) {
link.title = page.name;
link.innerHTML = page.icon;
+ jQuery(link).tipsy();
+
link.addEventListener("click", this._ui_change_page.bind(this, view, menu, sub_container, key));
el.appendChild(link);
@@ -103,6 +125,7 @@ FFZ.prototype.build_ui_popup = function(view) {
FFZ.prototype._ui_change_page = function(view, menu, container, page) {
this._last_page = page;
container.innerHTML = "";
+ container.setAttribute('data-page', page);
var els = menu.querySelectorAll('li.active');
for(var i=0; i < els.length; i++)
@@ -118,137 +141,48 @@ FFZ.prototype._ui_change_page = function(view, menu, container, page) {
}
-// --------------------
-// Settings Page
-// --------------------
-
-FFZ.menu_pages.settings = {
- render: function(view, container) {
- var settings = {},
- categories = [];
- for(var key in FFZ.settings_info) {
- var info = FFZ.settings_info[key],
- cat = info.category || "Miscellaneous",
- cs = settings[cat];
-
- if ( info.visible !== undefined && info.visible !== null ) {
- var visible = info.visible;
- if ( typeof info.visible == "function" )
- visible = info.visible.bind(this)();
-
- if ( ! visible )
- continue;
- }
-
- if ( ! cs ) {
- categories.push(cat);
- cs = settings[cat] = [];
- }
-
- cs.push([key, info]);
- }
-
- categories.sort(function(a,b) {
- var a = a.toLowerCase(),
- b = b.toLowerCase();
-
- if ( a < b ) return -1;
- else if ( a > b ) return 1;
- return 0;
- });
-
- for(var ci=0; ci < categories.length; ci++) {
- var category = categories[ci],
- cset = settings[category],
-
- menu = document.createElement('div'),
- heading = document.createElement('div');
-
- heading.className = 'heading';
- menu.className = 'chat-menu-content';
- heading.innerHTML = category;
- menu.appendChild(heading);
-
- cset.sort(function(a,b) {
- var ai = a[1],
- bi = b[1],
-
- an = ai.name.toLowerCase(),
- bn = bi.name.toLowerCase();
-
- if ( an < bn ) return -1;
- else if ( an > bn ) return 1;
- return 0;
- });
-
-
- for(var i=0; i < cset.length; i++) {
- var key = cset[i][0],
- info = cset[i][1],
- el = document.createElement('p'),
- val = this.settings.get(key);
-
- el.className = 'clearfix';
-
- if ( info.type == "boolean" ) {
- var swit = document.createElement('a'),
- label = document.createElement('span');
-
- swit.className = 'switch';
- swit.classList.toggle('active', val);
- swit.innerHTML = " ";
-
- label.className = 'switch-label';
- label.innerHTML = info.name;
-
- el.appendChild(swit);
- el.appendChild(label);
-
- swit.addEventListener("click", this._ui_toggle_setting.bind(this, swit, key));
-
- } else {
- el.classList.add("option");
- var link = document.createElement('a');
- link.innerHTML = info.name;
- link.href = "#";
- el.appendChild(link);
-
- link.addEventListener("click", info.method.bind(this));
- }
-
- if ( info.help ) {
- var help = document.createElement('span');
- help.className = 'help';
- help.innerHTML = info.help;
- el.appendChild(help);
- }
-
- menu.appendChild(el);
- }
-
- container.appendChild(menu);
- }
- },
-
- name: "Settings",
- icon: constants.GEAR
- };
-
-
-FFZ.prototype._ui_toggle_setting = function(swit, key) {
- var val = ! this.settings.get(key);
- this.settings.set(key, val);
- swit.classList.toggle('active', val);
-}
-
-
// --------------------
// Favorites Page
// --------------------
+FFZ.prototype._tokenize_message = function(message, room_id) {
+ var lc = App.__container__.lookup('controller:line'),
+ rc = App.__container__.lookup('controller:room'),
+ room = this.rooms[room_id],
+ user = this.get_user();
+
+ if ( ! lc || ! rc || ! room )
+ return [message];
+
+ rc.set('model', room.room);
+ lc.set('parentController', rc);
+
+ var model = {
+ from: user && user.login || "FrankerFaceZ",
+ message: message,
+ tags: {
+ emotes: room.room.tmiSession._emotesParser.parseEmotesTag(message)
+ }
+ };
+
+ lc.set('model', model);
+
+ var tokens = lc.get('tokenizedMessage');
+
+ lc.set('model', null);
+ rc.set('model', null);
+ lc.set('parentController', null);
+
+ return tokens;
+}
+
+
/*FFZ.menu_pages.favorites = {
render: function(view, container) {
-
+ // Get the current room.
+ var room_id = view.get('controller.currentRoom.id');
+
+
},
name: "Favorites",
@@ -266,35 +200,8 @@ FFZ.menu_pages.channel = {
var room_id = view.get('controller.currentRoom.id'),
room = this.rooms[room_id];
- //this.track('trackEvent', 'Menu', 'Open', room_id);
-
- // Add the header and ad button.
- /*var btn = document.createElement('a');
- btn.className = 'button glyph-only ffz-button';
- btn.title = 'Advertise for FrankerFaceZ in chat!';
- btn.href = '#';
- btn.innerHTML = ' ';
-
- var hdr = document.createElement('div');
- hdr.className = 'list-header first';
- hdr.appendChild(btn);
- hdr.appendChild(document.createTextNode('FrankerFaceZ'));
- inner.appendChild(hdr);*/
-
- var c = this._emotes_for_sets(inner, view, room && room.menu_sets || []);
-
- /*if ( ! this._ws_exists ) {
- btn.className = "button ffz-button primary";
- btn.innerHTML = "Server Error";
- btn.title = "FFZ Server Error";
- btn.addEventListener('click', alert.bind(window, "The FrankerFaceZ client was unable to create a WebSocket to communicate with the FrankerFaceZ server.\n\nThis is most likely due to your browser's configuration either disabling WebSockets entirely or limiting the number of simultaneous connections. Please ensure that WebSockets have not been disabled."));
-
- } else {
- if ( c === 0 )
- btn.addEventListener('click', this._add_emote.bind(this, view, "To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com"));
- else
- btn.addEventListener('click', this._add_emote.bind(this, view, "To view this channel's emoticons, get FrankerFaceZ from http://www.frankerfacez.com"));
- }*/
+ // Basic Emote Sets
+ this._emotes_for_sets(inner, view, room && room.menu_sets || []);
// Feature Friday!
this._feature_friday_ui(room_id, inner, view);
@@ -330,6 +237,9 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) {
continue;
for(var eid in set.emotes) {
+ if ( ! set.emotes.hasOwnProperty(eid) )
+ continue;
+
var emote = set.emotes[eid];
if ( !set.emotes.hasOwnProperty(eid) || emote.hidden )
continue;
@@ -356,11 +266,21 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) {
FFZ.prototype._add_emote = function(view, emote) {
- var room = view.get('controller.currentRoom'),
- current_text = room.get('messageToSend') || '';
+ var input_el, text, room;
- if ( current_text && current_text.substr(-1) !== " " )
- current_text += ' ';
+ if ( this.has_bttv ) {
+ input_el = view.get('element').querySelector('textarea');
+ text = input_el.value;
- room.set('messageToSend', current_text + (emote.name || emote));
+ } else {
+ room = view.get('controller.currentRoom');
+ text = room.get('messageToSend') || '';
+ }
+
+ text += (text && text.substr(-1) !== " " ? " " : "") + (emote.name || emote);
+
+ if ( input_el )
+ input_el.value = text;
+ else
+ room.set('messageToSend', text);
}
\ No newline at end of file
diff --git a/src/ui/my_emotes.js b/src/ui/my_emotes.js
new file mode 100644
index 00000000..1f4bb7dd
--- /dev/null
+++ b/src/ui/my_emotes.js
@@ -0,0 +1,298 @@
+var FFZ = window.FrankerFaceZ,
+ constants = require("../constants"),
+
+ TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/",
+ BANNED_SETS = {"00000turbo":true},
+
+
+ get_emotes = function(ffz) {
+ var Chat = App.__container__.lookup('controller:chat'),
+ room_id = Chat.get('currentRoom.id'),
+ room = ffz.rooms[room_id],
+ tmiSession = room ? room.room.tmiSession : null,
+
+ set_ids = tmiSession && tmiSession._emotesParser && tmiSession._emotesParser.emoticonSetIds || "0",
+ user = ffz.get_user(),
+ user_sets = user && ffz.users[user.login] && ffz.users[user.login].sets || [];
+
+ // Remove the 'default' set.
+ set_ids = set_ids.split(",").removeObject("0")
+
+ return [set_ids, user_sets];
+ };
+
+
+// -------------------
+// Initialization
+// -------------------
+
+FFZ.prototype.setup_my_emotes = function() {
+ this._twitch_emote_sets = {};
+ this._twitch_set_to_channel = {};
+
+ if ( localStorage.ffzTwitchSets ) {
+ try {
+ this._twitch_set_to_channel = JSON.parse(localStorage.ffzTwitchSets);
+ } catch(err) { }
+ }
+}
+
+
+// -------------------
+// Menu Page
+// -------------------
+
+FFZ.menu_pages.my_emotes = {
+ name: "My Emoticons",
+ icon: constants.EMOTE,
+
+ visible: function() {
+ var emotes = get_emotes(this);
+ return emotes[0].length > 0 || emotes[1].length > 0;
+ },
+
+ render: function(view, container) {
+ var emotes = get_emotes(this), f = this;
+
+ new RSVP.Promise(function(done) {
+ var needed_sets = [];
+ for(var i=0; i < emotes[0].length; i++) {
+ var set_id = emotes[0][i];
+ if ( ! f._twitch_emote_sets[set_id] )
+ needed_sets.push(set_id);
+ }
+
+ RSVP.all([
+ new RSVP.Promise(function(d) {
+ if ( ! needed_sets.length )
+ return d();
+
+ Twitch.api.get("chat/emoticon_images", {emotesets: needed_sets.join(",")}, {version: 3})
+ .done(function(data) {
+ if ( data.emoticon_sets ) {
+ for(var set_id in data.emoticon_sets) {
+ if ( ! data.emoticon_sets.hasOwnProperty(set_id) )
+ continue;
+
+ var set = f._twitch_emote_sets[set_id] = f._twitch_emote_sets[set_id] || {};
+ set.emotes = data.emoticon_sets[set_id];
+ set.source = "Twitch";
+ }
+ }
+ d();
+ }).fail(function() {
+ d();
+ });
+ }),
+ new RSVP.Promise(function(d) {
+ if ( ! needed_sets.length )
+ return d();
+
+ var promises = [],
+ old_needed = needed_sets,
+ handle_set = function(id, name) {
+ var set = f._twitch_emote_sets[id] = f._twitch_emote_sets[id] || {};
+
+ if ( !name || BANNED_SETS[name] )
+ return;
+
+ if ( name == "turbo" ) {
+ set.channel = "Twitch Turbo";
+ set.badge = "//cdn.frankerfacez.com/script/turbo_badge.png";
+ return;
+ }
+
+ // Badge Lookup
+ promises.push(new RSVP.Promise(function(set, name, dn) {
+ Twitch.api.get("chat/" + name + "/badges", null, {version: 3})
+ .done(function(data) {
+ if ( data.subscriber && data.subscriber.image )
+ set.badge = data.subscriber.image;
+ dn();
+ }).fail(dn)}.bind(this,set,name)));
+
+ // Mess Up Capitalization
+ var lname = name.toLowerCase(),
+ old_data = FFZ.capitalization[lname];
+ if ( old_data && Date.now() - old_data[1] < 3600000 ) {
+ set.channel = old_data[0];
+ return;
+ }
+
+ promises.push(new RSVP.Promise(function(set, lname, name, dn) {
+ if ( ! f.ws_send("get_display_name", lname, function(success, data) {
+ var cap_name = success ? data : name;
+ FFZ.capitalization[lname] = [cap_name, Date.now()];
+ set.channel = cap_name;
+ dn();
+ }) ) {
+ // Can't use socket.
+ set.channel = name;
+ dn();
+ }
+
+ // Timeout
+ setTimeout(function(set,name,dn) {
+ if ( ! set.channel )
+ set.channel = name;
+ dn();
+ }.bind(this,set,name,dn), 5000);
+ }.bind(this, set, lname, name)));
+ },
+ handle_promises = function() {
+ if ( promises.length )
+ RSVP.all(promises).then(d,d);
+ else
+ d();
+ };
+
+ // Process all the sets we already have.
+ needed_sets = [];
+ for(var i=0;i 0 ) {
+ f.ws_send("twitch_sets", needed_sets, function(success, data) {
+ needed_sets = [];
+ if ( success ) {
+ for(var set_id in data) {
+ if ( ! data.hasOwnProperty(set_id) )
+ continue;
+
+ f._twitch_set_to_channel[set_id] = data[set_id];
+ handle_set(set_id, data[set_id]);
+ }
+
+ localStorage.ffzTwitchSets = JSON.stringify(f._twitch_set_to_channel);
+ }
+
+ handle_promises();
+ });
+
+ // Timeout!
+ setTimeout(function() {
+ if ( needed_sets.length )
+ handle_promises();
+ }, 5000);
+
+ } else
+ handle_promises();
+ })
+ ]).then(function() {
+ var sets = {};
+ for(var i=0; i < emotes[0].length; i++) {
+ var set_id = emotes[0][i];
+ if ( f._twitch_emote_sets[set_id] )
+ sets[set_id] = f._twitch_emote_sets[set_id];
+ }
+ done(sets);
+ }, function() { done({}); })
+ }).then(function(twitch_sets) {
+ try {
+
+ // Don't override a different page. We can wait.
+ if ( container.getAttribute('data-page') != "my_emotes" )
+ return;
+
+ container.innerHTML = "";
+
+ var ffz_sets = {},
+ sets = [];
+
+ for(var set_id in twitch_sets) {
+ if ( ! twitch_sets.hasOwnProperty(set_id) )
+ continue;
+
+ var set = twitch_sets[set_id];
+ if ( set.channel && set.emotes && set.emotes.length )
+ sets.push([1, set.channel, set]);
+ }
+
+ sets.sort(function(a,b) {
+ if ( a[0] < b[0] ) return -1;
+ else if ( a[0] > b[0] ) return 1;
+
+ var an = a[1].toLowerCase(),
+ bn = b[1].toLowerCase();
+
+ if ( an === "twitch turbo" )
+ an = "zzz" + an;
+
+ if ( bn === "twitch turbo" )
+ bn = "zzz" + bn;
+
+ if ( an < bn ) return -1;
+ else if ( an > bn ) return 1;
+ return 0;
+ });
+
+ for(var i=0; i < sets.length; i++) {
+ var set = sets[i][2],
+ heading = document.createElement('div'),
+ menu = document.createElement('div');
+
+ heading.className = 'heading';
+ heading.innerHTML = '' + set.source + ' ' + FFZ.get_capitalization(set.channel);
+ if ( set.badge )
+ heading.style.backgroundImage = 'url("' + set.badge + '")';
+
+ menu.className = 'emoticon-grid';
+ menu.appendChild(heading);
+
+ for(var x=0; x < set.emotes.length; x++) {
+ var emote = set.emotes[x];
+
+ var s = document.createElement('span');
+ s.className = 'emoticon tooltip';
+ s.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")';
+
+ var img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)';
+ s.style.backgroundImage = '-webkit-' + img_set;
+ s.style.backgroundImage = '-moz-' + img_set;
+ s.style.backgroundImage = '-ms-' + img_set;
+ s.style.backgroundImage = img_set;
+
+ s.title = emote.code;
+ s.addEventListener('click', f._add_emote.bind(f, view, emote.code));
+ menu.appendChild(s);
+ }
+
+ container.appendChild(menu);
+ }
+
+ if ( ! sets.length ) {
+ var menu = document.createElement('div');
+
+ menu.className = 'chat-menu-content center';
+ menu.innerHTML = "Error Loading Subscriptions";
+
+ container.appendChild(menu);
+ }
+
+ } catch(err) {
+ f.log("My Emotes Menu Error", err);
+ container.innerHTML = "";
+
+ var menu = document.createElement('div'),
+ heading = document.createElement('div'),
+ p = document.createElement('p');
+
+ heading.className = 'heading';
+ heading.innerHTML = 'Error Loading Menu';
+ menu.appendChild(heading);
+
+ p.className = 'clearfix';
+ p.textContent = err;
+ menu.appendChild(p);
+
+ menu.className = 'chat-menu-content';
+ container.appendChild(menu);
+ }
+ });
+ }
+ };
diff --git a/src/ui/styles.js b/src/ui/styles.js
index 2267e7d8..ea4cd7f1 100644
--- a/src/ui/styles.js
+++ b/src/ui/styles.js
@@ -8,7 +8,7 @@ FFZ.prototype.setup_css = function() {
s.id = "ffz-ui-css";
s.setAttribute('rel', 'stylesheet');
- s.setAttribute('href', constants.SERVER + "script/style.css");
+ s.setAttribute('href', constants.SERVER + "script/style.css?_=" + Date.now());
document.head.appendChild(s);
jQuery.noty.themes.ffzTheme = {
diff --git a/src/utils.js b/src/utils.js
index f999923b..2deda7cb 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -42,8 +42,7 @@ var sanitize_cache = {},
rgb[i] = Math.pow( ((rgb[i]+0.055)/1.055), 2.4 );
}
}
- var l = (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]);
- return l;
+ return (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]);
};
diff --git a/style.css b/style.css
index 1ff84083..680d229f 100644
--- a/style.css
+++ b/style.css
@@ -292,15 +292,29 @@
color: inherit;
}
+.ffz-about-table a.twitch,
+.ffz-about-table a.youtube,
+.ffz-about-table a.twitter,
#ffz-race-popup a.twitch,
#ffz-race-popup a.hitbox {
display: inline-block;
height: 16px;
- margin: 0 5px;
+ margin-left: 5px;
background-repeat: no-repeat;
}
-#ffz-race-popup a.twitch {
+.ffz-about-table a.youtube {
+ width: 23px;
+ background-image: url("//cdn.frankerfacez.com/channel/global/youtube_logo.png");
+}
+
+.ffz-about-table a.twitter {
+ width: 20px;
+ background-image: url("//cdn.frankerfacez.com/channel/global/twitter_logo.png");
+}
+
+#ffz-race-popup a.twitch,
+.ffz-about-table a.twitch {
width: 15px;
background-image: url("//cdn.frankerfacez.com/channel/global/twitch_logo.png");
}
@@ -335,9 +349,16 @@
margin: 0 -20px;
}
-.chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content .heading {
+.chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content .heading,
+.chat-menu.ffz-ui-popup .ffz-ui-menu-page .emoticon-grid .heading {
margin-bottom: 5px;
border-bottom: 1px solid rgba(0,0,0, 0.2);
+ text-align: left;
+}
+
+.chat-menu.ffz-ui-popup .ffz-ui-menu-page .emoticon-grid .heading {
+ padding-left: 23px;
+ background-repeat: no-repeat;
}
.chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content {
@@ -345,6 +366,10 @@
background-color: transparent;
}
+.chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content + .chat-menu-content {
+ padding-top: 0;
+}
+
.ffz-ui-menu-page span.help {
display: block;
opacity: 0.75;
@@ -366,6 +391,7 @@
background-color: #282828;
}
+.ffz-ui-menu-page .heading .right,
.ffz-ui-popup ul.menu li.item {
float: right;
}
@@ -374,11 +400,10 @@
float: left;
}
-.ffz-ui-popup ul.menu li.title span {
+.ffz-ui-popup ul.menu li.title > span {
display: block;
padding: 10px 20px;
line-height: 16px;
- cursor: pointer;
}
.ffz-ui-popup ul.menu a {
@@ -414,6 +439,12 @@
/* Chat Mentions */
+.ember-chat .mentioned:empty,
+.ember-chat .mentioning:empty {
+ display: none;
+}
+
+.ffz-chat-background .ember-chat .mentioning,
.ffz-chat-background .ember-chat .mentioned {
border-radius: 10px;
padding: 3px 7px;
@@ -424,7 +455,10 @@
.ffz-chat-background .app-main.theatre .chat-container .chat-line .mentioned,
.ffz-chat-background .ember-chat-container.dark .chat-line .mentioned,
-.ffz-chat-background .chat-container.dark .chat-line .mentioned {
+.ffz-chat-background .chat-container.dark .chat-line .mentioned,
+.ffz-chat-background .app-main.theatre .chat-container .chat-line .mentioning,
+.ffz-chat-background .ember-chat-container.dark .chat-line .mentioning,
+.ffz-chat-background .chat-container.dark .chat-line .mentioning {
color: #8c8c9c;
background-color: rgba(16,16,20, 0.75);
}
@@ -541,4 +575,89 @@
.ffz-chat-background .chat-container.dark .ember-chat .chat-messages .chat-line.ffz-mentioned.ffz-alternate,
.ffz-chat-background .ember-chat-container.dark .ember-chat .chat-messages .chat-line.ffz-mentioned.ffz-alternate {
background-color: rgba(255,0,0, 0.3);
+}
+
+/* Emoticon Tooltips */
+
+.tipsy table.emote-data td { padding: 0 2px; }
+
+.tipsy table.emote-data td:first-of-type {
+ text-align: right;
+ font-weight: bold;
+ padding-left: 0;
+}
+
+.tipsy table.emote-data td:last-of-type {
+ text-align: left;
+ padding-right: 0;
+ white-space: nowrap;
+}
+
+.tipsy table.emote-data td.center { text-align: center; }
+
+/* Menu Page Loader */
+
+.ffz-ui-menu-page:empty {
+ overflow: hidden;
+}
+
+.ffz-ui-menu-page:empty::after {
+ content: " ";
+ display: block;
+ width: 80px;
+ height: 63px;
+ background-image: url("//cdn.frankerfacez.com/script/spinner-dark.png");
+
+ margin: 50px auto;
+ -webkit-animation: ffz-rotateplane 1.2s infinite linear;
+ animation: ffz-rotateplane 1.2s infinite linear;
+}
+
+@-webkit-keyframes ffz-rotateplane {
+ 0% { -webkit-transform: perspective(120px) rotateY(90deg) }
+ 25% { -webkit-transform: perspective(120px) rotateY(180deg) }
+ 75% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) }
+ 100% { -webkit-transform: perspective(120px) rotateY(90deg) rotateX(180deg) }
+}
+
+@keyframes ffz-rotateplane {
+ 0% { transform: perspective(120px) rotateY(90deg) }
+ 25% { transform: perspective(120px) rotateY(180deg) }
+ 75% { transform: perspective(120px) rotateY(180deg) rotateX(180deg) }
+ 100% { transform: perspective(120px) rotateY(90deg) rotateX(180deg) }
+}
+
+/* Menu About Page */
+
+.ffz-about-table {
+ width: 100%;
+}
+
+.ffz-about-table td:first-child {
+ text-align: left;
+ width: 100%;
+}
+
+.ffz-about-table .debug td {
+ padding-top: 10px;
+ opacity: 0.8;
+ font-size: 10px;
+}
+
+.ffz-about-subheading {
+ /*text-transform: uppercase;*/
+ letter-spacing: 2px;
+ margin: -5px 0 5px;
+}
+
+.button.ffz-donate {
+ margin-left: 10px;
+ background: #00b132;
+ color: #fff !important;
+ padding: 0 10px;
+ font-size: 12px;
+}
+
+.button.ffz-donate:not(.disabled):hover {
+ background: #08c43d;
}
\ No newline at end of file