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

3.5.328. Big refactor to how channel metadata is rendered. Follow buttons and SRL races are working again. Better debugging info for logs. Closes #37.

This commit is contained in:
SirStendec 2016-10-13 23:05:54 -04:00
parent 4e2c2f5056
commit 0e939e30ee
16 changed files with 859 additions and 902 deletions

View file

@ -1,3 +1,12 @@
<div class="list-header">3.5.328 <time datetime="2016-10-12">(2016-10-13)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Changed: Refactor channel metadata to make it easier to add with less code duplication.</li>
<li>Fixed: Follow buttons for Featured channels now appear again.</li>
<li>Fixed: SRL race data should now appear again.</li>
<li>Changed: Always show the Broadcaster separately in the viewer list, even if you're not currently watching that channel.</li>
<li>Changed: Export the entirety of the available debugging information when someone uploads logs.</li>
</ul>
<div class="list-header">3.5.327 <time datetime="2016-10-12">(2016-10-12)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: More changes to work with the new chat room manager component. Now with less breaking!</li>

View file

@ -160,7 +160,7 @@ FFZ.settings_info.hidden_badges = {
if ( new_val === null || new_val === undefined )
return;
f.settings.set("hidden_badges", _.unique(new_val.trim().toLowerCase().split(/\s*,\s*/)));
f.settings.set("hidden_badges", _.unique(new_val.trim().toLowerCase().split(/\s*,\s*/)).without(""));
}, 600
)
}

View file

