diff --git a/.gitignore b/.gitignore index 9e19ac9b..af54d5fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -node_modules -npm-debug.log +node_modules +npm-debug.log build \ No newline at end of file diff --git a/README.md b/README.md index 0666d96d..c35386df 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,39 @@ -FrankerFaceZ -============ - -Copyright (c) 2015 FrankerFaceZ - -This script is free to modify for personal use. You are not allowed to sell or -distribute FrankerFaceZ or any components of FrankerFaceZ. - - -Developing -========== - -FrankerFaceZ uses node.js to manage development dependencies and to run an HTTP -server for development. To get everything you need: - -1. Install node.js -2. Run ```npm install``` within the FrankerFaceZ directory. - - -From there, you can use gulp to build the extension from source simply by -running ```gulp```. For development, you can instruct gulp to watch the source -files for changes and re-build automatically with ```gulp watch``` - -FrankerFaceZ comes with a local development server that listens on port 8000 -and it serves up local development copies of files, falling back to the CDN -when a local copy of a file isn't present. To start the server, -run ```npm test``` - - -At this time, you will also need to use the included version of the Chrome -extension. Remove any existing copy of FrankerFaceZ from your browser and load -the unpacked extension in the ```Chrome Extension``` folder. - -Once you're using that extension, use the command ```/ffz developer_mode on``` -or ```/ffz developer_mode off``` in Twitch chat to toggle developer mode on or -off. You must then refresh the page for changes to take effect. If FFZ is not -working or the command otherwise fails to work, you can open the JavaScript -console on twitch.tv and run ```localStorage.ffzDebugMode = true;``` or +FrankerFaceZ +============ + +Copyright (c) 2015 FrankerFaceZ + +This script is free to modify for personal use. You are not allowed to sell or +distribute FrankerFaceZ or any components of FrankerFaceZ. + + +Developing +========== + +FrankerFaceZ uses node.js to manage development dependencies and to run an HTTP +server for development. To get everything you need: + +1. Install node.js +2. Run ```npm install``` within the FrankerFaceZ directory. + + +From there, you can use gulp to build the extension from source simply by +running ```gulp```. For development, you can instruct gulp to watch the source +files for changes and re-build automatically with ```gulp watch``` + +FrankerFaceZ comes with a local development server that listens on port 8000 +and it serves up local development copies of files, falling back to the CDN +when a local copy of a file isn't present. To start the server, +run ```npm test``` + + +At this time, you will also need to use the included version of the Chrome +extension. Remove any existing copy of FrankerFaceZ from your browser and load +the unpacked extension in the ```Chrome Extension``` folder. + +Once you're using that extension, use the command ```/ffz developer_mode on``` +or ```/ffz developer_mode off``` in Twitch chat to toggle developer mode on or +off. You must then refresh the page for changes to take effect. If FFZ is not +working or the command otherwise fails to work, you can open the JavaScript +console on twitch.tv and run ```localStorage.ffzDebugMode = true;``` or ```localStorage.ffzDebugMode = false;``` to enable or disable the feature. \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index cd35109b..c3fafee4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,40 +1,40 @@ -var fs = require('fs'), - gulp = require('gulp'), - browserify = require('gulp-browserify'), - header = require('gulp-header'), - footer = require('gulp-footer'), - concat = require('gulp-concat'), - clean = require('gulp-clean'), - util = require('gulp-util'), - rename = require('gulp-rename'), - uglify = require('gulp-uglify'); - - -gulp.task('clean', function() { - return gulp.src('build', {read:false}) - .pipe(clean()); -}); - -gulp.task('prepare', ['clean'], function() { - return gulp.src(['src/**/*']) - .pipe(gulp.dest('build/')); -}); - -gulp.task('scripts', ['prepare'], function() { - gulp.src(['build/main.js']) - .pipe(browserify()) - .pipe(concat('script.js')) - .pipe(header('(function(window) {')) - .pipe(footer(';window.ffz = new FrankerFaceZ()}(window));')) - .pipe(gulp.dest(__dirname)) - .pipe(uglify()) - .pipe(rename('script.min.js')) - .pipe(gulp.dest(__dirname)) - .on('error', util.log); -}); - -gulp.task('watch', ['default'], function() { - gulp.watch('src/**/*', ['default']); -}); - -gulp.task('default', ['scripts']); +var fs = require('fs'), + gulp = require('gulp'), + browserify = require('gulp-browserify'), + header = require('gulp-header'), + footer = require('gulp-footer'), + concat = require('gulp-concat'), + clean = require('gulp-clean'), + util = require('gulp-util'), + rename = require('gulp-rename'), + uglify = require('gulp-uglify'); + + +gulp.task('clean', function() { + return gulp.src('build', {read:false}) + .pipe(clean()); +}); + +gulp.task('prepare', ['clean'], function() { + return gulp.src(['src/**/*']) + .pipe(gulp.dest('build/')); +}); + +gulp.task('scripts', ['prepare'], function() { + gulp.src(['build/main.js']) + .pipe(browserify()) + .pipe(concat('script.js')) + .pipe(header('(function(window) {')) + .pipe(footer(';window.ffz = new FrankerFaceZ()}(window));')) + .pipe(gulp.dest(__dirname)) + .pipe(uglify()) + .pipe(rename('script.min.js')) + .pipe(gulp.dest(__dirname)) + .on('error', util.log); +}); + +gulp.task('watch', ['default'], function() { + gulp.watch('src/**/*', ['default']); +}); + +gulp.task('default', ['scripts']); diff --git a/script.js b/script.js index 60713251..1c1d2768 100644 --- a/script.js +++ b/script.js @@ -1,194 +1,194 @@ (function(window) {(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o' + SVGPATH + '', - CHAT_BUTTON: '' + SVGPATH + '' +var SVGPATH = '', + DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'); + +module.exports = { + DEBUG: DEBUG, + SERVER: DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/", + + SVGPATH: SVGPATH, + ZREKNARF: '' + SVGPATH + '', + CHAT_BUTTON: '' + SVGPATH + '' } },{}],4:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ; - - -// ----------------------- -// Developer Mode Command -// ----------------------- - -FFZ.chat_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" ) - enabled = true; - else if ( args == "n" || args == "no" || args == "false" || args == "off" ) - enabled = false; - - if ( enabled === undefined ) - return "Developer Mode is currently " + (localStorage.ffzDebugMode == "true" ? "enabled." : "disabled."); - - localStorage.ffzDebugMode = enabled; - return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; -} - -FFZ.chat_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."; +var FFZ = window.FrankerFaceZ; + + +// ----------------------- +// Developer Mode Command +// ----------------------- + +FFZ.chat_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" ) + enabled = true; + else if ( args == "n" || args == "no" || args == "false" || args == "off" ) + enabled = false; + + if ( enabled === undefined ) + return "Developer Mode is currently " + (localStorage.ffzDebugMode == "true" ? "enabled." : "disabled."); + + localStorage.ffzDebugMode = enabled; + return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; +} + +FFZ.chat_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(require,module,exports){ -var FFZ = window.FrankerFaceZ; - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_chatview = function() { - this.log("Hooking the Ember Chat view."); - - var Chat = App.__container__.resolve('view:chat'); - this._modify_cview(Chat); - - // For some reason, this doesn't work unless we create an instance of the - // chat view and then destroy it immediately. - Chat.create().destroy(); - - // Modify all existing Chat views. - for(var key in Ember.View.views) { - if ( ! Ember.View.views.hasOwnProperty(key) ) - continue; - - var view = Ember.View.views[key]; - if ( !(view instanceof Chat) ) - continue; - - this.log("Adding UI link manually to Chat view.", view); - view.$('.textarea-contain').append(this.build_ui_link(view)); - } -} - - -// -------------------- -// Modify Chat View -// -------------------- - -FFZ.prototype._modify_cview = function(view) { - var f = this; - - view.reopen({ - didInsertElement: function() { - this._super(); - this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); - }, - - willClearRender: function() { - this._super(); - this.$(".ffz-ui-toggle").remove(); - }, - - ffzUpdateLink: Ember.observer('controller.currentRoom', function() { - f.update_ui_link(); - }) - }); +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_chatview = function() { + this.log("Hooking the Ember Chat view."); + + var Chat = App.__container__.resolve('view:chat'); + this._modify_cview(Chat); + + // For some reason, this doesn't work unless we create an instance of the + // chat view and then destroy it immediately. + Chat.create().destroy(); + + // Modify all existing Chat views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Chat) ) + continue; + + this.log("Adding UI link manually to Chat view.", view); + view.$('.textarea-contain').append(this.build_ui_link(view)); + } +} + + +// -------------------- +// Modify Chat View +// -------------------- + +FFZ.prototype._modify_cview = function(view) { + var f = this; + + view.reopen({ + didInsertElement: function() { + this._super(); + this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + }, + + willClearRender: function() { + this._super(); + this.$(".ffz-ui-toggle").remove(); + }, + + ffzUpdateLink: Ember.observer('controller.currentRoom', function() { + f.update_ui_link(); + }) + }); } },{}],6:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ; - - -// --------------------- -// Initialization -// --------------------- - -FFZ.prototype.setup_line = function() { - this.log("Hooking the Ember Line controller."); - - var Line = App.__container__.resolve('controller:line'), - f = this; - - Line.reopen({ - tokenizedMessage: function() { - // Add our own step to the tokenization procedure. - return f._emoticonize(this, this._super()); - - }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") - // TODO: Copy the new properties from the new Twitch! - }); - - - this.log("Hooking the Ember Line view."); - var Line = App.__container__.resolve('view:line'); - - Line.reopen({ - didInsertElement: function() { - this._super(); - - var el = this.get('element'), - user = this.get('context.model.from'); - - el.setAttribute('data-room', this.get('context.parentController.content.id')); - el.setAttribute('data-sender', user); - - f.render_badge(this); - - if ( localStorage['ffzCapitalize'] != 'false' ) - f.capitalize(this, user); - - } - }); - - // Store the capitalization of our own name. - var user = this.get_user(); - if ( user && user.name ) - FFZ.capitalization[user.login] = [user.name, Date.now()]; -} - - -// --------------------- -// Capitalization -// --------------------- - -FFZ.capitalization = {}; -FFZ._cap_fetching = 0; - -FFZ.get_capitalization = function(name, callback) { - name = name.toLowerCase(); - if ( name == "jtv" || name == "twitchnotify" ) - return name; - - var old_data = FFZ.capitalization[name]; - if ( old_data ) { - if ( Date.now() - old_data[1] < 3600000 ) - return old_data[0]; - } - - if ( FFZ._cap_fetching < 5 ) { - 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--; - callback && callback(cap_name); - }); - } - - return old_data ? old_data[0] : name; -} - - -FFZ.prototype.capitalize = function(view, user) { - var name = FFZ.get_capitalization(user, this.capitalize.bind(this, view)); - if ( name ) - view.$('.from').text(name); -} - - -FFZ.chat_commands.capitalization = function(room, args) { - var enabled, args = args && args.length ? args[0].toLowerCase() : null; - if ( args == "y" || args == "yes" || args == "true" || args == "on" ) - enabled = true; - else if ( args == "n" || args == "no" || args == "false" || args == "off" ) - enabled = false; - - if ( enabled === undefined ) - return "Chat Name Capitalization is currently " + (localStorage.ffzCapitalize != "false" ? "enabled." : "disabled."); - - localStorage.ffzCapitalize = enabled; - return "Chat Name Capitalization is now " + (enabled ? "enabled." : "disabled."); -} - -FFZ.chat_commands.capitalization.help = "Usage: /ffz capitalization \nEnable or disable Chat Name Capitalization. This setting does not work with BetterTTV."; - - -// --------------------- -// Emoticon Replacement -// --------------------- - -FFZ.prototype._emoticonize = function(controller, tokens) { - var room_id = controller.get("parentController.model.id"), - user_id = controller.get("model.from"), - f = this; - - // Get our sets. - var sets = this.getEmotes(user_id, room_id), - emotes = []; - - // Build a list of emotes that match. - _.each(sets, function(set_id) { - var set = f.emote_sets[set_id]; - if ( ! set ) - return; - - _.each(set.emotes, function(emote) { - _.any(tokens, function(token) { - return _.isString(token) && token.match(emote.regex); - }) && emotes.push(emote); - }); - }); - - // Don't bother proceeding if we have no emotes. - if ( ! emotes.length ) - return tokens; - - // Now that we have all the matching tokens, do crazy stuff. - if ( typeof tokens == "string" ) - tokens = [tokens]; - - // This is weird stuff I basically copied from the old Twitch code. - // Here, for each emote, we split apart every text token and we - // put it back together with the matching bits of text replaced - // with an object telling Twitch's line template how to render the - // 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)}; - - tokens = _.compact(_.flatten(_.map(tokens, function(token) { - if ( _.isObject(token) ) - return token; - - var tbits = token.split(emote.regex), bits = []; - tbits.forEach(function(val, ind) { - bits.push(val); - if ( ind !== tbits.length - 1 ) - bits.push(eo); - }); - return bits; - }))); - }); - - return tokens; +var FFZ = window.FrankerFaceZ; + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.setup_line = function() { + this.log("Hooking the Ember Line controller."); + + var Line = App.__container__.resolve('controller:line'), + f = this; + + Line.reopen({ + tokenizedMessage: function() { + // Add our own step to the tokenization procedure. + return f._emoticonize(this, this._super()); + + }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") + // TODO: Copy the new properties from the new Twitch! + }); + + + this.log("Hooking the Ember Line view."); + var Line = App.__container__.resolve('view:line'); + + Line.reopen({ + didInsertElement: function() { + this._super(); + + var el = this.get('element'), + user = this.get('context.model.from'); + + el.setAttribute('data-room', this.get('context.parentController.content.id')); + el.setAttribute('data-sender', user); + + f.render_badge(this); + + if ( localStorage['ffzCapitalize'] != 'false' ) + f.capitalize(this, user); + + } + }); + + // Store the capitalization of our own name. + var user = this.get_user(); + if ( user && user.name ) + FFZ.capitalization[user.login] = [user.name, Date.now()]; +} + + +// --------------------- +// Capitalization +// --------------------- + +FFZ.capitalization = {}; +FFZ._cap_fetching = 0; + +FFZ.get_capitalization = function(name, callback) { + name = name.toLowerCase(); + if ( name == "jtv" || name == "twitchnotify" ) + return name; + + var old_data = FFZ.capitalization[name]; + if ( old_data ) { + if ( Date.now() - old_data[1] < 3600000 ) + return old_data[0]; + } + + if ( FFZ._cap_fetching < 5 ) { + 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--; + callback && callback(cap_name); + }); + } + + return old_data ? old_data[0] : name; +} + + +FFZ.prototype.capitalize = function(view, user) { + var name = FFZ.get_capitalization(user, this.capitalize.bind(this, view)); + if ( name ) + view.$('.from').text(name); +} + + +FFZ.chat_commands.capitalization = function(room, args) { + var enabled, args = args && args.length ? args[0].toLowerCase() : null; + if ( args == "y" || args == "yes" || args == "true" || args == "on" ) + enabled = true; + else if ( args == "n" || args == "no" || args == "false" || args == "off" ) + enabled = false; + + if ( enabled === undefined ) + return "Chat Name Capitalization is currently " + (localStorage.ffzCapitalize != "false" ? "enabled." : "disabled."); + + localStorage.ffzCapitalize = enabled; + return "Chat Name Capitalization is now " + (enabled ? "enabled." : "disabled."); +} + +FFZ.chat_commands.capitalization.help = "Usage: /ffz capitalization \nEnable or disable Chat Name Capitalization. This setting does not work with BetterTTV."; + + +// --------------------- +// Emoticon Replacement +// --------------------- + +FFZ.prototype._emoticonize = function(controller, tokens) { + var room_id = controller.get("parentController.model.id"), + user_id = controller.get("model.from"), + f = this; + + // Get our sets. + var sets = this.getEmotes(user_id, room_id), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emotes, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Now that we have all the matching tokens, do crazy stuff. + if ( typeof tokens == "string" ) + tokens = [tokens]; + + // This is weird stuff I basically copied from the old Twitch code. + // Here, for each emote, we split apart every text token and we + // put it back together with the matching bits of text replaced + // with an object telling Twitch's line template how to render the + // 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)}; + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(emote.regex), bits = []; + tbits.forEach(function(val, ind) { + bits.push(val); + if ( ind !== tbits.length - 1 ) + bits.push(eo); + }); + return bits; + }))); + }); + + return tokens; } },{}],7:[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*['"]([^'"]+)['"][^}]+(?:}|$)/, - GROUP_CHAT = /^_([^_]+)_\d+$/, - constants = require('../constants'), - utils = require('../utils'), - - - moderator_css = function(room) { - if ( ! room.moderator_badge ) - return ""; - - return '.chat-line[data-room="' + room.id + '"] .badges .moderator { background-image:url("' + room.moderator_badge + '") !important; }'; - } - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_room = function() { - this.rooms = {}; - - this.log("Creating room style element."); - var s = this._room_style = document.createElement("style"); - s.id = "ffz-room-css"; - document.head.appendChild(s); - - this.log("Hooking the Ember Room model."); - - var Room = App.__container__.resolve('model:room'); - this._modify_room(Room); - - // Modify all current instances of Room, as the changes to the base - // class won't be inherited automatically. - var instances = Room.instances; - for(var key in instances) { - if ( ! instances.hasOwnProperty(key) ) - continue; - - var inst = instances[key]; - this.add_room(inst.id, inst); - this._modify_room(inst); - } -} - - -// -------------------- -// Command System -// -------------------- - -FFZ.chat_commands = {}; - - -FFZ.prototype.room_message = function(room, text) { - var lines = text.split("\n"); - if ( this.has_bttv ) { - for(var i=0; i < lines.length; i++) - BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); - - } else { - for(var i=0; i < lines.length; i++) - room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]}); - } -} - - -FFZ.prototype.run_command = function(text, room_id) { - var room = this.rooms[room_id]; - if ( ! room || !room.room ) - return; - - if ( ! text ) { - // Try to pop-up the menu. - var link = document.querySelector('a.ffz-ui-toggle'); - if ( link ) - return link.click(); - - text = "help"; - } - - var args = text.split(" "), - cmd = args.shift().toLowerCase(); - - this.log("Received Command: " + cmd, args, true); - - var command = FFZ.chat_commands[cmd], output; - if ( command ) { - try { - output = command.bind(this)(room, args); - } catch(err) { - this.log("Error Running Command - " + cmd + ": " + err, room); - output = "There was an error running the command."; - } - } else - output = 'There is no "' + cmd + '" command.'; - - if ( output ) - this.room_message(room, output); -} - - -FFZ.chat_commands.help = function(room, args) { - if ( args && args.length ) { - var command = FFZ.chat_commands[args[0].toLowerCase()]; - if ( ! command ) - return 'There is no "' + args[0] + '" command.'; - - else if ( ! command.help ) - return 'No help is available for the command "' + args[0] + '".'; - - else - return command.help; - } - - var cmds = []; - for(var c in FFZ.chat_commands) - FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); - - return "The available commands are: " + cmds.join(", "); -} - -FFZ.chat_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; - - -// -------------------- -// Room Management -// -------------------- - -FFZ.prototype.add_room = function(id, room) { - if ( this.rooms[id] ) - return this.log("Tried to add existing room: " + id); - - this.log("Adding Room: " + id); - - // Create a basic data table for this room. - this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; - - // Let the server know where we are. - this.ws_send("sub", id); - - // For now, we use the legacy function to grab the .css file. - this._legacy_add_room(id); -} - - -FFZ.prototype.remove_room = function(id) { - var room = this.rooms[id]; - if ( ! room ) - return; - - this.log("Removing Room: " + id); - - // Remove the CSS - if ( room.css || room.moderator_badge ) - utils.update_css(this._room_style, id, null); - - // Let the server know we're gone and delete our data for this room. - this.ws_send("unsub", id); - delete this.rooms[id]; - - // Clean up sets we aren't using any longer. - for(var i=0; i < room.sets.length; i++) { - var set_id = room.sets[i], set = this.emote_sets[set_id]; - if ( ! set ) - continue; - - set.users.removeObject(id); - if ( !set.global && !set.users.length ) - this.unload_set(set_id); - } -} - - -// -------------------- -// Receiving Set Info -// -------------------- - -FFZ.prototype.load_room = function(room_id, callback) { - return this._legacy_load_room(room_id, callback); -} - - -FFZ.prototype._load_room_json = function(room_id, callback, data) { - // Preserve the pointer to the Room instance. - if ( this.rooms[room_id] ) - data.room = this.rooms[room_id].room; - - this.rooms[room_id] = data; - - if ( data.css || data.moderator_badge ) - utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); - - for(var i=0; i < data.sets.length; i++) { - var set_id = data.sets[i]; - if ( ! this.emote_sets.hasOwnProperty(set_id) ) - this.load_set(set_id); - } - - this.update_ui_link(); - - if ( callback ) - callback(true, data); -} - - -// -------------------- -// Ember Modifications -// -------------------- - -FFZ.prototype._modify_room = function(room) { - var f = this; - room.reopen({ - init: function() { - this._super(); - f.add_room(this.id, this); - }, - - willDestroy: function() { - this._super(); - f.remove_room(this.id); - }, - - send: function(text) { - var cmd = text.split(' ', 1)[0].toLowerCase(); - if ( cmd === "/ffz" ) { - this.set("messageToSend", ""); - f.run_command(text.substr(5), this.get('id')); - } else - return this._super(text); - } - }); -} - - -// -------------------- -// Legacy Data Support -// -------------------- - -FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_room_css(room_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return this._legacy_load_room_css(room_id, callback, null); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_add_room(room_id, callback, tries); - }); -} - - -FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { - var set_id = room_id, - match = set_id.match(GROUP_CHAT); - - if ( match && match[1] ) - set_id = match[1]; - - var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; - - if ( data ) - data = data.replace(CSS, "").trim(); - - if ( data ) { - data = data.replace(MOD_CSS, function(match, url) { - if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) - return match; - - output.moderator_badge = url; - return ""; - }); - } - - output.css = data || null; - return this._load_room_json(room_id, callback, output); +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*['"]([^'"]+)['"][^}]+(?:}|$)/, + GROUP_CHAT = /^_([^_]+)_\d+$/, + constants = require('../constants'), + utils = require('../utils'), + + + moderator_css = function(room) { + if ( ! room.moderator_badge ) + return ""; + + return '.chat-line[data-room="' + room.id + '"] .badges .moderator { background-image:url("' + room.moderator_badge + '") !important; }'; + } + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_room = function() { + this.rooms = {}; + + this.log("Creating room style element."); + var s = this._room_style = document.createElement("style"); + s.id = "ffz-room-css"; + document.head.appendChild(s); + + this.log("Hooking the Ember Room model."); + + var Room = App.__container__.resolve('model:room'); + this._modify_room(Room); + + // Modify all current instances of Room, as the changes to the base + // class won't be inherited automatically. + var instances = Room.instances; + for(var key in instances) { + if ( ! instances.hasOwnProperty(key) ) + continue; + + var inst = instances[key]; + this.add_room(inst.id, inst); + this._modify_room(inst); + } +} + + +// -------------------- +// Command System +// -------------------- + +FFZ.chat_commands = {}; + + +FFZ.prototype.room_message = function(room, text) { + var lines = text.split("\n"); + if ( this.has_bttv ) { + for(var i=0; i < lines.length; i++) + BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); + + } else { + for(var i=0; i < lines.length; i++) + room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]}); + } +} + + +FFZ.prototype.run_command = function(text, room_id) { + var room = this.rooms[room_id]; + if ( ! room || !room.room ) + return; + + if ( ! text ) { + // Try to pop-up the menu. + var link = document.querySelector('a.ffz-ui-toggle'); + if ( link ) + return link.click(); + + text = "help"; + } + + var args = text.split(" "), + cmd = args.shift().toLowerCase(); + + this.log("Received Command: " + cmd, args, true); + + var command = FFZ.chat_commands[cmd], output; + if ( command ) { + try { + output = command.bind(this)(room, args); + } catch(err) { + this.log("Error Running Command - " + cmd + ": " + err, room); + output = "There was an error running the command."; + } + } else + output = 'There is no "' + cmd + '" command.'; + + if ( output ) + this.room_message(room, output); +} + + +FFZ.chat_commands.help = function(room, args) { + if ( args && args.length ) { + var command = FFZ.chat_commands[args[0].toLowerCase()]; + if ( ! command ) + return 'There is no "' + args[0] + '" command.'; + + else if ( ! command.help ) + return 'No help is available for the command "' + args[0] + '".'; + + else + return command.help; + } + + var cmds = []; + for(var c in FFZ.chat_commands) + FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); + + return "The available commands are: " + cmds.join(", "); +} + +FFZ.chat_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; + + +// -------------------- +// Room Management +// -------------------- + +FFZ.prototype.add_room = function(id, room) { + if ( this.rooms[id] ) + return this.log("Tried to add existing room: " + id); + + this.log("Adding Room: " + id); + + // Create a basic data table for this room. + this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; + + // Let the server know where we are. + this.ws_send("sub", id); + + // For now, we use the legacy function to grab the .css file. + this._legacy_add_room(id); +} + + +FFZ.prototype.remove_room = function(id) { + var room = this.rooms[id]; + if ( ! room ) + return; + + this.log("Removing Room: " + id); + + // Remove the CSS + if ( room.css || room.moderator_badge ) + utils.update_css(this._room_style, id, null); + + // Let the server know we're gone and delete our data for this room. + this.ws_send("unsub", id); + delete this.rooms[id]; + + // Clean up sets we aren't using any longer. + for(var i=0; i < room.sets.length; i++) { + var set_id = room.sets[i], set = this.emote_sets[set_id]; + if ( ! set ) + continue; + + set.users.removeObject(id); + if ( !set.global && !set.users.length ) + this.unload_set(set_id); + } +} + + +// -------------------- +// Receiving Set Info +// -------------------- + +FFZ.prototype.load_room = function(room_id, callback) { + return this._legacy_load_room(room_id, callback); +} + + +FFZ.prototype._load_room_json = function(room_id, callback, data) { + // Preserve the pointer to the Room instance. + if ( this.rooms[room_id] ) + data.room = this.rooms[room_id].room; + + this.rooms[room_id] = data; + + if ( data.css || data.moderator_badge ) + utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); + + for(var i=0; i < data.sets.length; i++) { + var set_id = data.sets[i]; + if ( ! this.emote_sets.hasOwnProperty(set_id) ) + this.load_set(set_id); + } + + this.update_ui_link(); + + if ( callback ) + callback(true, data); +} + + +// -------------------- +// Ember Modifications +// -------------------- + +FFZ.prototype._modify_room = function(room) { + var f = this; + room.reopen({ + init: function() { + this._super(); + f.add_room(this.id, this); + }, + + willDestroy: function() { + this._super(); + f.remove_room(this.id); + }, + + send: function(text) { + var cmd = text.split(' ', 1)[0].toLowerCase(); + if ( cmd === "/ffz" ) { + this.set("messageToSend", ""); + f.run_command(text.substr(5), this.get('id')); + } else + return this._super(text); + } + }); +} + + +// -------------------- +// Legacy Data Support +// -------------------- + +FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_room_css(room_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return this._legacy_load_room_css(room_id, callback, null); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_add_room(room_id, callback, tries); + }); +} + + +FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { + var set_id = room_id, + match = set_id.match(GROUP_CHAT); + + if ( match && match[1] ) + set_id = match[1]; + + var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; + + if ( data ) + data = data.replace(CSS, "").trim(); + + if ( data ) { + data = data.replace(MOD_CSS, function(match, url) { + if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) + return match; + + output.moderator_badge = url; + return ""; + }); + } + + output.css = data || null; + return this._load_room_json(room_id, callback, output); } },{"../constants":3,"../utils":23}],8:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -905,202 +905,202 @@ FFZ.prototype._modify_viewers = function(controller) { }); } },{}],10:[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*['"]([^'"]+)['"][^}]+(?:}|$)/, - constants = require('./constants'), - utils = require('./utils'), - - - loaded_global = function(set_id, success, data) { - if ( ! success ) - return; - - data.global = true; - this.global_sets.push(set_id); - }, - - - check_margins = function(margins, height) { - var mlist = margins.split(/ +/); - if ( mlist.length != 2 ) - return margins; - - mlist[0] = parseFloat(mlist[0]); - mlist[1] = parseFloat(mlist[1]); - - if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) - return null; - - return margins; - }, - - - build_legacy_css = function(emote) { - var margin = emote.margins; - if ( ! margin ) - margin = ((emote.height - 18) / -2) + "px 0"; - return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; - }, - - - build_new_css = function(emote) { - if ( ! emote.margins && ! emote.extra_css ) - return build_legacy_css(emote); - - return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; - }, - - - build_css = build_new_css; - - -// --------------------- -// Initialization -// --------------------- - -FFZ.prototype.setup_emoticons = function() { - this.log("Preparing emoticon system."); - - this.emote_sets = {}; - this.global_sets = []; - this._last_emote_id = 0; - - this.log("Creating emoticon style element."); - var s = this._emote_style = document.createElement('style'); - s.id = "ffz-emoticon-css"; - document.head.appendChild(s); - - this.log("Loading global emote set."); - this.load_set("global", loaded_global.bind(this, "global")); -} - - -// --------------------- -// Set Management -// --------------------- - -FFZ.prototype.getEmotes = function(user_id, room_id) { - var user = this.users[user_id], - room = this.rooms[room_id]; - - return _.union(user && user.sets || [], room && room.sets || [], this.global_sets); -} - - -// --------------------- -// Commands -// --------------------- - -FFZ.ws_commands.reload_set = function(set_id) { - this.load_set(set_id); -} - - -// --------------------- -// Set Loading -// --------------------- - -FFZ.prototype.load_set = function(set_id, callback) { - return this._legacy_load_set(set_id, callback); -} - - -FFZ.prototype.unload_set = function(set_id) { - var set = this.emote_sets[set_id]; - if ( ! set ) - return; - - this.log("Unloading emoticons for set: " + set_id); - - utils.update_css(this._emote_style, set_id, null); - delete this.emote_sets[set_id]; - - for(var i=0; i < set.users.length; i++) { - var room = this.rooms[set.users[i]]; - if ( room ) - room.sets.removeObject(set_id); - } -} - - -FFZ.prototype._load_set_json = function(set_id, callback, data) { - // Store our set. - this.emote_sets[set_id] = data; - data.users = []; - data.global = false; - data.count = 0; - - // Iterate through all the emoticons, building CSS and regex objects as appropriate. - var output_css = ""; - - for(var key in data.emotes) { - if ( ! data.emotes.hasOwnProperty(key) ) - continue; - - var emote = data.emotes[key]; - emote.klass = "ffz-emote-" + emote.id; - - if ( emote.name[emote.name.length-1] === "!" ) - emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); - else - emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); - - output_css += build_css(emote); - data.count++; - } - - utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); - this.log("Updated emoticons for set: " + set_id, data); - this.update_ui_link(); - - if ( callback ) - callback(true, data); -} - - -FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_css(set_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return callback && callback(false); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_load_set(set_id, callback, tries); - - return callback && callback(false); - }); -} - - -FFZ.prototype._legacy_load_css = function(set_id, callback, data) { - var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; - - data = data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { - height = parseInt(height); width = parseInt(width); - 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}; - - emotes[id] = emote; - return ""; - }).trim(); - - if ( data ) - data.replace(MOD_CSS, function(match, url) { - if ( output.icon || url.substr(-11) !== 'modicon.png' ) - return; - - output.icon = url; - }); - - this._load_set_json(set_id, callback, output); +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*['"]([^'"]+)['"][^}]+(?:}|$)/, + constants = require('./constants'), + utils = require('./utils'), + + + loaded_global = function(set_id, success, data) { + if ( ! success ) + return; + + data.global = true; + this.global_sets.push(set_id); + }, + + + check_margins = function(margins, height) { + var mlist = margins.split(/ +/); + if ( mlist.length != 2 ) + return margins; + + mlist[0] = parseFloat(mlist[0]); + mlist[1] = parseFloat(mlist[1]); + + if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) + return null; + + return margins; + }, + + + build_legacy_css = function(emote) { + var margin = emote.margins; + if ( ! margin ) + margin = ((emote.height - 18) / -2) + "px 0"; + return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; + }, + + + build_new_css = function(emote) { + if ( ! emote.margins && ! emote.extra_css ) + return build_legacy_css(emote); + + return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; + }, + + + build_css = build_new_css; + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.setup_emoticons = function() { + this.log("Preparing emoticon system."); + + this.emote_sets = {}; + this.global_sets = []; + this._last_emote_id = 0; + + this.log("Creating emoticon style element."); + var s = this._emote_style = document.createElement('style'); + s.id = "ffz-emoticon-css"; + document.head.appendChild(s); + + this.log("Loading global emote set."); + this.load_set("global", loaded_global.bind(this, "global")); +} + + +// --------------------- +// Set Management +// --------------------- + +FFZ.prototype.getEmotes = function(user_id, room_id) { + var user = this.users[user_id], + room = this.rooms[room_id]; + + return _.union(user && user.sets || [], room && room.sets || [], this.global_sets); +} + + +// --------------------- +// Commands +// --------------------- + +FFZ.ws_commands.reload_set = function(set_id) { + this.load_set(set_id); +} + + +// --------------------- +// Set Loading +// --------------------- + +FFZ.prototype.load_set = function(set_id, callback) { + return this._legacy_load_set(set_id, callback); +} + + +FFZ.prototype.unload_set = function(set_id) { + var set = this.emote_sets[set_id]; + if ( ! set ) + return; + + this.log("Unloading emoticons for set: " + set_id); + + utils.update_css(this._emote_style, set_id, null); + delete this.emote_sets[set_id]; + + for(var i=0; i < set.users.length; i++) { + var room = this.rooms[set.users[i]]; + if ( room ) + room.sets.removeObject(set_id); + } +} + + +FFZ.prototype._load_set_json = function(set_id, callback, data) { + // Store our set. + this.emote_sets[set_id] = data; + data.users = []; + data.global = false; + data.count = 0; + + // Iterate through all the emoticons, building CSS and regex objects as appropriate. + var output_css = ""; + + for(var key in data.emotes) { + if ( ! data.emotes.hasOwnProperty(key) ) + continue; + + var emote = data.emotes[key]; + emote.klass = "ffz-emote-" + emote.id; + + if ( emote.name[emote.name.length-1] === "!" ) + emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); + else + emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); + + output_css += build_css(emote); + data.count++; + } + + utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); + this.log("Updated emoticons for set: " + set_id, data); + this.update_ui_link(); + + if ( callback ) + callback(true, data); +} + + +FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_css(set_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return callback && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_load_set(set_id, callback, tries); + + return callback && callback(false); + }); +} + + +FFZ.prototype._legacy_load_css = function(set_id, callback, data) { + var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; + + data = data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { + height = parseInt(height); width = parseInt(width); + 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}; + + emotes[id] = emote; + return ""; + }).trim(); + + if ( data ) + data.replace(MOD_CSS, function(match, url) { + if ( output.icon || url.substr(-11) !== 'modicon.png' ) + return; + + output.icon = url; + }); + + this._load_set_json(set_id, callback, output); } },{"./constants":3,"./utils":23}],11:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, @@ -1308,166 +1308,166 @@ FFZ.prototype._emote_menu_enumerator = function() { return emotes; } },{}],13:[function(require,module,exports){ -// Modify Array and others. -require('./shims'); - - -// ---------------- -// The Constructor -// ---------------- - -var FFZ = window.FrankerFaceZ = function() { - FFZ.instance = this; - - // Get things started. - this.initialize(); -} - - -FFZ.get = function() { return FFZ.instance; } - - -// Version -var VER = FFZ.version_info = { - major: 3, minor: 0, revision: 0, - toString: function() { - return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); - } -} - - -// Logging - -FFZ.prototype.log = function(msg, data, to_json) { - msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); - if ( data !== undefined && console.groupCollapsed && console.dir ) { - console.groupCollapsed(msg); - if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) - console.log(data); - else - console.dir(data); - - console.groupEnd(msg); - } else - console.log(msg); -} - - -// ------------------- -// User Data -// ------------------- - -FFZ.prototype.get_user = function() { - if ( window.PP && PP.login ) { - return PP; - } else if ( window.App ) { - var nc = App.__container__.lookup("controller:navigation"); - return nc ? nc.get("userData") : undefined; - } -} - - -// ------------------- -// Import Everything! -// ------------------- - -require('./socket'); -require('./emoticons'); -require('./badges'); - -require('./ember/router'); -require('./ember/room'); -require('./ember/line'); -require('./ember/chatview'); -require('./ember/viewers'); -//require('./ember/teams'); - -require('./tracking'); - -require('./debug'); - -require('./ext/betterttv'); -require('./ext/emote_menu'); - -require('./featurefriday'); - -require('./ui/styles'); -require('./ui/notifications'); -require('./ui/viewer_count'); - -require('./ui/menu_button'); -require('./ui/menu'); - -require('./commands'); - - -// --------------- -// Initialization -// --------------- - -FFZ.prototype.initialize = function(increment, delay) { - // Make sure that FrankerFaceZ doesn't start setting itself up until the - // Twitch ember application is ready. - - // TODO: Special Dashboard check. - - var loaded = window.App != undefined && - App.__container__ != undefined && - App.__container__.resolve('model:room') != undefined; - - if ( !loaded ) { - increment = increment || 10; - if ( delay >= 60000 ) - this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting."); - else - setTimeout(this.initialize.bind(this, increment, (delay||0) + increment), - increment); - return; - } - - this.setup_ember(delay); -} - - -FFZ.prototype.setup_ember = function(delay) { - var start = (window.performance && performance.now) ? performance.now() : Date.now(); - this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); - - this.users = {}; - - // Cleanup localStorage - for(var key in localStorage) { - if ( key.substr(0,4) == "ffz_" ) - localStorage.removeItem(key); - } - - // Initialize all the modules. - this.ws_create(); - this.setup_emoticons(); - this.setup_badges(); - - this.setup_piwik(); - - this.setup_router(); - this.setup_room(); - this.setup_line(); - this.setup_chatview(); - this.setup_viewers(); - - //this.setup_teams(); - - this.setup_css(); - this.setup_menu(); - - this.find_bttv(10); - this.find_emote_menu(10); - - this.check_ff(); - - var end = (window.performance && performance.now) ? performance.now() : Date.now(), - duration = end - start; - - this.log("Initialization complete in " + duration + "ms"); +// Modify Array and others. +require('./shims'); + + +// ---------------- +// The Constructor +// ---------------- + +var FFZ = window.FrankerFaceZ = function() { + FFZ.instance = this; + + // Get things started. + this.initialize(); +} + + +FFZ.get = function() { return FFZ.instance; } + + +// Version +var VER = FFZ.version_info = { + major: 3, minor: 0, revision: 0, + toString: function() { + return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); + } +} + + +// Logging + +FFZ.prototype.log = function(msg, data, to_json) { + msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + if ( data !== undefined && console.groupCollapsed && console.dir ) { + console.groupCollapsed(msg); + if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) + console.log(data); + else + console.dir(data); + + console.groupEnd(msg); + } else + console.log(msg); +} + + +// ------------------- +// User Data +// ------------------- + +FFZ.prototype.get_user = function() { + if ( window.PP && PP.login ) { + return PP; + } else if ( window.App ) { + var nc = App.__container__.lookup("controller:navigation"); + return nc ? nc.get("userData") : undefined; + } +} + + +// ------------------- +// Import Everything! +// ------------------- + +require('./socket'); +require('./emoticons'); +require('./badges'); + +require('./ember/router'); +require('./ember/room'); +require('./ember/line'); +require('./ember/chatview'); +require('./ember/viewers'); +//require('./ember/teams'); + +require('./tracking'); + +require('./debug'); + +require('./ext/betterttv'); +require('./ext/emote_menu'); + +require('./featurefriday'); + +require('./ui/styles'); +require('./ui/notifications'); +require('./ui/viewer_count'); + +require('./ui/menu_button'); +require('./ui/menu'); + +require('./commands'); + + +// --------------- +// Initialization +// --------------- + +FFZ.prototype.initialize = function(increment, delay) { + // Make sure that FrankerFaceZ doesn't start setting itself up until the + // Twitch ember application is ready. + + // TODO: Special Dashboard check. + + var loaded = window.App != undefined && + App.__container__ != undefined && + App.__container__.resolve('model:room') != undefined; + + if ( !loaded ) { + increment = increment || 10; + if ( delay >= 60000 ) + this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting."); + else + setTimeout(this.initialize.bind(this, increment, (delay||0) + increment), + increment); + return; + } + + this.setup_ember(delay); +} + + +FFZ.prototype.setup_ember = function(delay) { + var start = (window.performance && performance.now) ? performance.now() : Date.now(); + this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); + + this.users = {}; + + // Cleanup localStorage + for(var key in localStorage) { + if ( key.substr(0,4) == "ffz_" ) + localStorage.removeItem(key); + } + + // Initialize all the modules. + this.ws_create(); + this.setup_emoticons(); + this.setup_badges(); + + this.setup_piwik(); + + this.setup_router(); + this.setup_room(); + this.setup_line(); + this.setup_chatview(); + this.setup_viewers(); + + //this.setup_teams(); + + this.setup_css(); + this.setup_menu(); + + this.find_bttv(10); + this.find_emote_menu(10); + + this.check_ff(); + + var end = (window.performance && performance.now) ? performance.now() : Date.now(), + duration = end - start; + + this.log("Initialization complete in " + duration + "ms"); } },{"./badges":1,"./commands":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/room":7,"./ember/router":8,"./ember/viewers":9,"./emoticons":10,"./ext/betterttv":11,"./ext/emote_menu":12,"./featurefriday":14,"./shims":15,"./socket":16,"./tracking":17,"./ui/menu":18,"./ui/menu_button":19,"./ui/notifications":20,"./ui/styles":21,"./ui/viewer_count":22}],14:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, @@ -1617,141 +1617,141 @@ FFZ.prototype._update_ff_name = function(name) { this.feature_friday.display_name = name; } },{"./constants":3}],15:[function(require,module,exports){ -Array.prototype.equals = function (array) { - // if the other array is a falsy value, return - if (!array) - return false; - - // compare lengths - can save a lot of time - if (this.length != array.length) - return false; - - for (var i = 0, l=this.length; i < l; i++) { - // Check if we have nested arrays - if (this[i] instanceof Array && array[i] instanceof Array) { - // recurse into the nested arrays - if (!this[i].equals(array[i])) - return false; - } - else if (this[i] != array[i]) { - // Warning - two different object instances will never be equal: {x:20} != {x:20} - return false; - } - } - return true; -} - +Array.prototype.equals = function (array) { + // if the other array is a falsy value, return + if (!array) + return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) + return false; + + for (var i = 0, l=this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!this[i].equals(array[i])) + return false; + } + else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + return true; +} + },{}],16:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ; - -FFZ.prototype._ws_open = false; -FFZ.prototype._ws_delay = 0; - -FFZ.ws_commands = {}; - - -// ---------------- -// Socket Creation -// ---------------- - -FFZ.prototype.ws_create = function() { - var f = this; - - this._ws_last_req = 0; - this._ws_callbacks = {}; - this._ws_pending = this._ws_pending || []; - - var ws = this._ws_sock = new WebSocket("ws://ffz.stendec.me/"); - - ws.onopen = function(e) { - f._ws_open = true; - f._ws_delay = 0; - f.log("Socket connected."); - - var user = f.get_user(); - if ( user ) - f.ws_send("setuser", user.login); - - // Send the current rooms. - for(var room_id in f.rooms) - f.ws_send("sub", room_id); - - // Send any pending commands. - var pending = f._ws_pending; - f._ws_pending = []; - - for(var i=0; i < pending.length; i++) { - var d = pending[i]; - f.ws_send(d[0], d[1], d[2]); - } - } - - ws.onclose = function(e) { - f.log("Socket closed."); - f._ws_open = false; - - // We never ever want to not have a socket. - if ( f._ws_delay < 30000 ) - f._ws_delay += 5000; - - setTimeout(f.ws_create.bind(f), f._ws_delay); - } - - ws.onmessage = function(e) { - // Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA] - var cmd, data, ind = e.data.indexOf(" "), - msg = e.data.substr(ind + 1), - request = parseInt(e.data.slice(0, ind)); - - ind = msg.indexOf(" "); - if ( ind === -1 ) - ind = msg.length; - - cmd = msg.slice(0, ind); - msg = msg.substr(ind + 1); - if ( msg ) - data = JSON.parse(msg); - - if ( request === -1 ) { - // It's a command from the server. - var command = FFZ.ws_commands[cmd]; - if ( command ) - command.bind(f)(data); - else - f.log("Invalid command: " + cmd, data); - - } else { - var success = cmd === 'True', - callback = f._ws_callbacks[request]; - f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data); - if ( callback ) { - delete f._ws_callbacks[request]; - callback(success, data); - } - } - } -} - - -FFZ.prototype.ws_send = function(func, data, callback, can_wait) { - if ( ! this._ws_open ) { - if ( can_wait ) { - var pending = this._ws_pending = this._ws_pending || []; - pending.push([func, data, callback]); - return true; - } else - return false; - } - - var request = ++this._ws_last_req; - data = data !== undefined ? " " + JSON.stringify(data) : ""; - - if ( callback ) - this._ws_callbacks[request] = callback; - - this._ws_sock.send(request + " " + func + data); - return request; +var FFZ = window.FrankerFaceZ; + +FFZ.prototype._ws_open = false; +FFZ.prototype._ws_delay = 0; + +FFZ.ws_commands = {}; + + +// ---------------- +// Socket Creation +// ---------------- + +FFZ.prototype.ws_create = function() { + var f = this; + + this._ws_last_req = 0; + this._ws_callbacks = {}; + this._ws_pending = this._ws_pending || []; + + var ws = this._ws_sock = new WebSocket("ws://ffz.stendec.me/"); + + ws.onopen = function(e) { + f._ws_open = true; + f._ws_delay = 0; + f.log("Socket connected."); + + var user = f.get_user(); + if ( user ) + f.ws_send("setuser", user.login); + + // Send the current rooms. + for(var room_id in f.rooms) + f.ws_send("sub", room_id); + + // Send any pending commands. + var pending = f._ws_pending; + f._ws_pending = []; + + for(var i=0; i < pending.length; i++) { + var d = pending[i]; + f.ws_send(d[0], d[1], d[2]); + } + } + + ws.onclose = function(e) { + f.log("Socket closed."); + f._ws_open = false; + + // We never ever want to not have a socket. + if ( f._ws_delay < 30000 ) + f._ws_delay += 5000; + + setTimeout(f.ws_create.bind(f), f._ws_delay); + } + + ws.onmessage = function(e) { + // Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA] + var cmd, data, ind = e.data.indexOf(" "), + msg = e.data.substr(ind + 1), + request = parseInt(e.data.slice(0, ind)); + + ind = msg.indexOf(" "); + if ( ind === -1 ) + ind = msg.length; + + cmd = msg.slice(0, ind); + msg = msg.substr(ind + 1); + if ( msg ) + data = JSON.parse(msg); + + if ( request === -1 ) { + // It's a command from the server. + var command = FFZ.ws_commands[cmd]; + if ( command ) + command.bind(f)(data); + else + f.log("Invalid command: " + cmd, data); + + } else { + var success = cmd === 'True', + callback = f._ws_callbacks[request]; + f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data); + if ( callback ) { + delete f._ws_callbacks[request]; + callback(success, data); + } + } + } +} + + +FFZ.prototype.ws_send = function(func, data, callback, can_wait) { + if ( ! this._ws_open ) { + if ( can_wait ) { + var pending = this._ws_pending = this._ws_pending || []; + pending.push([func, data, callback]); + return true; + } else + return false; + } + + var request = ++this._ws_last_req; + data = data !== undefined ? " " + JSON.stringify(data) : ""; + + if ( callback ) + this._ws_callbacks[request] = callback; + + this._ws_sock.send(request + " " + func + data); + return request; } },{}],17:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, @@ -1886,307 +1886,307 @@ FFZ.prototype.track_page = function() { } } },{"./constants":3}],18:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ; - - -// -------------------- -// Initializer -// -------------------- - -FFZ.prototype.setup_menu = function() { - this.log("Installing mouse-up event to auto-close menus."); - var f = this; - - jQuery(document).mouseup(function(e) { - var popup = f._popup, parent; - if ( ! popup ) return; - popup = jQuery(popup); - parent = popup.parent(); - - if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { - popup.remove(); - delete f._popup; - } - }); -} - - -// -------------------- -// Create Menu -// -------------------- - -FFZ.prototype.build_ui_popup = function(view) { - var popup = this._popup; - if ( popup ) { - popup.parentElement.removeChild(popup); - delete this._popup; - return; - } - - // Start building the DOM. - var container = document.createElement('div'), - inner = document.createElement('div'); - - container.className = 'emoticon-selector chat-menu ffz-ui-popup'; - inner.className = 'emoticon-selector-box dropmenu'; - container.appendChild(inner); - - // TODO: Modularize for multiple menu pages! - - // Get the current room. - var room_id = view.get('controller.currentRoom.id'), - room = this.rooms[room_id]; - - this.log("Menu for Room: " + room_id, room); - 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 ( 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")); - - // Feature Friday! - this._feature_friday_ui(room_id, inner, view); - - // Add the menu to the DOM. - this._popup = container; - inner.style.maxHeight = Math.max(300, view.$().height() - 171) + "px"; - view.$('.chat-interface').append(container); -} - - -// -------------------- -// Emotes for Sets -// -------------------- - -FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { - if ( header != null ) { - var el_header = document.createElement('div'); - el_header.className = 'list-header'; - el_header.appendChild(document.createTextNode(header)); - - if ( btn ) - el_header.appendChild(btn); - - parent.appendChild(el_header); - } - - var grid = document.createElement('div'), c = 0; - grid.className = 'emoticon-grid'; - - for(var i=0; i < sets.length; i++) { - var set = this.emote_sets[sets[i]]; - if ( ! set || ! set.emotes ) - continue; - - for(var eid in set.emotes) { - var emote = set.emotes[eid]; - if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) - continue; - - c++; - var s = document.createElement('span'); - s.className = 'emoticon tooltip'; - s.style.backgroundImage = 'url("' + emote.url + '")'; - s.style.width = emote.width + "px"; - s.style.height = emote.height + "px"; - s.title = emote.name; - s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); - grid.appendChild(s); - } - } - - if ( !c ) { - grid.innerHTML = "This channel has no emoticons."; - grid.className = "chat-menu-content ffz-no-emotes center"; - } - - parent.appendChild(grid); -} - - -FFZ.prototype._add_emote = function(view, emote) { - var room = view.get('controller.currentRoom'), - current_text = room.get('messageToSend') || ''; - - if ( current_text && current_text.substr(-1) !== " " ) - current_text += ' '; - - room.set('messageToSend', current_text + (emote.name || emote)); +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initializer +// -------------------- + +FFZ.prototype.setup_menu = function() { + this.log("Installing mouse-up event to auto-close menus."); + var f = this; + + jQuery(document).mouseup(function(e) { + var popup = f._popup, parent; + if ( ! popup ) return; + popup = jQuery(popup); + parent = popup.parent(); + + if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { + popup.remove(); + delete f._popup; + } + }); +} + + +// -------------------- +// Create Menu +// -------------------- + +FFZ.prototype.build_ui_popup = function(view) { + var popup = this._popup; + if ( popup ) { + popup.parentElement.removeChild(popup); + delete this._popup; + return; + } + + // Start building the DOM. + var container = document.createElement('div'), + inner = document.createElement('div'); + + container.className = 'emoticon-selector chat-menu ffz-ui-popup'; + inner.className = 'emoticon-selector-box dropmenu'; + container.appendChild(inner); + + // TODO: Modularize for multiple menu pages! + + // Get the current room. + var room_id = view.get('controller.currentRoom.id'), + room = this.rooms[room_id]; + + this.log("Menu for Room: " + room_id, room); + 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 ( 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")); + + // Feature Friday! + this._feature_friday_ui(room_id, inner, view); + + // Add the menu to the DOM. + this._popup = container; + inner.style.maxHeight = Math.max(300, view.$().height() - 171) + "px"; + view.$('.chat-interface').append(container); +} + + +// -------------------- +// Emotes for Sets +// -------------------- + +FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { + if ( header != null ) { + var el_header = document.createElement('div'); + el_header.className = 'list-header'; + el_header.appendChild(document.createTextNode(header)); + + if ( btn ) + el_header.appendChild(btn); + + parent.appendChild(el_header); + } + + var grid = document.createElement('div'), c = 0; + grid.className = 'emoticon-grid'; + + for(var i=0; i < sets.length; i++) { + var set = this.emote_sets[sets[i]]; + if ( ! set || ! set.emotes ) + continue; + + for(var eid in set.emotes) { + var emote = set.emotes[eid]; + if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) + continue; + + c++; + var s = document.createElement('span'); + s.className = 'emoticon tooltip'; + s.style.backgroundImage = 'url("' + emote.url + '")'; + s.style.width = emote.width + "px"; + s.style.height = emote.height + "px"; + s.title = emote.name; + s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); + grid.appendChild(s); + } + } + + if ( !c ) { + grid.innerHTML = "This channel has no emoticons."; + grid.className = "chat-menu-content ffz-no-emotes center"; + } + + parent.appendChild(grid); +} + + +FFZ.prototype._add_emote = function(view, emote) { + var room = view.get('controller.currentRoom'), + current_text = room.get('messageToSend') || ''; + + if ( current_text && current_text.substr(-1) !== " " ) + current_text += ' '; + + room.set('messageToSend', current_text + (emote.name || emote)); } },{}],19:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'); - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.build_ui_link = function(view) { - var link = document.createElement('a'); - link.className = 'ffz-ui-toggle'; - link.innerHTML = constants.CHAT_BUTTON; - - link.addEventListener('click', this.build_ui_popup.bind(this, view)); - - this.update_ui_link(link); - return link; -} - - -FFZ.prototype.update_ui_link = function(link) { - var controller = App.__container__.lookup('controller:chat'); - link = link || document.querySelector('a.ffz-ui-toggle'); - if ( !link || !controller ) - return; - - var room_id = controller.get('currentRoom.id'), - room = this.rooms[room_id], - has_emotes = false, - - dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false), - blue = (this.has_bttv ? BetterTTV.settings.get('showBlueButtons') : false), - live = (this.feature_friday && this.feature_friday.live); - - - // 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; - } - } - } - - link.classList.toggle('no-emotes', ! has_emotes); - link.classList.toggle('live', live); - link.classList.toggle('dark', dark); - link.classList.toggle('blue', blue); +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.build_ui_link = function(view) { + var link = document.createElement('a'); + link.className = 'ffz-ui-toggle'; + link.innerHTML = constants.CHAT_BUTTON; + + link.addEventListener('click', this.build_ui_popup.bind(this, view)); + + this.update_ui_link(link); + return link; +} + + +FFZ.prototype.update_ui_link = function(link) { + var controller = App.__container__.lookup('controller:chat'); + link = link || document.querySelector('a.ffz-ui-toggle'); + if ( !link || !controller ) + return; + + var room_id = controller.get('currentRoom.id'), + room = this.rooms[room_id], + has_emotes = false, + + dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false), + blue = (this.has_bttv ? BetterTTV.settings.get('showBlueButtons') : false), + live = (this.feature_friday && this.feature_friday.live); + + + // 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; + } + } + } + + link.classList.toggle('no-emotes', ! has_emotes); + link.classList.toggle('live', live); + link.classList.toggle('dark', dark); + link.classList.toggle('blue', blue); } },{"../constants":3}],20:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ; - -FFZ.prototype.show_notification = function(message) { - window.noty({ - text: message, - theme: "ffzTheme", - layout: "bottomCenter", - closeWith: ["button"] - }).show(); -} - - -FFZ.ws_commands.message = function(message) { - this.show_notification(message); +var FFZ = window.FrankerFaceZ; + +FFZ.prototype.show_notification = function(message) { + window.noty({ + text: message, + theme: "ffzTheme", + layout: "bottomCenter", + closeWith: ["button"] + }).show(); +} + + +FFZ.ws_commands.message = function(message) { + this.show_notification(message); } },{}],21:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'); - -FFZ.prototype.setup_css = function() { - this.log("Injecting main FrankerFaceZ CSS."); - - var s = this._main_style = document.createElement('link'); - - s.id = "ffz-ui-css"; - s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.SERVER + "script/style.css"); - document.head.appendChild(s); - - 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() {} - } - }; +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +FFZ.prototype.setup_css = function() { + this.log("Injecting main FrankerFaceZ CSS."); + + var s = this._main_style = document.createElement('link'); + + s.id = "ffz-ui-css"; + s.setAttribute('rel', 'stylesheet'); + s.setAttribute('href', constants.SERVER + "script/style.css"); + document.head.appendChild(s); + + 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}],22:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'), - utils = require('../utils'); - -// ------------ -// Set Viewers -// ------------ - -FFZ.ws_commands.viewers = function(data) { - var channel = data[0], count = data[1]; - - var controller = App.__container__.lookup('controller:channel'), - id = controller && controller.get && controller.get('id'); - - if ( id !== channel ) - return; - - var view_count = document.querySelector('.channel-stats .ffz.stat'), - content = constants.ZREKNARF + ' ' + utils.number_commas(count); - - if ( view_count ) - view_count.innerHTML = content; - else { - var parent = document.querySelector('.channel-stats'); - if ( ! parent ) - return; - - view_count = document.createElement('span'); - view_count.className = 'ffz stat'; - view_count.title = 'Viewers with FrankerFaceZ'; - view_count.innerHTML = content; - - parent.appendChild(view_count); - jQuery(view_count).tipsy(); - } +var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'); + +// ------------ +// Set Viewers +// ------------ + +FFZ.ws_commands.viewers = function(data) { + var channel = data[0], count = data[1]; + + var controller = App.__container__.lookup('controller:channel'), + id = controller && controller.get && controller.get('id'); + + if ( id !== channel ) + return; + + var view_count = document.querySelector('.channel-stats .ffz.stat'), + content = constants.ZREKNARF + ' ' + utils.number_commas(count); + + if ( view_count ) + view_count.innerHTML = content; + else { + var parent = document.querySelector('.channel-stats'); + if ( ! parent ) + return; + + view_count = document.createElement('span'); + view_count.className = 'ffz stat'; + view_count.title = 'Viewers with FrankerFaceZ'; + view_count.innerHTML = content; + + parent.appendChild(view_count); + jQuery(view_count).tipsy(); + } } },{"../constants":3,"../utils":23}],23:[function(require,module,exports){ -var FFZ = window.FrankerFaceZ, - constants = require('./constants'); - -module.exports = { - update_css: function(element, id, css) { - var all = element.innerHTML, - start = "/*BEGIN " + id + "*/", - end = "/*END " + id + "*/", - s_ind = all.indexOf(start), - e_ind = all.indexOf(end), - found = s_ind !== -1 && e_ind !== -1 && e_ind > s_ind; - - if ( !found && !css ) - return; - - if ( found ) - all = all.substr(0, s_ind) + all.substr(e_ind + end.length); - - if ( css ) - all += start + css + end; - - element.innerHTML = all; - }, - - number_commas: function(x) { - var parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - return parts.join("."); - } +var FFZ = window.FrankerFaceZ, + constants = require('./constants'); + +module.exports = { + update_css: function(element, id, css) { + var all = element.innerHTML, + start = "/*BEGIN " + id + "*/", + end = "/*END " + id + "*/", + s_ind = all.indexOf(start), + e_ind = all.indexOf(end), + found = s_ind !== -1 && e_ind !== -1 && e_ind > s_ind; + + if ( !found && !css ) + return; + + if ( found ) + all = all.substr(0, s_ind) + all.substr(e_ind + end.length); + + if ( css ) + all += start + css + end; + + element.innerHTML = all; + }, + + number_commas: function(x) { + var parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); + } } },{"./constants":3}]},{},[13]);window.ffz = new FrankerFaceZ()}(window)); \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index a5505536..0df3f324 100644 --- a/src/badges.js +++ b/src/badges.js @@ -1,191 +1,191 @@ -var FFZ = window.FrankerFaceZ, - constants = require('./constants'), - utils = require('./utils'); - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_badges = function() { - this.log("Preparing badge system."); - this.badges = {}; - - this.log("Creating badge style element."); - var s = this._badge_style = document.createElement('style'); - s.id = "ffz-badge-css"; - document.head.appendChild(s); - - this.log("Adding legacy donor badges."); - this._legacy_add_donors(); -} - - -// -------------------- -// Badge CSS -// -------------------- - -var badge_css = function(badge) { - return ".badges .ffz-badge-" + badge.id + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.extra_css || "") + '}'; -} - - -// -------------------- -// Render Badge -// -------------------- - -FFZ.prototype.bttv_badges = function(data) { - var user_id = data.sender, - user = this.users[user_id], - badges_out = [], - insert_at = -1; - - if ( ! user || ! user.badges ) - return; - - // Determine where in the list to insert these badges. - for(var i=0; i < data.badges.length; i++) { - var badge = data.badges[i]; - if ( badge.type == "subscriber" || badge.type == "turbo" ) { - insert_at = i; - break; - } - } - - - for (var slot in user.badges) { - if ( ! user.badges.hasOwnProperty(slot) ) - continue; - - var badge = user.badges[slot], - full_badge = this.badges[badge.id] || {}, - desc = badge.title || full_badge.title, - style = "", - alpha = BetterTTV.settings.get('alphaTags'); - - if ( badge.image ) - style += 'background-image: url(\\"' + badge.image + '\\"); '; - - if ( badge.color && ! alpha ) - style += 'background-color: ' + badge.color + '; '; - - if ( badge.extra_css ) - style += badge.extra_css; - - if ( style ) - desc += '" style="' + style; - - badges_out.push([(insert_at == -1 ? 1 : -1) * slot, {type: "ffz-badge-" + badge.id + (alpha ? " alpha" : ""), name: "", description: desc}]); - } - - badges_out.sort(function(a,b){return a[0] - b[0]}); - - if ( insert_at == -1 ) { - while(badges_out.length) - data.badges.push(badges_out.shift()[1]); - } else { - while(badges_out.length) - data.badges.insertAt(insert_at, badges_out.shift()[1]); - } -} - - -FFZ.prototype.render_badge = function(view) { - var user = view.get('context.model.from'), - room_id = view.get('context.parentController.content.id'), - badges = view.$('.badges'); - - var data = this.users[user]; - if ( ! data || ! data.badges ) - return; - - // Figure out where to place our badge(s). - var before = badges.find('.badge').filter(function(i) { - var t = this.title.toLowerCase(); - return t == "subscriber" || t == "turbo"; - }).first(); - - var badges_out = [], reverse = !(!before.length); - for ( var slot in data.badges ) { - if ( ! data.badges.hasOwnProperty(slot) ) - continue; - - var badge = data.badges[slot], - full_badge = this.badges[badge.id] || {}; - - var el = document.createElement('div'); - el.className = 'badge float-left tooltip ffz-badge-' + badge.id; - el.setAttribute('title', badge.title || full_badge.title); - - if ( badge.image ) - el.style.backgroundImage = 'url("' + badge.image + '")'; - - if ( badge.color ) - el.style.backgroundColor = badge.color; - - if ( badge.extra_css ) - el.style.cssText += badge.extra_css; - - badges_out.push([((reverse ? 1 : -1) * slot), el]); - } - - badges_out.sort(function(a,b){return a[0] - b[0]}); - - if ( reverse ) { - while(badges_out.length) - before.before(badges_out.shift()[1]); - } else { - while(badges_out.length) - badges.append(badges_out.shift()[1]); - } -} - - -// -------------------- -// Legacy Support -// -------------------- - -FFZ.prototype._legacy_add_donors = function(tries) { - this.badges[1] = {id: 1, title: "FFZ Donor", color: "#755000", image: "//cdn.frankerfacez.com/channel/global/donoricon.png"}; - utils.update_css(this._badge_style, 1, badge_css(this.badges[1])); - - // Developer Badges - // TODO: Upload the badge to the proper CDN. - this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "//cdn.frankerfacez.com/channel/global/devicon.png"}; - utils.update_css(this._badge_style, 0, badge_css(this.badges[0])); - this.users.sirstendec = {badges: {0: {id:0}}}; - - jQuery.ajax(constants.SERVER + "script/donors.txt", {cache: false, context: this}) - .done(function(data) { - this._legacy_parse_donors(data); - - }).fail(function(data) { - if ( data.status == 404 ) - return; - - tries = (tries || 0) + 1; - if ( tries < 10 ) - return this._legacy_add_donors(tries); - }); -} - - -FFZ.prototype._legacy_parse_donors = function(data) { - var count = 0; - if ( data != null ) { - var lines = data.trim().split(/\W+/); - for(var i=0; i < lines.length; i++) { - var user_id = lines[i], - user = this.users[user_id] = this.users[user_id] || {}, - badges = user.badges = user.badges || {}; - - if ( badges[0] ) - continue; - - badges[0] = {id:1}; - count += 1; - } - } - - this.log("Added donor badge to " + utils.number_commas(count) + " users."); +var FFZ = window.FrankerFaceZ, + constants = require('./constants'), + utils = require('./utils'); + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_badges = function() { + this.log("Preparing badge system."); + this.badges = {}; + + this.log("Creating badge style element."); + var s = this._badge_style = document.createElement('style'); + s.id = "ffz-badge-css"; + document.head.appendChild(s); + + this.log("Adding legacy donor badges."); + this._legacy_add_donors(); +} + + +// -------------------- +// Badge CSS +// -------------------- + +var badge_css = function(badge) { + return ".badges .ffz-badge-" + badge.id + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.extra_css || "") + '}'; +} + + +// -------------------- +// Render Badge +// -------------------- + +FFZ.prototype.bttv_badges = function(data) { + var user_id = data.sender, + user = this.users[user_id], + badges_out = [], + insert_at = -1; + + if ( ! user || ! user.badges ) + return; + + // Determine where in the list to insert these badges. + for(var i=0; i < data.badges.length; i++) { + var badge = data.badges[i]; + if ( badge.type == "subscriber" || badge.type == "turbo" ) { + insert_at = i; + break; + } + } + + + for (var slot in user.badges) { + if ( ! user.badges.hasOwnProperty(slot) ) + continue; + + var badge = user.badges[slot], + full_badge = this.badges[badge.id] || {}, + desc = badge.title || full_badge.title, + style = "", + alpha = BetterTTV.settings.get('alphaTags'); + + if ( badge.image ) + style += 'background-image: url(\\"' + badge.image + '\\"); '; + + if ( badge.color && ! alpha ) + style += 'background-color: ' + badge.color + '; '; + + if ( badge.extra_css ) + style += badge.extra_css; + + if ( style ) + desc += '" style="' + style; + + badges_out.push([(insert_at == -1 ? 1 : -1) * slot, {type: "ffz-badge-" + badge.id + (alpha ? " alpha" : ""), name: "", description: desc}]); + } + + badges_out.sort(function(a,b){return a[0] - b[0]}); + + if ( insert_at == -1 ) { + while(badges_out.length) + data.badges.push(badges_out.shift()[1]); + } else { + while(badges_out.length) + data.badges.insertAt(insert_at, badges_out.shift()[1]); + } +} + + +FFZ.prototype.render_badge = function(view) { + var user = view.get('context.model.from'), + room_id = view.get('context.parentController.content.id'), + badges = view.$('.badges'); + + var data = this.users[user]; + if ( ! data || ! data.badges ) + return; + + // Figure out where to place our badge(s). + var before = badges.find('.badge').filter(function(i) { + var t = this.title.toLowerCase(); + return t == "subscriber" || t == "turbo"; + }).first(); + + var badges_out = [], reverse = !(!before.length); + for ( var slot in data.badges ) { + if ( ! data.badges.hasOwnProperty(slot) ) + continue; + + var badge = data.badges[slot], + full_badge = this.badges[badge.id] || {}; + + var el = document.createElement('div'); + el.className = 'badge float-left tooltip ffz-badge-' + badge.id; + el.setAttribute('title', badge.title || full_badge.title); + + if ( badge.image ) + el.style.backgroundImage = 'url("' + badge.image + '")'; + + if ( badge.color ) + el.style.backgroundColor = badge.color; + + if ( badge.extra_css ) + el.style.cssText += badge.extra_css; + + badges_out.push([((reverse ? 1 : -1) * slot), el]); + } + + badges_out.sort(function(a,b){return a[0] - b[0]}); + + if ( reverse ) { + while(badges_out.length) + before.before(badges_out.shift()[1]); + } else { + while(badges_out.length) + badges.append(badges_out.shift()[1]); + } +} + + +// -------------------- +// Legacy Support +// -------------------- + +FFZ.prototype._legacy_add_donors = function(tries) { + this.badges[1] = {id: 1, title: "FFZ Donor", color: "#755000", image: "//cdn.frankerfacez.com/channel/global/donoricon.png"}; + utils.update_css(this._badge_style, 1, badge_css(this.badges[1])); + + // Developer Badges + // TODO: Upload the badge to the proper CDN. + this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "//cdn.frankerfacez.com/channel/global/devicon.png"}; + utils.update_css(this._badge_style, 0, badge_css(this.badges[0])); + this.users.sirstendec = {badges: {0: {id:0}}}; + + jQuery.ajax(constants.SERVER + "script/donors.txt", {cache: false, context: this}) + .done(function(data) { + this._legacy_parse_donors(data); + + }).fail(function(data) { + if ( data.status == 404 ) + return; + + tries = (tries || 0) + 1; + if ( tries < 10 ) + return this._legacy_add_donors(tries); + }); +} + + +FFZ.prototype._legacy_parse_donors = function(data) { + var count = 0; + if ( data != null ) { + var lines = data.trim().split(/\W+/); + for(var i=0; i < lines.length; i++) { + var user_id = lines[i], + user = this.users[user_id] = this.users[user_id] || {}, + badges = user.badges = user.badges || {}; + + if ( badges[0] ) + continue; + + badges[0] = {id:1}; + count += 1; + } + } + + this.log("Added donor badge to " + utils.number_commas(count) + " users."); } \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 11f85158..cb656f26 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,11 +1,11 @@ -var SVGPATH = '', - DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'); - -module.exports = { - DEBUG: DEBUG, - SERVER: DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/", - - SVGPATH: SVGPATH, - ZREKNARF: '' + SVGPATH + '', - CHAT_BUTTON: '' + SVGPATH + '' +var SVGPATH = '', + DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'); + +module.exports = { + DEBUG: DEBUG, + SERVER: DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/", + + SVGPATH: SVGPATH, + ZREKNARF: '' + SVGPATH + '', + CHAT_BUTTON: '' + SVGPATH + '' } \ No newline at end of file diff --git a/src/debug.js b/src/debug.js index fe1d2093..c91e0108 100644 --- a/src/debug.js +++ b/src/debug.js @@ -1,22 +1,22 @@ -var FFZ = window.FrankerFaceZ; - - -// ----------------------- -// Developer Mode Command -// ----------------------- - -FFZ.chat_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" ) - enabled = true; - else if ( args == "n" || args == "no" || args == "false" || args == "off" ) - enabled = false; - - if ( enabled === undefined ) - return "Developer Mode is currently " + (localStorage.ffzDebugMode == "true" ? "enabled." : "disabled."); - - localStorage.ffzDebugMode = enabled; - return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; -} - -FFZ.chat_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."; +var FFZ = window.FrankerFaceZ; + + +// ----------------------- +// Developer Mode Command +// ----------------------- + +FFZ.chat_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" ) + enabled = true; + else if ( args == "n" || args == "no" || args == "false" || args == "off" ) + enabled = false; + + if ( enabled === undefined ) + return "Developer Mode is currently " + (localStorage.ffzDebugMode == "true" ? "enabled." : "disabled."); + + localStorage.ffzDebugMode = enabled; + return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; +} + +FFZ.chat_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."; diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 66a199c4..cd3a787b 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -1,55 +1,55 @@ -var FFZ = window.FrankerFaceZ; - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_chatview = function() { - this.log("Hooking the Ember Chat view."); - - var Chat = App.__container__.resolve('view:chat'); - this._modify_cview(Chat); - - // For some reason, this doesn't work unless we create an instance of the - // chat view and then destroy it immediately. - Chat.create().destroy(); - - // Modify all existing Chat views. - for(var key in Ember.View.views) { - if ( ! Ember.View.views.hasOwnProperty(key) ) - continue; - - var view = Ember.View.views[key]; - if ( !(view instanceof Chat) ) - continue; - - this.log("Adding UI link manually to Chat view.", view); - view.$('.textarea-contain').append(this.build_ui_link(view)); - } -} - - -// -------------------- -// Modify Chat View -// -------------------- - -FFZ.prototype._modify_cview = function(view) { - var f = this; - - view.reopen({ - didInsertElement: function() { - this._super(); - this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); - }, - - willClearRender: function() { - this._super(); - this.$(".ffz-ui-toggle").remove(); - }, - - ffzUpdateLink: Ember.observer('controller.currentRoom', function() { - f.update_ui_link(); - }) - }); +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_chatview = function() { + this.log("Hooking the Ember Chat view."); + + var Chat = App.__container__.resolve('view:chat'); + this._modify_cview(Chat); + + // For some reason, this doesn't work unless we create an instance of the + // chat view and then destroy it immediately. + Chat.create().destroy(); + + // Modify all existing Chat views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Chat) ) + continue; + + this.log("Adding UI link manually to Chat view.", view); + view.$('.textarea-contain').append(this.build_ui_link(view)); + } +} + + +// -------------------- +// Modify Chat View +// -------------------- + +FFZ.prototype._modify_cview = function(view) { + var f = this; + + view.reopen({ + didInsertElement: function() { + this._super(); + this.$() && this.$('.textarea-contain').append(f.build_ui_link(this)); + }, + + willClearRender: function() { + this._super(); + this.$(".ffz-ui-toggle").remove(); + }, + + ffzUpdateLink: Ember.observer('controller.currentRoom', function() { + f.update_ui_link(); + }) + }); } \ No newline at end of file diff --git a/src/ember/line.js b/src/ember/line.js index 638607d6..40742537 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -1,167 +1,167 @@ -var FFZ = window.FrankerFaceZ; - - -// --------------------- -// Initialization -// --------------------- - -FFZ.prototype.setup_line = function() { - this.log("Hooking the Ember Line controller."); - - var Line = App.__container__.resolve('controller:line'), - f = this; - - Line.reopen({ - tokenizedMessage: function() { - // Add our own step to the tokenization procedure. - return f._emoticonize(this, this._super()); - - }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") - // TODO: Copy the new properties from the new Twitch! - }); - - - this.log("Hooking the Ember Line view."); - var Line = App.__container__.resolve('view:line'); - - Line.reopen({ - didInsertElement: function() { - this._super(); - - var el = this.get('element'), - user = this.get('context.model.from'); - - el.setAttribute('data-room', this.get('context.parentController.content.id')); - el.setAttribute('data-sender', user); - - f.render_badge(this); - - if ( localStorage['ffzCapitalize'] != 'false' ) - f.capitalize(this, user); - - } - }); - - // Store the capitalization of our own name. - var user = this.get_user(); - if ( user && user.name ) - FFZ.capitalization[user.login] = [user.name, Date.now()]; -} - - -// --------------------- -// Capitalization -// --------------------- - -FFZ.capitalization = {}; -FFZ._cap_fetching = 0; - -FFZ.get_capitalization = function(name, callback) { - name = name.toLowerCase(); - if ( name == "jtv" || name == "twitchnotify" ) - return name; - - var old_data = FFZ.capitalization[name]; - if ( old_data ) { - if ( Date.now() - old_data[1] < 3600000 ) - return old_data[0]; - } - - if ( FFZ._cap_fetching < 5 ) { - 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--; - callback && callback(cap_name); - }); - } - - return old_data ? old_data[0] : name; -} - - -FFZ.prototype.capitalize = function(view, user) { - var name = FFZ.get_capitalization(user, this.capitalize.bind(this, view)); - if ( name ) - view.$('.from').text(name); -} - - -FFZ.chat_commands.capitalization = function(room, args) { - var enabled, args = args && args.length ? args[0].toLowerCase() : null; - if ( args == "y" || args == "yes" || args == "true" || args == "on" ) - enabled = true; - else if ( args == "n" || args == "no" || args == "false" || args == "off" ) - enabled = false; - - if ( enabled === undefined ) - return "Chat Name Capitalization is currently " + (localStorage.ffzCapitalize != "false" ? "enabled." : "disabled."); - - localStorage.ffzCapitalize = enabled; - return "Chat Name Capitalization is now " + (enabled ? "enabled." : "disabled."); -} - -FFZ.chat_commands.capitalization.help = "Usage: /ffz capitalization \nEnable or disable Chat Name Capitalization. This setting does not work with BetterTTV."; - - -// --------------------- -// Emoticon Replacement -// --------------------- - -FFZ.prototype._emoticonize = function(controller, tokens) { - var room_id = controller.get("parentController.model.id"), - user_id = controller.get("model.from"), - f = this; - - // Get our sets. - var sets = this.getEmotes(user_id, room_id), - emotes = []; - - // Build a list of emotes that match. - _.each(sets, function(set_id) { - var set = f.emote_sets[set_id]; - if ( ! set ) - return; - - _.each(set.emotes, function(emote) { - _.any(tokens, function(token) { - return _.isString(token) && token.match(emote.regex); - }) && emotes.push(emote); - }); - }); - - // Don't bother proceeding if we have no emotes. - if ( ! emotes.length ) - return tokens; - - // Now that we have all the matching tokens, do crazy stuff. - if ( typeof tokens == "string" ) - tokens = [tokens]; - - // This is weird stuff I basically copied from the old Twitch code. - // Here, for each emote, we split apart every text token and we - // put it back together with the matching bits of text replaced - // with an object telling Twitch's line template how to render the - // 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)}; - - tokens = _.compact(_.flatten(_.map(tokens, function(token) { - if ( _.isObject(token) ) - return token; - - var tbits = token.split(emote.regex), bits = []; - tbits.forEach(function(val, ind) { - bits.push(val); - if ( ind !== tbits.length - 1 ) - bits.push(eo); - }); - return bits; - }))); - }); - - return tokens; +var FFZ = window.FrankerFaceZ; + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.setup_line = function() { + this.log("Hooking the Ember Line controller."); + + var Line = App.__container__.resolve('controller:line'), + f = this; + + Line.reopen({ + tokenizedMessage: function() { + // Add our own step to the tokenization procedure. + return f._emoticonize(this, this._super()); + + }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") + // TODO: Copy the new properties from the new Twitch! + }); + + + this.log("Hooking the Ember Line view."); + var Line = App.__container__.resolve('view:line'); + + Line.reopen({ + didInsertElement: function() { + this._super(); + + var el = this.get('element'), + user = this.get('context.model.from'); + + el.setAttribute('data-room', this.get('context.parentController.content.id')); + el.setAttribute('data-sender', user); + + f.render_badge(this); + + if ( localStorage['ffzCapitalize'] != 'false' ) + f.capitalize(this, user); + + } + }); + + // Store the capitalization of our own name. + var user = this.get_user(); + if ( user && user.name ) + FFZ.capitalization[user.login] = [user.name, Date.now()]; +} + + +// --------------------- +// Capitalization +// --------------------- + +FFZ.capitalization = {}; +FFZ._cap_fetching = 0; + +FFZ.get_capitalization = function(name, callback) { + name = name.toLowerCase(); + if ( name == "jtv" || name == "twitchnotify" ) + return name; + + var old_data = FFZ.capitalization[name]; + if ( old_data ) { + if ( Date.now() - old_data[1] < 3600000 ) + return old_data[0]; + } + + if ( FFZ._cap_fetching < 5 ) { + 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--; + callback && callback(cap_name); + }); + } + + return old_data ? old_data[0] : name; +} + + +FFZ.prototype.capitalize = function(view, user) { + var name = FFZ.get_capitalization(user, this.capitalize.bind(this, view)); + if ( name ) + view.$('.from').text(name); +} + + +FFZ.chat_commands.capitalization = function(room, args) { + var enabled, args = args && args.length ? args[0].toLowerCase() : null; + if ( args == "y" || args == "yes" || args == "true" || args == "on" ) + enabled = true; + else if ( args == "n" || args == "no" || args == "false" || args == "off" ) + enabled = false; + + if ( enabled === undefined ) + return "Chat Name Capitalization is currently " + (localStorage.ffzCapitalize != "false" ? "enabled." : "disabled."); + + localStorage.ffzCapitalize = enabled; + return "Chat Name Capitalization is now " + (enabled ? "enabled." : "disabled."); +} + +FFZ.chat_commands.capitalization.help = "Usage: /ffz capitalization \nEnable or disable Chat Name Capitalization. This setting does not work with BetterTTV."; + + +// --------------------- +// Emoticon Replacement +// --------------------- + +FFZ.prototype._emoticonize = function(controller, tokens) { + var room_id = controller.get("parentController.model.id"), + user_id = controller.get("model.from"), + f = this; + + // Get our sets. + var sets = this.getEmotes(user_id, room_id), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emotes, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Now that we have all the matching tokens, do crazy stuff. + if ( typeof tokens == "string" ) + tokens = [tokens]; + + // This is weird stuff I basically copied from the old Twitch code. + // Here, for each emote, we split apart every text token and we + // put it back together with the matching bits of text replaced + // with an object telling Twitch's line template how to render the + // 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)}; + + tokens = _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(emote.regex), bits = []; + tbits.forEach(function(val, ind) { + bits.push(val); + if ( ind !== tbits.length - 1 ) + bits.push(eo); + }); + return bits; + }))); + }); + + return tokens; } \ No newline at end of file diff --git a/src/ember/room.js b/src/ember/room.js index c4f227c6..138cec2f 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -1,281 +1,281 @@ -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*['"]([^'"]+)['"][^}]+(?:}|$)/, - GROUP_CHAT = /^_([^_]+)_\d+$/, - constants = require('../constants'), - utils = require('../utils'), - - - moderator_css = function(room) { - if ( ! room.moderator_badge ) - return ""; - - return '.chat-line[data-room="' + room.id + '"] .badges .moderator { background-image:url("' + room.moderator_badge + '") !important; }'; - } - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_room = function() { - this.rooms = {}; - - this.log("Creating room style element."); - var s = this._room_style = document.createElement("style"); - s.id = "ffz-room-css"; - document.head.appendChild(s); - - this.log("Hooking the Ember Room model."); - - var Room = App.__container__.resolve('model:room'); - this._modify_room(Room); - - // Modify all current instances of Room, as the changes to the base - // class won't be inherited automatically. - var instances = Room.instances; - for(var key in instances) { - if ( ! instances.hasOwnProperty(key) ) - continue; - - var inst = instances[key]; - this.add_room(inst.id, inst); - this._modify_room(inst); - } -} - - -// -------------------- -// Command System -// -------------------- - -FFZ.chat_commands = {}; - - -FFZ.prototype.room_message = function(room, text) { - var lines = text.split("\n"); - if ( this.has_bttv ) { - for(var i=0; i < lines.length; i++) - BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); - - } else { - for(var i=0; i < lines.length; i++) - room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]}); - } -} - - -FFZ.prototype.run_command = function(text, room_id) { - var room = this.rooms[room_id]; - if ( ! room || !room.room ) - return; - - if ( ! text ) { - // Try to pop-up the menu. - var link = document.querySelector('a.ffz-ui-toggle'); - if ( link ) - return link.click(); - - text = "help"; - } - - var args = text.split(" "), - cmd = args.shift().toLowerCase(); - - this.log("Received Command: " + cmd, args, true); - - var command = FFZ.chat_commands[cmd], output; - if ( command ) { - try { - output = command.bind(this)(room, args); - } catch(err) { - this.log("Error Running Command - " + cmd + ": " + err, room); - output = "There was an error running the command."; - } - } else - output = 'There is no "' + cmd + '" command.'; - - if ( output ) - this.room_message(room, output); -} - - -FFZ.chat_commands.help = function(room, args) { - if ( args && args.length ) { - var command = FFZ.chat_commands[args[0].toLowerCase()]; - if ( ! command ) - return 'There is no "' + args[0] + '" command.'; - - else if ( ! command.help ) - return 'No help is available for the command "' + args[0] + '".'; - - else - return command.help; - } - - var cmds = []; - for(var c in FFZ.chat_commands) - FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); - - return "The available commands are: " + cmds.join(", "); -} - -FFZ.chat_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; - - -// -------------------- -// Room Management -// -------------------- - -FFZ.prototype.add_room = function(id, room) { - if ( this.rooms[id] ) - return this.log("Tried to add existing room: " + id); - - this.log("Adding Room: " + id); - - // Create a basic data table for this room. - this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; - - // Let the server know where we are. - this.ws_send("sub", id); - - // For now, we use the legacy function to grab the .css file. - this._legacy_add_room(id); -} - - -FFZ.prototype.remove_room = function(id) { - var room = this.rooms[id]; - if ( ! room ) - return; - - this.log("Removing Room: " + id); - - // Remove the CSS - if ( room.css || room.moderator_badge ) - utils.update_css(this._room_style, id, null); - - // Let the server know we're gone and delete our data for this room. - this.ws_send("unsub", id); - delete this.rooms[id]; - - // Clean up sets we aren't using any longer. - for(var i=0; i < room.sets.length; i++) { - var set_id = room.sets[i], set = this.emote_sets[set_id]; - if ( ! set ) - continue; - - set.users.removeObject(id); - if ( !set.global && !set.users.length ) - this.unload_set(set_id); - } -} - - -// -------------------- -// Receiving Set Info -// -------------------- - -FFZ.prototype.load_room = function(room_id, callback) { - return this._legacy_load_room(room_id, callback); -} - - -FFZ.prototype._load_room_json = function(room_id, callback, data) { - // Preserve the pointer to the Room instance. - if ( this.rooms[room_id] ) - data.room = this.rooms[room_id].room; - - this.rooms[room_id] = data; - - if ( data.css || data.moderator_badge ) - utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); - - for(var i=0; i < data.sets.length; i++) { - var set_id = data.sets[i]; - if ( ! this.emote_sets.hasOwnProperty(set_id) ) - this.load_set(set_id); - } - - this.update_ui_link(); - - if ( callback ) - callback(true, data); -} - - -// -------------------- -// Ember Modifications -// -------------------- - -FFZ.prototype._modify_room = function(room) { - var f = this; - room.reopen({ - init: function() { - this._super(); - f.add_room(this.id, this); - }, - - willDestroy: function() { - this._super(); - f.remove_room(this.id); - }, - - send: function(text) { - var cmd = text.split(' ', 1)[0].toLowerCase(); - if ( cmd === "/ffz" ) { - this.set("messageToSend", ""); - f.run_command(text.substr(5), this.get('id')); - } else - return this._super(text); - } - }); -} - - -// -------------------- -// Legacy Data Support -// -------------------- - -FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_room_css(room_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return this._legacy_load_room_css(room_id, callback, null); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_add_room(room_id, callback, tries); - }); -} - - -FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { - var set_id = room_id, - match = set_id.match(GROUP_CHAT); - - if ( match && match[1] ) - set_id = match[1]; - - var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; - - if ( data ) - data = data.replace(CSS, "").trim(); - - if ( data ) { - data = data.replace(MOD_CSS, function(match, url) { - if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) - return match; - - output.moderator_badge = url; - return ""; - }); - } - - output.css = data || null; - return this._load_room_json(room_id, callback, output); +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*['"]([^'"]+)['"][^}]+(?:}|$)/, + GROUP_CHAT = /^_([^_]+)_\d+$/, + constants = require('../constants'), + utils = require('../utils'), + + + moderator_css = function(room) { + if ( ! room.moderator_badge ) + return ""; + + return '.chat-line[data-room="' + room.id + '"] .badges .moderator { background-image:url("' + room.moderator_badge + '") !important; }'; + } + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_room = function() { + this.rooms = {}; + + this.log("Creating room style element."); + var s = this._room_style = document.createElement("style"); + s.id = "ffz-room-css"; + document.head.appendChild(s); + + this.log("Hooking the Ember Room model."); + + var Room = App.__container__.resolve('model:room'); + this._modify_room(Room); + + // Modify all current instances of Room, as the changes to the base + // class won't be inherited automatically. + var instances = Room.instances; + for(var key in instances) { + if ( ! instances.hasOwnProperty(key) ) + continue; + + var inst = instances[key]; + this.add_room(inst.id, inst); + this._modify_room(inst); + } +} + + +// -------------------- +// Command System +// -------------------- + +FFZ.chat_commands = {}; + + +FFZ.prototype.room_message = function(room, text) { + var lines = text.split("\n"); + if ( this.has_bttv ) { + for(var i=0; i < lines.length; i++) + BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); + + } else { + for(var i=0; i < lines.length; i++) + room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]}); + } +} + + +FFZ.prototype.run_command = function(text, room_id) { + var room = this.rooms[room_id]; + if ( ! room || !room.room ) + return; + + if ( ! text ) { + // Try to pop-up the menu. + var link = document.querySelector('a.ffz-ui-toggle'); + if ( link ) + return link.click(); + + text = "help"; + } + + var args = text.split(" "), + cmd = args.shift().toLowerCase(); + + this.log("Received Command: " + cmd, args, true); + + var command = FFZ.chat_commands[cmd], output; + if ( command ) { + try { + output = command.bind(this)(room, args); + } catch(err) { + this.log("Error Running Command - " + cmd + ": " + err, room); + output = "There was an error running the command."; + } + } else + output = 'There is no "' + cmd + '" command.'; + + if ( output ) + this.room_message(room, output); +} + + +FFZ.chat_commands.help = function(room, args) { + if ( args && args.length ) { + var command = FFZ.chat_commands[args[0].toLowerCase()]; + if ( ! command ) + return 'There is no "' + args[0] + '" command.'; + + else if ( ! command.help ) + return 'No help is available for the command "' + args[0] + '".'; + + else + return command.help; + } + + var cmds = []; + for(var c in FFZ.chat_commands) + FFZ.chat_commands.hasOwnProperty(c) && cmds.push(c); + + return "The available commands are: " + cmds.join(", "); +} + +FFZ.chat_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; + + +// -------------------- +// Room Management +// -------------------- + +FFZ.prototype.add_room = function(id, room) { + if ( this.rooms[id] ) + return this.log("Tried to add existing room: " + id); + + this.log("Adding Room: " + id); + + // Create a basic data table for this room. + this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; + + // Let the server know where we are. + this.ws_send("sub", id); + + // For now, we use the legacy function to grab the .css file. + this._legacy_add_room(id); +} + + +FFZ.prototype.remove_room = function(id) { + var room = this.rooms[id]; + if ( ! room ) + return; + + this.log("Removing Room: " + id); + + // Remove the CSS + if ( room.css || room.moderator_badge ) + utils.update_css(this._room_style, id, null); + + // Let the server know we're gone and delete our data for this room. + this.ws_send("unsub", id); + delete this.rooms[id]; + + // Clean up sets we aren't using any longer. + for(var i=0; i < room.sets.length; i++) { + var set_id = room.sets[i], set = this.emote_sets[set_id]; + if ( ! set ) + continue; + + set.users.removeObject(id); + if ( !set.global && !set.users.length ) + this.unload_set(set_id); + } +} + + +// -------------------- +// Receiving Set Info +// -------------------- + +FFZ.prototype.load_room = function(room_id, callback) { + return this._legacy_load_room(room_id, callback); +} + + +FFZ.prototype._load_room_json = function(room_id, callback, data) { + // Preserve the pointer to the Room instance. + if ( this.rooms[room_id] ) + data.room = this.rooms[room_id].room; + + this.rooms[room_id] = data; + + if ( data.css || data.moderator_badge ) + utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); + + for(var i=0; i < data.sets.length; i++) { + var set_id = data.sets[i]; + if ( ! this.emote_sets.hasOwnProperty(set_id) ) + this.load_set(set_id); + } + + this.update_ui_link(); + + if ( callback ) + callback(true, data); +} + + +// -------------------- +// Ember Modifications +// -------------------- + +FFZ.prototype._modify_room = function(room) { + var f = this; + room.reopen({ + init: function() { + this._super(); + f.add_room(this.id, this); + }, + + willDestroy: function() { + this._super(); + f.remove_room(this.id); + }, + + send: function(text) { + var cmd = text.split(' ', 1)[0].toLowerCase(); + if ( cmd === "/ffz" ) { + this.set("messageToSend", ""); + f.run_command(text.substr(5), this.get('id')); + } else + return this._super(text); + } + }); +} + + +// -------------------- +// Legacy Data Support +// -------------------- + +FFZ.prototype._legacy_add_room = function(room_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + room_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_room_css(room_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return this._legacy_load_room_css(room_id, callback, null); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_add_room(room_id, callback, tries); + }); +} + + +FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { + var set_id = room_id, + match = set_id.match(GROUP_CHAT); + + if ( match && match[1] ) + set_id = match[1]; + + var output = {id: room_id, menu_sets: [set_id], sets: [set_id], moderator_badge: null, css: null}; + + if ( data ) + data = data.replace(CSS, "").trim(); + + if ( data ) { + data = data.replace(MOD_CSS, function(match, url) { + if ( output.moderator_badge || url.substr(-11) !== 'modicon.png' ) + return match; + + output.moderator_badge = url; + return ""; + }); + } + + output.css = data || null; + return this._load_room_json(room_id, callback, output); } \ No newline at end of file diff --git a/src/emoticons.js b/src/emoticons.js index 251e94c1..cc393712 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -1,197 +1,197 @@ -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*['"]([^'"]+)['"][^}]+(?:}|$)/, - constants = require('./constants'), - utils = require('./utils'), - - - loaded_global = function(set_id, success, data) { - if ( ! success ) - return; - - data.global = true; - this.global_sets.push(set_id); - }, - - - check_margins = function(margins, height) { - var mlist = margins.split(/ +/); - if ( mlist.length != 2 ) - return margins; - - mlist[0] = parseFloat(mlist[0]); - mlist[1] = parseFloat(mlist[1]); - - if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) - return null; - - return margins; - }, - - - build_legacy_css = function(emote) { - var margin = emote.margins; - if ( ! margin ) - margin = ((emote.height - 18) / -2) + "px 0"; - return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; - }, - - - build_new_css = function(emote) { - if ( ! emote.margins && ! emote.extra_css ) - return build_legacy_css(emote); - - return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; - }, - - - build_css = build_new_css; - - -// --------------------- -// Initialization -// --------------------- - -FFZ.prototype.setup_emoticons = function() { - this.log("Preparing emoticon system."); - - this.emote_sets = {}; - this.global_sets = []; - this._last_emote_id = 0; - - this.log("Creating emoticon style element."); - var s = this._emote_style = document.createElement('style'); - s.id = "ffz-emoticon-css"; - document.head.appendChild(s); - - this.log("Loading global emote set."); - this.load_set("global", loaded_global.bind(this, "global")); -} - - -// --------------------- -// Set Management -// --------------------- - -FFZ.prototype.getEmotes = function(user_id, room_id) { - var user = this.users[user_id], - room = this.rooms[room_id]; - - return _.union(user && user.sets || [], room && room.sets || [], this.global_sets); -} - - -// --------------------- -// Commands -// --------------------- - -FFZ.ws_commands.reload_set = function(set_id) { - this.load_set(set_id); -} - - -// --------------------- -// Set Loading -// --------------------- - -FFZ.prototype.load_set = function(set_id, callback) { - return this._legacy_load_set(set_id, callback); -} - - -FFZ.prototype.unload_set = function(set_id) { - var set = this.emote_sets[set_id]; - if ( ! set ) - return; - - this.log("Unloading emoticons for set: " + set_id); - - utils.update_css(this._emote_style, set_id, null); - delete this.emote_sets[set_id]; - - for(var i=0; i < set.users.length; i++) { - var room = this.rooms[set.users[i]]; - if ( room ) - room.sets.removeObject(set_id); - } -} - - -FFZ.prototype._load_set_json = function(set_id, callback, data) { - // Store our set. - this.emote_sets[set_id] = data; - data.users = []; - data.global = false; - data.count = 0; - - // Iterate through all the emoticons, building CSS and regex objects as appropriate. - var output_css = ""; - - for(var key in data.emotes) { - if ( ! data.emotes.hasOwnProperty(key) ) - continue; - - var emote = data.emotes[key]; - emote.klass = "ffz-emote-" + emote.id; - - if ( emote.name[emote.name.length-1] === "!" ) - emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); - else - emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); - - output_css += build_css(emote); - data.count++; - } - - utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); - this.log("Updated emoticons for set: " + set_id, data); - this.update_ui_link(); - - if ( callback ) - callback(true, data); -} - - -FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { - jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) - .done(function(data) { - this._legacy_load_css(set_id, callback, data); - - }).fail(function(data) { - if ( data.status == 404 ) - return callback && callback(false); - - tries = tries || 0; - tries++; - if ( tries < 10 ) - return this._legacy_load_set(set_id, callback, tries); - - return callback && callback(false); - }); -} - - -FFZ.prototype._legacy_load_css = function(set_id, callback, data) { - var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; - - data = data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { - height = parseInt(height); width = parseInt(width); - 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}; - - emotes[id] = emote; - return ""; - }).trim(); - - if ( data ) - data.replace(MOD_CSS, function(match, url) { - if ( output.icon || url.substr(-11) !== 'modicon.png' ) - return; - - output.icon = url; - }); - - this._load_set_json(set_id, callback, output); +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*['"]([^'"]+)['"][^}]+(?:}|$)/, + constants = require('./constants'), + utils = require('./utils'), + + + loaded_global = function(set_id, success, data) { + if ( ! success ) + return; + + data.global = true; + this.global_sets.push(set_id); + }, + + + check_margins = function(margins, height) { + var mlist = margins.split(/ +/); + if ( mlist.length != 2 ) + return margins; + + mlist[0] = parseFloat(mlist[0]); + mlist[1] = parseFloat(mlist[1]); + + if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) + return null; + + return margins; + }, + + + build_legacy_css = function(emote) { + var margin = emote.margins; + if ( ! margin ) + margin = ((emote.height - 18) / -2) + "px 0"; + return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; + }, + + + build_new_css = function(emote) { + if ( ! emote.margins && ! emote.extra_css ) + return build_legacy_css(emote); + + return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; + }, + + + build_css = build_new_css; + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.setup_emoticons = function() { + this.log("Preparing emoticon system."); + + this.emote_sets = {}; + this.global_sets = []; + this._last_emote_id = 0; + + this.log("Creating emoticon style element."); + var s = this._emote_style = document.createElement('style'); + s.id = "ffz-emoticon-css"; + document.head.appendChild(s); + + this.log("Loading global emote set."); + this.load_set("global", loaded_global.bind(this, "global")); +} + + +// --------------------- +// Set Management +// --------------------- + +FFZ.prototype.getEmotes = function(user_id, room_id) { + var user = this.users[user_id], + room = this.rooms[room_id]; + + return _.union(user && user.sets || [], room && room.sets || [], this.global_sets); +} + + +// --------------------- +// Commands +// --------------------- + +FFZ.ws_commands.reload_set = function(set_id) { + this.load_set(set_id); +} + + +// --------------------- +// Set Loading +// --------------------- + +FFZ.prototype.load_set = function(set_id, callback) { + return this._legacy_load_set(set_id, callback); +} + + +FFZ.prototype.unload_set = function(set_id) { + var set = this.emote_sets[set_id]; + if ( ! set ) + return; + + this.log("Unloading emoticons for set: " + set_id); + + utils.update_css(this._emote_style, set_id, null); + delete this.emote_sets[set_id]; + + for(var i=0; i < set.users.length; i++) { + var room = this.rooms[set.users[i]]; + if ( room ) + room.sets.removeObject(set_id); + } +} + + +FFZ.prototype._load_set_json = function(set_id, callback, data) { + // Store our set. + this.emote_sets[set_id] = data; + data.users = []; + data.global = false; + data.count = 0; + + // Iterate through all the emoticons, building CSS and regex objects as appropriate. + var output_css = ""; + + for(var key in data.emotes) { + if ( ! data.emotes.hasOwnProperty(key) ) + continue; + + var emote = data.emotes[key]; + emote.klass = "ffz-emote-" + emote.id; + + if ( emote.name[emote.name.length-1] === "!" ) + emote.regex = new RegExp("\\b" + emote.name + "(?=\\W|$)", "g"); + else + emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); + + output_css += build_css(emote); + data.count++; + } + + utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); + this.log("Updated emoticons for set: " + set_id, data); + this.update_ui_link(); + + if ( callback ) + callback(true, data); +} + + +FFZ.prototype._legacy_load_set = function(set_id, callback, tries) { + jQuery.ajax(constants.SERVER + "channel/" + set_id + ".css", {cache: false, context:this}) + .done(function(data) { + this._legacy_load_css(set_id, callback, data); + + }).fail(function(data) { + if ( data.status == 404 ) + return callback && callback(false); + + tries = tries || 0; + tries++; + if ( tries < 10 ) + return this._legacy_load_set(set_id, callback, tries); + + return callback && callback(false); + }); +} + + +FFZ.prototype._legacy_load_css = function(set_id, callback, data) { + var emotes = {}, output = {id: set_id, emotes: emotes, extra_css: null}, f = this; + + data = data.replace(CSS, function(match, klass, name, path, height, width, margins, extra) { + height = parseInt(height); width = parseInt(width); + 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}; + + emotes[id] = emote; + return ""; + }).trim(); + + if ( data ) + data.replace(MOD_CSS, function(match, url) { + if ( output.icon || url.substr(-11) !== 'modicon.png' ) + return; + + output.icon = url; + }); + + this._load_set_json(set_id, callback, output); } \ No newline at end of file diff --git a/src/main.js b/src/main.js index f2117ee3..be3e1826 100644 --- a/src/main.js +++ b/src/main.js @@ -1,161 +1,161 @@ -// Modify Array and others. -require('./shims'); - - -// ---------------- -// The Constructor -// ---------------- - -var FFZ = window.FrankerFaceZ = function() { - FFZ.instance = this; - - // Get things started. - this.initialize(); -} - - -FFZ.get = function() { return FFZ.instance; } - - -// Version -var VER = FFZ.version_info = { - major: 3, minor: 0, revision: 0, - toString: function() { - return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); - } -} - - -// Logging - -FFZ.prototype.log = function(msg, data, to_json) { - msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); - if ( data !== undefined && console.groupCollapsed && console.dir ) { - console.groupCollapsed(msg); - if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) - console.log(data); - else - console.dir(data); - - console.groupEnd(msg); - } else - console.log(msg); -} - - -// ------------------- -// User Data -// ------------------- - -FFZ.prototype.get_user = function() { - if ( window.PP && PP.login ) { - return PP; - } else if ( window.App ) { - var nc = App.__container__.lookup("controller:navigation"); - return nc ? nc.get("userData") : undefined; - } -} - - -// ------------------- -// Import Everything! -// ------------------- - -require('./socket'); -require('./emoticons'); -require('./badges'); - -require('./ember/router'); -require('./ember/room'); -require('./ember/line'); -require('./ember/chatview'); -require('./ember/viewers'); -//require('./ember/teams'); - -require('./tracking'); - -require('./debug'); - -require('./ext/betterttv'); -require('./ext/emote_menu'); - -require('./featurefriday'); - -require('./ui/styles'); -require('./ui/notifications'); -require('./ui/viewer_count'); - -require('./ui/menu_button'); -require('./ui/menu'); - -require('./commands'); - - -// --------------- -// Initialization -// --------------- - -FFZ.prototype.initialize = function(increment, delay) { - // Make sure that FrankerFaceZ doesn't start setting itself up until the - // Twitch ember application is ready. - - // TODO: Special Dashboard check. - - var loaded = window.App != undefined && - App.__container__ != undefined && - App.__container__.resolve('model:room') != undefined; - - if ( !loaded ) { - increment = increment || 10; - if ( delay >= 60000 ) - this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting."); - else - setTimeout(this.initialize.bind(this, increment, (delay||0) + increment), - increment); - return; - } - - this.setup_ember(delay); -} - - -FFZ.prototype.setup_ember = function(delay) { - var start = (window.performance && performance.now) ? performance.now() : Date.now(); - this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); - - this.users = {}; - - // Cleanup localStorage - for(var key in localStorage) { - if ( key.substr(0,4) == "ffz_" ) - localStorage.removeItem(key); - } - - // Initialize all the modules. - this.ws_create(); - this.setup_emoticons(); - this.setup_badges(); - - this.setup_piwik(); - - this.setup_router(); - this.setup_room(); - this.setup_line(); - this.setup_chatview(); - this.setup_viewers(); - - //this.setup_teams(); - - this.setup_css(); - this.setup_menu(); - - this.find_bttv(10); - this.find_emote_menu(10); - - this.check_ff(); - - var end = (window.performance && performance.now) ? performance.now() : Date.now(), - duration = end - start; - - this.log("Initialization complete in " + duration + "ms"); +// Modify Array and others. +require('./shims'); + + +// ---------------- +// The Constructor +// ---------------- + +var FFZ = window.FrankerFaceZ = function() { + FFZ.instance = this; + + // Get things started. + this.initialize(); +} + + +FFZ.get = function() { return FFZ.instance; } + + +// Version +var VER = FFZ.version_info = { + major: 3, minor: 0, revision: 0, + toString: function() { + return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); + } +} + + +// Logging + +FFZ.prototype.log = function(msg, data, to_json) { + msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + if ( data !== undefined && console.groupCollapsed && console.dir ) { + console.groupCollapsed(msg); + if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) + console.log(data); + else + console.dir(data); + + console.groupEnd(msg); + } else + console.log(msg); +} + + +// ------------------- +// User Data +// ------------------- + +FFZ.prototype.get_user = function() { + if ( window.PP && PP.login ) { + return PP; + } else if ( window.App ) { + var nc = App.__container__.lookup("controller:navigation"); + return nc ? nc.get("userData") : undefined; + } +} + + +// ------------------- +// Import Everything! +// ------------------- + +require('./socket'); +require('./emoticons'); +require('./badges'); + +require('./ember/router'); +require('./ember/room'); +require('./ember/line'); +require('./ember/chatview'); +require('./ember/viewers'); +//require('./ember/teams'); + +require('./tracking'); + +require('./debug'); + +require('./ext/betterttv'); +require('./ext/emote_menu'); + +require('./featurefriday'); + +require('./ui/styles'); +require('./ui/notifications'); +require('./ui/viewer_count'); + +require('./ui/menu_button'); +require('./ui/menu'); + +require('./commands'); + + +// --------------- +// Initialization +// --------------- + +FFZ.prototype.initialize = function(increment, delay) { + // Make sure that FrankerFaceZ doesn't start setting itself up until the + // Twitch ember application is ready. + + // TODO: Special Dashboard check. + + var loaded = window.App != undefined && + App.__container__ != undefined && + App.__container__.resolve('model:room') != undefined; + + if ( !loaded ) { + increment = increment || 10; + if ( delay >= 60000 ) + this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting."); + else + setTimeout(this.initialize.bind(this, increment, (delay||0) + increment), + increment); + return; + } + + this.setup_ember(delay); +} + + +FFZ.prototype.setup_ember = function(delay) { + var start = (window.performance && performance.now) ? performance.now() : Date.now(); + this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); + + this.users = {}; + + // Cleanup localStorage + for(var key in localStorage) { + if ( key.substr(0,4) == "ffz_" ) + localStorage.removeItem(key); + } + + // Initialize all the modules. + this.ws_create(); + this.setup_emoticons(); + this.setup_badges(); + + this.setup_piwik(); + + this.setup_router(); + this.setup_room(); + this.setup_line(); + this.setup_chatview(); + this.setup_viewers(); + + //this.setup_teams(); + + this.setup_css(); + this.setup_menu(); + + this.find_bttv(10); + this.find_emote_menu(10); + + this.check_ff(); + + var end = (window.performance && performance.now) ? performance.now() : Date.now(), + duration = end - start; + + this.log("Initialization complete in " + duration + "ms"); } \ No newline at end of file diff --git a/src/shims.js b/src/shims.js index e062c67f..7bac5cb8 100644 --- a/src/shims.js +++ b/src/shims.js @@ -1,24 +1,24 @@ -Array.prototype.equals = function (array) { - // if the other array is a falsy value, return - if (!array) - return false; - - // compare lengths - can save a lot of time - if (this.length != array.length) - return false; - - for (var i = 0, l=this.length; i < l; i++) { - // Check if we have nested arrays - if (this[i] instanceof Array && array[i] instanceof Array) { - // recurse into the nested arrays - if (!this[i].equals(array[i])) - return false; - } - else if (this[i] != array[i]) { - // Warning - two different object instances will never be equal: {x:20} != {x:20} - return false; - } - } - return true; -} - +Array.prototype.equals = function (array) { + // if the other array is a falsy value, return + if (!array) + return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) + return false; + + for (var i = 0, l=this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!this[i].equals(array[i])) + return false; + } + else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + return true; +} + diff --git a/src/socket.js b/src/socket.js index ffb84135..ad4fbec0 100644 --- a/src/socket.js +++ b/src/socket.js @@ -1,110 +1,110 @@ -var FFZ = window.FrankerFaceZ; - -FFZ.prototype._ws_open = false; -FFZ.prototype._ws_delay = 0; - -FFZ.ws_commands = {}; - - -// ---------------- -// Socket Creation -// ---------------- - -FFZ.prototype.ws_create = function() { - var f = this; - - this._ws_last_req = 0; - this._ws_callbacks = {}; - this._ws_pending = this._ws_pending || []; - - var ws = this._ws_sock = new WebSocket("ws://ffz.stendec.me/"); - - ws.onopen = function(e) { - f._ws_open = true; - f._ws_delay = 0; - f.log("Socket connected."); - - var user = f.get_user(); - if ( user ) - f.ws_send("setuser", user.login); - - // Send the current rooms. - for(var room_id in f.rooms) - f.ws_send("sub", room_id); - - // Send any pending commands. - var pending = f._ws_pending; - f._ws_pending = []; - - for(var i=0; i < pending.length; i++) { - var d = pending[i]; - f.ws_send(d[0], d[1], d[2]); - } - } - - ws.onclose = function(e) { - f.log("Socket closed."); - f._ws_open = false; - - // We never ever want to not have a socket. - if ( f._ws_delay < 30000 ) - f._ws_delay += 5000; - - setTimeout(f.ws_create.bind(f), f._ws_delay); - } - - ws.onmessage = function(e) { - // Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA] - var cmd, data, ind = e.data.indexOf(" "), - msg = e.data.substr(ind + 1), - request = parseInt(e.data.slice(0, ind)); - - ind = msg.indexOf(" "); - if ( ind === -1 ) - ind = msg.length; - - cmd = msg.slice(0, ind); - msg = msg.substr(ind + 1); - if ( msg ) - data = JSON.parse(msg); - - if ( request === -1 ) { - // It's a command from the server. - var command = FFZ.ws_commands[cmd]; - if ( command ) - command.bind(f)(data); - else - f.log("Invalid command: " + cmd, data); - - } else { - var success = cmd === 'True', - callback = f._ws_callbacks[request]; - f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data); - if ( callback ) { - delete f._ws_callbacks[request]; - callback(success, data); - } - } - } -} - - -FFZ.prototype.ws_send = function(func, data, callback, can_wait) { - if ( ! this._ws_open ) { - if ( can_wait ) { - var pending = this._ws_pending = this._ws_pending || []; - pending.push([func, data, callback]); - return true; - } else - return false; - } - - var request = ++this._ws_last_req; - data = data !== undefined ? " " + JSON.stringify(data) : ""; - - if ( callback ) - this._ws_callbacks[request] = callback; - - this._ws_sock.send(request + " " + func + data); - return request; +var FFZ = window.FrankerFaceZ; + +FFZ.prototype._ws_open = false; +FFZ.prototype._ws_delay = 0; + +FFZ.ws_commands = {}; + + +// ---------------- +// Socket Creation +// ---------------- + +FFZ.prototype.ws_create = function() { + var f = this; + + this._ws_last_req = 0; + this._ws_callbacks = {}; + this._ws_pending = this._ws_pending || []; + + var ws = this._ws_sock = new WebSocket("ws://ffz.stendec.me/"); + + ws.onopen = function(e) { + f._ws_open = true; + f._ws_delay = 0; + f.log("Socket connected."); + + var user = f.get_user(); + if ( user ) + f.ws_send("setuser", user.login); + + // Send the current rooms. + for(var room_id in f.rooms) + f.ws_send("sub", room_id); + + // Send any pending commands. + var pending = f._ws_pending; + f._ws_pending = []; + + for(var i=0; i < pending.length; i++) { + var d = pending[i]; + f.ws_send(d[0], d[1], d[2]); + } + } + + ws.onclose = function(e) { + f.log("Socket closed."); + f._ws_open = false; + + // We never ever want to not have a socket. + if ( f._ws_delay < 30000 ) + f._ws_delay += 5000; + + setTimeout(f.ws_create.bind(f), f._ws_delay); + } + + ws.onmessage = function(e) { + // Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA] + var cmd, data, ind = e.data.indexOf(" "), + msg = e.data.substr(ind + 1), + request = parseInt(e.data.slice(0, ind)); + + ind = msg.indexOf(" "); + if ( ind === -1 ) + ind = msg.length; + + cmd = msg.slice(0, ind); + msg = msg.substr(ind + 1); + if ( msg ) + data = JSON.parse(msg); + + if ( request === -1 ) { + // It's a command from the server. + var command = FFZ.ws_commands[cmd]; + if ( command ) + command.bind(f)(data); + else + f.log("Invalid command: " + cmd, data); + + } else { + var success = cmd === 'True', + callback = f._ws_callbacks[request]; + f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data); + if ( callback ) { + delete f._ws_callbacks[request]; + callback(success, data); + } + } + } +} + + +FFZ.prototype.ws_send = function(func, data, callback, can_wait) { + if ( ! this._ws_open ) { + if ( can_wait ) { + var pending = this._ws_pending = this._ws_pending || []; + pending.push([func, data, callback]); + return true; + } else + return false; + } + + var request = ++this._ws_last_req; + data = data !== undefined ? " " + JSON.stringify(data) : ""; + + if ( callback ) + this._ws_callbacks[request] = callback; + + this._ws_sock.send(request + " " + func + data); + return request; } \ No newline at end of file diff --git a/src/ui/menu.js b/src/ui/menu.js index ab37e6d8..9c325218 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -1,143 +1,143 @@ -var FFZ = window.FrankerFaceZ; - - -// -------------------- -// Initializer -// -------------------- - -FFZ.prototype.setup_menu = function() { - this.log("Installing mouse-up event to auto-close menus."); - var f = this; - - jQuery(document).mouseup(function(e) { - var popup = f._popup, parent; - if ( ! popup ) return; - popup = jQuery(popup); - parent = popup.parent(); - - if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { - popup.remove(); - delete f._popup; - } - }); -} - - -// -------------------- -// Create Menu -// -------------------- - -FFZ.prototype.build_ui_popup = function(view) { - var popup = this._popup; - if ( popup ) { - popup.parentElement.removeChild(popup); - delete this._popup; - return; - } - - // Start building the DOM. - var container = document.createElement('div'), - inner = document.createElement('div'); - - container.className = 'emoticon-selector chat-menu ffz-ui-popup'; - inner.className = 'emoticon-selector-box dropmenu'; - container.appendChild(inner); - - // TODO: Modularize for multiple menu pages! - - // Get the current room. - var room_id = view.get('controller.currentRoom.id'), - room = this.rooms[room_id]; - - this.log("Menu for Room: " + room_id, room); - 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 ( 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")); - - // Feature Friday! - this._feature_friday_ui(room_id, inner, view); - - // Add the menu to the DOM. - this._popup = container; - inner.style.maxHeight = Math.max(300, view.$().height() - 171) + "px"; - view.$('.chat-interface').append(container); -} - - -// -------------------- -// Emotes for Sets -// -------------------- - -FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { - if ( header != null ) { - var el_header = document.createElement('div'); - el_header.className = 'list-header'; - el_header.appendChild(document.createTextNode(header)); - - if ( btn ) - el_header.appendChild(btn); - - parent.appendChild(el_header); - } - - var grid = document.createElement('div'), c = 0; - grid.className = 'emoticon-grid'; - - for(var i=0; i < sets.length; i++) { - var set = this.emote_sets[sets[i]]; - if ( ! set || ! set.emotes ) - continue; - - for(var eid in set.emotes) { - var emote = set.emotes[eid]; - if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) - continue; - - c++; - var s = document.createElement('span'); - s.className = 'emoticon tooltip'; - s.style.backgroundImage = 'url("' + emote.url + '")'; - s.style.width = emote.width + "px"; - s.style.height = emote.height + "px"; - s.title = emote.name; - s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); - grid.appendChild(s); - } - } - - if ( !c ) { - grid.innerHTML = "This channel has no emoticons."; - grid.className = "chat-menu-content ffz-no-emotes center"; - } - - parent.appendChild(grid); -} - - -FFZ.prototype._add_emote = function(view, emote) { - var room = view.get('controller.currentRoom'), - current_text = room.get('messageToSend') || ''; - - if ( current_text && current_text.substr(-1) !== " " ) - current_text += ' '; - - room.set('messageToSend', current_text + (emote.name || emote)); +var FFZ = window.FrankerFaceZ; + + +// -------------------- +// Initializer +// -------------------- + +FFZ.prototype.setup_menu = function() { + this.log("Installing mouse-up event to auto-close menus."); + var f = this; + + jQuery(document).mouseup(function(e) { + var popup = f._popup, parent; + if ( ! popup ) return; + popup = jQuery(popup); + parent = popup.parent(); + + if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { + popup.remove(); + delete f._popup; + } + }); +} + + +// -------------------- +// Create Menu +// -------------------- + +FFZ.prototype.build_ui_popup = function(view) { + var popup = this._popup; + if ( popup ) { + popup.parentElement.removeChild(popup); + delete this._popup; + return; + } + + // Start building the DOM. + var container = document.createElement('div'), + inner = document.createElement('div'); + + container.className = 'emoticon-selector chat-menu ffz-ui-popup'; + inner.className = 'emoticon-selector-box dropmenu'; + container.appendChild(inner); + + // TODO: Modularize for multiple menu pages! + + // Get the current room. + var room_id = view.get('controller.currentRoom.id'), + room = this.rooms[room_id]; + + this.log("Menu for Room: " + room_id, room); + 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 ( 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")); + + // Feature Friday! + this._feature_friday_ui(room_id, inner, view); + + // Add the menu to the DOM. + this._popup = container; + inner.style.maxHeight = Math.max(300, view.$().height() - 171) + "px"; + view.$('.chat-interface').append(container); +} + + +// -------------------- +// Emotes for Sets +// -------------------- + +FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { + if ( header != null ) { + var el_header = document.createElement('div'); + el_header.className = 'list-header'; + el_header.appendChild(document.createTextNode(header)); + + if ( btn ) + el_header.appendChild(btn); + + parent.appendChild(el_header); + } + + var grid = document.createElement('div'), c = 0; + grid.className = 'emoticon-grid'; + + for(var i=0; i < sets.length; i++) { + var set = this.emote_sets[sets[i]]; + if ( ! set || ! set.emotes ) + continue; + + for(var eid in set.emotes) { + var emote = set.emotes[eid]; + if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) + continue; + + c++; + var s = document.createElement('span'); + s.className = 'emoticon tooltip'; + s.style.backgroundImage = 'url("' + emote.url + '")'; + s.style.width = emote.width + "px"; + s.style.height = emote.height + "px"; + s.title = emote.name; + s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); + grid.appendChild(s); + } + } + + if ( !c ) { + grid.innerHTML = "This channel has no emoticons."; + grid.className = "chat-menu-content ffz-no-emotes center"; + } + + parent.appendChild(grid); +} + + +FFZ.prototype._add_emote = function(view, emote) { + var room = view.get('controller.currentRoom'), + current_text = room.get('messageToSend') || ''; + + if ( current_text && current_text.substr(-1) !== " " ) + current_text += ' '; + + room.set('messageToSend', current_text + (emote.name || emote)); } \ No newline at end of file diff --git a/src/ui/menu_button.js b/src/ui/menu_button.js index 8a2fe0d0..1aa67c31 100644 --- a/src/ui/menu_button.js +++ b/src/ui/menu_button.js @@ -1,50 +1,50 @@ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'); - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.build_ui_link = function(view) { - var link = document.createElement('a'); - link.className = 'ffz-ui-toggle'; - link.innerHTML = constants.CHAT_BUTTON; - - link.addEventListener('click', this.build_ui_popup.bind(this, view)); - - this.update_ui_link(link); - return link; -} - - -FFZ.prototype.update_ui_link = function(link) { - var controller = App.__container__.lookup('controller:chat'); - link = link || document.querySelector('a.ffz-ui-toggle'); - if ( !link || !controller ) - return; - - var room_id = controller.get('currentRoom.id'), - room = this.rooms[room_id], - has_emotes = false, - - dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false), - blue = (this.has_bttv ? BetterTTV.settings.get('showBlueButtons') : false), - live = (this.feature_friday && this.feature_friday.live); - - - // 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; - } - } - } - - link.classList.toggle('no-emotes', ! has_emotes); - link.classList.toggle('live', live); - link.classList.toggle('dark', dark); - link.classList.toggle('blue', blue); +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.build_ui_link = function(view) { + var link = document.createElement('a'); + link.className = 'ffz-ui-toggle'; + link.innerHTML = constants.CHAT_BUTTON; + + link.addEventListener('click', this.build_ui_popup.bind(this, view)); + + this.update_ui_link(link); + return link; +} + + +FFZ.prototype.update_ui_link = function(link) { + var controller = App.__container__.lookup('controller:chat'); + link = link || document.querySelector('a.ffz-ui-toggle'); + if ( !link || !controller ) + return; + + var room_id = controller.get('currentRoom.id'), + room = this.rooms[room_id], + has_emotes = false, + + dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false), + blue = (this.has_bttv ? BetterTTV.settings.get('showBlueButtons') : false), + live = (this.feature_friday && this.feature_friday.live); + + + // 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; + } + } + } + + link.classList.toggle('no-emotes', ! has_emotes); + link.classList.toggle('live', live); + link.classList.toggle('dark', dark); + link.classList.toggle('blue', blue); } \ No newline at end of file diff --git a/src/ui/notifications.js b/src/ui/notifications.js index 06d39059..fba726b0 100644 --- a/src/ui/notifications.js +++ b/src/ui/notifications.js @@ -1,15 +1,15 @@ -var FFZ = window.FrankerFaceZ; - -FFZ.prototype.show_notification = function(message) { - window.noty({ - text: message, - theme: "ffzTheme", - layout: "bottomCenter", - closeWith: ["button"] - }).show(); -} - - -FFZ.ws_commands.message = function(message) { - this.show_notification(message); +var FFZ = window.FrankerFaceZ; + +FFZ.prototype.show_notification = function(message) { + window.noty({ + text: message, + theme: "ffzTheme", + layout: "bottomCenter", + closeWith: ["button"] + }).show(); +} + + +FFZ.ws_commands.message = function(message) { + this.show_notification(message); } \ No newline at end of file diff --git a/src/ui/styles.js b/src/ui/styles.js index 6be8a1b2..2267e7d8 100644 --- a/src/ui/styles.js +++ b/src/ui/styles.js @@ -1,24 +1,24 @@ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'); - -FFZ.prototype.setup_css = function() { - this.log("Injecting main FrankerFaceZ CSS."); - - var s = this._main_style = document.createElement('link'); - - s.id = "ffz-ui-css"; - s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.SERVER + "script/style.css"); - document.head.appendChild(s); - - 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() {} - } - }; +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); + +FFZ.prototype.setup_css = function() { + this.log("Injecting main FrankerFaceZ CSS."); + + var s = this._main_style = document.createElement('link'); + + s.id = "ffz-ui-css"; + s.setAttribute('rel', 'stylesheet'); + s.setAttribute('href', constants.SERVER + "script/style.css"); + document.head.appendChild(s); + + 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() {} + } + }; } \ No newline at end of file diff --git a/src/ui/viewer_count.js b/src/ui/viewer_count.js index 5d129073..c4e624dc 100644 --- a/src/ui/viewer_count.js +++ b/src/ui/viewer_count.js @@ -1,36 +1,36 @@ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'), - utils = require('../utils'); - -// ------------ -// Set Viewers -// ------------ - -FFZ.ws_commands.viewers = function(data) { - var channel = data[0], count = data[1]; - - var controller = App.__container__.lookup('controller:channel'), - id = controller && controller.get && controller.get('id'); - - if ( id !== channel ) - return; - - var view_count = document.querySelector('.channel-stats .ffz.stat'), - content = constants.ZREKNARF + ' ' + utils.number_commas(count); - - if ( view_count ) - view_count.innerHTML = content; - else { - var parent = document.querySelector('.channel-stats'); - if ( ! parent ) - return; - - view_count = document.createElement('span'); - view_count.className = 'ffz stat'; - view_count.title = 'Viewers with FrankerFaceZ'; - view_count.innerHTML = content; - - parent.appendChild(view_count); - jQuery(view_count).tipsy(); - } +var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'); + +// ------------ +// Set Viewers +// ------------ + +FFZ.ws_commands.viewers = function(data) { + var channel = data[0], count = data[1]; + + var controller = App.__container__.lookup('controller:channel'), + id = controller && controller.get && controller.get('id'); + + if ( id !== channel ) + return; + + var view_count = document.querySelector('.channel-stats .ffz.stat'), + content = constants.ZREKNARF + ' ' + utils.number_commas(count); + + if ( view_count ) + view_count.innerHTML = content; + else { + var parent = document.querySelector('.channel-stats'); + if ( ! parent ) + return; + + view_count = document.createElement('span'); + view_count.className = 'ffz stat'; + view_count.title = 'Viewers with FrankerFaceZ'; + view_count.innerHTML = content; + + parent.appendChild(view_count); + jQuery(view_count).tipsy(); + } } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index c3a1ec9c..fb3aa4b2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,30 +1,30 @@ -var FFZ = window.FrankerFaceZ, - constants = require('./constants'); - -module.exports = { - update_css: function(element, id, css) { - var all = element.innerHTML, - start = "/*BEGIN " + id + "*/", - end = "/*END " + id + "*/", - s_ind = all.indexOf(start), - e_ind = all.indexOf(end), - found = s_ind !== -1 && e_ind !== -1 && e_ind > s_ind; - - if ( !found && !css ) - return; - - if ( found ) - all = all.substr(0, s_ind) + all.substr(e_ind + end.length); - - if ( css ) - all += start + css + end; - - element.innerHTML = all; - }, - - number_commas: function(x) { - var parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - return parts.join("."); - } +var FFZ = window.FrankerFaceZ, + constants = require('./constants'); + +module.exports = { + update_css: function(element, id, css) { + var all = element.innerHTML, + start = "/*BEGIN " + id + "*/", + end = "/*END " + id + "*/", + s_ind = all.indexOf(start), + e_ind = all.indexOf(end), + found = s_ind !== -1 && e_ind !== -1 && e_ind > s_ind; + + if ( !found && !css ) + return; + + if ( found ) + all = all.substr(0, s_ind) + all.substr(e_ind + end.length); + + if ( css ) + all += start + css + end; + + element.innerHTML = all; + }, + + number_commas: function(x) { + var parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); + } } \ No newline at end of file diff --git a/style.css b/style.css index 189dfc34..ced142ec 100644 --- a/style.css +++ b/style.css @@ -128,4 +128,12 @@ background-repeat: no-repeat; background-position: 115% -75%; background-size: 50%; -} \ No newline at end of file +} + +.ffz-live-team-channel .ffz-game { + display: inline-block; + max-width: 150px; + text-overflow: ellipsis; + overflow: hidden; + margin-bottom: -5px; +}