NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name dAmn Tab Logger // @namespace MiloticScale // @author Jonathon Braswell // @description Allows you to see all messages directed toward you neatly in a floating modal. // @include http://chat.deviantart.com/chat/* // @version 1.1 // ==/UserScript== // Parent function to append to the DOM on init var script = document.createElement('script'); var helperScript = freeFunctionString((function(){ // Pertinent object to house the utility functions. dAmn_TabLogger = { // version // Returns: String // Purpose: Shortcut for version version: "v.1.1", // testers // Returns: Array // Purpose: Keeps track of script testers who helped out. Might implement something special // for them at a later time. testers: ['pickley', 'deviant-garde', 'darkalchemistninja', 'romaji', 'farand', 'ricsmond', 'lady-ra', 'princejose', 'moronicglasskiller', 'kristenspork'], // updateStatus // Arguments: Boolean status // Returns: void // Purpose: Toggle text and styling of the on/off button in the modal updateStatus: function(status) { if(status) $('#dAmn_TabLoggerStatus') .removeAttr('style') .css(dAmn_TabLogger.CSS.status) .css(dAmn_TabLogger.CSS.onStatus) .text("on"); else $('#dAmn_TabLoggerStatus') .removeAttr('style') .css(dAmn_TabLogger.CSS.status) .css(dAmn_TabLogger.CSS.offStatus) .text("off"); }, // addLog // Arguments: Object info // Returns: void // Purpose: Collect message information from the DOM and reflect it in the modal. Calls this.playSound addLog: function(info) { $('#dAmn_TabLoggerLogs .no-logs').hide(); var infoString = info.time + " [" + info.chat + "] <<i></i>" + info.from + "<i></i>> " + info.message; var newLog = $('<span/>') .addClass('log') .html(infoString + "<hr>") .appendTo($('#dAmn_TabLoggerLogs')); }, // clearLog // Arguments: none // Returns: void // Purpose: Empty the modal of logs clearLog: function() { $('#dAmn_TabLoggerLogs .log, #dAmn_TabLoggerLogs hr').remove(); $('#dAmn_TabLoggerLogs .no-logs').fadeIn(); this.notify("Cleared tab logs"); }, // showLog // Arguments: none // Returns: void // Purpose: Display the modal showLog: function() { $('#dAmn_TabLoggerModal').fadeIn(); }, // hideLog // Arguments: none // Returns: void // Purpose: Hide the modal hideLog: function() { $('#dAmn_TabLoggerModal').fadeOut(); }, // showPreferences // Arguments: none // Returns: void // Purpose: Show the preferences modal showPreferences: function() { $('#dAmn_TabLoggerLogs').hide('fast'); $('#dAmn_TabLoggerPreferencesModal').show('fast'); }, // hidePreferences // Arguments: none // Returns: void // Purpose: Hide the preferences modal hidePreferences: function() { $('#dAmn_TabLoggerPreferencesModal').hide('fast'); $('#dAmn_TabLoggerLogs').show('fast'); }, // start // Arguments: Boolean doNotify // Returns: void // Purpose: Turns on logging, instantiates a MutationObserver. The MutationObserver // calls this.addLog when the user is tabbed. Flips the on bit in localStorage. // Calls this.notify and this.updateStatus startLog: function(doNotify) { if(localStorage.dAmn_TabLoggerStatus !== "on") { localStorage.dAmn_TabLoggerStatus = "on"; this.updateStatus(true); if(doNotify) this.notify("Started logging tabs"); } }, // stop // Arguments: Boolean doNotify // Returns: void // Purpose: Deletes the MutationObserver on the chat window. Flips the bit // in localStorage. Calls this.notify and this.updateStatus stopLog: function(doNotify) { if(localStorage.dAmn_TabLoggerStatus !== "off") { localStorage.dAmn_TabLoggerStatus = "off"; this.updateStatus(false); if(doNotify) this.notify("Stopped logging tabs"); } }, // notify // Arguments: String message // Returns: void // Purpose: Appends message to the chat window. Attaches onclick event to the message // which removes the element from the DOM. Make sure the window stays at the bottom, too. notify: function(message) { var notification = $('<div/>') .addClass('msg part') .css('font-weight', 'bold') .html('<span style="font-size: 90%">' + this.getTimestamp() + '</span>' + ' *** ' + message + ' *'); $(notification).appendTo($('.damncrc-chat')); $('.damncrc-chat-window').each(function(i, cWindow) { $(cWindow).animate({ scrollTop: $('.damncrc-chat').eq(i).height() }, 'fast'); }); }, // getTimestamp // Arguments: none // Returns: String // Purpose: Generates a timestamp getTimestamp: function() { function checkTime(time) { return (time < 10 ? "0" + time : time); } var now = new Date(); var h = now.getHours(); h = h % 12; h = checkTime(h); h = h ? h : 12; h = (h == "00" ? 12 : h); var m = checkTime(now.getMinutes()); var s = checkTime(now.getSeconds()); var ampm = (now.getHours() >= 12 ? 'PM' : 'AM'); return (h + ':' + m + ':' + s + ' ' + ampm); }, // CSS // Purpose: House all CSS styling necessary in this user script as objects CSS: { logModal: { 'position': 'absolute', 'top': '50%', 'left': '50%', 'margin-top': '-200px', 'margin-left': '-35%', 'z-index': '9999', 'border': '1px solid #000', 'border-radius': '8px', 'text-align': 'center', 'display': 'none', 'background': '#bbc2bb', 'box-shadow': '0px 0px 10px #000', 'width': '60%', 'min-height': '100px' }, logContainer: { 'padding': '20px', 'margin': '10px', 'background': '#fafffa', 'color': '#000', 'font-size': '13px', 'text-align': 'left', 'overflow': 'auto', 'min-height': '80%', 'min-width': '80%', 'max-height': '300px' }, preferencesModal: { 'z-index': '99999', 'text-align': 'left', 'box-shadow': '0px 0px 5px #000', 'padding': '20px', 'width': '30%', 'position': 'initial', 'margin': '0 auto', 'margin-top': '20px', 'margin-bottom': '20px', 'background': '#fafffa' }, status: { 'margin': '10px', 'border': '1px solid #000', 'border-radius': '50px', 'height': '15px', 'width': '35px', 'float': 'left', 'cursor': 'pointer', 'color': '#fafffa', 'text-align': 'center' }, onStatus: { 'background': '#00cc00' }, offStatus: { 'background': '#cc0000' }, clearButton: { 'margin': '10px', 'border': '1px solid #000', 'border-radius': '50px', 'height': '15px', 'width': '35px', 'position': 'absolute', 'top': '0', 'left': '50px', 'cursor': 'pointer', 'background': '#fafffa', 'text-align': 'center' }, preferencesButton: { 'margin': '10px', 'border': '1px solid #000', 'border-radius': '50px', 'height': '15px', 'width': '85px', 'position': 'absolute', 'top': '0', 'left': '100px', 'cursor': 'pointer', 'background': '#bbc2bb', 'text-align': 'center' } }, // bindCommands // Arguments: none // Returns: void // Purpose: Binds chat commands to the relevant object methods // Full credit for this model to electricjonny - http://electricjonny.deviantart.com bindCommands: function() { // Bind the chat commands to our object dAmnChanChat.prototype.loggerCommands_Init = dAmnChanChat.prototype.Init; dAmnChanChat.prototype.Init = function(cr, name, parent_el) { this.loggerCommands_Init(cr, name, parent_el); var cie = this.input; cie.cmds['tablogger']=[0]; }; dAmnChatInput_onKey_loggerCommands = dAmnChatInput_onKey; dAmnChatInput_onKey = function(e, kc, force) { var el = this.chatinput_el; if (kc == 13 && (force || !this.multiline || e.shiftKey || e.ctrlKey)) { var input = el.value; var args = /^\/(\S*)\s*(.*)$/i.exec(input); if (args) { var cmd = args[1]; var param = args[2]; var did = false; if (cmd) { if(cmd == "tablogger" && !param) { dAmn_TabLogger.showLog(); } switch (param) { case 'on': case 'start': dAmn_TabLogger.startLog(true); did = true; break; case 'off': case 'stop': dAmn_TabLogger.stopLog(true); did = true; break; case 'clear': case 'empty': dAmn_TabLogger.clearLog(); break; } if (did) { if (el.value) { el.value = ''; el.focus(); }; }; }; }; }; if (!did) { return this.onKey_loggerCommands(e, kc, force) ? true: false; } else { return false; }; }; dAmnChatInput.prototype.onKey = dAmnChatInput_onKey; dAmnChatInput.prototype.onKey_loggerCommands = dAmnChatInput_onKey_loggerCommands; }, // bindTabs // Arguments: none // Returns: void // Purpose: Binds message events. bindTabs: function() { dAmnChat_onData_alert = dAmnChat_onData; dAmnChat_onData = function ( pkt ){ this.onData_alert(pkt); if (pkt.param == this.ns) { switch (pkt.cmd) { case 'recv': var recv = dAmn_ParsePacket(pkt.body); var ns = pkt.param; var isPchat = (ns[0] == "p"); var whiteListOn; var isInWhiteList; // Make sure the packet being parsed here is not just data retrieval if(pkt.body.substring(0,3) == "msg" || pkt.body.substring(0,6) == "action") { // The below line is taken from dAmn's own code to determine whether your username is in the message // Added "i" argument for case-insensitive if(-1 != recv.body.search(RegExp("([^A-Za-z]+|^)" + dAmn_Client_Username + "([^A-Za-z]+|$|s$|s[^A-Za-z]+)" , "i")) || isPchat){ if(localStorage.dAmn_TabLoggerStatus == "on" && !isPchat) { var info = {}; info.time = dAmn_TabLogger.getTimestamp(); info.from = (typeof dAmnChats[ns].members.members[recv.args.from].name !== "undefined" ? dAmnChats[ns].members.members[recv.args.from].name : dAmn_Client_Username); info.chat = ns.replace("chat:", "#"); // Need to parse thumbs, links, emotes, etc. dAmnChanChat.prototype.FormatMsg(recv.body, function(t) { info.message = t; }.bind(this)); // Don't log if you tab yourself. // Or if you have white list enabled and the user is not in it. if(info.from !== dAmn_Client_Username) { whiteListOn = dAmn_TabLoggerPreferences.whiteList; isInWhiteList = $.inArray(info.from.toLowerCase(), localStorage.dAmn_TabLoggerWhiteList.split(',')) > -1; if((whiteListOn && isInWhiteList) || !whiteListOn) { dAmn_TabLogger.addLog(info); } } } // Don't make the noise if you tab yourself. // Or if you have white list enabled and the user is not in it. if(dAmn_TabLoggerPreferences.tabNoise && (recv.args.from !== dAmn_Client_Username)) { whiteListOn = dAmn_TabLoggerPreferences.whiteList; isInWhiteList = $.inArray(recv.args.from.toLowerCase(), localStorage.dAmn_TabLoggerWhiteList.split(',')) > 1; if((whiteListOn && isInWhiteList) || !whiteListOn) { dAmn_TabLogger.playSound(); } } // Switch the active chat tab to the source chat if preferred if(dAmn_TabLoggerPreferences.autoSwitch && (recv.args.from !== dAmn_Client_Username) && !isPchat) $('.tabbar').find('a:contains("' + ns.replace("chat:", "#") + '")').click(); } } } } }; dAmnChat.prototype.onData=dAmnChat_onData; dAmnChat.prototype.onData_alert=dAmnChat_onData_alert; }, // savePreferences // Arguments: none // Returns: void // Purpose: Constructs an object based on the preferences modal and stores in localStorage savePreferences: function() { var savedPreferences = { onAway: $('input.onAway').is(':checked'), onBack: $('input.onBack').is(':checked'), bindCommands: $('input.bindCommands').is(':checked'), tabNoise: $('input.tabNoise').is(':checked'), autoSwitch: $('input.autoSwitch').is(':checked'), whiteList: $('input.whiteList').is(':checked') }; localStorage.dAmn_TabLoggerPreferences = JSON.stringify(savedPreferences); $('.preferenceMessage').html('<b>Saved</b>'); this.getPreferences(); if(dAmn_TabLoggerPreferences.whiteList) this.bindWhiteList(); }, // playSound // Arguments: none // Returns: void // Purpose: Play the audio element playSound: function() { document.getElementById('dAmn_TabLoggerSound').play(); }, // getPreferences // Arguments: none // Returns: void // Purpose: Load existing preferences for future use. getPreferences: function() { dAmn_TabLoggerPreferences = JSON.parse(localStorage.dAmn_TabLoggerPreferences); }, // dependencies // Purpose: House all the filepaths of this script's dependencies dependencies: { js: [ '//code.jquery.com/ui/1.11.4/jquery-ui.js', ], css: [ '//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css', '//code.jquery.com/ui/1.9.2/themes/base/jquery-ui.css', ] }, // includeDependencies // Arguments: none // Returns: void // Purpose: Include all this script's dependencies includeDependencies: function() { var jsFiles = this.dependencies.js; var cssFiles = this.dependencies.css; $(cssFiles).each(function(i, ext) { $('<link/>').attr('href', ext).appendTo('head'); }); $(jsFiles).each(function(i, ext) { $('<script/>').attr('type', 'text/javascript').attr('src', ext).appendTo('body'); }); }, // bindWhiteList // Arguments: none // Returns: void // Purpose: Bind event on the correct DOM elements for whitelist management bindWhiteList: function() { // Add event listener to username spans to control whitelist if enabled $(document).click(function(event) { if(dAmn_TabLoggerPreferences.whiteList && $(event.target).hasClass('username')) { if(event.shiftKey && $(event.target).text() !== dAmn_Client_Username) { event.preventDefault(); event.stopPropagation(); var message, response, recipient, isInWhiteList, whiteList; whiteList = (localStorage.dAmn_TabLoggerWhiteList === "" ? [] : localStorage.dAmn_TabLoggerWhiteList.split(',')); recipient = $(event.target).text().toLowerCase(); isInWhiteList = $.inArray(recipient, whiteList) > -1; message = "Would you like to " + (isInWhiteList ? "remove" : "add") + " " + recipient + " " + (isInWhiteList ? "from" : "to") + " your dAmn Tab Logger White List?"; response = confirm(message); if(response) { if(!isInWhiteList) { whiteList.push(recipient); } else { whiteList.reverse(); whiteList.splice(recipient, 1); } dAmn_TabLogger.saveWhiteList(whiteList); } } } }); }, // saveWhiteList // Arguments: Object whitelist // Returns: void // Purpose: Save the passed in object as the new white list saveWhiteList: function(whitelist) { localStorage.dAmn_TabLoggerWhiteList = whitelist.join(','); this.notify("White List updated"); }, // init // Arguments: none // Returns: void // Purpose: Does everything. :D init: function() { // Create our "database" to store the on/off bit. localStorage.dAmn_TabLoggerStatus = ""; if(!localStorage.dAmn_TabLoggerPreferences) localStorage.dAmn_TabLoggerPreferences = JSON.stringify({onAway: false, onBack: false, bindCommands: false, tabNoise: false, autoSwitch: false, whiteList: false}); if(!localStorage.dAmn_TabLoggerWhiteList) localStorage.dAmn_TabLoggerWhiteList = ""; this.getPreferences(); // Bind commands if(dAmn_TabLoggerPreferences.bindCommands) this.bindCommands(); // Start listening for tabs this.bindTabs(); // Include the script dependencies this.includeDependencies(); // Initialize context menus if(dAmn_TabLoggerPreferences.whiteList) this.bindWhiteList(); // jQuery extension to re-center the modal $.fn.center = function () { $(this).css(dAmn_TabLogger.CSS.logModal); return $(this).fadeIn(); }; // jQuery extension to tell if the modal header text is fully visible on the screen $.fn.isFullyVisible = function() { var off, et, el, eh, ew, wh, ww, wx, wy; off = this.offset(); et = off.top; el = off.left; eh = this.height(); ew = this.width(); wh = window.innerHeight; ww = window.innerWidth; wx = window.pageXOffset; wy = window.pageYOffset; return (et >= wy && el >= wx && et + eh <= wh + wy && el + ew <= ww + wx); }; // Create div element that will serve as the header of the modal. // Append the header text and the X button to close the modal. var modalTitle = $('<div/>') .css('border-bottom','1px solid #000') .css('background', 'linear-gradient(to bottom, #9eb1a3, #607465)') .css('border-top-left-radius', '8px') .css('border-top-right-radius', '8px') .attr('id', 'dAmn_TabLoggerModalHeader') .append($('<div class="modalHeader" style="display: inline-block; margin: 10px; font-size: 20px;">dAmn Tab Logger</div>')) .append($('<span style="float: right; font-size: 25px; margin-right: 10px; cursor: pointer;" onclick="dAmn_TabLogger.hideLog()">×</span>')); // Create div element to display when there are no logs stored in the modal. var noLogs = $('<div/>') .addClass('no-logs') .html('<center>No logs!</center>') .css('font-style', 'italic'); // Create the modal div. // Append modalTitle and the div to house the actual log text. // Append credit div to the bottom of the modal. // Implement jQuery draggable. Only drag by the modal header, do not allow // the modal to let the page scroll, and reposition the entire modal // to the default position if any part of the modal header is off screen // when the modal is dropped by the user. var resultsModal = $('<div/>') .attr('id', 'dAmn_TabLoggerModal') .css(dAmn_TabLogger.CSS.logModal) .append(modalTitle) .append($('<div/>') .attr('id', 'dAmn_TabLoggerLogs') .css(dAmn_TabLogger.CSS.logContainer) .append(noLogs)) .append($('<sub/>') .attr('id', 'dAmn_TabLoggerInfo') .attr('style', 'color: #98a39f !important; float: left;') .html(dAmn_TabLogger.version + ' by <a href="http://miloticscale.deviantart.com">MiloticScale</a><span class="user-symbol senior"></span></a><br/>') .after($('<div style="clear:both;display:none;"></div>'))) .appendTo($('body')) .draggable({ scroll: false, handle: '#dAmn_TabLoggerModalHeader', stop: function() { if(!$('#dAmn_TabLoggerModalHeader .modalHeader').isFullyVisible()) $(this).center(); } }); // Create div element that will serve as a clear button. Add onclick event listener to call this.clearLog var clearButton = $('<div/>') .attr('id', 'dAmn_TabLoggerClear') .css(dAmn_TabLogger.CSS.clearButton) .html('clear') .click(function() { dAmn_TabLogger.clearLog(); }) .prependTo(resultsModal); // Create a link element and attach an onclick event listerner to it that shows the modal. // Ensure that all chat windows get the link via setInterval. // Pseudo-detection of /setaway appended var insertLink = setInterval(function() { $('.damncrc-iconbar-ctrls').each(function(i, el) { if($(el).find('.dAmn_TabLoggerLink').length < 1) { if(localStorage.dAmn_TabLogger == "on") { dAmn_TabLogger.stopLog(false); dAmn_TabLogger.startLog(false); } var loggerLink = $('<a/>') .addClass('dAmn_TabLoggerLink') .css('cursor', 'pointer') .html('Tab Log') .click(function() { dAmn_TabLogger.showLog(); }) .prependTo($(el)) .after(' <b>|</b> '); // Monitor the ctrls span and start logging on pseudo-detection of /setaway var awayTargets = document.querySelectorAll('.superdamn-awayspace'); var awayObserver = new MutationObserver(function(mutations) { $(mutations).each(function(i, mutation) { if(mutation.addedNodes.length > 0) { if(dAmn_TabLoggerPreferences.onAway) dAmn_TabLogger.startLog(true); } else { if(dAmn_TabLoggerPreferences.onBack) dAmn_TabLogger.stopLog(true); } }); }); $(awayTargets).each(function(i, target) { awayObserver.observe(target, {attributes: true, childList: true, characterData: true}); }); } }); }, 1000); // Create div element that will serve as the status icon in the modal. // Give it some text based on our bit in storage. // Attach onclick event listener to it which starts/stops logging // depending on our bit in storage. // Prepend this to the modal. var statusIcon = $('<div/>') .attr('id','dAmn_TabLoggerStatus') .css(dAmn_TabLogger.CSS.status) .css(localStorage.dAmn_TabLoggerStatus == "on" ? dAmn_TabLogger.CSS.onStatus : dAmn_TabLogger.CSS.offStatus) .html(localStorage.dAmn_TabLoggerStatus == "on" ? "on" : "off") .click(function() { if(localStorage.dAmn_TabLoggerStatus == "on") dAmn_TabLogger.stopLog(true); else dAmn_TabLogger.startLog(true); }) .prependTo(resultsModal); // Create div element that will serve as the preferences button in the modal. var preferencesButton = $('<div/>') .attr('id', 'dAmn_TabLoggerPreferencesButton') .css(dAmn_TabLogger.CSS.preferencesButton) .html('preferences') .click(function() { dAmn_TabLogger.showPreferences(); }) .prependTo(resultsModal); // Create preferences modal var preferencesModal = $('<div/>') .attr('id', 'dAmn_TabLoggerPreferencesModal') .css(dAmn_TabLogger.CSS.logModal) .css(dAmn_TabLogger.CSS.preferencesModal) .append(typeof MiddleMan !== "undefined" ? '<div class="preference"><input type="checkbox" class="onAway" /> Automatically start logging on /setaway</div><hr />' : '') .append(typeof MiddleMan !== "undefined" ? '<div class="preference"><input type="checkbox" class="onBack" /> Automatically stop logging on /setback</div><hr />' : '') .append('<div class="preference"><input type="checkbox" class="autoSwitch" /> Automatically switch to a chat I\'m tabbed in</div><hr />') .append('<div class="preference"><input type="checkbox" class="bindCommands" /> Enable chat commands</div><hr />') .append('<div class="preference"><input type="checkbox" class="whiteList" /> Enable white list</div><hr />') .append('<div class="preference"><input type="checkbox" class="tabNoise" /> Play sound when I\'m tabbed <br/><sub>(Volume: Shift + ↑/↓)</sub></div><hr/>') .append('<div style="float: left; border: 1px solid #000; border-radius: 5px; color: #fafffa; background: #00cc00; padding: 5px; cursor: pointer;" onclick="dAmn_TabLogger.savePreferences()">Save</div>') .append('<div style="display: inline-block; margin-left: 10px; border: 1px solid #000; border-radius: 5px; color: #000; background: #fafffa; padding: 5px; cursor: pointer;" onclick="dAmn_TabLogger.hidePreferences(); $(\'.preferenceMessage\').html(\'\');">Close</div>') .append('<div style="margin-left: 30px; display: inline-block" class="preferenceMessage"></div>') .appendTo(resultsModal); // Check the checkboxes based on current preferences var set = ['onAway', 'onBack', 'bindCommands', 'tabNoise', 'autoSwitch', 'whiteList']; $(set).each(function(i, s) { $('input.' + s).prop('checked', dAmn_TabLoggerPreferences[s]); }); // Create audio element to play when tabbed. $('<audio/>') .attr('id', 'dAmn_TabLoggerSound') .attr('src', 'http://jonbraswell.com/scripts/tabbed.wav') .attr('preload', 'auto') .appendTo($('body')); // Add event listener to the document to control the sound volume. $(document).on('keyup', function(event) { if(dAmn_TabLoggerPreferences.tabNoise) { var sound = document.getElementById('dAmn_TabLoggerSound'); var shift = event.shiftKey; var plus = (event.keyCode == '38'); var minus = (event.keyCode == '40'); if(shift && plus) { if(sound.volume < 1) { sound.volume += 0.1; dAmn_TabLogger.notify("Notification sound volume increased"); sound.play(); } else { dAmn_TabLogger.notify("Max notification sound volume reached"); } } if(shift && minus) { if(sound.volume > 0.1) { sound.volume -= 0.1; dAmn_TabLogger.notify("Notification sound volume decreased"); sound.play(); } else { dAmn_TabLogger.notify("Min notification sound volume reached"); } } } }); } }; DWait.ready(['jms/pages/chat07/chatpage.js', 'jms/pages/chat07/dAmn.js', 'jms/pages/chat07/dAmnChat.js'], function() { // Initialize! dAmn_TabLogger.init(); }); }).toString()); // Append our housing function to to the DOM as a child of the head element. script.appendChild(document.createTextNode(helperScript)); document.getElementsByTagName('head')[0].appendChild(script); // from electricnet's SuperdAmn. function freeFunctionString(str){ return str.replace(/^\s*function\s*\(\)\s*\{/, "").replace(/\}\s*$/, ""); }