diff --git a/dark.css b/dark.css index 23f049d9..ecaa814b 100644 --- a/dark.css +++ b/dark.css @@ -46,7 +46,7 @@ /* hover icon for chats menu */ .ffz-dark .ember-chat .chat-menu-button-container:hover svg path{ fill:#fff!important; -} +} /* dropdown arrows */ @@ -161,6 +161,7 @@ .ffz-dark .ember-chat .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, .ffz-dark .card, .ffz-dark #flyout .content, +.ffz-dark .whatisthis, .ffz-dark .ui-menu, .ffz-dark .dropmenu, .ffz-dark .top-dropdown, @@ -782,10 +783,17 @@ } .ffz-dark .whatisthis { - background-color: #333; box-shadow: 0 0 0 1px rgba(255,255,255,0.2); } +.ffz-dark .whatisthis:before { + border-top-color: #101010; +} + +.ffz-dark .whatisthis:after { + border-top-color: #32323e; +} + .ffz-dark #chart_container svg rect[fill="#FFFFFF"] { fill: rgb(32,32,32) !important; } @@ -843,6 +851,7 @@ background-color: transparent; } +.ffz-dark .whatisthis .actions .divider, .ffz-dark .dash-chat-column { border-color: rgba(255,255,255,0.2); } @@ -894,4 +903,20 @@ .ffz-dark li.video.vods img.video_type { filter: invert(100%); -webkit-filter: invert(100%); +} + +/* Playlist */ + +.ffz-dark .ember-chat .chat-header { + box-shadow: inset 0 -1px 0 0 rgba(255,255,255,0.2); +} + +.ffz-dark .playlist-controller, +.ffz-dark .playlist-item { + border-color: rgba(255,255,255,0.2); +} + +.ffz-dark .playlist-container:not(.playlist-enabled) .playlist-item:hover, +.ffz-dark .playlist-container:not(.playlist-enabled) .ui-sortable-helper { + background-color: rgba(255,255,255,0.2); } \ No newline at end of file diff --git a/image-proxy.html b/image-proxy.html deleted file mode 100644 index a0af78c8..00000000 --- a/image-proxy.html +++ /dev/null @@ -1,38 +0,0 @@ - - - \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index f418153c..8277fb01 100644 --- a/src/badges.js +++ b/src/badges.js @@ -431,7 +431,7 @@ FFZ.prototype._legacy_add_donors = function() { } FFZ.prototype._legacy_load_bots = function(tries) { - jQuery.ajax(constants.SERVER + "script/bots.txt", {cache: false, context: this}) + jQuery.ajax(constants.SERVER + "script/bots.txt", {context: this}) .done(function(data) { this._legacy_parse_badges(data, 0, 2, "Bot (By: {})"); @@ -446,7 +446,7 @@ FFZ.prototype._legacy_load_bots = function(tries) { } FFZ.prototype._legacy_load_donors = function(tries) { - jQuery.ajax(constants.SERVER + "script/donors.txt", {cache: false, context: this}) + jQuery.ajax(constants.SERVER + "script/donors.txt", {context: this}) .done(function(data) { this._legacy_parse_badges(data, 1, 1); diff --git a/src/ember/channel.js b/src/ember/channel.js index f6197b24..ea154da4 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -361,9 +361,9 @@ FFZ.prototype._modify_cindex = function(view) { controller.set('ffz_host_updating', true); if ( now_hosting === target ) - room.send("/unhost"); + room.send("/unhost", true); else - room.send("/host " + target); + room.send("/host " + target, true); }, @@ -480,8 +480,25 @@ FFZ.prototype._modify_cindex = function(view) { container.appendChild(stat_el); } - stat_el.title = 'Stream Latency\nFPS: ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'; - el.textContent = stats.hlsLatencyBroadcaster + 's'; + var delay = parseFloat(stats.hlsLatencyBroadcaster); + + if ( delay > 180 ) { + delay = Math.floor(delay); + stat_el.setAttribute('original-title', 'Video Information\nBroadcast ' + utils.time_to_string(delay, true) + ' Ago\n\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps') + el.textContent = utils.time_to_string(Math.floor(delay), true, delay > 172800) + ' old'; + } else { + stat_el.setAttribute('original-title', 'Stream Latency\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'); + + delay = stats.hlsLatencyBroadcaster; + var pos = delay.lastIndexOf('.'); + + if ( pos === -1 ) + delay = delay + '.00'; + else if ( delay.length - pos < 3 ) + delay = delay + '0'; + + el.textContent = delay + 's'; + } } } @@ -516,8 +533,25 @@ FFZ.prototype._modify_cindex = function(view) { container.appendChild(stat_el); } - stat_el.title = 'Stream Latency\nFPS: ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'; - el.textContent = stats.hlsLatencyBroadcaster + 's'; + var delay = parseFloat(stats.hlsLatencyBroadcaster); + + if ( delay > 180 ) { + delay = Math.floor(delay); + stat_el.setAttribute('original-title', 'Video Information\nBroadcast ' + utils.time_to_string(delay, true) + ' Ago\n\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps') + el.textContent = utils.time_to_string(Math.floor(delay), true, delay > 172800) + ' old'; + } else { + stat_el.setAttribute('original-title', 'Stream Latency\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'); + + delay = stats.hlsLatencyBroadcaster; + var pos = delay.lastIndexOf('.'); + + if ( pos === -1 ) + delay = delay + '.00'; + else if ( delay.length - pos < 3 ) + delay = delay + '0'; + + el.textContent = delay + 's'; + } } } }, @@ -581,7 +615,7 @@ FFZ.prototype._modify_cindex = function(view) { jQuery(stat).tipsy({html: true}); } - el.innerHTML = utils.time_to_string(uptime); + el.innerHTML = utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1 || f.settings.stream_uptime === 3); }, ffzTeardown: function() { @@ -698,10 +732,27 @@ FFZ.settings_info.stream_host_button = { FFZ.settings_info.stream_uptime = { - type: "boolean", - value: false, - no_mobile: true, + type: "select", + options: { + 0: "Disabled", + 1: "Enabled", + 2: "Enabled (with Seconds)", + 3: "Enabled (Channel Only)", + 4: "Enabled (Channel Only with Seconds)" + }, + value: 1, + process_value: function(val) { + if ( val === false ) + return 0; + if ( val === true ) + return 2; + if ( typeof val === "string" ) + return parseInt(val || "0") || 0; + return val; + }, + + no_mobile: true, category: "Channel Metadata", name: "Stream Uptime", help: 'Display the stream uptime under a channel by the viewer count.', @@ -725,4 +776,31 @@ FFZ.settings_info.stream_title = { if ( this._cindex ) this._cindex.ffzFixTitle(); } - }; \ No newline at end of file + }; + + +FFZ.basic_settings.channel_info = { + type: "select", + options: { + 0: "Disabled", + 1: "Enabled", + 2: "Enabled (with Seconds)", + 3: "Enabled (Channel Only)", + 4: "Enabled (Channel Only with Seconds)" + }, + + category: "General", + name: "Stream Uptime", + help: "Display the current stream's uptime under the player.", + + get: function() { + return this.settings.stream_uptime; + }, + + set: function(val) { + if ( typeof val === 'string' ) + val = parseInt(val || "0"); + + this.settings.set('stream_uptime', val); + } +} \ No newline at end of file diff --git a/src/ember/chat-input.js b/src/ember/chat-input.js index 3bd5e28f..4eddef44 100644 --- a/src/ember/chat-input.js +++ b/src/ember/chat-input.js @@ -67,7 +67,7 @@ FFZ.settings_info.input_mru = { category: "Chat Input", no_bttv: true, - + name: "Chat Input History", help: "Use the Up and Down arrows in chat to select previously sent chat messages." }; @@ -79,7 +79,7 @@ FFZ.settings_info.input_emoji = { category: "Chat Input", //visible: false, no_bttv: true, - + name: "Enter Emoji By Name", help: "Replace emoji that you type by name with the character. :+1: becomes 👍." }; @@ -113,10 +113,10 @@ FFZ.prototype.setup_chat_input = function() { FFZ.prototype._modify_chat_input = function(component) { var f = this; - + component.reopen({ ffz_mru_index: -1, - + didInsertElement: function() { this._super(); @@ -124,7 +124,7 @@ FFZ.prototype._modify_chat_input = function(component) { this.ffzInit(); } catch(err) { f.error("ChatInput didInsertElement: " + err); } }, - + willClearRender: function() { try { this.ffzTeardown(); @@ -141,7 +141,7 @@ FFZ.prototype._modify_chat_input = function(component) { // Redo our key bindings. var t = this.$("textarea"); - + t.off("keydown"); t.on("keydown", this._ffzKeyDown.bind(this)); @@ -153,10 +153,10 @@ FFZ.prototype._modify_chat_input = function(component) { /*var suggestions = this._parentView.get('context.model.chatSuggestions'); this.set('ffz_chatters', suggestions);*/ }, - + ffzTeardown: function() { if ( f._inputv === this ) - f._inputv = undefined; + f._inputv = undefined; this.ffzResizeInput(); @@ -167,7 +167,7 @@ FFZ.prototype._modify_chat_input = function(component) { // Reset normal key bindings. var t = this.$("textarea"); - + t.attr('rows', undefined); t.off("keydown"); @@ -175,42 +175,42 @@ FFZ.prototype._modify_chat_input = function(component) { }, // Input Control - + ffzOnInput: function() { - if ( ! f._chat_style || ! f.settings.minimal_chat || is_android ) + if ( ! f._chat_style || f.settings.minimal_chat < 2 || is_android ) return; - + var now = Date.now(), since = now - (this._ffz_last_resize || 0); - + if ( since > 500 ) this.ffzResizeInput(); }.observes('textareaValue'), - + ffzResizeInput: function() { this._ffz_last_resize = Date.now(); - + var el = this.get('element'), t = el && el.querySelector('textarea'); - - if ( ! t || ! f._chat_style || ! f.settings.minimal_chat ) + + if ( ! t || ! f._chat_style || f.settings.minimal_chat < 2 ) return; - + // Unfortunately, we need to change this with CSS. - this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat .ember-chat .chat-interface .textarea-contain textarea { height: auto !important; }'; - var height = Math.max(32, Math.min(128, t.scrollHeight)); - this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat .ember-chat .chat-interface .textarea-contain textarea { height: ' + height + 'px !important; }'; - + this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textarea { height: auto !important; }'; + var height = Math.max(32, Math.min(128, t.scrollHeight)); + this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textarea { height: ' + height + 'px !important; }'; + if ( height !== this._ffz_last_height ) { - utils.update_css(f._chat_style, "input_height", 'body.ffz-minimal-chat .ember-chat .chat-interface { height: ' + height + 'px !important; }' + - 'body.ffz-minimal-chat .ember-chat .chat-messages, body.ffz-minimal-chat .ember-chat .chat-interface .emoticon-selector { bottom: ' + height + 'px !important; }'); + utils.update_css(f._chat_style, "input_height", 'body.ffz-minimal-chat-input .ember-chat .chat-interface { height: ' + height + 'px !important; }' + + 'body.ffz-minimal-chat-input .ember-chat .chat-messages, body.ffz-minimal-chat-input .ember-chat .chat-interface .emoticon-selector { bottom: ' + height + 'px !important; }'); f._roomv && f._roomv.get('stuckToBottom') && f._roomv._scrollToBottom(); } this._ffz_last_height = height; }, - + _ffzKeyDown: function(event) { var e = event || window.event, key = e.charCode || e.keyCode; @@ -227,7 +227,7 @@ FFZ.prototype._modify_chat_input = function(component) { else return this._onKeyDown(event); break; - + case KEYCODES.SPACE: if ( f.settings.input_quick_reply && selection_start(this.get("chatTextArea")) === 2 && this.get("textareaValue").substring(0,2) === "/r" ) { var t = this; @@ -237,7 +237,7 @@ FFZ.prototype._modify_chat_input = function(component) { var text = "/w " + wt + t.get("textareaValue").substr(2); t.set("_currentWhisperTarget", 0); t.set("textareaValue", text); - + Ember.run.next(function() { move_selection(t.get('chatTextArea'), 4 + wt.length); }); @@ -246,7 +246,7 @@ FFZ.prototype._modify_chat_input = function(component) { } else return this._onKeyDown(event); break; - + case KEYCODES.COLON: case KEYCODES.FAKE_COLON: if ( f.settings.input_emoji && (e.shiftKey || e.shiftLeft) ) { @@ -274,22 +274,22 @@ FFZ.prototype._modify_chat_input = function(component) { return; } return this._onKeyDown(event); - + case KEYCODES.ENTER: if ( ! e.shiftKey && ! e.shiftLeft ) this.set('ffz_mru_index', -1); - + default: return this._onKeyDown(event); } }, - + ffzCycleMRU: function(key, start_ind) { // We don't want to do this if the keys were just moving the cursor around. var cur_pos = selection_start(this.get("chatTextArea")); if ( start_ind !== cur_pos ) return; - + var ind = this.get('ffz_mru_index'), mru = this._parentView.get('context.model.mru_list') || []; @@ -369,8 +369,8 @@ FFZ.prototype._modify_chat_input = function(component) { output.push({id:emote.name}); } } - - return output; + + return output; }.property(), ffz_chatters: [], @@ -379,17 +379,17 @@ FFZ.prototype._modify_chat_input = function(component) { if ( arguments.length > 1 ) { this.set('ffz_chatters', value); } - + var output = []; - + // Chatters output = output.concat(this.get('ffz_chatters')); - + // Emoticons if ( this.get('isSuggestionsTriggeredWithTab') ) { output = output.concat(this.get('ffz_emoticons')); } - + return output; }.property("ffz_emoticons", "ffz_chatters", "isSuggestionsTriggeredWithTab")*/ }); diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 48a3dcfd..f6d6fb02 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -40,16 +40,35 @@ FFZ.basic_settings.delayed_chat = { FFZ.settings_info.minimal_chat = { - type: "boolean", - value: false, + type: "select", + options: { + 0: "Disabled", + 1: "No Heading", + 2: "Minimalistic Input", + 3: "All" + }, + + value: 0, category: "Chat Appearance", name: "Minimalistic Chat", help: "Hide all of the chat user interface, only showing messages and an input box.", + process_value: function(val) { + if ( val === false ) + return 0; + else if ( val === true ) + return 3; + else if ( typeof val === "string" ) + return parseInt(val) || 0; + return val; + }, + on_update: function(val) { - document.body.classList.toggle("ffz-minimal-chat", val); + document.body.classList.toggle("ffz-minimal-chat-head", val === 1 || val === 3); + document.body.classList.toggle("ffz-minimal-chat-input", val > 1); + if ( this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { var f = this; setTimeout(function() { @@ -58,11 +77,11 @@ FFZ.settings_info.minimal_chat = { },0); } - if ( this._chatv && this._chatv.get('controller.showList') ) + if ( (val === 1 || val === 3) && 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 ( ! (val > 1) && this._chat_style ) { if ( this._inputv ) { if ( this._inputv._ffz_minimal_style ) this._inputv._ffz_minimal_style.innerHTML = ''; @@ -73,7 +92,7 @@ FFZ.settings_info.minimal_chat = { utils.update_css(this._chat_style, "input_height", ''); this._roomv && this._roomv.get('stuckToBottom') && this._roomv._scrollToBottom(); - } else if ( this._inputv ) + } else if ( val > 1 && this._inputv ) this._inputv.ffzResizeInput(); } }; @@ -247,7 +266,8 @@ FFZ.settings_info.visible_rooms = { // -------------------- FFZ.prototype.setup_chatview = function() { - document.body.classList.toggle("ffz-minimal-chat", this.settings.minimal_chat); + document.body.classList.toggle("ffz-minimal-chat-head", this.settings.minimal_chat === 1 || this.settings.minimal_chat === 3); + document.body.classList.toggle("ffz-minimal-chat-input", this.settings.minimal_chat === 2 || this.settings.minimal_chat === 3); this.log("Hooking the Ember Chat controller."); diff --git a/src/ember/directory.js b/src/ember/directory.js new file mode 100644 index 00000000..31c218e2 --- /dev/null +++ b/src/ember/directory.js @@ -0,0 +1,191 @@ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + constants = require('../constants'); + + +// -------------------- +// Settings +// -------------------- + +FFZ.settings_info.directory_logos = { + type: "boolean", + value: false, + + category: "Appearance", + no_mobile: true, + + name: "Directory Logos", + help: "Display channel logos in the Twitch directory." + }; + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_directory = function() { + this.log("Hooking the Ember Directory View."); + + var ChannelView = App.__container__.resolve('view:channel'); + if ( ChannelView ) + this._modify_directory_live(ChannelView); + + var HostView = App.__container__.resolve('view:host'); + if ( HostView ) + this._modify_directory_host(HostView); + + var VideoView = App.__container__.resolve('view:video'); + if ( VideoView ) + this._modify_directory_video(VideoView); + + // TODO: Process existing views. +} + + +FFZ.prototype._modify_directory_video = function(dir) { + var f = this; + dir.reopen({ + didInsertElement: function() { + this._super(); + + f.log("New Video View", this); + window.v = this; + } + }); + + try { + dir.create().destroy(); + } catch(err) { } +} + + +FFZ.prototype._modify_directory_live = function(dir) { + var f = this; + dir.reopen({ + didInsertElement: function() { + this._super(); + + var el = this.get('element'), + meta = el && el.querySelector('.meta'), + thumb = el && el.querySelector('.thumb'), + cap = thumb && thumb.querySelector('.cap'); + + + if ( f.settings.stream_uptime && f.settings.stream_uptime < 3 && cap ) { + var t_el = this._ffz_uptime = document.createElement('div'); + t_el.className = 'overlay_info length live'; + + jQuery(t_el).tipsy({html: true}); + + cap.appendChild(t_el); + this._ffz_uptime_timer = setInterval(this.ffzUpdateUptime.bind(this), 1000); + this.ffzUpdateUptime(); + } + + if ( f.settings.directory_logos ) { + el.classList.add('ffz-directory-logo'); + + var link = document.createElement('a'), + logo = document.createElement('img'), + t = this, + target = this.get('context.model.channel.name'); + + logo.className = 'profile-photo'; + logo.src = this.get('context.model.channel.logo') || "http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png"; + logo.alt = this.get('context.model.channel.display_name'); + + link.href = '/' + target; + link.addEventListener('click', function(e) { + var Channel = App.__container__.resolve('model:channel'); + if ( ! Channel ) + return; + + e.preventDefault(); + t.get('controller').transitionTo('channel.index', Channel.find({id: target}).load()); + return false; + }); + + link.appendChild(logo); + meta.insertBefore(link, meta.firstChild); + } + }, + + willClearRender: function() { + if ( this._ffz_uptime ) { + this._ffz_uptime.parentElement.removeChild(this._ffz_uptime); + this._ffz_uptime = null; + } + + if ( this._ffz_uptime_timer ) + clearInterval(this._ffz_uptime_timer); + + this._super(); + }, + + + ffzUpdateUptime: function() { + var raw_created = this.get('context.model.created_at'), + up_since = raw_created && utils.parse_date(raw_created), + uptime = up_since && Math.floor((Date.now() - up_since.getTime()) / 1000) || 0; + + if ( uptime > 0 ) { + this._ffz_uptime.innerHTML = constants.CLOCK + utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1); + this._ffz_uptime.setAttribute('original-title', 'Stream Uptime (since ' + up_since.toLocaleString() + ')');; + } else { + this._ffz_uptime.setAttribute('original-title', ''); + this._ffz_uptime.innerHTML = ''; + } + } + }); + + try { + dir.create().destroy(); + } catch(err) { } +} + + +FFZ.prototype._modify_directory_host = function(dir) { + var f = this; + dir.reopen({ + didInsertElement: function() { + this._super(); + + var el = this.get('element'), + meta = el && el.querySelector('.meta'), + thumb = el && el.querySelector('.thumb'), + cap = thumb && thumb.querySelector('.cap'); + + + if ( f.settings.directory_logos ) { + el.classList.add('ffz-directory-logo'); + + var link = document.createElement('a'), + logo = document.createElement('img'), + t = this, + target = this.get('context.model.target.channel.name'); + + logo.className = 'profile-photo'; + logo.src = this.get('context.model.target.channel.logo') || "http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png"; + logo.alt = this.get('context.model.target.channel.display_name'); + + link.href = '/' + target; + link.addEventListener('click', function(e) { + var Channel = App.__container__.resolve('model:channel'); + if ( ! Channel ) + return; + + e.preventDefault(); + t.get('controller').transitionTo('channel.index', Channel.find({id: target}).load()); + return false; + }); + + link.appendChild(logo); + meta.insertBefore(link, meta.firstChild); + } + } + }); + + try { + dir.create().destroy(); + } catch(err) { } +} \ No newline at end of file diff --git a/src/ember/layout.js b/src/ember/layout.js index c5cfc04c..0d4de51b 100644 --- a/src/ember/layout.js +++ b/src/ember/layout.js @@ -12,7 +12,7 @@ FFZ.settings_info.swap_sidebars = { category: "Appearance", no_mobile: true, no_bttv: true, - + name: "Swap Sidebar Positions", help: "Swap the positions of the left and right sidebars, placing chat on the left.", @@ -26,6 +26,26 @@ FFZ.settings_info.swap_sidebars = { }; +FFZ.settings_info.flip_dashboard = { + type: "boolean", + value: false, + + category: "Appearance", + no_mobile: true, + no_bttv: true, + + name: "Swap Dashboard Positions", + help: "Swap the positions of the left and right columns of the dashboard.", + + on_update: function(val) { + if ( this.has_bttv ) + return; + + document.body.classList.toggle("ffz-flip-dashboard", val); + } + }; + + FFZ.settings_info.right_column_width = { type: "button", value: 340, @@ -33,17 +53,17 @@ FFZ.settings_info.right_column_width = { category: "Appearance", no_mobile: true, no_bttv: true, - + name: "Right Sidebar Width", help: "Set the width of the right sidebar for chat.", - + method: function() { var old_val = this.settings.right_column_width || 340, new_val = prompt("Right Sidebar Width\n\nPlease enter a new width for the right sidebar, in pixels. Minimum: 250, Default: 340", old_val); - + if ( new_val === null || new_val === undefined ) return; - + var width = parseInt(new_val); if ( ! width || width === NaN ) width = 340; @@ -54,11 +74,11 @@ FFZ.settings_info.right_column_width = { on_update: function(val) { if ( this.has_bttv ) return; - + var Layout = App.__container__.lookup('controller:layout'); if ( ! Layout ) return; - + Layout.set('rightColumnWidth', val); Ember.propertyDidChange(Layout, 'contentWidth'); } @@ -89,48 +109,64 @@ FFZ.prototype.setup_layout = function() { Layout.reopen({ rightColumnWidth: 340, - + isTooSmallForRightColumn: function() { return this.get("windowWidth") < (1090 - this.get('rightColumnWidth')) }.property("windowWidth", "rightColumnWidth"), - + contentWidth: function() { var left_width = this.get("isLeftColumnClosed") ? 50 : 240, right_width = this.get("isRightColumnClosed") ? 0 : this.get("rightColumnWidth"); return this.get("windowWidth") - left_width - right_width - 60; - + }.property("windowWidth", "isRightColumnClosed", "isLeftColumnClosed", "rightColumnWidth"), - + + playerStyle: function() { + var h = this.get('windowHeight'), + c = this.get('PLAYER_CONTROLS_HEIGHT'), + r = this.get('contentWidth'), + + i = (9 * r / 16) + c, + d = h - 120 - 60, + c = h - 94 - 185, + + l = Math.floor(r), + o = Math.floor(Math.min(i, d)), + s = Math.floor(Math.min(i, c)); + + return ""; + }.property("contentWidth", "windowHeight", "PLAYER_CONTROLS_HEIGHT"), + /*ffzUpdateWidth: _.throttle(function() { var rc = document.querySelector('#right_close'); if ( ! rc ) return; - + var left_width = this.get("isLeftColumnClosed") ? 50 : 240, right_width; - + if ( f.settings.swap_sidebars ) right_width = rc.offsetLeft; // + this.get('rightColumnWidth') - 5; else right_width = document.body.offsetWidth - rc.offsetLeft - left_width - 25; - + if ( right_width < 250 ) { // Close it! - + } this.set('rightColumnWidth', right_width); Ember.propertyDidChange(Layout, 'contentWidth'); }, 200),*/ - + ffzUpdateCss: function() { var width = this.get('rightColumnWidth'); - + f._layout_style.innerHTML = '#main_col.expandRight #right_close { left: none !important; } #right_col { width: ' + width + 'px; } body:not(.ffz-sidebar-swap) #main_col:not(.expandRight) { margin-right: ' + width + 'px; } body.ffz-sidebar-swap #main_col:not(.expandRight) { margin-left: ' + width + 'px; }'; }.observes("rightColumnWidth"), - + ffzFixTabs: function() { if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { setTimeout(function() { diff --git a/src/ember/line.js b/src/ember/line.js index 3c660534..a1955756 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -27,25 +27,6 @@ FFZ.settings_info.room_status = { }; -FFZ.settings_info.line_purge_icon = { - type: "boolean", - value: false, - - no_bttv: true, - category: "Chat Moderation", - - name: "Purge Icon in Mod Icons", - help: "Display a Purge Icon in chat line Mod Icons for quickly purging users.", - - on_update: function(val) { - if ( this.has_bttv ) - return; - - document.body.classList.toggle("ffz-chat-purge-icon", val); - } - }; - - FFZ.settings_info.replace_bad_emotes = { type: "boolean", value: true, @@ -297,18 +278,22 @@ FFZ.settings_info.chat_separators = { options: { 0: "Disabled", 1: "Basic Line (1px solid)", - 2: "3D Line (2px groove)" + 2: "3D Line (2px groove)", + 3: "3D Line (2px groove inset)", + 4: "Wide Line (2px solid)" }, - value: '0', + value: 0, category: "Chat Appearance", no_bttv: true, process_value: function(val) { if ( val === false ) - return '0'; + return 0; else if ( val === true ) - return '1'; + return 1; + else if ( typeof val === "string" ) + return parseInt(val) || 0; return val; }, @@ -316,8 +301,10 @@ FFZ.settings_info.chat_separators = { help: "Display thin lines between chat messages for further visual separation.", on_update: function(val) { - document.body.classList.toggle("ffz-chat-separator", !this.has_bttv && val !== '0'); - document.body.classList.toggle("ffz-chat-separator-3d", !this.has_bttv && val === '2'); + document.body.classList.toggle("ffz-chat-separator", !this.has_bttv && val !== 0); + document.body.classList.toggle("ffz-chat-separator-3d", !this.has_bttv && val === 2); + document.body.classList.toggle("ffz-chat-separator-3d-inset", !this.has_bttv && val === 3); + document.body.classList.toggle("ffz-chat-separator-wide", !this.has_bttv && val === 4); } }; @@ -543,10 +530,11 @@ FFZ.prototype.setup_line = function() { document.body.classList.toggle("ffz-chat-colors-gray", !this.has_bttv && this.settings.fix_color === '-1'); document.body.classList.toggle('ffz-chat-background', !this.has_bttv && this.settings.chat_rows); - document.body.classList.toggle("ffz-chat-separator", !this.has_bttv && this.settings.chat_separators !== '0'); - document.body.classList.toggle("ffz-chat-separator-3d", !this.has_bttv && this.settings.chat_separators === '2'); + document.body.classList.toggle("ffz-chat-separator", !this.has_bttv && this.settings.chat_separators !== 0); + document.body.classList.toggle("ffz-chat-separator-wide", !this.has_bttv && this.settings.chat_separators === 4); + document.body.classList.toggle("ffz-chat-separator-3d", !this.has_bttv && this.settings.chat_separators === 2); + document.body.classList.toggle("ffz-chat-separator-3d-inset", !this.has_bttv && this.settings.chat_separators === 3); document.body.classList.toggle("ffz-chat-padding", !this.has_bttv && this.settings.chat_padding); - document.body.classList.toggle("ffz-chat-purge-icon", !this.has_bttv && this.settings.line_purge_icon); document.body.classList.toggle("ffz-high-contrast-chat-text", !this.has_bttv && this.settings.high_contrast_chat[2] === '1'); document.body.classList.toggle("ffz-high-contrast-chat-bold", !this.has_bttv && this.settings.high_contrast_chat[1] === '1'); @@ -645,16 +633,30 @@ FFZ.prototype._modify_line = function(component) { if ( e.target && e.target.classList.contains('mod-icon') ) { jQuery(e.target).trigger('mouseout'); - if ( e.target.classList.contains('purge') ) { + /*if ( e.target.classList.contains('purge') ) { var i = this.get('msgObject.from'), room_id = this.get('msgObject.room'), room = room_id && f.rooms[room_id] && f.rooms[room_id].room; if ( room ) { - room.send("/timeout " + i + " 1"); + room.send("/timeout " + i + " 1", true); room.clearMessages(i); } return; + }*/ + + if ( e.target.classList.contains('custom') ) { + var room_id = this.get('msgObject.room'), + room = room_id && f.rooms[room_id] && f.rooms[room_id].room, + + cmd = e.target.getAttribute('data-cmd'); + + if ( room ) { + room.send(cmd, true); + if ( e.target.classList.contains('is-timeout') ) + room.clearMessages(this.get('msgObject.from')); + } + return; } } @@ -678,7 +680,7 @@ FFZ.prototype._modify_line = function(component) { return 4; else if ( this.get('isBroadcaster') ) return 3; - else if ( this.get('isGlobalModerator') ) + else if ( this.get('isGlobalMod') ) return 2; else if ( this.get('isModerator') ) return 1; @@ -712,13 +714,34 @@ FFZ.prototype._modify_line = function(component) { if ( ! is_whisper && this_ul < other_ul ) { e.push(''); - if ( deleted ) - e.push('Unban'); - else - e.push('Ban'); + for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) { + var pair = f.settings.mod_buttons[i], + prefix = pair[0], btn = pair[1], + + cmd, tip; + + if ( btn === false ) { + if ( deleted ) + e.push('Unban'); + else + e.push('Ban'); + + } else if ( btn === 600 ) + e.push('Timeout'); + + else { + if ( typeof btn === "string" ) { + cmd = btn.replace(/{user}/g, user); + tip = 'Custom Command\n' + cmd; + } else { + cmd = "/timeout " + user + " " + btn; + tip = "Timeout User (" + utils.duration_string(btn) + ")"; + } + + e.push('' + prefix + ''); + } + } - e.push('Timeout'); - e.push('Purge'); e.push(''); } @@ -730,7 +753,7 @@ FFZ.prototype._modify_line = function(component) { else if ( this.get('isAdmin') ) badges[0] = {klass: 'admin', title: 'Admin'}; else if ( this.get('isGlobalMod') ) - badges[0] = {klass: 'global-moderator', title: 'Global Moderator'}; + badges[0] = {klass: 'global-mod', title: 'Global Moderator'}; else if ( ! is_whisper && this.get('isModerator') ) badges[0] = {klass: 'moderator', title: 'Moderator'}; @@ -881,7 +904,7 @@ FFZ.get_capitalization = function(name, callback) { FFZ.prototype._remove_banned = function(tokens) { var banned_words = this.settings.banned_words, - banned_links = this.settings.filter_bad_shorteners ? ['goo.gl', 'j.mp', 'bit.ly'] : null, + banned_links = this.settings.filter_bad_shorteners ? ['apo.af', 'goo.gl', 'j.mp', 'bit.ly'] : null, has_banned_words = banned_words && banned_words.length; diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index c7a1602a..37cf14e9 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -12,33 +12,7 @@ var FFZ = window.FrankerFaceZ, }, MESSAGE = '', - CHECK = '', - - DURATIONS = {}, - duration_string = function(val) { - if ( val === 1 ) - return 'Purge'; - - if ( DURATIONS[val] ) - return DURATIONS[val]; - - var weeks, days, hours, minutes, seconds; - - weeks = Math.floor(val / 604800); - seconds = val % 604800; - - days = Math.floor(seconds / 86400); - seconds %= 86400; - - hours = Math.floor(seconds / 3600); - seconds %= 3600; - - minutes = Math.floor(seconds / 60); - seconds %= 60; - - var out = DURATIONS[val] = (weeks ? weeks + 'w' : '') + ((days || (weeks && (hours || minutes || seconds))) ? days + 'd' : '') + ((hours || ((weeks || days) && (minutes || seconds))) ? hours + 'h' : '') + ((minutes || ((weeks || days || hours) && seconds)) ? minutes + 'm' : '') + (seconds ? seconds + 's' : ''); - return out; - }; + CHECK = ''; try { @@ -169,6 +143,127 @@ FFZ.settings_info.mod_card_history = { }; +FFZ.settings_info.mod_buttons = { + type: "button", + + // Special Values + // false = Ban/Unban + // integer = Timeout (that amount of time) + value: [['', false, false], ['',600, false]], //, ['', 1, false]], + + category: "Chat Moderation", + no_bttv: true, + + name: "Custom In-Line Moderation Icons", + help: "Change out the different in-line moderation icons to use any command quickly.", + + method: function() { + var old_val = ""; + for(var i=0; i < this.settings.mod_buttons.length; i++) { + var pair = this.settings.mod_buttons[i], + prefix = pair[0], cmd = pair[1], had_prefix = pair[2]; + + if ( cmd === false ) + cmd = ""; + else if ( typeof cmd !== "string" ) + cmd = '' + cmd; + + if ( ! had_prefix ) + prefix = ''; + else + prefix += '='; + + if ( cmd.substr(cmd.length - 7) === ' {user}' ) + cmd = cmd.substr(0, cmd.length - 7); + + if ( cmd.indexOf(' ') !== -1 ) + old_val += ' ' + prefix + '"' + cmd + '"'; + else + old_val += ' ' + prefix + cmd; + } + + var new_val = prompt("Custom In-Line Moderation Icons\n\nPlease enter a list of commands to be made available as mod icons within chat lines. 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}\"\n\nNumeric values will become timeout buttons for that number of seconds. The text \"\" is a special value that will act like the normal Ban button in chat.\n\nTo assign a specific letter for use as the icon, specify it at the start of the command followed by an equals sign.\n\nExample: A=\"!reg add\"\n\nDefault: 600", old_val); + + if ( new_val === null || new_val === undefined ) + return; + + var vals = [], prefix = ''; + new_val = new_val.trim(); + + while(new_val) { + if ( new_val.charAt(1) === '=' ) { + prefix = new_val.charAt(0); + new_val = new_val.substr(2); + continue; + } + + if ( new_val.charAt(0) === '"' ) { + var end = new_val.indexOf('"', 1); + if ( end === -1 ) + end = new_val.length; + + var segment = new_val.substr(1, end - 1); + if ( segment ) { + vals.push([prefix, segment]); + prefix = ''; + } + + new_val = new_val.substr(end + 1); + + } else { + var ind = new_val.indexOf(' '); + if ( ind === -1 ) { + if ( new_val ) { + vals.push([prefix, new_val]); + prefix = ''; + } + + new_val = ''; + + } else { + var segment = new_val.substr(0, ind); + if ( segment ) { + vals.push([prefix, segment]); + prefix = ''; + } + + new_val = new_val.substr(ind + 1); + } + } + } + + var final = []; + for(var i=0; i < vals.length; i++) { + var had_prefix = false, prefix = vals[i][0], val = vals[i][1]; + if ( val === "" ) + val = false; + + var num = parseInt(val); + if ( num > 0 && num !== NaN ) + val = num; + + if ( ! prefix ) { + var tmp; + if ( typeof val === "string" ) + tmp = /\w/.exec(val); + else + tmp = utils.duration_string(val); + + prefix = tmp && tmp.length ? tmp[0].toUpperCase() : "C"; + } else + had_prefix = true; + + if ( typeof val === "string" && val.indexOf('{user}') === -1 ) + val += ' {user}'; + + final.push([prefix, val, had_prefix]); + } + + this.settings.set('mod_buttons', final); + } + }; + + FFZ.settings_info.mod_card_buttons = { type: "button", value: [], @@ -351,6 +446,8 @@ FFZ.prototype.setup_mod_card = function() { controller = this.get('controller'), line, + is_mod = controller.get('cardInfo.isModeratorOrHigher'), + user_id = controller.get('cardInfo.user.id'), alias = f.aliases[user_id]; @@ -384,7 +481,7 @@ FFZ.prototype.setup_mod_card = function() { } // Additional Buttons - if ( f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) { + if ( is_mod && f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) { line = document.createElement('div'); line.className = 'extra-interface interface clearfix'; @@ -394,7 +491,7 @@ FFZ.prototype.setup_mod_card = function() { cont = App.__container__.lookup('controller:chat'), room = cont && cont.get('currentRoom'); - room && room.send(cmd.replace(/{user}/g, user_id)); + room && room.send(cmd.replace(/{user}/g, user_id), true); }, add_btn_make = function(cmd) { @@ -446,16 +543,16 @@ FFZ.prototype.setup_mod_card = function() { room = App.__container__.lookup('controller:chat').get('currentRoom'); if ( is_mod && key == keycodes.P ) - room.send("/timeout " + user_id + " 1"); + room.send("/timeout " + user_id + " 1", true); else if ( is_mod && key == keycodes.B ) - room.send("/ban " + user_id); + room.send("/ban " + user_id, true); else if ( is_mod && key == keycodes.T ) - room.send("/timeout " + user_id + " 600"); + room.send("/timeout " + user_id + " 600", true); else if ( is_mod && key == keycodes.U ) - room.send("/unban " + user_id); + room.send("/unban " + user_id, true); else if ( key != keycodes.ESC ) return; @@ -466,51 +563,23 @@ FFZ.prototype.setup_mod_card = function() { // Only do the big stuff if we're mod. - if ( controller.get('cardInfo.isModeratorOrHigher') ) { + if ( is_mod ) { el.classList.add('ffz-is-mod'); - // Key Handling - if ( f.settings.mod_card_hotkeys ) { - el.classList.add('no-mousetrap'); - - el.addEventListener('keyup', function(e) { - var key = e.keyCode || e.which, - user_id = controller.get('cardInfo.user.id'), - room = App.__container__.lookup('controller:chat').get('currentRoom'); - - if ( key == keycodes.P ) - room.send("/timeout " + user_id + " 1"); - - else if ( key == keycodes.B ) - room.send("/ban " + user_id); - - else if ( key == keycodes.T ) - room.send("/timeout " + user_id + " 600"); - - else if ( key == keycodes.U ) - room.send("/unban " + user_id); - - else if ( key != keycodes.ESC ) - return; - - controller.send('close'); - }); - } - var btn_click = function(timeout) { var user_id = controller.get('cardInfo.user.id'), room = App.__container__.lookup('controller:chat').get('currentRoom'); if ( timeout === -1 ) - room.send("/unban " + user_id); + room.send("/unban " + user_id, true); else - room.send("/timeout " + user_id + " " + timeout); + room.send("/timeout " + user_id + " " + timeout, true); }, btn_make = function(timeout) { var btn = document.createElement('button') btn.className = 'button'; - btn.innerHTML = duration_string(timeout); + btn.innerHTML = utils.duration_string(timeout); btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); if ( f.settings.mod_card_hotkeys && timeout === 600 ) @@ -746,7 +815,7 @@ FFZ.chat_commands.purge = function(room, args) { for(var i=0; i < args.length; i++) { var name = args[i]; if ( name ) - room.room.send("/timeout " + name + " 1"); + room.room.send("/timeout " + name + " 1", true); } } @@ -760,7 +829,7 @@ FFZ.chat_commands.p.enabled = function() { return this.settings.short_commands; FFZ.chat_commands.t = function(room, args) { if ( ! args || ! args.length ) return "Timeout Usage: /t username [duration]"; - room.room.send("/timeout " + args.join(" ")); + room.room.send("/timeout " + args.join(" "), true); } FFZ.chat_commands.t.enabled = function() { return this.settings.short_commands; } @@ -776,7 +845,7 @@ FFZ.chat_commands.b = function(room, args) { for(var i=0; i < args.length; i++) { var name = args[i]; if ( name ) - room.room.send("/ban " + name); + room.room.send("/ban " + name, true); } } @@ -793,7 +862,7 @@ FFZ.chat_commands.u = function(room, args) { for(var i=0; i < args.length; i++) { var name = args[i]; if ( name ) - room.room.send("/unban " + name); + room.room.send("/unban " + name, true); } } diff --git a/src/ember/player.js b/src/ember/player.js index 9e0a9339..bfa09250 100644 --- a/src/ember/player.js +++ b/src/ember/player.js @@ -13,42 +13,76 @@ FFZ.settings_info.player_stats = { category: "Channel Metadata", name: "Stream Latency", - help: "New HTML5 Player Only. Display your current stream latency (how far behind the broadcast you are) under the player, with a few useful statistics in a tooltip.", + help: "Display your current stream latency (how far behind the broadcast you are) under the player, with a few useful statistics in a tooltip.", on_update: function(val) { + for(var key in this.players) { + var player = this.players[key]; + if ( player && player.player && player.player.ffzSetStatsEnabled ) + player.player.ffzSetStatsEnabled(val || player.player.ffz_stats); + } + if ( ! this._cindex ) return; - + this._cindex.ffzUpdatePlayerStats(); } }; +FFZ.settings_info.classic_player = { + type: "boolean", + value: false, + no_mobile: true, + + category: "Appearance", + + name: "Classic Player", + help: "Alter the appearance of the player to resemble the older Twitch player with always visible controls.", + + on_update: function(val) { + document.body.classList.toggle('ffz-classic-player', val); + var Layout = window.App && App.__container__.lookup('controller:layout'); + if ( Layout ) + Layout.set('PLAYER_CONTROLS_HEIGHT', val ? 32 : 0); + } + }; + + // --------------- // Initialization // --------------- FFZ.prototype.setup_player = function() { + document.body.classList.toggle('ffz-classic-player', this.settings.classic_player); + var Layout = window.App && App.__container__.lookup('controller:layout'); + if ( Layout ) + Layout.set('PLAYER_CONTROLS_HEIGHT', this.settings.classic_player ? 32 : 0); + this.players = {}; - + var Player2 = App && App.__container__.resolve('component:twitch-player2'); if ( ! Player2 ) return this.log("Unable to find twitch-player2 component."); - + this.log("Hooking HTML5 Player UI."); this._modify_player(Player2) - + // Modify all existing players. + if ( ! window.Ember ) + return; + for(var key in Ember.View.views) { if ( ! Ember.View.views.hasOwnProperty(key) ) continue; - + var view = Ember.View.views[key]; if ( !(view instanceof Player2) ) continue; this.log("Manually updating existing Player instance.", view); try { + this._modify_player(view); view.ffzInit(); if ( view.get('player') ) view.ffzPostPlayer(); @@ -65,7 +99,12 @@ FFZ.prototype.setup_player = function() { // --------------- FFZ.prototype._modify_player = function(player) { - var f = this; + var f = this, + update_stats = function() { + f._cindex && f._cindex.ffzUpdatePlayerStats(); + }; + + player.reopen({ didInsertElement: function() { this._super(); @@ -75,7 +114,7 @@ FFZ.prototype._modify_player = function(player) { f.error("Player2 didInsertElement: " + err); } }, - + willClearRender: function() { try { this.ffzTeardown(); @@ -84,7 +123,7 @@ FFZ.prototype._modify_player = function(player) { } this._super(); }, - + postPlayerSetup: function() { this._super(); try { @@ -93,55 +132,101 @@ FFZ.prototype._modify_player = function(player) { f.error("Player2 postPlayerSetup: " + err); } }, - + ffzInit: function() { var id = this.get('channel.id'); f.players[id] = this; - - this._ffz_stat_update = this.ffzStatUpdate.bind(this); }, - + ffzTeardown: function() { var id = this.get('channel.id'); if ( f.players[id] === this ) f.players[id] = undefined; + + if ( this._ffz_stat_interval ) { + clearInterval(this._ffz_stat_interval); + this._ffz_stat_interval = null; + } }, - - ffzStatUpdate: function() { - f._cindex && f._cindex.ffzUpdatePlayerStats(); - }, - + ffzPostPlayer: function() { var player = this.get('player'); if ( ! player ) return; - // Make it so stats can no longer be disabled. - player.ffzSetStatsEnabled = player.setStatsEnabled; - player.setStatsEnabled = function() {} - - // We can't just request stats straight away... - this.ffzWaitForStats(); + // Subscribe to the qualities event. + //player.addEventListener('qualitieschange', this.ffzQualitiesUpdated.bind(this)); + //this.ffzQualitiesUpdated(); + + // Only set up the stats hooks if we need stats. + if ( ! player.getVideo() ) + this.ffzInitStats(); }, - - ffzWaitForStats: function() { + + ffzInitStats: function() { + if ( this.get('ffzStatsInitialized') ) + return; + var player = this.get('player'); if ( ! player ) return; - if ( player.stats ) { - // Add the event listener. - player.addEventListener('statschange', this._ffz_stat_update); + this.set('ffzStatsInitialized', true); - } else { - // Keep going until we've got it. - player.ffzSetStatsEnabled(false); - var t = this; - setTimeout(function() { - player.ffzSetStatsEnabled(true); - setTimeout(t.ffzWaitForStats.bind(t), 1250); - }, 250); + // Make it so stats can no longer be disabled if we want them. + player.ffzSetStatsEnabled = player.setStatsEnabled; + player.ffz_stats = player.getStatsEnabled(); + + var t = this; + + player.setStatsEnabled = function(e, s) { + if ( s !== false ) + player.ffz_stats = e; + + var out = player.ffzSetStatsEnabled(e || f.settings.player_stats); + + if ( ! t._ffz_player_stats_initialized ) { + t._ffz_player_stats_initialized = true; + player.addEventListener('statschange', update_stats); + } + + return out; } - } + + this._ffz_stat_interval = setInterval(function() { + if ( f.settings.player_stats || player.ffz_stats ) { + player.ffzSetStatsEnabled(false); + player.ffzSetStatsEnabled(true); + } + }, 5000); + + if ( f.settings.player_stats && ! player.ffz_stats ) { + this._ffz_player_stats_initialized = true; + player.addEventListener('statschange', update_stats); + player.ffzSetStatsEnabled(true); + } + }, + + ffzSetQuality: function(q) { + var player = this.get('player'); + if ( ! player ) + return; + + this.$(".js-quality-display-contain").attr("data-q", "loading"); + + player.setQuality(q); + + var t = this.$(".js-player-alert"); + t.find(".js-player-alert__message").text(); + t.attr("data-active", !0); + }, + + ffzGetQualities: function() { + var player = this.get('player'); + if ( ! player ) + return []; + return player.getQualities(); + }, + }); } \ No newline at end of file diff --git a/src/ember/room.js b/src/ember/room.js index 9f06fa6a..a4b04135 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -1180,12 +1180,12 @@ FFZ.prototype._modify_room = function(room) { return this._super(e); }, - send: function(text) { + send: function(text, ignore_history) { if ( f.settings.group_tabs && f.settings.whisper_room && this.ffz_whisper_room ) return; try { - if ( text ) { + if ( text && ! ignore_history ) { // Command History var mru = this.get('mru_list'), ind = mru.indexOf(text); diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index b5919466..4a3369de 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -66,7 +66,10 @@ FFZ.prototype.setup_bttv = function(delay) { document.body.classList.remove("ffz-chat-padding"); document.body.classList.remove("ffz-chat-separator"); document.body.classList.remove("ffz-chat-separator-3d"); + document.body.classList.remove("ffz-chat-separator-wide"); + document.body.classList.remove("ffz-chat-separator-3d-inset"); document.body.classList.remove("ffz-sidebar-swap"); + document.body.classList.remove("ffz-flip-dashboard"); document.body.classList.remove("ffz-transparent-badges"); document.body.classList.remove("ffz-high-contrast-chat-text"); document.body.classList.remove("ffz-high-contrast-chat-bg"); diff --git a/src/featurefriday.js b/src/featurefriday.js index fed32535..e1cbe3ec 100644 --- a/src/featurefriday.js +++ b/src/featurefriday.js @@ -17,7 +17,7 @@ FFZ.prototype.check_ff = function(tries) { if ( ! tries ) this.log("Checking for Feature Friday data..."); - jQuery.ajax(constants.SERVER + "script/event.json", {cache: false, dataType: "json", context: this}) + jQuery.ajax(constants.SERVER + "script/event.json", {dataType: "json", context: this}) .done(function(data) { return this._load_ff(data); }).fail(function(data) { diff --git a/src/localization.js b/src/localization.js new file mode 100644 index 00000000..8bcd4948 --- /dev/null +++ b/src/localization.js @@ -0,0 +1,6 @@ +var FFZ = window.FrankerFaceZ; + + +FFZ.prototype.tr = function(s) { + return s; +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 159cc75a..94f78330 100644 --- a/src/main.js +++ b/src/main.js @@ -21,7 +21,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 5, revision: 21, + major: 3, minor: 5, revision: 30, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -103,6 +103,7 @@ FFZ.prototype.get_user = function() { // ------------------- // Import these first to set up data structures +require('./localization'); require('./ui/menu'); require('./settings'); require('./socket'); @@ -111,11 +112,12 @@ require('./colors'); require('./emoticons'); require('./badges'); require('./tokenize'); +//require('./filtering'); // Analytics: require('./ember/router'); require('./ember/channel'); -//require('./ember/player'); +require('./ember/player'); require('./ember/room'); require('./ember/layout'); require('./ember/line'); @@ -124,6 +126,7 @@ require('./ember/viewers'); require('./ember/moderation-card'); require('./ember/chat-input'); //require('./ember/teams'); +require('./ember/directory'); require('./debug'); @@ -158,7 +161,7 @@ FFZ.prototype.initialize = function(increment, delay) { // Check for the player if ( location.hostname === 'player.twitch.tv' ) { - //this.init_player(delay); + this.init_player(delay); return; } @@ -211,6 +214,8 @@ FFZ.prototype.init_player = function(delay) { // Literally only make it dark. this.load_settings(); this.setup_dark(); + this.setup_css(); + this.setup_player(); var end = (window.performance && performance.now) ? performance.now() : Date.now(), duration = end - start; @@ -279,7 +284,9 @@ FFZ.prototype.init_dashboard = function(delay) { this.setup_tokenization(); this.setup_notifications(); + this.setup_following_count(false); this.setup_css(); + this.setup_menu(); this._update_subscribers(); @@ -318,7 +325,9 @@ FFZ.prototype.init_ember = function(delay) { //this.setup_router(); this.setup_colors(); this.setup_tokenization(); - //this.setup_player(); + //this.setup_filtering(); + + this.setup_player(); this.setup_channel(); this.setup_room(); this.setup_line(); @@ -327,6 +336,7 @@ FFZ.prototype.init_ember = function(delay) { this.setup_viewers(); this.setup_mod_card(); this.setup_chat_input(); + this.setup_directory(); //this.setup_teams(); diff --git a/src/settings.js b/src/settings.js index 33f5b2b7..d030b343 100644 --- a/src/settings.js +++ b/src/settings.js @@ -89,11 +89,37 @@ FFZ.prototype.load_settings = function() { // Backup and Restore // -------------------- +FFZ.prototype.reset_settings = function() { + if ( ! confirm(this.tr('Are you sure you wish to reset FrankerFaceZ?\n\nThis will force the tab to refresh.')) ) + return; + + + // Clear Settings + for(var key in FFZ.settings_info) { + if ( ! FFZ.settings_info.hasOwnProperty(key) ) + continue; + + this.settings.del(key); + } + + // Clear Aliases + this.aliases = {}; + localStorage.ffz_aliases = '{}'; + + // TODO: Filters + + + // Refresh + window.location.reload(); +} + + FFZ.prototype.save_settings_file = function() { var data = { version: 1, script_version: FFZ.version_info + '', aliases: this.aliases, + filters: this.filters, settings: {} }; @@ -135,8 +161,8 @@ FFZ.prototype._load_settings_file = function(data) { this.log("Loading Settings Data", data); - var skipped = [], - applied = []; + var skipped = [], applied = [], + aliases = 0; if ( data.settings ) { for(var key in data.settings) { @@ -158,9 +184,26 @@ FFZ.prototype._load_settings_file = function(data) { } } + if ( data.aliases ) { + for(var key in data.aliases) { + if ( this.aliases[key] === data.aliases[key] ) + continue; + + this.aliases[key] = data.aliases[key]; + aliases++; + } + + if ( aliases ) + localStorage.ffz_aliases = JSON.stringify(this.aliases); + } + + if ( data.filters ) { + // TODO: Load filters! + } + // 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.'); + alert('Successfully loaded ' + applied.length + ' settings and skipped ' + skipped.length + ' settings. Added ' + aliases + ' user nicknames.'); }); } @@ -257,8 +300,11 @@ FFZ.menu_pages.settings = { render_save: function(view, container) { var backup_head = document.createElement('div'), restore_head = document.createElement('div'), + reset_head = document.createElement('div'), + backup_cont = document.createElement('div'), restore_cont = document.createElement('div'), + reset_cont = document.createElement('div'), backup_para = document.createElement('p'), backup_link = document.createElement('a'), @@ -268,6 +314,10 @@ FFZ.menu_pages.settings = { restore_input = document.createElement('input'), restore_link = document.createElement('a'), restore_help = document.createElement('span'), + + reset_para = document.createElement('p'), + reset_link = document.createElement('a'), + reset_help = document.createElement('span'), f = this; @@ -310,8 +360,27 @@ FFZ.menu_pages.settings = { restore_para.appendChild(restore_help); restore_cont.appendChild(restore_para); + reset_cont.className = 'chat-menu-content'; + reset_head.className = 'heading'; + reset_head.innerHTML = this.tr('Reset Settings'); + reset_cont.appendChild(reset_head); + + reset_para.className = 'clearfix option'; + + reset_link.href = '#'; + reset_link.innerHTML = this.tr('Reset FrankerFaceZ'); + reset_link.addEventListener('click', this.reset_settings.bind(this)); + + reset_help.className = 'help'; + reset_help.innerHTML = this.tr('This resets all of your FFZ data. That includes chat filters, nicknames for users, and settings.'); + + reset_para.appendChild(reset_link); + reset_para.appendChild(reset_help); + reset_cont.appendChild(reset_para); + container.appendChild(backup_cont); container.appendChild(restore_cont); + container.appendChild(reset_cont); }, render_basic: function(view, container) { @@ -797,11 +866,11 @@ FFZ.prototype._setting_del = function(key) { if ( localStorage.hasOwnProperty(ls_key) ) localStorage.removeItem(ls_key); - delete this.settings[key]; - if ( info ) val = this.settings[key] = info.hasOwnProperty("value") ? info.value : undefined; + this.settings[key] = val; + if ( info.on_update ) try { info.on_update.bind(this)(val, true); diff --git a/src/tokenize.js b/src/tokenize.js index 0e5a4ce9..e24c4b46 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -108,7 +108,7 @@ var FFZ = window.FrankerFaceZ, } image_iframe = function(href, extra_class) { - return ''; + return ''; }, @@ -352,7 +352,7 @@ FFZ.prototype.setup_tokenization = function() { // --------------------- FFZ.prototype.load_twitch_emote_data = function(tries) { - jQuery.ajax(constants.SERVER + "script/twitch_emotes.json", {cache: false, context: this}) + jQuery.ajax(constants.SERVER + "script/twitch_emotes.json", {context: this}) .done(function(data) { for(var set_id in data) { var set = data[set_id]; diff --git a/src/ui/dark.js b/src/ui/dark.js index 50256331..6de8f3bd 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -50,11 +50,11 @@ FFZ.basic_settings.minimalistic_chat = { 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; + return this.settings.minimal_chat === 3 && this.settings.chat_padding; }, set: function(val) { - this.settings.set('minimal_chat', val); + this.settings.set('minimal_chat', val ? 3 : 0); this.settings.set('chat_padding', val); } }; diff --git a/src/ui/styles.js b/src/ui/styles.js index 11184095..b21f008d 100644 --- a/src/ui/styles.js +++ b/src/ui/styles.js @@ -2,6 +2,8 @@ var FFZ = window.FrankerFaceZ, constants = require('../constants'); FFZ.prototype.setup_css = function() { + document.body.classList.toggle('ffz-flip-dashboard', this.settings.flip_dashboard); + this.log("Injecting main FrankerFaceZ CSS."); var s = this._main_style = document.createElement('link'); @@ -11,14 +13,15 @@ FFZ.prototype.setup_css = function() { s.setAttribute('href', constants.SERVER + "script/style.css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); - jQuery.noty.themes.ffzTheme = { - name: "ffzTheme", - style: function() { - this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type); - }, - callback: { - onShow: function() {}, - onClose: function() {} - } - }; + if ( window.jQuery && jQuery.noty ) + jQuery.noty.themes.ffzTheme = { + name: "ffzTheme", + style: function() { + this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type); + }, + callback: { + onShow: function() {}, + onClose: function() {} + } + }; } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index b8fd7e05..512e008f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,13 +8,15 @@ var sanitize_el = document.createElement('span'), sanitize_el.textContent = msg; return sanitize_el.innerHTML; }, - + R_QUOTE = /"/g, R_SQUOTE = /'/g, R_AMP = /&/g, R_LT = //g, - + + DURATIONS = {}, + quote_attr = function(msg) { return msg.replace(R_AMP, "&").replace(R_QUOTE, """).replace(R_SQUOTE, "'").replace(R_LT, "<").replace(R_GT, ">"); }, @@ -256,7 +258,7 @@ module.exports = { return 'less than a second'; }, - time_to_string: function(elapsed, separate_days, days_only, no_hours) { + time_to_string: function(elapsed, separate_days, days_only, no_hours, no_seconds) { var seconds = elapsed % 60, minutes = Math.floor(elapsed / 60), hours = Math.floor(minutes / 60), @@ -273,7 +275,32 @@ module.exports = { days = ( days > 0 ) ? days + " days, " : ""; } - return days + ((!no_hours || days || hours) ? ((hours < 10 ? "0" : "") + hours + ':') : '') + (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds; + return days + ((!no_hours || days || hours) ? ((days && hours < 10 ? "0" : "") + hours + ':') : '') + (minutes < 10 ? "0" : "") + minutes + (no_seconds ? "" : (":" + (seconds < 10 ? "0" : "") + seconds)); + }, + + duration_string: function(val) { + if ( val === 1 ) + return 'Purge'; + + if ( DURATIONS[val] ) + return DURATIONS[val]; + + var weeks, days, hours, minutes, seconds; + + weeks = Math.floor(val / 604800); + seconds = val % 604800; + + days = Math.floor(seconds / 86400); + seconds %= 86400; + + hours = Math.floor(seconds / 3600); + seconds %= 3600; + + minutes = Math.floor(seconds / 60); + seconds %= 60; + + var out = DURATIONS[val] = (weeks ? weeks + 'w' : '') + ((days || (weeks && (hours || minutes || seconds))) ? days + 'd' : '') + ((hours || ((weeks || days) && (minutes || seconds))) ? hours + 'h' : '') + ((minutes || ((weeks || days || hours) && seconds)) ? minutes + 'm' : '') + (seconds ? seconds + 's' : ''); + return out; }, format_unread: function(count) { diff --git a/style.css b/style.css index 1cd8d82d..5cd812e0 100644 --- a/style.css +++ b/style.css @@ -21,20 +21,20 @@ body > div.tipsy .tipsy-arrow { opacity: 0.8; } .ffz-hide-recent-past-broadcast .recent-past-broadcast, .ffz-hide-view-count .stat.twitch-channel-views, -.ffz-minimal-chat .emoticon-selector-toggle, +.ffz-minimal-chat-input .emoticon-selector-toggle, .ffz-menu-replace .emoticon-selector-toggle { display: none !important; } -body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + .ffz-ui-toggle svg, -body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle svg +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + .ffz-ui-toggle svg, +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle svg { height: 14px; width: 18px; } -body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + .ffz-ui-toggle, -body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle { +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + .ffz-ui-toggle, +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle { height: 14px; width: 18px; top: 28px; @@ -43,6 +43,8 @@ body:not(.ffz-minimal-chat):not(.ffz-menu-replace) .emoticon-selector-toggle + s .ffz-ui-toggle svg.svg-emoticons path { fill: rgba(0,0,0,0.2); } .ffz-ui-toggle:hover svg.svg-emoticons path { fill: rgba(0,0,0,0.5); } +.streams .stream .content .overlay_info.live svg path, +.videos .video .content .overlay_info.live svg path { fill: #ff2020; } .ember-chat-container.dark .ffz-ui-toggle svg.svg-emoticons path, .chat-container.dark .ffz-ui-toggle svg.svg-emoticons path, @@ -974,6 +976,16 @@ body:not(.ffz-chat-purge-icon) .ember-chat .mod-icons .purge { display: none; } background-repeat: no-repeat; } +.ember-chat .mod-icons .custom { + text-indent: 0; + text-align: center; + text-decoration: none; + font-size: 18px; + font-weight: bold; + color: #888 !important; +} + + /* Chat Rows */ .ffz-alias { font-style: italic; } @@ -1045,10 +1057,19 @@ body:not(.ffz-chat-purge-icon) .ember-chat .mod-icons .purge { display: none; } border-bottom: 1px solid #aaa; } +.ffz-chat-separator-wide .chat-line:before { + border-top: 1px solid #aaa; +} + .ffz-chat-separator-3d .chat-line:before { border-top: 1px solid rgba(255,255,255,0.5); } +.ffz-chat-separator-3d-inset .chat-line:before { + border-bottom-color: rgba(255,255,255,0.5); + border-top: 1px solid #aaa; +} + .ffz-chat-separator-3d ul.chat-lines div:first-of-type .chat-line:before { border-top: none; } @@ -1066,6 +1087,14 @@ body:not(.ffz-chat-purge-icon) .ember-chat .mod-icons .purge { display: none; } border-bottom-color: #000; } +.ffz-chat-separator-wide .app-main.theatre .chat-line:before, +.ffz-chat-separator-wide .chat-container.dark .chat-line:before, +.ffz-chat-separator-wide .chat-container.force-dark .chat-line:before, +.ffz-chat-separator-wide .ember-chat-container.dark .chat-line:before, +.ffz-chat-separator-wide .ember-chat-container.force-dark .chat-line:before { + border-top-color: #000; +} + .ffz-chat-separator-3d .app-main.theatre .chat-line:before, .ffz-chat-separator-3d .chat-container.dark .chat-line:before, .ffz-chat-separator-3d .chat-container.force-dark .chat-line:before, @@ -1074,6 +1103,15 @@ body:not(.ffz-chat-purge-icon) .ember-chat .mod-icons .purge { display: none; } border-top-color: rgba(255,255,255,0.1); } +.ffz-chat-separator-3d-inset .app-main.theatre .chat-line:before, +.ffz-chat-separator-3d-inset .chat-container.dark .chat-line:before, +.ffz-chat-separator-3d-inset .chat-container.force-dark .chat-line:before, +.ffz-chat-separator-3d-inset .ember-chat-container.dark .chat-line:before, +.ffz-chat-separator-3d-inset .ember-chat-container.force-dark .chat-line:before { + border-bottom-color: rgba(255,255,255,0.1); + border-top-color: #000; +} + .ffz-chat-background .chat-history .chat-line.ffz-alternate:before, .ffz-chat-background .ember-chat .chat-messages .chat-line.ffz-alternate:before { background-color: rgba(0,0,0, 0.1); @@ -1213,6 +1251,7 @@ body:not(.ffz-chat-purge-icon) .ember-chat .mod-icons .purge { display: none; } display: block; width: 80px; height: 63px; + background-image: url("//cdn.frankerfacez.com/script/spinner-dark.png"); margin: 50px auto; @@ -1514,9 +1553,9 @@ th.ffz-row-switch { /* Minimalistic Chat */ -body.ffz-minimal-chat .ember-chat .chat-header, -body.ffz-minimal-chat .ember-chat #ffz-group-tabs, -body.ffz-minimal-chat .ember-chat .chat-buttons-container { +body.ffz-minimal-chat-head .ember-chat > .chat-header, +body.ffz-minimal-chat-head .ember-chat #ffz-group-tabs, +body.ffz-minimal-chat-input .ember-chat .chat-buttons-container { display: none !important; } @@ -1525,30 +1564,30 @@ body.ffz-minimal-chat .ember-chat .chat-interface .emoticon-selector { bottom: 33px; }*/ -body.ffz-minimal-chat .ember-chat .chat-interface .emoticon-selector { +body.ffz-minimal-chat-input .ember-chat .chat-interface .emoticon-selector { right: 10px; } -body.ffz-minimal-chat .ember-chat .chat-interface .emoticon-selector .dropmenu { +body.ffz-minimal-chat-input .ember-chat .chat-interface .emoticon-selector .dropmenu { margin-bottom: 10px; } -body.ffz-minimal-chat .ember-chat .chat-room { +body.ffz-minimal-chat-head .ember-chat .chat-room { top: 0 !important; } -body.ffz-minimal-chat .ember-chat .chat-interface { +body.ffz-minimal-chat-input .ember-chat .chat-interface { /*height: 33px !important;*/ padding: 0; } -body.ffz-minimal-chat .ember-chat .chat-interface .textarea-contain { +body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain { top: 0 !important; margin: 0 !important; height: auto; } -body.ffz-minimal-chat .ember-chat .chat-interface .textarea-contain textarea { +body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textarea { /*height: 33px !important;*/ overflow: hidden; border-bottom: 0 !important; @@ -1982,4 +2021,119 @@ li[data-name="following"] a { .ffz-yt-thumb { max-height: 90px; +} + +/* Classic Player */ + +.ffz-classic-player .player .player-video { + position: absolute; + top: 0; bottom: 32px; + left: 0; right: 0; +} + +.ffz-classic-player .player.player-isvod .player-video { + bottom: 36px; +} + +.ffz-classic-player .player .player-controls-bottom { + opacity: 1; + + padding-top: 0; + border-top: 1px solid #000; + border-bottom: 1px solid #000; + + background: -webkit-linear-gradient(bottom, #252525, #666); + background: linear-gradient(to top, #252525, #666); +} + +.ffz-classic-player .app-main.theatre .player .player-video, +.ffz-classic-player .player[data-fullscreen="true"] .player-video { + bottom: 0; +} + +.ffz-classic-player .app-main.theatre .player .player-controls-bottom, +.ffz-classic-player .player[data-fullscreen="true"] .player-controls-bottom { + margin-bottom: -32px; + -webkit-transition: margin-bottom .2s ease-out; + transition: margin-bottom .2s ease-out; +} + +.ffz-classic-player .app-main.theatre .player.player-isvod .player-controls-bottom, +.ffz-classic-player .player.player-isvod[data-fullscreen="true"] .player-controls-bottom { + margin-bottom: -36px; +} + +.ffz-classic-player .app-main.theatre .player-column:hover .player .player-controls-bottom, +.ffz-classic-player .app-main.theatre .player-column:focus .player .player-controls-bottom, +.ffz-classic-player .player[data-fullscreen="true"][data-controls="true"] .player-controls-bottom { + margin-bottom: 0; +} + +.ffz-classic-player .player .player-button { + padding-bottom: 0; + height: 30px; +} + + +.ffz-classic-player .player .player-slider:before, +.ffz-classic-player .player .player-button, +.ffz-classic-player .player .player-slider .ui-slider-handle, +.ffz-classic-player .player .player-seek .player-seek__time { + -webkit-filter: drop-shadow(0px 0px 1px #000); + filter: drop-shadow(0px 0px 1px #000); +} + + +.ffz-classic-player .player .player-slider .ui-slider-handle { background-color: #aeaeae; } +.ffz-classic-player .player .player-button svg { fill: #aeaeae; } +.ffz-classic-player .player .player-seek .player-seek__time { color: #ddd; } + +.ffz-classic-player .player .player-volume__slider-container { + width: auto; +} + + +.ffz-classic-player .player .player-seek { + padding: 0; +} + +.ffz-classic-player .player .player-seek .player-slider { + margin: -1em 0; +} + +.ffz-classic-player .player .player-seek .player-seek__time-container { + position: absolute; + bottom: -12px; + left: 210px; +} + +.ffz-classic-player .player .player-seek .player-seek__time + .player-seek__time:before { + content: "/"; + padding: 0 5px; + opacity: 0.8; +} + +/* Directory Logos */ + +.ffz-directory-logo .meta p { width: auto; } + +.ffz-directory-logo .profile-photo { + float: left; + height: 46px; + width: 46px; + margin-right: 10px; +} + +/* Flip Dashboard */ + +.ffz-flip-dashboard #dash_main #controls_column { + float: right; + margin-left: 20px; + margin-right: 0; +} + +.ffz-flip-dashboard #dash_main .dash-chat-column { + right: inherit; + left: 0; + margin-right: 20px; } \ No newline at end of file