1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-29 07:45:33 +00:00

Basic settings. Pull in FileSaver as a requirement. Menus can have sub-pages now, so that's cool.

This commit is contained in:
SirStendec 2015-08-04 01:43:08 -04:00
parent 9ece18ec0f
commit 771e290197
15 changed files with 2590 additions and 694 deletions

1622
script.js

File diff suppressed because it is too large Load diff

16
script.min.js vendored

File diff suppressed because one or more lines are too long

256
src/FileSaver.js Normal file
View file

@ -0,0 +1,256 @@
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 1.1.20150716
*
* By Eli Grey, http://eligrey.com
* License: X11/MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs || (function(view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = "download" in save_link
, click = function(node) {
var event = new MouseEvent("click");
node.dispatchEvent(event);
}
, webkit_req_fs = view.webkitRequestFileSystem
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
, throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
, fs_min_size = 0
// See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
// https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
// for the reasoning behind the timeout and revocation flow
, arbitrary_revoke_timeout = 500 // in ms
, revoke = function(file) {
var revoker = function() {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
if (view.chrome) {
revoker();
} else {
setTimeout(revoker, arbitrary_revoke_timeout);
}
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, auto_bom = function(blob) {
// prepend BOM for UTF-8 XML and text/* types (including HTML)
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob(["\ufeff", blob], {type: blob.type});
}
return blob;
}
, FileSaver = function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, blob_changed = false
, object_url
, target_view
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
// don't create more object URLs than needed
if (blob_changed || !object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (target_view) {
target_view.location.href = object_url;
} else {
var new_tab = view.open(object_url, "_blank");
if (new_tab == undefined && typeof safari !== "undefined") {
//Apple do not allow window.open, see http://bit.ly/1kZffRI
view.location.href = object_url
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
}
, abortable = function(func) {
return function() {
if (filesaver.readyState !== filesaver.DONE) {
return func.apply(this, arguments);
}
};
}
, create_if_not_found = {create: true, exclusive: false}
, slice
;
filesaver.readyState = filesaver.INIT;
if (!name) {
name = "download";
}
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
save_link.href = object_url;
save_link.download = name;
setTimeout(function() {
click(save_link);
dispatch_all();
revoke(object_url);
filesaver.readyState = filesaver.DONE;
});
return;
}
// Object and web filesystem URLs have a problem saving in Google Chrome when
// viewed in a tab, so I force save with application/octet-stream
// http://code.google.com/p/chromium/issues/detail?id=91158
// Update: Google errantly closed 91158, I submitted it again:
// https://code.google.com/p/chromium/issues/detail?id=389642
if (view.chrome && type && type !== force_saveable_type) {
slice = blob.slice || blob.webkitSlice;
blob = slice.call(blob, 0, blob.size, force_saveable_type);
blob_changed = true;
}
// Since I can't be sure that the guessed media type will trigger a download
// in WebKit, I append .download to the filename.
// https://bugs.webkit.org/show_bug.cgi?id=65440
if (webkit_req_fs && name !== "download") {
name += ".download";
}
if (type === force_saveable_type || webkit_req_fs) {
target_view = view;
}
if (!req_fs) {
fs_error();
return;
}
fs_min_size += blob.size;
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
var save = function() {
dir.getFile(name, create_if_not_found, abortable(function(file) {
file.createWriter(abortable(function(writer) {
writer.onwriteend = function(event) {
target_view.location.href = file.toURL();
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "writeend", event);
revoke(file);
};
writer.onerror = function() {
var error = writer.error;
if (error.code !== error.ABORT_ERR) {
fs_error();
}
};
"writestart progress write abort".split(" ").forEach(function(event) {
writer["on" + event] = filesaver["on" + event];
});
writer.write(blob);
filesaver.abort = function() {
writer.abort();
filesaver.readyState = filesaver.DONE;
};
filesaver.readyState = filesaver.WRITING;
}), fs_error);
}), fs_error);
};
dir.getFile(name, {create: false}, abortable(function(file) {
// delete file if it already exists
file.remove();
save();
}), abortable(function(ex) {
if (ex.code === ex.NOT_FOUND_ERR) {
save();
} else {
fs_error();
}
}));
}), fs_error);
}), fs_error);
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name, no_auto_bom) {
return new FileSaver(blob, name, no_auto_bom);
}
;
// IE 10+ (native saveAs)
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
return function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
return navigator.msSaveOrOpenBlob(blob, name || "download");
};
}
FS_proto.abort = function() {
var filesaver = this;
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "abort");
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(
typeof self !== "undefined" && self
|| typeof window !== "undefined" && window
|| this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
define([], function() {
return saveAs;
});
}

View file

