NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Turkmaster (Mturk) // @namespace https://greasyfork.org/users/3408 // @author DonovanM // @description A page-monitoring web app for Mturk (Mechanical Turk) designed to make turking more efficient. Easily monitor mturk search pages and requesters and Auto-Accept the HITs you missed. // @include https://www.mturk.com/mturk/* // @version 1.4.1 // @require https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js // @require https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js // @grant GM_getValue // @grant GM_setValue // ==/UserScript== var settings = (function() { var LOCAL_STORAGE = "turkmaster_settings"; var pub = { sound : true, animation : true, preloadHits : false, volume : 50, notifications : true, alertOnly : false, fontSize : 10, typeface : "Oxygen", desktopNotifications : false, canHide : false, stopOnCaptcha : true } _load(); function _setFontSize(val) { if (val >= 5 && val <= 20) { pub.fontSize = val; $("#dispatcher div").css("font-size", val + "pt"); $(".notification_panel p").css("font-size", val + "pt"); $("#settingsDialog, #settingsDialog div, #settingsDialog li, #settingsDialog input, #settingsDialog button").css("font-size", val + "pt"); _save(); } } function _setDesktopNotifications(val, callback) { if (val) { requestDesktopNotifications(function(isPermitted) { callback(isPermitted); pub.desktopNotifications = isPermitted; _save(); }); pub.desktopNotifications = false; } else { pub.desktopNotifications = false; } _save(); } function _setVolume(val) { if (val >= 0 && val <= 100) { Sound.setVolume(val); pub.volume = val; _save(); } } function _save() { // localStorage.setItem(LOCAL_STORAGE, JSON.stringify(pub)); GM_setValue(LOCAL_STORAGE, JSON.stringify(pub)); } function _load() { var values = GM_getValue(LOCAL_STORAGE); if (typeof values === 'undefined') values = localStorage.getItem(LOCAL_STORAGE); if (values) { values = JSON.parse(values); for (i in values) pub[i] = values[i]; } } pub.setFontSize = _setFontSize; pub.setVolume = _setVolume; pub.setDesktopNotifications = _setDesktopNotifications; pub.save = _save; return pub; }()); var pageType = { MAIN : true, // This is so remote watcher requests don't add new watchers to multiple pages and cause mturk errors. DASHBOARD : false, HIT : false, REQUESTER : false, SEARCH : false }; var loadError = false; var wasViewed = false; var dispatch; var notificationPanel; if(!('contains' in String.prototype)) { String.prototype.contains = function(str, startIndex) { return -1 !== String.prototype.indexOf.call(this, str, startIndex); }; } $(document).ready(function(){ checkPageType(); loadFonts(); if (pageType.DASHBOARD) { dispatch = new Dispatch(); DispatchUI.create(dispatch); createDetailsPanel(); IgnoreList.init(); if (settings.preloadHits) loadDefaultWatchers(); else dispatch.load(); requestMain(); preloadImages(); addFormStyle(); } if (pageType.HIT || pageType.REQUESTER || pageType.SEARCH) addWatchButton(); notificationPanel = new NotificationPanel(); // Listen to messages window.addEventListener('storage', onStorageEvent, false); }); $(window).on('beforeunload', function() { if (pageType.DASHBOARD && pageType.MAIN) { dispatch.save(); } }); function loadFonts() { WebFont.load({ google: { families: [ 'Oxygen:400,700:latin', 'Droid Sans Mono' ] } }); } function onStorageEvent(event) { if (event.key.substring(0,13) === "notifier_msg_") onMessageReceived(event.key.substring(13), JSON.parse(event.newValue).content); } function checkPageType() { // Dashboard, hit, requester, search if (document.URL === "https://www.mturk.com/mturk/dashboard") pageType.DASHBOARD = true; else if (document.URL.match(/https:\/\/www.mturk.com\/mturk\/(preview|accept).+groupId=.*/) !== null) pageType.HIT = true; else if (document.URL.match(/requesterId=([A-Z0-9]+)/) !== null) pageType.REQUESTER = true; else if (document.URL.match(/(searchbar|findhits)/) !== null) pageType.SEARCH = true; } function requestMain() { sendMessage({ header: "request_main" }); } function preloadImages() { var images = [ 'https://i.imgur.com/guRzYEL.png', 'https://i.imgur.com/5snaSxU.png', 'https://i.imgur.com/VTHXHI4.png', 'https://i.imgur.com/peEhuHZ.png' ]; $(images).each(function(){ $('<img>')[0].src = this; }); } var SettingsDialog = function() { var DOMElement, widgetGroups = [ { id : 'soundSettings', text : 'Sound', type : 'toggle', setting : 'sound', items : [ { id : 'volume', text : 'Volume (0 - 100)', type : 'text', setting : 'volume', handler : function (target, value) { settings.setVolume(value); } } ] }, { id : 'notificationSettings', text : 'Notifications', type : 'toggle', setting : 'notifications', items : [ { id : 'desktopNotifications', text : 'Desktop Notifications', type : 'toggle', setting : 'desktopNotifications', handler : _handleDesktopNotificationToggle } ] }, { id : 'more', text : 'Captchas', items : [ { id : 'stopOnCaptcha', text : 'Stop on Captcha', type : 'toggle', setting : 'stopOnCaptcha' } ] }, { id : 'fontSettings', text : 'Font', items : [ { id : 'fontSize', text : 'Size (pt)', type : 'text', setting : 'fontSize', handler : function (target, value) { settings.setFontSize(value); } } ] }, { id : 'uiSettings', text : 'User Interface', items : [ { id : 'hideable', text : 'Hideable', type : 'toggle', setting : 'canHide', handler : function (target, value) { settings.canHide = value; setTimeout(function () { DispatchUI.setHide() }, 50); } } ] }, { id : 'backup', text : 'Backup', items : [ { type : 'more', id : 'export', text : 'Export', handler: _showExport }, { type : 'more', id : 'import', text : 'Import', handler: _showImport } ] } ]; function _show() { if (!DOMElement) _createDOMElement() _setCurrentSettings(widgetGroups); DOMElement.show(); $(window).on('click', _handleWindowClick); } function _isVisible() { if (DOMElement) return DOMElement.is(":visible"); else return false; } // Sets the widgets to reflect the current settings function _setCurrentSettings(list) { list.forEach(function (item) { if (item.type) _setSetting(item.id, item.type, item.setting); if (item.items) _setCurrentSettings(item.items); }) } function _setSetting(id, type, setting) { var widget = DOMElement.find("#" + id), value = settings[setting]; if (type === "toggle" && value) { widget.children(".on_off").addClass("on"); } else if (type === "text") { widget.children("input").val(value); } } function _cancel() { DOMElement.hide(); } function _createDOMElement() { _addStyle(); DOMElement = $('<div id="settingsDialog"><h2>Settings</h2></div>').append(_createGroups(widgetGroups)); _addHandlers(); $("body").append(DOMElement); } // Creates settings groups for each group item in groups function _createGroups(groups) { return groups.map(function (group) { return _createGroup(group); }); } function _addDefaultHandlers(items) { items.forEach(function (item) { if (item.type && !item.handler) item.handler = _setValue; if (item.items) _addDefaultHandlers(item.items); }) } function _setValue(target, value) { settings[this.setting] = value; } function _createGroup(params) { var widgetHTML = widgetHTML || "", group, list; if (params.type === 'toggle') widgetHTML = getToggleHTML(); group = $('<div id="' + params.id + '">' + widgetHTML + '<h3>' + params.text + '</h3><ul></ul></div>'); list = group.find('ul'); params.items.forEach(function (item) { list.append(_createItem(item)); }); return group; } function _createItem(params) { return (params.type === 'toggle') ? _createToggleItem(params.id, params.text) : (params.type === 'text') ? _createTextItem(params.id, params.text) : (params.type === 'more') ? _createMoreItem(params.id, params.text) : undefined; } function _createTextItem(id, text) { return $('<li id="' + id + '">' + text + '<input type="text" class="text" /></li>'); } function _createToggleItem(id, text) { return $('<li id="' + id + '">' + getToggleHTML() + text + '</li>'); } function _createMoreItem(id, text) { return $('<li id="' + id + '"><button class="more">...</button>' + text + '</li>'); } function getToggleHTML() { return '<button class="on_off"><span>ON</span><span>OFF</span></button>'; } function _addHandlers() { DOMElement.on('click', function(e) { if (e.target.tagName === "BUTTON" || e.target.parentNode.tagName === "BUTTON") _handleWidgetClick(e); }); DOMElement.on('change', _handleInputChange); _addDefaultHandlers(widgetGroups); } function _handleWindowClick(e) { var target = e.target; // Hides the settings dialog if user clicks outside of the dialog if (!DOMElement.is(target) && DOMElement.has(target).length === 0 && $("#settings img").get(0) !== target) { _cancel(); $(window).off('click', _handleWindowClick); } } function _handleInputChange(e) { var target = $(e.target), value = target.val(), id = target.parent().attr('id'); _setWidget(id, target, value); } function _handleWidgetClick(e) { e.preventDefault(); // Chrome returns the span as the target while FF returns the button var target = (e.target.tagName === "BUTTON") ? $(e.target) : $(e.target).parent(), value = target.hasClass("on"), id = target.parent().attr('id'); // Toggle widget unless it's for desktop notifications (because it's asynchronous) if (id !== "desktopNotifications") value = _toggle(target); _setWidget(id, target, value); } function _setWidget(id, target, value) { _findWidget(widgetGroups, id).handler(target, value); settings.save(); } function _toggle(element) { if (element.hasClass("on")) { element.removeClass("on"); return false; } else { element.addClass("on"); return true; } } // Recursively looks for a widget with specified id function _findWidget(widgets, id, i) { return widgets.reduce(function (prev, curr) { return (prev !== null) ? prev : (curr.id === id) ? curr : (curr.items) ? _findWidget(curr.items, id) : null; }, null); } function _handleDesktopNotificationToggle(target, value) { if (value) target.removeClass("on"); // Desktop notification requests require user action so we need a callback // for when the user responds. settings.setDesktopNotifications(!value, function(isPermitted) { if (isPermitted) { target.addClass("on"); } else { target.removeClass("on"); console.error("Desktop notifications are blocked."); } }); } function _showExport() { var div = $('<div id="export-box" class="dialog-big"><h2>Export Watchers</h2><h3>Copy the text below. (Triple-click to highlight all)</h3><p>' + dispatch.exportWatchers() + '</p></h2></div>'); $('<button>Close</button>') .click(function() { div.remove() }) .appendTo(div); div.appendTo($("body")); } function _showImport() { var div = $('<div id="import-box" class="dialog-big"><h2>Import Watchers</h2><h3>Paste the backup text to load watchers</h3><textarea></textarea></h2></div>'); $('<button>Save</button>') .click(function() { dispatch.importWatchers($("#import-box textarea").val()); div.remove() }) .appendTo(div); $('<button>Close</button>') .click(function() { div.remove() }) .appendTo(div); div.appendTo($("body")); div.find("textarea").focus(); } function _addStyle() { addStyle("\ #settingsDialog {\ position: fixed;\ top: 16px;\ left: 249px;\ background-color: #fafafa;\ padding: 10px;\ width: 300px;\ font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif;\ border-bottom: 1px solid #DDD;\ border-right: 1px solid #DDD;\ border-radius: 0.3em;\ color: #333;\ }\ #settingsDialog div, #settingsDialog li, #settingsDialog input, #settingsDialog button {\ font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif;\ }\ #settingsDialog > div {\ margin: 0px 0px 0.5em;\ border: 1px solid #eee;\ padding: 0.35em 0.60em 0.05em;\ background-color: #fff;\ }\ #settingsDialog h2, #settingsDialog h3 {\ margin: 0 0 0.5em;\ font-size: 95%;\ letter-spacing: 0.5px;\ }\ #settingsDialog h2 {\ font-weight: 400;\ text-align: center;\ font-size: 110%;\ color: #333;\ }\ #settingsDialog button.on_off {\ background: none;\ border: none;\ padding: 0;\ outline: none;\ height: 1.3em;\ margin-top: 0em;\ }\ #settingsDialog .on_off span { color: #333; margin: 1px; font-size: 56%; font-weight: bold; border-radius: 1.6em; }\ #settingsDialog .on_off span:nth-child(2) { background-color: #aeaeae; color: #fff; padding: 0.4em 0.8em; }\ #settingsDialog .on_off.on span:nth-child(1) { background-color: #55b8ea; color: #fff; padding: 0.4em 0.8em; }\ #settingsDialog .on_off.on span:nth-child(2) { background-color: inherit; color: #333; padding: 0 0.8em 0 0; }\ #settingsDialog .on_off { margin-top: 6px; }\ #settingsDialog ul { margin: 0 0 0.2em; padding: 0 0 0 1.9em }\ #settingsDialog ul li { list-style: none; margin-bottom: 0.5em; }\ #settingsDialog li input, #settingsDialog li button { float: right; }\ #settingsDialog li input[type='text'] { width: 3em; font-size: 80%; margin-right: 0.8em; text-align: right; padding-right: 0.5em }\ #settingsDialog li .more { width: 24px; border: none; color: #808080; font: bold 160% inital; line-height: 0%; transform: rotate(90deg); background-color: transparent; position: relative; top: 2px; cursor: pointer; height: 0.7em; padding: 0 0 0.65em; }\ #settingsDialog li#typeface input { width: 8em }\ .dialog-big { position: fixed; top: 2em; left: 50%; width: 860px; margin-left: -430px; background-color: white; padding: 2%; border: 1px solid #ddd; font-family: 'Oxygen'; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.4); border-radius: 10px; }\ .dialog-big p { background-color: #f7f7f7; padding: 1.5em; height: 500px; overflow: scroll; }\ .dialog-big h2, #export-box h3 { font-weight: 400 }\ .dialog-big h2 { font-size: 170%; margin-top: 0 }\ .dialog-big button { background-color: #cecece; color: white; padding: 3px 10px; font-family: 'Oxygen'; border: none; border-radius: 3px; font-weight: bold; transition: background-color 0.3s; margin-right: 0.5em }\ .dialog-big button:hover { background-color: #55B8EA }\ .dialog-big textarea { display: block; width: 100%; height: 500px; margin: 1em 1em 1em 0; }\ "); } return { show: _show, hide: _cancel, isVisible: _isVisible } }(); function addWatchButton() { var type = (pageType.HIT) ? 'hit' : (pageType.REQUESTER) ? 'requester' : (pageType.SEARCH) ? 'page' : ''; var button = $("<div>").addClass("watcher_button") .append($("<a>") .text("Watch this " + type + "?") .attr('href', "javascript:void(0)") .click(addWatcher) ); function addWatcher() { // Get current and default values var time = 60, auto = true, alert = false, name = "", stopOnCatch = true; // Find the name if available if (pageType.REQUESTER) { if ($(".title_orange_text_bold").length > 0) { name = $(".title_orange_text_bold").text().match(/Created by '(.+)'/); name = (typeof name !== 'undefined') ? name[1] : ""; } else if (document.URL.match(/prevRequester=/)) { name = document.URL.match(/prevRequester=([^&]*)/)[1]; } } else if (pageType.SEARCH) { name = document.URL.match(/searchWords=([^&]*)/); if (name !== null) { name = name[1].replace('+', ' '); name = name.charAt(0).toUpperCase() + name.slice(1); // Capitalize first letter } else { name = ""; } } else if (pageType.HIT) { name = $(".capsulelink_bold > div:nth-child(1)").text().trim(); } // Pull up a Watcher Dialog with default values set watcherDialog( { name: name, time: time * 1000, type: type, option: { auto : auto, alert : alert, stopOnCatch : stopOnCatch } }, function(values) { var id = (document.URL.match(/groupId=([A-Z0-9]+)/) || document.URL.match(/requesterId=([A-Z0-9]+)/) || [,document.URL])[1], watcher = { id : id, duration : values.time, type : (type === "page") ? "url" : type, name : values.name, auto : values.auto, alert : values.alert, stopOnCatch : values.stopOnCatch, muteBatch : values.muteBatch }; sendMessage({ header : 'add_watcher', content : watcher, timestamp : true }); } ); } var location; // Location to add the watch button if (pageType.HIT) { if ($(".message.success h6").length) location = $(".message.success h6"); else if ($("#javascriptDependentFunctionality").length) location = $("#javascriptDependentFunctionality"); else if ($("body > form:nth-child(7) > table:nth-child(9) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(1)").length) location = $("body > form:nth-child(7) > table:nth-child(9) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(1)"); } else if (pageType.REQUESTER || pageType.SEARCH) { if ($(".title_orange_text_bold").length) location = $(".title_orange_text_bold"); else location = $(".error_title"); } location.append(button); addFormStyle(); } function addFormStyle() { addStyle("\ #add_watcher_form {\ position: fixed;\ width: 600px;\ top: 50px;\ left: 50%;\ margin: 50px -300px;\ background-color: #fcfcfe;\ border: 1px solid #aaa;\ border-radius: 1px;\ text-align: center;\ }\ #add_watcher_form h3 {\ font: 12pt Verdana;\ margin: 0 0 15px;\ background-color: #def;\ background-color: rgba(230, 230, 230, 1);\ padding: 3px;\ color: #111;\ }\ #add_watcher_form input[type='text'] {\ font: 10pt Verdana;\ margin: 10px 20px 0 0;\ }\ #add_watcher_form input[type='button'] {\ margin-top: 20px;\ font: 9pt Verdana;\ color: #444;\ background-color: #eee;\ border: 1px solid #999;\ }\ #add_watcher_form input[type='button']:hover {\ background-color: #9df;\ }\ #add_watcher_form p {\ margin: 10px;\ font: 11pt Verdana;\ }\ #add_watcher_form .form_buttons input {\ margin: 5px;\ }\ .watcher_button { display: inline; }\ .watcher_button a {\ text-decoration: none;\ font-weight: normal;\ background-color: #CECECE;\ color: white;\ padding: 3px 10px;\ border-radius: 10px;\ font-family: 'Oxygen', verdana, sans-serif;\ transition: background-color 0.4s;\ }\ .watcher_button a:hover { background-color: #55B8EA }\ .error_title .watcher_button { display: block; margin: 15px }\ "); } function addStyle(styleText) { var style = '<style type="text/css">' + styleText + '</style>'; $("head").append(style); } function loadDefaultWatchers() { // Add a few watchers. Won't be done like this in the future dispatch.isLoading = true; dispatch.add(new Watcher({ id: "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&searchWords=survey&minReward=0.25&qualifiedFor=on&x=13&y=10", time: 60000, type: 'url', name: "Surveys $0.25 and up"})); //$.25 surveys dispatch.add(new Watcher({ id: "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&searchWords=survey&minReward=0.75&qualifiedFor=on&x=13&y=10", time: 60000, type: 'url', name: "Surveys $0.75 and up"})); //$.75 surveys dispatch.add(new Watcher({ id: "A11L036EBWKONR", time: 120000, type: 'requester', name: "Project Endor", option: {alert:true}})); // Endor dispatch.add(new Watcher({ id: "A6YG5FKV2TAVC", time: 300000, type: 'requester', name: "Agent Agent", option: {alert:true}})); // Agent Agent dispatch.add(new Watcher({ id: "AKEBQYX32KM19", time: 120000, type: 'requester', name: 'Crowdsurf Support'})); dispatch.add(new Watcher({ id: "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&searchWords=transcri&minReward=0.00&qualifiedFor=on&x=0&y=0", time: 60000, type: 'url', name: "Transcription HITs"})); // Transcription HITs dispatch.isLoading = false; dispatch.save(); } function onMessageReceived(header, message) { if (pageType.DASHBOARD && pageType.MAIN) { switch(header) { case 'notification_viewed' : wasViewed = true; break; case 'add_watcher' : var msg = message; dispatch.add(new Watcher({ id : msg.id, time : msg.duration, type : msg.type, name : msg.name, option : { auto: msg.auto, stopOnCatch: msg.stopOnCatch, alert: msg.alert } })).start(); break; case 'ignore_requester' : IgnoreList.add(IgnoreList.REQUESTER, message.id); break; case 'mute_hit' : var id = message.id; IgnoreList.add(IgnoreList.HIT, id); break; case 'unmute_hit' : var id = message.id; IgnoreList.remove(IgnoreList.HIT, id); break; case 'request_main' : sendMessage({ header: "request_denied" }); break; case 'request_denied' : dispatch.onRequestMainDenied(); break; case 'show_main' : alert("Showing the main dashboard. (Close this Mturk page to establish a notifier in a different tab or window)"); break; } } else if (!pageType.DASHBOARD || (pageType.DASHBOARD && !pageType.MAIN)) { switch(header) { case 'new_hits' : var hits = message.hits; // Re-create the hits so their methods can be used for(var i = hits.length; i--;) hits[i] = new Hit(hits[i]); // Show the hits and let the dashboard know it was seen if (document.hasFocus()) sendMessage({ header: "notification_viewed" }); notificationPanel.add(new NotificationGroup({ title: message.title, hits: hits, url: message.url })); break; case 'captcha' : if (document.hasFocus()) alert("Captcha Alert!"); break; case 'turkopticon' : // This needs a more elegant solution. If the servers start lagging we might be // using addTO() for the wrong group. It won't show the TO for the wrong requester, // though, so it's safe to use for now. It's just that some ratings could be missing. notificationPanel.notifications[notificationPanel.notifications.length - 1].addTO(message); break; } } } function sendMessage(message) { var header = message.header; var content = message.content || new Date().getTime(); // Make the content a timestamp when there's no actual content var timestamp = message.timestamp && new Date().getTime(); // If wanted, adds a timestamp to the content so messages with the same content will still trigger the event consecutively localStorage.setItem('notifier_msg_' + header, JSON.stringify({ content: content, timestamp: timestamp})); } function sendDesktopNotification(hits, watcher) { // Let's check if the user is okay to get some notification if (Notification.permission === "granted" && settings.desktopNotifications) { // If the user isn't on a mturk page to receive a rich notification, then send a web notification if (!wasViewed) { var bodyText = ""; for (var i = 0, len = hits.length; i < len; i++) bodyText += "\n" + hits[i].title.substring(0, 40) + ((hits[i].title.length > 40) ? "..." : "") + "\n" + hits[i].reward + "\n"; var notification = new Notification( watcher.name, { body: bodyText, icon: "http://halfelf.org/wp-content/uploads/sites/2/2012/06/amazon_icon.png" } ); notification.onclick = function() { window.focus(); // Focus this window (dashboard) this.close(); // Closes the notification showDetailsPanel(watcher); // Opens the details panel for whatever watcher the notification was for }; notification.onshow = function() { setTimeout(function() { notification.close() }, 5000) }; // Need to set a close time for Chrome } } } function requestDesktopNotifications(callback) { // Let's check if the browser supports notifications if (!("Notification" in window)) { alert("This browser does not support desktop notification"); } else { window.Notification.requestPermission(function (permission) { // Whatever the user answers, we make sure Chrome stores the information if(!('permission' in Notification)) window.Notification.permission = permission; // If the user is okay, let's create a notification if (permission === "granted") { var notification = new window.Notification("Desktop notifications enabled."); notification.onshow = function() { setTimeout(function() { notification.close() }, 5000) }; callback(true); } else { callback(false); } }); } } function Hit(attrs) { attrs = attrs || {}; this.id = attrs.id; this.uid = attrs.uid; this.isAutoAccept = attrs.isAutoAccept || false; this.requester = attrs.requester; this.requesterID = attrs.requesterID; this.url = attrs.url; this.title = attrs.title; this.reward = attrs.reward; this.description = attrs.description; this.available = attrs.available; this.time = attrs.time; this.isQualified = (typeof attrs.isQualified !== 'undefined') ? attrs.isQualified : true; this.canPreview = (typeof attrs.canPreview !== 'undefined') ? attrs.canPreview : true; } Hit.prototype.getURL = function(type) { switch(type) { case 'preview': return "https://www.mturk.com/mturk/preview?groupId=" + this.id; case 'accept' : return (this.isQualified) ? "https://www.mturk.com/mturk/previewandaccept?groupId=" + this.id : null; case 'auto' : return "https://www.mturk.com/mturk/previewandaccept?groupId=" + this.id + "&autoAcceptEnabled=true"; case 'view' : return "https://www.mturk.com/mturk/continue?hitId=" + this.uid; case 'return' : // This will need to be changed. It's the same as 'view' until more testing is done on AMT's return functionality return "https://www.mturk.com/mturk/preview?hitId=" + this.uid; default: return ""; } }; // Returns the position of a hit in a hit array by its ID Hit.indexOf = function(hitId, hits) { for (var i = 0, len = hits.length; i < len; i++) { if (hitId === hits[i].id) return i; } return -1; }; // Returns true if there are multiple hits in the array and all of the hits are from the same requester Hit.isSameRequester = function(hits) { if (hits.length > 1) { var compareRequester = hits[0].requester; for (var i = 1, len = hits.length; i < len; i++) { if (compareRequester !== hits[i].requester) return false; } return true; } else { return false; } }; // Returns a list of unique requester IDs from an array of hits Hit.getUniqueReqeusters = function(hits) { var ids = []; for (var i = 0, len = hits.length; i < len; ++i) { var id = hits[i].requesterID; if (ids.indexOf(id) === -1) ids.push(id) } return ids; } // Message object (Not used) function Message() { /* Status (changed): Unchanged, Added, Removed, Count We should mark each Hit in the message with what has changed. The count change should be sent with this. The message will also tell the client whether or not to pop-up the notification. */ } // The details panel for each watcher function createDetailsPanel() { var div = $('<div>').attr('id', 'details_panel').addClass('notification_panel'); addStyle("#details_panel {\ background-color: #fff;\ position: absolute;\ top: 0px;\ margin-left: 1px;\ left: 270px;\ width: 500px;\ border: 1px solid #e3e3e3;\ border-radius: 0 0 3px 0;\ border-width: 0 1px 1px 0;\ transition: left 0.5s ease;\ display: none }\ #details_panel h4 { display: none }\ #details_panel.left { left: 30px }"); $(div).mouseleave(function() { $(this).hide() }); $("body").append(div); } var lastWatcher = ""; function showDetailsPanel(watcher) { var panel = $("#details_panel"); var group; // Only change the panel contents if it's a different watcher or the same one, but updated if (watcher !== lastWatcher || (watcher === lastWatcher && watcher.isUpdated)) { $("*", panel).remove(); if (watcher.lastHits.length > 0) { group = new NotificationGroup({ hits: watcher.lastHits, isSticky: false, watcher: watcher }); $(panel).append((group).getDOMElement()); // This doesn't need a callback since the data will already be cached at this point group.addTO(TO.get(Hit.getUniqueReqeusters(watcher.lastHits), _handleTOReceived)); } else { $(panel).append($('<div>').append('<h2>').css('text-align', 'center').html("<br />There are no HITs available.<br /><br />")); } } $(panel).show(); function _handleTOReceived(data) { group.addTO(data); } lastWatcher = watcher; } var IgnoreList = (function() { var _time = 60000, _hits = []; _requesters = [], _HIT = 0, _REQUESTER = 1; function _init() { // _clear(); _load(); _addListeners(); } function _addListeners() { $(window).on('unload', function() { _save(); }) } function _save() { localStorage.setItem('notifier_ignore', JSON.stringify(_hits)); localStorage.setItem('notifier_ignore_requesters', JSON.stringify(_requesters)); } function _load() { var storedHits = localStorage.getItem('notifier_ignore'); var storedRequesters = localStorage.getItem('notifier_ignore_requesters'); if (storedHits !== null) { try { _hits = JSON.parse(storedHits); } catch (e) { _clear(); _save(); console.error("Ignored hits couldn't be loaded correctly."); } } else { console.log("No ignored hits found"); } if (storedRequesters !== null) { try { _requesters = JSON.parse(storedRequesters); } catch (e) { _clear(); _save(); console.error("Ignored requesters couldn't be loaded correctly."); } } else { console.log("No ignored requesters found"); } } function _clear() { _hits = []; _requesters = []; localStorage.removeItem('notifier_ignore'); localStorage.removeItem('notifier_ignore_requesters'); } function _contains(type, item) { if (type === _HIT) return (_hits.indexOf(item) !== -1); else return (_requesters.indexOf(item) !== -1); } function _isIgnored(requester) { return (_requesters.indexOf(requester) !== -1); } function _isMuted(item) { return (_hits.indexOf(item) !== -1); } function _filter(hits) { var filteredHits = []; for (var i = 0, len = hits.length; i < len; i++) { var hit = hits[i]; if ((_hits.indexOf(hit.id) === -1) && (_requesters.indexOf(hit.requester) === -1)) filteredHits.push(hit); } return filteredHits; } function _add(type, id) { if (type === _HIT) { if (_hits.indexOf(id) === -1) _hits.push(id); } else if (type === _REQUESTER) { if (_requesters.indexOf(id) === -1) _requesters.push(id); } _save(); } function _remove(type, id) { if (type === _HIT) { var index = _hits.indexOf(id); if (index !== -1) _hits.splice(index, 1); } else if (type === _REQUESTER) { var index = _requesters.indexOf(id); if (index !== -1) _requesters.splice(index, 1); } _save(); } return { init: _init, add: _add, remove: _remove, filter: _filter, isMuted: _isMuted, isIgnored: _isIgnored, HIT: _HIT, REQUESTER: _REQUESTER } })(); function Evt() { /* Nothing */ }; Evt.ADD = 1; Evt.REMOVE = 2; Evt.START = 3; Evt.STOP = 4; Evt.CHANGE = 5; Evt.UPDATE = 6; Evt.HITS_CHANGE = 7; Evt.DELETE = 8; Evt.VIEW_DETAILS = 9; Evt.prototype.addListener = function(type, callback) { switch(type) { case Evt.ADD: this.listener.onadd.push(callback); break; case Evt.REMOVE: this.listener.onremove.push(callback); break; case Evt.START: this.listener.onstart.push(callback); break; case Evt.STOP: this.listener.onstop.push(callback); break; case Evt.CHANGE: this.listener.onchange.push(callback); break; case Evt.UPDATE: this.listener.onupdate.push(callback); break; case Evt.HITS_CHANGE: this.listener.onhitschange.push(callback); break; case Evt.DELETE: this.listener.ondelete.push(callback); break; case Evt.VIEW_DETAILS: this.listener.onviewdetails.push(callback); break; default: console.error("Invalid Event type in addListener()"); } } Evt.prototype.notify = function(type, data) { switch(type) { case Evt.ADD: this.callFunctionArray(this.listener.onadd, data); break; case Evt.REMOVE: this.callFunctionArray(this.listener.onremove, data); break; case Evt.START: this.callFunctionArray(this.listener.onstart, data); break; case Evt.STOP: this.callFunctionArray(this.listener.onstop, data); break; case Evt.CHANGE: this.callFunctionArray(this.listener.onchange, data); break; case Evt.UPDATE: this.callFunctionArray(this.listener.onupdate, data); break; case Evt.HITS_CHANGE: this.callFunctionArray(this.listener.onhitschange, data); break; case Evt.DELETE: this.callFunctionArray(this.listener.ondelete, data); break; case Evt.VIEW_DETAILS: this.callFunctionArray(this.listener.onviewdetails, data); break; default: console.error("Unknown event type:", type); } } Evt.prototype.callFunctionArray = function(functions, data) { if (functions.length > 0) for (var i = 0, len = functions.length; i < len; i++) functions[i](data); } var DispatchUI = { create: function(dispatch) { DispatchUI.dispatch = dispatch; DispatchUI.init(); DispatchUI.addStyle(); DispatchUI.addActions(); DispatchUI.addListeners(); DispatchUI.addDragAndDrop(); return DispatchUI.div; }, init: function() { var div = DispatchUI.div = $('<div id="dispatcher"><div id="controller"></div><div id="watcher_container"></div></div>'); DispatchUI.watchers = []; // Move dashboard contents to the right and put the dispatch panel on the left var pageElements = $("body > *"); $("body").html(""); $("body").append( $("<div>") .attr('id', "content_container") .append($(pageElements)) ); $("body").css('margin', "0").prepend(div); var ctrl = DispatchUI.ctrl = $("#controller", div); var settingsBtn = $('<a id="settings" href="javascript:void(0)" title="Setting"><img /></a') .click(function() { if (!SettingsDialog.isVisible()) SettingsDialog.show(); else SettingsDialog.hide(); }); ctrl.append( settingsBtn, '<div class="play_container">\ <div class="play_all" title="Start All"></div>\ <div class="play selected" title="Start Selected"></div>\ <div class="pause" title="Pause All"></div>\ </div>', "Turkmaster" ); // Adding the data URL inline wouldn't work for some reason, so I'm doing it this way. // Image from http://latierrasenosestrecha.org/wp-content/themes/purity/img/icons/settings.png $("img", settingsBtn)[0].src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpFRUQ3N0Q2NkUyQjJFMDExOTM4OUZBRkY5RUM4NjkxMiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFQjNFQjA2OEIyRTYxMUUwOUZDRUUxRERBNzIzQkY1NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFQjNFQjA2N0IyRTYxMUUwOUZDRUUxRERBNzIzQkY1NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFRkQ3N0Q2NkUyQjJFMDExOTM4OUZBRkY5RUM4NjkxMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFRUQ3N0Q2NkUyQjJFMDExOTM4OUZBRkY5RUM4NjkxMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PiP+YRgAAALDSURBVHja7FdPZB1BGJ9tawmt8HinrUQpjdBqqFaq5BChJBI5xCvVQ1mPRq89hGqkeusxpLpKDtFTqpEorUZKKdFcWo2WR7REwyOE8niUeP19/Jbxmd3NbjXv0Aw/szO733z/ft/MrNdqtUw72zHT5tZ2A04UEYqiaBTdPFAC9oDb1Wp15TAjMEflhv3cP4kAPC2j6wK24OEv61WgPg2U3El054BtyO2m6fCSqgCLDKBbBjqBHWAQ+A3cAmYcItPAAp/XgDOAGD0GI97nMgDKRel3K8yGuRbP/BSHxMCGkhMjemBEPQ8HetUica79jJT6Djlx5nxeEn4GagnvNiSswCl45UnP8UbC99vAehEOSA4/AmUrvPeBF8A9YJgEFQVvgMdABXhgRUoIeBWGbhUtw07rOWRkvgB3qNywrwKfGIVJJb9fdB+oWJ6Ih8LkRRLR1WT+JfAO+GBxonLgMkTYx9E9cxBpBLgO3OW4xt1vHTJDlIkj8oQGPFdrSBWFkFlKi8CsQ7khicat8YQolwf0qyrso4yWcVTRbFYKAofgPpTsWWRsYrzJiL2yKiNuZZLP1YIsA1ybxXEoKlmLdmB8kd6PcK7f+n7XMtZkra/PgpAHS5eaFwUrZL+0RRgRMtfD6jASwg4k7AeTefaBGda0NMnzTeBrincx0XpYDdc49xCRmi5ShpHkm89D9PRGSn5lfoJkjZU3uU6hfcDnwRK3p0zFBS5at/Iacb5fMb2Rsm8kG4Dwd6B7q8ItBj3iES0p6eNZ0Mfxa773VUWs8XTNdSERlp9NeHeZO6IYepBLT0CZ1Twp+EZCaYI1M5Q1HXJyH9jMlQJev8YoLO0HcAXoBqYS1pri+0v83lg3onru45ghLvFqVcMiDWu+5TDaUxzq/as7YcZl9afaVneg6PRhXstDVYZh0Wu5d/Rv+N8b8EeAAQBg+uBx8hdn9gAAAABJRU5ErkJggg=="; }, addActions: function() { var dispatch = DispatchUI.dispatch, ctrl = DispatchUI.div.find("#controller"); $("div.play_all", ctrl).mousedown(function() { dispatch.start(true); }); $("div.play", ctrl).mousedown(function() { dispatch.start(); }); $("div.pause", ctrl).mousedown(function() { dispatch.stop(); }); }, addListeners: function() { var dispatch = DispatchUI.dispatch; var div = DispatchUI.div; dispatch.addListener(Evt.ADD, function(watcher) { // This could be done on one line, but then we would lose access to the WatcherUI's internal Watcher object and functionality var watcher = WatcherUI.create(watcher); $("#watcher_container", DispatchUI.div).append(watcher.element); DispatchUI.watchers.push(watcher); // watchers.push(WatcherUI.create(watcher).appendTo($("#watcher_container", div))); }); dispatch.addListener(Evt.REMOVE, function(watcher) { // Remove watcher from array var index = -1, watchers = DispatchUI.watchers; for (var i = 0, len = watchers.length; i < len; i++) { if (watchers[i].watcher === watcher) { index = i; break; } } if (index !== -1) DispatchUI.watchers.splice(index, 1); }); DispatchUI.setHide = function() { if (settings.canHide) { $(window).on('click', handleWindowClick); } else { $(window).off('click', handleWindowClick); } } DispatchUI.setHide(); function handleWindowClick(e) { if (!div.is(e.target) && div.has(e.target).length === 0 && $(".notification_panel").has(e.target).length === 0 && !$("#settingsDialog").is(e.target) && $("#settingsDialog").has(e.target).length === 0) { hide(); $(window).off('click', handleWindowClick); $(div).on('click', handleDivClick); } } function handleDivClick(e) { show(); $(div).off('click', handleDivClick); if (settings.canHide) $(window).on('click', handleWindowClick); } function hide() { div.addClass("tm-hidden"); $("#content_container").addClass("full"); $("#details_panel").addClass("left"); } function show() { div.removeClass("tm-hidden"); $("#content_container").removeClass("full"); $("#details_panel").removeClass("left"); } }, addStyle: function() { addStyle("#dispatcher { background-color: #f5f5f5; position: fixed; top: 0px; float: left; left: 0; height: 100%; width: 270px; font-size: 8pt; margin-left: 0px; transition: left 0.5s ease; }\ #dispatcher.tm-hidden { left: -240px }\ #content_container { position: absolute; left: 270px; top: 0; right: 0; border-left: 2px solid #dadada; transition: left 0.5s ease; }\ #content_container.full { left: 30px }\ #dispatcher #controller { text-align: center; font: 160% Candara, sans-serif; color: #585858; position: relative; padding: 3px 5px; }\ #dispatcher #controller .on_off { margin: 6px 5px 0 0 }\ #dispatcher #controller .on_off a { font-size: 80% }\ #dispatcher #controller #settings { top: 2px; position: absolute; right: 5px; }\ #dispatcher #controller #settings img { width: 1.5em }\ #dispatcher #controller .play_container { position: absolute; left: 5px }\ #dispatcher #watcher_container { position: absolute; top: 30px; bottom: 0; overflow-y:auto; width: 100%;}\ #dispatcher #watcher_container p { margin: 30px 0px }\ #dispatcher #watcher_container .error_button a { text-decoration: none; color: #555; background-color: #fff; padding: 3px 10px; margin: 5px; border: 1px solid #aaa; border-radius: 2px }\ #dispatcher #watcher_container .error_button a:hover { background-color: #def; border-color: #aaa }\ #dispatcher div { font-size: 7pt }\ #dispatcher .watcher {\ box-sizing: border-box;\ margin: 3px 3px 0;\ background-color: #fff;\ position: relative;\ border-bottom: 1px solid #ddd;\ border-right: 1px solid #ddd;\ top: 0;\ transition: background-color 0.5s, top 0.1s;\ -moz-user-select: none;\ -webkit-touch-callout: none;\ -webkit-user-select: none;\ -khtml-user-select: none;\ }\ #dispatcher .watcher.dragging { cursor: grabbing; z-index: 100; opacity: 0.8; transition: background-color 0.5s, top 0s }\ #dispatcher .watcher div { font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif }\ #dispatcher .watcher.running .details { background-color: #BADFF2 }\ #dispatcher .watcher.updated { background-color: rgba(218, 240, 251, 1); }\ #dispatcher .watcher .details { width: 25px; text-align: center; float: right; background-color: rgba(234, 234, 234, 1); position: absolute; top: 0; bottom: 0; right: 0; font-size: 90%; color: #fff; transition: background-color 0.5s }\ #dispatcher .watcher .details.updated { background-color: rgba(218, 240, 251, 1); background-color: #F0FF60; }\ #dispatcher .watcher .name { font-size 130%; color: black; text-decoration: none; display: inline-block; margin-top: -3px}\ #dispatcher .watcher .name:hover { text-decoration: underline }\ #dispatcher .watcher.dragging .name:hover { text-decoration: none }\ #dispatcher .watcher .time { display: block; float: left; font-size: 80% }\ .on_off { float: right; cursor: pointer }\ .on_off a { color: #333; margin: 1px; font-size: 56%; font-weight: bold }\ .on_off a:nth-child(2) { background-color: #aeaeae; color: #fff; border-radius: 12px; padding: 3px 6px; }\ .on_off.on a:nth-child(1) { background-color: #55b8ea; color: #fff; border-radius: 12px; padding: 3px 6px; }\ .on_off.on a:nth-child(2) { background-color: inherit; color: #333; border-radius: inherit; padding: inherit; }\ .watcher .on_off { }\ #dispatcher .watcher > .content { margin-right: 25px; padding: 5px 5px 5px 33px;}\ #dispatcher .watcher .bottom { margin: 0 0 -5px; color: #aaa }\ #dispatcher .watcher .bottom a:link { color: black; }\ #dispatcher .watcher .bottom a:hover { color: #cef; }\ #dispatcher .watcher .details { font-size: 150%; font-weight: bold }\ #dispatcher .watcher .last_updated { position: absolute; right: 30px; bottom: 4px; font-size: 80% }\ #dispatcher .watcher .icons { visibility: hidden; margin-left: 10px; bottom: 5px }\ #dispatcher .watcher:hover .icons { visibility: visible }\ #dispatcher .watcher .icons img { opacity: 0.2; height: 0.9em }\ #dispatcher .watcher .icons img:hover { opacity: 1 }\ #dispatcher .watcher .color_code { position: absolute; left: 0; top: 0; bottom: 0; width: 9px; cursor: grab;}\ #dispatcher .watcher .color_code div { position: absolute; left: 0; top: 0; bottom: 0; width: 5px; transition: width 0.15s; }\ #dispatcher .watcher.dragging .color_code { cursor: grabbing; }\ #dispatcher .watcher .color_code:hover div { width: 9px }\ #dispatcher .watcher .color_code.hit div { background-color: rgba(234, 111, 111, .7); }\ #dispatcher .watcher .color_code.requester div { background-color: rgba(51, 147, 255, .7); }\ #dispatcher .watcher .color_code.url div { background-color: rgba(57, 221, 122, .7); }\ .watcher .play_container {\ padding: 0px 0px 0px 12px;\ float: left;\ cursor: default;\ }\ .play, .pause, .play_all {\ width:20px;\ height: 20px;\ position: relative;\ display: block;\ }\ #controller .play, #controller .pause, #controller .play_all { float: left; }\ .play:before, .play_all:before {\ width: 0;\ height: 0;\ border-width: 8px 11px;\ border-style: solid;\ border-color: transparent transparent transparent #747474;\ position: absolute;\ content: '';\ top: 3px;\ left: 0px;\ }\ .play.selected:after {\ width: 6px;\ height: 6px;\ border-radius: 1px;\ position: absolute;\ content: '';\ background-color: #999;\ top: 13px;\ right: 9px;\ }\ .play_all:after {\ width: 0;\ height: 0;\ border-width: 8px 11px;\ border-style: solid;\ border-color: transparent transparent transparent #999;\ position: absolute;\ content: '';\ top: 3px;\ left: 5px;\ }\ .watcher.running .play:before, .watcher.running .play:after, .pause:before, .pause:after {\ width: 4px;\ height: 15px;\ background: #747474;\ position: absolute;\ content: '';\ top: 3px;\ }\ .watcher.running .play:before, .pause:before {\ left: 0px;\ border: none;\ }\ .watcher.running .play:after, .pause:after {\ left: 6px;\ }\ .play_select {\ width: 6px;\ height: 6px;\ border: 2px solid #cecece;\ border-radius: 2px;\ margin-top: 2px;\ }\ .watcher.selected .play_select { background-color: #55b8ea; border-color: #b4e6ff; }\ "); }, addDragAndDrop: function() { // Drag watchers var startY, currentBaseY, max, min, height, dragDiv, nextDiv, prevDiv, startPos, endPos, isDragging, slop = 7, currentWatcher, watchers = DispatchUI.watchers; DispatchUI.div.on("mousedown", ".watcher", function(e) { isDragging = false; // Get the position of the watcher in the listing startPos = endPos = $("#watcher_container .watcher").index(e.currentTarget); // Get reference to the selected watcher currentWatcher = watchers[startPos]; dragDiv = currentWatcher.element; nextDiv = dragDiv.next(); prevDiv = dragDiv.prev(); // TODO Check target to prevent dragging from a component inside the watcher (i.e. buttons, links, etc.) height = dragDiv.outerHeight(true); startY = e.clientY; currentBaseY = dragDiv.offset().top; // max = Math.min($("#watcher_container").outerHeight(true), height * (DispatchUI.dispatch.watchers.length + .75)) - height; min = watchers[0].element.offset().top; max = Math.min($("#watcher_container").outerHeight(true), height * (watchers.length - 1) + watchers[0].element.offset().top); $(window).on("mousemove", move); $(window).on("mouseup", up); }); function move(e) { var offsetY = e.clientY - startY; if (!isDragging && (Math.abs(offsetY) > slop)) { // Start dragging isDragging = true; dragDiv.addClass("dragging"); } if (isDragging) { dragDiv.css('top', offsetY); var diffY = dragDiv.offset().top - currentBaseY; if (dragDiv.offset().top > max) { dragDiv.offset({ top: max }); diffY = 0; } else if (dragDiv.offset().top < min) { dragDiv.offset({ top: min }); diffY = 0; } if (diffY > height / 2) { // Move down one spot nextDiv.offset({ 'top': nextDiv.offset().top - height }); nextDiv = nextDiv.nextAll(":not(.dragging)").first(); prevDiv = (prevDiv.length) ? prevDiv.nextAll(":not(.dragging)").first() : (dragDiv !== watchers[0].element) ? watchers[0].element : watchers[1].element; currentBaseY += height; endPos++; } else if (-diffY > height / 2) { // Move up one spot prevDiv.offset({ 'top': prevDiv.offset().top + height }); prevDiv = prevDiv.prevAll(":not(.dragging)").first(); nextDiv = (nextDiv.length) ? nextDiv.prevAll(":not(.dragging)").first() : (dragDiv !== watchers[watchers.length - 1].element) ? watchers[watchers.length - 1].element : watchers[watchers.length - 2].element; currentBaseY -= height; endPos--; } } } function up(e) { $(window).off("mousemove", move); $(window).off("mouseup", up); if (isDragging) { e.preventDefault(); dragDiv.removeClass("dragging"); isDragging = false; // $("div", colorCode).css('width', ''); dragDiv.css('cursor', ''); dragDiv.css('z-index', ''); dragDiv.css('opacity', ''); $(".name", dragDiv).removeClass("no_hover"); // Reset all watcher offsets $("#watcher_container .watcher").css('transition', "background-color 0.5s, top 0s"); $("#watcher_container .watcher").css('top', ''); setTimeout(function() { $("#watcher_container .watcher").css('transition', ''); }, 600); if (startPos !== endPos) { if (endPos > startPos) dragDiv.insertAfter($("#watcher_container .watcher")[endPos]); else dragDiv.insertBefore($("#watcher_container .watcher")[endPos]); DispatchUI.dispatch.moveWatcher(startPos, endPos); // Re-arrange our watchers array watchers.splice(startPos, 1); watchers.splice(endPos, 0, currentWatcher); } } } } } /** Dispatch object. Controls all of the watchers. **/ function Dispatch() { this.watchers = new Array(); this.isLoading = false; // Listeners this.listener = { onadd: [], onremove: [], onstart: [], onstop: [], onchange: [] }; } Dispatch.prototype = new Evt(); Dispatch.prototype.start = function(startAll) { if (this.watchers.length > 0) { var count = 0; for (var i = 0, len = this.watchers.length; i < len; i++) { // Don't start them all at the same time. There is a 2 second delay // between each start. It had to be done in a self-executing function // in order for the setTimeout to work properly. if (this.watchers[i].state.isSelected || startAll) { (function (watcher, x){ watcher.timer = setTimeout(function() { watcher.start(); }, x * 0000); // Let's try 0ms })(this.watchers[i], count++); } } } this.notify(Evt.START, null); } Dispatch.prototype.stop = function() { // Stop all Watchers if (this.watchers.length > 0) { for (var i = 0, len = this.watchers.length; i < len; i++) this.watchers[i].stop(); } this.interruptStart = true; this.notify(Evt.STOP, null) } Dispatch.prototype.add = function(watcher) { var self = this; watcher.addListener(Evt.CHANGE, function() { self.save(); }) this.watchers.push(watcher); if (!this.isLoading) { this.save(); } this.notify(Evt.ADD, watcher); // TODO Add a listener to save the watcher list after a watcher has been changed return watcher; } Dispatch.prototype.save = function() { if (!loadError) { // localStorage.setItem('notifier_watchers', JSON.stringify(dispatch.watchers, Watcher.replacerArray)); GM_setValue('notifier_watchers', JSON.stringify(dispatch.watchers, Watcher.replacerArray)); var lastChecked = getLastChecked(dispatch.watchers); if (lastChecked > 0) localStorage.setItem('notifier_watchers_lastChecked', JSON.stringify(lastChecked)); } function getLastChecked(watchers) { var lastChecked = (watchers[0].date) ? watchers[0].date.getTime() : 0; for (var i = 1, len = watchers.length; i < len; i++) { if (watchers[i].isRunning) return new Date.getTime(); if ((watchers[i].date) && (watchers[i].date.getTime() > lastChecked)) lastChecked = watchers[i].date.getTime(); } return lastChecked; } } Dispatch.prototype.load = function() { this.isLoading = true; var data; var watchers, lastChecked = localStorage.getItem('notifier_watchers_lastChecked'); data = GM_getValue('notifier_watchers'); if (typeof data === 'undefined') data = localStorage.getItem('notifier_watchers'); if (data !== null) { try { watchers = JSON.parse(data); try { lastChecked = JSON.parse(lastChecked); } catch(e) { lastChecked = null; } var now = new Date().getTime(), expTime = 180000, // 3 minutes expired = (lastChecked !== null) ? now - lastChecked > expTime : false; // Expired if most recent watcher update happened more than x minutes before page was loaded // Add the watchers. Clear last hits if past the expiration time for(var i = 0; i < watchers.length; i++) { if (expired) watchers[i].lastHits = []; this.add(new Watcher(watchers[i])); } } catch(e) { loadError = true; console.error("Error loading saved list", e); } } else { loadDefaultWatchers(); } this.isLoading = false; } Dispatch.prototype.remove = function(watcher) { var index = this.watchers.indexOf(watcher); if (index !== -1) this.watchers.splice(index, 1); watcher.delete(); this.save(); this.notify(Evt.REMOVE, watcher); } Dispatch.prototype.moveWatcher = function(from, to) { if ((to >= 0 && to < this.watchers.length) && (from >= 0 && from < this.watchers.length)) { var watcher = this.watchers.splice(from, 1); this.watchers.splice(to, 0, watcher[0]); this.save(); } } Dispatch.prototype.getWatcherByProperty = function(name, value) { if (this.watchers.length > 0) { for (var i = 0, len = this.watchers.length; i < len; i++) { if (this.watchers[i][name] === value) return this.watchers[i]; } } return null; } Dispatch.prototype.getWatcherIndex = function(watcher) { return this.watchers.indexOf(watcher); } Dispatch.prototype.getWatcher = function(index) { return this.watchers[index]; } Dispatch.prototype.getWatcherCount = function() { return this.watchers.length; } Dispatch.prototype.hideWatchers = function() { $("#controller a").css('display', "none"); $("#watcher_container").html(""); $("#watcher_container") .css('background-color', "#f9f9f9") .css('color', "#ff6b6b") .css('text-align', "center").append( $("<p>").text("There is already a notifier running on a different page."), $("<p>").addClass("error_button").append( $("<a>") .html("Close") .attr('href', "javascript:void(0)") .click(function() { $("#dispatcher").css('display', "none"); $("#content_container").css('left', "0px"); }), $("<a>") .html("Show") .attr('href', "javascript:void(0)") .click(function() { sendMessage({ header: 'show_main' }); }) )); } Dispatch.prototype.onRequestMainDenied = function() { pageType.MAIN = false; this.hideWatchers(); } Dispatch.prototype.exportWatchers = function() { var watcherAttrs = ["id", "time", "type", "name", "option", "auto", "alert", "stopOnCatch", "state", "isSelected", "url"]; return JSON.stringify(this.watchers, watcherAttrs); } Dispatch.prototype.importWatchers = function(data) { try { data = JSON.parse(data); dispatch.isLoading = true; for (var i = 0, len = data.length; i < len; i++) { var watcher = new Watcher(data[i]); if (!this.getWatcherByProperty('id', watcher.id) && !this.getWatcherByProperty('name', watcher.name)) this.add(watcher); } dispatch.isLoading = false; dispatch.save(); console.log("Watchers imported", dispatch.watchers); } catch(e) { console.error("Error importing watchers", e, data); alert("Invalid input. Try disabling word wrap on your text editor and re-copy.") } } function watcherDialog(watcher, callback) { var dialog = $("<div>").attr('id', 'add_watcher_form').append( $("<h3>").text("Add a watcher"), $("<p>").append( $("<label>").text("Name ").append( $("<input>").attr('id', "watcherName").attr('type', "text").val(watcher.name)), $("<label>").text(" Time ").append( $("<input>").attr('id', "watcherDuration").attr('type', "text").val(watcher.time / 1000)) ), (watcher.type === "hit") ? $("<p>").append( $("<input>").attr('type', "checkbox").attr('id', "autoaccept").prop('checked', watcher.option.auto), $("<label>").attr('for', "autoaccept").text("Auto-accept") ) : "", (watcher.type === "hit") ? $("<p>").append( $("<input>").attr('type', "checkbox").attr('id', "stopaccept").prop('checked', watcher.option.stopOnCatch), $("<label>").attr('for', "stopaccept").text("Stop on accept") ) : "", $("<p>").append( $("<input>").attr('type', "checkbox").attr('id', "alert").prop('checked', watcher.option.alert), $("<label>").attr('for', "alert").text("Alert") ), (watcher.type === "hit") ? $("<p>").append( $("<input>").attr('type', "checkbox").attr('id', "mutebatch").prop('checked', watcher.option.muteBatch), $("<label>").attr('for', "mutebatch").text("Silence batches after first seen") ) : "", $("<p>").addClass("form_buttons").append( $("<input>").attr('type', "button").attr('value', "Save"), $("<input>").attr('type', "button").attr('value', "Cancel") ) ); function save() { callback({ name : $("#watcherName", dialog).val(), time : parseInt($("#watcherDuration", dialog).val(), 10) * 1000, alert : $("#alert", dialog).prop('checked'), auto : $("#autoaccept", dialog).prop('checked'), stopOnCatch : $("#stopaccept", dialog).prop('checked'), muteBatch : $("#mutebatch", dialog).prop('checked') }) hide(); }; function hide() { dialog.hide(); dialog.remove(); dialog.empty(); } $("input[value='Save']", dialog).click(save); $("input[type='button']", dialog).click(hide); $(dialog).keydown(function(e) { switch(e.keyCode) { case 13: // Enter save(); break; case 27: // Esc hide(); break; } }); $("body").append(dialog); if ($("#watcherName", dialog).val() === "") $("#watcherName", dialog).focus().select(); else $("#watcherDuration", dialog).focus().select(); } function WatcherUI() { /* Nothing */ }; WatcherUI.create = function(watcher) { // Create jQuery Element... var div = $("<div>").addClass("watcher") .html('<div class="details"> > </div>\ <div class="play_container"><div class="play"></div><div class="play_select"></div></div>\ <div class="content">\ <a class="name" href="' + watcher.getURL() + '" target="_blank">' + ((typeof watcher.name !== 'undefined') ? watcher.name : watcher.id) + '</a>\ <div class="bottom">\ <span class="time">' + (watcher.time / 1000) + ' seconds </span>\ <span class="icons">\ <a class="edit" href="javascript:void(0)"><img src="https://i.imgur.com/peEhuHZ.png" /></a>\ <a class="delete" href="javascript:void(0)"><img src="https://i.imgur.com/5snaSxU.png" /></a>\ </span>\ <div class="last_updated" title="Last checked: ' + ((typeof watcher.date !== 'undefined') ? watcher.date.toString() : "n/a") + '">' + ((typeof watcher.date !== 'undefined') ? watcher.getFormattedTime() : "n/a") + '</div>\ </div>\ <div class="color_code"><div></div></div>\ </div>'); if (watcher.state.isSelected) div.addClass("selected"); // Add listeners watcher.addListener(Evt.START, function() { div.addClass("running"); }); watcher.addListener(Evt.STOP, function() { div.removeClass("running"); }); watcher.addListener(Evt.UPDATE, function(e) { $(".last_updated", div).text(watcher.getFormattedTime()).attr('title', "Last checked: " + watcher.date.toString()); div.addClass("updated"); setTimeout(function() { div.removeClass("updated") }, 1000); }); watcher.addListener(Evt.CHANGE, function() { $(".name", div).text(watcher.name).attr('href', watcher.url); $(".time", div).text(watcher.time / 1000 + " seconds"); if (watcher.state.isSelected) $(div).addClass("selected"); else $(div).removeClass("selected"); }); watcher.addListener(Evt.HITS_CHANGE, function() { $(".details", div).addClass("updated"); }); watcher.addListener(Evt.DELETE, function() { div.remove(); }); watcher.addListener(Evt.VIEW_DETAILS, function() { $(".details", div).removeClass("updated"); }); // Set actions $(".edit", div).click(showWatcherDialog); div.dblclick(showWatcherDialog); function showWatcherDialog() { watcherDialog(watcher, function(values) { watcher.setValues({ name : values.name, time : values.time, alert : values.alert, auto : values.auto, stopOnCatch : values.stopOnCatch, muteBatch : values.muteBatch }) }); } $(".delete", div).click(function() { dispatch.remove(watcher); }); $(".details", div).mouseover(function () { showDetailsPanel(watcher); $(this).removeClass("updated"); }); $("div.play_select", div).mousedown(function() { watcher.toggleSelected(); }); $("div.play", div).mousedown(function() { if (watcher.state.isRunning) watcher.stop(); else watcher.start(); }); // Add colors for watcher type var colorCode = $(".color_code", div); if (watcher.type === 'hit') { colorCode.addClass("hit"); colorCode.attr('title', "HIT Watcher"); } else if (watcher.type === 'requester') { colorCode.addClass("requester"); colorCode.attr('title', "Requester Watcher"); } else if (watcher.type === 'url') { colorCode.addClass("url"); colorCode.attr('title', "URL Watcher"); } colorCode.attr('title', colorCode.attr('title') + "\nClick and drag to re-order"); $(".delete img", div).hover(function() { $(this).attr('src', "https://i.imgur.com/guRzYEL.png")}, function() {$(this).attr('src', "https://i.imgur.com/5snaSxU.png")}); $(".edit img", div).hover(function() { $(this).attr('src', "https://i.imgur.com/VTHXHI4.png")}, function() {$(this).attr('src', "https://i.imgur.com/peEhuHZ.png")}); return { element: div, watcher: watcher }; } /** The Watcher object. This is what controls the pages that are monitored and how often Events: onStart - The watcher has started to check the desired page with a time interval onStop - The time interval has stopped onUpdate - The watcher has just checked the page for hits onChange - Attributes of the watcher changed, like name, interval time, etc. onDelete - When a watcher has been deleted onHitsChange - The watcher updated and found a different set of hits from last time onCaptcha? - The watcher encounters a captcha. Not sure if this should be handled by the Watcher or Loader (maybe both) **/ function Watcher(attrs) { var DEFAULT_TIME = 60000; this.interval = null; // For continuous interval this.timer = null; // For initial setTimeout this.newHits = []; attrs = attrs || {}; // Default states this.state = {}; state = attrs.state || {}; this.state.isRunning = (typeof state.isRunning !== 'undefined') ? state.isRunning : false; this.state.isSelected = (typeof state.isSelected !== 'undefined') ? state.isSelected : false; this.state.isUpdated = (typeof state.isUpdated !== 'undefined') ? state.isUpdated : false; // TODO Erase these state overwrites once we implement resuming state after a page load // Currently if a watcher is on when dispatch saves the watcher list, it'll still be marked as running even // though it wouldn't be running on page load. this.state.isRunning = false; this.state.isUpdated = false; // Required attributes this.id = attrs.id; this.time = attrs.time || DEFAULT_TIME; this.type = attrs.type; this.name = attrs.name || this.id; this.lastHits = attrs.lastHits || []; // Options this.option = {}; option = attrs.option || {}; this.option.auto = (typeof option.auto !== 'undefined') ? option.auto : false; this.option.alert = (typeof option.alert !== 'undefined') ? option.alert : false; this.option.stopOnCatch = (typeof option.stopOnCatch !== 'undefined') ? option.stopOnCatch : true; this.option.muteBatch = (typeof option.muteBatch !== 'undefined') ? option.muteBatch : false; // Figure out the URL this.url = attrs.url; if (typeof this.url === 'undefined') this.setUrl(); // Listeners this.listener = { onstart : [], onstop : [], onupdate : [], onchange : [], onhitschange : [], ondelete : [], onviewdetails : [] }; return this; } Watcher.prototype = new Evt(); Watcher.prototype.toString = function() { return this.name; } Watcher.prototype.getHTML = function() { this.DOMElement = $("<div>"); return $("<div>"); } Watcher.prototype.getURL = function() { return this.url; } Watcher.prototype.setUrl = function() { switch(this.type) { case 'hit': this.url = "https://www.mturk.com/mturk/preview" + (this.option.auto ? "andaccept" : "") + "?groupId=" + this.id; break; case 'requester': this.url = "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId=" + this.id; break; case 'url': if (typeof this.url === 'undefined') this.url = this.id; // URL watchers get a random id because of id requirements for CSS this.id = "A" + Math.floor(Math.random() * 100000000); break; } } Watcher.prototype.setAuto = function(isAuto) { this.option.auto = isAuto; this.setUrl(); } Watcher.prototype.isNewHit = function (hit) { return (this.newHits.indexOf(hit) !== -1); } Watcher.prototype.onChanged = function(newHits) { Messenger.sendHits(this, newHits); this.isUpdated = true; this.notify(Evt.HITS_CHANGE, newHits); } Watcher.prototype.start = function() { if (!this.state.isRunning) { var _this = this; // Set the interval and start right away this.interval = setInterval(function(){ _this.getData() }, this.time); this.getData(); this.state.isRunning = true; this.notify(Evt.START, null); } return this; } Watcher.prototype.stop = function() { // Stop the interval object and the timer object clearInterval(this.interval); clearTimeout(this.timer); this.state.isRunning = false; this.notify(Evt.STOP, null); } Watcher.prototype.delete = function() { this.notify(Evt.DELETE, this); this.stop(); this.listener = null; this.newHits = null; this.lastHits = null; } Watcher.prototype.filterMessages = function(newHits) { // Determine which hits, if any, the user should be notified of // For now just showing new hits var filteredHits; if (typeof this.lastHits !== 'undefined' && this.lastHits.length > 0) { filteredHits = []; for (var i = 0, len = newHits.length; i < len; i++) { for (var j = 0, len2 = this.lastHits.length; j < len2; j++) { if (newHits[i].id === this.lastHits[j].id) break; // If we reach the end with no matches, add it to the changed hits array if (j === len2 - 1 ) filteredHits.push(newHits[i]); } } } else { // If "last hits" doesn't exist, then all of the new hits should be considered new filteredHits = newHits; } this.lastHits = newHits; return filteredHits; } Watcher.prototype.toggleSelected = function() { if (this.state.isSelected) this.state.isSelected = false; else this.state.isSelected = true; this.notify(Evt.CHANGE, null); } Watcher.prototype.markViewed = function () { if (this.isUpdated) { isUpdated = false; this.notify(Evt.VIEW_DETAILS, null); } } Watcher.prototype.updateWatcherPanel = function() { this.date = new Date(); this.notify(Evt.UPDATE, null); } Watcher.prototype.setValues = function(values) { var val = values || {}; this.name = val.name || this.name; this.setAuto(val.auto); this.option.stopOnCatch = val.stopOnCatch; this.option.alert = val.alert; this.option.muteBatch = val.muteBatch; if (typeof val.time !== 'undefined' && this.time !== val.time) { this.time = val.time; if (this.state.isRunning) { this.stop(); this.start(); } } this.notify(Evt.CHANGE, null); } Watcher.prototype.getFormattedTime = function() { if (typeof this.date !== 'undefined') { var time = this.date; var str = ""; var hours = time.getHours(); var ampm = "am"; if (hours >= 12) { if (hours > 12) hours -= 12; ampm = "pm"; } else if (hours === 0) { hours = 12; } str += hours + ":" + ((time.getMinutes() < 10) ? "0" : "") + time.getMinutes() + ":" + ((time.getSeconds() < 10) ? "0" : "") + time.getSeconds() + ampm; return str; } else { return "N/A"; } } Watcher.prototype.setHits = function(hits) { if (typeof hits !== 'undefined') { if (Object.prototype.toString.call(hits) !== '[object Array]') hits = new Array(hits); this.sendHits(hits); } this.updateWatcherPanel(); } Watcher.prototype.sendHits = function(hits) { // Only send the hits if there is actually something to send // In the near future this will have to be changed to show when HITs go away completely if (typeof hits !== 'undefined' && hits.length > 0) { var newHits = this.newHits = this.filterMessages(hits); if (newHits.length) { this.onChanged(newHits); } else if (this.option.auto && !this.option.stopOnCatch) { this.onChanged(newHits); // Might add a different method for this case, but using onChanged for now } } } Watcher.prototype.getData = function() { var _this = this; Loader.load(this, this.url, function(data) { _this.onDataReceived($(data)); }); } Watcher.prototype.onDataReceived = function(data) { var error = $(".error_title", data); if (error.length > 0) { if (error.text().contains("You have exceeded")) { console.error("Exceeded the maximum rate!"); return; } } if (this.type === 'hit') this.setHits(this.parseHitPage(data)); else this.setHits(this.parseListing(data)); } Watcher.prototype.parseListing = function(data) { var hitCount = $("table:nth-child(3) > tbody:nth-child(1) > tr", data).length; var hits = new Array(); var qryUrl = "td:nth-child(3) > span:nth-child(1) > a", qryTitle = "td:nth-child(1) > a:nth-child(1)", qryRequester = "td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > a", qryReward = "td:nth-child(3) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > span:nth-child(1)", qryAvailable = "td:nth-child(3) > table > tbody > tr:nth-child(2) > td:nth-child(2)", qryTime = "td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2)"; for (var i = 0; i < hitCount; i++) { // Get nearby ancestors so jQuery won't have to do a full search for each element (faster) var base = $("table:nth-child(3) > tbody:nth-child(1) > tr:nth-child(" + (i+1) + ") > td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1)", data), topRow = $("tr:nth-child(2) > td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1)", base), content = $("tr:nth-child(3) > td:nth-child(3) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1)", base); var hit = new Hit(); hit.requester = $(qryRequester, content).text(); hit.requesterID = $(qryRequester, content).attr("href").match(/requesterId=([A-Z0-9]+)/)[1]; hit.title = $(qryTitle, topRow).text().trim(); hit.reward = $(qryReward, content).text().trim(); hit.available = $(qryAvailable, content).text().trim(); hit.time = $(qryTime, content).text().trim(); var urlData = $(qryUrl, topRow); hit.url = urlData.attr("href"); var idMatch = hit.url.match(/(group|notqualified\?hit|requestqualification\?qualification)Id=([A-Z0-9]+)/); if (idMatch !== null) { hit.id = idMatch[2]; } hit.canPreview = false; // Check each link to see if user is qualified or can preview the HIT, etc. urlData.each(function() { if (typeof this.href !== 'undefined') { if (this.href.contains("qual")) hit.isQualified = false; else if (this.href.contains("preview")) hit.canPreview = true; } }); hits[i] = hit; } return hits; } Watcher.prototype.parseHitPage = function(data) { var msgbox = $("#alertboxHeader", data); var hasCaptcha = ($(data).length > 0) ? ($(data).text()).contains("In order to accept your next HIT") : false; if ($(msgbox).length > 0 && ($(msgbox).text()).contains("There are no more available HITs in this group.")) { // If there aren't any more available, keep checking. If they were just previously available // then we should alert the user that it's gone. } else { // If it's newly available, alert the user and start auto-stacking if that's desired. //TODO We need to test for "You are not qualified to accept this HIT." if (hasCaptcha) { console.log("Has captcha"); sendMessage({header: 'captcha'}); } var uid = $("input[name='hitId']", data).attr("value"); var hit = new Hit({id: this.id, uid: uid, isAutoAccept: this.option.auto}); hit.requester = $("form:nth-child(7) > div:nth-child(9) > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(3) > td:nth-child(3) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)", data).text().trim(); hit.title = $(".capsulelink_bold > div:nth-child(1)", data).text().trim(); hit.reward = $("td.capsule_field_text:nth-child(5) > span:nth-child(1)", data).text().trim(); hit.available = $("td.capsule_field_text:nth-child(8)", data).text().trim(); hit.time = $("td.capsule_field_text:nth-child(11)", data).text().trim(); if (((hasCaptcha && settings.stopOnCaptcha) || (this.option.auto && this.option.stopOnCatch)) && this.state.isRunning) // We should probably toggle off all auto-accept hits when we encounter a captcha. Maybe send a special message to all mturk windows while we're at it. // The special message could be some kind of banner that says that no more hits can be accepted in the background until the captcha is entered. (It would // be pretty cool if we could pull up the captcha image in the background and just show it and the form to enter it from another page). this.stop(); return new Array(hit); } } Watcher.replacerArray = ["id", "time", "type", "name", "option", "auto", "alert", "stopOnCatch", "state", "isRunning", "isSelected", "isUpdated", "url", "lastHits"]; var Messenger = function() { var SEND_HITS = "new_hits"; var SEND_TO = "turkopticon"; var notificationGroup; function _sendHits(watcher, hits) { // Pass through ignore filters hits = IgnoreList.filter(hits); if (hits.length) { var toData = TO.get(Hit.getUniqueReqeusters(hits), _handleTOReceived); if (settings.notifications) { // Set wasViewed to false to check if any receiving windows were focused when this was sent. wasViewed = false; // Send Hits sendMessage({ header: SEND_HITS, content: { 'title': watcher.name, 'hits': hits, 'url': watcher.url } }); // Get TO and send it if (toData) sendMessage({ header: SEND_TO, content: toData }); // Attempt to send a browser notification after a brief period of time. If another mturk // page was visible when it received the hits, this will cancel out. if (!document.hasFocus()) setTimeout(function() { sendDesktopNotification(hits, watcher); }, 200); } // Show notification on dashboard, too notificationGroup = notificationPanel.add(new NotificationGroup({ title: watcher.name, hits: hits, url: watcher.url })); notificationGroup.addTO(toData); // Sound alert for auto-accept HIT watchers and watchers that have the alert set on if (watcher.option.auto || watcher.option.alert) Sound.alert(watcher); } else { if (!watcher.option.muteBatch && watcher.option.auto && !watcher.option.stopOnCatch) Sound.alert(watcher); } } function _handleTOReceived(data) { sendMessage({ header: SEND_TO, content: data }); if (data) notificationGroup.addTO(data); } return { sendHits: _sendHits } }(); /** Watcher Stack and Queue Stack - Grab as many as possible right away Limit - The number of HITs to stack at once Stop - Stop after the limit is reached Queue - Start queing after the limit is reached Queue - Grab one at a time, paced about as fast as they can be done **/ // Loader. This is what loads pages in the background. Page requests get added to a queue // so we can load pages in moderation to avoid exceeding the maximum request rate. // // Public methods: // - load(watcher, url, callback) is the only "public" method. The callback receives the data from // the requested page. var Loader = function() { var queue = [], pauseTime = 2000, // The amount of time to pause (in milliseconds) intervalTime = 200, // The amount of time between page loads count = 0, paused = true, maxLoad = 6; // The max number of pages to load without pausing function _load(watcher, url, callback) { if (!_isQueued(watcher)) { queue.push({url: url, callback: callback, watcher: watcher}); // If queue length is now 1 and was paused, it means we should resume loading if (queue.length === 1 && paused) { paused = false; _next(); } } } // Checks to see if the watcher is already queued function _isQueued(watcher) { if (queue.length > 0) { for (var i = 0, len = queue.length; i < len; i++) if (queue[i].watcher === watcher) return true; } return false; } // GETs thet next URL in the queue function _next() { if (queue.length > 0) { var info = queue.shift(); _getData(info.url, info.callback); } else { paused = true; } } function _getData(url, callback) { $.get(url, function(data) { callback(data); if (++count < maxLoad) { setTimeout(_next, intervalTime); } else { paused = true; count = 0; setTimeout(function() { if (paused) _next(); }, pauseTime); } }) } return { load: _load } }(); var TO = function() { var URL_PREFIX = "https://turkopticon.ucsd.edu/api/multi-attrs.php?ids=", cache = {}; setInterval(function() { cache = {}; }, 3600000); // Clear cache once per hour function _get(ids, callback) { var results = _getFromCache(ids); // If not all requesters found in storage, fetch from server if (results.missing.length > 0) _fetchFromServer(URL_PREFIX + results.missing.join(','), callback); return JSON.stringify(results.found); } function _getFromCache(ids) { var sorted = { found: {}, missing: [] }; for (var i = 0, len = ids.length; i < len; i++) { if (cache[ids[i]]) sorted.found[ids[i]] = cache[ids[i]]; else sorted.missing.push(ids[i]); } return sorted; } function _fetchFromServer(url, callback) { $.get(url, function(data) { _cache(data); if (typeof callback === 'function') callback(data); }) } function _getCount(obj) { var count = 0; for (key in obj) count++; } function _cache(data) { var ratings = JSON.parse(data); for (id in ratings) { cache[id] = ratings[id]; } } return { get: _get } }(); /** The NotificationPanel object. This holds and manipulates incoming notification groups **/ function NotificationPanel() { this.isHidden = true; this.notifications = new Array(); this.createPanel(); this.isHovered = false; this.timeout = null; } NotificationPanel.prototype.add = function(notification) { var _this = this; // Get rid of the leftover notification if there's one there if (this.notifications.length > 0 && this.notifications[0].hasTimedOut && !this.notifications[0].isHovered) { var oldNotification = this.notifications[0]; setTimeout(function() { _this.onTimeoutListener(oldNotification);}, 1500); } // Cancel delayed timeout from mouseout (so panel won't close right after a new // notification comes in) clearTimeout(this.timeout); notification.onTimeout = function() { _this.onTimeoutListener(notification) }; this.notifications.push(notification); this.addToPanel(notification); if (this.isHidden) { this.show(); } return notification; } NotificationPanel.prototype.remove = function(notification) { // Don't remove the notification if the user has their mouse hovering over it. // The notification will trigger onTimeout later on mouseout which will call // this method again for removal. if (!notification.isHovered) { this.removeFromPanel(notification); var newArray = new Array(); for (var i = 0, len = this.notifications.length; i < len; i++) if (this.notifications[i] !== notification) newArray.push(this.notifications[i]); this.notifications = newArray; } } NotificationPanel.prototype.show = function() { if (this.isHidden) { this.getDOMElement().removeClass("tm-hidden"); this.isHidden = false; } } NotificationPanel.prototype.hide = function() { if (!this.isHidden && !this.isHovered) { this.getDOMElement().addClass("tm-hidden"); this.isHidden = true; } } NotificationPanel.prototype.createPanel = function() { var _this = this; var panel = $('<div class="notification_panel tm-hidden" id="receiver"></div>') .hover( function() { clearTimeout(this.timeout); _this.isHovered = true; _this.show() }, function(){ _this.isHovered = false; this.timeout = setTimeout(function() { _this.hide() }, 1500); // Delay hiding the panel } ) $("body").append(panel); this.DOMElement = panel; addStyle("\ .notification_panel div, .notification_panel p { font: " + settings.fontSize + "pt 'Oxygen', verdana, sans-serif; }\ #receiver.notification_panel { \ position : fixed;\ width : 400px;\ bottom : 0px;\ right : 0px;\ background : rgba(255, 255, 255, 1);\ padding : 5px;\ border : 1px solid #d5d5d5;\ border-size : 1px 0 0 1px;\ overflow : auto;\ border-radius : 5px 0 0 0;\ border-right : 0;\ transition : right 0.2s;\ }\ #receiver.notification_panel.tm-hidden {\ right: -395px;\ }\ #receiver .notification_group {\ background : #fdfdfd;\ border : 1px solid #eaeaea;\ padding : 5px;\ margin : 10px 0;\ opacity : 1;\ overflow : hidden;\ transition : opacity 0.7s, max-height 0.2s ease-in-out 0.7s, margin 0.2s linear 0.7s, padding 0.2s linear 0.7s;\ border-right-color : #dedede;\ border-bottom-color : #dedede;\ }\ #receiver .notification_group.removed {\ opacity : 0;\ max-height : 0;\ padding : 0;\ margin : 0;\ }\ #receiver .notification_group h3 { margin: 3px; font-weight: normal }\ #receiver .notification_group h3 a:link, #receiver .notification_group h3 a:visited { color: #333 }\ #receiver .notification_group h4 a:link,\ #receiver .notification_group h4 a:visited { margin: 2px 0 0 4px; color: #222; }\ .notification_panel h2, #details_panel h2 { font-size: 100%; font-weight: normal; margin: 0px }\ .notification {\ padding : 5px 3px 0 5px;\ background-color : #fff;\ border-bottom : 1px solid #e9e9e9;\ position : relative;\ margin-left : 5px;\ }\ .notification:last-child { border: none; padding-bottom: 3px }\ .notification .mute {\ position : absolute;\ bottom : 6px;\ right : 5px;\ color : #999;\ cursor : pointer;\ font-size : 76%;\ }\ .notification a.requester:link, .notification a.requester:visited {\ margin-top : 2px;\ color : black;\ font-size : 80%;\ font-weight : bold;\ }\ #details_panel .notification.ignored {\ opacity: 0.4;\ }\ #receiver .notification.ignored {\ display: none;\ }\ .notification .ignore {\ font-size: 60%;\ color: #999;\ visibility: hidden;\ cursor: pointer;\ }\ .notification:hover .ignore {\ visibility: visible;\ }\ .notification .extra_info {\ font-style : italic;\ font-size : 80%;\ color : #505050;\ cursor : default;\ }\ .notification_panel a:link, .notification_panel a:visited {\ text-decoration : none;\ color : #6bf;\ }\ .notification_panel a.title:link, .notification_panel a.title:visited {\ display : block;\ white-space : nowrap;\ overflow : hidden;\ text-overflow : ellipsis;\ font-size : 102%;\ }\ .notification_panel .links {\ position : absolute;\ bottom : 6px;\ right : 35px;\ }\ .notification_panel a.hit_link {\ font-size : 70%;\ color : #fff;\ background : none repeat scroll 0% 0% #55B8EA;\ border-radius : 12px;\ display : inline;\ margin : 10px 5px 0px 0px;\ padding : 3px 9px;\ font-weight : bold;\ transition : background-color 0.25s;\ }\ .notification_panel a.hit_link:visited { background-color: #9df; }\ .notification_panel a.hit_link:hover { background: #8df; }\ .notification_panel p { margin: 3px 0 6px 0; font-size: 80%; cursor: default }\ .notification_panel .autoaccept {\ background-color : rgba(148, 236, 255, .3);\ background-color : rgba(214, 255, 91, 1);\ background-color : rgba(252, 255, 143, 1);\ }\ .notification.not_qualified { background-color: rgba(245, 244, 229, 1) }\ .notification_panel .new { background-color: rgba(220, 255, 228, 1); }\ .notification_panel .ratings-button {\ float: left;\ margin-right: 0.3em;\ height: 0.7em;\ width: 0.7em;\ background-color: #93C9FF;\ border-radius: 3px;\ font-size: 80%;\ position: relative;\ top: 0.6em;\ }\ .notification_panel .ratings.no-TO .ratings-button {\ background-color: #ccc;\ }\ .notification_panel .ratings-button > .ratings-chart {\ position: absolute;\ bottom: -3em;\ left: 0.4em;\ background-color: rgb(255, 255, 255);\ color: #444;\ visibility: hidden;\ padding: 0.3em;\ border: 1px solid #f0f0f0;\ z-index: 100;\ }\ .notification_panel .ratings-button:hover > .ratings-chart { visibility: visible; }\ .notification_panel .ratings.no-TO .ratings-button > .ratings-chart { bottom: -1em; }\ .notification_panel .ratings-chart table { border-collapse: collapse; }\ .notification_panel .ratings-chart td { font-family: 'Oxygen',verdana,sans-serif; font-size: 70%; color: #444; padding: 0 2em 0 0; cursor: default; vertical-align: center }\ .notification_panel .ratings-chart td:nth-child(3) { font-family: 'Droid Sans Mono',fixed-width; font-size: 60%; white-space: nowrap }\ .notification_panel .ratings-chart p { font-size: 80%; padding: 0 2em 0 0; margin: 0.5em 0 0; white-space: nowrap }\ .notification_panel .ratings-chart .light { opacity: 0.6 }\ .notification_panel .ratings.no-TO .ratings-chart { padding: 0.5em }\ .notification_panel .ratings-chart .rating { padding: 0.3em 0 0 }\ .notification_panel .ratings-chart .rating > div { background-color: #eee; height: 0.5em; width: 13em; margin: 0 0.5em 0 0; border-radius: 4px }\ .notification_panel .ratings-chart .rating > div > div { background-color: #55B8EA; height: 100%; border-radius: 4px }\ "); } NotificationPanel.prototype.getDOMElement = function() { return this.DOMElement; } NotificationPanel.prototype.addToPanel = function(notification) { $(this.getDOMElement()).prepend(notification.getDOMElement()); } NotificationPanel.prototype.removeFromPanel = function(notification) { $(notification.getDOMElement()).remove(); } NotificationPanel.prototype.onTimeoutListener = function(notification) { if (this.notifications.length > 1) { var _this = this; if (document.hasFocus() && settings.animation) { notification.fadeOut(); setTimeout(function() { _this.remove(notification) }, 905); } else { setTimeout(function() { _this.remove(notification) }, 705); } } else { this.hide(); } } /** The NotificationGroup object. This holds groups of Notifications and interacts directly with the NotificationPanel **/ function NotificationGroup(obj) { // title, hits, isSticky, watcher, url this.title = obj.title || null; this.hits = obj.hits; this.isSticky = (typeof obj.isSticky !== 'undefined') ? obj.isSticky : this.hasAutoAccept(); this.url = obj.url; this.timeout = (this.isSticky) ? 15000 : 6000; this.hasTimedOut = false; this.isHovered = false; if (typeof obj.watcher !== 'undefined') this.watcher = obj.watcher; var _this = this; setTimeout(function() { if (typeof _this.onTimeout !== 'undefined' && _this.onTimeout !== null) { _this.hasTimedOut = true; if (!_this.isHovered) _this.onTimeout(_this); } }, this.timeout); if (typeof this.hits[0] === 'undefined') console.error("Error, no hits for notification", document.URL, obj); this.createDOMElement(); } NotificationGroup.prototype.addTO = function(data) { if (data !== "{}") { var ratings = JSON.parse(data); var group = this.getDOMElement(); var notifications = group.find(".notification"), singleRequester = group.find("h4 .requester"); if (singleRequester.length > 0) { var id for (var i in ratings) id = i; this.appendRatings({ notification: singleRequester.parent(), id: id, ratings: ratings[id] }); } for (var id in ratings) { currentNotification = notifications.filter(function() { return $(this).data("requesterID") === id }); this.appendRatings({ notification: currentNotification, id: id, ratings: ratings[id] }); } } } NotificationGroup.prototype.appendRatings = function(obj) { var notification = obj.notification, requesterID = obj.id, ratings = obj.ratings, attrs = ratings.attrs, requesterEl = notification.find(".requester"); // Would be nice to have a chart-looking icon var element = $('<div class="ratings"><div class="ratings-button" style="float: left"><div class="ratings-chart"></div></div></div>'); if (ratings) { var html = '\ <table><tbody>\ <tr><td>Communicativity</td><td class="rating">' + bar(attrs.comm) + '</td><td class="light">' + attrs.comm + ' / 5</td></tr>\ <tr><td>Pay</td> <td class="rating">' + bar(attrs.pay ) + '</td><td class="light">' + attrs.pay + ' / 5</td></tr>\ <tr><td>Fairness</td> <td class="rating">' + bar(attrs.fair) + '</td><td class="light">' + attrs.fair + ' / 5</td></tr>\ <tr><td>Quickness</td> <td class="rating">' + bar(attrs.fast) + '</td><td class="light">' + attrs.fast + ' / 5</td></tr>\ </tbody></table>'; var count = ratings.reviews; html += '<p>Scores based on <a href="http://turkopticon.ucsd.edu/' + requesterID + '" target="_blank">' + count + ' review' + ((count !== 1) ? "s" : "") + '</a>'; html += ' <span class="light">(' + ratings.tos_flags + ' TOS violation' + ((ratings.tos_flags !== 1) ? "s" : "") + ')</light> - <a href="http://turkopticon.ucsd.edu/report?requester[amzn_id]=' + requesterID + '&requester[amzn_name]=' + ratings.name +'" target="_blank">Add review</a></p>'; element.find(".ratings-chart").append(html); element.find(".ratings-button").css('background-color', getHsl(avg(attrs) / 5 * 100)); } else { var html = '<p>No ratings available.</p>'; html += '<p>Be the first to <a href="http://turkopticon.ucsd.edu/report?requester[amzn_id]=' + requesterID + '&requester[amzn_name]=' + ratings.name +'" target="_blank">review this requester</a>.'; element.find(".ratings-chart").append(html); element.addClass("no-TO"); } function bar(rating) { var percent = rating / 5 * 100, color = getHsl(percent); return '<div><div style="width: ' + percent + '%; background-color: ' + color + '"> </div></div>'; } function getHsl(percent) { var hue = ((percent / 100 * 5) - 1) / 4 * 100; // Max hue = 100 (green) return 'hsl(' + hue + ', 78%, 50%)'; } function avg(attrs) { var count = 0, sum = 0, comm = parseFloat(attrs.comm, 10), pay = parseFloat(attrs.pay, 10), fast = parseFloat(attrs.fast, 10), fair = parseFloat(attrs.fair, 10); if (comm !== 0) { sum += comm; count ++; } if (pay !== 0) { sum += pay; count ++; } if (fast !== 0) { sum += fast; count ++; } if (fair !== 0) { sum += fair; count ++; } return sum / count; } requesterEl.before(element); } NotificationGroup.prototype.createDOMElement = function() { var _this = this, REQUESTER_PREFIX = "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId=", hit = this.hits[0], isSameReq = Hit.isSameRequester(this.hits), self = this; var div = $('<div>').addClass("notification_group") .append((this.title !== null) ? $('<h3><a href="' + this.url + '" target="_blank">' + this.title + '</a></h3>') : "") .append((isSameReq) ? $('<h4><a href="' + REQUESTER_PREFIX + hit.requesterID + '" target="_blank" class="requester">' + hit.requester + '</a></h4>') : "") .hover( function() { _this.isHovered = true }, function() { _this.isHovered = false; if (_this.hasTimedOut && typeof _this.onTimeout === 'function') _this.onTimeout(_this); } ); // Sort the notifications (ignored go to the bottom) if (pageType.DASHBOARD && pageType.MAIN) this.hits.sort(function(a, b) { return (IgnoreList.isIgnored(a.requester)) ? 1 : 0 }); // Add the notifications for (var i = 0, len = this.hits.length; i < len; i++) { var notification = new NotificationHit(this.hits[i], isSameReq, (typeof this.watcher !== 'undefined') ? this.watcher : null); notification.onIgnore = function(requesterID) { // Remove all notifications within the group that match the requester ID var notifications = self.DOMElement.find(".notification"); notifications.filter(function() { return $(this).data("requesterID") === requesterID }).addClass("ignored"); }; notification.onUnIgnore = function(requesterID) { // Remove all notifications within the group that match the requester ID var notifications = self.DOMElement.find(".notification"); notifications.filter(function() { return $(this).data("requesterID") === requesterID }).removeClass("ignored"); }; $(div).append(notification.getDOMElement()); } if (this.hits[0].isAutoAccept) div.addClass("autoaccept"); this.DOMElement = div; // This is required to get the shrinking effect when notifications are removed setTimeout(function() { div.css('max-height', div.height()); }, 1000); } NotificationGroup.prototype.getDOMElement = function() { return this.DOMElement; } NotificationGroup.prototype.hasAutoAccept = function() { var hasAutoAccept = false; for (var i = 0, len = this.hits.length; i < len; i++) if (this.hits[i].isAutoAccept) hasAutoAccept = true; return hasAutoAccept; } NotificationGroup.prototype.fadeOut = function(duration) { this.getDOMElement().addClass("removed"); } /** The Notification object. This holds the notification data for individual hits **/ function NotificationHit(hit, isSameReq, watcher) { this.hit = hit; this.isSameReq = isSameReq; if (typeof watcher !== 'undefined') this.watcher = watcher; this.createDOMElement(); } NotificationHit.prototype.createDOMElement = function() { var URL_PREFIX = "https://www.mturk.com/mturk/searchbar?selectedSearchType=hitgroups&requesterId="; // Create notification var hit = this.hit; var notification = $('<div>').addClass("notification").append( '<a class="title" target="_blank" href="' + hit.getURL('preview') + '" title="' + hit.title + '">' + hit.title + '</a>', (!this.isSameReq) ? $('<a class="requester" href="' + URL_PREFIX + hit.requesterID + '" target="_blank">' + hit.requester + '</a> <a class="ignore">ignore</a>') : "", '<p>' + hit.reward + " - " + hit.available + " rem. - " + hit.time.replace("minutes", "mins") + '</p>\ <div class="links"></div>\ <div><a class="mute"></a></div>' ).data("requesterID", hit.requesterID); // Add links if (typeof hit.id !== 'undefined' && hit.id !== "undefined" && hit.isQualified) { if (this.hit.isAutoAccept) { $(".links", notification).append('<a class="hit_link" href="' + hit.getURL('view') + '" target="_blank">VIEW</a>'); } else { $(".links", notification).append('\ <a class="hit_link" target="_blank" href="' + hit.getURL('preview') + '">PREVIEW</a>\ <a class="hit_link" target="_blank" href="' + hit.getURL('accept') + '">ACCEPT</a>\ <a class="hit_link" target="_blank" href="' + hit.getURL('auto') + '">+AUTO</a>'); } } else { $(notification).addClass("not_qualified"); $(".links", notification).append( (hit.canPreview) ? '<a class="hit_link" href="' + hit.getURL('preview') + '" target="_blank">PREVIEW</a>' : "", '<span class="extra_info">Not Qualified </span>'); } if (IgnoreList.isIgnored(hit.requester)) notification.addClass("ignored"); var id = hit.id; var muteButton = $('a.mute', notification); $(muteButton).text((typeof IgnoreList !== 'undefined' && IgnoreList.isMuted(id)) ? "muted" : "mute"); $(muteButton).click(function () { if (!pageType.DASHBOARD || (pageType.DASHBOARD && !pageType.MAIN)) { if ($(this).text() === "mute") sendMessage({ header: "mute_hit", content: { id: id }, timestamp: true }); else sendMessage({ header: "unmute_hit", content: { id: id }, timestamp: true }); } else { if (!IgnoreList.isMuted(id)) IgnoreList.add(IgnoreList.HIT, id); else IgnoreList.remove(IgnoreList.HIT, id); } if ($(this).text() === "mute") $(this).text("muted"); else $(this).text("mute"); }); var ignoreButton = $("a.ignore", notification); var self = this; ignoreButton.click(function() { if (!pageType.DASHBOARD || (pageType.DASHBOARD && !pageType.MAIN)) { sendMessage({ header: "ignore_requester", content: { id: hit.requester } }); } else { if (!notification.hasClass("ignored")) IgnoreList.add(IgnoreList.REQUESTER, hit.requester); else IgnoreList.remove(IgnoreList.REQUESTER, hit.requester); } if (!notification.hasClass("ignored")) { if (self.onIgnore && typeof self.onIgnore === 'function') self.onIgnore(hit.requesterID); } else { if (self.onUnIgnore && typeof self.onUnIgnore === 'function') self.onUnIgnore(hit.requesterID); } }); if (hit.isAutoAccept) notification.addClass("autoaccept"); if (typeof this.watcher !== 'undefined' && this.watcher !== null && this.watcher.isNewHit(hit)) $(notification).addClass("new"); $(notification).append(muteButton); this.DOMElement = notification; } NotificationHit.prototype.getDOMElement = function() { return this.DOMElement; } var Sound = function() { var sound = new Audio(), altSound = new Audio(); if (sound.canPlayType('audio/ogg;codecs="vorbis"')) { // Sound from http://rpg.hamsterrepublic.com/wiki-images/3/3e/Heal8-Bit.ogg sound.src = "data:audio/ogg;base64," + "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"; sound.volume = settings.volume / 100; } if (altSound.canPlayType('audio/mp3')) { // Sound from http://www.freesfx.co.uk (Multimedia System Alert 003) altSound.src = "data:audio/mp3;base64," + "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VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuOf/7WGT/j/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABDdVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuOf/7WGT/j/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABDdVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7WGT/j/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="; altSound.volume = settings.volume / 100; } function _alert(context) { if (settings.sound) { if (context instanceof Watcher) { // It's written this way in case we want sounds for other things in the future var option = context.option; if (!option.stopOnCatch && option.auto) { altSound.currentTime = 0; altSound.play(); } else if (option.auto || option.alert) { sound.currentTime = 0; sound.play(); } } } } function _setVolume(volume) { sound.volume = altSound.volume = volume / 100; } return { alert : _alert, setVolume : _setVolume } }();