vBulletin
Show a button in the results to export the specified HIT with vBulletin formatted text to share on forums.
IRC
Show a button in the results to export the specified HIT streamlined for sharing on IRC.
Reddit
Show a button in the results to export the specified HIT for sharing on Reddit, formatted to
r/HITsWorthTurkingFor standards.
Bubble New HITs
When this option is enabled, new HITs will always be placed at the top of the results table.
Color Type
simple HIT Scraper will use a simple weighted average to
determine the overall TO rating and colorize results using that value. Use this setting to make coloring consistent between
HIT Scraper and Color Coded Search.
adjusted HIT Scraper will calculate a Bayesian adjusted average
based on confidence of the TO rating to colorize results. Confidence is proportional to the number of reviews.
Sort Type
simple
HIT Scraper will sort results based simply on value regardless of the number of reviews.
adjusted HIT Scraper will use a Bayesian adjusted rating
based on reliability (i.e. confidence) of the data. It factors in the number of reviews such that, for example,
a requester with 100 reviews rated at 4.6 will rightfully be ranked higher than a requester with 3 reviews rated at 5.
This gives a more accurate representation of the data.
Alert Volume
${Math.floor(this.user.volume.ding * 100)}%
${Math.floor(this.user.volume.squee * 100)}%
TO Weighting
Specify weights for TO attributes to place greater importance on certain attributes over others.
The default values, [1, 3, 3, 1], ensure consistency between HIT Scraper and
Color Coded Search;
recommended values for adjusted coloring are [1, 6, 3.5, 1].
`,//}}}
_appearance =//{{{
`
Display Checkboxes
show
Shows all checkboxes and radio inputs on the control panel for sake of clarity.
hide
Hides checkboxes and radio inputs for a cleaner, neater appearance. Their visibility is not required for proper
operation; all options can still be toggled while hidden.
Themes
HIT Coloring
link
Apply coloring based on Turkopticon reviews to all applicable links in the results table.
cell
Apply coloring based on Turkopticon reviews to the background of all applicable cells in the results table.
Note: The Classic theme is exempt from these settings and will always colorize cells.
Font SizeNew HIT Offset
Change the font size (measured in px) for text in the results table. Default is 11px.
Controls the font size of new HITs relative to the rest of the results. Default is 1px. Example: With a font size of 11px and an offset of 1px, new HITs will be displayed at 12px.
`,//}}}
_blocks = //{{{
`
Advanced Matching
Allows for the use of asterisks (*) as wildcards in the blocklist for simple glob matching. Any blocklist entry
without an asterisk is treated the same as the default behavior--the entry must exactly match a HIT title or requester to
trigger a block.
Wildcards have the potential to block more HITs than intended if using a pattern that's too generic.
Matching is not case sensitive regardless of the wildcard setting. Entries without an opening asterisk are
expected to match the beginning of a line, likewise, entries without a closing asterisk are expected to match
the end of a line. Example usage below.
Matches
Does not match
Notes
foo*baz
foo bar bat baz
bar foo bat baz
no leading or closing asterisks; foo must be at the start of a line,
and baz must be at the end of a line for a positive match
foobarbatbaz
foo bar bat
*foo
bar baz foo
foo baz
matches and blocks any line ending in foo
foo*
foo bat bar
bat foo baz
matches and blocks any line beginning with foo
*bar*
foo bar bat baz
foo bat baz
matches and blocks any line containing bar
bar bat baz
foo bar
foobatbarbaz
** foo
** foo
** foo bar baz
Multiple consecutive asterisks will be treated as a string rather than a wildcard. This makes it
compatible with HITs using multiple asterisks in their titles, e.g., *** contains peanuts ***.
** *bar* ***
** foo bar baz bat ***
foo bar baz
Consecutive asterisks used in conjunction with single asterisks.
*
nothing
all
A single asterisk would usually match anything and everything,
but here, it matches nothing. This prevents accidentally blocking everything from the results table.
`,//}}}
_notify = //{{{
`
Additional Notifications
blink
Blink the tab when there are new HITs.
taskbar
Create an HTML5 browser notification when there are new HITs, which appears over the taskbar for 10 seconds.
Note: These notification options will only apply when the page does not have active focus.
`,//}}}
_utils =//{{{
`
Export/Import
Export
Export your current settings, block list, and include list as a local file.
Import
Import your settings, block list, and include list from a local file.
`,//}}}
_main = //{{{
`
GeneralAppearanceBlocklistNotificationsUtilities
${_general}
${_appearance}
${_blocks}
${_notify}
${_utils}
`;//}}}
this.main = document.body.appendChild(document.createElement('DIV'));
this.main.id = 'settingsMain';
this.main.innerHTML = _main;
return this;
},//}}} Settings::draw
init: function() {//{{{
var get = (q,all) => this.main['querySelector' + (all ? 'All': '')](q),
sidebarFn = function(e) {
if (e.target.classList.contains('settingsSelected')) return;
get('#'+get('.settingsSelected').textContent).style.display = 'none';
get('.settingsSelected').classList.toggle('settingsSelected');
e.target.classList.toggle('settingsSelected');
get('#'+e.target.textContent).style.display = 'block';
}.bind(this),
sliderFn = function(e) {
e.target.nextElementSibling.textContent = Math.floor(e.target.value * 100) + '%';
},
optChangeFn = function(e) {//{{{
var tag = e.target.tagName, type = e.target.type, id = e.target.id,
isChecked = e.target.checked, name = e.target.name, value = e.target.value;
switch(tag) {
case 'SELECT':
//get('#thedit').textContent = value === 'random' ? 'Re-roll!' : 'Edit Current Theme';
this.user.themes.name = value;
Themes.apply(value, this.user.hitColor);
break;
case 'INPUT':
switch(type) {
case 'radio':
if (name === 'checkbox') {
this.user.showCheckboxes = (value === 'true');
Array.from(document.querySelectorAll('#controlpanel input[type=checkbox],#controlpanel input[type=radio]'))
.forEach(v => v.classList.toggle('hidden'));
}
else this.user[name] = value;
if (name === 'hitColor') Themes.apply(this.user.themes.name, value);
break;
case 'checkbox':
this.user[id] = isChecked;
if (name === 'export')
Array.from(document.querySelectorAll(`button.${value}`))
.forEach(v => v.style.display = isChecked ? '' : 'none');
if (id === 'notifyTaskbar' && isChecked && Notification.permission === 'default')
Notification.requestPermission();
break;
case 'number':
if (name === 'fontSize')
document.head.querySelector('#lazyfont').sheet.cssRules[0].style.fontSize = value + 'px';
else if (name === 'shineOffset')
document.head.querySelector('#lazyfont').sheet.cssRules[1].style.fontSize = +this.user.fontSize + (+value) + 'px';
if (name === 'TOW') this.user.toWeights[id] = value;
else this.user[name] = value;
break;
case 'range':
this.user.volume[name] = value;
let audio = document.querySelector(`#${name}`);
audio.volume = value;
audio.play();
break;
} break;
}
Settings.save();
}.bind(this);//}}}
get('#settingsClose').onclick = this.die.bind(this);
get('#General').style.display = 'block';
Array.from(get('#settingsSidebar span', true)).forEach(v => v.onclick = sidebarFn);
Array.from(get('input:not([type=file]),select',true)).forEach(v => v.onchange = optChangeFn);
Array.from(get('input[type=range]', true)).forEach(v => v.oninput = sliderFn);
get('#thedit').onclick = () => { this.die.call(this); new Editor('theme'); };
get('#sexport').onclick = FileHandler.exports;
get('#simport').onclick = () => { get('#fsimport').value = ''; get('#eisStatus').innerHTML = ''; get('#fsimport').click(); };
get('#fsimport').onchange = FileHandler.imports;
},//}}} Settings::init
die: function() { Interface.toggleOverflow('off'); this.main.remove(); }
},//}}} Settings
Themes = {//{{{
default: defaults.themes,
generateCSS: function(theme, mode) {//{{{
var ref = theme === 'random' ? this.randomize() : Settings.user.themes.colors[theme],
_ms = mode === 'cell' || theme === 'classic',
cellFix = {
row: k => `.${k} ` + (_ms ? '{background:' : 'a {color:') + ref[k] + '}',
text: k => `.${k} {color:` + (_ms ? this.tune(ref.bodytable,ref[k]) : ref.bodytable) + '}',
export: k => `.${k} button {color:` + (_ms ? this.tune(ref.export,ref[k]) : ref.export) + '}',
vlink: k => `.${k} a:not(.static):visited {color:` + (_ms ? this.tune(ref.vlink,ref[k]) : ref.vlink) + '}'
},
css = `body {color:${ref.defaultText}; background-color:${ref.background}}
/*#status {color:${ref.secondText}}*/
#sortdirs {color:${ref.inputText}}
#curtain {background:${ref.background}; opacity:0.5}
.controlpanel i:after {color:${ref.accent}}
#controlpanel {background:${ref.cpBackground}}
#controlpanel input${theme === 'classic' ? '' : ', #controlpanel select'}
{color:${ref.inputText}; border:1px solid; background:${theme === 'classic' ? '#fff' : ref.cpBackground}}
#controlpanel label {color:${ref.defaultText}; background:${ref.cpBackground}}
#controlpanel label:hover {background:${ref.hover}}
#controlpanel label.checked {color:${ref.secondText}; background:${ref.highlight}}
/*#resultsTable tbody a:not(.static):visited {color:${ref.vlink}}*/
/*#resultsTable button {color:${ref.export}}*/
thead, caption, a {color:${ref.defaultText}}
tbody a {color:${ref.link}}
.nohitDB {color:#000; background:${ref.nohitDB}}
.hitDB {color:#000; background:${ref.hitDB}}
.reqmaster {color:#000; background:${ref.reqmaster}}
.nomaster {color:#000; background:${ref.nomaster}}
.tooweak {background:${ref.unqualified}}
${cellFix.row('toNone')} ${cellFix.text('toNone')} ${cellFix.export('toNone')} ${cellFix.vlink('toNone')}
${cellFix.row('toHigh')} ${cellFix.text('toHigh')} ${cellFix.export('toHigh')} ${cellFix.vlink('toHigh')}
${cellFix.row('toGood')} ${cellFix.text('toGood')} ${cellFix.export('toGood')} ${cellFix.vlink('toGood')}
${cellFix.row('toAverage')} ${cellFix.text('toAverage')} ${cellFix.export('toAverage')} ${cellFix.vlink('toAverage')}
${cellFix.row('toLow')} ${cellFix.text('toLow')} ${cellFix.export('toLow')} ${cellFix.vlink('toLow')}
${cellFix.row('toPoor')} ${cellFix.text('toPoor')} ${cellFix.export('toPoor')} ${cellFix.vlink('toPoor')}`;
if (theme !== 'classic') css += `\n.controlpanel button {color:${ref.accent}; background:transparent;}`;
return css;
},//}}} Themes::generateCSS
tune: function(fg,bg) {//{{{
var cbg = this.getBrightness(bg),
lighten = c => { c.s = Math.max(0, c.s-5); c.v = Math.min(100, c.v+5); return c; },
darken = c => { c.s = Math.min(100, c.s+5); c.v = Math.max(0, c.v-5); return c; },
tune = (function() { if (cbg >= 128) return darken; else return lighten; })(),
hex2hsv = function(c) {//{{{
var r = parseInt(c.slice(1,3),16), g = parseInt(c.slice(3,5),16), b = parseInt(c.slice(5,7),16),
min = Math.min(r,g,b), max = Math.max(r,g,b), delta = max-min, _hue;
switch(max) {
case r: _hue = Math.round(60 * (g - b)/delta); break;
case g: _hue = Math.round(120 + 60 * (b - r)/delta); break;
case b: _hue = Math.round(240 + 60 * (r - g)/delta); break;
}
return { h:_hue < 0 ? _hue + 360 : _hue, s:max === 0 ? 0 : Math.round(100 * delta/max), v:Math.round(max * 100/255) };
}, //}}}
hsv2hex = function(c) {//{{{
var r, g, b, pad = s => ('00'+s.toString(16)).slice(-2);
if (c.s === 0) r = g = b = Math.round(c.v * 2.55).toString(16);
else {
c = { h: c.h/60, s: c.s/100, v: c.v/100 }; // convert to prime to calc chroma
var _t1 = Math.round((c.v * (1 - c.s)) * 255),
_t2 = Math.round((c.v * (1 - c.s * (c.h - Math.floor(c.h)))) * 255),
_t3 = Math.round((c.v * (1 - c.s * (1 - (c.h - Math.floor(c.h))))) * 255);
switch (Math.floor(c.h)) {
case 1: r = _t2; g = Math.round(c.v * 255); b = _t1; break;
case 2: r = _t1; g = Math.round(c.v * 255); b = _t3; break;
case 3: r = _t1; g = _t2; b = Math.round(c.v * 255); break;
case 4: r = _t3; g = _t1; b = Math.round(c.v * 255); break;
case 0: r = Math.round(c.v * 255); g = _t3; b = _t1; break;
default: r = Math.round(c.v * 255); g = _t1; b = _t2; break;
}
} return '#' + pad(r) + pad(g) + pad(b);
};//}}}
while (Math.abs(this.getBrightness(fg)-cbg) < 90) fg = hsv2hex(tune(hex2hsv(fg)));
return fg;
},//}}}
getBrightness: function(hex) {//{{{
// TODO: put in Colors object
var r = parseInt(hex.slice(1,3),16), g = parseInt(hex.slice(3,5),16), b = parseInt(hex.slice(5,7),16);
return ((r*299) + (g*587) + (b*114))/1000;
},//}}} Themes::getBrightness
apply: function(theme, mode) {//{{{
var cssNew = URL.createObjectURL(new Blob([this.generateCSS(theme, mode)], {type:'text/css'})),
rel = document.head.querySelector('link[rel=stylesheet]'), cssOld = rel.href;
rel.href = cssNew;
URL.revokeObjectURL(cssOld);
},//}}} Themes::apply
},//}}} Themes
Interface = {//{{{
user: Settings.user, time: Date.now(), focused: true, blackhole: {},
isLoggedout: document.querySelector('#lnkWorkerSignin') ? true : false,
resetTitle: function() {//{{{
if (this.blackhole.blink) clearInterval(this.blackhole.blink);
document.title = DOC_TITLE;
},//}}}
toggleOverflow: function(state) {//{{{
document.body.querySelector('#curtain').style.display = state === 'on' ? 'block' : 'none';
document.body.style.overflow = state === 'on' ? 'hidden' : 'auto';
},//}}} Interface::curtains
draw: function() {//{{{
var user = this.user = Settings.user, _cb = user.showCheckboxes ? '' : 'hidden',
_u0 = new Uint8Array(Array.prototype.map.call(window.atob(audio0), v => v.charCodeAt(0))),
_u1 = new Uint8Array(Array.prototype.map.call(window.atob(audio1), v => v.charCodeAt(0))),
ding = URL.createObjectURL(new Blob([_u0], {type:'audio/ogg'})),
squee = URL.createObjectURL(new Blob([_u1], {type:'audio/mp3'})),
titles = {//{{{
refresh: "Enter search refresh delay in seconds.\nEnter 0 for no auto-refresh.\nDefault is 0 (no auto-refresh).",
pages: "Enter number of pages to scrape. Default is 1.",
skips: "Searches additional pages to get a more consistent number of results. Helpful if you're blocking a lot of items.",
resultsPerPage: "Number of results to return per page (maximum is 100, default is 30)",
batch: "Enter minimum HITs for batch search (must be searching by Most Available).",
pay: "Enter the minimum desired pay per HIT (e.g. 0.10).",
qual: "Only show HITs you're currently qualified for (must be logged in).",
monly: "Only show HITs that require Masters qualifications.",
mhide: "Remove masters hits from the results if selected, otherwise display both masters and non-masters HITS.\n" +
"The 'qualified' setting supercedes this option.",
searchBy: "Get search results by...\n Latest = HIT Creation Date (newest first),\n " +
"Most Available = HITs Available (most first),\n Reward = Reward Amount (most first),\n Title = Title (A-Z)",
invert: "Reverse the order of the Search By choice, so...\n Latest = HIT Creation Date (oldest first),\n " +
"Most Available = HITs Available (least first),\n Reward = Reward Amount (least first),\n Title = Title (Z-A)",
shine: "Enter time (in seconds) to keep new HITs highlighted.\nDefault is 300 (5 minutes).",
sound: "Play a sound when new results are found.",
soundSelect: "Select which sound will be played.",
minTOPay: "After getting search results, hide any results below this average Turkopticon pay rating.\n" +
"Minimum is 1, maximum is 5, decimals up to 2 places, such as 3.25",
hideNoTO: "After getting search results, hide any results that have no, or too few, Turkopticon pay ratings.",
disableTO: "Disable attempts to download ratings data from Turkopticon for the results table.\n" +
"NOTE: TO is cached. That means if TO is availible from a previous scrape, it will use that value even if " +
"TO is disabled. This option only prevents the retrieval of ratings from the Turkopticon servers,",
sortPay: "After getting search results, re-sort the results based on their average Turkopticon pay ratings.",
sortAll: "After getting search results, re-sort the results by their overall Turkopticon rating.",
sortAsc: "Sort results in ascending (low to high) order.",
sortDsc: "Sort results in descending (high to low) order.",
search: "Enter keywords to search for; default is blank (no search terms).",
hideBlock: "When enabled, hide HITs that match your blocklist.\n"+
"When disabled, HITs that match your blocklist will be displayed with a red border.",
onlyIncludes: "Show only HITs that match your includelist.\nBe sure to edit your includelist first or no results will be displayed.",
shineInc: "Outline HITs that match your includelist with a dashed green border.",
mainlink: "Version: " + VERSION + "\nRead the documentation for HIT Scraper With Export on its Greasyfork page.",
gbatch: "Apply the 'Minimum batch size' filter to all search options.",
onlyViable: 'Filters out HITs with qualifications you do not have and \ncan neither request nor take a test to obtain.\n' +
'Does not work while logged out.'
},//}}}
css = [//{{{
'body {font-family:Verdana, Arial; font-size:14px}',
'p {margin:8px auto}',
'.cpdefault {display:inline-block; visibility:visible; overflow:hidden; padding:8px 5px 1px 5px; transition:all 0.3s;}',
'#controlpanel i:after, #status i:after {content:" | "}',
'input[type="checkbox"], input[type="radio"] {vertical-align:middle}',
'input[type="number"] {width:50px; text-align:center}',
'label {padding:2px}',
'.hiddenpanel {width:0px; height:0px; visibility:hidden}',
'.hidden {display:none}',
'button {border:1px solid}',
'textarea {font-family:inherit; font-size:11px; margin:auto; padding:2px}',
'.pop {position:fixed; top:15%; left:50%; margin:auto; transform:translateX(-50%); padding:5px;' + // for editors/exporters
'background:black; color:white; z-index:20; font-size:12px; box-shadow:0px 0px 6px 1px #fff}',
'dt {text-transform:uppercase; clear:both; margin:3px}',
'.icbutt {float:left;border:1px solid #fff;cursor:pointer} .icbutt > input {opacity:0;display:block;width:25px;height:25px;border:none}',
// settings
'#settingsMain {z-index:20; position:fixed; background:#fff; color:#000; box-shadow:-3px 3px 2px 2px #7B8C89; line-height:initial;' +
'top:50%; left:50%; width:85%; height:85%; margin-right:-50%; transform:translate(-50%, -50%)}',
'#settingsMain > div {margin:5px; padding:3px; position:relative; border:1px solid grey; line-height:initial}',
'.close {position:relative; font-weight:bold; font-size:1em; color:white; background:black; cursor:pointer}',
'#settingsSidebar {width:100px; min-width:90px; height:92%; float:left}',
'#settingsSidebar > span {display:block; margin-bottom:5px; width:100px; font-size:1em; cursor:pointer}',
'.settingsPanel {position:absolute; top:0;left:0; display:none; width:100%; height:100%; font-size:11px}',
'.settingsPanel > div {margin:15px 5px; position:relative; background:#CCFFFA; overflow:auto; padding:6px 10px}',
'.settingsSelected {background:aquamarine}',
'.ble {border:1px solid black; border-collapse:collapse;} .blec {padding:5px; text-align:left;}',
'.toLink {position:relative;}',
'.toLink:before {content:""; display:none; z-index:5; position:absolute; top:0; left:-6px; width:0; height:0;' +
'border-top:6px solid transparent; border-bottom:6px solid transparent; border-left:6px solid black}',
'.toLink:hover:before {display:block;}',
'.tooltip {position:absolute;top:0;right:calc(100% + 6px);text-align:left;transform:translateY(-20%);padding:5px;font-weight:normal;' +
'font-size:11px; line-height:1; display:none; background:black; color:white; box-shadow:0px 0px 6px 1px #fff}',
'meter {width:100%; position:relative; height:15px;}',
'meter:before, .ffmb {display:block; font-size:10px; font-weight:bold; color:black; content:attr(data-attr); position:absolute; top:1px}',
'meter:after, .ffma {display:block; font-size:10px; font-weight:bold; color:black; content:attr(value); position:absolute; top:1px; right:0}',
'#resultsTable button {height:14px; font-size:8px; border:1px solid; padding:0; background:transparent}',
'#resultsTable tbody td > div {display:table-cell}',
'#resultsTable tbody td > div:first-child {padding-right:2px; vertical-align:middle; white-space:nowrap}',
'button.disabled {position:relative}',
'button.disabled:before {content:""; display:none; z-index:5; position:absolute; top:-7px; left:50%; width:0; height:0;' +
'border-left:6px solid transparent; border-right:6px solid transparent; border-top:6px solid black; transform:translateX(-50%)}',
'button.disabled:after {content:"Exports are disabled while logged out."; display:none; z-index:5; position:absolute;' +
'top:-7px; left:50%; color:white; background:black; width:230px; padding:2px; transform:translate(-50%,-100%);' +
'box-shadow:0px 0px 6px 1px #fff; font-size:12px}',
'button.disabled:focus:before {display:block} button.disabled:focus:after {display:block}',
'.spinner {display: inline-block; animation: kfspin 0.7s infinite linear; font-weight:bold;}',
'@keyframes kfspin { 0% { transform: rotate(0deg) } 100% { transform: rotate(359deg) } }',
'.spinner:before{content:"*"}',
'.exhwtf {width:70px; background:black; color:white; vertical-align:top; border-radius:5px}',
'.ignored td {border:2px solid #00E5FF}',
'.includelisted td {border:3px dashed #008800}',
'.blocklisted td {border:3px solid #cc0000}',
],//}}}
fCss =
`#resultsTable tbody {font-size:${user.fontSize}px;}` +
`.shine td {border:1px dotted #fff; font-size:${(+user.fontSize) + (+user.shineOffset)}px; font-weight:bold}`,
//{{{ body
body = `
Auto-refresh delay:
Pages to scrape:
Results per page:
`,//}}}
head = `${DOC_TITLE}` +
`` +
`` +
``;
document.head.innerHTML = head;
document.body.innerHTML = body;
this.elkeys = Object.keys(titles);
return this;
},//}}} Interface::draw
init: function() {//{{{
this.panel = {}; this.buttons = {};
var get = (q,all) => document['querySelector' + (all ? 'All': '')](q),
sortdirs = get('#sortdirs'),
moveSortdirs = function(node) {
if (!node.checked) { sortdirs.style.display = 'none'; return; }
sortdirs.style.display = 'inline';
sortdirs.remove();
node.parentNode.insertBefore(sortdirs, node.nextSibling);
},
kdFn = e => { if (e.keyCode === kb.ENTER) setTimeout(() => this.buttons.main.click(), 30); },
optChangeFn = function(e) {//{{{
var tag = e.target.tagName, type = e.target.type, id = e.target.id,
isChecked = e.target.checked, name = e.target.name, value = e.target.value;
switch(tag) {
case 'SELECT':
if (id === 'soundSelect')
this.user.notifySound[1] = e.target.value;
else
this.user[id] = e.target.selectedIndex;
break;
case 'INPUT':
switch(type) {
case 'number':
case 'text':
this.user[id] = value; break;
case 'radio':
Array.from(get(`input[name=${name}]`,true))
.forEach(v => { this.user[v.id] = v.checked; get(`label[for=${v.id}]`).classList.toggle('checked'); });
break;
case 'checkbox':
if (name === 'sort') {
Array.from(get(`input[name=${name}]`,true)).forEach(v => {
if (e.target !== v) v.checked = false;
get(`label[for=${v.id}]`).className = v.checked ? 'checked' : '';
this.user[v.id] = v.checked;
});
moveSortdirs(e.target);
break;
} else if (id === 'sound') {
this.user.notifySound[0] = isChecked;
e.target.nextElementSibling.style.display = isChecked ? 'inline' : 'none';
}
this.user[id] = isChecked;
get(`label[for=${id}]`).classList.toggle('checked');
break;
} break;
}
Settings.save();
}.bind(this);//}}}
'ding squee'.split(' ').forEach(v => get(`#${v}`).volume = this.user.volume[v]);
Themes.apply(this.user.themes.name);
if (this.isLoggedout) get('#loggedout').textContent = 'you are currently logged out of mturk';
// get references to control panel elements and set up click events
this.Status = {
node: get('#status').firstChild,
push: function(t) { this.node.innerHTML = t; },
append: function(t) { this.node.innerHTML += t; },
cd: function() { this.node.innerHTML = this.node.innerHTML.replace(/\d+(?= seconds)/, m => +m-1); }
};
for (var k of this.elkeys) {
if (k === 'mainlink') continue;
this.panel[k] = document.getElementById(k);
this.panel[k].onchange = optChangeFn;
if (k === 'pay' || k === 'search') this.panel[k].onkeydown = kdFn;
if ((k === 'sortPay' || k === 'sortAll') && this.panel[k].checked) moveSortdirs(this.panel[k]);
}
// get references to buttons
Array.from(get('button',true)).forEach(v => this.buttons[v.id.slice(3).toLowerCase()] = v);
// set up button click events
this.buttons.main.onclick = function(e) {
e.target.textContent = e.target.textContent === 'Start' ? 'Stop' : 'Start';
Core.run();
};
this.buttons.hide.onclick = function(e) {
get('#controlpanel').classList.toggle('hiddenpanel');
e.target.textContent = e.target.textContent === 'Hide Panel' ? 'Show Panel' : 'Hide Panel';
};
this.buttons.blocks.onclick = () => { this.toggleOverflow('on'); new Editor('ignore'); };
this.buttons.incs.onclick = () => { this.toggleOverflow('on'); new Editor('include'); };
this.buttons.ignores.onclick = () => Array.from(get('.ignored:not(.blocklisted)',true)).forEach(v => v.classList.toggle('hidden'));
this.buttons.settings.onclick = () => { this.toggleOverflow('on'); Settings.draw().init(); };
get('#hideBlock').addEventListener('change', () => Array.from(get('.blocklisted',true)).forEach(v => v.classList.toggle('hidden')));
window.onblur = document.body.onblur = () => this.focused = false;
window.onfocus = document.body.onfocus = () => { this.focused = true; this.resetTitle(); };
}//}}} Interface::init
},//}}} Interface
Editor = function(type) {//{{{
if (!type) return { setDefaultBlocks: setDefaultBlocks };
Interface.toggleOverflow('on');
this.node = document.body.appendChild(document.createElement('DIV'));
this.node.classList.add('pop');
this.die = () => {Interface.toggleOverflow('off'); this.node.remove();};
this.type = type;
this.caller = arguments[1] || null;
function setDefaultBlocks() { return localStorage.setItem('scraper_ignore_list',
'oscar smith^diamond tip research llc^jonathan weber^jerry torres^' +
'crowdsource^we-pay-you-fast^turk experiment^jon brelig^p9r^scoutit'); }
switch(type) {
case 'include':
case 'ignore':
if (type === 'ignore' && !localStorage.getItem('scraper_ignore_list')) setDefaultBlocks();
var titleText = type === 'ignore' ?
'BLOCKLIST - Edit the blocklist with what you want to ignore/hide. Separate requester names and HIT titles with the ' +
'^ character. After clicking "Save", you\'ll need to scrape again to apply the changes.'
: 'INCLUDELIST - Focus the results on your favorite requesters. Separate requester names and HIT titles with the ' +
'^ character. When the "Restrict to includelist" option is selected, ' +
'HIT Scraper only shows results matching the includelist.';
this.node.innerHTML = '
' + titleText + '
' +
'' +
''+
'';
this.node.querySelector('#edSave').onclick = () => {
localStorage.setItem(`scraper_${type}_list`, this.node.querySelector('textarea').value.trim()); this.die();
}; break;
case 'theme':
var dlbody = [], _th = Settings.user.themes, split = obj => {
var a = []; for (var k in obj) if (obj.hasOwnProperty(k)) a.push({k:k, v:obj[k]});
return a.sort((a,b) => a.k < b.k ? -1 : 1);
}, _colors = split(_th.colors[_th.name]),
define = k => '
' + _dd[k] + '
',
_dd = {//{{{
highlight:'Distinguishes between active and inactive states in the control panel',
background:'Background color',
accent:'Color of spacer text (and control panel buttons on themes other than \'classic\')',
bodytable:'Default color of text elements in the results table (this is ignored if HIT coloring is set to \'cell\')',
cpBackground:'Background color of the control panel',
toHigh:'Color for results with high TO',
toGood:'Color for results with good TO',
toAverage:'Color for results with average TO',
toLow:'Color for results with low TO',
toPoor:'Color for results with poor TO',
toNone:'Color for results with no TO',
hitDB:'Designates that a match was found in your HITdb',
nohitDB:'Designates that a match was not found in your HITdb',
unqualified:'Designates that you do not have the qualifications necessary to work on the HIT',
reqmaster:'Designates HITs that require Masters',
nomaster:'Designates HITs that do not require Masters',
defaultText:'Default text color',
inputText:'Color of input boxes in the control panel',
secondText:'Color for text used on selected control panel items',
link:'Default color of unvisited links',
vlink:'Default color of visited links',
export:'Color of buttons in the results table--export and block buttons',
hover:'Color of control panel options on mouseover',
};//}}}
for (var r of _colors)
dlbody.push(`
' +
'/r/HitsWorthTurkingFor Export: Use the buttons on the left for single-click copying. ' +
'Before you post, please remember to replace "COMTIME" with how long it took you to complete the HIT.
' +
'' +
' ' + '' +
' ' + '' +
' ' + '' +
' ' + '' +
' ' +
'';
var copyfn = function(e) { e.target.nextSibling.select(); document.execCommand('copy'); };
Array.from(this.node.querySelectorAll('.exhwtf')).forEach(v => v.onclick = copyfn);
this.node.querySelector('#exClose').onclick = this.die;
this.node.querySelector('textarea').setSelectionRange(tIndex, tIndex+7);
};//}}}
switch(this.caller.textContent.toLowerCase()){
case 'vb':
_vb();break;
case 'irc':
_irc();break;
case 'hwtf':
_hwtf();break;
}
},//}}} Exporter
HITStorage = {//{{{
db: null,
attach: function(name) {//{{{
var dbh = window.indexedDB.open(name);
dbh.onversionchange = e => { e.target.result.close(); console.info('DB connection closed by external source'); };
dbh.onsuccess = e => this.db = e.target.result;
},//}}} HITStorage::attach
test: function(node) {//{{{
if (!this.db || !this.db.objectStoreNames.contains('HIT')) return;
this.db.transaction('HIT','readonly').objectStore('HIT').index(node.dataset.index).get(node.dataset.value)
.onsuccess = e => { if (e.target.result) node.className = node.className.replace(/no/,''); };
},//}}} HITStorage::test
query: function(node) {//{{{
var range = window.IDBKeyRange.only(node.dataset.value), results = [];
return new Promise((a,r) => {
if (!this.db || !this.db.objectStoreNames.contains('HIT')) r(0);
this.db.transaction('HIT','readonly').objectStore('HIT').index(node.dataset.index).openCursor(range)
.onsuccess = e => {
if (e.target.result) {
results.push(e.target.result.value);
e.target.result.continue();
} else
a(results.sort((a,b) => a.date > b.date ? 1 : -1));
};
});
}//}}} HITStorage::query
},//}}} HITStorage
FileHandler = {//{{{
exports: function() {//{{{
var obj = {
settings: JSON.stringify(Settings.user),
ignore_list: localStorage.getItem('scraper_ignore_list') || '',
include_list: localStorage.getItem('scraper_include_list') || '',
},
blob = new Blob([JSON.stringify(obj)], {type: 'application/json'}),
a = document.body.appendChild(document.createElement('a'));
a.href = URL.createObjectURL(blob);
a.download = 'hitscraper_settings.json';
a.click(); a.remove();
},//}}}
imports: function(e) {//{{{
var f = e.target.files,
invalid = () => Settings.main.querySelector('#eisStatus').textContent = 'Invalid file.';
if (!f.length) return;
if (!f[0].name.includes('json')) return invalid();
var reader = new FileReader();
reader.readAsText(f[0]);
reader.onload = function() {
var obj;
try { obj = JSON.parse(this.result); } catch(err) { return invalid(); }
for (var key of ['settings', 'ignore_list', 'include_list']) {
if (key in obj && typeof obj[key] === 'string')
localStorage.setItem('scraper_' + key, obj[key]);
}
initialize();
};
}//}}}
};//}}} FileHandler
console.log('HS hook');
if (document.getElementById('control_panel')) {
if (confirm('Another version of HITScraper was detected and has already claimed this page. Open HITScraper in a new tab?'))
window.open('https://www.mturk.com/mturk/findhits?match=true?hit_scraper-dev');
} else {
initialize();
HITStorage.attach('HITDB');
}
function initialize() {//{{{
Settings.user = Object.assign({}, Settings.defaults, JSON.parse(localStorage.getItem('scraper_settings')));
Interface.draw().init();
}//}}}
function createTooltip(type,obj) {//{{{
var html, bullet = li => `
${li}
`,
reason = Settings.user.disableTO ? bullet('TO disabled in user settings')
: (Interface.isLoggedout ? bullet('Cannot retrieve TO while logged out')
: (obj === '' ? bullet('Requester has not been reviewed yet') : bullet('Invalid response from server'))),
_genMeters = function() {
var attrmap = { comm: 'Communicativity', pay: 'Generosity', fair: 'Fairness', fast: 'Promptness' }, html = [];
for (var k in attrmap) { if (attrmap.hasOwnProperty(k)) {
html.push(``);
}}
if (ISFF) // firefox is shitty and doesn't support ::after/::before pseudo-elements on meter elements
html.forEach((v,i,a) => a[i] = '
' + v +
`${attrmap[Object.keys(attrmap)[i]]}` +
`${obj.attrs[Object.keys(attrmap)[i]]}
`);
return html.join('');
};
if (!obj) {
html = `
Turkopticon data unavailable:${reason}
`;
} else if (type === 'to')
html = `
${obj.name} Reviews: ${obj.reviews} | TOS Flags: ${obj.tos_flags}