1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-29 15:55: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

1082
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

@ -292,7 +292,7 @@ FFZ.prototype._modify_cview = function(view) {
ffzInit: function() { ffzInit: function() {
f._chatv = this; f._chatv = this;
this.$('.textarea-contain').append(f.build_ui_link(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 ) if ( !f.has_bttv && f.settings.group_tabs )
this.ffzEnableTabs(); this.ffzEnableTabs();

View file

@ -55,7 +55,8 @@ FFZ.settings_info.replace_bad_emotes = {
name: "Fix Low Quality Twitch Global Emoticons", 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." 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 = { FFZ.settings_info.parse_emoji = {
type: "boolean", type: "boolean",
@ -211,7 +212,6 @@ FFZ.settings_info.clickable_emoticons = {
}; };
FFZ.settings_info.link_info = { FFZ.settings_info.link_info = {
type: "boolean", type: "boolean",
value: true, value: true,
@ -333,7 +333,7 @@ FFZ.settings_info.high_contrast_chat = {
'112': "Background + Bold", '112': "Background + Bold",
'111': 'All' '111': 'All'
}, },
value: '000', value: '222',
category: "Chat Appearance", category: "Chat Appearance",
no_bttv: true, no_bttv: true,
@ -343,7 +343,7 @@ FFZ.settings_info.high_contrast_chat = {
process_value: function(val) { process_value: function(val) {
if ( val === false ) if ( val === false )
return '000'; return '222';
else if ( val === true ) else if ( val === true )
return '111'; return '111';
return val; return val;

View file

@ -50,6 +50,43 @@ try {
// Settings // 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 = { FFZ.settings_info.chat_hover_pause = {
type: "boolean", type: "boolean",
value: false, value: false,
@ -58,7 +95,7 @@ FFZ.settings_info.chat_hover_pause = {
category: "Chat Moderation", category: "Chat Moderation",
name: "Pause Chat Scrolling on Mouse Hover", 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) { on_update: function(val) {
if ( ! this._roomv ) if ( ! this._roomv )
@ -676,22 +713,21 @@ FFZ.prototype.setup_mod_card = function() {
FFZ.prototype._update_alias = function(user) { FFZ.prototype._update_alias = function(user) {
var alias = this.aliases && this.aliases[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'), el = this._roomv && this._roomv.get('element'),
lines = el && el.querySelectorAll('.chat-line[data-sender="' + user + '"]'); lines = el && el.querySelectorAll('.chat-line[data-sender="' + user + '"]');
if ( ! lines ) if ( ! lines )
return; return;
if ( ! display_name )
display_name = FFZ.get_capitalization(user);
for(var i=0, l = lines.length; i < l; i++) { for(var i=0, l = lines.length; i < l; i++) {
var line = lines[i], var line = lines[i],
el_from = line.querySelector('.from'); el_from = line.querySelector('.from');
el_from.classList.toggle('ffz-alias', alias); el_from.classList.toggle('ffz-alias', alias);
el_from.textContent = display_name; el_from.textContent = display_name;
el_from.title = alias ? cap_name : '';
} }
} }

View file

@ -76,6 +76,7 @@ FFZ.prototype.setup_bttv = function(delay) {
if ( this.settings.following_count ) { if ( this.settings.following_count ) {
this._schedule_following_count(); this._schedule_following_count();
this._draw_following_count(); this._draw_following_count();
this._draw_following_channels();
} }
// Remove Sub Count // Remove Sub Count

View file

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

View file

@ -1,5 +1,6 @@
var FFZ = window.FrankerFaceZ, var FFZ = window.FrankerFaceZ,
constants = require("./constants"); constants = require("./constants"),
FileSaver = require("./FileSaver");
make_ls = function(key) { make_ls = function(key) {
@ -14,6 +15,25 @@ var FFZ = window.FrankerFaceZ,
option_setting = function(select, key) { option_setting = function(select, key) {
this.settings.set(key, JSON.parse(select.options[select.selectedIndex].value)); 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 // Initializer
// -------------------- // --------------------
FFZ.settings_info = {}; FFZ.settings_info = {
advanced_settings: { value: false, visible: false }
};
FFZ.basic_settings = {};
FFZ.prototype.load_settings = function() { FFZ.prototype.load_settings = function() {
this.log("Loading settings."); this.log("Loading settings.");
@ -61,12 +85,421 @@ 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 // Menu Page
// -------------------- // --------------------
FFZ.menu_pages.settings = { FFZ.menu_pages.settings = {
render: function(view, container) { render: function(view, container) {
// Bottom Bar
var menu = document.createElement('ul'),
page = document.createElement('div'),
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;
}
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_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;
return 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);
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_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 {
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);
}
},
render_advanced: function(view, container) {
var settings = {}, var settings = {},
categories = [], categories = [],
is_android = navigator.userAgent.indexOf('Android') !== -1; is_android = navigator.userAgent.indexOf('Android') !== -1;
@ -254,7 +687,8 @@ FFZ.menu_pages.settings = {
name: "Settings", name: "Settings",
icon: constants.GEAR, icon: constants.GEAR,
sort_order: 99999, sort_order: 99999,
wide: true wide: true,
sub_menu: true
}; };

View file

@ -6,6 +6,107 @@ var FFZ = window.FrankerFaceZ,
// Settings // 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 = { FFZ.settings_info.twitch_chat_dark = {
type: "boolean", type: "boolean",
value: false, value: false,

View file

@ -1,19 +1,25 @@
var FFZ = window.FrankerFaceZ, var FFZ = window.FrankerFaceZ,
utils = require('../utils'), 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 = { FFZ.settings_info.following_count = {
type: "boolean", type: "boolean",
value: true, value: true,
no_bttv: true,
no_mobile: true, no_mobile: true,
category: "Appearance", category: "Appearance",
name: "Sidebar Following Count", name: "Sidebar Following Data",
help: "Display the number of live channels you're following on the sidebar.", help: "Display the number of live channels you're following on the sidebar, and list the channels in a tooltip.",
on_update: function(val) { on_update: function(val) {
this._schedule_following_count(); this._schedule_following_count();
@ -21,10 +27,14 @@ FFZ.settings_info.following_count = {
var Stream = window.App && App.__container__.resolve('model:stream'), var Stream = window.App && App.__container__.resolve('model:stream'),
Live = Stream && Stream.find("live"); Live = Stream && Stream.find("live");
if ( Live ) if ( Live ) {
this._draw_following_count(Live.get('total') || 0); var total = Live.get('total') || 0;
else this._draw_following_count(total);
this._draw_following_channels(Live.get('content'), total);;
} else {
this._update_following_count(); 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 ) if ( this.settings.following_count )
this._schedule_following_count(); this._schedule_following_count();
// Tooltips~!
this._install_following_tooltips();
// If we don't have Ember, no point in trying this stuff. // If we don't have Ember, no point in trying this stuff.
if ( ! has_ember ) if ( ! has_ember )
return this._update_following_count(); return this._update_following_count();
@ -68,7 +81,7 @@ FFZ.prototype.setup_following_count = function(has_ember) {
FFZ.prototype._schedule_following_count = function() { FFZ.prototype._schedule_following_count = function() {
if ( this.has_bttv || ! this.settings.following_count ) { if ( ! this.settings.following_count ) {
if ( this._following_count_timer ) { if ( this._following_count_timer ) {
clearTimeout(this._following_count_timer); clearTimeout(this._following_count_timer);
this._following_count_timer = undefined; this._following_count_timer = undefined;
@ -110,9 +123,21 @@ FFZ.prototype._update_following_count = function() {
} }
FFZ.prototype._draw_following_channels = function(streams, total) { FFZ.prototype._build_following_tooltip = function(el) {
// First, build the data. if ( el.id !== 'header_following' && el.parentElement.getAttribute('data-name') !== 'following' )
var tooltip = '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 ) { if ( streams && streams.length ) {
var c = 0; var c = 0;
@ -122,61 +147,93 @@ FFZ.prototype._draw_following_channels = function(streams, total) {
continue; continue;
c += 1; c += 1;
if ( c > 5 ) { if ( c > max_lines ) {
var ttl = total || streams.length; tooltip += '<hr><span>And ' + utils.number_commas(total - max_lines) + ' more...</span>';
tooltip += '<hr><span>And ' + utils.number_commas(ttl - 5) + ' more...</span>';
break; 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 // Small
var small_following = jQuery('#small_nav ul.game_filters li[data-name="following"] a'); var small_following = jQuery('#small_nav ul.game_filters li[data-name="following"] a');
if ( small_following && small_following.length ) { if ( small_following && small_following.length ) {
var data = small_following.data('tipsy'); var td = small_following.data('tipsy');
if ( data && data.options ) { if ( td && td.options ) {
data.options.gravity = function() { return this.parentElement.getAttribute('data-name') === 'following' ? 'nw': 'w'; }; td.options = _.extend(td.options, data);
data.options.html = true; td.options.gravity = function() { return FOLLOW_GRAVITY(f, this); };
data.options.className = 'ffz-wide-tip';
} else } else
small_following.tipsy({html: true, className: 'ffz-wide-tip', gravity: 'nw'}); small_following.tipsy(_.extend({gravity: function() { return FOLLOW_GRAVITY(f, this); }}, data));
small_following.attr('title', tooltip);
} }
// Large // Large
var large_following = jQuery('#large_nav #nav_personal li[data-name="following"] a'); var large_following = jQuery('#large_nav #nav_personal li[data-name="following"] a');
if ( large_following && large_following.length ) { if ( large_following && large_following.length ) {
var data = large_following.data('tipsy'); var td = large_following.data('tipsy');
if ( data && data.options ) { if ( td && td.options )
data.options.html = true; td.options = _.extend(td.options, data);
data.options.className = 'ffz-wide-tip'; else
} else large_following.tipsy(data);
large_following.tipsy({html:true, className: 'ffz-wide-tip'});
large_following.attr('title', tooltip);
} }
// Heading // Heading
var head_following = jQuery('#header_actions #header_following'); var head_following = jQuery('#header_actions #header_following');
if ( head_following && head_following.length ) { if ( head_following && head_following.length ) {
var data = head_following.data('tipsy'); var td = head_following.data('tipsy');
if ( data && data.options ) { if ( td && td.options )
data.options.html = true; td.options = _.extend(td.options, data);
data.options.className = 'ffz-wide-tip'; else
} else head_following.tipsy(data);
head_following.tipsy({html: true, className: 'ffz-wide-tip'});
head_following.attr('title', tooltip);
} }
} }
FFZ.prototype._draw_following_channels = function(streams, total) {
this._tooltip_streams = streams;
this._tooltip_total = total;
}
FFZ.prototype._draw_following_count = function(count) { FFZ.prototype._draw_following_count = function(count) {
// Small // Small
var small_following = document.querySelector('#small_nav ul.game_filters li[data-name="following"] a'); 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'), el = document.createElement('li'),
link = document.createElement('a'); link = document.createElement('a');
el.className = 'item'; el.className = 'item' + (page.sub_menu ? ' has-sub-menu' : '');
el.id = "ffz-menu-page-" + key; el.id = "ffz-menu-page-" + key;
link.title = page.name; link.title = page.name;
link.innerHTML = page.icon; link.innerHTML = page.icon;

View file

@ -10,6 +10,25 @@ var FFZ = window.FrankerFaceZ,
// Initialization // 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 = { FFZ.settings_info.replace_twitch_menu = {
type: "boolean", type: "boolean",
value: false, value: false,

View file

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