diff --git a/dark.css b/dark.css
index ecaa814b..de420715 100644
--- a/dark.css
+++ b/dark.css
@@ -117,6 +117,12 @@
border-right:1px solid rgb(0,0,0)!important;
}
+.ffz-dark.ffz-portrait div#left_col .column,
+.ffz-dark.ffz-portrait div#main_col {
+ border-right: none !important;
+ border-bottom: 1px solid rgb(0,0,0) !important;
+}
+
/* stream title */
.ffz-dark span.real_title{
diff --git a/src/constants.js b/src/constants.js
index dad381a7..75e56249 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -25,6 +25,7 @@ module.exports = {
"\\:-?[\\\\/]": ":-/",
"\\;-?\\)": ";-)",
"R-?\\)": "R-)",
+ "[oO](_|\\.)[oO]": "O.o",
"[o|O](_|\\.)[o|O]": "O.o",
"\\:-?D": ":-D",
"\\:-?(o|O)": ":-O",
diff --git a/src/ember/directory.js b/src/ember/directory.js
index 31c218e2..843a8ae7 100644
--- a/src/ember/directory.js
+++ b/src/ember/directory.js
@@ -47,9 +47,6 @@ FFZ.prototype._modify_directory_video = function(dir) {
dir.reopen({
didInsertElement: function() {
this._super();
-
- f.log("New Video View", this);
- window.v = this;
}
});
diff --git a/src/ember/layout.js b/src/ember/layout.js
index 0d4de51b..8fb0c58f 100644
--- a/src/ember/layout.js
+++ b/src/ember/layout.js
@@ -5,6 +5,48 @@ var FFZ = window.FrankerFaceZ;
// Settings
// --------------------
+FFZ.settings_info.portrait_mode = {
+ type: "select",
+ options: {
+ 0: "Disabled",
+ 1: "Automatic (Use Window Aspect Ratio)",
+ 2: "Always On",
+ 3: "Automatic (Video Below)",
+ 4: "Always On (Video Below)"
+ },
+
+ value: 0,
+
+ process_value: function(val) {
+ if ( val === false )
+ return 0;
+ if ( val === true )
+ return 1;
+ if ( typeof val === "string" )
+ return parseInt(val) || 0;
+ return val;
+ },
+
+ category: "Appearance",
+ no_mobile: true,
+ no_bttv: true,
+
+ name: "Portrait Mode (Chat Below Video)",
+ help: "Display the right sidebar beneath (or above) the video player for viewing in portrait orientations.",
+
+ on_update: function(val) {
+ if ( this.has_bttv )
+ return;
+
+ var Layout = window.App && App.__container__.lookup('controller:layout');
+ if ( ! Layout )
+ return;
+
+ Layout.set('rawPortraitMode', val);
+ this._fix_menu_position();
+ }
+}
+
FFZ.settings_info.swap_sidebars = {
type: "boolean",
value: false,
@@ -94,6 +136,7 @@ FFZ.prototype.setup_layout = function() {
return;
document.body.classList.toggle("ffz-sidebar-swap", this.settings.swap_sidebars);
+ document.body.classList.toggle("ffz-portrait", this.settings.portrait_mode);
this.log("Creating layout style element.");
var s = this._layout_style = document.createElement('style');
@@ -109,20 +152,47 @@ FFZ.prototype.setup_layout = function() {
Layout.reopen({
rightColumnWidth: 340,
+ rawPortraitMode: 0,
+
+ portraitVideoBelow: false,
+
+ portraitMode: function() {
+ var raw = this.get("rawPortraitMode");
+ this.set('portraitVideoBelow', raw === 3 || raw === 4);
+
+ if ( raw === 0 )
+ return false;
+ if ( raw === 2 || raw === 4 )
+ return true;
+
+ // Not sure if I should be adding some other value to offset the ratio. What feels best?
+ var ratio = this.get("windowWidth") / (this.get("windowHeight") + 120 + 60);
+ return ratio < 1;
+
+ }.property("rawPortraitMode", "windowHeight", "windowWidth"),
isTooSmallForRightColumn: function() {
- return this.get("windowWidth") < (1090 - this.get('rightColumnWidth'))
- }.property("windowWidth", "rightColumnWidth"),
+ if ( ! f.has_bttv && this.get('portraitMode') ) {
+ var size = this.get('playerSize'),
+ height = size[1];
+
+ // Make sure we have at least a bit of room for the chat.
+ return this.get("windowHeight") < (height + 120 + 60 + 100);
+
+ } else
+ return this.get("windowWidth") < (1090 - this.get('rightColumnWidth'))
+
+ }.property("windowWidth", "rightColumnWidth", "playerSize", "windowHeight"),
contentWidth: function() {
var left_width = this.get("isLeftColumnClosed") ? 50 : 240,
- right_width = this.get("isRightColumnClosed") ? 0 : this.get("rightColumnWidth");
+ right_width = ! f.has_bttv && this.get('portraitMode') ? 0 : this.get("isRightColumnClosed") ? 0 : this.get("rightColumnWidth");
return this.get("windowWidth") - left_width - right_width - 60;
- }.property("windowWidth", "isRightColumnClosed", "isLeftColumnClosed", "rightColumnWidth"),
+ }.property("windowWidth", "portraitMode", "isRightColumnClosed", "isLeftColumnClosed", "rightColumnWidth"),
- playerStyle: function() {
+ playerSize: function() {
var h = this.get('windowHeight'),
c = this.get('PLAYER_CONTROLS_HEIGHT'),
r = this.get('contentWidth'),
@@ -135,8 +205,17 @@ FFZ.prototype.setup_layout = function() {
o = Math.floor(Math.min(i, d)),
s = Math.floor(Math.min(i, c));
- return "";
- }.property("contentWidth", "windowHeight", "PLAYER_CONTROLS_HEIGHT"),
+ return [l, o, s];
+ }.property("contentWidth", "windowHeight", "portraitMode", "PLAYER_CONTROLS_HEIGHT"),
+
+ playerStyle: function() {
+ var size = this.get('playerSize'),
+ width = size[0],
+ height = size[1],
+ host_height = size[2];
+
+ return "";
+ }.property("playerSize"),
/*ffzUpdateWidth: _.throttle(function() {
var rc = document.querySelector('#right_close');
@@ -161,11 +240,70 @@ FFZ.prototype.setup_layout = function() {
}, 200),*/
ffzUpdateCss: function() {
- var width = this.get('rightColumnWidth');
+ // TODO: Fix this mess of duplicate code.
+ var out = '';
- f._layout_style.innerHTML = '#main_col.expandRight #right_close { left: none !important; } #right_col { width: ' + width + 'px; } body:not(.ffz-sidebar-swap) #main_col:not(.expandRight) { margin-right: ' + width + 'px; } body.ffz-sidebar-swap #main_col:not(.expandRight) { margin-left: ' + width + 'px; }';
+ if ( ! f.has_bttv ) {
+ if ( this.get('portraitMode') ) {
+ var size = this.get('playerSize'),
+ height = size[1],
+ top = height + 120 + 60;
- }.observes("rightColumnWidth"),
+ if ( this.get('portraitVideoBelow') ) {
+ var wh = this.get("windowHeight"),
+ mch = wh - top;
+
+ out = (this.get('isRightColumnClosed') ? '' : 'body[data-current-path^="user."] #left_col, ') +
+ 'body[data-current-path^="user."]:not(.ffz-sidebar-swap) #main_col:not(.expandRight) { margin-right: 0 !important; top: ' + mch + 'px; height: ' + top + 'px; }' +
+ 'body[data-current-path^="user."].ffz-sidebar-swap #main_col:not(.expandRight) { margin-left: 0 !important; top: ' + mch + 'px; height: ' + top + 'px; }' +
+ 'body[data-current-path^="user."] #right_col { width: 100%; height: ' + mch + 'px; left: 0; }';
+ } else
+ out = (this.get('isRightColumnClosed') ? '' : 'body[data-current-path^="user."] #left_col, ') +
+ 'body[data-current-path^="user."]:not(.ffz-sidebar-swap) #main_col:not(.expandRight) { margin-right: 0 !important; height: ' + top + 'px; }' +
+ 'body[data-current-path^="user."].ffz-sidebar-swap #main_col:not(.expandRight) { margin-left: 0 !important; height: ' + top + 'px; }' +
+ 'body[data-current-path^="user."] #right_col { width: 100%; top: ' + top + 'px; left: 0; }';
+
+ // Theatre Mode Portrait
+ if ( true ) { //this.get('theaterPortraitMode') ) {
+ // Recalculate the player height, not including the title or anything special.
+ var width = this.get("windowWidth"),
+ wh = this.get("windowHeight"),
+ height = (9 * width / 16);
+
+ height = Math.floor(Math.max(wh * 0.1, Math.min(wh - 300, height)));
+
+ if ( this.get('portraitVideoBelow') ) {
+ var mch = this.get("windowHeight") - height;
+
+ out += (this.get('isRightColumnClosed') ? '' : 'body[data-current-path^="user."] .app-main.theatre #left_col, ') +
+ 'body[data-current-path^="user."]:not(.ffz-sidebar-swap) .app-main.theatre #main_col:not(.expandRight) { margin-right: 0 !important; top: ' + mch + 'px; height: ' + height + 'px; }' +
+ 'body[data-current-path^="user."].ffz-sidebar-swap .app-main.theatre #main_col:not(.expandRight) { margin-left: 0 !important; top: ' + mch + 'px; height: ' + height + 'px; }' +
+ 'body[data-current-path^="user."] .app-main.theatre #right_col { width: 100%; height: ' + mch + 'px; left: 0; }';
+ } else
+ out += 'body[data-current-path^="user."]:not(.ffz-sidebar-swap) .app-main.theatre #main_col:not(.expandRight) { margin-right: 0 !important; height: ' + height + 'px; }' +
+ 'body[data-current-path^="user."].ffz-sidebar-swap .app-main.theatre #main_col:not(.expandRight) { margin-left: 0 !important; height: ' + height + 'px; }' +
+ 'body[data-current-path^="user."] .app-main.theatre #right_col { width: 100%; top: ' + height + 'px; left: 0; }';
+ }
+
+ } else {
+ var width = this.get('rightColumnWidth');
+
+ out = '#main_col.expandRight #right_close { left: none !important; }' +
+ '#right_col { width: ' + width + 'px; }' +
+ 'body:not(.ffz-sidebar-swap) #main_col:not(.expandRight) { margin-right: ' + width + 'px; }' +
+ 'body.ffz-sidebar-swap #main_col:not(.expandRight) { margin-left: ' + width + 'px; }';
+ }
+
+ f._layout_style.innerHTML = out;
+ }
+
+ }.observes("isRightColumnClosed", "playerSize", "rightColumnWidth", "portraitMode", "windowHeight", "windowWidth"),
+
+ ffzUpdatePortraitCSS: function() {
+ var portrait = this.get("portraitMode");
+ document.body.classList.toggle("ffz-portrait", ! f.has_bttv && portrait);
+
+ }.observes("portraitMode"),
ffzFixTabs: function() {
if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) {
@@ -173,7 +311,7 @@ FFZ.prototype.setup_layout = function() {
f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px");
},0);
}
- }.observes("isRightColumnClosed", "rightColumnWidth")
+ }.observes("isRightColumnClosed", "rightColumnWidth", "portraitMode", "playerSize")
});
/*
@@ -191,5 +329,9 @@ FFZ.prototype.setup_layout = function() {
// Force the layout to update.
Layout.set('rightColumnWidth', this.settings.right_column_width);
+ Layout.set('rawPortraitMode', this.settings.portrait_mode);
+
+ // Force re-calculation of everything.
Ember.propertyDidChange(Layout, 'contentWidth');
+ Ember.propertyDidChange(Layout, 'windowHeight');
}
\ No newline at end of file
diff --git a/src/ember/line.js b/src/ember/line.js
index a1955756..81d51e37 100644
--- a/src/ember/line.js
+++ b/src/ember/line.js
@@ -666,7 +666,11 @@ FFZ.prototype._modify_line = function(component) {
window.open("https://twitchemotes.com/emote/" + eid);
else {
eid = e.target.getAttribute("data-ffz-emote");
- window.open("https://www.frankerfacez.com/emoticons/" + eid);
+ var es = e.target.getAttribute("data-ffz-set"),
+ set = es && f.emote_sets[es];
+
+ if ( ! set || ! set.hasOwnProperty('source_ext') )
+ window.open("https://www.frankerfacez.com/emoticons/" + eid);
}
}
diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js
index 37cf14e9..4100d297 100644
--- a/src/ember/moderation-card.js
+++ b/src/ember/moderation-card.js
@@ -448,6 +448,10 @@ FFZ.prototype.setup_mod_card = function() {
is_mod = controller.get('cardInfo.isModeratorOrHigher'),
+ chat = window.App && App.__container__.lookup('controller:chat'),
+ user = f.get_user(),
+ is_broadcaster = user && chat && chat.get('currentRoom.id') === user.login,
+
user_id = controller.get('cardInfo.user.id'),
alias = f.aliases[user_id];
@@ -633,9 +637,7 @@ FFZ.prototype.setup_mod_card = function() {
// More Fixing Other Buttons
var op_btn = el.querySelector('button.mod');
if ( op_btn ) {
- var is_owner = controller.get('cardInfo.isChannelOwner'),
- user = ffz.get_user();
- can_op = is_owner || (user && user.is_admin) || (user && user.is_staff);
+ var can_op = is_broadcaster || (user && user.is_admin) || (user && user.is_staff);
if ( ! can_op )
op_btn.parentElement.removeChild(op_btn);
diff --git a/src/ember/player.js b/src/ember/player.js
index bfa09250..46b6df6f 100644
--- a/src/ember/player.js
+++ b/src/ember/player.js
@@ -154,10 +154,6 @@ FFZ.prototype._modify_player = function(player) {
if ( ! player )
return;
- // Subscribe to the qualities event.
- //player.addEventListener('qualitieschange', this.ffzQualitiesUpdated.bind(this));
- //this.ffzQualitiesUpdated();
-
// Only set up the stats hooks if we need stats.
if ( ! player.getVideo() )
this.ffzInitStats();
@@ -175,7 +171,13 @@ FFZ.prototype._modify_player = function(player) {
// Make it so stats can no longer be disabled if we want them.
player.ffzSetStatsEnabled = player.setStatsEnabled;
- player.ffz_stats = player.getStatsEnabled();
+ try {
+ player.ffz_stats = player.getStatsEnabled();
+ } catch(err) {
+ // Assume stats are off.
+ f.log("player ffzInitStats: getStatsEnabled still doesn't work: " + err);
+ player.ffz_stats = false;
+ }
var t = this;
diff --git a/src/ember/room.js b/src/ember/room.js
index a4b04135..c0184363 100644
--- a/src/ember/room.js
+++ b/src/ember/room.js
@@ -567,7 +567,7 @@ FFZ.prototype.add_room = function(id, room) {
this.log("Adding Room: " + id);
// Create a basic data table for this room.
- var data = this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null, needs_history: false};
+ var data = this.rooms[id] = {id: id, room: room, sets: [], ext_sets: [], css: null, needs_history: false};
if ( this.follow_sets && this.follow_sets[id] ) {
data.extra_sets = this.follow_sets[id];
@@ -602,8 +602,14 @@ FFZ.prototype.add_room = function(id, room) {
// Why don't we set the scrollback length, too?
room.set('messageBufferSize', this.settings.scrollback_length + ((this._roomv && !this._roomv.get('stuckToBottom') && this._roomv.get('controller.model.id') === id) ? 150 : 0));
- // For now, we use the legacy function to grab the .css file.
+ // Load the room's data from the API.
this.load_room(id);
+
+ // Announce this room to any extension callback functions.
+ for(var api_id in this._apis) {
+ var api = this._apis[api_id];
+ api._room_callbacks(id, data);
+ }
}
diff --git a/src/ember/router.js b/src/ember/router.js
index 201179c6..26d96818 100644
--- a/src/ember/router.js
+++ b/src/ember/router.js
@@ -7,15 +7,22 @@ var FFZ = window.FrankerFaceZ;
FFZ.prototype.setup_router = function() {
this.log("Hooking the Ember router.");
+ if ( ! window.App )
+ return;
- var f = this;
- App.__container__.lookup('router:main').reopen({
- ffzTransition: function() {
- try {
- f.track_page();
- } catch(err) {
- f.error("ffzTransition: " + err);
- }
- }.on('didTransition')
- });
+ var f = this,
+ Router = App.__container__.lookup('router:main');
+
+ if ( Router )
+ Router.reopen({
+ ffzTransition: function() {
+ try {
+ document.body.setAttribute('data-current-path', App.get('currentPath'));
+ } catch(err) {
+ f.error("ffzTransition: " + err);
+ }
+ }.on('didTransition')
+ });
+
+ document.body.setAttribute('data-current-path', App.get('currentPath'));
}
\ No newline at end of file
diff --git a/src/emoticons.js b/src/emoticons.js
index d22b3f24..e82e126a 100644
--- a/src/emoticons.js
+++ b/src/emoticons.js
@@ -5,7 +5,7 @@ var FFZ = window.FrankerFaceZ,
utils = require('./utils'),
- check_margins = function(margins, height) {
+ /*check_margins = function(margins, height) {
var mlist = margins.split(/ +/);
if ( mlist.length != 2 )
return margins;
@@ -36,19 +36,17 @@ var FFZ = window.FrankerFaceZ,
}
return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.urls[1] + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (srcset ? '; ' + srcset : '') + (emote.css ? "; " + emote.css : "") + "}\n";
- },
+ },*/
- build_new_css = function(emote) {
+ build_css = function(emote) {
if ( ! emote.margins && ! emote.css )
- return build_legacy_css(emote);
+ return ""; //build_legacy_css(emote);
- return build_legacy_css(emote) + 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n";
+ return /*build_legacy_css(emote) +*/ 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n";
},
- build_css = build_new_css,
-
from_code_point = function(cp) {
var code = typeof cp === "string" ? parseInt(cp, 16) : cp;
if ( code < 0x10000)
@@ -94,6 +92,27 @@ FFZ.prototype.setup_emoticons = function() {
this.log("Watching Twitch emoticon parser to ensure it loads.");
this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000);
+
+
+ if ( this._apis ) {
+ for(var api_id in this._apis) {
+ var api = this._apis[api_id];
+ for(var es_id in api.emote_sets)
+ this.emote_sets[es_id] = api.emote_sets[es_id];
+
+ for(var i=0; i < api.global_sets.length; i++) {
+ var es_id = api.global_sets[i];
+ if ( this.global_sets.indexOf(es_id) === -1 )
+ this.global_sets.push(es_id);
+ }
+
+ for(var i=0; i < api.default_sets.length; i++) {
+ var es_id = api.default_sets[i];
+ if ( this.default_sets.indexOf(es_id) === -1 )
+ this.default_sets.push(es_id);
+ }
+ }
+ }
}
@@ -173,7 +192,7 @@ FFZ.prototype.getEmotes = function(user_id, room_id) {
var user = this.users && this.users[user_id],
room = this.rooms && this.rooms[room_id];
- return _.union(user && user.sets || [], room && room.set && [room.set] || [], room && room.extra_sets || [], this.default_sets);
+ return _.union(user && user.sets || [], room && room.set && [room.set] || [], room && room.extra_sets || [], room && room.ext_sets || [], this.default_sets);
}
@@ -205,9 +224,10 @@ FFZ.prototype._emote_tooltip = function(emote) {
var set = this.emote_sets[emote.set_id],
owner = emote.owner,
- title = set && set.title || "Global";
+ title = set && set.title || "Global",
+ source = set && set.source || "FFZ";
- emote._tooltip = "Emoticon: " + (emote.hidden ? "???" : emote.name) + "\nFFZ " + title + (owner ? "\nBy: " + owner.display_name : "");
+ emote._tooltip = "Emoticon: " + (emote.hidden ? "???" : emote.name) + "\n" + source + " " + title + (owner ? "\nBy: " + owner.display_name : "");
return emote._tooltip;
}
@@ -339,6 +359,12 @@ FFZ.prototype.unload_set = function(set_id) {
utils.update_css(this._emote_style, set_id, null);
delete this.emote_sets[set_id];
+
+ if ( set.hasOwnProperty('source_ext') ) {
+ var api = this._apis[set.source_ext];
+ if ( api && api.emote_sets && api.emote_sets[set_id] )
+ api.emote_sets[set_id] = undefined;
+ }
}
@@ -364,7 +390,7 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) {
for(var i=0; i < ems.length; i++) {
var emote = ems[i];
- emote.klass = "ffz-emote-" + emote.id;
+ //emote.klass = "ffz-emote-" + emote.id;
emote.set_id = set_id;
emote.srcSet = emote.urls[1] + " 1x";
@@ -374,9 +400,9 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) {
emote.srcSet += ", " + emote.urls[4] + " 4x";
if ( emote.name[emote.name.length-1] === "!" )
- emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")(?=\\W|$)", "g");
+ emote.regex = new RegExp("(^|\\W|\\b)(" + RegExp.escape(emote.name) + ")(?=\\W|$)", "g");
else
- emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")\\b", "g");
+ emote.regex = new RegExp("(^|\\W|\\b)(" + RegExp.escape(emote.name) + ")\\b", "g");
output_css += build_css(emote);
data.count++;
diff --git a/src/ext/api.js b/src/ext/api.js
new file mode 100644
index 00000000..75e94850
--- /dev/null
+++ b/src/ext/api.js
@@ -0,0 +1,298 @@
+var FFZ = window.FrankerFaceZ,
+ utils = require('../utils'),
+
+
+ build_css = function(emote) {
+ if ( ! emote.margins && ! emote.css )
+ return "";
+
+ return 'img[src="' + emote.urls[1] + '"]{' + (emote.margins ? 'margin:' + emote.margins + ';' : '') + (emote.css || "") + '}'
+ };
+
+
+// ---------------------
+// API Constructor
+// ---------------------
+
+var API = FFZ.API = function(instance, name, icon) {
+ this.ffz = instance || FFZ.get();
+
+ // Check for a known API!
+ if ( name ) {
+ for(var id in this.ffz._known_apis) {
+ if ( this.ffz._known_apis[id] === name ) {
+ this.id = id;
+ break;
+ }
+ }
+ }
+
+ if ( ! this.id ) {
+ var i = 0;
+ while( ! this.id ) {
+ if ( ! this.ffz._known_apis.hasOwnProperty(i) ) {
+ this.id = i;
+ break;
+ }
+ i++;
+ }
+
+ if ( name ) {
+ this.ffz._known_apis[this.id] = name;
+ localStorage.ffz_known_apis = JSON.stringify(this.ffz._known_apis);
+ }
+ }
+
+
+ this.ffz._apis[this.id] = this;
+
+ this.emote_sets = {};
+ this.global_sets = [];
+ this.default_sets = [];
+
+ this.on_room_callbacks = [];
+
+ this.name = name || ("Extension#" + this.id);
+ this.icon = icon || null;
+
+ this.ffz.log('Registered New Extension #' + this.id + ': ' + this.name);
+};
+
+
+FFZ.prototype.api = function(name, icon) {
+ // Load the known APIs list.
+ if ( ! this._known_apis ) {
+ this._known_apis = {};
+ if ( localStorage.hasOwnProperty('ffz_known_apis') )
+ try {
+ this._known_apis = JSON.parse(localStorage.ffz_known_apis);
+ } catch(err) {
+ this.log("Error loading Known APIs: " + err);
+ }
+ }
+
+ return new API(this, name, icon);
+}
+
+
+API.prototype.log = function(msg, data, to_json, log_json) {
+ this.ffz.log('Ext "' + this.name + '": ' + msg, data, to_json, log_json);
+}
+
+
+// ---------------------
+// Set Loading
+// ---------------------
+
+API.prototype._load_set = function(real_id, set_id, data) {
+ if ( ! data )
+ return false;
+
+ // Check for an existing set to copy the users.
+ var users = [];
+ if ( this.emote_sets[real_id] && this.emote_sets[real_id].users )
+ users = this.emote_sets[real_id].users;
+
+ var set = {
+ source: this.name,
+ source_ext: this.id,
+ source_id: set_id,
+ users: users,
+ count: 0,
+ emoticons: {},
+ _type: data._type || 0,
+ css: data.css || null,
+ description: data.description || null,
+ icon: data.icon || this.icon || null,
+ id: real_id,
+ title: data.title || "Global Emoticons",
+ };
+
+ this.emote_sets[real_id] = set;
+
+ if ( this.ffz.emote_sets )
+ this.ffz.emote_sets[real_id] = set;
+
+ var output_css = "",
+ ems = data.emoticons,
+ emoticons = set.emoticons;
+
+ for(var i=0; i < ems.length; i++) {
+ var emote = ems[i],
+ new_emote = {urls: {}},
+ id = emote.id || (this.name + '-' + set_id + '-' + i);
+
+ if ( ! emote.name )
+ continue;
+
+ new_emote.id = id;
+ new_emote.set_id = real_id;
+ new_emote.name = emote.name;
+
+ new_emote.width = emote.width;
+ new_emote.height = emote.height;
+
+ new_emote.hidden = emote.hidden;
+ new_emote.owner = emote.owner;
+
+ new_emote.css = emote.css;
+ new_emote.margins = emote.margins;
+
+ new_emote.srcSet = emote.urls[1] + ' 1x';
+ new_emote.urls[1] = emote.urls[1];
+
+ if ( emote.urls[2] ) {
+ new_emote.urls[2] = emote.urls[2];
+ new_emote.srcSet += ', ' + emote.urls[2] + ' 2x';
+ }
+ if ( emote.urls[3] ) {
+ new_emote.urls[3] = emote.urls[3];
+ new_emote.srcSet += ', ' + emote.urls[3] + ' 3x';
+ }
+ if ( emote.urls[4] ) {
+ new_emote.urls[4] = emote.urls[4];
+ new_emote.srcSet += ', ' + emote.urls[4] + ' 4x';
+ }
+
+ if ( emote.regex )
+ new_emote.regex = emote.regex;
+ else if ( typeof emote.name !== "string" )
+ new_emote.regex = emote.name;
+ else
+ new_emote.regex = new RegExp("(^|\\W|\\b)(" + RegExp.escape(emote.name) + ")(?=\\W|$)", "g");
+
+ output_css += build_css(new_emote);
+ set.count++;
+ emoticons[id] = new_emote;
+ }
+
+ utils.update_css(this.ffz._emote_style, real_id, output_css + (set.css || ""));
+
+ if ( this.ffz._cindex )
+ this.ffz._cindex.ffzFixTitle();
+
+ try {
+ this.ffz.update_ui_link();
+ } catch(err) { }
+
+ return set;
+}
+
+
+// ---------------------
+// Global Emote Sets
+// ---------------------
+
+API.prototype.register_global_set = function(id, set) {
+ var exact_id = this.id + '-' + id;
+
+ set.title = set.title || "Global Emoticons";
+ set._type = 0;
+ set = this._load_set(exact_id, id, set);
+
+ this.log("Loaded Emoticon Set #" + id + ": " + set.title + " (" + set.count + " emotes)", set);
+
+ if ( this.global_sets.indexOf(exact_id) === -1 )
+ this.global_sets.push(exact_id);
+
+ if ( this.default_sets.indexOf(exact_id) === -1 )
+ this.default_sets.push(exact_id);
+
+ if ( this.ffz.global_sets && this.ffz.global_sets.indexOf(exact_id) === -1 )
+ this.ffz.global_sets.push(exact_id);
+
+ if ( this.ffz.default_sets && this.ffz.default_sets.indexOf(exact_id) === -1 )
+ this.ffz.default_sets.push(exact_id);
+};
+
+
+API.prototype.unregister_global_set = function(id) {
+ var exact_id = this.id + '-' + id,
+ set = this.emote_sets[exact_id];
+
+ if ( ! set )
+ return;
+
+ utils.update_css(this.ffz._emote_style, exact_id, null);
+
+ this.emote_sets[exact_id] = undefined;
+
+ if ( this.ffz.emote_sets )
+ this.ffz.emote_sets[exact_id] = undefined;
+
+ var ind = this.global_sets.indexOf(exact_id);
+ if ( ind !== -1 )
+ this.global_sets.splice(ind,1);
+
+ ind = this.default_sets.indexOf(exact_id);
+ if ( ind !== -1 )
+ this.default_sets.splice(ind,1);
+
+ ind = this.ffz.global_sets ? this.ffz.global_sets.indexOf(exact_id) : -1;
+ if ( ind !== -1 )
+ this.ffz.global_sets.splice(ind,1);
+
+ ind = this.ffz.default_sets ? this.ffz.default_sets.indexOf(exact_id) : -1;
+ if ( ind !== -1 )
+ this.ffz.default_sets.splice(ind,1);
+
+ this.log("Unloaded Emoticon Set #" + id + ": " + set.title, set);;
+};
+
+
+// -----------------------
+// Per-Channel Emote Sets
+// -----------------------
+
+API.prototype._room_callbacks = function(room_id, room, specific_func) {
+ var api = this;
+
+ var callback = function(id, set) {
+ var exact_id = api.id + '-' + id;
+
+ set.title = set.title || "Channel: " + room_id;
+ set._type = 1;
+
+ set = api._load_set(exact_id, id, set);
+ api.log("Loaded Emoticon Set #" + id + ": " + set.title + " (" + set.count + " emotes)", set);
+
+ room.ext_sets.push(exact_id);
+ set.users.push(room_id);
+ };
+
+ if ( specific_func ) {
+ try {
+ specific_func(room_id, callback);
+ } catch(err) {
+ this.log("Error in On-Room Callback: " + err);
+ }
+
+ } else {
+ for(var i=0; i < this.on_room_callbacks.length; i++) {
+ var cb = this.on_room_callbacks[i];
+ try {
+ cb(room_id, callback);
+ } catch(err) {
+ this.log("Error in On-Room Callback: " + err);
+ }
+ }
+ }
+}
+
+
+API.prototype.register_on_room_callback = function(callback) {
+ this.on_room_callbacks.push(callback);
+
+ // Call this for all current rooms.
+ if ( this.ffz.rooms ) {
+ for(var room_id in this.ffz.rooms)
+ this._room_callbacks(room_id, this.ffz.rooms[room_id], callback);
+ }
+}
+
+
+API.prototype.unregister_on_room_callback = function(callback) {
+ var ind = this.on_room_callbacks.indexOf(callback);
+ if ( ind !== -1 )
+ this.on_room_callbacks.splice(ind, 1);
+}
\ No newline at end of file
diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js
index 4a3369de..84d5d8af 100644
--- a/src/ext/betterttv.js
+++ b/src/ext/betterttv.js
@@ -69,6 +69,7 @@ FFZ.prototype.setup_bttv = function(delay) {
document.body.classList.remove("ffz-chat-separator-wide");
document.body.classList.remove("ffz-chat-separator-3d-inset");
document.body.classList.remove("ffz-sidebar-swap");
+ document.body.classList.remove("ffz-portrait");
document.body.classList.remove("ffz-flip-dashboard");
document.body.classList.remove("ffz-transparent-badges");
document.body.classList.remove("ffz-high-contrast-chat-text");
diff --git a/src/ext/emote_menu.js b/src/ext/emote_menu.js
index 502e7880..80b83ec3 100644
--- a/src/ext/emote_menu.js
+++ b/src/ext/emote_menu.js
@@ -49,7 +49,7 @@ FFZ.prototype._emote_menu_enumerator = function() {
if ( emote.hidden )
continue;
- var title = "FrankerFaceZ " + set.title,
+ var title = (set.source || "FrankerFaceZ") + " " + set.title,
badge = set.icon || '//cdn.frankerfacez.com/script/devicon.png';
emotes.push({text: emote.name, url: emote.urls[1],
diff --git a/src/featurefriday.js b/src/featurefriday.js
index e1cbe3ec..959a48e0 100644
--- a/src/featurefriday.js
+++ b/src/featurefriday.js
@@ -17,7 +17,7 @@ FFZ.prototype.check_ff = function(tries) {
if ( ! tries )
this.log("Checking for Feature Friday data...");
- jQuery.ajax(constants.SERVER + "script/event.json", {dataType: "json", context: this})
+ jQuery.ajax(constants.SERVER + "script/event.json?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info), {dataType: "json", context: this})
.done(function(data) {
return this._load_ff(data);
}).fail(function(data) {
@@ -44,7 +44,7 @@ FFZ.ws_commands.reload_ff = function() {
// --------------------
FFZ.prototype._feature_friday_ui = function(room_id, parent, view) {
- if ( ! this.feature_friday || this.feature_friday.channel == room_id )
+ if ( ! this.feature_friday || this.feature_friday.channel === room_id )
return;
this._emotes_for_sets(parent, view, [this.feature_friday.set], this.feature_friday.title, this.feature_friday.icon, "FrankerFaceZ");
@@ -52,7 +52,7 @@ FFZ.prototype._feature_friday_ui = function(room_id, parent, view) {
// Before we add the button, make sure the channel isn't the
// current channel.
var Channel = App.__container__.lookup('controller:channel');
- if ( Channel && Channel.get('id') == this.feature_friday.channel )
+ if ( ! this.feature_friday.channel || (Channel && Channel.get('id') === this.feature_friday.channel) )
return;
@@ -98,13 +98,14 @@ FFZ.prototype._load_ff = function(data) {
}
// If there's no data, just leave.
- if ( ! data || ! data.set || ! data.channel )
+ if ( ! data || ! data.set )
return;
// We have our data! Set it up.
this.feature_friday = {set: data.set, channel: data.channel, live: false,
title: data.title || "Feature Friday",
- display_name: FFZ.get_capitalization(data.channel, this._update_ff_name.bind(this))};
+ icon: data.icon,
+ display_name: data.channel ? FFZ.get_capitalization(data.channel, this._update_ff_name.bind(this)) : data.title || "Feature Friday"};
// Add the set.
this.global_sets.push(data.set);
@@ -117,7 +118,7 @@ FFZ.prototype._load_ff = function(data) {
FFZ.prototype._update_ff_live = function() {
- if ( ! this.feature_friday )
+ if ( ! this.feature_friday || ! this.feature_friday.channel )
return;
var f = this;
diff --git a/src/main.js b/src/main.js
index 94f78330..6c655f70 100644
--- a/src/main.js
+++ b/src/main.js
@@ -10,6 +10,7 @@ var FFZ = window.FrankerFaceZ = function() {
// Logging
this._log_data = [];
+ this._apis = {};
// Get things started.
this.initialize();
@@ -21,7 +22,7 @@ FFZ.get = function() { return FFZ.instance; }
// Version
var VER = FFZ.version_info = {
- major: 3, minor: 5, revision: 30,
+ major: 3, minor: 5, revision: 36,
toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
}
@@ -115,7 +116,7 @@ require('./tokenize');
//require('./filtering');
-// Analytics: require('./ember/router');
+require('./ember/router');
require('./ember/channel');
require('./ember/player');
require('./ember/room');
@@ -149,6 +150,7 @@ require('./ui/my_emotes');
require('./ui/about_page');
require('./commands');
+require('./ext/api');
// ---------------
@@ -322,7 +324,7 @@ FFZ.prototype.init_ember = function(delay) {
this.setup_emoticons();
this.setup_badges();
- //this.setup_router();
+ this.setup_router();
this.setup_colors();
this.setup_tokenization();
//this.setup_filtering();
diff --git a/src/settings.js b/src/settings.js
index d030b343..facd1ec5 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -53,32 +53,18 @@ FFZ.prototype.load_settings = function() {
// Build a settings object.
this.settings = {};
- for(var key in FFZ.settings_info) {
- if ( ! FFZ.settings_info.hasOwnProperty(key) )
- continue;
-
- var info = FFZ.settings_info[key],
- ls_key = info.storage_key || make_ls(key),
- val = info.hasOwnProperty("value") ? info.value : undefined;
-
- if ( localStorage.hasOwnProperty(ls_key) ) {
- try {
- val = JSON.parse(localStorage.getItem(ls_key));
- } catch(err) {
- this.log('Error loading value for "' + key + '": ' + err);
- }
- }
-
- if ( info.process_value )
- val = info.process_value.bind(this)(val);
-
- this.settings[key] = val;
- }
-
// Helpers
this.settings.get = this._setting_get.bind(this);
this.settings.set = this._setting_set.bind(this);
this.settings.del = this._setting_del.bind(this);
+ this.settings.load = this._setting_load.bind(this);
+
+ for(var key in FFZ.settings_info) {
+ if ( ! FFZ.settings_info.hasOwnProperty(key) )
+ continue;
+
+ this._setting_load(key);
+ }
// Listen for Changes
window.addEventListener("storage", this._setting_update.bind(this), false);
@@ -825,6 +811,26 @@ FFZ.prototype._setting_update = function(e) {
// Settings Access
// --------------------
+FFZ.prototype._setting_load = function(key, default_value) {
+ var info = FFZ.settings_info[key],
+ ls_key = info && info.storage_key || make_ls(key),
+ val = default_value || (info && info.hasOwnProperty("value") ? info.value : undefined);
+
+ if ( localStorage.hasOwnProperty(ls_key) ) {
+ try {
+ val = JSON.parse(localStorage.getItem(ls_key));
+ } catch(err) {
+ this.log('Error loading value for "' + key + '": ' + err);
+ }
+ }
+
+ if ( info && info.process_value )
+ val = info.process_value.bind(this)(val);
+
+ this.settings[key] = val;
+}
+
+
FFZ.prototype._setting_get = function(key) {
return this.settings[key];
}
diff --git a/src/socket.js b/src/socket.js
index 6263c280..7739a086 100644
--- a/src/socket.js
+++ b/src/socket.js
@@ -29,6 +29,9 @@ FFZ.prototype.ws_iframe = function() {
FFZ.prototype.ws_create = function() {
+ // Disable sockets for now.
+ return;
+
var f = this, ws;
this._ws_last_req = 0;
diff --git a/src/tokenize.js b/src/tokenize.js
index e24c4b46..618b43c3 100644
--- a/src/tokenize.js
+++ b/src/tokenize.js
@@ -543,7 +543,7 @@ FFZ.prototype.render_tokens = function(tokens, render_links) {
tooltip = emote ? utils.sanitize(f._emote_tooltip(emote)) : token.altText;
srcset = emote ? emote.srcSet : token.srcSet;
- extra = ' data-ffz-emote="' + emote.id + '"';
+ extra = (emote ? ' data-ffz-emote="' + emote.id + '"' : '') + (emote_set ? ' data-ffz-set="' + emote_set.id + '"' : '');
} else if ( token.ffzEmoji ) {
var eid = token.ffzEmoji,
diff --git a/src/ui/menu.js b/src/ui/menu.js
index c1c54be5..7e135abb 100644
--- a/src/ui/menu.js
+++ b/src/ui/menu.js
@@ -5,7 +5,7 @@ var FFZ = window.FrankerFaceZ,
TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/",
fix_menu_position = function(container) {
- var swapped = document.body.classList.contains('ffz-sidebar-swap');
+ var swapped = document.body.classList.contains('ffz-sidebar-swap') && ! document.body.classList.contains('ffz-portrait');
var bounds = container.getBoundingClientRect(),
left = parseInt(container.style.left || '0'),
@@ -419,8 +419,11 @@ FFZ.menu_pages.channel = {
grid.className = "emoticon-grid";
header.className = "heading";
- if ( icon )
+ if ( icon ) {
header.style.backgroundImage = 'url("' + icon + '")';
+ if ( icon.indexOf('.svg') !== -1 )
+ header.style.backgroundSize = '18px';
+ }
header.innerHTML = 'TwitchSubscriber Emoticons';
grid.appendChild(header);
@@ -507,7 +510,7 @@ FFZ.menu_pages.channel = {
}
// Do we have extra sets?
- var extra_sets = room && room.extra_sets || [];
+ var extra_sets = _.union(room && room.extra_sets || [], room && room.ext_sets || [], []);
// Basic Emote Sets
this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product || extra_sets.length ) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ");
@@ -515,9 +518,9 @@ FFZ.menu_pages.channel = {
for(var i=0; i < extra_sets.length; i++) {
// Look up the set name.
var set = this.emote_sets[extra_sets[i]],
- name = set ? "Featured " + set.title : "Featured Channel";
+ name = set ? (set.hasOwnProperty('source_ext') ? "" : "Featured ") + set.title : "Featured Channel";
- this._emotes_for_sets(inner, view, [extra_sets[i]], name, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ");
+ this._emotes_for_sets(inner, view, [extra_sets[i]], name, set.icon || "//cdn.frankerfacez.com/script/devicon.png", set.source || "FrankerFaceZ");
}
// Feature Friday!
@@ -550,8 +553,11 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub
el_header.appendChild(document.createTextNode(header));
- if ( image )
+ if ( image ) {
el_header.style.backgroundImage = 'url("' + image + '")';
+ if ( image.indexOf('.svg') !== -1 )
+ el_header.style.backgroundSize = '18px';
+ }
grid.appendChild(el_header);
}
@@ -610,7 +616,7 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub
s.addEventListener('click', function(id, code, e) {
e.preventDefault();
- if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons )
+ if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons && ! set.hasOwnProperty('source_ext') )
window.open("https://www.frankerfacez.com/emoticons/" + id);
else
this._add_emote(view, code);
diff --git a/src/ui/my_emotes.js b/src/ui/my_emotes.js
index 8606cffe..2379bbec 100644
--- a/src/ui/my_emotes.js
+++ b/src/ui/my_emotes.js
@@ -269,17 +269,23 @@ FFZ.menu_pages.myemotes = {
var heading = document.createElement('div'),
menu = document.createElement('div'),
f = this,
- emotes = [];
+ emotes = [],
+
+ menu_id = set.hasOwnProperty('source_ext') ? 'ffz-ext-' + set.source_ext + '-' + set.source_id : 'ffz-' + set.id,
+ icon = set.icon || (set.hasOwnProperty('source_ext') && '//cdn.frankerfacez.com/emoji/tw-1f4ac.svg') || '//cdn.frankerfacez.com/script/devicon.png';
heading.className = 'heading';
- heading.innerHTML = 'FrankerFaceZ' + set.title;
- heading.style.backgroundImage = 'url("' + (set.icon || '//cdn.frankerfacez.com/script/devicon.png') + '")';
+ heading.innerHTML = '' + (utils.sanitize(set.source) || 'FrankerFaceZ') + '' + set.title;
+
+ heading.style.backgroundImage = 'url("' + icon + '")';
+ if ( icon.indexOf('.svg') !== -1 )
+ heading.style.backgroundSize = "18px";
menu.className = 'emoticon-grid collapsable';
menu.appendChild(heading);
- menu.setAttribute('data-set', 'ffz-' + set.id);
- menu.classList.toggle('collapsed', this.settings.emote_menu_collapsed.indexOf('ffz-' + set.id) !== -1);
+ menu.setAttribute('data-set', menu_id);
+ menu.classList.toggle('collapsed', this.settings.emote_menu_collapsed.indexOf(menu_id) !== -1);
heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this); });
for(var emote_id in set.emoticons)
@@ -325,7 +331,7 @@ FFZ.menu_pages.myemotes = {
em.title = this._emote_tooltip(emote);
em.addEventListener("click", function(id, code, e) {
e.preventDefault();
- if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons )
+ if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons && ! set.hasOwnProperty('source_ext') )
window.open("https://www.frankerfacez.com/emoticons/" + id);
else
this._add_emote(view, code);
diff --git a/style.css b/style.css
index 5cd812e0..651dd885 100644
--- a/style.css
+++ b/style.css
@@ -1673,7 +1673,11 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar
/* Swap Sidebars */
-.ffz-sidebar-swap .ember-chat .chat-interface .emoticon-selector {
+body[data-current-path^="user."].ffz-portrait #right_close { transform: rotate(90deg); }
+body[data-current-path^="user."].ffz-portrait .archives-contain .more-archives { width: 100%; }
+
+body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-interface .emoticon-selector,
+.ffz-sidebar-swap:not(.ffz-portrait) .ember-chat .chat-interface .emoticon-selector {
right: auto;
left: 20px;
}
@@ -1688,7 +1692,7 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar
left: 0;
}
-.ffz-sidebar-swap #right_close,
+.ffz-sidebar-swap:not(.ffz-portrait) #right_close,
.ffz-sidebar-swap #left_close {
transform: scaleX(-1);
}