@ -50,7 +50,7 @@ FFZ.prototype.setup_channel = function() {
view.ffzInit();
};
this.log("Hooking the Ember Channel model.");
Channel = App.__container__.resolve('model:channel');
if ( ! Channel )
@ -58,7 +58,7 @@ FFZ.prototype.setup_channel = function() {
Channel.reopen({
ffz_host_target: undefined,
setHostMode: function(e) {
if ( f.settings.hosted_channels ) {
this.set('ffz_host_target', e.target);
@ -87,13 +87,13 @@ FFZ.prototype.setup_channel = function() {
ffzUpdateInfo: function() {
if ( this._ffz_update_timer )
clearTimeout(this._ffz_update_timer);
if ( ! this.get('content.id') )
return;
this._ffz_update_timer = setTimeout(this.ffzCheckUpdate.bind(this), 60000);
}.observes("content.id"),
ffzCheckUpdate: function() {
var t = this,
id = t.get('content.id');
@ -115,11 +115,11 @@ FFZ.prototype.setup_channel = function() {
t.set('game', game);
t.set('rollbackData.game', game);
}
if ( data.stream.channel ) {
if ( data.stream.channel.status )
t.set('status', data.stream.channel.status);
if ( data.stream.channel.views )
t.set('views', data.stream.channel.views);
@ -176,7 +176,7 @@ FFZ.prototype.setup_channel = function() {
}.observes("content.hostModeTarget")
});
Channel.ffzUpdateInfo();
}
@ -265,8 +265,8 @@ FFZ.prototype._modify_cindex = function(view) {
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,
hosts_left = room && room.ffz_hosts_left,
el = this.get('element');
this.set('ffz_host_updating', false);
@ -283,13 +283,13 @@ FFZ.prototype._modify_cindex = function(view) {
btn = document.createElement('span');
btn.id = 'ffz-ui-host-button';
btn.className = 'button action tooltip';
btn.addEventListener('click', this.ffzClickHost.bind(btn, this, false));
var before;
try { before = container.querySelector(':scope > .theatre-button'); }
catch(err) { before = undefined; }
if ( before )
container.insertBefore(btn, before);
else
@ -307,8 +307,8 @@ FFZ.prototype._modify_cindex = function(view) {
btn.title += ' You have ' + hosts_left + ' host command' + utils.pluralize(hosts_left) + ' remaining this half hour.';
}
}
if ( hosted_id ) {
var container = el && el.querySelector('#hostmode .channel-actions'),
btn = container && container.querySelector('#ffz-ui-host-button');
@ -321,9 +321,9 @@ FFZ.prototype._modify_cindex = function(view) {
btn = document.createElement('span');
btn.id = 'ffz-ui-host-button';
btn.className = 'button action tooltip';
btn.addEventListener('click', this.ffzClickHost.bind(btn, this, true));
var before;
try { before = container.querySelector(':scope > .theatre-button'); }
catch(err) { before = undefined; }
@ -344,9 +344,9 @@ FFZ.prototype._modify_cindex = function(view) {
if ( typeof hosts_left === "number" )
btn.title += ' You have ' + hosts_left + ' host command' + utils.pluralize(hosts_left) + ' remaining this half hour.';
}
}
}
},
ffzClickHost: function(controller, is_host) {
var target = controller.get(is_host ? 'controller.hostModeTarget.id' : 'controller.id'),
user = f.get_user(),
@ -454,7 +454,7 @@ FFZ.prototype._modify_cindex = function(view) {
var container = el && el.querySelector('.stats-and-actions .channel-stats'),
stat_el = container && container.querySelector('#ffz-ui-player-stats'),
el = stat_el && stat_el.querySelector('span'),
player_cont = f.players && f.players[channel_id],
player = player_cont && player_cont.player,
stats = player && player.stats;
@ -468,29 +468,29 @@ FFZ.prototype._modify_cindex = function(view) {
stat_el = document.createElement('span');
stat_el.id = 'ffz-ui-player-stats';
stat_el.className = 'ffz stat tooltip';
stat_el.innerHTML = constants.GRAPH + " ";
el = document.createElement('span');
stat_el.appendChild(el);
var other = container.querySelector('#ffz-uptime-display');
if ( other )
container.insertBefore(stat_el, other.nextSibling);
else
container.appendChild(stat_el);
}
stat_el.title = 'Stream Latency\nFPS: ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps';
el.textContent = stats.hlsLatencyBroadcaster + 's';
}
}
if ( hosted_id ) {
var container = el && el.querySelector('#hostmode .channel-stats'),
stat_el = container && container.querySelector('#ffz-ui-player-stats'),
el = stat_el && stat_el.querySelector('span'),
player_cont = f.players && f.players[hosted_id],
player = player_cont && player_cont.player,
stats = player && player.stats;
@ -504,22 +504,22 @@ FFZ.prototype._modify_cindex = function(view) {
stat_el = document.createElement('span');
stat_el.id = 'ffz-ui-player-stats';
stat_el.className = 'ffz stat tooltip';
stat_el.innerHTML = constants.GRAPH + " ";
el = document.createElement('span');
stat_el.appendChild(el);
var other = container.querySelector('#ffz-uptime-display');
if ( other )
container.insertBefore(stat_el, other.nextSibling);
else
container.appendChild(stat_el);
}
stat_el.title = 'Stream Latency\nFPS: ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps';
el.textContent = stats.hlsLatencyBroadcaster + 's';
}
}
}
},
@ -667,10 +667,10 @@ FFZ.settings_info.hosted_channels = {
var cb = document.querySelector('input.ffz-setting-hosted-channels');
if ( cb )
cb.checked = val;
if ( ! this._cindex )
return;
var chan = this._cindex.get('controller.model'),
room = chan && this.rooms && this.rooms[chan.get('id')],
target = room && room.room && room.room.get('ffz_host_target');

View file

@ -12,7 +12,7 @@ FFZ.settings_info.minimal_chat = {
value: false,
category: "Chat Appearance",
name: "Minimalistic Chat",
help: "Hide all of the chat user interface, only showing messages and an input box.",
@ -25,22 +25,22 @@ FFZ.settings_info.minimal_chat = {
f._roomv && f._roomv.get('stuckToBottom') && f._roomv._scrollToBottom();
},0);
}
if ( this._chatv && this._chatv.get('controller.showList') )
this._chatv.set('controller.showList', false);
// Remove the style if we have it.
if ( ! val && this._chat_style ) {
if ( this._inputv ) {
if ( this._inputv._ffz_minimal_style )
this._inputv._ffz_minimal_style.innerHTML = '';
this._inputv._ffz_last_height = undefined;
}
utils.update_css(this._chat_style, "input_height", '');
this._roomv && this._roomv.get('stuckToBottom') && this._roomv._scrollToBottom();
} else if ( this._inputv )
this._inputv.ffzResizeInput();
}
@ -71,7 +71,7 @@ FFZ.settings_info.remove_deleted = {
total = msgs.get('length'),
i = total,
alternate;
while(i--) {
var msg = msgs.get(i);
@ -81,7 +81,7 @@ FFZ.settings_info.remove_deleted = {
msgs.removeAt(i);
continue;
}
if ( alternate === undefined )
alternate = msg.ffz_alternate;
else {
@ -186,11 +186,11 @@ FFZ.prototype.setup_chatview = function() {
ffzUpdateChannels: function() {
if ( ! f._chatv )
return;
f._chatv.ffzRebuildMenu();
if ( f.settings.group_tabs )
f._chatv.ffzRebuildTabs();
}.observes("currentChannelRoom", "connectedPrivateGroupRooms"),
removeCurrentChannelRoom: function() {
@ -204,7 +204,7 @@ FFZ.prototype.setup_chatview = function() {
if ( ! f.settings.pinned_rooms || f.settings.pinned_rooms.indexOf(room_id) === -1 ) {
if ( room === this.get("currentRoom") )
this.blurRoom();
// Don't destroy it if it's the user's room.
if ( room && user && user.login === room_id )
room.destroy();
@ -292,7 +292,7 @@ FFZ.prototype._modify_cview = function(view) {
ffzInit: function() {
f._chatv = this;
this.$('.textarea-contain').append(f.build_ui_link(this));
this.$('.chat-messages').find('.html-tooltip').tipsy({live: true, html: true});
this.$('.chat-messages').find('.html-tooltip').tipsy({live: true, html: true, gravity: jQuery.fn.tipsy.autoNS});
if ( !f.has_bttv && f.settings.group_tabs )
this.ffzEnableTabs();
@ -330,7 +330,7 @@ FFZ.prototype._modify_cview = function(view) {
if ( room )
rows.children('.ffz-room-row[data-room="' + room.get('id') + '"]').addClass('active').children('span').text('');
}
if ( this._ffz_group_table ) {
rows = jQuery(this._ffz_group_table);
rows.children('.ffz-room-row').removeClass('active');
@ -361,23 +361,23 @@ FFZ.prototype._modify_cview = function(view) {
}),
// Better Menu
ffzRebuildMenu: function() {
return;
var el = this.get('element'),
room_list = el && el.querySelector('.chat-rooms .tse-content');
if ( ! room_list )
return;
if ( ! room_list.classList.contains('ffz-room-list') ) {
room_list.classList.add('ffz-room-list');
// Find the Pending Invitations
var headers = room_list.querySelectorAll('.list-header'),
hdr = headers.length ? headers[headers.length-1] : undefined;
if ( hdr ) {
hdr.classList.add('ffz');
if ( hdr.nextSibling && hdr.nextSibling.classList )
@ -389,7 +389,7 @@ FFZ.prototype._modify_cview = function(view) {
// Channel Table
var t = this,
chan_table = this._ffz_chan_table || room_list.querySelector('#ffz-channel-table tbody');
if ( ! chan_table ) {
var tbl = document.createElement('table');
tbl.setAttribute('cellspacing', 0);
@ -409,13 +409,13 @@ FFZ.prototype._modify_cview = function(view) {
row = this.ffzBuildRow(this, room, true);
row && chan_table.appendChild(row);
}
// Host Target
if ( this._ffz_host_room ) {
row = this.ffzBuildRow(this, this._ffz_host_room, false, true);
row && chan_table.appendChild(row);
}
// Pinned Rooms
for(var i=0; i < f.settings.pinned_rooms.length; i++) {
var room_id = f.settings.pinned_rooms[i];
@ -424,8 +424,8 @@ FFZ.prototype._modify_cview = function(view) {
row && chan_table.appendChild(row);
}
}
// Group Chat Table
var group_table = this._ffz_group_table || room_list.querySelector('#ffz-group-table tbody');
if ( ! group_table ) {
@ -434,49 +434,49 @@ FFZ.prototype._modify_cview = function(view) {
tbl.id = 'ffz-group-table';
tbl.className = 'ffz';
tbl.innerHTML = '<thead><tr><th colspan="2">Group Chats</th><th class="ffz-row-switch">Pin</th></tr></thead><tbody></tbody>';
var before = room_list.querySelector('#ffz-channel-table');
room_list.insertBefore(tbl, before.nextSibling);
group_table = this._ffz_group_table = tbl.querySelector('tbody');
}
group_table.innerHTML = '';
_.each(this.get('controller.connectedPrivateGroupRooms'), function(room) {
var row = t.ffzBuildRow(t, room);
row && group_table && group_table.appendChild(row);
});
// Change Create Tooltip
var create_btn = el.querySelector('.button.create');
if ( create_btn )
create_btn.title = 'Create a Group Room';
},
ffzBuildRow: function(view, room, current_channel, host_channel) {
var row = document.createElement('tr'),
icon = document.createElement('td'),
name_el = document.createElement('td'),
btn,
toggle_pinned = document.createElement('td'),
toggle_visible = document.createElement('td'),
group = room.get('isGroupRoom'),
current = room === view.get('controller.currentRoom'),
//unread = format_unread(current ? 0 : room.get('unreadCount')),
name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'), function(name) {
f.log("Name for Row: " + name);
//unread = format_unread(current ? 0 : room.get('unreadCount'));
name_el.innerHTML = utils.sanitize(name);
}));
name_el.className = 'ffz-room';
name_el.innerHTML = utils.sanitize(name);
if ( current_channel ) {
icon.innerHTML = constants.CAMERA;
icon.title = name_el.title = "Current Channel";
@ -486,12 +486,12 @@ FFZ.prototype._modify_cview = function(view) {
icon.title = name_el.title = "Hosted Channel";
icon.className = name_el.className = 'tooltip';
}
toggle_pinned.className = toggle_visible.className = 'ffz-row-switch';
toggle_pinned.innerHTML = '<a class="switch' + (f.settings.pinned_rooms.indexOf(room.get('id')) !== -1 ? ' active' : '') + '"><span></span></a>';
toggle_visible.innerHTML = '<a class="switch' + (f.settings.visible_rooms.indexOf(room.get('id')) !== -1 ? ' active' : '') + '"><span></span></a>';
row.setAttribute('data-room', room.get('id'));
row.className = 'ffz-room-row';
@ -499,20 +499,20 @@ FFZ.prototype._modify_cview = function(view) {
row.classList.toggle('host-channel', host_channel);
row.classList.toggle('group-chat', group);
row.classList.toggle('active', current);
row.appendChild(icon);
row.appendChild(name_el);
if ( ! group ) {
row.appendChild(toggle_pinned);
btn = toggle_pinned.querySelector('a.switch');
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation && e.stopPropagation();
var room_id = room.get('id'),
is_pinned = f.settings.pinned_rooms.indexOf(room_id) !== -1;
if ( is_pinned )
f._leave_room(room_id);
else
@ -527,14 +527,14 @@ FFZ.prototype._modify_cview = function(view) {
btn.title = 'Leave Group';
name_el.appendChild(btn);
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation && e.stopPropagation();
if ( ! confirm('Are you sure you want to leave the group room "' + name + '"?') )
return;
room.get('isGroupRoom') && room.del();
});
}
@ -544,12 +544,12 @@ FFZ.prototype._modify_cview = function(view) {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation && e.stopPropagation();
var room_id = room.get('id'),
visible_rooms = f.settings.visible_rooms,
is_visible = visible_rooms.indexOf(room_id) !== -1;
if ( is_visible )
if ( is_visible )
visible_rooms.removeObject(room_id);
else
visible_rooms.push(room_id);
@ -558,13 +558,13 @@ FFZ.prototype._modify_cview = function(view) {
this.classList.toggle('active', !is_visible);
view.ffzRebuildTabs();
});
row.addEventListener('click', function() {
var controller = view.get('controller');
controller.focusRoom(room);
controller.set('showList', false);
});
return row;
},
@ -684,7 +684,7 @@ FFZ.prototype._modify_cview = function(view) {
ffzTabUnread: function(room_id) {
// TODO: Update menu.
if ( f.has_bttv || ! f.settings.group_tabs )
return;
@ -803,7 +803,7 @@ FFZ.prototype.connect_extra_chat = function() {
r = Room && Room.findOne(user.login);
}
}
if ( this.has_bttv )
return;

View file

@ -55,7 +55,8 @@ FFZ.settings_info.replace_bad_emotes = {
name: "Fix Low Quality Twitch Global Emoticons",
help: "Replace emoticons such as DansGame and RedCoat with cleaned up versions that don't have pixels around the edges or white backgrounds for nicer display on dark chat."
}
};
FFZ.settings_info.parse_emoji = {
type: "boolean",
@ -211,7 +212,6 @@ FFZ.settings_info.clickable_emoticons = {
};
FFZ.settings_info.link_info = {
type: "boolean",
value: true,
@ -333,7 +333,7 @@ FFZ.settings_info.high_contrast_chat = {
'112': "Background + Bold",
'111': 'All'
},
value: '000',
value: '222',
category: "Chat Appearance",
no_bttv: true,
@ -343,7 +343,7 @@ FFZ.settings_info.high_contrast_chat = {
process_value: function(val) {
if ( val === false )
return '000';
return '222';
else if ( val === true )
return '111';
return val;

View file

@ -50,6 +50,43 @@ try {
// Settings
// ----------------
FFZ.basic_settings.enhanced_moderation_cards = {
type: "boolean",
no_bttv: true,
category: "Chat",
name: "Enhanced Moderation Cards",
help: "Improve moderation cards with hotkeys, additional buttons, chat history, and other information to make moderating easier.",
get: function() {
return this.settings.mod_card_hotkeys &&
this.settings.mod_card_info &&
this.settings.mod_card_history;
},
set: function(val) {
this.settings.set('mod_card_hotkeys', val);
this.settings.set('mod_card_info', val);
this.settings.set('mod_card_history', val);
}
};
FFZ.basic_settings.chat_hover_pause = {
type: "boolean",
no_bttv: true,
category: "Chat",
name: "Pause Chat Scrolling on Mouse Hover",
help: "Automatically prevent the chat from scrolling when moving the mouse over it to prevent moderation mistakes and link misclicks.",
get: 'chat_hover_pause',
set: 'chat_hover_pause'
};
FFZ.settings_info.chat_hover_pause = {
type: "boolean",
value: false,
@ -58,7 +95,7 @@ FFZ.settings_info.chat_hover_pause = {
category: "Chat Moderation",
name: "Pause Chat Scrolling on Mouse Hover",
help: "Automatically prevent the chat from scrolling when moving the mouse over it to prevent moderation mistakes and link mis-clicks.",
help: "Automatically prevent the chat from scrolling when moving the mouse over it to prevent moderation mistakes and link misclicks.",
on_update: function(val) {
if ( ! this._roomv )
@ -151,7 +188,7 @@ FFZ.settings_info.mod_card_buttons = {
else
old_val += ' ' + cmd;
}
var new_val = prompt("Moderation Card Additional Buttons\n\nPlease enter a list of additional commands to display buttons for on moderation cards. Commands are separated by spaces. To include spaces in a command, surround the command with double quotes (\"). Use \"{user}\" to insert the user's username into the command, otherwise it will be appended to the end.\n\nExample: !permit \"!reg add {user}\"", old_val);
if ( new_val === null || new_val === undefined )
@ -159,7 +196,7 @@ FFZ.settings_info.mod_card_buttons = {
var vals = [];
new_val = new_val.trim();
while(new_val) {
if ( new_val.charAt(0) === '"' ) {
var end = new_val.indexOf('"', 1);
@ -170,8 +207,8 @@ FFZ.settings_info.mod_card_buttons = {
if ( segment )
vals.push(segment);
new_val = new_val.substr(end + 1);
new_val = new_val.substr(end + 1);
} else {
var ind = new_val.indexOf(' ');
if ( ind === -1 ) {
@ -274,7 +311,7 @@ FFZ.prototype.setup_mod_card = function() {
if ( typeof followers === "number" ) {
out += '<span class="stat tooltip" title="Followers">' + constants.HEART + ' ' + utils.number_commas(followers || 0) + '</span>';
} else if ( followers === undefined ) {
var t = this;
this.set('cardInfo.user.ffz_followers', false);
@ -299,7 +336,7 @@ FFZ.prototype.setup_mod_card = function() {
userName: Ember.computed("cardInfo.user.id", "cardInfo.user.display_name", function() {
var user_id = this.get("cardInfo.user.id"),
alias = f.aliases[user_id];
return alias || this.get("cardInfo.user.display_name") || user_id.capitalize();
}),
@ -313,7 +350,7 @@ FFZ.prototype.setup_mod_card = function() {
var el = this.get('element'),
controller = this.get('controller'),
line,
user_id = controller.get('cardInfo.user.id'),
alias = f.aliases[user_id];
@ -340,7 +377,7 @@ FFZ.prototype.setup_mod_card = function() {
after = el.querySelector('h3.name');
if ( after ) {
el.classList.add('ffz-has-info');
info.className = 'info channel-stats';
info.className = 'info channel-stats';
after.parentElement.insertBefore(info, after.nextSibling);
this.ffzRebuildInfo();
}
@ -350,16 +387,16 @@ FFZ.prototype.setup_mod_card = function() {
if ( f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) {
line = document.createElement('div');
line.className = 'extra-interface interface clearfix';
var cmds = {},
add_btn_click = function(cmd) {
var user_id = controller.get('cardInfo.user.id'),
cont = App.__container__.lookup('controller:chat'),
room = cont && cont.get('currentRoom');
room && room.send(cmd.replace(/{user}/g, user_id));
},
add_btn_make = function(cmd) {
var btn = document.createElement('button'),
segment = cmd.split(' ', 1)[0],
@ -373,12 +410,12 @@ FFZ.prototype.setup_mod_card = function() {
btn.className = 'button';
btn.innerHTML = utils.sanitize(title);
btn.title = utils.sanitize(cmd.replace(/{user}/g, controller.get('cardInfo.user.id') || '{user}'));
jQuery(btn).tipsy();
btn.addEventListener('click', add_btn_click.bind(this, cmd));
return btn;
};
var cmds = {};
for(var i=0; i < f.settings.mod_card_buttons.length; i++)
cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] = (cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] || 0) + 1;
@ -392,7 +429,7 @@ FFZ.prototype.setup_mod_card = function() {
line.appendChild(add_btn_make(cmd))
}
el.appendChild(line);
}
@ -431,7 +468,7 @@ FFZ.prototype.setup_mod_card = function() {
// Only do the big stuff if we're mod.
if ( controller.get('cardInfo.isModeratorOrHigher') ) {
el.classList.add('ffz-is-mod');
// Key Handling
if ( f.settings.mod_card_hotkeys ) {
el.classList.add('no-mousetrap');
@ -544,31 +581,31 @@ FFZ.prototype.setup_mod_card = function() {
msg_btn.title = "Whisper User";
jQuery(msg_btn).tipsy();
var real_msg = document.createElement('button');
real_msg.className = 'message-button button glyph-only message tooltip';
real_msg.innerHTML = MESSAGE;
real_msg.title = "Message User";
real_msg.addEventListener('click', function() {
window.open('http://www.twitch.tv/message/compose?to=' + controller.get('cardInfo.user.id'));
})
msg_btn.parentElement.insertBefore(real_msg, msg_btn.nextSibling);
}
// Alias Button
var alias_btn = document.createElement('button');
alias_btn.className = 'alias button glyph-only tooltip';
alias_btn.innerHTML = constants.EDIT;
alias_btn.title = "Set Alias";
alias_btn.addEventListener('click', function() {
var user = controller.get('cardInfo.user.id'),
alias = f.aliases[user];
var new_val = prompt("Alias for User: " + user + "\n\nPlease enter an alias for the user. Leave it blank to remove the alias.", alias);
if ( new_val === null || new_val === undefined )
return;
@ -579,20 +616,20 @@ FFZ.prototype.setup_mod_card = function() {
f.aliases[user] = new_val;
f.save_aliases();
// Update UI
f._update_alias(user);
Ember.propertyDidChange(controller, 'userName');
var name = el.querySelector('h3.name'),
link = name && name.querySelector('a');
if ( link )
name = link;
if ( name )
name.classList.toggle('ffz-alias', new_val);
name.classList.toggle('ffz-alias', new_val);
});
if ( msg_btn )
msg_btn.parentElement.insertBefore(alias_btn, msg_btn);
else {
@ -676,22 +713,21 @@ FFZ.prototype.setup_mod_card = function() {
FFZ.prototype._update_alias = function(user) {
var alias = this.aliases && this.aliases[user],
display_name = alias,
cap_name = FFZ.get_capitalization(user),
display_name = alias || cap_name,
el = this._roomv && this._roomv.get('element'),
lines = el && el.querySelectorAll('.chat-line[data-sender="' + user + '"]');
if ( ! lines )
return;
if ( ! display_name )
display_name = FFZ.get_capitalization(user);
for(var i=0, l = lines.length; i < l; i++) {
var line = lines[i],
el_from = line.querySelector('.from');
el_from.classList.toggle('ffz-alias', alias);
el_from.textContent = display_name;
el_from.title = alias ? cap_name : '';
}
}

View file

@ -33,12 +33,12 @@ FFZ.prototype.setup_bttv = function(delay) {
this._dark_style.parentElement.removeChild(this._dark_style);
this._dark_style = undefined;
}
if ( this._layout_style ) {
this._layout_style.parentElement.removeChild(this._layout_style);
this._layout_style = undefined;
}
if ( this._chat_style ) {
utils.update_css(this._chat_style, 'chat_font_size', '');
utils.update_css(this._chat_style, 'chat_ts_font_size', '');
@ -76,6 +76,7 @@ FFZ.prototype.setup_bttv = function(delay) {
if ( this.settings.following_count ) {
this._schedule_following_count();
this._draw_following_count();
this._draw_following_channels();
}
// Remove Sub Count
@ -186,7 +187,7 @@ FFZ.prototype.setup_bttv = function(delay) {
var original_emoticonize = BetterTTV.chat.templates.emoticonize;
BetterTTV.chat.templates.emoticonize = function(message, emotes) {
var tokens = original_emoticonize(message, emotes),
room = (received_room || BetterTTV.getChannel()),
l_room = room && room.toLowerCase(),
l_sender = received_sender && received_sender.toLowerCase(),

View file

@ -21,7 +21,7 @@ FFZ.get = function() { return FFZ.instance; }
// Version
var VER = FFZ.version_info = {
major: 3, minor: 5, revision: 10,
major: 3, minor: 5, revision: 12,
toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
}

View file

@ -1,5 +1,6 @@
var FFZ = window.FrankerFaceZ,
constants = require("./constants");
constants = require("./constants"),
FileSaver = require("./FileSaver");
make_ls = function(key) {
@ -11,9 +12,28 @@ var FFZ = window.FrankerFaceZ,
this.settings.set(key, val);
swit.classList.toggle('active', val);
},
option_setting = function(select, key) {
this.settings.set(key, JSON.parse(select.options[select.selectedIndex].value));
},
toggle_basic_setting = function(swit, key) {
var getter = FFZ.basic_settings[key].get,
val = !(typeof getter === 'function' ? getter.bind(this)() : this.settings.get(getter)),
setter = FFZ.basic_settings[key].set;
if ( typeof setter === 'function' )
setter.bind(this)(val);
else
this.settings.set(setter, val);
swit.classList.toggle('active', val);
},
option_basic_setting = function(select, key) {
FFZ.basic_settings[key].set.bind(this)(JSON.parse(select.options[select.selectedIndex].value));
};
@ -21,7 +41,11 @@ var FFZ = window.FrankerFaceZ,
// Initializer
// --------------------
FFZ.settings_info = {};
FFZ.settings_info = {
advanced_settings: { value: false, visible: false }
};
FFZ.basic_settings = {};
FFZ.prototype.load_settings = function() {
this.log("Loading settings.");
@ -61,200 +85,610 @@ FFZ.prototype.load_settings = function() {
}
// --------------------
// Backup and Restore
// --------------------
FFZ.prototype.save_settings_file = function() {
var data = {
version: 1,
script_version: FFZ.version_info + '',
aliases: this.aliases,
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);
if ( localStorage.hasOwnProperty(ls_key) )
data.settings[key] = this.settings[key];
}
var blob = new Blob([JSON.stringify(data, null, 4)], {type: "application/json;charset=utf-8"});
FileSaver.saveAs(blob, "ffz-settings.json");
}
FFZ.prototype.load_settings_file = function(file) {
if ( typeof file === "string" )
this._load_settings_file(file);
else {
var reader = new FileReader(),
f = this;
reader.onload = function(e) { f._load_settings_file(e.target.result); }
reader.readAsText(file);
}
}
FFZ.prototype._load_settings_file = function(data) {
try {
data = JSON.parse(data);
} catch(err) {
this.error("Error Loading Settings: " + err);
return alert("There was an error attempting to read the provided settings data.");
}
this.log("Loading Settings Data", data);
var skipped = [],
applied = [];
if ( data.settings ) {
for(var key in data.settings) {
if ( ! FFZ.settings_info.hasOwnProperty(key) ) {
skipped.push(key);
continue;
}
var info = FFZ.settings_info[key],
val = data.settings[key];
if ( info.process_value )
val = info.process_value.bind(this)(val);
if ( val !== this.settings.get(key) )
this.settings.set(key, val);
applied.push(key);
}
}
// Do this in a timeout so that any styles have a moment to update.
setTimeout(function(){
alert('Successfully loaded ' + applied.length + ' settings and skipped ' + skipped.length + ' settings.');
});
}
// --------------------
// Menu Page
// --------------------
FFZ.menu_pages.settings = {
render: function(view, container) {
var settings = {},
categories = [],
is_android = navigator.userAgent.indexOf('Android') !== -1;
// Bottom Bar
var menu = document.createElement('ul'),
page = document.createElement('div'),
for(var key in FFZ.settings_info) {
if ( ! FFZ.settings_info.hasOwnProperty(key) )
tab_basic = document.createElement('li'),
link_basic = document.createElement('a'),
tab_adv = document.createElement('li'),
link_adv = document.createElement('a'),
tab_save = document.createElement('li'),
link_save = document.createElement('a'),
height = parseInt(container.style.maxHeight || '0');
// Height Calculation
if ( ! height )
height = Math.max(200, view.$().height() - 172);
if ( height && height !== NaN ) {
height -= 37;
page.style.maxHeight = height + 'px';
}
// Menu Building
page.className = 'ffz-ui-sub-menu-page';
menu.className = 'menu sub-menu clearfix';
tab_basic.className = 'item';
tab_basic.id = 'ffz-settings-page-basic';
link_basic.innerHTML = 'Basic';
tab_basic.appendChild(link_basic);
tab_adv.className = 'item';
tab_adv.id = 'ffz-settings-page-advanced';
link_adv.innerHTML = 'Advanced';
tab_adv.appendChild(link_adv);
tab_save.className = 'item';
tab_save.id = 'ffz-settings-page-save';
link_save.textContent = 'Backup & Restore';
tab_save.appendChild(link_save);
menu.appendChild(tab_basic);
menu.appendChild(tab_adv);
menu.appendChild(tab_save);
var cp = FFZ.menu_pages.settings.change_page;
link_basic.addEventListener('click', cp.bind(this, view, container, menu, page, 'basic'));
link_adv.addEventListener('click', cp.bind(this, view, container, menu, page, 'advanced'));
link_save.addEventListener('click', cp.bind(this, view, container, menu, page, 'save'));
if ( this.settings.advanced_settings )
link_adv.click();
else
link_basic.click();
container.appendChild(page);
container.appendChild(menu);
},
change_page: function(view, container, menu, page, key) {
page.innerHTML = '';
page.setAttribute('data-page', key);
var els = menu.querySelectorAll('li.active');
for(var i=0, l = els.length; i < l; i++)
els[i].classList.remove('active');
var el = menu.querySelector('#ffz-settings-page-' + key);
if ( el )
el.classList.add('active');
FFZ.menu_pages.settings['render_' + key].bind(this)(view, page);
if ( key === 'advanced' )
this.settings.set('advanced_settings', true);
else if ( key === 'basic' )
this.settings.set('advanced_settings', false);
},
render_save: function(view, container) {
var backup_head = document.createElement('div'),
restore_head = document.createElement('div'),
backup_cont = document.createElement('div'),
restore_cont = document.createElement('div'),
backup_para = document.createElement('p'),
backup_link = document.createElement('a'),
backup_help = document.createElement('span'),
restore_para = document.createElement('p'),
restore_input = document.createElement('input'),
restore_link = document.createElement('a'),
restore_help = document.createElement('span'),
f = this;
backup_cont.className = 'chat-menu-content';
backup_head.className = 'heading';
backup_head.innerHTML = 'Backup Settings';
backup_cont.appendChild(backup_head);
backup_para.className = 'clearfix option';
backup_link.href = '#';
backup_link.innerHTML = 'Save to File';
backup_link.addEventListener('click', this.save_settings_file.bind(this));
backup_help.className = 'help';
backup_help.innerHTML = 'This generates a JSON file containing all of your settings and prompts you to save it.';
backup_para.appendChild(backup_link);
backup_para.appendChild(backup_help);
backup_cont.appendChild(backup_para);
restore_cont.className = 'chat-menu-content';
restore_head.className = 'heading';
restore_head.innerHTML = 'Restore Settings';
restore_cont.appendChild(restore_head);
restore_para.className = 'clearfix option';
restore_input.type = 'file';
restore_input.addEventListener('change', function() { f.load_settings_file(this.files[0]); })
restore_link.href = '#';
restore_link.innerHTML = 'Restore from File';
restore_link.addEventListener('click', function(e) { e.preventDefault(); restore_input.click(); });
restore_help.className = 'help';
restore_help.innerHTML = 'This loads settings from a previously generated JSON file.';
restore_para.appendChild(restore_link);
restore_para.appendChild(restore_help);
restore_cont.appendChild(restore_para);
container.appendChild(backup_cont);
container.appendChild(restore_cont);
},
render_basic: function(view, container) {
var settings = {},
categories = [],
is_android = navigator.userAgent.indexOf('Android') !== -1;
for(var key in FFZ.basic_settings) {
if ( ! FFZ.basic_settings.hasOwnProperty(key) )
continue;
var info = FFZ.basic_settings[key],
cat = info.category || "Miscellaneous",
cs = settings[cat];
if ( info.visible !== undefined && info.visible !== null ) {
var visible = info.visible;
if ( typeof info.visible == "function" )
visible = info.visible.bind(this)();
if ( ! visible )
continue;
var info = FFZ.settings_info[key],
cat = info.category || "Miscellaneous",
cs = settings[cat];
if ( info.visible !== undefined && info.visible !== null ) {
var visible = info.visible;
if ( typeof info.visible == "function" )
visible = info.visible.bind(this)();
if ( ! visible )
continue;
}
if ( is_android && info.no_mobile )
continue;
if ( ! cs ) {
categories.push(cat);
cs = settings[cat] = [];
}
cs.push([key, info]);
}
categories.sort(function(a,b) {
var a = a.toLowerCase(),
b = b.toLowerCase();
if ( is_android && info.no_mobile )
continue;
if ( a === "debugging" )
a = "zzz" + a;
if ( ! cs ) {
categories.push(cat);
cs = settings[cat] = [];
}
if ( b === "debugging" )
b = "zzz" + b;
cs.push([key, info]);
}
categories.sort(function(a,b) {
var a = a.toLowerCase(),
b = b.toLowerCase();
if ( a === "debugging" )
a = "zzz" + a;
if ( b === "debugging" )
b = "zzz" + b;
if ( a < b ) return -1;
else if ( a > b ) return 1;
return 0;
});
var f = this,
current_page = this._ffz_basic_settings_page || categories[0];
for(var ci=0; ci < categories.length; ci++) {
var category = categories[ci],
cset = settings[category],
menu = document.createElement('div'),
heading = document.createElement('div');
heading.className = 'heading';
menu.className = 'chat-menu-content'; // collapsable';
menu.setAttribute('data-category', category);
//menu.classList.toggle('collapsed', current_page !== category);
heading.innerHTML = category;
menu.appendChild(heading);
/*menu.addEventListener('click', function() {
if ( ! this.classList.contains('collapsed') )
return;
var t = this,
old_selection = container.querySelectorAll('.chat-menu-content:not(.collapsed)');
for(var i=0; i < old_selection.length; i++)
old_selection[i].classList.add('collapsed');
f._ffz_basic_settings_page = t.getAttribute('data-category');
t.classList.remove('collapsed');
setTimeout(function(){t.scrollIntoViewIfNeeded()});
});*/
cset.sort(function(a,b) {
var a = a[1],
b = b[1],
at = a.type === "boolean" ? 1 : 2,
bt = b.type === "boolean" ? 1 : 2,
an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( at < bt ) return -1;
else if ( at > bt ) return 1;
else if ( an < bn ) return -1;
else if ( an > bn ) return 1;
if ( a < b ) return -1;
else if ( a > b ) return 1;
return 0;
});
var f = this,
current_page = this._ffz_settings_page || categories[0];
for(var i=0; i < cset.length; i++) {
var key = cset[i][0],
info = cset[i][1],
el = document.createElement('p'),
val = info.type !== "button" && typeof info.get === 'function' ? info.get.bind(this)() : this.settings.get(info.get);
for(var ci=0; ci < categories.length; ci++) {
var category = categories[ci],
cset = settings[category],
el.className = 'clearfix';
menu = document.createElement('div'),
heading = document.createElement('div');
if ( this.has_bttv && info.no_bttv ) {
var label = document.createElement('span'),
help = document.createElement('span');
label.className = 'switch-label';
label.innerHTML = info.name;
heading.className = 'heading';
menu.className = 'chat-menu-content collapsable';
menu.setAttribute('data-category', category);
menu.classList.toggle('collapsed', current_page !== category);
help = document.createElement('span');
help.className = 'help';
help.innerHTML = 'Disabled due to incompatibility with BetterTTV.';
heading.innerHTML = category;
menu.appendChild(heading);
el.classList.add('disabled');
el.appendChild(label);
el.appendChild(help);
menu.addEventListener('click', function() {
if ( ! this.classList.contains('collapsed') )
return;
} else {
if ( info.type == "boolean" ) {
var swit = document.createElement('a'),
label = document.createElement('span');
var t = this,
old_selection = container.querySelectorAll('.chat-menu-content:not(.collapsed)');
for(var i=0; i < old_selection.length; i++)
old_selection[i].classList.add('collapsed');
f._ffz_settings_page = t.getAttribute('data-category');
t.classList.remove('collapsed');
setTimeout(function(){t.scrollIntoViewIfNeeded()});
});
swit.className = 'switch';
swit.classList.toggle('active', val);
swit.innerHTML = "<span></span>";
cset.sort(function(a,b) {
var a = a[1],
b = b[1],
at = a.type === "boolean" ? 1 : 2,
bt = b.type === "boolean" ? 1 : 2,
an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( at < bt ) return -1;
else if ( at > bt ) return 1;
else if ( an < bn ) return -1;
else if ( an > bn ) return 1;
return 0;
});
for(var i=0; i < cset.length; i++) {
var key = cset[i][0],
info = cset[i][1],
el = document.createElement('p'),
val = this.settings.get(key);
el.className = 'clearfix';
if ( this.has_bttv && info.no_bttv ) {
var label = document.createElement('span'),
help = document.createElement('span');
label.className = 'switch-label';
label.innerHTML = info.name;
help = document.createElement('span');
help.className = 'help';
help.innerHTML = 'Disabled due to incompatibility with BetterTTV.';
el.classList.add('disabled');
el.appendChild(swit);
el.appendChild(label);
el.appendChild(help);
swit.addEventListener("click", toggle_basic_setting.bind(this, swit, key));
} else if ( info.type === "select" ) {
var select = document.createElement('select'),
label = document.createElement('span');
label.className = 'option-label';
label.innerHTML = info.name;
for(var ok in info.options) {
var op = document.createElement('option');
op.value = JSON.stringify(ok);
if ( val === ok )
op.setAttribute('selected', true);
op.innerHTML = info.options[ok];
select.appendChild(op);
}
select.addEventListener('change', option_basic_setting.bind(this, select, key));
el.appendChild(label);
el.appendChild(select);
} else {
if ( info.type == "boolean" ) {
var swit = document.createElement('a'),
label = document.createElement('span');
el.classList.add("option");
var link = document.createElement('a');
link.innerHTML = info.name;
link.href = "#";
el.appendChild(link);
swit.className = 'switch';
swit.classList.toggle('active', val);
swit.innerHTML = "<span></span>";
label.className = 'switch-label';
label.innerHTML = info.name;
el.appendChild(swit);
el.appendChild(label);
swit.addEventListener("click", toggle_setting.bind(this, swit, key));
} else if ( info.type === "select" ) {
var select = document.createElement('select'),
label = document.createElement('span');
label.className = 'option-label';
label.innerHTML = info.name;
for(var ok in info.options) {
var op = document.createElement('option');
op.value = JSON.stringify(ok);
if ( val === ok )
op.setAttribute('selected', true);
op.innerHTML = info.options[ok];
select.appendChild(op);
}
select.addEventListener('change', option_setting.bind(this, select, key));
el.appendChild(label);
el.appendChild(select);
} else {
el.classList.add("option");
var link = document.createElement('a');
link.innerHTML = info.name;
link.href = "#";
el.appendChild(link);
link.addEventListener("click", info.method.bind(this));
}
if ( info.help ) {
var help = document.createElement('span');
help.className = 'help';
help.innerHTML = info.help;
el.appendChild(help);
}
link.addEventListener("click", info.method.bind(this));
}
menu.appendChild(el);
if ( info.help ) {
var help = document.createElement('span');
help.className = 'help';
help.innerHTML = info.help;
el.appendChild(help);
}
}
container.appendChild(menu);
menu.appendChild(el);
}
},
container.appendChild(menu);
}
},
render_advanced: function(view, container) {
var settings = {},
categories = [],
is_android = navigator.userAgent.indexOf('Android') !== -1;
for(var key in FFZ.settings_info) {
if ( ! FFZ.settings_info.hasOwnProperty(key) )
continue;
var info = FFZ.settings_info[key],
cat = info.category || "Miscellaneous",
cs = settings[cat];
if ( info.visible !== undefined && info.visible !== null ) {
var visible = info.visible;
if ( typeof info.visible == "function" )
visible = info.visible.bind(this)();
if ( ! visible )
continue;
}
if ( is_android && info.no_mobile )
continue;
if ( ! cs ) {
categories.push(cat);
cs = settings[cat] = [];
}
cs.push([key, info]);
}
categories.sort(function(a,b) {
var a = a.toLowerCase(),
b = b.toLowerCase();
if ( a === "debugging" )
a = "zzz" + a;
if ( b === "debugging" )
b = "zzz" + b;
if ( a < b ) return -1;
else if ( a > b ) return 1;
return 0;
});
var f = this,
current_page = this._ffz_settings_page || categories[0];
for(var ci=0; ci < categories.length; ci++) {
var category = categories[ci],
cset = settings[category],
menu = document.createElement('div'),
heading = document.createElement('div');
heading.className = 'heading';
menu.className = 'chat-menu-content collapsable';
menu.setAttribute('data-category', category);
menu.classList.toggle('collapsed', current_page !== category);
heading.innerHTML = category;
menu.appendChild(heading);
menu.addEventListener('click', function() {
if ( ! this.classList.contains('collapsed') )
return;
var t = this,
old_selection = container.querySelectorAll('.chat-menu-content:not(.collapsed)');
for(var i=0; i < old_selection.length; i++)
old_selection[i].classList.add('collapsed');
f._ffz_settings_page = t.getAttribute('data-category');
t.classList.remove('collapsed');
setTimeout(function(){t.scrollIntoViewIfNeeded()});
});
cset.sort(function(a,b) {
var a = a[1],
b = b[1],
at = a.type === "boolean" ? 1 : 2,
bt = b.type === "boolean" ? 1 : 2,
an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( at < bt ) return -1;
else if ( at > bt ) return 1;
else if ( an < bn ) return -1;
else if ( an > bn ) return 1;
return 0;
});
for(var i=0; i < cset.length; i++) {
var key = cset[i][0],
info = cset[i][1],
el = document.createElement('p'),
val = this.settings.get(key);
el.className = 'clearfix';
if ( this.has_bttv && info.no_bttv ) {
var label = document.createElement('span'),
help = document.createElement('span');
label.className = 'switch-label';
label.innerHTML = info.name;
help = document.createElement('span');
help.className = 'help';
help.innerHTML = 'Disabled due to incompatibility with BetterTTV.';
el.classList.add('disabled');
el.appendChild(label);
el.appendChild(help);
} else {
if ( info.type == "boolean" ) {
var swit = document.createElement('a'),
label = document.createElement('span');
swit.className = 'switch';
swit.classList.toggle('active', val);
swit.innerHTML = "<span></span>";
label.className = 'switch-label';
label.innerHTML = info.name;
el.appendChild(swit);
el.appendChild(label);
swit.addEventListener("click", toggle_setting.bind(this, swit, key));
} else if ( info.type === "select" ) {
var select = document.createElement('select'),
label = document.createElement('span');
label.className = 'option-label';
label.innerHTML = info.name;
for(var ok in info.options) {
var op = document.createElement('option');
op.value = JSON.stringify(ok);
if ( val === ok )
op.setAttribute('selected', true);
op.innerHTML = info.options[ok];
select.appendChild(op);
}
select.addEventListener('change', option_setting.bind(this, select, key));
el.appendChild(label);
el.appendChild(select);
} else {
el.classList.add("option");
var link = document.createElement('a');
link.innerHTML = info.name;
link.href = "#";
el.appendChild(link);
link.addEventListener("click", info.method.bind(this));
}
if ( info.help ) {
var help = document.createElement('span');
help.className = 'help';
help.innerHTML = info.help;
el.appendChild(help);
}
}
menu.appendChild(el);
}
container.appendChild(menu);
}
},
name: "Settings",
icon: constants.GEAR,
sort_order: 99999,
wide: true
wide: true,
sub_menu: true
};

View file

@ -6,6 +6,107 @@ var FFZ = window.FrankerFaceZ,
// Settings
// ---------------------
FFZ.basic_settings.dark_twitch = {
type: "boolean",
no_bttv: true,
category: "General",
name: "Dark Twitch",
help: "Apply a dark background to channels and other related pages for easier viewing.",
get: function() {
return this.settings.dark_twitch;
},
set: function(val) {
this.settings.set('dark_twitch', val);
this.settings.set('dark_no_blue', val);
}
};
FFZ.basic_settings.separated_chat = {
type: "boolean",
no_bttv: true,
category: "Chat",
name: "Separated Lines",
help: "Use alternating rows and thin lines to visually separate chat messages for easier reading.",
get: function() {
return this.settings.chat_rows && this.settings.chat_separators !== '0';
},
set: function(val) {
this.settings.set('chat_rows', val);
this.settings.set('chat_separators', val ? '2' : '0');
}
};
FFZ.basic_settings.minimalistic_chat = {
type: "boolean",
category: "Chat",
name: "Minimalistic UI",
help: "Hide all of chat except messages and the input box and reduce chat margins.",
get: function() {
return this.settings.minimal_chat && this.settings.chat_padding;
},
set: function(val) {
this.settings.set('minimal_chat', val);
this.settings.set('chat_padding', val);
}
};
FFZ.basic_settings.high_contrast = {
type: "boolean",
category: "Chat",
no_bttv: true,
name: "High Contrast",
help: "Display chat using white and black for maximum contrast. This is suitable for capturing and chroma keying chat to display on stream.",
get: function() {
return this.settings.high_contrast_chat !== '222';
},
set: function(val) {
this.settings.set('high_contrast_chat', val ? '111': '222');
}
};
FFZ.basic_settings.keywords = {
type: "button",
category: "Chat",
no_bttv: true,
name: "Highlight Keywords",
help: "Set additional keywords that will be highlighted in chat.",
method: function() {
FFZ.settings_info.keywords.method.bind(this)();
}
};
FFZ.basic_settings.banned_words = {
type: "button",
category: "Chat",
no_bttv: true,
name: "Banned Keywords",
help: "Set a list of words that will be removed from chat messages, locally.",
method: function() {
FFZ.settings_info.banned_words.method.bind(this)();
}
};
FFZ.settings_info.twitch_chat_dark = {
type: "boolean",
value: false,
@ -28,7 +129,7 @@ FFZ.settings_info.dark_twitch = {
var cb = document.querySelector('input.ffz-setting-dark-twitch');
if ( cb )
cb.checked = val;
if ( this.has_bttv )
return;

View file

@ -1,19 +1,25 @@
var FFZ = window.FrankerFaceZ,
utils = require('../utils'),
constants = require('../constants');
constants = require('../constants'),
FOLLOW_GRAVITY = function(f, el) {
return (f.settings.following_count && el.parentElement.getAttribute('data-name') === 'following' ? 'n' : '') + (f.settings.swap_sidebars ? 'e' : 'w');
},
WIDE_TIP = function(f, el) {
return ( ! f.settings.following_count || (el.id !== 'header_following' && el.parentElement.getAttribute('data-name') !== 'following') ) ? '' : 'ffz-wide-tip';
};
FFZ.settings_info.following_count = {
type: "boolean",
value: true,
no_bttv: true,
no_mobile: true,
category: "Appearance",
name: "Sidebar Following Count",
help: "Display the number of live channels you're following on the sidebar.",
name: "Sidebar Following Data",
help: "Display the number of live channels you're following on the sidebar, and list the channels in a tooltip.",
on_update: function(val) {
this._schedule_following_count();
@ -21,10 +27,14 @@ FFZ.settings_info.following_count = {
var Stream = window.App && App.__container__.resolve('model:stream'),
Live = Stream && Stream.find("live");
if ( Live )
this._draw_following_count(Live.get('total') || 0);
else
if ( Live ) {
var total = Live.get('total') || 0;
this._draw_following_count(total);
this._draw_following_channels(Live.get('content'), total);;
} else {
this._update_following_count();
this._draw_following_channels();
}
}
};
@ -37,6 +47,9 @@ FFZ.prototype.setup_following_count = function(has_ember) {
if ( this.settings.following_count )
this._schedule_following_count();
// Tooltips~!
this._install_following_tooltips();
// If we don't have Ember, no point in trying this stuff.
if ( ! has_ember )
return this._update_following_count();
@ -68,7 +81,7 @@ FFZ.prototype.setup_following_count = function(has_ember) {
FFZ.prototype._schedule_following_count = function() {
if ( this.has_bttv || ! this.settings.following_count ) {
if ( ! this.settings.following_count ) {
if ( this._following_count_timer ) {
clearTimeout(this._following_count_timer);
this._following_count_timer = undefined;
@ -110,9 +123,21 @@ FFZ.prototype._update_following_count = function() {
}
FFZ.prototype._draw_following_channels = function(streams, total) {
// First, build the data.
var tooltip = 'Following';
FFZ.prototype._build_following_tooltip = function(el) {
if ( el.id !== 'header_following' && el.parentElement.getAttribute('data-name') !== 'following' )
return el.getAttribute('original-title');
if ( ! this.settings.following_count )
return 'Following';
var tooltip = (this.has_bttv ? '<span class="stat playing">FrankerFaceZ</span>' : '') + 'Following',
bb = el.getBoundingClientRect(),
height = document.body.clientHeight - (bb.bottom + 54),
max_lines = Math.max(Math.floor(height / 36) - 1, 2),
streams = this._tooltip_streams,
total = this._tooltip_total || (streams && streams.length) || 0;
if ( streams && streams.length ) {
var c = 0;
@ -122,61 +147,93 @@ FFZ.prototype._draw_following_channels = function(streams, total) {
continue;
c += 1;
if ( c > 5 ) {
var ttl = total || streams.length;
tooltip += '<hr><span>And ' + utils.number_commas(ttl - 5) + ' more...</span>';
if ( c > max_lines ) {
tooltip += '<hr><span>And ' + utils.number_commas(total - max_lines) + ' more...</span>';
break;
}
tooltip += (i > 0 ? '<br>' : '<hr>') + '<span class="viewers">' + constants.LIVE + ' ' + utils.number_commas(stream.viewers) + '</span><b>' + utils.sanitize(stream.channel.display_name || stream.channel.name) + '</b><br><span class="playing">' + (stream.channel.game ? 'Playing ' + utils.sanitize(stream.channel.game) : 'Not Playing') + '</span>';
}
}
var up_since = this.settings.stream_uptime && stream.created_at && utils.parse_date(stream.created_at),
uptime = up_since && Math.floor((Date.now() - up_since.getTime()) / 1000) || 0,
minutes = Math.floor(uptime / 60) % 60,
hours = Math.floor(uptime / 3600);
tooltip += (i === 0 ? '<hr>' : '') +
(uptime > 0 ? '<span class="stat">' + constants.CLOCK + ' ' + (hours > 0 ? hours + 'h' : '') + minutes + 'm</span>' : '') +
'<span class="stat">' + constants.LIVE + ' ' + utils.number_commas(stream.viewers) + '</span>' +
'<b>' + utils.sanitize(stream.channel.display_name || stream.channel.name) + '</b><br>' +
'<span class="playing">' + (stream.channel.game ? 'Playing ' + utils.sanitize(stream.channel.game) : 'Not Playing') + '</span>';
}
} else
tooltip += "<hr>No one you're following is online.";
// Reposition the tooltip.
setTimeout(function() {
var tip = document.querySelector('.tipsy'),
bb = tip.getBoundingClientRect(),
left = parseInt(tip.style.left || '0'),
right = bb.left + tip.scrollWidth;
if ( bb.left < 5 )
tip.style.left = (left - bb.left) + 5 + 'px';
else if ( right > document.body.clientWidth - 5 )
tip.style.left = (left - (5 + right - document.body.clientWidth)) + 'px';
});
return tooltip;
}
FFZ.prototype._install_following_tooltips = function() {
var f = this,
data = {
html: true,
className: function() { return WIDE_TIP(f, this); },
title: function() { return f._build_following_tooltip(this); }
};
// Small
var small_following = jQuery('#small_nav ul.game_filters li[data-name="following"] a');
if ( small_following && small_following.length ) {
var data = small_following.data('tipsy');
if ( data && data.options ) {
data.options.gravity = function() { return this.parentElement.getAttribute('data-name') === 'following' ? 'nw': 'w'; };
data.options.html = true;
data.options.className = 'ffz-wide-tip';
var td = small_following.data('tipsy');
if ( td && td.options ) {
td.options = _.extend(td.options, data);
td.options.gravity = function() { return FOLLOW_GRAVITY(f, this); };
} else
small_following.tipsy({html: true, className: 'ffz-wide-tip', gravity: 'nw'});
small_following.attr('title', tooltip);
small_following.tipsy(_.extend({gravity: function() { return FOLLOW_GRAVITY(f, this); }}, data));
}
// Large
var large_following = jQuery('#large_nav #nav_personal li[data-name="following"] a');
if ( large_following && large_following.length ) {
var data = large_following.data('tipsy');
if ( data && data.options ) {
data.options.html = true;
data.options.className = 'ffz-wide-tip';
} else
large_following.tipsy({html:true, className: 'ffz-wide-tip'});
large_following.attr('title', tooltip);
var td = large_following.data('tipsy');
if ( td && td.options )
td.options = _.extend(td.options, data);
else
large_following.tipsy(data);
}
// Heading
var head_following = jQuery('#header_actions #header_following');
if ( head_following && head_following.length ) {
var data = head_following.data('tipsy');
if ( data && data.options ) {
data.options.html = true;
data.options.className = 'ffz-wide-tip';
} else
head_following.tipsy({html: true, className: 'ffz-wide-tip'});
head_following.attr('title', tooltip);
var td = head_following.data('tipsy');
if ( td && td.options )
td.options = _.extend(td.options, data);
else
head_following.tipsy(data);
}
}
FFZ.prototype._draw_following_channels = function(streams, total) {
this._tooltip_streams = streams;
this._tooltip_total = total;
}
FFZ.prototype._draw_following_count = function(count) {
// Small
var small_following = document.querySelector('#small_nav ul.game_filters li[data-name="following"] a');

View file

@ -308,7 +308,7 @@ FFZ.prototype.build_ui_popup = function(view) {
el = document.createElement('li'),
link = document.createElement('a');
el.className = 'item';
el.className = 'item' + (page.sub_menu ? ' has-sub-menu' : '');
el.id = "ffz-menu-page-" + key;
link.title = page.name;
link.innerHTML = page.icon;

View file

@ -10,6 +10,25 @@ var FFZ = window.FrankerFaceZ,
// Initialization
// -------------------
FFZ.basic_settings.replace_twitch_menu = {
type: "boolean",
category: "Chat",
name: "Unified Emoticons Menu",
help: "Completely replace the default Twitch emoticon menu and display global emoticons in the My Emoticons menu.",
get: function() {
return this.settings.replace_twitch_menu && this.settings.global_emotes_in_menu && this.settings.emoji_in_menu;
},
set: function(val) {
this.settings.set('replace_twitch_menu', val);
this.settings.set('global_emotes_in_menu', val);
this.settings.set('emoji_in_menu', val);
}
};
FFZ.settings_info.replace_twitch_menu = {
type: "boolean",
value: false,

View file

@ -476,6 +476,7 @@ body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + s
margin: 11px 13px;
}
.ffz-ui-sub-menu-page,
.ffz-ui-menu-page { overflow-y: auto; }
.ffz-ui-menu-page[data-page="about"],
@ -540,10 +541,15 @@ body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + s
line-height: 25px;
}
.ffz-ui-menu-page input,
.ffz-ui-menu-page select {
margin: 0 10px 5px;
}
.ffz-ui-menu-page input[type="file"] {
width: auto;
}
#ffz-chat-menu { pointer-events: none; }
.ffz-ui-popup ul.menu {
@ -570,15 +576,42 @@ body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + s
background-color: #282828;
}
.ffz-ui-popup ul.sub-menu li.title,
.ffz-ui-menu-page .heading .right,
.ffz-ui-popup ul.menu li.item {
float: right;
}
.ffz-ui-popup ul.sub-menu li.item,
.ffz-ui-popup ul.menu li.title {
float: left;
}
.ffz-ui-popup ul.sub-menu { background-color: #dfdfdf; }
.app-main.theatre .ffz-ui-popup ul.sub-menu,
.chat-container.dark .ffz-ui-popup ul.sub-menu,
.chat-container.force-dark .ffz-ui-popup ul.sub-menu,
.ember-chat-container.dark .ffz-ui-popup ul.sub-menu,
.ember-chat-container.force-dark .ffz-ui-popup ul.sub-menu,
.ffz-ui-popup.dark ul.sub-menu {
background-color: #181818;
}
.ffz-ui-popup ul.sub-menu a {
text-decoration: none;
color: #333;
}
.app-main.theatre .ffz-ui-popup ul.sub-menu a,
.chat-container.dark .ffz-ui-popup ul.sub-menu a,
.chat-container.force-dark .ffz-ui-popup ul.sub-menu a,
.ember-chat-container.dark .ffz-ui-popup ul.sub-menu a,
.ember-chat-container.force-dark .ffz-ui-popup ul.sub-menu a,
.ffz-ui-popup.dark ul.sub-menu a {
color: #d3d3d3 !important;
}
span.ffz-handle {
display: inline-block;
position: relative;
@ -668,6 +701,11 @@ span.ffz-handle:after { left: 8px }
border-top: 1px solid transparent;
}
.ffz-ui-popup ul.sub-menu a {
border-left: none;
border-right: 1px solid rgba(0,0,0,0.2);
}
.ffz-ui-popup ul.menu li.active {
background-color: #fff;
}
@ -676,6 +714,15 @@ span.ffz-handle:after { left: 8px }
border-top-color: #fff;
}
.ffz-ui-popup ul.menu li.active.has-sub-menu {
background-color: #dfdfdf;
}
.ffz-ui-popup ul.menu li.active.has-sub-menu a {
border-top-color: #dfdfdf;
}
.chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active,
.chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active,
.ember-chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active,
@ -694,6 +741,24 @@ span.ffz-handle:after { left: 8px }
border-top-color: rgb(16,16,16);
}
.chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu,
.chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu,
.ember-chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu,
.ember-chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu,
.app-main.theatre .chat-container .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu,
.ffz-ui-popup.dark ul.menu li.active.has-sub-menu {
background-color: #181818;
}
.chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu a,
.chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu a,
.ember-chat-container.dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu a,
.ember-chat-container.force-dark .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu a,
.app-main.theatre .chat-container .chat-interface .ffz-ui-popup ul.menu li.active.has-sub-menu a,
.ffz-ui-popup.dark ul.menu li.active.has-sub-menu a {
border-top-color: #181818;
}
.chat-container.dark .chat-interface .ffz-ui-popup a,
.chat-container.force-dark .chat-interface .ffz-ui-popup a,
.ember-chat-container.dark .chat-interface .ffz-ui-popup a,
@ -735,6 +800,7 @@ span.ffz-handle:after { left: 8px }
.chat-history::-webkit-scrollbar,
#ffz-race-popup .table::-webkit-scrollbar,
.emoticon-selector-box .all-emotes::-webkit-scrollbar,
.ffz-ui-sub-menu-page::-webkit-scrollbar,
.ffz-ui-menu-page::-webkit-scrollbar {
width: 6px;
}
@ -742,6 +808,7 @@ span.ffz-handle:after { left: 8px }
.chat-history::-webkit-scrollbar-thumb,
#ffz-race-popup .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 {
border-radius: 7px;
background: rgba(0,0,0,0.7);
@ -755,7 +822,10 @@ span.ffz-handle:after { left: 8px }
.app-main.theatre .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb,
.ember-chat-container.dark .ffz-ui-menu-page::-webkit-scrollbar-thumb,
.chat-container.dark .ffz-ui-menu-page::-webkit-scrollbar-thumb,
.app-main.theatre .ffz-ui-menu-page::-webkit-scrollbar-thumb {
.app-main.theatre .ffz-ui-menu-page::-webkit-scrollbar-thumb,
.ember-chat-container.dark .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb,
.chat-container.dark .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb,
.app-main.theatre .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.6);
box-shadow: 0 0 1px 1px rgba(0,0,0,0.25);
}
@ -1080,21 +1150,35 @@ body:not(.ffz-chat-purge-icon) .ember-chat .mod-icons .purge { display: none; }
/* Emoticon Tooltips */
.ffz-wide-tip .tipsy-inner {
min-width: 300px;
max-width: 600px;
text-align: left;
position: relative;
}
.ffz-wide-tip span.viewers {
.ffz-wide-tip span.stat {
float: right;
margin-left: 5px;
}
.ffz-wide-tip span.viewers svg {
.ffz-wide-tip b { margin-right: 20px; }
.ffz-wide-tip span.stat svg {
float: left;
margin: 1px;
}
.ffz-wide-tip svg path { fill: #fff; }
.ffz-wide-tip span.playing { opacity: 0.7; }
.ffz-wide-tip span.playing {
opacity: 0.7;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: block;
position: relative;
left: 0;
right: 0;
}
.tipsy .tipsy-inner {
white-space: pre-wrap;
@ -1102,10 +1186,12 @@ body:not(.ffz-chat-purge-icon) .ember-chat .mod-icons .purge { display: none; }
/* Menu Page Loader */
.ffz-ui-sub-menu-page:empty,
.ffz-ui-menu-page:empty {
overflow: hidden;
}
.ffz-ui-sub-menu-page:empty::after,
.ffz-ui-menu-page:empty::after {
content: " ";
display: block;