· 6 years ago · May 10, 2019, 06:10 AM
1// ==UserScript==
2// @name HIT Scraper WITH EXPORT (David Edit)
3// @author David Liang
4// @description Snag HITs. mturk.
5// @include https://worker.mturk.com/hit_scraper/David
6// @version 4.1.4
7// @grant none
8// ==/UserScript==
9var runCtr = 0;
10
11(function() {
12 'use strict';
13
14 const ENV = Object.freeze({
15 LEGACY : 'www.mturk.com',
16 NEXT : 'worker.mturk.com',
17 HOST : window.location.hostname,
18 ORIGIN : window.location.origin,
19 ISFF : Boolean(window.sidebar),
20 VERSION: '4.1.4'
21 });
22 const URL_SELF = 'https://greasyfork.org/en/scripts/10615-hit-scraper-with-export#ugTop';
23 const DOC_TITLE = 'mTurk HIT Scraper';
24 const TO_BASE = 'https://turkopticon.ucsd.edu/';
25 const TO_REPORTS = TO_BASE + 'reports?id=';
26 const TO_API = TO_BASE + 'api/multi-attrs.php?ids=';
27
28 const ico = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wsTDwctGwJAtQAAAYRJREFUOMudkz1rWmEUx3/6gFeC1A5p6RJByGTGTpEM3iHfwMmxfggH1yyB69BFyFAy5FIQ6wdw9CVxUOhQXFykYFcTLvEaiuafQU0wXoPkwIHnOS8/znPOeWAh18AU0I7qA61lLjfbAl3X1Ww2k+u620AtgP9BTsdx1O/3lUqlNBgM5DhOEGAaWh7eLeEgYywWo1QqMRwO8X2fdrtNsVjcCtkorVKpSJKy2awsy1Imk1Gv19vWh02j53mSpHQ6vctENo2dTkcrGY/HqlarSiaTuwMSiYRqtZomk8kzqNvt7g5YqTFGtm1LkjzPC4wJnEKz2cS2bSKRCMYYAOr1+htTCFtr1EKhoEajId/3NRqNVC6XFY/aHF/69LyKfE9bivlikfA72D+H8DHh8e3PyOfAMfJjDj58YoMjvP4bQPVz9g4MU7Ifh7y2EDJgoHB/BqQ3fLuDzAdz14fIXaP6w+okvT8jnRFub+r0mTr6+bmJzVVgLmKw5w1ER/SQiH4O6fw80AJ4AC20qRDxx28IAAAAASUVORK5CYII=',
29 audio0 = 'T2dnUwACAAAAAAAAAAB8mpoRAAAAAFLKt9gBHgF2b3JiaXMAAAAAARErAAAAAAAAkGUAAAAAAACZAU9nZ1MAAAAAAAAAAAAAfJqaEQEAAACHYsq6Cy3///////////+1A3ZvcmJpcx0AAABYaXBoLk9yZyBsaWJWb3JiaXMgSSAyMDA1MDMwNAAAAAABBXZvcmJpcxJCQ1YBAAABAAxSFCElGVNKYwiVUlIpBR1jUFtHHWPUOUYhZBBTiEkZpXtPKpVYSsgRUlgpRR1TTFNJlVKWKUUdYxRTSCFT1jFloXMUS4ZJCSVsTa50FkvomWOWMUYdY85aSp1j1jFFHWNSUkmhcxg6ZiVkFDpGxehifDA6laJCKL7H3lLpLYWKW4q91xpT6y2EGEtpwQhhc+211dxKasUYY4wxxsXiUyiC0JBVAAABAABABAFCQ1YBAAoAAMJQDEVRgNCQVQBABgCAABRFcRTHcRxHkiTLAkJDVgEAQAAAAgAAKI7hKJIjSZJkWZZlWZameZaouaov+64u667t6roOhIasBADIAAAYhiGH3knMkFOQSSYpVcw5CKH1DjnlFGTSUsaYYoxRzpBTDDEFMYbQKYUQ1E45pQwiCENInWTOIEs96OBi5zgQGrIiAIgCAACMQYwhxpBzDEoGIXKOScggRM45KZ2UTEoorbSWSQktldYi55yUTkompbQWUsuklNZCKwUAAAQ4AAAEWAiFhqwIAKIAABCDkFJIKcSUYk4xh5RSjinHkFLMOcWYcowx6CBUzDHIHIRIKcUYc0455iBkDCrmHIQMMgEAAAEOAAABFkKhISsCgDgBAIMkaZqlaaJoaZooeqaoqqIoqqrleabpmaaqeqKpqqaquq6pqq5seZ5peqaoqp4pqqqpqq5rqqrriqpqy6ar2rbpqrbsyrJuu7Ks256qyrapurJuqq5tu7Js664s27rkearqmabreqbpuqrr2rLqurLtmabriqor26bryrLryratyrKua6bpuqKr2q6purLtyq5tu7Ks+6br6rbqyrquyrLu27au+7KtC7vourauyq6uq7Ks67It67Zs20LJ81TVM03X9UzTdVXXtW3VdW1bM03XNV1XlkXVdWXVlXVddWVb90zTdU1XlWXTVWVZlWXddmVXl0XXtW1Vln1ddWVfl23d92VZ133TdXVblWXbV2VZ92Vd94VZt33dU1VbN11X103X1X1b131htm3fF11X11XZ1oVVlnXf1n1lmHWdMLqurqu27OuqLOu+ruvGMOu6MKy6bfyurQvDq+vGseu+rty+j2rbvvDqtjG8um4cu7Abv+37xrGpqm2brqvrpivrumzrvm/runGMrqvrqiz7uurKvm/ruvDrvi8Mo+vquirLurDasq/Lui4Mu64bw2rbwu7aunDMsi4Mt+8rx68LQ9W2heHVdaOr28ZvC8PSN3a+AACAAQcAgAATykChISsCgDgBAAYhCBVjECrGIIQQUgohpFQxBiFjDkrGHJQQSkkhlNIqxiBkjknIHJMQSmiplNBKKKWlUEpLoZTWUmotptRaDKG0FEpprZTSWmopttRSbBVjEDLnpGSOSSiltFZKaSlzTErGoKQOQiqlpNJKSa1lzknJoKPSOUippNJSSam1UEproZTWSkqxpdJKba3FGkppLaTSWkmptdRSba21WiPGIGSMQcmck1JKSamU0lrmnJQOOiqZg5JKKamVklKsmJPSQSglg4xKSaW1kkoroZTWSkqxhVJaa63VmFJLNZSSWkmpxVBKa621GlMrNYVQUgultBZKaa21VmtqLbZQQmuhpBZLKjG1FmNtrcUYSmmtpBJbKanFFluNrbVYU0s1lpJibK3V2EotOdZaa0ot1tJSjK21mFtMucVYaw0ltBZKaa2U0lpKrcXWWq2hlNZKKrGVklpsrdXYWow1lNJiKSm1kEpsrbVYW2w1ppZibLHVWFKLMcZYc0u11ZRai621WEsrNcYYa2415VIAAMCAAwBAgAlloNCQlQBAFAAAYAxjjEFoFHLMOSmNUs45JyVzDkIIKWXOQQghpc45CKW01DkHoZSUQikppRRbKCWl1losAACgwAEAIMAGTYnFAQoNWQkARAEAIMYoxRiExiClGIPQGKMUYxAqpRhzDkKlFGPOQcgYc85BKRljzkEnJYQQQimlhBBCKKWUAgAAChwAAAJs0JRYHKDQkBUBQBQAAGAMYgwxhiB0UjopEYRMSielkRJaCylllkqKJcbMWomtxNhICa2F1jJrJcbSYkatxFhiKgAA7MABAOzAQig0ZCUAkAcAQBijFGPOOWcQYsw5CCE0CDHmHIQQKsaccw5CCBVjzjkHIYTOOecghBBC55xzEEIIoYMQQgillNJBCCGEUkrpIIQQQimldBBCCKGUUgoAACpwAAAIsFFkc4KRoEJDVgIAeQAAgDFKOSclpUYpxiCkFFujFGMQUmqtYgxCSq3FWDEGIaXWYuwgpNRajLV2EFJqLcZaQ0qtxVhrziGl1mKsNdfUWoy15tx7ai3GWnPOuQAA3AUHALADG0U2JxgJKjRkJQCQBwBAIKQUY4w5h5RijDHnnENKMcaYc84pxhhzzjnnFGOMOeecc4wx55xzzjnGmHPOOeecc84556CDkDnnnHPQQeicc845CCF0zjnnHIQQCgAAKnAAAAiwUWRzgpGgQkNWAgDhAACAMZRSSimllFJKqKOUUkoppZRSAiGllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimVUkoppZRSSimllFJKKaUAIN8KBwD/BxtnWEk6KxwNLjRkJQAQDgAAGMMYhIw5JyWlhjEIpXROSkklNYxBKKVzElJKKYPQWmqlpNJSShmElGILIZWUWgqltFZrKam1lFIoKcUaS0qppdYy5ySkklpLrbaYOQelpNZaaq3FEEJKsbXWUmuxdVJSSa211lptLaSUWmstxtZibCWlllprqcXWWkyptRZbSy3G1mJLrcXYYosxxhoLAOBucACASLBxhpWks8LR4EJDVgIAIQEABDJKOeecgxBCCCFSijHnoIMQQgghREox5pyDEEIIIYSMMecghBBCCKGUkDHmHIQQQgghhFI65yCEUEoJpZRSSucchBBCCKWUUkoJIYQQQiillFJKKSGEEEoppZRSSiklhBBCKKWUUkoppYQQQiillFJKKaWUEEIopZRSSimllBJCCKGUUkoppZRSQgillFJKKaWUUkooIYRSSimllFJKCSWUUkoppZRSSikhlFJKKaWUUkoppQAAgAMHAIAAI+gko8oibDThwgMQAAAAAgACTACBAYKCUQgChBEIAAAAAAAIAPgAAEgKgIiIaOYMDhASFBYYGhweICIkAAAAAAAAAAAAAAAABE9nZ1MABAgkAAAAAAAAfJqaEQIAAAB89IOyJjhEQUNNRE5TRENHS0xTRllHSEpISUdORk1GSEdISUNHP0ZHS1IhquPYHv5OAgC/7wFATp2pUBdX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',
30 /*'*/
31 audio1 = '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VVVVVVVVVVVVVQBqs0jMnBWmgKng1FRU32lmZmb+N6/V+w+MygnAXTBSU3/Ffjw4KGwvhYijgbg038TRQL8U3//xv5cO4FZf/z8bdj8V3CpuILkwNl5P8KE+53G3eCoQUAIQxdpeAGCps5MdqtGpNehUFBQFS2qx1bDEzNSDATAKKlIBjsJGEQBFZnmMUrStzKW+pXKVDOZ+hnQ2jloarUNK2iGVjPlUVcHRF4NCI9e2HcNCJ4NQ0eEQlnbmh0FYaUxBTUUzLjk5LjNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kkScD/K0GL+QyBnCYEgHsjBlXAAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=';
32
33 let scraperHistory;
34 const defaults = { //{{{
35 themes : {//{{{
36 whisper : {
37 highlight : '#1F3847',
38 background : '#232A2F',
39 accent : '#00ffff',
40 bodytable : '#AFCCDE',
41 cpBackground: '#394752',
42 toHigh : '#009DFF',
43 toGood : '#40B6FF',
44 toAverage : '#7ACCFF',
45 toLow : '#B5E3FF',
46 toPoor : '#DEF1FC',
47 hitDB : '#CADA95',
48 nohitDB : '#DA95A8',
49 unqualified : '#808080',
50 reqmaster : '#C1E1F6',
51 nomaster : '#D6C1F6',
52 defaultText : '#AFCCDE',
53 inputText : '#98D6D6',
54 secondText : '#808080',
55 link : '#003759',
56 vlink : '#40F0F0',
57 toNone : '#AFCCDE',
58 export : '#86939C',
59 hover : '#1E303B'
60 },
61 solDark : {
62 highlight : '#657b83',
63 background : '#002b36',
64 accent : '#b58900',
65 bodytable : '#839496',
66 cpBackground: '#073642',
67 toHigh : '#859900',
68 toGood : '#A2BA00',
69 toAverage : '#b58900',
70 toLow : '#cb4b16',
71 toPoor : '#dc322f',
72 hitDB : '#82D336',
73 nohitDB : '#D33682',
74 unqualified : '#9F9F9F',
75 reqmaster : '#B58900',
76 nomaster : '#839496',
77 defaultText : '#839496',
78 inputText : '#eee8d5',
79 secondText : '#93a1a1',
80 link : '#000000',
81 vlink : '#6c71c4',
82 toNone : '#839496',
83 export : '#CCC6B4',
84 hover : '#122A30'
85 },
86 solLight: {
87 highlight : '#657b83',
88 background : '#fdf6e3',
89 accent : '#b58900',
90 bodytable : '#657b83',
91 cpBackground: '#eee8d5',
92 toHigh : '#859900',
93 toGood : '#A2BA00',
94 toAverage : '#b58900',
95 toLow : '#cb4b16',
96 toPoor : '#dc322f',
97 hitDB : '#82D336',
98 nohitDB : '#36D0D3',
99 unqualified : '#9F9F9F',
100 reqmaster : '#B58900',
101 nomaster : '#6C71C4',
102 defaultText : '#657b83',
103 inputText : '#6FA3A3',
104 secondText : '#A6BABA',
105 link : '#000000',
106 vlink : '#6c71c4',
107 toNone : '#657b83',
108 export : '#000000',
109 hover : '#C7D2D6'
110 },
111 classic : {
112 highlight : '#30302F',
113 background : '#131313',
114 accent : '#94704D',
115 bodytable : '#000000',
116 cpBackground: '#131313',
117 toHigh : '#66CC66',
118 toGood : '#ADFF2F',
119 toAverage : '#FFD700',
120 toLow : '#FF9900',
121 toPoor : '#FF3030',
122 hitDB : '#66CC66',
123 nohitDB : '#FF3030',
124 unqualified : '#9F9F9F',
125 reqmaster : '#551A8B',
126 nomaster : '#0066CC',
127 defaultText : '#94704D',
128 inputText : '#000000',
129 secondText : '#997553',
130 link : '#0000FF',
131 vlink : '#800080',
132 toNone : '#d3d3d3',
133 export : '#000000',
134 hover : '#21211F'
135 },
136 deluge : {
137 highlight : '#1F3847',
138 background : '#434e56',
139 accent : '#fbde2d',
140 bodytable : '#f8f8f8',
141 cpBackground: '#384147',
142 toHigh : '#6FFA3C',
143 toGood : '#D9FC35',
144 toAverage : '#fbde2d',
145 toLow : '#FAB050',
146 toPoor : '#FA6F50',
147 hitDB : '#d8fa3c',
148 nohitDB : '#DA95A8',
149 unqualified : '#ADC6EE',
150 reqmaster : '#BFADEE',
151 nomaster : '#ADEEDF',
152 defaultText : '#f8f8f8',
153 inputText : '#D8FA3C',
154 secondText : '#ADC6EE',
155 link : '#99004F',
156 vlink : '#DCEEAD',
157 toNone : '#97A167',
158 export : '#ADC6EE',
159 hover : '#426075'
160 }
161 },//}}}
162 vbTemplate: '[table][tr][td][b]Title:[/b] [URL=${previewLink}]${title}[/URL] | [URL=${pandaLink}]PANDA[/URL]\n' +
163 '[b]Requester:[/b] [URL=${requesterLink}]${requesterName}[/URL] [${requesterId}] ' +
164 '([URL=' + TO_REPORTS + '${requesterId}]TO[/URL])\n' +
165 '[b]TO Ratings:[/b]\n${toVerbose}\n${toFoot}\n' +
166 '[b]Description:[/b] ${description}\n[b]Time:[/b] ${time}\n[b]HITs Available:[/b] ${numHits}\n' +
167 '[b]Reward:[/b] [COLOR=green][b]${reward}[/b][/COLOR]\n' +
168 '[b]Qualifications:[/b] ${quals}[/td][/tr][/table]'
169 },//}}}
170
171 Settings = {//{{{
172 defaults : {//{{{
173 themes : { name: 'classic', colors: defaults.themes },
174 colorType : 'sim',
175 sortType : 'adj',
176 toWeights : { comm: '1', pay: '3', fair: '3', fast: '1' },
177 exportVb : true,
178 exportIrc : true,
179 exportHwtf : true,
180 notifySound : [false, 'ding'],
181 notifyBlink : false,
182 notifyTaskbar : false,
183 volume : { ding: 1, squee: 1 },
184 wildblocks : true,
185 showCheckboxes: true,
186 hitColor : 'link',
187 fontSize : 11,
188 shineOffset : 1,
189
190 refresh : '0',
191 pages : '1',
192 skips : false,
193 resultsPerPage: '50',
194 batch : '',
195 pay : '',
196 qual : true,
197 monly : false,
198 mhide : false,
199 searchBy : 0,
200 invert : false,
201 shine : '300',
202 minTOPay : '',
203 hideNoTO : false,
204 onlyViable : false,
205 disableTO : false,
206 sortPay : false,
207 sortAll : false,
208 search : '',
209 hideBlock : true,
210 onlyIncludes : false,
211 shineInc : true,
212 sortAsc : false,
213 sortDsc : true,
214 gbatch : false,
215 bubbleNew : false,
216
217 vbTemplate: defaults.vbTemplate,
218 vbSym : '\u2605' // star
219 },//}}}
220 user: {}, save: function() { localStorage.setItem('scraper_settings', JSON.stringify(this.user)); },
221 draw : function() {//{{{
222 var
223 _ccs = 'https://greasyfork.org/en/scripts/3118-mmmturkeybacon-color-coded-search-with-checkpoints',
224 _hwtf = 'https://www.reddit.com/r/HITsWorthTurkingFor',
225 _general = //{{{
226 `<div>
227<div style="float:left; margin-left:15px">
228<span style="position:relative; left:-8px"><b>Export Buttons</b></span>
229<p><label for="exportVb" style="float:left; width:51px">vBulletin</label>
230<input id="exportVb" name="export" value="vb" type="checkbox" ${this.user.exportVb ? 'checked' : ''}/></p>
231<p><label for="exportIrc" style="float:left; width:51px">IRC</label>
232<input id="exportIrc" name="export" value="irc" type="checkbox" ${this.user.exportIrc ? 'checked' : ''}/></p>
233<p><label for="exportHwtf" style="float:left; width:51px">Reddit</label>
234<input id="exportHwtf" name="export" value="hwtf" type="checkbox" ${this.user.exportHwtf ? 'checked' : ''}/></p>
235</div>
236<section style="margin-left:110px">
237<span style="position:relative; left:10px"><i>vBulletin</i></span><br>
238Show a button in the results to export the specified HIT with vBulletin formatted text to share on forums.
239</section><section style="margin-left:110px">
240<span style="position:relative; left:10px"><i>IRC</i></span><br>
241Show a button in the results to export the specified HIT streamlined for sharing on IRC.
242</section><section style="margin-left:110px">
243<span style="position:relative; left:10px"><i>Reddit</i></span><br>
244Show a button in the results to export the specified HIT for sharing on Reddit, formatted to
245<a style="color:black" href="${_hwtf}" target="_blank">r/HITsWorthTurkingFor</a> standards.
246</section>
247</div><div>
248<div style="float:left; margin-left:15px">
249<span style="position:relative; left:-8px"><b>Bubble New HITs</b></span>
250<p><label for="bubbleNew" style="float:left; width:51px">Enable</label>
251<input id="bubbleNew" type="checkbox" ${this.user.bubbleNew ? 'checked' : ''}></p>
252</div>
253<section style="margin-left:100px; margin-top:23px">
254When this option is enabled, new HITs will always be placed at the top of the results table.
255</section>
256</div><div>
257<div style="float:left; margin-left:15px">
258<span style="position:relative; left:-8px"><b>Color Type</b></span>
259<p><label for="ctSim" style="float:left; width:51px">Simple</label>
260<input id="ctSim" type="radio" name="colorType" value="sim"
261${this.user.colorType === 'sim' ? 'checked' : ''}/></p>
262<p><label for="ctAdj" style="float:left; width:51px">Adjusted</label>
263<input id="ctAdj" type="radio" name="colorType" value="adj"
264${this.user.colorType === 'adj' ? 'checked' : ''}/></p>
265</div>
266<section style="margin-left:100px">
267<span style="position:relative; left:10px"><i>simple</i></span><br>HIT Scraper will use a simple weighted average to
268determine the overall TO rating and colorize results using that value. Use this setting to make coloring consistent between
269HIT Scraper and <a style="color:black" href="${_ccs}" target="_blank">Color Coded Search</a>.
270</section><section style="margin-left:100px">
271<span style="position:relative; left:10px;"><i>adjusted</i></span><br>HIT Scraper will calculate a Bayesian adjusted average
272based on confidence of the TO rating to colorize results. Confidence is proportional to the number of reviews.
273</section>
274</div><div>
275<div style="float:left; margin-left:15px">
276<span style="position:relative; left:-8px"><b>Sort Type</b></span>
277<p><label for="stSim" style="float:left; width:51px">Simple</label>
278<input id="stSim" type="radio" name="sortType" value="sim"
279${this.user.sortType === 'sim' ? 'checked' : ''}/></p>
280<p><label for="stAdj" style="float:left; width:51px">Adjusted</label>
281<input id="stAdj" type="radio" name="sortType" value="adj"
282${this.user.sortType === 'adj' ? 'checked' : ''}/></p>
283</div>
284<section style="margin-left:100px">
285<span style="position:relative; left:10px"><i>simple</i></span><br>
286HIT Scraper will sort results based simply on value regardless of the number of reviews.
287</section><section style="margin-left:100px">
288<span style="position:relative; left:10px;"><i>adjusted</i></span><br>HIT Scraper will use a Bayesian adjusted rating
289based on reliability (i.e. confidence) of the data. It factors in the number of reviews such that, for example,
290a requester with 100 reviews rated at 4.6 will rightfully be ranked higher than a requester with 3 reviews rated at 5.
291This gives a more accurate representation of the data.
292</section>
293</div><div>
294<div style="float:left; margin-left:15px">
295<span style="position:relative; left:-8px"><b>Alert Volume</b></span>
296<p><label style="float:left;width:45px">Ding</label>
297<input name="ding" type="range" value=${this.user.volume.ding} max="1" step="0.02" min="0" />
298<span style="padding-left:10px">${Math.floor(this.user.volume.ding * 100)}%</span></p>
299<p><label style="float:left;width:45px">Squee</label>
300<input name="squee" type="range" value=${this.user.volume.squee} max="1" step="0.02" min="0" />
301<span style="padding-left:10px">${Math.floor(this.user.volume.squee * 100)}%</span></p>
302</div>
303</div><div>
304<div style="float:left; margin-left:15px">
305<span style="position:relative; left:-8px"><b>TO Weighting</b></span>
306<p><label for="comm" style="float:left; width:45px">comm</label>
307<input id="comm" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.comm} style="width:40px"/></p>
308<p><label for="pay" style="float:left; width:45px">pay</label>
309<input id="pay" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.pay} style="width:40px"/></p>
310<p><label for="fair" style="float:left; width:45px">fair</label>
311<input id="fair" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.fair} style="width:40px"/></p>
312<p><label for="fast" style="float:left; width:45px">fast</label>
313<input id="fast" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.fast} style="width:40px"/></p>
314</div>
315<section style="margin-left:110px; padding:10px">
316Specify weights for TO attributes to place greater importance on certain attributes over others.
317<p>The default values, [1, 3, 3, 1], ensure consistency between HIT Scraper and
318<a style="color:black" href="${_ccs}" target="_blank">Color Coded Search</a>;
319recommended values for adjusted coloring are [1, 6, 3.5, 1].</p>
320</section>
321</div>`,//}}}
322 _appearance =//{{{
323 `<div>
324<div style="float:left; margin-left:15px">
325<span style="position:relative;left:-8px"><b>Display Checkboxes</b></span>
326<p><label for="checkshow" style="float:left;width:51px">Show</label>
327<input id="checkshow" type="radio" name="checkbox" value="true"
328${this.user.showCheckboxes ? 'checked' : ''} /></p>
329<p><label for="checkhide" style="float:left;width:51px">Hide</label>
330<input id="checkhide" type="radio" name="checkbox" value="false"
331${this.user.showCheckboxes ? '' : 'checked'} /></p>
332</div>
333<section style="margin-left:133px">
334<span style="position:relative;left:10px"><i>show</i></span><br>
335Shows all checkboxes and radio inputs on the control panel for sake of clarity.
336</section><section style="margin-left:133px">
337<span style="position:relative;left:10px"><i>hide</i></span><br>
338Hides checkboxes and radio inputs for a cleaner, neater appearance. Their visibility is not required for proper
339operation; all options can still be toggled while hidden.
340</section>
341</div><div>
342<div style="float:left;margin-left:15px">
343<span style="position:relative;left:-8px"><b>Themes</b></span>
344<p><select>
345<option value="classic" ${this.user.themes.name === 'classic' ? 'selected' : ''}>Classic</option>
346<option value="deluge" ${this.user.themes.name === 'deluge' ? 'selected' : ''}>Deluge</option>
347<option value="solDark" ${this.user.themes.name === 'solDark' ? 'selected' : ''}>Solarium:Dark</option>
348<option value="solLight" ${this.user.themes.name === 'solLight' ? 'selected' : ''}>Solarium:Light</option>
349<option value="whisper" ${this.user.themes.name === 'whisper' ? 'selected' : ''}>Whisper</option>` +
350 //<option value="random" ${this.user.themes.name === 'random' ? 'selected' : ''}>I'm Feelin'
351 // Lucky!</option>
352 `</select> <button id="thedit" style="cursor:pointer">Edit Current Theme</button></p>
353</div>
354</div><div>
355<div style="float:left;margin-left:15px">
356<span style="position:relative;left:-8px"><b>HIT Coloring</b></span>
357<p><label for="link" style="float:left;width:51px">Link</label>
358<input id="link" type="radio" name="hitColor" value="link"
359${this.user.hitColor === 'link' ? 'checked' : ''} /></p>
360<p><label for="cell" style="float:left;width:51px">Cell</label>
361<input id="cell" type="radio" name="hitColor" value="cell"
362${this.user.hitColor === 'cell' ? 'checked' : ''} /></p>
363</div>
364<section style="margin-left:100px;padding-top:10px">
365<span style="position:relative;left:10px"><i>link</i></span><br>
366Apply coloring based on Turkopticon reviews to all applicable links in the results table.
367</section><section style="margin-left:100px">
368<span style="position:relative;left:10px"><i>cell</i></span><br>
369Apply coloring based on Turkopticon reviews to the background of all applicable cells in the results table.
370</section>
371<p style="clear:both"><b>Note:</b> The Classic theme is exempt from these settings and will always colorize cells.</p>
372</div><div>
373<div style="float:left;margin-left:15px">
374<span style="position:relative;left:-8px"><b>Font Size</b></span>
375<p><input name="fontSize" type="number" min="5" value="${this.user.fontSize}" style="width:45px"></p>
376<span style="position:relative;left:-8px"><b>New HIT Offset</b></span>
377<p><input name="shineOffset" type="number" value="${this.user.shineOffset}" style="width:45px"></p>
378</div>
379<section style="margin-left:100px;margin-top:15px">
380Change the font size (measured in px) for text in the results table. Default is 11px.
381</section><section style="margin-left:100px;margin-top:40px;">
382Controls the font size of new HITs relative to the rest of the results. Default is 1px. <br />
383<i>Example:</i> With a font size of 11px and an offset of 1px, new HITs will be displayed at 12px.
384</section>
385</div>`,//}}}
386 _blocks = //{{{
387 `<div>
388<div style="float:left; margin-left:15px">
389<span style="position:relative; left:-8px"><b>Advanced Matching</b></span>
390<p><label for="wildblocks" style="float:left; width:95px">Allow Wildcards</label>
391<input id="wildblocks" type="checkbox" ${this.user.wildblocks ? 'checked' : ''}/></p>
392</div>
393<section style="margin-left:150px">
394Allows for the use of asterisks <code>(*)</code> as wildcards in the blocklist for simple glob matching. Any blocklist entry
395without an asterisk is treated the same as the default behavior--the entry must exactly match a HIT title or requester to
396trigger a block.
397<p><em>Wildcards have the potential to block more HITs than intended if using a pattern that's too generic.</em></p>
398<p>Matching is not case sensitive regardless of the wildcard setting. Entries without an opening asterisk are
399expected to match the beginning of a line, likewise, entries without a closing asterisk are expected to match
400the end of a line. Example usage below.</p>
401<table class="ble" style="left:-100px;position:relative;width:110%;">
402<tr>
403<th class="blec ble"></th>
404<th class="blec ble">Matches</th>
405<th class="blec ble" style="width:86px">Does not match</th>
406<th class="blec ble">Notes</th>
407</tr><tr>
408<td rowspan="2" class="blec ble"><code>foo*baz</code></td>
409<td class="blec ble">foo bar bat baz</td>
410<td class="blec ble">bar foo bat baz</td>
411<td rowspan="2" class="blec ble">no leading or closing asterisks; <code>foo</code> must be at the start of a line,
412and <code>baz</code> must be at the end of a line for a positive match</td>
413</tr><tr><td class="blec ble">foobarbatbaz</td><td class="blec ble">foo bar bat</td>
414</tr><tr>
415<td class="blec ble"><code>*foo</code></td>
416<td class="ble blec">bar baz foo</td>
417<td class="blec ble">foo baz</td>
418<td class="ble blec">matches and blocks any line ending in <code>foo</code></td>
419</tr><tr>
420<td class="blec ble"><code>foo*</code></td>
421<td class="ble blec">foo bat bar</td>
422<td class="ble blec">bat foo baz</td>
423<td class="ble blec">matches and blocks any line beginning with <code>foo</code></td>
424</tr><tr>
425<td class="ble blec" rowspan="4"><code>*bar*</code></td>
426<td class="ble blec">foo bar bat baz</td>
427<td class="ble blec" rowspan="4">foo bat baz</td>
428<td class="ble blec" rowspan="4">matches and blocks any line containing <code>bar</code></td>
429</tr><tr><td class="ble blec">bar bat baz</td>
430</tr><tr><td class="ble blec">foo bar</td>
431</tr><tr><td class="ble blec">foobatbarbaz</td>
432</tr><tr>
433<td class="ble blec"><code>** foo</code></td>
434<td class="ble blec">** foo</td>
435<td class="ble blec">** foo bar baz</td>
436<td class="ble blec">Multiple consecutive asterisks will be treated as a string rather than a wildcard. This makes it
437compatible with HITs using multiple asterisks in their titles, <i>e.g.</i>, <code>*** contains peanuts ***</code>.</td>
438</tr><tr>
439<td class="ble blec"><code>** *bar* ***</td>
440<td class="ble blec">** foo bar baz bat ***</td>
441<td class="ble blec">foo bar baz</td>
442<td class="ble blec">Consecutive asterisks used in conjunction with single asterisks.</td>
443</tr><tr>
444<td class="ble blec"><code>*</code></td>
445<td class="ble blec"><i>nothing</i></td>
446<td class="ble blec"><i>all</i></td>
447<td class="ble blec">A single asterisk would usually match anything and everything,
448but here, it matches nothing. This prevents accidentally blocking everything from the results table.</td>
449</tr>
450</table>
451</section>
452</div>`,//}}}
453 _notify = //{{{
454 `<div>
455<div style="float:left; margin-left:15px">
456<span style="position:relative; left:-8px"><b>Additional Notifications</b></span><br>
457<p><label for="notifyBlink" style="float:left; width:51px">Blink</label>
458<input id="notifyBlink" type="checkbox" name="notify" ${this.user.notifyBlink ? 'checked' : ''}/></p>
459<p><label for="notifyTaskbar" style="float:left; width:51px">Taskbar</label>
460<input id="notifyTaskbar" type="checkbox" name="notify" ${this.user.notifyTaskbar ? 'checked' : ''}/></p>
461</div>
462<section style="margin-left:160px">
463<span style="position:relative; left:10px"><i>blink</i></span><br>
464Blink the tab when there are new HITs.
465</section>
466<section style="margin-left:160px">
467<span style="position:relative; left:10px"><i>taskbar</i></span><br>
468Create an HTML5 browser notification when there are new HITs, which appears over the taskbar for 10 seconds.
469</section>
470<p style="clear:both"><b>Note:</b> These notification options will only apply when the page does not have active focus.</p>
471</div>`,//}}}
472 _utils =//{{{
473 `<div>
474<div style="float:left; margin-left:15px">
475<span style="position relative; left:-8px"><b>Export/Import</b></span>
476<p><button id="sexport">Export</button></p>
477<p><button id="simport">Import</button></p>
478<input type="file" id="fsimport" style="display:none">
479</div>
480<section style="margin-left:130px; margin-top:15px">
481<span style="position:relative; left:10px"><i>Export</i></span><br>
482Export your current settings, block list, and include list as a local file.
483</section>
484<section style="margin-left:130px">
485<span style="position:relative; left:10px"><i>Import</i></span></br>
486Import your settings, block list, and include list from a local file.
487</section>
488<div style="margin-top:10px" id="eisStatus"></div>
489</div>`,//}}}
490 _main = //{{{
491 `<div style="top:0;left:0;margin:0;text-align:right;padding:0px;border:none;width:100%">
492<label id="settingsClose" class="close" title="Close"> ✘ </label>
493</div><div id="settingsSidebar">
494<span class="settingsSelected">General</span>
495<span>Appearance</span>
496<span>Blocklist</span>
497<span>Notifications</span>
498<span>Utilities</span>
499</div><div id="panelContainer" style="margin-left:10px;border:none;overflow:auto;width:auto;height:92%">
500<div id="General" class="settingsPanel">${_general}</div>
501<div id="Appearance" class="settingsPanel">${_appearance}</div>
502<div id="Blocklist" class="settingsPanel">${_blocks}</div>
503<div id="Notifications" class="settingsPanel">${_notify}</div>
504<div id="Utilities" class="settingsPanel">${_utils}</div>
505</div>`;//}}}
506
507 this.main = document.body.appendChild(document.createElement('DIV'));
508 this.main.id = 'settingsMain';
509 this.main.innerHTML = _main;
510 return this;
511 },//}}} Settings::draw
512 init : function() {//{{{
513 var get = (q, all) => this.main['querySelector' + (all ? 'All' : '')](q),
514 sidebarFn = function(e) {
515 if (e.target.classList.contains('settingsSelected')) return;
516 get('#' + get('.settingsSelected').textContent).style.display = 'none';
517 get('.settingsSelected').classList.toggle('settingsSelected');
518 e.target.classList.toggle('settingsSelected');
519 get('#' + e.target.textContent).style.display = 'block';
520 }.bind(this),
521 sliderFn = function(e) {
522 e.target.nextElementSibling.textContent = Math.floor(e.target.value * 100) + '%';
523 },
524 optChangeFn = function(e) {//{{{
525 var tag = e.target.tagName, type = e.target.type, id = e.target.id,
526 isChecked = e.target.checked, name = e.target.name, value = e.target.value;
527
528 switch (tag) {
529 case 'SELECT':
530 //get('#thedit').textContent = value === 'random' ? 'Re-roll!' : 'Edit Current Theme';
531 this.user.themes.name = value;
532 Themes.apply(value, this.user.hitColor);
533 break;
534 case 'INPUT':
535 switch (type) {
536 case 'radio':
537 if (name === 'checkbox') {
538 this.user.showCheckboxes = (value === 'true');
539 Array.from(document.querySelectorAll('#controlpanel input[type=checkbox],#controlpanel input[type=radio]'))
540 .forEach(v => v.classList.toggle('hidden'));
541 }
542 else this.user[name] = value;
543 if (name === 'hitColor') Themes.apply(this.user.themes.name, value);
544 break;
545 case 'checkbox':
546 this.user[id] = isChecked;
547 if (name === 'export')
548 Array.from(document.querySelectorAll(`button.${value}`))
549 .forEach(v => v.style.display = isChecked ? '' : 'none');
550 if (id === 'notifyTaskbar' && isChecked && Notification.permission === 'default')
551 Notification.requestPermission();
552 break;
553 case 'number':
554 if (name === 'fontSize')
555 document.head.querySelector('#lazyfont').sheet.cssRules[0].style.fontSize = value + 'px';
556 else if (name === 'shineOffset')
557 document.head.querySelector('#lazyfont').sheet.cssRules[1].style.fontSize = +this.user.fontSize + (+value) + 'px';
558 if (name === 'TOW') this.user.toWeights[id] = value;
559 else this.user[name] = value;
560 break;
561 case 'range':
562 this.user.volume[name] = value;
563 let audio = document.querySelector(`#${name}`);
564 audio.volume = value;
565 audio.play();
566 break;
567 }
568 break;
569 }
570 Settings.save();
571 }.bind(this);//}}}
572
573 get('#settingsClose').onclick = this.die.bind(this);
574 get('#General').style.display = 'block';
575 Array.from(get('#settingsSidebar span', true)).forEach(v => v.onclick = sidebarFn);
576 Array.from(get('input:not([type=file]),select', true)).forEach(v => v.onchange = optChangeFn);
577 Array.from(get('input[type=range]', true)).forEach(v => v.oninput = sliderFn);
578 get('#thedit').onclick = () => {
579 this.die.call(this);
580 new Editor('theme');
581 };
582 get('#sexport').onclick = FileHandler.exports;
583 get('#simport').onclick = () => {
584 get('#fsimport').value = '';
585 get('#eisStatus').innerHTML = '';
586 get('#fsimport').click();
587 };
588 get('#fsimport').onchange = FileHandler.imports;
589 },//}}} Settings::init
590 die : function() {
591 Interface.toggleOverflow('off');
592 this.main.remove();
593 }
594 },//}}} Settings
595
596 Themes = {//{{{
597 default : defaults.themes,
598 generateCSS : function(theme, mode) {//{{{
599 var ref = theme === 'random' ? this.randomize() : Settings.user.themes.colors[theme],
600 _ms = mode === 'cell' || theme === 'classic',
601 cellFix = {
602 row : k => `.${k} ` + (_ms ? '{background:' : 'a {color:') + ref[k] + '}',
603 text : k => `.${k} {color:` + (_ms ? this.tune(ref.bodytable, ref[k]) : ref.bodytable) + '}',
604 export: k => `.${k} button {color:` + (_ms ? this.tune(ref.export, ref[k]) : ref.export) + '}',
605 vlink : k => `.${k} a:not(.static):visited {color:` + (_ms
606 ? this.tune(ref.vlink, ref[k])
607 : ref.vlink) + '}'
608 },
609 css = `body {color:${ref.defaultText}; background-color:${ref.background}}
610/*#status {color:${ref.secondText}}*/
611#sortdirs {color:${ref.inputText}}
612#curtain {background:${ref.background}; opacity:0.5}
613.controlpanel i:after {color:${ref.accent}}
614#controlpanel {background:${ref.cpBackground}}
615#controlpanel input${theme === 'classic' ? '' : ', #controlpanel select'}
616{color:${ref.inputText}; border:1px solid; background:${theme === 'classic' ? '#fff' : ref.cpBackground}}
617#controlpanel label {color:${ref.defaultText}; background:${ref.cpBackground}}
618#controlpanel label:hover {background:${ref.hover}}
619#controlpanel label.checked {color:${ref.secondText}; background:${ref.highlight}}
620/*#resultsTable tbody a:not(.static):visited {color:${ref.vlink}}*/
621/*#resultsTable button {color:${ref.export}}*/
622thead, caption, a {color:${ref.defaultText}}
623tbody a {color:${ref.link}}
624.nohitDB {color:#000; background:${ref.nohitDB}}
625.hitDB {color:#000; background:${ref.hitDB}}
626.reqmaster {color:#000; background:${ref.reqmaster}}
627.nomaster {color:#000; background:${ref.nomaster}}
628.tooweak {background:${ref.unqualified}}
629${cellFix.row('toNone')} ${cellFix.text('toNone')} ${cellFix.export('toNone')} ${cellFix.vlink('toNone')}
630${cellFix.row('toHigh')} ${cellFix.text('toHigh')} ${cellFix.export('toHigh')} ${cellFix.vlink('toHigh')}
631${cellFix.row('toGood')} ${cellFix.text('toGood')} ${cellFix.export('toGood')} ${cellFix.vlink('toGood')}
632${cellFix.row('toAverage')} ${cellFix.text('toAverage')} ${cellFix.export('toAverage')} ${cellFix.vlink('toAverage')}
633${cellFix.row('toLow')} ${cellFix.text('toLow')} ${cellFix.export('toLow')} ${cellFix.vlink('toLow')}
634${cellFix.row('toPoor')} ${cellFix.text('toPoor')} ${cellFix.export('toPoor')} ${cellFix.vlink('toPoor')}`;
635 if (theme !== 'classic') css += `\n.controlpanel button {color:${ref.accent}; background:transparent;}`;
636 return css;
637 },//}}} Themes::generateCSS
638 tune : function(fg, bg) {//{{{
639 var cbg = this.getBrightness(bg),
640 lighten = c => {
641 c.s = Math.max(0, c.s - 5);
642 c.v = Math.min(100, c.v + 5);
643 return c;
644 },
645 darken = c => {
646 c.s = Math.min(100, c.s + 5);
647 c.v = Math.max(0, c.v - 5);
648 return c;
649 },
650 tune = (function() { if (cbg >= 128) return darken; else return lighten; })(),
651 hex2hsv = function(c) {//{{{
652 var r = parseInt(c.slice(1, 3), 16), g = parseInt(c.slice(3, 5), 16), b = parseInt(c.slice(5, 7), 16),
653 min = Math.min(r, g, b), max = Math.max(r, g, b), delta = max - min, _hue;
654 switch (max) {
655 case r:
656 _hue = Math.round(60 * (g - b) / delta);
657 break;
658 case g:
659 _hue = Math.round(120 + 60 * (b - r) / delta);
660 break;
661 case b:
662 _hue = Math.round(240 + 60 * (r - g) / delta);
663 break;
664 }
665 return {
666 h: _hue < 0 ? _hue + 360 : _hue,
667 s: max === 0 ? 0 : Math.round(100 * delta / max),
668 v: Math.round(max * 100 / 255)
669 };
670 }, //}}}
671 hsv2hex = function(c) {//{{{
672 var r, g, b, pad = s => ('00' + s.toString(16)).slice(-2);
673 if (c.s === 0) r = g = b = Math.round(c.v * 2.55).toString(16);
674 else {
675 c = { h: c.h / 60, s: c.s / 100, v: c.v / 100 }; // convert to prime to calc chroma
676 var _t1 = Math.round((c.v * (1 - c.s)) * 255),
677 _t2 = Math.round((c.v * (1 - c.s * (c.h - Math.floor(c.h)))) * 255),
678 _t3 = Math.round((c.v * (1 - c.s * (1 - (c.h - Math.floor(c.h))))) * 255);
679 switch (Math.floor(c.h)) {
680 case 1:
681 r = _t2;
682 g = Math.round(c.v * 255);
683 b = _t1;
684 break;
685 case 2:
686 r = _t1;
687 g = Math.round(c.v * 255);
688 b = _t3;
689 break;
690 case 3:
691 r = _t1;
692 g = _t2;
693 b = Math.round(c.v * 255);
694 break;
695 case 4:
696 r = _t3;
697 g = _t1;
698 b = Math.round(c.v * 255);
699 break;
700 case 0:
701 r = Math.round(c.v * 255);
702 g = _t3;
703 b = _t1;
704 break;
705 default:
706 r = Math.round(c.v * 255);
707 g = _t1;
708 b = _t2;
709 break;
710 }
711 }
712 return '#' + pad(r) + pad(g) + pad(b);
713 };//}}}
714
715 while (Math.abs(this.getBrightness(fg) - cbg) < 90) fg = hsv2hex(tune(hex2hsv(fg)));
716 return fg;
717 },//}}}
718 getBrightness: function(hex) {//{{{
719 // TODO: put in Colors object
720 var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
721 return ((r * 299) + (g * 587) + (b * 114)) / 1000;
722 },//}}} Themes::getBrightness
723 apply : function(theme, mode) {//{{{
724 var cssNew = URL.createObjectURL(new Blob([this.generateCSS(theme, mode)], { type: 'text/css' })),
725 rel = document.head.querySelector('link[rel=stylesheet]'), cssOld = rel.href;
726 rel.href = cssNew;
727 URL.revokeObjectURL(cssOld);
728 }//}}} Themes::apply
729 },//}}} Themes
730
731 Interface = {//{{{
732 user : Settings.user,
733 time : Date.now(),
734 focused : true,
735 blackhole : {},
736 isLoggedout : document.querySelector('#lnkWorkerSignin') ? true : false,
737 resetTitle : function() {//{{{
738 if (this.blackhole.blink) clearInterval(this.blackhole.blink);
739 document.title = DOC_TITLE;
740 },//}}}
741 toggleOverflow: function(state) {//{{{
742 document.body.querySelector('#curtain').style.display = state === 'on' ? 'block' : 'none';
743 document.body.style.overflow = state === 'on' ? 'hidden' : 'auto';
744 },//}}} Interface::curtains
745 draw : function() {//{{{
746 var user = this.user = Settings.user,
747 _cb = user.showCheckboxes ? '' : 'hidden',
748 _u0 = new Uint8Array(Array.prototype.map.call(window.atob(audio0), v => v.charCodeAt(0))),
749 _u1 = new Uint8Array(Array.prototype.map.call(window.atob(audio1), v => v.charCodeAt(0))),
750 ding = URL.createObjectURL(new Blob([_u0], { type: 'audio/ogg' })),
751 squee = URL.createObjectURL(new Blob([_u1], { type: 'audio/mp3' })),
752 titles = {//{{{
753 refresh : 'Enter search refresh delay in seconds.\nEnter 0 for no auto-refresh.\nDefault is 0 (no auto-refresh).',
754 pages : 'Enter number of pages to scrape. Default is 1.',
755 skips : 'Searches additional pages to get a more consistent number of results. Helpful if you\'re blocking a lot of items.',
756 resultsPerPage: 'Number of results to return per page (maximum is 100, default is 30)',
757 batch : 'Enter minimum HITs for batch search (must be searching by Most Available).',
758 pay : 'Enter the minimum desired pay per HIT (e.g. 0.10).',
759 qual : 'Only show HITs you\'re currently qualified for (must be logged in).',
760 monly : 'Only show HITs that require Masters qualifications.',
761 mhide : 'Remove masters hits from the results if selected, otherwise display both masters and non-masters HITS.\n' +
762 'The \'qualified\' setting supercedes this option.',
763 searchBy : 'Get search results by...\n Latest = HIT Creation Date (newest first),\n ' +
764 'Most Available = HITs Available (most first),\n Reward = Reward Amount (most first),\n Title = Title (A-Z)',
765 invert : 'Reverse the order of the Search By choice, so...\n Latest = HIT Creation Date (oldest first),\n ' +
766 'Most Available = HITs Available (least first),\n Reward = Reward Amount (least first),\n Title = Title (Z-A)',
767 shine : 'Enter time (in seconds) to keep new HITs highlighted.\nDefault is 300 (5 minutes).',
768 sound : 'Play a sound when new results are found.',
769 soundSelect : 'Select which sound will be played.',
770 minTOPay : 'After getting search results, hide any results below this average Turkopticon pay rating.\n' +
771 'Minimum is 1, maximum is 5, decimals up to 2 places, such as 3.25',
772 hideNoTO : 'After getting search results, hide any results that have no, or too few, Turkopticon pay ratings.',
773 disableTO : 'Disable attempts to download ratings data from Turkopticon for the results table.\n' +
774 'NOTE: TO is cached. That means if TO is availible from a previous scrape, it will use that value even if ' +
775 'TO is disabled. This option only prevents the retrieval of ratings from the Turkopticon servers,',
776 sortPay : 'After getting search results, re-sort the results based on their average Turkopticon pay ratings.',
777 sortAll : 'After getting search results, re-sort the results by their overall Turkopticon rating.',
778 sortAsc : 'Sort results in ascending (low to high) order.',
779 sortDsc : 'Sort results in descending (high to low) order.',
780 search : 'Enter keywords to search for; default is blank (no search terms).',
781 hideBlock : 'When enabled, hide HITs that match your blocklist.\n' +
782 'When disabled, HITs that match your blocklist will be displayed with a red border.',
783 onlyIncludes : 'Show only HITs that match your includelist.\nBe sure to edit your includelist first or no results will be displayed.',
784 shineInc : 'Outline HITs that match your includelist with a dashed green border.',
785 mainlink : 'Version: ' + ENV.VERSION + '\nRead the documentation for HIT Scraper With Export on its Greasyfork page.',
786 gbatch : 'Apply the \'Minimum batch size\' filter to all search options.',
787 onlyViable : 'Filters out HITs with qualifications you do not have and \ncan neither request nor take a test to obtain.\n' +
788 'Does not work while logged out.'
789 },//}}}
790 css = [//{{{
791 'body {font-family:Verdana, Arial; font-size:14px}',
792 'p {margin:8px auto}',
793 '.cpdefault {display:inline-block; visibility:visible; overflow:hidden; padding:8px 5px 1px 5px; transition:all 0.3s;}',
794 '#controlpanel i:after, #status i:after {content:" | "}',
795 'input[type="checkbox"], input[type="radio"] {vertical-align:middle}',
796 'input[type="number"] {width:50px; text-align:center}',
797 'label {padding:2px}',
798 '.hiddenpanel {width:0px; height:0px; visibility:hidden}',
799 '.hidden {display:none}',
800 'button {border:1px solid}',
801 'textarea {font-family:inherit; font-size:11px; margin:auto; padding:2px}',
802 '.pop {position:fixed; top:15%; left:50%; margin:auto; transform:translateX(-50%); padding:5px;' + // for
803 // editors/exporters
804 'background:black; color:white; z-index:20; font-size:12px; box-shadow:0px 0px 6px 1px #fff}',
805 'dt {text-transform:uppercase; clear:both; margin:3px}',
806 '.icbutt {float:left;border:1px solid #fff;cursor:pointer} .icbutt > input {opacity:0;display:block;width:25px;height:25px;border:none}',
807 // settings
808 '#settingsMain {z-index:20; position:fixed; background:#fff; color:#000; box-shadow:-3px 3px 2px 2px #7B8C89; line-height:initial;' +
809 'top:50%; left:50%; width:85%; height:85%; margin-right:-50%; transform:translate(-50%, -50%)}',
810 '#settingsMain > div {margin:5px; padding:3px; position:relative; border:1px solid grey; line-height:initial}',
811 '.close {position:relative; font-weight:bold; font-size:1em; color:white; background:black; cursor:pointer}',
812 '#settingsSidebar {width:100px; min-width:90px; height:92%; float:left}',
813 '#settingsSidebar > span {display:block; margin-bottom:5px; width:100px; font-size:1em; cursor:pointer}',
814 '.settingsPanel {position:absolute; top:0;left:0; display:none; width:100%; height:100%; font-size:11px}',
815 '.settingsPanel > div {margin:15px 5px; position:relative; background:#CCFFFA; overflow:auto; padding:6px 10px}',
816 '.settingsSelected {background:aquamarine}',
817 '.ble {border:1px solid black; border-collapse:collapse;} .blec {padding:5px; text-align:left;}',
818
819 '.toLink {position:relative;}',
820 '.toLink:before {content:""; display:none; z-index:5; position:absolute; top:0; left:-6px; width:0; height:0;' +
821 'border-top:6px solid transparent; border-bottom:6px solid transparent; border-left:6px solid black}',
822 '.toLink:hover:before {display:block;}',
823 '.tooltip {position:absolute;top:0;right:calc(100% + 6px);text-align:left;transform:translateY(-20%);padding:5px;font-weight:normal;' +
824 'font-size:11px; line-height:1; display:none; background:black; color:white; box-shadow:0px 0px 6px 1px #fff}',
825 'meter {width:100%; position:relative; height:15px;}',
826 'meter:before, .ffmb {display:block; font-size:10px; font-weight:bold; color:black; content:attr(data-attr); position:absolute; top:1px}',
827 'meter:after, .ffma {display:block; font-size:10px; font-weight:bold; color:black; content:attr(value); position:absolute; top:1px; right:0}',
828 '#resultsTable button {height:14px; font-size:8px; border:1px solid; padding:0; background:transparent}',
829 '#resultsTable tbody td > div {display:table-cell}',
830 '#resultsTable tbody td > div:first-child {padding-right:2px; vertical-align:middle; white-space:nowrap}',
831 'button.disabled {position:relative}',
832 'button.disabled:before {content:""; display:none; z-index:5; position:absolute; top:-7px; left:50%; width:0; height:0;' +
833 'border-left:6px solid transparent; border-right:6px solid transparent; border-top:6px solid black; transform:translateX(-50%)}',
834 'button.disabled:after {content:"Exports are disabled while logged out."; display:none; z-index:5; position:absolute;' +
835 'top:-7px; left:50%; color:white; background:black; width:230px; padding:2px; transform:translate(-50%,-100%);' +
836 'box-shadow:0px 0px 6px 1px #fff; font-size:12px}',
837 'button.disabled:focus:before {display:block} button.disabled:focus:after {display:block}',
838 '.spinner {display: inline-block; animation: kfspin 0.7s infinite linear; font-weight:bold;}',
839 '@keyframes kfspin { 0% { transform: rotate(0deg) } 100% { transform: rotate(359deg) } }',
840 '.spinner:before{content:"*"}',
841 '.exhwtf {width:70px; background:black; color:white; vertical-align:top; border-radius:5px}',
842 '.ignored td {border:2px solid #00E5FF}',
843 '.includelisted td {border:3px dashed #008800}',
844 '.blocklisted td {border:3px solid #cc0000}'
845 ],//}}}
846 fCss =
847 `#resultsTable tbody {font-size:${user.fontSize}px;}` +
848 `.shine td {border:1px dotted #fff; font-size:${(+user.fontSize) + (+user.shineOffset)}px; font-weight:bold}`,
849 //{{{ body
850 body = `
851<audio id="ding"> <source src=${ding}> </audio>
852<audio id="squee"> <source src=${squee}> </audio>
853<div id="curtain" style="position:fixed;width:100%;height:100%;display:none;z-index:10"></div>
854<div id="controlpanel" class="controlpanel cpdefault">
855<p>
856Auto-refresh delay: <input id="refresh" type="number" title="${titles.refresh}" min="0" value="${user.refresh}" /><i></i>
857Pages to scrape: <input id="pages" type="number" title="${titles.pages}" min="1" max="100" value="${user.pages}" /><i></i>
858<label class="${user.skips ? 'checked' : ''}" title="${titles.skips}" for="skips">Correct for skips</label>
859<input id="skips" class="${_cb}" type="checkbox" title="${titles.skips}" ${user.skips ? 'checked' : ''} /><i></i>
860Results per page: <input id="resultsPerPage" type="number" title="${titles.resultsPerPage}"
861min="1" max="100" value="${user.resultsPerPage || 10}" />
862</p></p>
863Min reward: <input id="pay" type="number" title="${titles.pay}" min="0" step="0.05" value="${user.pay}" /><i></i>
864<label class="${user.qual ? 'checked' : ''}" title="${titles.qual}" for="qual">Qualified</label>
865<input id="qual" class="${_cb}" type="checkbox" title="${titles.qual}" ${user.qual ? 'checked' : ''} /><i></i>
866<label class="${user.monly ? 'checked' : ''}" title="${titles.monly}" for="monly">Masters Only</label>
867<input id="monly" class="${_cb}" type="checkbox" title="${titles.monly}" ${user.monly ? 'checked' : ''} /><i></i>
868<label class="${user.mhide ? 'checked' : ''}" title="${titles.mhide}" for="mhide">Hide Masters</label>
869<input id="mhide" class="${_cb}" type="checkbox" title="${titles.mhide}" ${user.mhide ? 'checked' : ''} /><i></i>
870<label class="${user.onlyViable ? 'checked' : ''}" title="${titles.onlyViable}" for="onlyViable">Hide Infeasible</label>
871<input id="onlyViable" class="${_cb}" type="checkbox" title="${titles.onlyViable}" ${user.onlyViable
872 ? 'checked'
873 : ''} /><i></i>
874Min batch size: <input id="batch" type="number" title="${titles.batch}" min="1" value="${user.batch}" /> -
875<label class="${user.gbatch ? 'checked' : ''}" title="${titles.gbatch}" for="gbatch">Global</label>
876<input id="gbatch" class="${_cb}" type="checkbox" title="${titles.gbatch}" ${user.gbatch ? 'checked' : ''} />
877</p><p>
878New HIT highlighting: <input id="shine" type="number" title="${titles.shine}" min="0" max="3600" value="${user.shine}" /><i></i>
879<label class="${user.notifySound[0] ? 'checked' : ''}" title="${titles.sound}" for="sound">Sound on new HIT</label>
880<input id="sound" class="${_cb}" type="checkbox" title="${titles.sound}" ${user.notifySound[0]
881 ? 'checked'
882 : ''} />
883<select id="soundSelect" title="${titles.soundSelect}" style="display:${user.notifySound[0]
884 ? 'inline'
885 : 'none'}">
886<option value="ding" ${user.notifySound[1] === 'ding' ? 'selected' : ''}>Ding</option>
887<option value="squee" ${user.notifySound[1] === 'squee' ? 'selected' : ''}>Squee</option>
888</select><i></i>
889<label class="${user.disableTO ? 'checked' : ''}" title="${titles.disableTO}" for="disableTO">Disable TO</label>
890<input id="disableTO" class="${_cb}" type="checkbox" title="${titles.disableTO}" ${user.disableTO
891 ? 'checked'
892 : ''} /><i></i>
893Search by: <select id="searchBy" title="${titles.searchBy}">
894<option value="late" ${user.searchBy === 0 ? 'selected' : ''}>Latest</option>
895<option value="most" ${user.searchBy === 1 ? 'selected' : ''}>Most Available</option>
896<option value="amount" ${user.searchBy === 2 ? 'selected' : ''}>Reward</option>
897<option value="alpha" ${user.searchBy === 3 ? 'selected' : ''}>Title</option>
898</select> -
899<label class="${user.invert ? 'checked' : ''}" title="${titles.invert}" for="invert">Invert</label>
900<input id="invert" class="${_cb}" type="checkbox" title="${titles.invert}" ${user.invert ? 'checked' : ''} />
901</p><p>
902Min pay TO: <input id="minTOPay" type="number" title="${titles.minTOPay}" min="1" max="5" step="0.25" value="${user.minTOPay}" /><i></i>
903<label class="${user.hideNoTO ? 'checked' : ''}" title="${titles.hideNoTO}" for="hideNoTO">Hide no TO</label>
904<input id="hideNoTO" class="${_cb}" type="checkbox" title="${titles.hideNoTO}" ${user.hideNoTO
905 ? 'checked'
906 : ''} /><i></i>
907<label class="${user.sortPay ? 'checked' : ''}" title="${titles.sortPay}" for="sortPay">Sort by TO pay</label>
908<input id="sortPay" class="${_cb}" type="checkbox" title="${titles.sortPay}" name="sort" ${user.sortPay
909 ? 'checked'
910 : ''} /><i></i>
911<label class="${user.sortAll ? 'checked' : ''}" title="${titles.sortAll}" for="sortAll">Sort by overall TO</label>
912<input id="sortAll" class="${_cb}" type="checkbox" title="${titles.sortAll}" name="sort" ${user.sortAll
913 ? 'checked'
914 : ''} />
915<div id="sortdirs" style="font-size:15px;display:${user.sortPay || user.sortAll ? 'inline' : 'none'}">
916<label class="${user.sortAsc ? 'checked' : ''}" for="sortAsc" title="${titles.sortAsc}"> ▲ </label>
917<input id="sortAsc" class="${_cb}" type="radio" title="${titles.sortAsc}" name="sortDir" ${user.sortAsc
918 ? 'checked'
919 : ''} />
920<label class="${user.sortDsc ? 'checked' : ''}" for="sortDsc" title="${titles.sortDsc}"> ▼ </label>
921<input id="sortDsc" class="${_cb}" type="radio" title="${titles.sortDsc}" name="sortDir" ${user.sortDsc
922 ? 'checked'
923 : ''} />
924</div>
925</p><p>
926Search Terms: <input id="search" title="${titles.search}" placeholder="Enter search terms here" value="${user.search}" /><i></i>
927<label class="${user.hideBlock ? 'checked' : ''}" title="${titles.hideBlock}" for="hideBlock">Hide blocklisted</label>
928<input id="hideBlock" class="${_cb}" type="checkbox" title="${titles.hideBlock}" ${user.hideBlock
929 ? 'checked'
930 : ''} /><i></i>
931<label class="${user.onlyIncludes ? 'checked' : ''}" title="${titles.onlyIncludes}" for="onlyIncludes">Restrict to includelist</label>
932<input id="onlyIncludes" class="${_cb}" type="checkbox" title="${titles.onlyIncludes}" ${user.onlyIncludes
933 ? 'checked'
934 : ''} /><i></i>
935<label class="${user.shineInc ? 'checked' : ''}" title="${titles.shineInc}" for="shineInc">Highlight Includelisted</label>
936<input id="shineInc" class="${_cb}" type="checkbox" title="${titles.shineInc}" ${user.shineInc
937 ? 'checked'
938 : ''} />
939</p>
940</div><div id="controlbuttons" class="controlpanel" style="margin-top:5px">
941<button id="btnMain">Start</button> <button id="btnHide">Hide Panel</button> <button id="btnBlocks">Edit Blocklist</button>
942<button id="btnIncs">Edit Includelist</button> <button id="btnIgnores">Toggle Ignored HITs</button>
943<button id="btnSettings">Settings</button>
944</div>
945<div id="loggedout" style="font-size:11px;margin-left:10px;text-transform:uppercase"></div>
946<div id="status" style="height:34px"><p>Stopped</p></div>
947<div id="results">
948<table id="resultsTable" style="width:100%">
949<caption style="font-weight:800;line-height:1.25em;font-size:1.5em;">
950<a class="mainlink" target="_blank" href="${URL_SELF}" title="${titles.mainlink}">HIT Scraper</a> Results
951</caption>
952<thead><tr style="font-weight:800;font-size:0.87em;text-align:center">
953<td>Requester</td><td>Title</td><td style="width:70px">Reward & PandA</td><td style="width:35px"># Avail</td>
954<td style="width:30px">TO Pay</td><td style="width:15px">M</td>
955<td style="width:15px"></td><td style="width:15px"></td>
956</tr></thead>
957<tbody></tbody>
958</table>
959</div>`,//}}}
960 head = `<title>${DOC_TITLE}</title>` +
961 `<style type="text/css" id="lazyfont">${fCss}</style>` +
962 `<style type="text/css">${css.join('')}</style>` +
963 `<link rel="icon" type="image/png" href="${ico}" /><link rel="stylesheet" type="text/css" />`;
964
965 document.head.innerHTML = head;
966 document.body.innerHTML = body;
967 this.elkeys = Object.keys(titles);
968 return this;
969 },//}}} Interface::draw
970 init : function() {//{{{
971 this.panel = {};
972 this.buttons = {};
973 var get = (q, all) => document['querySelector' + (all ? 'All' : '')](q),
974 sortdirs = get('#sortdirs'),
975 moveSortdirs = function(node) {
976 if (!node.checked) {
977 sortdirs.style.display = 'none';
978 return;
979 }
980 sortdirs.style.display = 'inline';
981 sortdirs.remove();
982 node.parentNode.insertBefore(sortdirs, node.nextSibling);
983 },
984 kdFn = e => { if (e.keyCode === kb.ENTER) setTimeout(() => this.buttons.main.click(), 30); },
985 optChangeFn = function(e) {//{{{
986 var tag = e.target.tagName, type = e.target.type, id = e.target.id,
987 isChecked = e.target.checked, name = e.target.name, value = e.target.value;
988
989 switch (tag) {
990 case 'SELECT':
991 if (id === 'soundSelect')
992 this.user.notifySound[1] = e.target.value;
993 else
994 this.user[id] = e.target.selectedIndex;
995 break;
996 case 'INPUT':
997 switch (type) {
998 case 'number':
999 case 'text':
1000 this.user[id] = value;
1001 break;
1002 case 'radio':
1003 Array.from(get(`input[name=${name}]`, true))
1004 .forEach(v => {
1005 this.user[v.id] = v.checked;
1006 get(`label[for=${v.id}]`).classList.toggle('checked');
1007 });
1008 break;
1009 case 'checkbox':
1010 if (name === 'sort') {
1011 Array.from(get(`input[name=${name}]`, true)).forEach(v => {
1012 if (e.target !== v) v.checked = false;
1013 get(`label[for=${v.id}]`).className = v.checked ? 'checked' : '';
1014 this.user[v.id] = v.checked;
1015 });
1016 moveSortdirs(e.target);
1017 break;
1018 } else if (id === 'sound') {
1019 this.user.notifySound[0] = isChecked;
1020 e.target.nextElementSibling.style.display = isChecked ? 'inline' : 'none';
1021 }
1022 this.user[id] = isChecked;
1023 get(`label[for=${id}]`).classList.toggle('checked');
1024 break;
1025 }
1026 break;
1027 }
1028 Settings.save();
1029 }.bind(this);//}}}
1030
1031 'ding squee'.split(' ').forEach(v => get(`#${v}`).volume = this.user.volume[v]);
1032
1033 Themes.apply(this.user.themes.name);
1034 if (this.isLoggedout) get('#loggedout').textContent = 'you are currently logged out of mturk';
1035 // get references to control panel elements and set up click events
1036 this.Status = {
1037 node : get('#status').firstChild,
1038 push : function(t) { this.node.innerHTML = t; },
1039 append: function(t) { this.node.innerHTML += t; },
1040 cd : function() { this.node.innerHTML = this.node.innerHTML.replace(/\d+(?= seconds)/, m => +m - 1); }
1041 };
1042 for (var k of this.elkeys) {
1043 if (k === 'mainlink') continue;
1044 this.panel[k] = document.getElementById(k);
1045 this.panel[k].onchange = optChangeFn;
1046 if (k === 'pay' || k === 'search') this.panel[k].onkeydown = kdFn;
1047 if ((k === 'sortPay' || k === 'sortAll') && this.panel[k].checked) moveSortdirs(this.panel[k]);
1048 }
1049
1050 // get references to buttons
1051 Array.from(get('button', true)).forEach(v => this.buttons[v.id.slice(3).toLowerCase()] = v);
1052 // set up button click events
1053 this.buttons.main.onclick = function(e) {
1054 e.target.textContent = e.target.textContent === 'Start' ? 'Stop' : 'Start';
1055 Core.run();
1056 };
1057 this.buttons.hide.onclick = function(e) {
1058 get('#controlpanel').classList.toggle('hiddenpanel');
1059 e.target.textContent = e.target.textContent === 'Hide Panel' ? 'Show Panel' : 'Hide Panel';
1060 };
1061 this.buttons.blocks.onclick = () => {
1062 this.toggleOverflow('on');
1063 new Editor('ignore');
1064 };
1065 this.buttons.incs.onclick = () => {
1066 this.toggleOverflow('on');
1067 new Editor('include');
1068 };
1069 this.buttons.ignores.onclick = () => Array.from(get('.ignored:not(.blocklisted)', true)).forEach(v => v.classList.toggle('hidden'));
1070 this.buttons.settings.onclick = () => {
1071 this.toggleOverflow('on');
1072 Settings.draw().init();
1073 };
1074 get('#hideBlock').addEventListener('change', () => Array.from(get('.blocklisted', true)).forEach(v => v.classList.toggle('hidden')));
1075 document.body.onblur = () => this.focused = false;
1076 document.body.onfocus = () => {
1077 this.focused = true;
1078 this.resetTitle();
1079 };
1080 document.getElementById("btnMain").click();
1081 }//}}} Interface::init
1082 },//}}} Interface
1083
1084 Editor = function(type) {//{{{
1085 if (!type) return { setDefaultBlocks: setDefaultBlocks };
1086 Interface.toggleOverflow('on');
1087 this.node = document.body.appendChild(document.createElement('DIV'));
1088 this.node.classList.add('pop');
1089 this.die = () => {
1090 Interface.toggleOverflow('off');
1091 this.node.remove();
1092 };
1093 this.type = type;
1094 this.caller = arguments[1] || null;
1095
1096 function setDefaultBlocks() {
1097 return localStorage.setItem('scraper_ignore_list',
1098 'oscar smith^diamond tip research llc^jonathan weber^jerry torres^' +
1099 'crowdsource^we-pay-you-fast^turk experiment^jon brelig^p9r^scoutit');
1100 }
1101
1102 switch (type) {
1103 case 'include':
1104 case 'ignore':
1105 if (type === 'ignore' && !localStorage.getItem('scraper_ignore_list')) setDefaultBlocks();
1106 var titleText = type === 'ignore'
1107 ? '<b>BLOCKLIST</b> - Edit the blocklist with what you want to ignore/hide. Separate requester names and HIT titles with the ' +
1108 '<code>^</code> character. After clicking "Save", you\'ll need to scrape again to apply the changes.'
1109 : '<b>INCLUDELIST</b> - Focus the results on your favorite requesters. Separate requester names and HIT titles with the ' +
1110 '<code>^</code> character. When the "Restrict to includelist" option is selected, ' +
1111 'HIT Scraper only shows results matching the includelist.';
1112 this.node.innerHTML = '<div style="width:500px">' + titleText + '</div>' +
1113 '<textarea style="display:block;height:200px;width:500px;font:12px monospace" placeholder="nothing here yet">' +
1114 (localStorage.getItem(`scraper_${type}_list`) || '') + '</textarea>' +
1115 '<button id="edSave" style="margin:5px auto;width:50%;color:white;background:black">Save</button>' +
1116 '<button id="edCancel" style="margin:5px auto;width:50%;color:white;background:black">Cancel</button>';
1117 this.node.querySelector('#edSave').onclick = () => {
1118 localStorage.setItem(`scraper_${type}_list`, this.node.querySelector('textarea').value.trim());
1119 this.die();
1120 };
1121 break;
1122 case 'theme':
1123 var dlbody = [], _th = Settings.user.themes, split = obj => {
1124 var a = [];
1125 for (var k in obj) if (obj.hasOwnProperty(k)) a.push({ k: k, v: obj[k] });
1126 return a.sort((a, b) => a.k < b.k ? -1 : 1);
1127 }, _colors = split(_th.colors[_th.name]),
1128 define = k => '<div style="margin-left:37px">' + _dd[k] + '</div>',
1129 _dd = {//{{{
1130 highlight : 'Distinguishes between active and inactive states in the control panel',
1131 background : 'Background color',
1132 accent : 'Color of spacer text (and control panel buttons on themes other than \'classic\')',
1133 bodytable : 'Default color of text elements in the results table (this is ignored if HIT coloring is set to \'cell\')',
1134 cpBackground: 'Background color of the control panel',
1135 toHigh : 'Color for results with high TO',
1136 toGood : 'Color for results with good TO',
1137 toAverage : 'Color for results with average TO',
1138 toLow : 'Color for results with low TO',
1139 toPoor : 'Color for results with poor TO',
1140 toNone : 'Color for results with no TO',
1141 hitDB : 'Designates that a match was found in your HITdb',
1142 nohitDB : 'Designates that a match was not found in your HITdb',
1143 unqualified : 'Designates that you do not have the qualifications necessary to work on the HIT',
1144 reqmaster : 'Designates HITs that require Masters',
1145 nomaster : 'Designates HITs that do not require Masters',
1146 defaultText : 'Default text color',
1147 inputText : 'Color of input boxes in the control panel',
1148 secondText : 'Color for text used on selected control panel items',
1149 link : 'Default color of unvisited links',
1150 vlink : 'Default color of visited links',
1151 export : 'Color of buttons in the results table--export and block buttons',
1152 hover : 'Color of control panel options on mouseover'
1153 };//}}}
1154 for (var r of _colors)
1155 dlbody.push(`<dt>${r.k}</dt><dd><div class="icbutt"><input data-key="${r.k}" type="color" value="${r.v}" /></div>${define(r.k)}</dd>`);
1156 this.node.innerHTML = '<b>THEME EDITOR</b><p></p><div style="height:87%;overflow:auto"><dl>' + dlbody.join('') + '</dl></div>' +
1157 '<button id="edSave" style="margin:5px auto;width:33%;color:white;background:black">Save</button>' +
1158 '<button id="edDefault" style="margin:5px auto;width:33%;color:white;background:black">Restore Default</button>' +
1159 '<button id="edCancel" style="margin:5px auto;width:33%;color:white;background:black">Cancel</button>';
1160 this.node.style.height = '57%';
1161 Array.from(this.node.querySelectorAll('.icbutt')).forEach(v => {
1162 v.style.background = v.firstChild.value;
1163 v.firstChild.onchange = e => {
1164 var k = e.target.dataset.key;
1165 v.style.background = e.target.value;
1166 _th.colors[_th.name][k] = e.target.value;
1167 Themes.apply(_th.name, Settings.user.hitColor);
1168 };
1169 });
1170 this.node.querySelector('#edDefault').onclick = () => {
1171 _th.colors[_th.name] = Themes.default[_th.name];
1172 Themes.apply(_th.name, Settings.user.hitColor);
1173 this.die();
1174 new Editor('theme');
1175 };
1176 this.node.querySelector('#edSave').onclick = () => {
1177 Settings.save();
1178 this.die();
1179 };
1180 break;
1181 case 'vbTemplate':
1182 this.node.innerHTML = '<b>VBULLETIN TEMPLATE</b><div style="float:right;margin-bottom:5px">Ratings Symbol: ' +
1183 `<input style="text-align:center" type="text" size="1" maxlength="1" value="${Settings.user.vbSym}" /></div>` +
1184 '<textarea style="display:block;height:200px;width:500px;font:12px monospace">' +
1185 Settings.user.vbTemplate + '</textarea>' +
1186 '<button id="edSave" style="margin:5px auto;width:33%;color:white;background:black">Save</button>' +
1187 '<button id="edDefault" style="margin:5px auto;width:33%;color:white;background:black">Restore Default</button>' +
1188 '<button id="edCancel" style="margin:5px auto;width:33%;color:white;background:black">Cancel</button>';
1189 this.node.querySelector('#edDefault').onclick = () => {
1190 this.node.querySelector('textarea').value = Settings.defaults.vbTemplate;
1191 this.node.querySelector('#edSave').click();
1192 };
1193 this.node.querySelector('#edSave').onclick = () => {
1194 Settings.user.vbTemplate = this.node.querySelector('textarea').value.trim();
1195 Settings.user.vbSym = this.node.querySelector('input').value;
1196 Settings.save();
1197 this.die();
1198 new Exporter({ target: this.caller });
1199 };
1200 break;
1201 }
1202 this.node.querySelector('#edCancel').onclick = () => this.die();
1203 },//}}}
1204
1205 Core = {//{{{
1206 active : false,
1207 timer : null,
1208 cooldown : null,
1209 lastScrape: null,
1210 getPayload: function(page=1) {//{{{
1211 const user = Settings.user,
1212 payload = {
1213 legacy: {
1214 searchWords : user.search,
1215 minReward : user.pay,
1216 qualifiedFor : Interface.isLoggedout ? 'off' : (user.qual ? 'on' : 'off'),
1217 requiresMasterQual: user.monly ? 'on' : 'off',
1218 sortType : '',
1219 pageNumber : page,
1220 pageSize : user.resultsPerPage || 50
1221 },
1222 next : {
1223 filters : {
1224 search_term: user.search,
1225 qualified : user.qual,
1226 masters : user.monly,
1227 min_reward : user.pay
1228 },
1229 page_size : user.resultsPerPage || 50,
1230 sort : '',
1231 page_number: page,
1232 format: 'json'
1233 }
1234 };
1235 const sort = user.invert ? 'asc' : 'desc';
1236 switch (user.searchBy) {
1237 case 0:
1238 payload.legacy.sortType = `LastUpdatedTime:${+!user.invert}`;
1239 payload.next.sort = 'updated_' + sort;
1240 break;
1241 case 1:
1242 payload.legacy.sortType = `NumHITs:${+!user.invert}`;
1243 payload.next.sort = 'num_hits_' + sort;
1244 break;
1245 case 2:
1246 payload.legacy.sortType = `Reward:${+!user.invert}`;
1247 payload.next.sort = 'reward_' + sort;
1248 break;
1249 case 3:
1250 payload.legacy.sortType = `Title:${+user.invert}`;
1251 payload.next.sort = 'title_' + sort;
1252 break;
1253 }
1254 return payload;
1255 },//}}} Core::init
1256 run : function(skiptoggle) {//{{{
1257 if (!skiptoggle) this.active = !this.active;
1258 this.cooldown = +Settings.user.refresh;
1259 clearTimeout(this.timer);
1260 Interface.resetTitle();
1261 if (this.active) {
1262 const next = ENV.HOST === ENV.NEXT;
1263 const path = next ? '/' : '/mturk/searchbar';
1264 const resType = next ? 'json' : 'document';
1265 Interface.Status.push(' <b class="spinner"></b> Processing page: 1');
1266 this.fetch(path, this.getPayload(), resType);
1267 }
1268 },//}}} Core::run
1269 cruise : function() {//{{{
1270 if (!this.active) return;
1271 if (--this.cooldown === 0) this.run(true);
1272 else {
1273 Interface.Status.cd();
1274 this.timer = setTimeout(this.cruise.bind(this), 1000);
1275 }
1276 },//}}}
1277 dispatch : function(type, src) {//{{{
1278 switch (type) {
1279 case 'external':
1280 this.meld(src);
1281 break;
1282 case 'internal':
1283 if (ENV.HOST === ENV.LEGACY) {
1284 const error = src.querySelector('td[class="error_title"]');
1285 if (error && /page request/.test(error.textContent))
1286 return setTimeout(this.fetch.bind(this), 3000, src.documentURI);
1287 }
1288 this.scrape(src);
1289 break;
1290 case 'control':
1291 const blocked = scraperHistory.filter(v => v.current && v.blocked).length,
1292 _rpp = +Settings.user.resultsPerPage,
1293 skiplimit = 5,
1294 pagelimit = Settings.user.skips
1295 ? ((+Settings.user.pages + Math.floor(blocked / _rpp) + (blocked % _rpp > 0.66 * _rpp
1296 ? 1
1297 : 0)) || 3)
1298 : (+Settings.user.pages || 3);
1299
1300 if (!this.active || !src.nextPageURL || src.page >= pagelimit || (pagelimit - Settings.user.pages) >= skiplimit || (Interface.isLoggedout && src.page === 20)) {
1301 if (Settings.user.disableTO)
1302 this.meld();
1303 else {
1304 const ids = scraperHistory.filter(v => v.current && v.TO === null && v.requester.id, true)
1305 .filter((v, i, a) => a.indexOf(v) === i).join();
1306 if (!ids.length) return this.meld();
1307 Interface.Status.push(' <b class="spinner"></b> Retrieving TO data');
1308 this.fetch(TO_API + ids, null, 'json');
1309 }
1310 } else {
1311 Interface.Status.push(` <b class="spinner"></b> Processing page: ${+src.page + 1}`);
1312 if (+src.page + 1 > +Settings.user.pages) Interface.Status.append('; Correcting for skips');
1313 setTimeout(this.fetch.bind(this), 250, src.nextPageURL, src.payload, src.responseType);
1314 }
1315 break;
1316 }
1317 },//}}} Core::dispatch
1318 scrapeNext: function(src) {
1319 src.results.forEach((v, i) => {
1320 const data = {
1321 discovery: Date.now(),
1322 title: v.title,
1323 index: src.page_number + ('00' + i).slice(-2),
1324 requester: { name: v.requester_name, id: v.requester_id, link: ENV.ORIGIN + v.requester_url },
1325 pay: '$' + v.monetary_reward.amount_in_dollars.toFixed(2),
1326 payRaw: v.monetary_reward.amount_in_dollars,
1327 time: v.assignment_duration_in_seconds,
1328 desc: v.description,
1329 quals: v.project_requirements.length ? v.project_requirements.map(getQuals) : ['None'],
1330 hit: { preview: ENV.ORIGIN + v.project_tasks_url, panda: ENV.ORIGIN + v.accept_project_task_url },
1331 groupId: v.hit_set_id,
1332 TO: null,
1333 masters: !!~v.project_requirements.findIndex(q => q.qualification_type_id === '2F1QJWKUDD8XADTFD2Q0G6UTO95ALH'),
1334 numHits: v.assignable_hits_count,
1335 blocked: false,
1336 included: false,
1337 current: true,
1338 qualified: v.caller_meets_requirements,
1339 viable: !~v.project_requirements.findIndex(q => q.caller_meets_requirement === false && q.qualification_type.is_requestable === false)
1340 };
1341
1342 const listsxr = this.crossRef(data.requester.name, data.title); //check block/include lists
1343 data.blocked = listsxr[0];
1344 data.included = listsxr[1];
1345 if (Settings.user.searchBy === 1 && +Settings.user.batch > 1 && +data.numHits < +Settings.user.batch) return;
1346 else if (Settings.user.gbatch && +Settings.user.batch > 1 && +data.numHits < +Settings.user.batch) return;
1347 else if (Settings.user.onlyViable && !data.viable) return;
1348 scraperHistory.set(data.groupId, data);
1349
1350 //if (!data.blocked) {
1351 // console.log(data);
1352 // var popup = window.open(data.hit.panda);
1353 // popup.close();
1354 //}
1355 }, this);
1356
1357 const dispatchObj = {
1358 method: 'next',
1359 page: src.page_number,
1360 nextPageURL: src.num_results < Settings.user.resultsPerPage ? null : '/',
1361 payload: this.getPayload(src.page_number+1),
1362 responseType: 'json'
1363 };
1364 this.dispatch('control', dispatchObj);
1365
1366 function getQuals(qual) {
1367 return `${qual.qualification_type.name} ${qual.comparator} ${qual.qualification_values.join()}`;
1368 }
1369 },
1370 scrape : function(src) {//{{{
1371 if (ENV.HOST === ENV.NEXT) return this.scrapeNext(src);
1372 let page = +src.documentURI.match(/pageNumber=(\d+)/)[1],
1373 nextPageURL = src.querySelector('img[src="/media/right_arrow.gif"]'),
1374 titles = Array.from(src.querySelectorAll('a.capsulelink')),
1375 getCapsule = n => {
1376 for (let i = 0; i < 7; i++) n = n.parentNode;
1377 return n;
1378 };
1379 nextPageURL = nextPageURL ? nextPageURL.parentNode.href : null;
1380
1381 titles.forEach(function(v, i) {
1382 let capsule = getCapsule(v),
1383 get = q => capsule.querySelector(q),
1384 pad = n => ('00' + n).slice(-2),
1385 qualrows = Array.prototype.slice.call(get('a[id^="qualifications"]').parentNode.parentNode.parentNode.rows, 1),
1386 viable = true,
1387 capData = {
1388 discovery: Date.now(),
1389 title : v.textContent.trim(),
1390 index : page + pad(i),
1391 requester: { name: get('.requesterIdentity').textContent, id: null, link: null, linkTemplate: null },
1392 pay : get('span.reward').textContent,
1393 time : get('a[id^="duration"]').parentNode.nextElementSibling.textContent,
1394 desc : get('a[id^="description"]').parentNode.nextElementSibling.textContent,
1395 quals : qualrows.length
1396 ? qualrows.map(v => v.cells[0].textContent.trim().replace(/\s+/g, ' '))
1397 : ['None'],
1398 hit : { preview: null, previewTemplate: null, panda: null, pandaTemplate: null },
1399 groupId : null,
1400 TO : null,
1401 masters : null,
1402 numHits : null,
1403 blocked : false,
1404 included : false,
1405 current : true,
1406 qualified: !Boolean(get('a[href*="notqualified?"],a[id^="private_hit"]'))
1407 },
1408 listsxr = this.crossRef(capData.requester.name, capData.title); //check block/include lists
1409 capData.blocked = listsxr[0];
1410 capData.included = listsxr[1];
1411 capData.masters = /Masters/.test(capData.quals.join());
1412
1413 if (Interface.isLoggedout) {
1414 capData.TO = '';
1415 capData.qualified = false;
1416 capData.numHits = 'n/a';
1417 } else {
1418 viable = !qualrows.map(v => v.cells[2].textContent).filter(v => v.includes('do not')).length;
1419 capData.numHits = get('a[id^="number_of_hits"]').parentNode.nextElementSibling.textContent.trim();
1420 }
1421
1422 try { // groupid
1423 capData.groupId = get('a[href*="roupId="]').href.match(/[A-Z0-9]{30}/)[0];
1424 } catch(e) {
1425 void(e);
1426 capData.groupId = this.getHash(capData.requester.name + capData.title + capData.pay);
1427 }
1428 try { // requesterid, requester search link, groupid
1429 var _r = get('a[href*="requesterId"]');
1430 capData.requester.link = _r.href;
1431 capData.requester.id = _r.href.match(/[^=]+$/)[0];
1432 } catch(e) {
1433 void(e);
1434 capData.requester.link = '/mturk/searchbar?searchWords=' + window.encodeURIComponent(capData.requester.name);
1435 }
1436 try { // preview/panda links
1437 var _l = get('a[href*="preview?"]');
1438 capData.hit.preview = _l.href.split('?')[0] + '?groupId=' + capData.groupId;
1439 capData.hit.panda = capData.hit.preview.replace(/(\?)/, 'andaccept$1');
1440 } catch(e) {
1441 void(e);
1442 capData.hit.preview = 'https://www.mturk.com/mturk/searchbar?searchWords=' + window.encodeURIComponent(capData.title);
1443 }
1444
1445 if (Settings.user.searchBy === 1 && +Settings.user.batch > 1 && +capData.numHits < +Settings.user.batch) return;
1446 else if (Settings.user.gbatch && +Settings.user.batch > 1 && +capData.numHits < +Settings.user.batch) return;
1447 else if (Settings.user.onlyViable && !viable) return;
1448 scraperHistory.set(capData.groupId, capData);
1449 }, this);
1450
1451 this.dispatch('control', { method: 'legacy', page: page, nextPageURL: nextPageURL });
1452 },//}}} Core::scrape
1453 meld : function() {//{{{
1454 let reviews = arguments.length ? arguments[0] : null,
1455 table = document.querySelector('#resultsTable').tBodies[0], html = [], field, /*_gp, _gq,*/
1456 getClassFromValue = (val, type) => type === 'sim' ? (val > 4 ? 'toHigh' : (val > 3 ? 'toGood' : (val > 2
1457 ? 'toAverage'
1458 : 'toPoor')))
1459 : (val > 4.05 ? 'toHigh' : (val > 3.06 ? 'toGood' : (val > 2.4 ? 'toAverage' : (val > 1.7
1460 ? 'toLow'
1461 : 'toPoor')))),
1462 addRowHTML = r => {//{{{
1463 var _st = Interface.isLoggedout ? 'disabled' : '',
1464 _sh = ex => Settings.user['export' + ex] ? '' : 'hidden',
1465 _rt = r.blocked
1466 ? ''
1467 : `<div><button name="block" value="${r.requester.name}" style="width:15px" title="Block this requester">R</button>` +
1468 `<button name="block" value="${r.title.replace(/"/g, '"')}" style="width:15px" title="Block this title">T</button></div>`;
1469 return `<tr class="${r.included ? 'includelisted' : ''} ${shouldHide ? 'ignored hidden' : ''} ` +
1470 `${r.blocked ? 'blocklisted' : ''} ${r.rowColor} ${r.shine ? 'shine' : ''}">` +
1471 `<td>${_rt}<div><a class="static" target="_blank" href="${r.requester.link}">${r.requester.name}</a><div></td>` +
1472 `<td><div><button class="ex vb ${_st} ${_sh('Vb')}" style="width:30px" data-gid="${r.groupId}">vB</button>
1473<button class="ex irc ${_st} ${_sh('Irc')}" style="width:30px" data-gid="${r.groupId}">IRC</button>
1474<button class="ex hwtf ${_st} ${_sh('Hwtf')}" style="width:33px" data-gid="${r.groupId}">HWTF</button></div><div>
1475<a title="Description: ${r.desc.replace(/"/g, '"')}\n\nQualifications: ${r.quals.join('; ')}" target="_blank" href="${r.hit.preview}">${r.title}</a>
1476</div></td>` +
1477 `<td style="text-align:center"><a target="_blank" ${r.hit.panda
1478 ? 'href="' + r.hit.panda + '"'
1479 : ''}>${r.pay}</a></td>` +
1480 `<td style="text-align:center" >${r.numHits}</td>` +
1481 `<td style="text-align:center"><a class="static toLink" target="_blank" data-rid="${r.requester.id
1482 ? r.requester.id
1483 : 'null'}" ` +
1484 (r.requester.id ? 'href="' + TO_REPORTS + r.requester.id + '"' : '') + '>' +
1485 (r.TO ? r.TO.attrs.pay : 'n/a') + createTooltip('to', r.TO) + '</a></td>' +
1486 `<td class="${r.masters ? 'reqmaster' : 'nomaster'}" style="text-align:center">${r.masters
1487 ? 'Y'
1488 : 'N'}</td>` +
1489 `<td class="db nohitDB" data-index="requester${r.requester.id ? 'Id' : 'Name'}"
1490data-value="${r.requester[r.requester.id ? 'id' : 'name']}" data-cmp-value="${r.title}"
1491data-cmp-index="title" style="text-align:center;cursor:default">R</td>` +
1492 `<td class="db nohitDB" data-index="title" data-value="${r.title}" data-cmp-value="${r.requester.name}"
1493data-cmp-index="requesterName" style="text-align:center;cursor:default">T</td>` +
1494 `${r.qualified ? '' : '<td class="tooweak" title="Not qualified to work on this HIT">NQ</td>'}` +
1495 '</tr>';
1496 },//}}}
1497 setRowColor = r => {
1498 var _t = Settings.user.colorType;
1499 if (!r.TO || r.TO.reviews < 5) {
1500 r.rowColor = 'toNone';
1501 return;
1502 }
1503 r.rowColor = getClassFromValue(_t === 'sim' ? r.TO.attrs.qual : r.TO.attrs.adjQual, _t);
1504 },
1505 bubbleNewHits = a => {
1506 var _new, _old = [];
1507 _new = a.filter(v => v.shine ? true : _old.push(v) && false);
1508 return _new.concat(_old);
1509 };
1510
1511 if (reviews) scraperHistory.updateTOData(prepReviews(reviews));
1512 let results = scraperHistory.filter(v => {
1513 if (!v.current) return false;
1514 v.current = false;
1515 if (Settings.user.mhide && v.masters) return false;
1516 else return true;
1517 });
1518
1519 // sorting
1520 if (!Interface.isLoggedout && !Settings.user.disableTO && Settings.user.sortPay !== Settings.user.sortAll) {
1521 if (Settings.user.sortPay)
1522 field = Settings.user.sortType === 'sim' ? 'pay' : 'adjPay';
1523 else if (Settings.user.sortAll)
1524 field = Settings.user.sortType === 'sim' ? 'qual' : 'adjQual';
1525
1526 results.sort((a, b) => {
1527 a = a.TO ? +a.TO.attrs[field] : 0;
1528 b = b.TO ? +b.TO.attrs[field] : 0;
1529 return b - a;
1530 });
1531 if (Settings.user.sortAsc) results.reverse();
1532 } else
1533 results.sort((a, b) => a.index - b.index);
1534
1535 // populating
1536 const counts = { total: results.length, new: 0, newVis: 0, ignored: 0, blocked: 0, included: 0, incNew: 0 };
1537 for (let r of (Settings.user.bubbleNew ? bubbleNewHits(results) : results)) {
1538 var shouldHide = Boolean((Settings.user.hideBlock && r.blocked) || (Settings.user.hideNoTO && !r.TO) ||
1539 (Settings.user.minTOPay && r.TO && +r.TO.attrs.pay < +Settings.user.minTOPay));
1540 counts.new += r.isNew ? 1 : 0;
1541 counts.newVis += r.isNew && !shouldHide ? 1 : 0;
1542 counts.ignored += shouldHide ? 1 : 0;
1543 counts.blocked += r.blocked ? 1 : 0;
1544 counts.included += r.included ? 1 : 0;
1545 counts.incNew += r.included && r.isNew ? 1 : 0;
1546 setRowColor(r);
1547 html.push(addRowHTML(r));
1548 }
1549 table.innerHTML = html.join('');
1550 this.notify(counts);
1551
1552 Array.from(table.querySelectorAll('.db')).forEach(v => HITStorage.test(v));
1553
1554 if (this.active) {
1555 if (this.cooldown === 0) Interface.buttons.main.click();
1556 else {
1557 try {
1558 this.timer = setTimeout(this.cruise.bind(this), 1000);
1559 Interface.Status.append(`<br />Scraping again in ${this.cooldown} seconds`);
1560 }
1561 catch(err) {
1562 console.log (err);
1563 Interface.Status.append(`<br />error spawned`);
1564 location.reload();
1565 document.getElementById("btnMain").click();
1566 }
1567 }
1568 }
1569 results = null;
1570 reviews = null;
1571 this.lastScrape = Date.now();
1572 },//}}}
1573 getHash : function(str) {//{{{
1574 var hash = 0, ch;
1575 for (var i = 0; i < str.length; i++) {
1576 ch = str.charCodeAt(i);
1577 hash = ch + (hash << 6) + (hash << 16) - hash;
1578 }
1579 return hash;
1580 },//}}} Core::getHash
1581 fetch : function(url, payload, responseType, inline) {//{{{
1582 const enc = window.encodeURIComponent;
1583 responseType = responseType || 'document';
1584 inline = inline === undefined ? true : inline;
1585 if (payload) {
1586 const key = ENV.HOST === ENV.NEXT ? 'next' : 'legacy';
1587 payload = payload[key];
1588 url += '?' + Object.entries(payload).map(stringify).join('&');
1589 }
1590
1591 function stringify(v) {
1592 const predicate = typeof v[1] !== 'string' && !(v[1] instanceof Array) ? Object.entries(v[1]) : '';
1593 if (predicate.length)
1594 return predicate.map(vp => (vp[0] = enc(`${v[0]}[${vp[0]}]`)) && vp) // 0 = o[i] => o%5Bi%5D
1595 .map(stringify)
1596 .join('&');
1597 return `${v[0]}=${enc(v[1])}`;
1598 }
1599
1600 const _p = new Promise(function(accept, rej) {
1601 const xhr = new XMLHttpRequest();
1602 xhr.open('GET', url, true);
1603 xhr.responseType = responseType;
1604 xhr.timeout = 6000;
1605 xhr.send();
1606 xhr.onload = function() {
1607 if (this.status === 200) accept(this.response);
1608 else rej(new Error(this.status + ' - ' + this.statusText));
1609 };
1610 xhr.onerror = function() {
1611 rej(new Error(this.status + ' - ' + this.statusText));
1612 console.log('error: ', this);
1613 };
1614 xhr.ontimeout = function() {
1615 rej(new Error('Request timed out - ' + url));
1616 console.log('timeout: ', this);
1617 };
1618 });
1619 const source = url.split('?')[0].includes('turkopticon') ? 'external' : 'internal';
1620 if (inline) _p.then(this.dispatch.bind(this, source), err => {
1621 console.warn(err);
1622 this.meld.apply(this);
1623 });
1624 else return _p;
1625 },//}}} Core::fetch
1626 crossRef : function(...needles) {//{{{
1627 var found = [false, false], s;
1628 if (Settings.user.onlyIncludes) { // everything not in includelist gets blocked, unless includelist is empty or doesn't exist
1629 var list = (localStorage.getItem('scraper_include_list') || '').toLowerCase().split('^');
1630 if (list.length === 1 && !list[0].length) return found; // includelist is empty
1631 for (s of needles) {
1632 found[1] = Boolean(~list.indexOf(s.toLowerCase().replace(/\s+/g, ' ')));
1633 if (found[1]) {
1634 found[0] = false;
1635 break;
1636 } else
1637 found[0] = true;
1638 }
1639 return found;
1640 } else {
1641 if (localStorage.getItem('scraper_ignore_list') === null) new Editor().setDefaultBlocks();
1642 var blist = (localStorage.getItem('scraper_ignore_list') || '').toLowerCase().split('^'),
1643 ilist = (localStorage.getItem('scraper_include_list') || '').toLowerCase().split('^'),
1644 blist_wild = Settings.user.wildblocks ? blist.filter(v => /.*?[*].*/.test(v)) : null;
1645 if (blist_wild) blist_wild.forEach((v, i, a) =>
1646 a[i] = new RegExp('^' + (v.replace(/([+${}[\](\)^|?.\\])/g, '\\$1') // escape non wildcard special chars
1647 .replace(/([^*]|^)[*](?!\*)/g, '$1.*') // turn
1648 // glob
1649 // into
1650 // regex
1651 .replace(/\*{2,}/g, s => s.replace(/\*/g, '\\$&'))) + '$'), 'i'); // escape consecutive asterisks
1652 for (s of needles) {
1653 found[0] = found[0] || Boolean(~blist.indexOf(s.toLowerCase().replace(/\s+/g, ' ')));
1654 found[1] = found[1] || Boolean(~ilist.indexOf(s.toLowerCase().replace(/\s+/g, ' ')));
1655 if (blist_wild && blist_wild.length && !found[0])
1656 for (var i = 0; !found[0] && i < blist_wild.length; i++) found[0] = blist_wild[i].test(s.toLowerCase().replace(/\s+/g, ' '));
1657 }
1658 return found; // [ blocklist,includelist ]
1659 }
1660 },//}}} Core::crossRef
1661 notify : function(c) {//{{{
1662 var s = ['Scrape Complete: '];
1663 s.push(c.total > 0 ? `${c.total} HIT${c.total > 1 ? 's' : ''}` : '<b>No HITs found.</b>');
1664 if (c.new) s.push(`<i></i>${c.new} new`);
1665 if (c.newVis !== c.new) s.push(` (${c.newVis} shown)`);
1666 if (c.included) s.push(`<i></i><b>${c.included} from includelist</b>`);
1667 if (c.ignored) s.push(`<i></i>${c.ignored} hidden -- `);
1668 if (c.blocked) s.push(`${c.ignored ? '' : '<i></i>'}${c.blocked} from blocklist`);
1669 if (c.ignored - c.blocked > 0) s.push(`${c.blocked
1670 ? '<i></i>'
1671 : ''}${c.ignored - c.blocked} below TO threshold`);
1672 Interface.Status.push(s.join(''));
1673
1674 if (c.newVis && Settings.user.notifySound[0]) document.getElementById(Settings.user.notifySound[1]).play();
1675 if (!c.newVis || Interface.focused) return;
1676 document.title = `[${c.newVis} new]` + DOC_TITLE;
1677 if (Settings.user.notifyBlink) Interface.blackhole.blink =
1678 setInterval(() => document.title = /scraper/i.test(document.title)
1679 ? `${c.newVis} new HITs`
1680 : DOC_TITLE, 1000);
1681 if (Settings.user.notifyTaskbar && Notification.permission === 'granted') {
1682 var inc = c.incNew ? ` (${c.incNew} from includelist)` : '',
1683 n = new Notification('HITScraper found ' + c.newVis + ' new HITs' + inc);
1684 n.onclick = n.close;
1685 setTimeout(n.close.bind(n), 5000);
1686 }
1687 }//}}} Core::notify
1688 },//}}} Core
1689
1690 Exporter = function(e) {//{{{
1691 Interface.toggleOverflow('on');
1692 this.caller = e.target;
1693 this.node = document.body.appendChild(document.createElement('DIV'));
1694 this.node.classList.add('pop');
1695 this.die = () => {
1696 Interface.toggleOverflow('off');
1697 this.node.remove();
1698 };
1699 this.record = scraperHistory.get(this.caller.dataset.gid);
1700
1701 if (Interface.isLoggedout) return this.die();
1702
1703 var _vb = () => {//{{{
1704 var
1705 getColor = attr => {
1706 switch (attr) {
1707 case 5:
1708 case 4:
1709 return 'green';
1710 case 3:
1711 return 'yellow';
1712 case 2:
1713 return 'orange';
1714 case 1:
1715 return 'red';
1716 default:
1717 return 'white';
1718 }
1719 },
1720 templateVars = {//{{{
1721 title : this.record.title,
1722 requesterName: this.record.requester.name,
1723 requesterLink: this.record.requester.link,
1724 requesterId : this.record.requester.id,
1725 description : this.record.desc,
1726 reward : this.record.pay,
1727 quals : this.record.quals.join(';').replace(/(;?)(\w* ?Masters.+?)(;?)/g, '$1[COLOR=red][b]$2[/b][/COLOR]$3'),
1728 previewLink : this.record.hit.preview,
1729 pandaLink : this.record.hit.panda,
1730 time : this.record.time,
1731 numHits : this.record.numHits,
1732 toImg : '', // deprecated
1733 toCompact : (function() {//{{{
1734 var _to = this.record.TO, txt = ['[b]'], color;
1735 if (!_to) return 'TO Unavailable';
1736 for (var a of ['comm', 'pay', 'fair', 'fast']) {
1737 color = getColor(Math.floor(_to.attrs[a]));
1738 txt.push(`[ ${a}: [COLOR=${color}]${_to.attrs[a]}[/COLOR] ]`);
1739 }
1740 return txt.join('') + '[/b]';
1741 }).apply(this),//}}} toCompact
1742 toVerbose : (function() {//{{{
1743 var _to = this.record.TO, txt = [], color, _attr, sym = Settings.user.vbSym,
1744 _long = { comm: 'Communicativity', pay: 'Generosity', fair: 'Fairness', fast: 'Promptness' };
1745 if (!_to) return 'TO Unavailable';
1746 for (var a of ['comm', 'pay', 'fair', 'fast']) {
1747 _attr = Math.floor(_to.attrs[a]);
1748 color = getColor(_attr);
1749 txt.push((_attr > 0 ? (`[COLOR=${color}]${sym.repeat(_attr)}[/COLOR]` + (_attr < 5
1750 ? `[COLOR=white]${sym.repeat(5 - _attr)}[/COLOR]`
1751 : ''))
1752 : '[COLOR=white]' + sym.repeat(5) + '[/COLOR]') + ` ${_to.attrs[a]} ${_long[a]}`);
1753 }
1754 return txt.join('\n');
1755 }).apply(this),//}}} toText
1756 toFoot : (function() {//{{{
1757 var _to = this.record.TO,
1758 payload = `requester[amzn_id]=${this.record.requester.id}&requester[amzn_name]=${this.record.requester.name}`,
1759 newReview = `[URL="${TO_BASE + 'report?' + payload}"]Submit a new TO review[/URL]`;
1760 if (!_to) return newReview;
1761 return `Number of Reviews: ${_to.reviews} | TOS Flags: ${_to.tos_flags}\n` + newReview;
1762 }).apply(this)//}}} toFoot
1763 },//}}} templateVars obj
1764 createTemplate = function(str) {
1765 /*jshint -W054*/ // ignore evil due to required eval (function constructor)
1766 // TODO: find a concise way to dynamically generate a template without using eval
1767 var _str = str.replace(/\$\{ *([-\w\d.]+) *\}/g, (_, p1) => `\$\{vars.${p1}\}`);
1768 return new Function('vars', `try {return \`${_str}\`} catch(e) {return "Error in template: "+e.message}`);
1769 };
1770 templateVars.toText = templateVars.toVerbose; // temporary backwards compatibility
1771 this.node.innerHTML = '<p>vB Export</p>' +
1772 '<textarea style="display:block;padding:2px;margin:auto;height:250px;width:500px" tabindex="1">' +
1773 createTemplate(Settings.user.vbTemplate)(templateVars) + '</textarea>' +
1774 '<button id="exTemplate" style="margin-top:5px;width:50%;color:white;background:black">Edit Template</button>' +
1775 '<button id="exClose" style="margin-top:5px;width:50%;color:white;background:black">Close</button>';
1776 this.node.querySelector('#exTemplate').onclick = () => {
1777 this.die();
1778 new Editor('vbTemplate', this.caller);
1779 };
1780 this.node.querySelector('#exClose').onclick = this.die;
1781 this.node.querySelector('textarea').select();
1782 },//}}}
1783 _irc = () => {//{{{
1784 // custom MTurk/TO url shortener courtesy of Tjololo
1785 var api = 'https://ns4t.net/yourls-api.php?action=bulkshortener&title=MTurk&signature=39f6cf4959',
1786 urlArr = [], payload, sym = '\u2022', // sym = bullet
1787 getTO = () => {
1788 var _to = this.record.TO;
1789 if (!_to) return 'Unavailable';
1790 else return `Pay=${_to.attrs.pay} Fair=${_to.attrs.fair} Comm=${_to.attrs.comm}`;
1791 };
1792
1793 urlArr.push(window.encodeURIComponent(this.record.requester.link));
1794 urlArr.push(window.encodeURIComponent(this.record.hit.preview));
1795 urlArr.push(window.encodeURIComponent(TO_REPORTS + this.record.requester.id));
1796 urlArr.push(window.encodeURIComponent(this.record.hit.panda));
1797 payload = '&urls[]=' + urlArr.join('&urls[]=');
1798
1799 this.node.innerHTML = '<span style="font-size:16px">Shortening URLs... <i class="spinner"></i></span>';
1800 Core.fetch(api + payload, null, 'text', false).then(r => {
1801 urlArr = r.split(';').slice(0, 4);
1802 this.node.innerHTML = '<p>IRC Export</p>' +
1803 '<textarea style="display:block;padding:2px;margin:auto;height:130px;width:500px" tabindex="1">' +
1804 (/masters/i.test(this.record.quals.join()) ? `MASTERS ${sym} ` : '') +
1805 `Requester: ${this.record.requester.name} ${urlArr[0]} ${sym} HIT: ${this.record.title} ` +
1806 `${urlArr[1]} ${sym} Pay: ${this.record.pay} ${sym} Avail: ${this.record.numHits} ${sym} ` +
1807 `Limit: ${this.record.time} ${sym} TO: ${getTO()} ${urlArr[2]} ${sym} PandA: ${urlArr[3]}</textarea>` +
1808 '<button id="exClose" style="width:100%;padding:5px;margin-top:5px;background:black;color:white">Close</button>';
1809 this.node.querySelector('textarea').select();
1810 this.node.querySelector('#exClose').onclick = this.die;
1811 }, err => {
1812 console.error(err);
1813 this.die();
1814 });
1815 },//}}}
1816 _hwtf = () => {//{{{
1817 var _location = 'ICA', _quals, _masters = '', _title, _r = this.record, tIndex;
1818 // format qualifications string
1819 _quals = _r.quals.map(v => {
1820 if (/(is US|: US$)/.test(v))
1821 _location = 'US';
1822 else if (/Masters/.test(v))
1823 _masters = `[${v.match(/.*Masters/)[0].toUpperCase()}]`;
1824 else if (/approv[aled]+ (rate|HITs)/.test(v))
1825 return v.replace(/.+ is (.+) than (\d+)/, (_, p1, p2) => {
1826 if (/^(not g|less)/.test(p1)) return '<' + p2 + (/%/.test(_) ? '%' : '');
1827 else if (/^(not l|greater)/.test(p1)) return '>' + p2 + (/%/.test(_) ? '%' : '');
1828 else console.error('match error', [_, p1, p2]);
1829 return _;
1830 });
1831 else
1832 return v;
1833 }).filter(v => v).sort(a => /[><]/.test(a) ? -1 : 1);
1834 _title = `${_location} - ${_r.title} - ${_r.requester.name} - ${_r.pay}/COMTIME - (${_quals.join(', ') || 'None'}) ${_masters}`;
1835 tIndex = _title.search(/COMTIME/);
1836 this.node.style.whiteSpace = 'nowrap';
1837 this.node.innerHTML = '<p style="width:500px;white-space:normal">' +
1838 '/r/HitsWorthTurkingFor Export: Use the buttons on the left for single-click copying. ' +
1839 'Before you post, please remember to replace "COMTIME" with how long it took you to complete the HIT.</p>' +
1840 '<button class="exhwtf" style="height:65px">Title</button>' +
1841 '<textarea style="padding:2px;margin:auto;height:60px;width:430px;resize:none" tabindex="1" autofocus>' +
1842 _title + '</textarea><br />' + '<button class="exhwtf" style="height:35px">Preview</button>' +
1843 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="2">' +
1844 'Preview: ' + _r.hit.preview + '</textarea><br />' + '<button class="exhwtf" style="height:35px;">Req</button>' +
1845 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="3">' +
1846 'Req: ' + _r.requester.link + '</textarea><br />' + '<button class="exhwtf" style="height:35px;">PandA</button>' +
1847 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="4">' +
1848 'PandA: ' + _r.hit.panda + '</textarea><br />' + '<button class="exhwtf" style="height:35px;">TO</button>' +
1849 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="5">' +
1850 'TO: ' + TO_REPORTS + _r.requester.id + '</textarea><br />' +
1851 '<button id="exClose" style="width:100%;padding:5px;margin-top:5px;background:black;color:white">Close</button>';
1852
1853 var copyfn = function(e) {
1854 e.target.nextSibling.select();
1855 document.execCommand('copy');
1856 };
1857 Array.from(this.node.querySelectorAll('.exhwtf')).forEach(v => v.onclick = copyfn);
1858 this.node.querySelector('#exClose').onclick = this.die;
1859 this.node.querySelector('textarea').setSelectionRange(tIndex, tIndex + 7);
1860 };//}}}
1861
1862 switch (this.caller.textContent.toLowerCase()) {
1863 case 'vb':
1864 _vb();
1865 break;
1866 case 'irc':
1867 _irc();
1868 break;
1869 case 'hwtf':
1870 _hwtf();
1871 break;
1872 }
1873 },//}}} Exporter
1874
1875 HITStorage = {//{{{
1876 db : null,
1877 attach: function(name) {//{{{
1878 var dbh = window.indexedDB.open(name);
1879 dbh.onversionchange = e => {
1880 e.target.result.close();
1881 console.info('DB connection closed by external source');
1882 };
1883 dbh.onsuccess = e => this.db = e.target.result;
1884 },//}}} HITStorage::attach
1885 test : function(node) {//{{{
1886 if (!this.db || !this.db.objectStoreNames.contains('HIT')) return;
1887 this.db.transaction('HIT', 'readonly').objectStore('HIT').index(node.dataset.index).get(node.dataset.value)
1888 .onsuccess = e => { if (e.target.result) node.className = node.className.replace(/no/, ''); };
1889 },//}}} HITStorage::test
1890 query : function(node) {//{{{
1891 var range = window.IDBKeyRange.only(node.dataset.value), results = [];
1892 return new Promise((a, r) => {
1893 if (!this.db || !this.db.objectStoreNames.contains('HIT')) r(0);
1894 this.db.transaction('HIT', 'readonly').objectStore('HIT').index(node.dataset.index).openCursor(range)
1895 .onsuccess = e => {
1896 if (e.target.result) {
1897 results.push(e.target.result.value);
1898 e.target.result.continue();
1899 } else
1900 a(results.sort((a, b) => a.date > b.date ? 1 : -1));
1901 };
1902 });
1903 }//}}} HITStorage::query
1904 },//}}} HITStorage
1905
1906 FileHandler = {//{{{
1907 exports: function() {//{{{
1908 var obj = {
1909 settings : JSON.stringify(Settings.user),
1910 ignore_list : localStorage.getItem('scraper_ignore_list') || '',
1911 include_list: localStorage.getItem('scraper_include_list') || ''
1912 },
1913 blob = new Blob([JSON.stringify(obj)], { type: 'application/json' }),
1914 a = document.body.appendChild(document.createElement('a'));
1915 a.href = URL.createObjectURL(blob);
1916 a.download = 'hitscraper_settings.json';
1917 a.click();
1918 a.remove();
1919 },//}}}
1920 imports: function(e) {//{{{
1921 var f = e.target.files,
1922 invalid = () => Settings.main.querySelector('#eisStatus').textContent = 'Invalid file.';
1923 if (!f.length) return;
1924 if (!f[0].name.includes('json')) return invalid();
1925 var reader = new FileReader();
1926 reader.readAsText(f[0]);
1927 reader.onload = function() {
1928 var obj;
1929 try { obj = JSON.parse(this.result); } catch(err) { return invalid(); }
1930 for (var key of ['settings', 'ignore_list', 'include_list']) {
1931 if (key in obj && typeof obj[key] === 'string')
1932 localStorage.setItem('scraper_' + key, obj[key]);
1933 }
1934 initialize();
1935 };
1936 }//}}}
1937 };//}}} FileHandler
1938
1939 function initialize() {//{{{
1940 Settings.user = Object.assign({}, Settings.defaults, JSON.parse(localStorage.getItem('scraper_settings')));
1941 Interface.draw().init();
1942 scraperHistory = new ScraperCache(650);
1943 }//}}}
1944
1945 function createTooltip(type, obj) {//{{{
1946 var html, bullet = li => `<ul><li>${li}</li></ul>`,
1947 reason = Settings.user.disableTO ? bullet('TO disabled in user settings')
1948 : (Interface.isLoggedout ? bullet('Cannot retrieve TO while logged out')
1949 : (obj === '' ? bullet('Requester has not been reviewed yet') : bullet('Invalid response from server'))),
1950 _genMeters = function() {
1951 var attrmap = { comm: 'Communicativity', pay: 'Generosity', fair: 'Fairness', fast: 'Promptness' }, html = [];
1952 for (var k in attrmap) {
1953 if (attrmap.hasOwnProperty(k)) {
1954 html.push(`<meter min="0.8" low="2.5" high="3.4" optimum="5" max="5" value=${obj.attrs[k]} data-attr=${attrmap[k]}></meter>`);
1955 }
1956 }
1957 if (ENV.ISFF) // firefox is shitty and doesn't support ::after/::before pseudo-elements on meter elements
1958 html.forEach((v, i, a) => a[i] = '<div style="position:relative">' + v +
1959 `<span class="ffmb">${attrmap[Object.keys(attrmap)[i]]}</span>` +
1960 `<span class="ffma">${obj.attrs[Object.keys(attrmap)[i]]}</span></div>`);
1961 return html.join('');
1962 };
1963
1964 if (!obj) {
1965 html = `<div class="tooltip" style="width:260px;"><p style="padding-left:5px">Turkopticon data unavailable:${reason}</p></div>`;
1966 } else if (type === 'to')
1967 html = `<div class="tooltip" style="width:260px">
1968<p style="padding-left:5px"><b>${obj.name}</b><br />Reviews: ${obj.reviews} | TOS Flags: ${obj.tos_flags}</p>
1969${_genMeters()}</div>`;
1970 /*<table style="margin-top:6px;width:100%;font-size:10px"><tr><td>Adjusted Pay</td><td>${obj.attrs.adjPay}</td>
1971 <td>${getClassFromValue(obj.attrs.adjPay, 'adj').slice(2)}</td></tr><tr><td>Weighted Score</td><td>${obj.attrs.qual}</td>
1972 <td>${getClassFromValue(obj.attrs.qual, 'sim').slice(2)}</td></tr><tr><td>Adjusted Score</td><td>${obj.attrs.adjQual}</td>
1973 <td>${getClassFromValue(obj.attrs.adjQual, 'adj').slice(2)}</td></tr></table></div>;*/
1974 else // XXX not used atm
1975 html = `<div class="tooltip" style="width:300px"><dl><dt>description</dt><dd>${obj.desc}</dd>
1976<dt>qualifications</dt><dd>${obj.quals}</dd></dl>`;
1977
1978 return html;
1979 }//}}}
1980
1981 function prepReviews(reviews) {
1982 const adj = (x, n) => ((x * n + 15) / (n + 5)) - 1.645 * Math.sqrt((Math.pow(1.0693 * x, 2) - Math.pow(x, 2)) / (n + 5));
1983 Object.keys(reviews).forEach(rid => {
1984 if (typeof reviews[rid] === 'string') return delete reviews[rid]; // no reviews yet
1985
1986 //adjust ratings
1987 let n = 0, d = 0;
1988 Object.keys(reviews[rid].attrs).forEach(attr => {
1989 n += reviews[rid].attrs[attr] * Settings.user.toWeights[attr];
1990 d += +Settings.user.toWeights[attr];
1991 });
1992 reviews[rid].attrs.qual = (n / d).toPrecision(4);
1993 reviews[rid].attrs.adjQual = adj(n / d, +reviews[rid].reviews).toPrecision(4);
1994 reviews[rid].attrs.adjPay = adj(+reviews[rid].attrs.pay, +reviews[rid].reviews).toPrecision(4);
1995 });
1996 return reviews;
1997 }
1998
1999 class Cache {
2000 constructor(limit = 500) {
2001 this.limit = limit;
2002 this._length = 0;
2003 this._cache = Object.create(null);
2004 this._tmp = Object.create(null);
2005 }
2006
2007 get(key) {
2008 let val = this._cache[key];
2009 if (val)
2010 return val;
2011 else if ((val = this._tmp[key]))
2012 return this._update(key, val);
2013 else
2014 return null;
2015 }
2016
2017 set(key, value) {
2018 if (this._cache[key])
2019 return (this._cache[key] = value);
2020 else
2021 this._update(key, value);
2022 }
2023
2024 has(key) {
2025 return !!this.get(key);
2026 }
2027
2028 _update(key, value) {
2029 this._cache[key] = value;
2030 if (++this._length > this.limit) {
2031 this._length = 0;
2032 this._tmp = this._cache;
2033 this._cache = Object.create(null);
2034 }
2035 return value;
2036 }
2037 }
2038
2039 class ScraperCache extends Cache {
2040
2041 constructor(limit = 500) {
2042 super(limit);
2043 this._toCache = new TOCache();
2044 }
2045
2046 set(key, value) {
2047 const first = !Core.lastScrape;
2048 if (this.get(key)) { // exists
2049 const age = Math.floor((Date.now() - this._cache[key].discovery) / 1000),
2050 obj = { isNew: false, shine: !!(this._cache[key].shine && age < +Settings.user.shine && !first) };
2051 value.discovery = this._cache[key].discovery;
2052 return (this._cache[key] = Object.assign(value, obj));
2053 } else { // new
2054 const obj = { isNew: !first, shine: !first, TO: this._toCache.get(value.requester.id) };
2055 this._update(key, Object.assign(value, obj));
2056 }
2057 }
2058
2059 filter(callback, rids = false) {
2060 const results = [], keys = Object.keys(this._cache);
2061 Object.keys(this._cache).forEach(key => {
2062 const val = this.get(key);
2063 if (callback(val, key, this._cache))
2064 results.push(rids ? val.requester.id : val);
2065 });
2066 return results;
2067 }
2068
2069 updateTOData(reviews) {
2070 runCtr++;
2071 this._toCache.setBatch(reviews);
2072
2073 this.filter(v => v.current && v.TO === null).forEach(group => {
2074 if (this._toCache.has(group.requester.id))
2075 this._cache[group.groupId].TO = Object.assign(this._toCache.get(group.requester.id), { name: group.requester.name });
2076
2077 if (!this._cache[group.groupId].blocked && !(this._cache[group.groupId].TO === null)) {
2078
2079 if (this._cache[group.groupId].TO.attrs.pay >= 3) {
2080 console.log(runCtr);
2081 var a = window.open(this._cache[group.groupId].hit.panda);
2082 a.blur();
2083 a.close();
2084 }
2085 }
2086 });
2087 }
2088 }
2089
2090 class TOCache extends Cache {
2091 setBatch(reviews) {
2092 if (!reviews) return null;
2093 Object.keys(reviews).forEach(rid => this._update(rid, reviews[rid]));
2094 return reviews;
2095 }
2096 }
2097
2098 const kb = { ESC: 27, ENTER: 13 };
2099
2100 function Dialogue(caller) {//{{{
2101 Interface.toggleOverflow('on');
2102 this.node = document.body.appendChild(document.createElement('DIV'));
2103 this.die = () => {
2104 Interface.toggleOverflow('off');
2105 this.node.remove();
2106 };
2107 this.node.style.cssText = 'position:fixed;z-index:20;top:15%;left:50%;width:320px;padding:20px;transform:translate(-50%);' +
2108 'background:#000;color:#fff;box-shadow:0px 0px 6px 1px #fff';
2109 var target = caller.textContent === 'R' ? 'requester' : 'title';
2110 this.node.innerHTML = `<p><b>Add this ${target} to the blocklist?</b></p><p>"${caller.value}"</p>
2111<div style="text-align:right;margin-right:30px;margin-top:10px;padding-top:10px">
2112<button id="confirm" style="font-weight:bold;padding:7px;width:65px">OK</button>
2113<button id="cancel" style="padding:7px;width:65px;">Cancel</button></div>`;
2114 this.node.querySelector('#confirm').onclick = () => {
2115 var bl = localStorage.getItem('scraper_ignore_list'), bstr = caller.value.toLowerCase().replace(/\s+/g, ' ');
2116 if (!bl) bl = bstr;
2117 else if (bl.slice(-1) === '^') bl += bstr;
2118 else bl += '^' + bstr;
2119 localStorage.setItem('scraper_ignore_list', bl);
2120
2121 Array.prototype.forEach.call(document.getElementById('resultsTable').tBodies[0].rows, v => {
2122 var c0 = v.cells[0].lastChild.textContent, c1 = v.cells[1].lastChild.textContent.trim();
2123 if (v.classList.contains('blocklisted') || c0 !== caller.value && c1 !== caller.value) return;
2124 v.cells[0].firstChild.remove();
2125 return v.classList.add('blocklisted') || Settings.user.hideBlock && v.classList.add('hidden');
2126 });
2127 this.die();
2128 };
2129 this.node.querySelector('#cancel').onclick = this.die;
2130 this.node.addEventListener('keydown', e => {
2131 if (e.keyCode === kb.ESC)
2132 this.die();
2133 }, true);
2134 this.node.querySelector('#confirm').focus();
2135 }//}}}
2136
2137 function DBQuery(node) {//{{{
2138 Interface.toggleOverflow('on');
2139 this.node = document.body.appendChild(document.createElement('DIV'));
2140 this.die = () => {
2141 this.node.remove();
2142 Interface.toggleOverflow('off');
2143 };
2144 this.node.style.cssText = 'position:fixed;z-index:20;top:50%;left:50%;padding:8px;' +
2145 'background:#fff;color:#000;box-shadow:0px 0px 6px 1px #bfbfbf;transform:translate(-50%,-50%);';
2146 this.node.innerHTML = '<div style="text-align:center;font-size:16px;"><p><b>Querying database... <i class="spinner"></i></b></p></div>';
2147 HITStorage.query(node).then(r => {
2148 var _tbody = [], _tfoot, t = { hits: 0, app: 0, rej: 0, pen: 0 },
2149 _thead = '<tr style="background:lightgrey;color:black"><th style="width:90px;padding:5px">Date</th>' +
2150 '<th style="width:120px">Requester</th><th>Title</th><th>Pay</th><th>Bonus</th><th>Status</th><th>Feedback</th></tr>',
2151 html = '<div style="position:absolute;top:0;left:0;margin:0;text-align:right;padding:0px;border:none;width:100%">' +
2152 '<label id="close" class="close" title="Close"> ✘ </label></div>';
2153 if (!r.length)
2154 html += `<h2>Nothing found matching "${node.dataset.value}"</h2>`;
2155 else {
2156 r.forEach((v, i) => {
2157 var _pay, _bonus, _sc, _bg;
2158 if (typeof v.reward === 'object') {
2159 _pay = '$' + v.reward.pay.toFixed(2);
2160 _bonus = v.reward.bonus > 0 ? '$' + v.reward.bonus.toFixed(2) : '';
2161 } else {
2162 _pay = '$' + v.reward.toFixed(2);
2163 _bonus = '';
2164 }
2165
2166 _sc = /(paid|approved)/i.test(v.status) ? 'green' : (/approval/i.test(v.status) ? 'orange' : 'red');
2167 _bg = v[node.dataset.cmpIndex] === node.dataset.cmpValue ? 'lightgreen' : (i % 2 ? '#F1F3EB' : '#fff');
2168 _tbody.push(`<tr style="background:${_bg}">
2169<td>${v.date}</td><td>${v.requesterName}</td><td>${v.title}</td><td>${_pay}</td><td>${_bonus}</td>
2170<td style="color:${_sc}">${v.status}</td><td>${v.feedback}</td></tr>`);
2171 t.hits++;
2172 t.app += /(paid|approved)/i.test(v.status) ? +_pay.slice(1) : 0;
2173 t.rej += /rejected/i.test(v.status) ? +_pay.slice(1) : 0;
2174 t.pen += /approval/i.test(v.status) ? +_pay.slice(1) : 0;
2175 });
2176 _tfoot = `<tr style="background:lightgrey;text-align:center"><td colspan="7">${t.hits} HITs: $${t.app.toFixed(2)} approved,
2177$${t.pen.toFixed(2)} pending, $${t.rej.toFixed(2)} rejected</td>`;
2178 html += `<div style="margin-top:20px;width:100%;height:calc(100% - 20px);overflow:auto">
2179<table style="border:1px solid black;border-collapse:collapse;width:100%">
2180<thead>${_thead}</thead><tbody>${_tbody.join('')}</tbody><tfoot>${_tfoot}</tfoot></table></div>`;
2181 }
2182 this.node.style.cssText += `width:85%;${r.length ? 'height:85%;' : 'max-height:85%;'}`;
2183 this.node.innerHTML = html;
2184 this.node.querySelector('#close').onclick = this.die;
2185 }, () => this.die());
2186 }//}}}
2187
2188 // helpers
2189 function on(target, type, handler) { target.addEventListener(type, handler); }
2190
2191 function delegate(target, selector, type, handler) {
2192 function dispatcher(event) {
2193 const targets = target.querySelectorAll(selector);
2194 let i = targets.length;
2195
2196 while (i--) {
2197 if (event.target === targets[i]) {
2198 handler(event);
2199 break;
2200 }
2201 }
2202 }
2203
2204 on(target, type, dispatcher);
2205 }
2206
2207 Object.entries = Object.entries || function(obj) {
2208 const props = Object.keys(obj);
2209 let i = props.length;
2210 const objArray = new Array(i);
2211 while (i--) objArray[i] = [props[i], obj[props[i]]];
2212 return objArray;
2213 };
2214
2215 // event handlers
2216 function tomouseover(e) {
2217 e.target.children[0].style.display = 'block';
2218 const tt = e.target.children[0], rect = tt.getBoundingClientRect();
2219 if (rect.height > (window.innerHeight - e.clientY)) tt.style.transform = 'translateY(calc(-100% + 22px))';
2220 }
2221
2222 function tomouseout(e) {
2223 const tt = e.target.querySelector('.tooltip');
2224 if (!tt) return;
2225 tt.style.transform = '';
2226 tt.style.display = 'none';
2227 }
2228
2229 // ep
2230 console.log('HS hook');
2231 if (document.getElementById('control_panel')) {
2232 if (confirm('Another version of HITScraper was detected and has already claimed this page. Open HITScraper in a new tab?'))
2233 window.open('https://www.mturk.com/mturk/findhits?match=true?hit_scraper-dev');
2234 } else {
2235 initialize();
2236 HITStorage.attach('HITDB');
2237 const rt = document.getElementById('resultsTable');
2238 delegate(rt, 'tr:not(hidden) .toLink', 'mouseover', tomouseover);
2239 delegate(rt, 'tr:not(hidden) .toLink', 'mouseout', tomouseout);
2240 delegate(rt, 'tr:not(hidden) .ex', 'click', e => new Exporter(e));
2241 delegate(rt, 'tr:not(hidden) button[name=block]', 'click', ({ target }) => new Dialogue(target));
2242 delegate(rt, 'tr:not(hidden) .db', 'click', ({ target }) => new DBQuery(target));
2243 }
2244
2245})();
2246
2247// vim: ts=2:sw=2:et:fdm=marker:noai