@ -154,11 +154,13 @@ FFZ.prototype.cache_command_aliases = function() {
// -----------------
FFZ.ffz_commands.log = function(room, args) {
this._pastebin(this._log_data.join("\n"), function(url) {
if ( ! url )
return this.room_message(room, "There was an error uploading the FrankerFaceZ log.");
this.room_message(room, "Your FrankerFaceZ log has been pasted to: " + url);
var f = this;
this.get_debugging_info().then(function(result) {
f._pastebin(result).then(function(url) {
f.room_message(room, "Your FrankerFaceZ logs have been pasted to: " + url);
}).catch(function() {
f.room_message(room, "An error occured uploading the logs to a pastebin.");
});
});
};

View file

@ -48,38 +48,6 @@ FFZ.prototype.setup_channel = function() {
this.log("Hooking the Ember Channel controller.");
Channel.reopen({
/*isEditable: function() {
var channel_id = this.get('content.id'),
user = this.get('login.userData');
if ( ! user || ! user.login )
return false;
else if ( user.login === channel_id || user.is_admin || user.is_staff)
return true;
// Okay, have we loaded this user's editor status? Try that.
if ( f._editor_of )
return f._editor_of.indexOf(channel_id) !== -1;
var t = this;
f.get_user_editor_of().then(function(result) {
// Once editor status is loaded, if the user does have editor
// status for this channel, update this property.
if ( result.indexOf(channel_id) !== -1 )
Ember.propertyDidChange(t, 'isEditable');
});
return false;
}.property('content.id', 'login.userData', 'login.userData.login'),*/
/*ffzUpdateUptime: function() {
if ( f._cindex )
f._cindex.ffzUpdateUptime();
}.observes("isLive", "channel.id"),*/
ffzUpdateInfo: function() {
if ( this._ffz_update_timer )
clearTimeout(this._ffz_update_timer);
@ -134,17 +102,6 @@ FFZ.prototype.setup_channel = function() {
id = target && target.get('id'),
display_name = target && target.get('display_name');
/*if ( id !== f.__old_host_target ) {
if ( f.__old_host_target )
f.ws_send("unsub", "channel." + f.__old_host_target);
if ( id ) {
f.ws_send("sub", "channel." + id);
f.__old_host_target = id;
} else
delete f.__old_host_target;
}*/
if ( display_name )
FFZ.capitalization[name] = [display_name, Date.now()];
@ -163,6 +120,9 @@ FFZ.prototype.setup_channel = function() {
Channel.ffzUpdateInfo();
}
// These have to be done in order to ensure the channel metadata all sorts correctly.
FFZ.prototype.modify_channel_share_box = function(view) {
utils.ember_reopen_view(view, {
ffz_init: function() {
@ -188,6 +148,8 @@ FFZ.prototype.modify_channel_broadcast_link = function(view) {
}
// Channel Live
FFZ.prototype.modify_channel_live = function(view) {
var f = this;
utils.ember_reopen_view(view, {
@ -202,10 +164,8 @@ FFZ.prototype.modify_channel_live = function(view) {
this.ffzUpdateAttributes();
this.ffzFixTitle();
this.ffzUpdateUptime();
this.ffzUpdateChatters();
this.ffzUpdateHostButton();
this.ffzUpdatePlayerStats();
this.ffzUpdateMetadata();
if ( f.settings.auto_theater ) {
var layout = this.get('layout'),
@ -298,259 +258,183 @@ FFZ.prototype.modify_channel_live = function(view) {
el && el.html(f.render_tokens(tokens));
}.observes('channel.id', 'channel.status', 'channel.game'),
ffzUpdateUptime: function() {
if ( this._ffz_update_uptime ) {
clearTimeout(this._ffz_update_uptime);
delete this._ffz_update_uptime;
}
ffzUpdateMetadata: function(key) {
var t = this,
keys = key ? [key] : Object.keys(FFZ.channel_metadata),
basic_info = [this, this.get('channel')],
timers = this.ffz_timers = this.ffz_timers || {},
var container = this.get('element');
if ( this.isDestroyed || ! container || ! f.settings.stream_uptime || ! this.get('isLiveAccordingToKraken') )
return container && this.$("#ffz-uptime-display").remove();
container = this.get('element'),
metabar = container && container.querySelector('.cn-metabar__more');
// Schedule an update.
this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000);
// Determine when the channel last went live.
var online = this.get("channel.stream.createdAt"),
now = Date.now() - (f._ws_server_offset || 0);
var uptime = online && Math.floor((now - online.getTime()) / 1000) || -1;
if ( uptime < 0 )
return this.$("#ffz-uptime-display").remove();
var el = container.querySelector('#ffz-uptime-display span');
if ( ! el ) {
var cont = container.querySelector('.cn-metabar__more');
if ( ! cont )
return;
var stat = utils.createElement('span'),
figure = utils.createElement('figure', 'icon cn-metabar__icon', constants.CLOCK + ' '),
stat_wrapper = utils.createElement('div', 'cn-metabar__ffz html-tooltip flex__item', figure);
stat_wrapper.appendChild(stat);
stat_wrapper.id = 'ffz-uptime-display';
stat_wrapper.title = 'Stream Uptime <nobr>(since ' + online.toLocaleString() + ')</nobr>';
cont.appendChild(stat_wrapper);
el = stat;
}
el.innerHTML = utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1 || f.settings.stream_uptime === 3);
}.observes('channel.stream.createdAt', 'isLiveAccordingToKraken'),
ffzUpdatePlayerStats: function() {
if ( this._ffz_update_stats ) {
clearTimeout(this._ffz_update_stats);
this._ffz_update_stats = null;
}
// Stop scheduling this so it can die.
if ( this.isDestroyed )
// Stop once this is destroyed.
if ( this.isDestroyed || ! metabar )
return;
// Schedule an update.
if ( f.settings.player_stats )
this._ffz_update_stats = setTimeout(this.ffzUpdatePlayerStats.bind(this), 1000);
var channel_id = this.get("channel.id"),
container = this.get("element"),
player_cont = f.players && f.players[channel_id],
player, stats;
try {
player = player_cont && player_cont.get('player');
stats = player && player.getVideoInfo();
} catch(err) { } // This gets spammy if we try logging it.
if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hls_latency_broadcaster )
return container && this.$("#ffz-player-stats").remove();
var je, el = container.querySelector("#ffz-player-stats");
if ( ! el ) {
var cont = container.querySelector('.cn-metabar__more');
if ( ! cont )
return;
var stat = utils.createElement('span'),
figure = utils.createElement('figure', 'icon cn-metabar__icon', constants.GRAPH + ' ');
el = utils.createElement('div', 'cn-metabar__ffz flex__item', figure);
el.id = 'ffz-player-stats';
el.appendChild(stat);
je = jQuery(el);
je.hover(
function() { je.data("hover", true).tipsy("show") },
function() { je.data("hover", false).tipsy("hide") })
.data("hover", false)
.tipsy({
trigger: 'manual',
html: true,
gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')
});
cont.appendChild(el);
} else
je = jQuery(el)
var stat = el.querySelector('span'),
delay = Math.round(stats.hls_latency_broadcaster / 10) / 100,
dropped = utils.number_commas(stats.dropped_frames || 0),
bitrate;
if ( stats.playback_bytes_per_second )
bitrate = Math.round(stats.playback_bytes_per_second * 8 / 10.24) / 100;
else
bitrate = Math.round(stats.current_bitrate * 100) / 100;
var is_old = delay > 180;
if ( is_old ) {
delay = Math.floor(delay);
stat.textContent = utils.time_to_string(delay, true, delay > 172800) + ' old';
} else {
delay = delay.toString();
var ind = delay.indexOf('.');
delay += (ind === -1 ? '.00' : (ind >= delay.length - 2 ? '0' : '')) + 's';
stat.textContent = delay;
}
el.setAttribute('original-title', (is_old ? 'Video Information<br>' +
'Broadcast ' + utils.time_to_string(delay, true) + ' Ago<br><br>' : 'Stream Latency<br>') +
'Video: ' + stats.vid_width + 'x' + stats.vid_height + 'p ' + stats.current_fps + ' fps<br>' +
'Playback Rate: ' + bitrate + ' Kbps<br>' +
'Dropped Frames: ' + dropped);
if ( je.data("hover") )
je.tipsy("hide").tipsy("show");
for(var i=0; i < keys.length; i++)
this._ffzUpdateStat(keys[i], basic_info, timers, metabar);
},
ffzUpdateChatters: function() {
var channel_id = this.get("channel.id"),
room = f.rooms && f.rooms[channel_id],
container = this.get('element');
_ffzUpdateStat: function(key, basic_info, timers, metabar) {
var t = this,
info = FFZ.channel_metadata[key];
if ( ! info )
return;
if ( ! container || ! room || ! f.settings.chatter_count )
return container && this.$("#ffz-chatter-display").remove();
if ( timers[key] )
clearTimeout(timers[key]);
var chatter_count = Object.keys(room.room.get('ffz_chatters') || {}).length,
el = container.querySelector('#ffz-chatter-display span');
// Build the data we use for function calls.
var data = info.setup ? info.setup.apply(f, basic_info) : basic_info,
refresh = typeof info.refresh === "function" ? info.refresh.apply(f, data) : info.refresh;
if ( ! el ) {
var cont = container.querySelector('.cn-metabar__more');
if ( ! cont )
return;
// If we have a positive refresh value, schedule another go.
if ( refresh )
timers[key] = setTimeout(this.ffzUpdateMetadata.bind(this, key), typeof refresh === "number" ? refresh : 1000);
var stat = utils.createElement('span'),
figure = utils.createElement('figure', 'icon cn-metabar__icon', constants.ROOMS + ' '),
balloon = utils.createElement('div', 'balloon balloon--tooltip balloon--down balloon--center', 'Currently in Chat'),
balloon_wrapper = utils.createElement('div', 'balloon-wrapper', figure),
stat_wrapper = utils.createElement('div', 'cn-metabar__ffz flex__item mg-l-1', balloon_wrapper);
var el = metabar.querySelector('.cn-metabar__ffz[data-key="' + key + '"]'),
je,
stat,
dynamic_tooltip = typeof info.tooltip === "function",
label = typeof info.label === "function" ? info.label.apply(f, data) : info.label;
balloon_wrapper.appendChild(stat);
balloon_wrapper.appendChild(balloon);
if ( ! label ) {
if ( el )
el.parentElement.removeChild(el);
stat_wrapper.id = 'ffz-chatter-display';
if ( f._popup && f._popup.id === 'ffz-metadata-popup' && f._popup.getAttribute('data-key') === key )
f.close_popup();
var viewers = cont.querySelector('#ffz-player-stats') || cont.querySelector('#ffz-uptime-display') || cont.querySelector(".cn-metabar__livecount") || cont.querySelector(".cn-metabar__viewcount");
if ( viewers )
cont.insertBefore(stat_wrapper, viewers.nextSibling);
else
cont.appendChild(stat_wrapper);
return;
el = stat;
} else if ( ! el ) {
var btn,
static_label = typeof info.static_label === "function" ? info.static_label.apply(f, data) : info.static_label;
if ( ! static_label )
static_label = '';
else if ( static_label.substr(0,4) === '<svg' )
static_label = utils.createElement('figure', 'icon cn-metabar__icon', static_label + ' ');
if ( info.popup ) {
btn = utils.createElement('button', 'button button--dropmenu', static_label)
el = utils.createElement('div', 'cn-metabar__ffz flex__item ember-view balloon-wrapper inline-block', btn);
btn.classList.add(info.button ? 'button--hollow' : 'button--text');
} else if ( info.button ) {
btn = utils.createElement('button', 'button', static_label);
el = utils.createElement('div', 'cn-metabar__ffz flex__item ember-view inline-block', btn);
btn.classList.add(typeof info.button === 'string' ? info.button : 'button--hollow');
} else
btn = el = utils.createElement('div', 'cn-metabar__ffz flex__item', static_label);
el.setAttribute('data-key', key);
if ( info.order )
el.style.order = info.order;
if ( ! dynamic_tooltip && info.tooltip ) {
btn.classList.add('html-tooltip');
btn.title = info.tooltip;
}
stat = utils.createElement('span', 'ffz-label');
btn.appendChild(stat);
if ( dynamic_tooltip ) {
je = jQuery(btn)
je.hover(
function() { je.data("hover", true).tipsy("show") },
function() { je.data("hover", false).tipsy("hide") })
.data("hover", false)
.tipsy({
trigger: 'manual',
html: true,
gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n'),
title: function() {
var data = [t, t.get('channel')];
data = info.setup ? info.setup.apply(f, data) : data;
return info.tooltip.apply(f, data);
}
})
}
if ( info.click )
btn.addEventListener('click', function(e) {
if ( btn.disabled || btn.classList.contains('disabled') )
return false;
e.update_stat = t._ffzUpdateStat.bind(t, key, basic_info, timers, metabar);
var data = [t, t.get('channel')];
data = info.setup ? info.setup.apply(f, data) : data;
data.unshift(btn);
data.unshift(e);
return info.click.apply(f, data);
});
if ( info.popup ) {
btn.classList.add('button--dropmenu');
btn.addEventListener('click', function(el, e) {
if ( btn.disabled || btn.classList.contains('disabled') )
return false;
var popup = f.close_popup();
if ( popup && popup.id === 'ffz-metadata-popup' && popup.getAttribute('data-key') === key )
return;
var data = [t, t.get('channel')];
data = info.setup ? info.setup.apply(f, data) : data;
var balloon = utils.createElement('div', 'balloon balloon--up show');
data.unshift(balloon);
balloon.id = 'ffz-metadata-popup';
balloon.setAttribute('data-key', key);
var result = info.popup.apply(f, data);
if ( result === false )
return false;
// Set the balloon to face away from the nearest side of the channel.
var container = t.get('element'),
outer = container.getBoundingClientRect(),
rect = el.getBoundingClientRect();
balloon.classList.toggle('balloon--right', (rect.left - outer.left) > (outer.right - rect.right));
f._popup_kill = info.on_popup_close ? function() { info.on_popup_close.apply(f, data) } : null;
f._popup_allow_parent = true;
f._popup = balloon;
el.appendChild(balloon);
}.bind(this, el));
}
metabar.appendChild(el);
el = btn;
} else {
stat = el.querySelector('span.ffz-label');
if ( dynamic_tooltip )
je = jQuery(el);
}
el.innerHTML = utils.number_commas(chatter_count);
}.observes('channel.id'),
stat.innerHTML = label;
if ( dynamic_tooltip && je.data("hover") )
je.tipsy("hide").tipsy("show");
if ( info.hasOwnProperty('disabled') )
el.classList.toggle('disabled', typeof info.disabled === "function" ? info.disabled.apply(f, data) : info.disabled);
},
ffzUpdateHostButton: function() {
var t = this,
channel_id = this.get("channel.id"),
hosted_id = this.get("channel.hostModeTarget.id"),
user = f.get_user(),
room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room,
now_hosting = room && room.ffz_host_target,
hosts_left = room && room.ffz_hosts_left,
el = this.get("element"),
update_button = function(channel, container, before) {
if ( ! container )
return;
var btn = container.querySelector('#ffz-ui-host-button');
if ( ! f.settings.stream_host_button || ! user || user.login === channel ) {
if ( btn )
btn.parentElement.removeChild(btn);
return;
}
if ( ! btn ) {
btn = utils.createElement('button', 'button button--hollow mg-l-1'),
btn.id = 'ffz-ui-host-button';
btn.addEventListener('click', t.ffzClickHost.bind(t, channel !== channel_id));
if ( before )
container.insertBefore(btn, before);
else
container.appendChild(btn);
jQuery(btn).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')});
}
btn.classList.remove('disabled');
btn.innerHTML = channel === now_hosting ? 'Unhost' : 'Host';
if ( now_hosting ) {
var name = FFZ.get_capitalization(now_hosting);
btn.title = 'You are now hosting ' + f.format_display_name(name, now_hosting, true)[0] + '.';
} else
btn.title = 'You are not hosting any channel.';
if ( typeof hosts_left === 'number' )
btn.title += ' You have ' + hosts_left + ' host command' + utils.pluralize(hosts_left) + ' remaining this half hour.';
};
if ( ! el )
return;
this.set("ffz_host_updating", false);
if ( channel_id ) {
var container = el.querySelector('.cn-metabar__more'),
share = container && container.querySelector('.js-share-box');
update_button(channel_id, container, share ? share.parentElement : null);
}
if ( hosted_id )
update_button(hosted_id, el.querySelector('.cn-hosting--bottom'));
}.observes('channel.id', 'channel.hostModeTarget.id'),
ffzClickHost: function(is_host, e) {
var btn = e.target,
target = this.get(is_host ? 'channel.hostModeTarget.id' : 'channel.id'),
user = f.get_user(),
room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room,
now_hosting = room && room.ffz_host_target;
if ( ! room || this.get('ffz_host_updating') )
return;
btn.classList.add('disabled');
btn.title = 'Updating...';
this.set('ffz_host_updating', true);
if ( now_hosting === target )
room.send('/unhost', true);
else
room.send('/host ' + target, true);
}
this.set('ffz_host_updating', false);
return this.ffzUpdateMetadata('host');
}.observes('channel.id', 'channel.hostModeTarget.id')
});
}

View file

@ -21,7 +21,7 @@ FFZ.settings_info.player_stats = {
if ( ! this._cindex )
return;
this._cindex.ffzUpdatePlayerStats();
this._cindex.ffzUpdateMetadata('player_stats');
}
};

View file

@ -2232,7 +2232,7 @@ FFZ.prototype._modify_room = function(room) {
return;
if ( f._cindex )
f._cindex.ffzUpdateChatters();
f._cindex.ffzUpdateMetadata('chatters');
if ( window !== window.parent && parent.postMessage )
parent.postMessage({from_ffz: true, command: 'chatter_count', data: Object.keys(this.get('ffz_chatters') || {}).length}, "*"); //location.protocol + "//www.twitch.tv/");

View file

@ -49,22 +49,15 @@ FFZ.prototype.modify_viewer_list = function(component) {
// Get the broadcaster name.
var Channel = utils.ember_lookup('controller:channel'),
room_id = this.get('model.id'),
broadcaster = Channel && Channel.get('model.id');
broadcaster = room_id = this.get('model.id');
// We can get capitalization for the broadcaster from the channel.
if ( broadcaster ) {
var display_name = Channel.get('model.display_name');
if ( Channel && Channel.get('channelModel.id') === room_id ) {
var display_name = Channel.get('channelModel.displayName');
if ( display_name )
FFZ.capitalization[broadcaster] = [display_name, Date.now()];
}
// If the current room isn't the channel's chat, then we shouldn't
// display them as the broadcaster.
if ( room_id !== broadcaster )
broadcaster = null;
// Iterate over everything~!
for(var i=0; i < VIEWER_CATEGORIES.length; i++) {
var data = raw_viewers[VIEWER_CATEGORIES[i][0]],

View file

@ -30,11 +30,12 @@ FFZ.get = function() { return FFZ.instance; }
// TODO: This should be in a module.
FFZ.msg_commands = {};
FFZ.channel_metadata = {};
// Version
var VER = FFZ.version_info = {
major: 3, minor: 5, revision: 327,
major: 3, minor: 5, revision: 328,
toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
}
@ -86,22 +87,30 @@ FFZ.prototype.error = function(msg, error, to_json, log_json) {
FFZ.prototype.paste_logs = function() {
this._pastebin(this._log_data.join("\n"), function(url) {
if ( ! url )
return console.log("FFZ Error: Unable to upload log to pastebin.");
var f = this,
output = function(result) {
f._pastebin(result).then(function(url) {
f.log("Your FrankerFaceZ logs have been uploaded to: " + url);
}).catch(function() {
f.error("An error occured uploading the logs to a pastebin.");
});
}
console.log("FFZ: Your FrankerFaceZ log has been pasted to: " + url);
this.get_debugging_info().then(function(data) {
output(data);
}).catch(function(err) {
f.error("Error building debugging information.", err);
output(f._log_data.join("\n"));
});
}
FFZ.prototype._pastebin = function(data, callback) {
jQuery.ajax({url: "https://putco.de/", type: "PUT", data: data, context: this})
.success(function(e) {
callback.call(this, e.trim() + ".log");
}).fail(function(e) {
callback.call(this, null);
});
FFZ.prototype._pastebin = function(data) {
return new Promise(function(succeed, fail) {
jQuery.ajax({url: "https://putco.de/", type: "PUT", data: data})
.success(function(e) { succeed(e.trim() + ".log"); })
.fail(function(e) { fail(null); });
});
}
@ -202,6 +211,7 @@ require('./ext/emote_menu');
require('./featurefriday');
require('./ui/channel_stats');
require('./ui/logviewer');
//require('./ui/chatpane');
require('./ui/popups');
@ -209,7 +219,7 @@ require('./ui/styles');
require('./ui/dark');
require('./ui/tooltips');
require('./ui/notifications');
require('./ui/viewer_count');
//require('./ui/viewer_count');
require('./ui/sub_count');
require('./ui/dash_stats');
require('./ui/dash_feed');

View file

@ -115,7 +115,7 @@ FFZ.prototype.reset_settings = function() {
}
FFZ.prototype._get_settings_object = function() {
FFZ.prototype._get_settings_object = function(skip_default) {
var data = {
version: 1,
script_version: FFZ.version_info + '',
@ -131,7 +131,7 @@ FFZ.prototype._get_settings_object = function() {
var info = FFZ.settings_info[key],
ls_key = info.storage_key || make_ls(key);
if ( localStorage.hasOwnProperty(ls_key) )
if ( localStorage.hasOwnProperty(ls_key) && (!skip_default || this.settings[key] !== info.value) )
data.settings[key] = this.settings[key];
}

View file

@ -3,11 +3,7 @@ var FFZ = window.FrankerFaceZ,
utils = require("../utils"),
createElement = utils.createElement,
NICE_DESCRIPTION = {
"cluster": null,
"manifest_cluster": null,
"user_ip": null
};
BANNED_KEYS = ['user_ip'];
// -------------------
@ -156,18 +152,24 @@ FFZ.debugging_blocks = {
return succeed(output);
}
var perms = [],
ul = data.me.valid ? data.me.level : 0,
chan = data.channel;
ul >= chan.viewlogs && perms.push('view');
ul >= chan.viewmodlogs && perms.push('view-mod');
ul >= chan.viewcomments && perms.push('comment-view');
ul >= chan.writecomments && perms.push('comment-write');
ul >= chan.deletecomments && perms.push('comment-delete');
output.push(['Logging Enabled', data.channel.active === 1]);
output.push(['User Level', data.me.valid ? data.me.level : '<i>invalid</i>']);
output.push(['Level: View Logs', data.channel.viewlogs]);
output.push(['Level: View Moderation Logs', data.channel.viewmodlogs]);
output.push(['Level: View Comments', data.channel.viewcomments]);
output.push(['Level: Write Comments', data.channel.writecomments]);
output.push(['Level: Delete Comments', data.channel.deletecomments]);
output.push(['User Permissions', perms.join(', ') || '<i>none</i>']);
succeed(output);
}).catch(function(err) {
succeed(['Authentication', '<i>unable to get token</i>']);
succeed([['Authentication', '<i>unable to get token</i>']]);
});
});
}
@ -286,13 +288,33 @@ FFZ.debugging_blocks = {
var sorted_keys = Object.keys(data).sort(),
output = [];
for(var i=0; i < sorted_keys.length; i++)
output.push([sorted_keys[i], data[sorted_keys[i]]]);
for(var i=0; i < sorted_keys.length; i++) {
var key = sorted_keys[i];
if ( BANNED_KEYS.indexOf(key) === -1 )
output.push([key, data[key]]);
}
return output;
}
},
settings: {
order: 8,
title: "Current Settings",
refresh: false,
type: "text",
render: function() {
var output = this._get_settings_object(true).settings;
delete output.favorite_settings;
delete output.mod_card_reasons;
delete output.emote_menu_collapsed;
delete output.favorite_emotes;
return JSON.stringify(output, null, 2);
}
},
logs: {
order: 100,
title: "Logs",
@ -305,6 +327,78 @@ FFZ.debugging_blocks = {
}
}
FFZ.prototype._sorted_debug_blocks = function() {
var segments = [];
for(var key in FFZ.debugging_blocks) {
var info = FFZ.debugging_blocks[key];
if ( ! info )
continue;
var visible = info.visible || true;
if ( typeof visible === "function" )
visible = visible.call(this);
if ( ! visible )
continue;
segments.push([info.order || 50, info]);
}
segments.sort(function(a,b) { return a[0] > b[0] });
return segments;
}
FFZ.prototype.get_debugging_info = function() {
var f = this;
return new Promise(function(succeed, fail) {
var output = [
'FrankerFaceZ - Debugging Information',
(new Date).toISOString(), ''];
var segments = f._sorted_debug_blocks(),
promises = [];
for(var i=0; i < segments.length; i++) {
var info = segments[i][1];
promises.push(new Promise(function(info, s) {
var result = info.render.call(f);
if (!( result instanceof Promise ))
result = Promise.resolve(result);
result.then(function(data) {
var el = utils.createElement('span'),
out = [info.title, '----------------------------------------'];
if ( info.type === 'list' )
for(var x=0; x < data.length; x++) {
if ( data[x] ) {
el.innerHTML = data[x].join(': ');
out.push(el.textContent);
} else
out.push('');
}
else if ( info.type === 'text' )
out.push(data);
s(out);
}).catch(function(err) {
s(['', info.title, 'Error: ' + err]);
});
}.bind(f, info)));
}
Promise.all(promises).then(function(result) {
for(var i=0; i < result.length; i++) {
output.push.apply(output, result[i]);
output.push('');
output.push('');
}
succeed(output.join('\n').trim());
});
});
}
var include_html = function(heading_text, filename, callback) {
return function(view, container) {
@ -436,12 +530,15 @@ FFZ.menu_pages.about = {
return;
getting_logs = true;
f._pastebin(f._log_data.join("\n"), function(url) {
getting_logs = false;
if ( ! url )
alert("There was an error uploading the FrankerFaceZ logs.");
else
f.get_debugging_info().then(function(data) {
f._pastebin(data).then(function(url) {
getting_logs = false;
prompt("Your FrankerFaceZ logs have been uploaded to the URL:", url);
}).catch(function() {
getting_logs = false;
alert("An error occured uploading your FrankerFaceZ logs.");
});
});
});
@ -493,24 +590,7 @@ FFZ.menu_pages.about = {
container.appendChild(createElement('div', 'chat-menu-content center',
'<h1>FrankerFaceZ</h1><div class="ffz-about-subheading">woofs for nerds</div>'));
var segments = [];
for(var key in FFZ.debugging_blocks) {
var info = FFZ.debugging_blocks[key];
if ( ! info )
continue;
var visible = info.visible || true;
if ( typeof visible === "function" )
visible = visible.call(this);
if ( ! visible )
continue;
segments.push([info.order || 50, info]);
}
segments.sort(function(a,b) { return a[0] > b[0] });
var segments = this._sorted_debug_blocks();
for(var i=0; i < segments.length; i++) {
var info = segments[i][1],
output;

View file

@ -1,13 +1,165 @@
var FFZ = window.FrankerFaceZ,
constants = require('../constants'),
utils = require('../utils');
utils = require('../utils'),
metadata = FFZ.channel_metadata;
// --------------
// Channel Stats
// --------------
FFZ.stat_info = {};
metadata.uptime = {
refresh: function(channel) { return this.settings.stream_uptime > 0; },
setup: function(view, channel) {
var online = channel.get('stream.createdAt'),
now = Date.now() - (this._ws_server_offset || 0),
uptime = online && Math.floor((now - online.getTime()) / 1000) || -1;
return [online, uptime];
},
order: 2,
static_label: constants.CLOCK,
label: function(online, uptime) {
var setting = this.settings.stream_uptime;
if ( uptime < 0 || ! setting )
return null;
return utils.time_to_string(uptime, false, false, false, setting === 1 || setting === 3);
},
tooltip: function(online) {
return 'Stream Uptime <nobr>(since ' + online.toLocaleString() + ')</nobr>';
}
};
metadata.chatters = {
refresh: false,
static_label: constants.ROOMS,
label: function(view, channel) {
var channel_id = channel.get('id'),
room = this.rooms[channel_id];
if ( ! room || ! this.settings.chatter_count )
return null;
return utils.number_commas(Object.keys(room.room.get('ffz_chatters') || {}).length);
},
tooltip: 'Currently in Chat'
};
metadata.player_stats = {
refresh: function() { return this.settings.player_stats },
setup: function(view, channel) {
var channel_id = channel.get('id'),
player_cont = this.players && this.players[channel_id],
player = player_cont && player_cont.player,
stats;
try {
stats = player.getVideoInfo();
} catch(err) { }
var delay = stats && Math.round(stats.hls_latency_broadcaster / 10) / 100;
return [stats, delay, delay > 180, player_cont];
},
order: 3,
static_label: constants.GRAPH,
label: function(stats, delay, is_old) {
if ( ! this.settings.player_stats || ! stats || ! stats.hls_latency_broadcaster )
return null;
if ( is_old )
return utils.time_to_string(Math.floor(delay), true, delay > 172800) + ' old'
else {
delay = delay.toString();
var ind = delay.indexOf('.');
return delay + (ind === -1 ? '.00' : (ind >= delay.length - 2 ? '0' : '')) + 's';
}
},
click: function(event, button, stats, delay, is_old, player_cont) {
player_cont.$('.js-stats-toggle').click();
},
tooltip: function(stats, delay, is_old) {
if ( ! stats || ! stats.hls_latency_broadcaster )
return 'Stream Latency';
var bitrate;
if ( stats.playback_bytes_per_second )
bitrate = Math.round(stats.playback_bytes_per_second * 8 / 10.24) / 1000;
else
bitrate = Math.round(stats.current_bitrate * 100) / 100;
return (is_old ? 'Video Information<br>' +
'Broadcast ' + utils.time_to_string(Math.floor(delay), true) + ' Ago<br><br>' : 'Stream Latency<br>') +
'Video: ' + stats.vid_width + 'x' + stats.vid_height + 'p' + stats.current_fps + '<br>' +
'Playback Rate: ' + utils.number_commas(bitrate) + ' Kbps<br>' +
'Dropped Frames: ' + utils.number_commas(stats.dropped_frames || 0);
}
};
metadata.host = {
refresh: false,
setup: function(view, channel) {
var channel_id = channel.get('id'),
user = this.get_user(),
room = user && this.rooms[user.login] && this.rooms[user.login].room,
now_hosting = room && room.ffz_host_target,
hosts_remaining = room && room.ffz_hosts_left;
return [user, channel_id, now_hosting, hosts_remaining, view.get('ffz_host_updating'), view];
},
order: 98,
label: function(user, channel_id, hosting_id) {
if ( ! user || user.login === channel_id )
return null;
return channel_id === hosting_id ? 'Unhost' : 'Host';
},
button: true,
disabled: function(user, channel_id, hosting_id, hosts_remaining, updating, view) {
return !!view.get('ffz_host_updating')
},
click: function(event, button, user, channel_id, hosting_id, hosts_remaining, updating, view) {
view.set('ffz_host_updating', true);
event.update_stat();
var room = user && this.rooms[user.login] && this.rooms[user.login].room;
if ( channel_id === hosting_id )
room.send('/unhost', true);
else
room.send('/host ' + channel_id, true);
return true;
},
tooltip: function(user, channel_id, hosting_id, hosts_remaining, updating) {
var out;
if ( updating )
return 'Updating...';
if ( hosting_id ) {
var display_name = FFZ.get_capitalization(hosting_id);
out = 'You are now hosting ' + this.format_display_name(display_name, hosting_id, true)[0] + '.';
} else
out = 'You are not hosting any channel.';
return out + (hosts_remaining ? ' You have ' + hosts_remaining + ' host command' + utils.pluralize(hosts_remaining) + ' remaining this half hour.' : '');
}
};
// ---------------

View file

@ -1,5 +1,6 @@
var FFZ = window.FrankerFaceZ,
utils = require('../utils'),
constants = require('../constants'),
VALID_CHANNEL = /^[A-Za-z0-9_]+$/,
TWITCH_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([A-Za-z0-9_]+)/i;
@ -26,12 +27,13 @@ FFZ.settings_info.follow_buttons = {
no_mobile: true,
category: "Channel Metadata",
name: "Relevant Follow Buttons",
help: 'Display additional Follow buttons for channels relevant to the stream, such as people participating in co-operative gameplay.',
name: "Featured Channels",
help: 'Display additional Follow buttons for channels featured by the stream, such as people participating in co-operative gameplay.',
on_update: function(val) {
this.rebuild_following_ui();
}
};
if ( this._cindex )
this._cindex.ffzUpdateMetadata('following');
}
};
// ---------------
@ -105,8 +107,8 @@ FFZ.ws_on_close.push(function() {
}
}
if ( need_update )
this.rebuild_following_ui();
if ( need_update && this._cindex )
this._cindex.ffzUpdateMetadata('following');
});
@ -124,8 +126,8 @@ FFZ.ws_commands.follow_buttons = function(data) {
need_update = true;
}
if ( need_update )
this.rebuild_following_ui();
if ( need_update && this._cindex )
this._cindex.ffzUpdateMetadata('following');
}
@ -186,259 +188,143 @@ FFZ.ws_commands.follow_sets = function(data) {
// Following UI
// ---------------
FFZ.prototype.rebuild_following_ui = function() {
if ( ! this._cindex )
return;
FFZ.channel_metadata.following = {
refresh: false,
var channel_id = this._cindex.get('channel.id'),
hosted_id = this._cindex.get('channel.hostModeTarget.id');
setup: function(view, channel) {
var channel_id = channel.get('id'),
data = this.follow_data && this.follow_data[channel_id];
if ( channel_id ) {
var data = this.follow_data && this.follow_data[channel_id],
return [_.unique(data).without("")];
},
el = this._cindex.get('element'),
container = el && el.querySelector('.stats-and-actions .channel-actions'),
cont = container && container.querySelector('#ffz-ui-following');
order: 97,
button: true,
static_label: constants.HEART,
label: function(data) {
if ( ! data || ! data.length )
return null;
if ( ! container || ! this.settings.follow_buttons || ! data || ! data.length ) {
if ( cont )
cont.parentElement.removeChild(cont);
return 'Featured';
},
} else {
if ( ! cont ) {
cont = document.createElement('span');
cont.id = 'ffz-ui-following';
var before = null;
try {
var before_btn = container.querySelector('.subscribe-button');
if ( before_btn )
before = before_btn.parentElement.nextSibling;
else {
before_btn = container.querySelector('.notification-controls');
if ( before_btn )
before = before_btn.nextSibling;
}
} catch(err) { }
if ( before )
container.insertBefore(cont, before);
else
container.appendChild(cont);
} else
cont.innerHTML = '';
var processed = [channel_id];
for(var i=0; i < data.length && i < 10; i++) {
var cid = data[i];
if ( processed.indexOf(cid) !== -1 )
continue;
this._build_following_button(cont, cid);
processed.push(cid);
}
popup: function(container, data) {
var user = this.get_user();
if ( ! user || ! user.login ) {
Ember.$.login({mpSourceAction: "follow-button"});
return false;
}
}
container.classList.add('balloon--md');
var scroller = utils.createElement('div', 'scroller');
container.appendChild(scroller);
if ( hosted_id ) {
var data = this.follow_data && this.follow_data[hosted_id],
for(var i=0; i < data.length && i < 50; i++)
FFZ.channel_metadata.following.draw_row.call(this, scroller, data[i]);
},
el = this._cindex.get('element'),
container = el && el.querySelector('#hostmode .channel-actions'),
cont = container && container.querySelector('#ffz-ui-following');
draw_row: function(container, user_id) {
var f = this,
user = this.get_user(),
if ( ! container || ! this.settings.follow_buttons || ! data || ! data.length ) {
if ( cont )
cont.parentElement.removeChild(cont);
el = utils.createElement('div', 'ffz-following-row'),
} else {
if ( ! cont ) {
cont = document.createElement('span');
cont.id = 'ffz-ui-following';
avatar = utils.createElement('img', 'image'),
name_el = utils.createElement('a', 'html-tooltip'),
var before = null;
try {
var before_btn = container.querySelector('.subscribe-button');
if ( before_btn )
before = before_btn.parentElement.nextSibling;
else {
before_btn = container.querySelector('.notification-controls');
if ( before_btn )
before = before_btn.nextSibling;
}
} catch(err) { }
btn_follow = utils.createElement('button', 'follow-button button'),
sw_notif = utils.createElement('a', 'switch html-tooltip', '<span>'),
if ( before )
container.insertBefore(cont, before);
else
container.appendChild(cont);
} else
cont.innerHTML = '';
channel = {
name: user_id
},
var processed = [hosted_id];
for(var i=0; i < data.length && i < 10; i++) {
var cid = data[i];
if ( processed.indexOf(cid) !== -1 )
continue;
this._build_following_button(cont, cid);
processed.push(cid);
}
}
}
}
is_following = null,
is_notified = false,
update = function() {
if ( channel.logo )
avatar.src = channel.logo;
// ---------------
// UI Construction
// ---------------
var name = f.format_display_name(channel.display_name || user_id, user_id);
name_el.innerHTML = name[0];
name_el.setAttribute('original-title', name[1] || '');
FFZ.prototype._build_following_button = function(cont, channel_id) {
if ( ! VALID_CHANNEL.test(channel_id) )
return this.log("Ignoring Invalid Channel: " + utils.sanitize(channel_id));
el.setAttribute('data-loaded', is_following !== null);
el.setAttribute('data-following', is_following);
var f = this,
btn = utils.createElement('button', 'follow-button button html-tooltip'),
btn_follow.textContent = is_following ? 'Unfollow' : 'Follow';
btn_follow.classList.toggle('is-following', is_following);
btn_follow.classList.toggle('button--status', is_following);
sw_notif.classList.toggle('active', is_notified);
sw_notif.setAttribute('original-title', 'Notify me when ' + name[0] + ' goes live.');
},
noti = utils.createElement('a', 'toggle-notification-menu js-toggle-notification-menu'),
noti_c = utils.createElement('div', 'notification-controls v2 hidden', noti),
check_following = function() {
// Minimize our API calls.
utils.api.get("users/:login/follows/channels/" + user_id)
.done(function(data) {
is_following = true;
is_notified = data.notifications;
channel = data.channel;
update();
display_name,
tooltip,
}).fail(function() {
utils.api.get("channels/" + user_id)
.done(function(data) {
is_following = false;
is_notified = false;
channel = data;
update();
}).fail(function() {
el.removeChild(btn_follow);
el.removeChild(sw_notif);
el.appendChild(utils.createElement('span', 'right', 'Invalid Channel'));
});
});
},
following = false,
notifications = false,
do_follow = function(notice) {
if ( notice !== false )
notice = true;
update = function() {
btn.classList.toggle('is-following', following);
btn.classList.toggle('button--status', following);
btn.title = tooltip ? (following ? "Unf" : "F") + "ollow " + tooltip : '';
btn.innerHTML = (following ? "" : "Follow ") + display_name;
noti_c.classList.toggle('hidden', !following);
},
is_following = true;
is_notified = notice;
update();
check_following = function() {
var user = f.get_user();
if ( ! user || ! user.login ) {
following = false;
notification = false;
btn.classList.add('is-initialized');
return update();
}
return utils.api.put("users/:login/follows/channels/" + user_id, {notifications: notice})
.fail(check_following);
};
utils.api.get("users/" + user.login + "/follows/channels/" + channel_id)
.done(function(data) {
following = true;
notifications = data.notifications;
btn.classList.add('is-initialized');
update();
}).fail(function(data) {
following = false;
notifications = false;
btn.classList.add('is-initialized');
update();
});
},
do_follow = function(notice) {
if ( notice !== false )
notice = true;
var user = f.get_user();
if ( ! user || ! user.login )
return null;
notifications = notice;
return utils.api.put("users/:login/follows/channels/" + channel_id, {notifications: notifications})
.fail(check_following);
},
on_name = function(cap_name) {
var results = f.format_display_name(cap_name, channel_id, true, true);
display_name = results[0];
tooltip = results[1];
btn_follow.addEventListener('click', function() {
is_following = is_notified = ! is_following;
update();
};
// The drop-down button!
noti.href = '#';
f.ws_send("track_follow", [user_id, is_following]);
// Event Listeners!
btn.addEventListener('click', function(e) {
var user = f.get_user();
if ( ! user || ! user.login )
// Show the login dialog~!
return Ember.$.login({mpSourceAction: "follow-button", follow: channel_id});
if ( is_following )
do_follow();
else
utils.api.del("users/:login/follows/channels/" + user_id)
.fail(check_following);
});
// Immediate update for nice UI.
following = ! following;
sw_notif.addEventListener('click', function() {
do_follow(!is_notified);
});
el.setAttribute('data-user', user_id);
name_el.href = 'https://www.twitch.tv/' + user_id;
name_el.target = '_blank';
el.appendChild(avatar);
el.appendChild(name_el);
el.appendChild(btn_follow);
el.appendChild(sw_notif);
container.appendChild(el);
check_following();
update();
// Report it!
f.ws_send("track_follow", [channel_id, following]);
// Do it, and make sure it happened.
if ( following )
do_follow()
else
utils.api.del("users/:login/follows/channels/" + channel_id)
.done(check_following);
return false;
});
btn.addEventListener('mousedown', function(e) {
if ( e.button !== 1 )
return;
e.preventDefault();
window.open(Twitch.uri.profile(channel_id));
});
noti.addEventListener('click', function() {
var sw = f._build_following_popup(noti_c, channel_id, notifications);
if ( sw )
sw.addEventListener('click', function() {
var notice = ! notifications;
sw.classList.toggle('active', notice);
do_follow(notice);
return false;
});
return false;
});
on_name(FFZ.get_capitalization(channel_id, on_name));
setTimeout(check_following, Math.random()*5000);
cont.appendChild(btn);
cont.appendChild(noti_c);
}
FFZ.prototype._build_following_popup = function(container, channel_id, notifications) {
var popup = this.close_popup(), out = '',
pos = container.offsetLeft + container.offsetWidth;
if ( popup && popup.id == "ffz-following-popup" && popup.getAttribute('data-channel') === channel_id )
return null;
popup = this._popup = utils.createElement('div', 'dropmenu notify-menu js-notify');
popup.id = 'ffz-following-popup';
popup.setAttribute('data-channel', channel_id);
this._popup_allow_parent = true;
this._popup_parent = container;
var results = this.format_display_name(FFZ.get_capitalization(channel_id), channel_id, true);
out = '<div class="header">You are following <span' + (results[1] ? ' class="html-tooltip" title="' + utils.quote_attr(results[1]) + '"' : '') + '>' + results[0] + '</span></div>';
out += '<p class="clearfix">';
out += '<a class="switch' + (notifications ? ' active' : '') + '"><span></span></a>';
out += '<span class="switch-label">Notify me when the broadcaster goes live</span>';
out += '</p>';
popup.innerHTML = out;
container.insertBefore(popup, container.firstChild);
return popup.querySelector('a.switch');
}
}

View file

@ -588,7 +588,7 @@ FFZ.mod_card_pages.notes = {
this.tokenize_chat_line(message, true, false);
var can_edit = false, // mod_card.lv_write_notes && user && user.login === message.from,
can_delete = false, //mod_card.lv_delete_notes || (user && user.login === message.from),
can_delete = mod_card.lv_delete_notes || (user && user.login === message.from),
can_mod = can_edit || can_delete;
var output = this._build_mod_card_history(message, mod_card, true, false, can_mod);

View file

@ -25,9 +25,10 @@ FFZ.settings_info.srl_races = {
name: "SRL Race Information",
help: 'Display information about <a href="http://www.speedrunslive.com/" target="_new">SpeedRunsLive</a> races under channels.',
on_update: function(val) {
this.rebuild_race_ui();
}
};
if ( this._cindex )
this._cindex.ffzUpdateMetadata('srl_race');
}
};
// ---------------
@ -49,8 +50,8 @@ FFZ.ws_on_close.push(function() {
need_update = true;
}
if ( need_update )
this.rebuild_race_ui();
if ( need_update && this._cindex )
this._cindex.ffzUpdateMetadata('srl_race');
});
@ -81,8 +82,8 @@ FFZ.ws_commands.srl_race = function(data) {
}
}
if ( need_update )
this.rebuild_race_ui();
if ( need_update && this._cindex )
this._cindex.ffzUpdateMetadata('srl_race');
}
@ -90,245 +91,52 @@ FFZ.ws_commands.srl_race = function(data) {
// Race UI
// ---------------
FFZ.prototype.rebuild_race_ui = function() {
if ( ! this._cindex )
return;
FFZ.channel_metadata.srl_race = {
refresh: false,
var channel_id = this._cindex.get('channel.id'),
hosted_id = this._cindex.get('channel.hostModeTarget.id');
setup: function(view, channel) {
var channel_id = channel.get('id'),
race = this.srl_races && this.srl_races[channel_id],
entrant_id = race && race.twitch_entrants[channel_id],
entrant = entrant_id && race.entrants[entrant_id];
if ( channel_id ) {
var race = this.srl_races && this.srl_races[channel_id],
return [channel, channel_id, race, entrant];
},
el = this._cindex.get('element'),
container = el && el.querySelector('.stats-and-actions .channel-actions'),
race_container = container && container.querySelector('#ffz-ui-race');
static_label: '<figure class="icon cn-metabar__icon"><span class="srl-logo"></span></figure>',
label: function(channel, channel_id, race, entrant) {
if ( ! entrant )
return null;
if ( ! container || ! this.settings.srl_races || ! race ) {
if ( race_container )
race_container.parentElement.removeChild(race_container);
return utils.placement(entrant) || '&#8203;';
},
} else {
if ( ! race_container ) {
race_container = utils.createElement('span', 'balloon-wrapper inline');
race_container.id = 'ffz-ui-race';
race_container.setAttribute('data-channel', channel_id);
tooltip: "SpeedRunsLive Race",
var btn = document.createElement('span');
btn.className = 'button button--text button--dropmenu';
btn.title = "SpeedRunsLive Race";
btn.innerHTML = '<span class="logo"></span>';
btn.addEventListener('click', this._build_race_popup.bind(this, race_container, channel_id));
race_container.appendChild(btn);
container.appendChild(race_container);
}
this._update_race(race_container, true);
on_popup_close: function(container) {
if ( this._race_interval ) {
clearInterval(this._race_interval);
this._race_interval = null;
}
}
},
if ( hosted_id ) {
var race = this.srl_races && this.srl_races[hosted_id],
update_popup: function(container, channel, channel_id, race, entrant) {
var now = (Date.now() - (this._ws_server_offset || 0)) / 1000,
elapsed = Math.floor(now - race.time);
el = this._cindex.get('element'),
container = el && el.querySelector('#hostmode .channel-actions'),
race_container = container && container.querySelector('#ffz-ui-race');
var tbody = container.querySelector('tbody'),
info = container.querySelector('.heading > div'),
timer = container.querySelector('.heading > span');
if ( ! container || ! this.settings.srl_races || ! race ) {
if ( race_container )
race_container.parentElement.removeChild(race_container);
} else {
if ( ! race_container ) {
race_container = utils.createElement('span', 'balloon-wrapper inline');
race_container.id = 'ffz-ui-race';
race_container.setAttribute('data-channel', hosted_id);
var btn = document.createElement('span');
btn.className = 'button button--text button--dropmenu';
btn.title = "SpeedRunsLive Race";
btn.innerHTML = '<span class="logo"></span>';
btn.addEventListener('click', this._build_race_popup.bind(this, race_container, hosted_id));
race_container.appendChild(btn);
container.appendChild(race_container);
}
this._update_race(race_container, true);
}
}
}
// ---------------
// Race Popup
// ---------------
FFZ.prototype._race_kill = function() {
if ( this._race_timer ) {
clearTimeout(this._race_timer);
delete this._race_timer;
}
delete this._race_game;
delete this._race_goal;
}
FFZ.prototype._build_race_popup = function(container, channel_id) {
var popup = this.close_popup();
if ( popup && popup.id === "ffz-race-popup" && popup.getAttribute('data-channel') === channel_id )
return;
if ( ! container )
return;
var el = container.querySelector('.button'),
pos = el.offsetLeft + el.offsetWidth,
race = this.srl_races[channel_id];
var popup = utils.createElement('div', 'share balloon balloon--md balloon--up balloon--dropmenu'), out = '';
popup.id = 'ffz-race-popup';
popup.setAttribute('data-channel', channel_id);
//popup.className = (pos >= 300 ? 'right' : 'left') + ' share dropmenu';
this._popup_kill = this._race_kill.bind(this);
this._popup_allow_parent = true;
this._popup = popup;
var link = 'http://kadgar.net/live',
has_entrant = false;
for(var ent in race.entrants) {
var state = race.entrants[ent].state;
if ( race.entrants.hasOwnProperty(ent) && race.entrants[ent].channel && (state == "racing" || state == "entered") ) {
link += "/" + race.entrants[ent].channel;
has_entrant = true;
}
}
var height = document.querySelector('.app-main.theatre') ? document.body.clientHeight - 300 : container.parentElement.parentElement.offsetTop - 175,
controller = utils.ember_lookup('controller:channel'),
display_name = controller && controller.get('content.id') === channel_id ? controller.get('content.display_name') : FFZ.get_capitalization(channel_id),
tweet = encodeURIComponent("I'm watching " + display_name + " race " + race.goal + " in " + race.game + " on SpeedRunsLive!");
out = '<div class="heading"><div></div><span class="html-tooltip"></span></div>';
out += '<div class="table" style="max-height:' + height + 'px"><table><thead><tr><th>#</th><th>Entrant</th><th>&nbsp;</th><th>Time</th></tr></thead>';
out += '<tbody></tbody></table></div>';
out += '<iframe class="twitter_share_button" style="width:130px; height:25px" src="https://platform.twitter.com/widgets/tweet_button.html?text=' + tweet + '%20Watch%20at&via=Twitch&url=http://www.twitch.tv/' + channel_id + '"></iframe>';
out += '<p class="right"><a target="_new" href="http://www.speedrunslive.com/race/?id=' + race.id + '">SRL</a>';
if ( has_entrant )
out += ' &nbsp; <a target="_new" href="' + link + '">Multitwitch</a>';
out += '</p>';
popup.innerHTML = out;
container.appendChild(popup);
this._update_race(container, true);
}
FFZ.prototype._update_race = function(container, not_timer) {
if ( this._race_timer && not_timer ) {
clearTimeout(this._race_timer);
delete this._race_timer;
}
if ( ! container )
return;
var channel_id = container.getAttribute('data-channel'),
race = this.srl_races[channel_id];
if ( ! race ) {
// No race. Abort.
container.parentElement.removeChild(container);
if ( this._popup && this._popup.id === 'ffz-race-popup' && this._popup.getAttribute('data-channel') === channel_id )
this.close_popup();
return;
}
var entrant_id = race.twitch_entrants[channel_id],
entrant = race.entrants[entrant_id],
popup = container.querySelector('#ffz-race-popup'),
now = (Date.now() - (this._ws_server_offset || 0)) / 1000,
elapsed = Math.floor(now - race.time);
container.querySelector('.logo').innerHTML = utils.placement(entrant);
if ( popup ) {
var tbody = popup.querySelector('tbody'),
timer = popup.querySelector('.heading > span'),
info = popup.querySelector('.heading div');
// Make sure we don't leave any tooltips lying around when we update.
// Of course, we should just rewrite logic to not constantly mutilate
// rows.
jQuery('.html-tooltip', tbody).trigger('mouseout');
tbody.innerHTML = '';
var entrants = [], done = true;
for(var ent in race.entrants) {
if ( ! race.entrants.hasOwnProperty(ent) ) continue;
if ( race.entrants[ent].state == "racing" )
done = false;
entrants.push(race.entrants[ent]);
}
entrants.sort(function(a,b) {
var a_place = a.place || 9999,
b_place = b.place || 9999,
a_time = a.time || elapsed,
b_time = b.time || elapsed;
if ( a.state == "forfeit" || a.state == "dq" )
a_place = 10000;
if ( b.state == "forfeit" || b.state == "dq" )
b_place = 10000;
if ( a_place < b_place ) return -1;
else if ( a_place > b_place ) return 1;
else if ( a.name < b.name ) return -1;
else if ( a.name > b.name ) return 1;
else if ( a_time < b_time ) return -1;
else if ( a_time > b_time ) return 1;
});
for(var i=0; i < entrants.length; i++) {
var ent = entrants[i],
name = '<a target="_new" href="http://www.speedrunslive.com/profiles/#!/' + utils.sanitize(ent.name) + '">' + ent.display_name + '</a>',
twitch_link = ent.channel ? '<a target="_new" class="twitch" href="//www.twitch.tv/' + utils.sanitize(ent.channel) + '"></a>' : '',
hitbox_link = ent.hitbox ? '<a target="_new" class="hitbox" href="http://www.hitbox.tv/' + utils.sanitize(ent.hitbox) + '"></a>' : '',
time = elapsed ? utils.time_to_string(ent.time||elapsed) : "",
place = utils.place_string(ent.place),
comment = ent.comment ? utils.quote_san(ent.comment) : "";
tbody.innerHTML += '<tr' + (comment ? ' title="' + comment + '"' : '') + ' class="' + ent.state + (comment ? ' html-tooltip' : '') + '"><td>' + place + '</td><td>' + name + '</td><td>' + twitch_link + hitbox_link + '</td><td class="time">' + (ent.state == "forfeit" ? "Forfeit" : time) + '</td></tr>';
}
if ( this._race_game != race.game || this._race_goal != race.goal ) {
this._race_game = race.game;
this._race_goal = race.goal;
if ( info.getAttribute('data-game') != race.game || info.getAttribute('data-goal') != race.goal ) {
info.setAttribute('data-game', race.game);
info.setAttribute('data-goal', race.goal);
var game = utils.quote_san(race.game),
goal = utils.unquote_attr(race.goal),
old_goal = popup.getAttribute('data-old-goal');
goal = utils.unquote_attr(race.goal);
if ( goal !== old_goal ) {
popup.setAttribute('data-old-goal', goal);
goal = goal ? this.render_tokens(this.tokenize_line("jtv", null, goal, true)) : '';
info.innerHTML = '<h2 class="html-tooltip" title="' + game + '">' + game + '</h2><span class="goal"><b>Goal: </b>' + goal + '</span>';
}
goal = goal ? this.render_tokens(this.tokenize_line("jtv", null, goal, true)) : '';
info.innerHTML = '<h2 class="html-tooltip" title="' + game + '">' + game + '</h2><span class="goal"><b>Goal: </b>' + goal + '</span>';
}
if ( race.time != timer.getAttribute('data-time') ) {
@ -337,12 +145,111 @@ FFZ.prototype._update_race = function(container, not_timer) {
}
if ( ! elapsed )
timer.innerHTML = "Entry Open";
else if ( done )
timer.innerHTML = "Done";
else {
timer.innerHTML = 'Entry Open';
else
timer.innerHTML = utils.time_to_string(elapsed);
this._race_timer = setTimeout(this._update_race.bind(this, container), 1000);
var entrants = [],
done = true;
for(var ent in race.entrants) {
var e = race.entrants[ent];
if ( e.state === 'racing' )
done = false;
entrants.push(e);
}
entrants.sort(function(a,b) {
var a_place = a.place || 9999,
b_place = b.place || 9999;
if ( a.state === 'forfeit' || a.state === 'dq' )
a_place = 10000;
if ( b.state === 'forfeit' || b.state === 'dq' )
b_place = 10000;
if ( a_place < b_place ) return -1;
else if ( a_place > b_place ) return 1;
else if ( a.name < b.name ) return -1;
else if ( a.name > b.name ) return 1;
});
for(var i=0; i < entrants.length; i++) {
var ent = entrants[i],
line = tbody.children[i],
matching = false;
if ( line ) {
matching = line.getAttribute('data-entrant') === ent.name;
if ( ! matching )
jQuery('.html-tooltip', line).trigger('mouseout');
} else {
line = utils.createElement('tr', 'html-tooltip');
tbody.appendChild(line);
}
var place = utils.place_string(ent.place),
comment = ent.comment ? utils.quote_san(ent.comment) : '',
time = elapsed ? utils.time_to_string(ent.time || elapsed) : '';
if ( ! matching ) {
var name = '<a target="_blank" href="http://www.speedrunslive.com/profiles/#1/' + utils.quote_san(ent.name) + '">' + this.format_display_name(ent.display_name, ent.name)[0] + '</a>',
twitch_link = ent.channel ? '<a target="_blank" class="twitch" href="https://www.twitch.tv/' + utils.quote_san(ent.channel) + '"></a>' : '',
hitbox_link = ent.hitbox ? '<a target="_blank" class="hitbox" href="https://www.hitbox.tv/' + uitls.quote_san(ent.hitbox) + '"></a>' : '';
line.setAttribute('data-entrant', ent.name);
line.innerHTML = '<td></td><td>' + name + '</td><td>' + twitch_link + hitbox_link + '</td><td class="time"></td>';
}
line.setAttribute('original-title', comment);
line.setAttribute('data-state', ent.state);
line.children[0].textContent = place;
line.children[3].textContent = ent.state === 'forfeit' ? 'Forfeit' : time;
}
while(tbody.children.length > entrants.length)
tbody.removeChild(tbody.children[entrants.length]);
},
popup: function(container, channel, channel_id, race, entrant) {
if ( this._race_interval )
clearInterval(this._race_interval);
container.classList.add('balloon--md');
var link = 'http://kadgar.net/live',
has_racing_entrant = false;
for(var ent in race.entrants) {
var state = race.entrants[ent].state,
e_channel = race.entrants[ent].channel
if ( e_channel && (state === 'racing' || state === 'entered') ) {
link += '/' + e_channel;
has_racing_entrant = true;
}
}
var display_name = channel.get('displayName'),
tweet = encodeURIComponent("I'm watching " + display_name + " race " + race.goal + " in " + race.game + " on SpeedRunsLive! Watch at"),
height = Math.max(300, document.querySelector('#player').clientHeight - 100);
container.innerHTML = '<div class="heading"><div></div><span class="html-tooltip"></span></div>' +
'<div class="table" style="max-height:' + height + 'px"><table>' +
'<thead><tr><th>#</th><th>Entrant</th><th>&nbsp;</th><th>Time</th></tr></thead>' +
'<tbody></tbody></table></div>' +
'<iframe class="twitter_share_button" style="width:130px; height: 25px" src="https://platform.twitter.com/widgets/tweet_button.html?text=' + utils.quote_attr(tweet) + '&via=Twitch&url=https://www.twitch.tv/' + utils.quote_san(channel_id) + '"></iframe>' +
'<p class="right"><a target="_blank" href="http://www.speedrunslive.com/race/?id=' + race.id + '">SRL</a>' +
(has_racing_entrant ? ' &nbsp; <a target="_blank" href="' + link + '">Multitwitch</a>' : '') +
'</p>';
var func = FFZ.channel_metadata.srl_race.update_popup.bind(this, container, channel, channel_id, race, entrant);
func();
this._race_interval = setInterval(func, 1000);
}
}
};

View file

@ -19,7 +19,7 @@ FFZ.ws_commands.chatters = function(data) {
if ( room ) {
room.ffz_chatters = count;
if ( this._cindex )
this._cindex.ffzUpdateChatters();
this._cindex.ffzUpdateMetadata('chatters');
}
return;
}
@ -39,7 +39,7 @@ FFZ.ws_commands.viewers = function(data) {
if ( room ) {
room.ffz_viewers = count;
if ( this._cindex )
this._cindex.ffzUpdateChatters();
this._cindex.ffzUpdateMetadata('chatters');
}
return;
}

178
style.css
View file

@ -18,6 +18,9 @@ body > div.tipsy .tipsy-arrow { opacity: 0.8; }
cursor: pointer;
}
.ffz-following-row[data-loaded="false"] .button,
.ffz-following-row[data-loaded="false"] .switch,
.ffz-following-row[data-following="false"] .switch,
.ffz-moderation-card:not(.lv-notes) ul.menu li[data-page="notes"],
.ffz-moderation-card:not(.lv-logs) ul.menu li[data-page="history"],
.ffz-moderation-card:not(.lv-logs) ul.menu li[data-page="stats"],
@ -240,18 +243,6 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
#dash_main #stats .stat.dark#ffz_count svg path { fill: #cacaca; }
#ffz-ui-following .follow-button a {
padding: 0 10px;
color: #fff;
}
#ffz-following-popup {
background-image: url('//cdn.frankerfacez.com/script/zreknarf-bg.png');
background-repeat: no-repeat;
background-position: 115% -75%;
background-size: 50%;
}
.ffz-live-team-channel .ffz-game {
display: inline-block;
max-width: 150px;
@ -357,76 +348,61 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
#ffz-ui-host-button { vertical-align: middle }
#ffz-following-popup.right {
right: 0;
left: auto;
}
#ffz-ui-following .notification-controls,
#ffz-ui-race {
position: relative;
}
#ffz-ui-race .button span {
.srl-logo {
display: inline-block;
height: 30px;
background: no-repeat 0 50%;
}
#ffz-ui-race .button span.logo {
padding-left: 44px;
margin-bottom: -10px;
width: 34px;
height: 16px;
background-image: url("//cdn.frankerfacez.com/script/srl_button.png");
}
.cn-metabar__ffz .srl-logo {
vertical-align: middle;
margin-top: -2px;
}
#ffz-race-popup {
position: absolute;
display: block;
#ffz-metadata-popup[data-key="srl_race"] {
background-image: url("//cdn.frankerfacez.com/script/zreknarf-bg.png");
background-repeat: no-repeat;
padding: 1rem;
background-position: 115% 110%;
}
#ffz-race-popup.right { right: 10px; }
#ffz-race-popup .heading {
#ffz-metadata-popup[data-key="srl_race"] .heading {
margin: -1rem -1rem 1rem;
height: 65px;
position: relative;
}
#ffz-race-popup .heading div {
#ffz-metadata-popup[data-key="srl_race"] .heading div {
padding: 0 1rem;
}
#ffz-race-popup .heading h2,
#ffz-race-popup .heading .goal {
#ffz-metadata-popup[data-key="srl_race"] .heading h2,
#ffz-metadata-popup[data-key="srl_race"] .heading .goal {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#ffz-race-popup .heading .goal {
#ffz-metadata-popup[data-key="srl_race"] .heading .goal {
display: block;
margin: calc(-1rem + 1px);
padding: calc(1rem - 1px);
}
#ffz-race-popup .heading .goal:hover {
#ffz-metadata-popup[data-key="srl_race"] .heading .goal:hover {
white-space: normal;
background-color: rgba(255,255,255,0.9);
border-bottom: 1px solid rgba(0,0,0,0.2);
}
.theatre #ffz-race-popup .heading .goal:hover,
.ffz-dark #ffz-race-popup .heading .goal:hover {
.theatre #ffz-metadata-popup[data-key="srl_race"] .heading .goal:hover,
.ffz-dark #ffz-metadata-popup[data-key="srl_race"] .heading .goal:hover {
background-color: rgba(16,16,16,0.9);
border-color: rgba(255,255,255,0.2);
}
#ffz-race-popup .heading h2 {
#ffz-metadata-popup[data-key="srl_race"] .heading h2 {
max-width: 240px;
font-size: 1.5em;
padding-bottom: 5px;
@ -435,7 +411,7 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
margin-bottom: 0;
}
#ffz-race-popup .heading > span {
#ffz-metadata-popup[data-key="srl_race"] .heading > span {
line-height: 30px;
position: absolute;
top: 7.5px;
@ -446,32 +422,37 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
border-radius: 5px;
}
#ffz-race-popup .right {
#ffz-metadata-popup[data-key="srl_race"] p {
float: right;
margin-bottom: 0;
}
#ffz-metadata-popup[data-key="srl_race"] .right {
padding-top: 0;
text-align: right;
}
#ffz-race-popup .table {
#ffz-metadata-popup[data-key="srl_race"] .table {
overflow-y: auto;
border-bottom: 1px solid;
margin-bottom: 1rem;
}
#ffz-race-popup table {
#ffz-metadata-popup[data-key="srl_race"] table {
width: 100%;
text-align: center;
border-spacing: 0;
}
#ffz-race-popup table a {
#ffz-metadata-popup[data-key="srl_race"] table a {
color: inherit;
}
.ffz-about-table a.twitch,
.ffz-about-table a.youtube,
.ffz-about-table a.twitter,
#ffz-race-popup a.twitch,
#ffz-race-popup a.hitbox {
#ffz-metadata-popup[data-key="srl_race"] a.twitch,
#ffz-metadata-popup[data-key="srl_race"] a.hitbox {
display: inline-block;
height: 16px;
margin-left: 5px;
@ -488,25 +469,25 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
background-image: url("//cdn.frankerfacez.com/script/twitter_logo.png");
}
#ffz-race-popup a.twitch,
#ffz-metadata-popup[data-key="srl_race"] a.twitch,
.ffz-about-table a.twitch {
width: 15px;
background-image: url("//cdn.frankerfacez.com/script/twitch_logo.png");
}
#ffz-race-popup a.hitbox {
#ffz-metadata-popup[data-key="srl_race"] a.hitbox {
width: 12px;
background-image: url("//cdn.frankerfacez.com/script/hitbox_logo.png");
}
#ffz-race-popup table tbody tr.done:nth-child(0n+1) td { background-color: rgba(255,255,0,.2); }
#ffz-race-popup table tbody tr.done:nth-child(0n+2) td { background-color: rgba(128,128,128,.2); }
#ffz-race-popup table tbody tr.done:nth-child(0n+3) td { background-color: rgba(210,100,0,.2); }
#ffz-race-popup table tbody tr.forfeit td { opacity: 0.5; background-color: rgba(210,100,100,.2); }
#ffz-race-popup table tbody tr.racing td.time { opacity: 0.5; }
#ffz-metadata-popup[data-key="srl_race"] table tbody tr[data-state="done"]:nth-child(0n+1) td { background-color: rgba(255,255,0,.2); }
#ffz-metadata-popup[data-key="srl_race"] table tbody tr[data-state="done"]:nth-child(0n+2) td { background-color: rgba(128,128,128,.2); }
#ffz-metadata-popup[data-key="srl_race"] table tbody tr[data-state="done"]:nth-child(0n+3) td { background-color: rgba(210,100,0,.2); }
#ffz-metadata-popup[data-key="srl_race"] table tbody tr[data-state="forfeit"] td { opacity: 0.5; background-color: rgba(210,100,100,.2); }
#ffz-metadata-popup[data-key="srl_race"] table tbody tr[data-state="racing"] td.time { opacity: 0.5; }
#ffz-race-popup table th, #ffz-race-popup td { padding: 1px; }
#ffz-race-popup table th { border-bottom: 1px solid; }
#ffz-metadata-popup[data-key="srl_race"] table th, #ffz-metadata-popup[data-key="srl_race"] td { padding: 1px; }
#ffz-metadata-popup[data-key="srl_race"] table th { border-bottom: 1px solid; }
/* Dark Menu */
@ -592,10 +573,10 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill:
}
.emoticon-selector .emoticon-grid.ffz-no-emotes img {
padding: 5px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
padding: 5px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.ffz-ui-sub-menu-page[data-page="favorites"] .emoticon-grid.ffz-no-emotes {
@ -1088,10 +1069,11 @@ body.ffz-bttv-dark .ffz-ui-popup .ffz-ui-menu-page { border-bottom: none }
}
.ffz-subwindow p {
margin-bottom: 0;
padding: 0 0 10px;
margin-bottom: 0;
padding: 0 0 10px;
}
#ffz-metadata-popup,
.ffz-subwindow .card,
.ffz-channel-selector {
background-image: url("//cdn.frankerfacez.com/script/zreknarf-bg.png");
@ -1194,6 +1176,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
/* Menu Scrollbar */
#ffz-metadata-popup .scroller::-webkit-scrollbar,
.searchPanel .collectionWrapper::-webkit-scrollbar,
.activity-react__all::-webkit-scrollbar,
.conversations-list .scroll-container::-webkit-scrollbar,
@ -1203,14 +1186,15 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
.conversations-list .conversations-list-inner::-webkit-scrollbar,
.conversation-window .conversation-content::-webkit-scrollbar,
.chat-history::-webkit-scrollbar,
#ffz-race-popup .table::-webkit-scrollbar,
#ffz-metadata-popup[data-key="srl_race"] .table::-webkit-scrollbar,
.emoticon-selector-box .all-emotes::-webkit-scrollbar,
.ffz-ui-sub-menu-page::-webkit-scrollbar,
.ffz-ui-menu-page::-webkit-scrollbar {
height: 6px;
height: 6px;
width: 6px;
}
#ffz-metadata-popup .scroller::-webkit-scrollbar-thumb,
.searchPanel .collectionWrapper::-webkit-scrollbar-thumb,
.activity-react__all::-webkit-scrollbar-thumb,
.conversations-list .scroll-container::-webkit-scrollbar-thumb,
@ -1220,7 +1204,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
.conversations-list .conversations-list-inner::-webkit-scrollbar-thumb,
.conversation-window .conversation-content::-webkit-scrollbar-thumb,
.chat-history::-webkit-scrollbar-thumb,
#ffz-race-popup .table::-webkit-scrollbar-thumb,
#ffz-metadata-popup[data-key="srl_race"] .table::-webkit-scrollbar-thumb,
.emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb,
.ffz-ui-sub-menu-page::-webkit-scrollbar-thumb,
.ffz-ui-menu-page::-webkit-scrollbar-thumb {
@ -1229,6 +1213,9 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
box-shadow: 0 0 1px 1px rgba(255,255,255,0.25);
}
.ffz-dark #ffz-metadata-popup .scroller::-webkit-scrollbar-thumb,
.theatre #ffz-metadata-popup .scroller::-webkit-scrollbar-thumb,
.ffz-dark .searchPanel .collectionWrapper::-webkit-scrollbar-thumb,
.ffz-dark .activity-react__all::-webkit-scrollbar-thumb,
.ffz-dark .conversations-list .scroll-container::-webkit-scrollbar-thumb,
@ -1419,6 +1406,9 @@ img.channel_background[src="null"] { display: none; }
.ffz-moderation-card ul.menu span { text-decoration: underline }
.ffz-dark .ffz-following-row,
.theatre .ffz-following-row,
.ffz-dark .moderation-card .interface,
.theatre .moderation-card .interface,
.dark .moderation-card .interface,
@ -3591,10 +3581,10 @@ body:not(.ffz-channel-bar-bottom).ffz-small-player.ffz-minimal-channel-bar #play
.ffz-share-box .qa-share-box__button { margin: 0 !important }
.cn-metabar__more > .cn-metabar__livecount { order: 1 }
.cn-metabar__more > #ffz-uptime-display { order: 2 }
.cn-metabar__more > #ffz-player-stats { order: 3 }
.cn-metabar__more > .cn-metabar__viewcount { order: 50; flex-grow: 0 !important }
/*.cn-metabar__more > .cn-metabar__ffz[data-key="uptime"] { order: 2 }
.cn-metabar__more > #ffz-chatter-display { order: 51 }
.cn-metabar__more > #ffz-player-stats { order: 3 }*/
.cn-metabar__more > .cn-metabar__viewcount { order: 50; flex-grow: 0 !important }
.cn-metabar__more > #ffz-ui-host-button { order: 98 }
.cn-metabar__more > .ffz-channel-broadcast-link { order: 100 }
@ -3619,4 +3609,48 @@ body:not(.ffz-channel-bar-bottom).ffz-small-player.ffz-minimal-channel-bar #play
.ffz-moderation-card .mod-icons .edit-note {
background-image: url("//cdn.frankerfacez.com/script/button_edit.svg");
background-repeat: no-repeat;
}
/* Following Menu */
#ffz-metadata-popup .scroller {
max-height: 420px;
overflow-y: scroll;
overflow-x: hidden;
margin: -1rem;
padding: 1rem;
}
/* Remove BTTV's ugly styling */
.ffz-following-row .button {
color: #fff !important;
box-shadow: none !important;
}
.ffz-following-row {
padding: 1rem 0;
line-height: 30px;
border-bottom: 1px solid rgba(0,0,0,0.2);
}
.ffz-following-row:first-child { padding-top: 0 }
.ffz-following-row:last-child { padding-bottom: 0; border-bottom: none }
.ffz-following-row .image:not([src]) { visibility: hidden }
.ffz-following-row .image {
display: inline-block;
height: 30px;
width: 30px;
margin-right: 1rem;
}
.ffz-following-row .right,
.ffz-following-row .button {
float: right;
}
.ffz-following-row .switch {
float: right;
margin: 7px 1rem 0 0;
}